Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
297 changes: 297 additions & 0 deletions mage/args_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,303 @@ not coughing
}
}

func TestVariadicArgs(t *testing.T) {
for _, tc := range []struct {
name string
args []string
want string
}{
{
name: "empty",
args: []string{"variadic"},
want: "variadic:[]\n",
},
{
name: "all remaining tokens",
args: []string{"variadic", "first", "-flag", "variadic", "--"},
want: "variadic:[\"first\" \"-flag\" \"variadic\" \"--\"]\n",
},
} {
t.Run(tc.name, func(t *testing.T) {
stderr := &bytes.Buffer{}
stdout := &bytes.Buffer{}
inv := Invocation{
Dir: "./testdata/variadic",
Stderr: stderr,
Stdout: stdout,
Args: tc.args,
}
code := Invoke(inv)
if code != 0 {
t.Fatalf("expected code 0, got %d; stderr: %s", code, stderr)
}
if got := stdout.String(); got != tc.want {
t.Fatalf("expected output %q, got %q", tc.want, got)
}
})
}
}

// TestOptionalVariadicArgs verifies implicit and explicit pass-through boundaries
// across empty, fixed-prefix, and typed optional-argument target signatures.
func TestOptionalVariadicArgs(t *testing.T) {
for _, tc := range []struct {
name string
args []string
want string
}{
{
name: "implicit tail preserves later option-like tokens",
args: []string{
"optionalandvariadic", "-prefix=chosen",
"first", "-unknown=value", "--", "fixed",
},
want: "optional:chosen:[\"first\" \"-unknown=value\" \"--\" \"fixed\"]\n",
},
{
name: "empty options and tail",
args: []string{"optionalandvariadic"},
want: "optional:<nil>:[]\n",
},
{
name: "required prefix and typed options",
args: []string{
"optionaltypes", "required",
"-text=value", "-count=2", "-ratio=1.5", "-enabled", "-timeout=25ms",
"first", "-count=99",
},
want: "optionaltypes:required:value:2:1.5:true:25ms:[\"first\" \"-count=99\"]\n",
},
{
name: "double dash starts an option-like tail",
args: []string{
"optionalandvariadic", "-prefix=chosen", "--",
"-unknown=value", "--", "fixed",
},
want: "optional:chosen:[\"-unknown=value\" \"--\" \"fixed\"]\n",
},
} {
t.Run(tc.name, func(t *testing.T) {
stderr := &bytes.Buffer{}
stdout := &bytes.Buffer{}
code := Invoke(Invocation{
Dir: "./testdata/variadic",
Stderr: stderr,
Stdout: stdout,
Args: tc.args,
})
if code != 0 {
t.Fatalf("expected code 0, got %d; stderr: %s", code, stderr)
}
if got := stdout.String(); got != tc.want {
t.Fatalf("expected output %q, got %q", tc.want, got)
}
})
}
}

// TestOptionalVariadicArgsRejectInvalidOptionsBeforeTail verifies that option
// validation remains active until a variadic pass-through boundary is reached.
func TestOptionalVariadicArgsRejectInvalidOptionsBeforeTail(t *testing.T) {
for _, tc := range []struct {
name string
arg string
want string
}{
{
name: "unknown",
arg: "-unknown=value",
want: "unknown option \"unknown\" for target \"OptionalAndVariadic\"\n",
},
{
name: "missing equals",
arg: "-prefix",
want: "invalid option \"-prefix\" for target \"OptionalAndVariadic\", expected -name=value format\n",
},
} {
t.Run(tc.name, func(t *testing.T) {
stderr := &bytes.Buffer{}
code := Invoke(Invocation{
Dir: "./testdata/variadic",
Stderr: stderr,
Stdout: &bytes.Buffer{},
Args: []string{"optionalandvariadic", tc.arg},
})
if code != 2 {
t.Fatalf("expected code 2, got %d; stderr: %s", code, stderr)
}
if got := stderr.String(); got != tc.want {
t.Fatalf("expected error %q, got %q", tc.want, got)
}
})
}
}

// TestDoubleDashRemainsInvalidForOptionalNonVariadicTarget verifies that the
// target-level separator does not change optional-only target semantics.
func TestDoubleDashRemainsInvalidForOptionalNonVariadicTarget(t *testing.T) {
stderr := &bytes.Buffer{}
code := Invoke(Invocation{
Dir: "./testdata/optargs",
Stderr: stderr,
Stdout: &bytes.Buffer{},
Args: []string{"greet", "World", "--"},
})
if code != 2 {
t.Fatalf("expected code 2, got %d; stderr: %s", code, stderr)
}
want := "invalid option \"--\" for target \"Greet\", expected -name=value format\n"
if got := stderr.String(); got != want {
t.Fatalf("expected error %q, got %q", want, got)
}
}

// TestOptionalVariadicArgsHelp verifies source-parsed help for optional flags
// followed by the local variadic pass-through boundary.
func TestOptionalVariadicArgsHelp(t *testing.T) {
stderr := &bytes.Buffer{}
stdout := &bytes.Buffer{}
code := Invoke(Invocation{
Dir: "./testdata/variadic",
Stderr: stderr,
Stdout: stdout,
Help: true,
Args: []string{"optionalandvariadic"},
})
if code != 0 {
t.Fatalf("expected code 0, got %d; stderr: %s", code, stderr)
}
want := `OptionalAndVariadic prints an optional prefix and pass-through arguments.

Usage:

mage optionalandvariadic [-prefix=<string>] [--] [<args>...]

Flags:

-prefix=<string>

Pass-through:

The first non-option token starts <args>. To start with a "-" token, use --; the separator is omitted and all following tokens are passed unchanged.

`
if got := stdout.String(); got != want {
t.Fatalf("expected output %q, got %q", want, got)
}
}

func TestVariadicArgsHelp(t *testing.T) {
stderr := &bytes.Buffer{}
stdout := &bytes.Buffer{}
inv := Invocation{
Dir: "./testdata/variadic",
Stderr: stderr,
Stdout: stdout,
Help: true,
Args: []string{"variadic"},
}
code := Invoke(inv)
if code != 0 {
t.Fatalf("expected code 0, got %d; stderr: %s", code, stderr)
}
want := `Variadic prints all remaining arguments.

Usage:

mage variadic [<args>...]

Aliases: v

`
if got := stdout.String(); got != want {
t.Fatalf("expected output %q, got %q", want, got)
}
}

func TestVariadicDefaultTarget(t *testing.T) {
stderr := &bytes.Buffer{}
stdout := &bytes.Buffer{}
inv := Invocation{
Dir: "./testdata/variadic",
Stderr: stderr,
Stdout: stdout,
}
code := Invoke(inv)
if code != 0 {
t.Fatalf("expected code 0, got %d; stderr: %s", code, stderr)
}
if got, want := stdout.String(), "variadic:[]\n"; got != want {
t.Fatalf("expected output %q, got %q", want, got)
}
}

func TestVariadicArgsWithFixedPrefixAndPreviousTarget(t *testing.T) {
stderr := &bytes.Buffer{}
stdout := &bytes.Buffer{}
inv := Invocation{
Dir: "./testdata/variadic",
Stderr: stderr,
Stdout: stdout,
Args: []string{"fixed", "before", "collect", "prefix", "-flag", "fixed", "after"},
}
code := Invoke(inv)
if code != 0 {
t.Fatalf("expected code 0, got %d; stderr: %s", code, stderr)
}
want := "fixed:before\ncollect:prefix:[\"-flag\" \"fixed\" \"after\"]\n"
if got := stdout.String(); got != want {
t.Fatalf("expected output %q, got %q", want, got)
}
}

func TestVariadicTargetForms(t *testing.T) {
for _, tc := range []struct {
name string
args []string
want string
}{
{
name: "alias",
args: []string{"v", "one", "two"},
want: "variadic:[\"one\" \"two\"]\n",
},
{
name: "namespace",
args: []string{"tools:collect", "prefix", "one", "two"},
want: "tools:collect:prefix:[\"one\" \"two\"]\n",
},
{
name: "imported",
args: []string{"shared:collect", "prefix", "one", "two"},
want: "shared:collect:prefix:[\"one\" \"two\"]\n",
},
{
name: "fixed argument types",
args: []string{"types", "3", "1.5", "true", "25ms", "one", "two"},
want: "types:3:1.5:true:25ms:[\"one\" \"two\"]\n",
},
} {
t.Run(tc.name, func(t *testing.T) {
stderr := &bytes.Buffer{}
stdout := &bytes.Buffer{}
inv := Invocation{
Dir: "./testdata/variadic",
Stderr: stderr,
Stdout: stdout,
Args: tc.args,
}
code := Invoke(inv)
if code != 0 {
t.Fatalf("expected code 0, got %d; stderr: %s", code, stderr)
}
if got := stdout.String(); got != tc.want {
t.Fatalf("expected output %q, got %q", tc.want, got)
}
})
}
}

func TestBadIntArg(t *testing.T) {
stderr := &bytes.Buffer{}
stdout := &bytes.Buffer{}
Expand Down
13 changes: 1 addition & 12 deletions mage/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -633,18 +633,7 @@ func mageHelpOutput(data mainfileTemplateData, target string) (output string, co
}

// Build usage line matching template format.
_, _ = fmt.Fprintf(&buf, "Usage:\n\n\t%s %s", data.BinaryName, strings.ToLower(fn.TargetName()))
for _, a := range fn.RequiredArgs() {
_, _ = fmt.Fprintf(&buf, " <%s>", a.Name)
}
if fn.MultipleOptionalArgs() {
_, _ = fmt.Fprint(&buf, " [<flags>]")
} else {
for _, a := range fn.OptionalArgs() {
_, _ = fmt.Fprintf(&buf, " [-%s=<%s>]", a.Name, a.Type)
}
}
_, _ = fmt.Fprint(&buf, "\n\n")
_, _ = fmt.Fprintf(&buf, "Usage:\n\n\t%s %s%s\n\n", data.BinaryName, strings.ToLower(fn.TargetName()), fn.UsageArgs())

if fn.ShowFlagDocs() {
_, _ = fmt.Fprint(&buf, fn.FlagDocsString())
Expand Down
7 changes: 5 additions & 2 deletions mage/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ Options:
_fmt.Println({{printf "%q" .Comment}})
_fmt.Println()
{{end}}
_fmt.Print("Usage:\n\n\t{{$.BinaryName}} {{lower .TargetName}}{{range .RequiredArgs}} <{{.Name}}>{{end}}{{if .MultipleOptionalArgs}} [<flags>]{{else}}{{range .OptionalArgs}} [-{{.Name}}=<{{.Type}}>]{{end}}{{end}}\n\n")
_fmt.Print("Usage:\n\n\t{{$.BinaryName}} {{lower .TargetName}}{{.UsageArgs}}\n\n")
{{if .ShowFlagDocs}}_fmt.Print({{printf "%q" .FlagDocsString}})
{{end -}}
var aliases []string
Expand All @@ -269,7 +269,7 @@ Options:
_fmt.Println({{printf "%q" .Comment}})
_fmt.Println()
{{end}}
_fmt.Print("Usage:\n\n\t{{$.BinaryName}} {{lower .TargetName}}{{range .RequiredArgs}} <{{.Name}}>{{end}}{{if .MultipleOptionalArgs}} [<flags>]{{else}}{{range .OptionalArgs}} [-{{.Name}}=<{{.Type}}>]{{end}}{{end}}\n\n")
_fmt.Print("Usage:\n\n\t{{$.BinaryName}} {{lower .TargetName}}{{.UsageArgs}}\n\n")
{{if .ShowFlagDocs}}_fmt.Print({{printf "%q" .FlagDocsString}})
{{end -}}
var aliases []string
Expand Down Expand Up @@ -300,6 +300,9 @@ Options:
}
return
}
{{- if .DefaultFunc.VariadicArgs}}
x := 0
{{- end}}
{{.DefaultFunc.ExecCode}}
handleError(logger, ret)
return
Expand Down
8 changes: 8 additions & 0 deletions mage/testdata/variadic/imported/targets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package variadicimport

import "fmt"

// Collect prints imported variadic arguments.
func Collect(prefix string, args ...string) {
fmt.Printf("shared:collect:%s:%q\n", prefix, args)
}
Loading