Merge pull request #150 from alexflint/push-coverage-up-more

Push test coverage up to 100%
This commit is contained in:
Alex Flint 2021-04-19 21:27:53 -07:00 committed by GitHub
commit a0937d1b58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 383 additions and 104 deletions

View File

@ -95,6 +95,19 @@ func Example_mappings() {
// output: map[john:123 mary:456] // output: map[john:123 mary:456]
} }
// This example demonstrates arguments with keys and values separated by commas
func Example_mappingsWithCommas() {
// The args you would pass in on the command line
os.Args = split("./example --userids john=123 mary=456")
var args struct {
UserIDs map[string]int
}
MustParse(&args)
fmt.Println(args.UserIDs)
// output: map[john:123 mary:456]
}
// This eample demonstrates multiple value arguments that can be mixed with // This eample demonstrates multiple value arguments that can be mixed with
// other arguments. // other arguments.
func Example_multipleMixed() { func Example_multipleMixed() {
@ -130,6 +143,7 @@ func Example_helpText() {
// This is only necessary when running inside golang's runnable example harness // This is only necessary when running inside golang's runnable example harness
osExit = func(int) {} osExit = func(int) {}
stdout = os.Stdout
MustParse(&args) MustParse(&args)
@ -162,6 +176,7 @@ func Example_helpPlaceholder() {
// This is only necessary when running inside golang's runnable example harness // This is only necessary when running inside golang's runnable example harness
osExit = func(int) {} osExit = func(int) {}
stdout = os.Stdout
MustParse(&args) MustParse(&args)
@ -202,6 +217,7 @@ func Example_helpTextWithSubcommand() {
// This is only necessary when running inside golang's runnable example harness // This is only necessary when running inside golang's runnable example harness
osExit = func(int) {} osExit = func(int) {}
stdout = os.Stdout
MustParse(&args) MustParse(&args)
@ -239,6 +255,7 @@ func Example_helpTextForSubcommand() {
// This is only necessary when running inside golang's runnable example harness // This is only necessary when running inside golang's runnable example harness
osExit = func(int) {} osExit = func(int) {}
stdout = os.Stdout
MustParse(&args) MustParse(&args)

View File

@ -13,9 +13,6 @@ import (
scalar "github.com/alexflint/go-scalar" scalar "github.com/alexflint/go-scalar"
) )
// to enable monkey-patching during tests
var osExit = os.Exit
// path represents a sequence of steps to find the output location for an // path represents a sequence of steps to find the output location for an
// argument or subcommand in the final destination struct // argument or subcommand in the final destination struct
type path struct { type path struct {
@ -80,7 +77,7 @@ var ErrVersion = errors.New("version requested by user")
func MustParse(dest ...interface{}) *Parser { func MustParse(dest ...interface{}) *Parser {
p, err := NewParser(Config{}, dest...) p, err := NewParser(Config{}, dest...)
if err != nil { if err != nil {
fmt.Println(err) fmt.Fprintln(stdout, err)
osExit(-1) osExit(-1)
return nil // just in case osExit was monkey-patched return nil // just in case osExit was monkey-patched
} }
@ -88,10 +85,10 @@ func MustParse(dest ...interface{}) *Parser {
err = p.Parse(flags()) err = p.Parse(flags())
switch { switch {
case err == ErrHelp: case err == ErrHelp:
p.writeHelpForCommand(os.Stdout, p.lastCmd) p.writeHelpForCommand(stdout, p.lastCmd)
osExit(0) osExit(0)
case err == ErrVersion: case err == ErrVersion:
fmt.Println(p.version) fmt.Fprintln(stdout, p.version)
osExit(0) osExit(0)
case err != nil: case err != nil:
p.failWithCommand(err.Error(), p.lastCmd) p.failWithCommand(err.Error(), p.lastCmd)
@ -688,15 +685,7 @@ func (p *Parser) val(dest path) reflect.Value {
v = v.Elem() v = v.Elem()
} }
next := v.FieldByIndex(field.Index) v = v.FieldByIndex(field.Index)
if !next.IsValid() {
// it is appropriate to panic here because this can only happen due to
// an internal bug in this library (since we construct the path ourselves
// by reflecting on the same struct)
panic(fmt.Errorf("error resolving path %v: %v has no field named %v",
dest.fields, v.Type(), field))
}
v = next
} }
return v return v
} }
@ -723,15 +712,3 @@ func findSubcommand(cmds []*command, name string) *command {
} }
return nil return nil
} }
// isZero returns true if v contains the zero value for its type
func isZero(v reflect.Value) bool {
t := v.Type()
if t.Kind() == reflect.Slice || t.Kind() == reflect.Map {
return v.IsNil()
}
if !t.Comparable() {
return false
}
return v.Interface() == reflect.Zero(t).Interface()
}

View File

@ -1,6 +1,8 @@
package arg package arg
import ( import (
"bytes"
"fmt"
"net" "net"
"net/mail" "net/mail"
"os" "os"
@ -24,14 +26,34 @@ func parse(cmdline string, dest interface{}) error {
} }
func pparse(cmdline string, dest interface{}) (*Parser, error) { func pparse(cmdline string, dest interface{}) (*Parser, error) {
return parseWithEnv(cmdline, nil, dest)
}
func parseWithEnv(cmdline string, env []string, dest interface{}) (*Parser, error) {
p, err := NewParser(Config{}, dest) p, err := NewParser(Config{}, dest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// split the command line
var parts []string var parts []string
if len(cmdline) > 0 { if len(cmdline) > 0 {
parts = strings.Split(cmdline, " ") parts = strings.Split(cmdline, " ")
} }
// split the environment vars
for _, s := range env {
pos := strings.Index(s, "=")
if pos == -1 {
return nil, fmt.Errorf("missing equals sign in %q", s)
}
err := os.Setenv(s[:pos], s[pos+1:])
if err != nil {
return nil, err
}
}
// execute the parser
return p, p.Parse(parts) return p, p.Parse(parts)
} }
@ -461,7 +483,7 @@ func TestMissingValueAtEnd(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} }
func TestMissingValueInMIddle(t *testing.T) { func TestMissingValueInMiddle(t *testing.T) {
var args struct { var args struct {
Foo string Foo string
Bar string Bar string
@ -546,6 +568,14 @@ func TestNoMoreOptions(t *testing.T) {
assert.Equal(t, []string{"abc", "--foo", "xyz"}, args.Bar) assert.Equal(t, []string{"abc", "--foo", "xyz"}, args.Bar)
} }
func TestNoMoreOptionsBeforeHelp(t *testing.T) {
var args struct {
Foo int
}
err := parse("not_an_integer -- --help", &args)
assert.NotEqual(t, ErrHelp, err)
}
func TestHelpFlag(t *testing.T) { func TestHelpFlag(t *testing.T) {
var args struct { var args struct {
Foo string Foo string
@ -633,9 +663,8 @@ func TestEnvironmentVariable(t *testing.T) {
var args struct { var args struct {
Foo string `arg:"env"` Foo string `arg:"env"`
} }
setenv(t, "FOO", "bar") _, err := parseWithEnv("", []string{"FOO=bar"}, &args)
os.Args = []string{"example"} require.NoError(t, err)
MustParse(&args)
assert.Equal(t, "bar", args.Foo) assert.Equal(t, "bar", args.Foo)
} }
@ -643,8 +672,8 @@ func TestEnvironmentVariableNotPresent(t *testing.T) {
var args struct { var args struct {
NotPresent string `arg:"env"` NotPresent string `arg:"env"`
} }
os.Args = []string{"example"} _, err := parseWithEnv("", nil, &args)
MustParse(&args) require.NoError(t, err)
assert.Equal(t, "", args.NotPresent) assert.Equal(t, "", args.NotPresent)
} }
@ -652,9 +681,8 @@ func TestEnvironmentVariableOverrideName(t *testing.T) {
var args struct { var args struct {
Foo string `arg:"env:BAZ"` Foo string `arg:"env:BAZ"`
} }
setenv(t, "BAZ", "bar") _, err := parseWithEnv("", []string{"BAZ=bar"}, &args)
os.Args = []string{"example"} require.NoError(t, err)
MustParse(&args)
assert.Equal(t, "bar", args.Foo) assert.Equal(t, "bar", args.Foo)
} }
@ -662,19 +690,16 @@ func TestEnvironmentVariableOverrideArgument(t *testing.T) {
var args struct { var args struct {
Foo string `arg:"env"` Foo string `arg:"env"`
} }
setenv(t, "FOO", "bar") _, err := parseWithEnv("--foo zzz", []string{"FOO=bar"}, &args)
os.Args = []string{"example", "--foo", "baz"} require.NoError(t, err)
MustParse(&args) assert.Equal(t, "zzz", args.Foo)
assert.Equal(t, "baz", args.Foo)
} }
func TestEnvironmentVariableError(t *testing.T) { func TestEnvironmentVariableError(t *testing.T) {
var args struct { var args struct {
Foo int `arg:"env"` Foo int `arg:"env"`
} }
setenv(t, "FOO", "bar") _, err := parseWithEnv("", []string{"FOO=bar"}, &args)
os.Args = []string{"example"}
err := Parse(&args)
assert.Error(t, err) assert.Error(t, err)
} }
@ -682,9 +707,8 @@ func TestEnvironmentVariableRequired(t *testing.T) {
var args struct { var args struct {
Foo string `arg:"env,required"` Foo string `arg:"env,required"`
} }
setenv(t, "FOO", "bar") _, err := parseWithEnv("", []string{"FOO=bar"}, &args)
os.Args = []string{"example"} require.NoError(t, err)
MustParse(&args)
assert.Equal(t, "bar", args.Foo) assert.Equal(t, "bar", args.Foo)
} }
@ -692,8 +716,8 @@ func TestEnvironmentVariableSliceArgumentString(t *testing.T) {
var args struct { var args struct {
Foo []string `arg:"env"` Foo []string `arg:"env"`
} }
setenv(t, "FOO", `bar,"baz, qux"`) _, err := parseWithEnv("", []string{`FOO=bar,"baz, qux"`}, &args)
MustParse(&args) require.NoError(t, err)
assert.Equal(t, []string{"bar", "baz, qux"}, args.Foo) assert.Equal(t, []string{"bar", "baz, qux"}, args.Foo)
} }
@ -701,8 +725,8 @@ func TestEnvironmentVariableSliceArgumentInteger(t *testing.T) {
var args struct { var args struct {
Foo []int `arg:"env"` Foo []int `arg:"env"`
} }
setenv(t, "FOO", "1,99") _, err := parseWithEnv("", []string{`FOO=1,99`}, &args)
MustParse(&args) require.NoError(t, err)
assert.Equal(t, []int{1, 99}, args.Foo) assert.Equal(t, []int{1, 99}, args.Foo)
} }
@ -710,8 +734,8 @@ func TestEnvironmentVariableSliceArgumentFloat(t *testing.T) {
var args struct { var args struct {
Foo []float32 `arg:"env"` Foo []float32 `arg:"env"`
} }
setenv(t, "FOO", "1.1,99.9") _, err := parseWithEnv("", []string{`FOO=1.1,99.9`}, &args)
MustParse(&args) require.NoError(t, err)
assert.Equal(t, []float32{1.1, 99.9}, args.Foo) assert.Equal(t, []float32{1.1, 99.9}, args.Foo)
} }
@ -719,8 +743,8 @@ func TestEnvironmentVariableSliceArgumentBool(t *testing.T) {
var args struct { var args struct {
Foo []bool `arg:"env"` Foo []bool `arg:"env"`
} }
setenv(t, "FOO", "true,false,0,1") _, err := parseWithEnv("", []string{`FOO=true,false,0,1`}, &args)
MustParse(&args) require.NoError(t, err)
assert.Equal(t, []bool{true, false, false, true}, args.Foo) assert.Equal(t, []bool{true, false, false, true}, args.Foo)
} }
@ -728,8 +752,7 @@ func TestEnvironmentVariableSliceArgumentWrongCsv(t *testing.T) {
var args struct { var args struct {
Foo []int `arg:"env"` Foo []int `arg:"env"`
} }
setenv(t, "FOO", "1,99\"") _, err := parseWithEnv("", []string{`FOO=1,99\"`}, &args)
err := Parse(&args)
assert.Error(t, err) assert.Error(t, err)
} }
@ -737,8 +760,7 @@ func TestEnvironmentVariableSliceArgumentWrongType(t *testing.T) {
var args struct { var args struct {
Foo []bool `arg:"env"` Foo []bool `arg:"env"`
} }
setenv(t, "FOO", "one,two") _, err := parseWithEnv("", []string{`FOO=one,two`}, &args)
err := Parse(&args)
assert.Error(t, err) assert.Error(t, err)
} }
@ -746,8 +768,8 @@ func TestEnvironmentVariableMap(t *testing.T) {
var args struct { var args struct {
Foo map[int]string `arg:"env"` Foo map[int]string `arg:"env"`
} }
setenv(t, "FOO", "1=one,99=ninetynine") _, err := parseWithEnv("", []string{`FOO=1=one,99=ninetynine`}, &args)
MustParse(&args) require.NoError(t, err)
assert.Len(t, args.Foo, 2) assert.Len(t, args.Foo, 2)
assert.Equal(t, "one", args.Foo[1]) assert.Equal(t, "one", args.Foo[1])
assert.Equal(t, "ninetynine", args.Foo[99]) assert.Equal(t, "ninetynine", args.Foo[99])
@ -1299,3 +1321,70 @@ func TestUnexportedFieldsSkipped(t *testing.T) {
_, err := NewParser(Config{}, &args) _, err := NewParser(Config{}, &args)
require.NoError(t, err) require.NoError(t, err)
} }
func TestMustParseInvalidParser(t *testing.T) {
originalExit := osExit
originalStdout := stdout
defer func() {
osExit = originalExit
stdout = originalStdout
}()
var exitCode int
osExit = func(code int) { exitCode = code }
stdout = &bytes.Buffer{}
var args struct {
CannotParse struct{}
}
parser := MustParse(&args)
assert.Nil(t, parser)
assert.Equal(t, -1, exitCode)
}
func TestMustParsePrintsHelp(t *testing.T) {
originalExit := osExit
originalStdout := stdout
originalArgs := os.Args
defer func() {
osExit = originalExit
stdout = originalStdout
os.Args = originalArgs
}()
var exitCode *int
osExit = func(code int) { exitCode = &code }
os.Args = []string{"someprogram", "--help"}
stdout = &bytes.Buffer{}
var args struct{}
parser := MustParse(&args)
assert.NotNil(t, parser)
require.NotNil(t, exitCode)
assert.Equal(t, 0, *exitCode)
}
func TestMustParsePrintsVersion(t *testing.T) {
originalExit := osExit
originalStdout := stdout
originalArgs := os.Args
defer func() {
osExit = originalExit
stdout = originalStdout
os.Args = originalArgs
}()
var exitCode *int
osExit = func(code int) { exitCode = &code }
os.Args = []string{"someprogram", "--version"}
var b bytes.Buffer
stdout = &b
var args versioned
parser := MustParse(&args)
require.NotNil(t, parser)
require.NotNil(t, exitCode)
assert.Equal(t, 0, *exitCode)
assert.Equal(t, "example 3.2.1\n", b.String())
}

View File

@ -94,3 +94,15 @@ func isExported(field string) bool {
r, _ := utf8.DecodeRuneInString(field) // returns RuneError for empty string or invalid UTF8 r, _ := utf8.DecodeRuneInString(field) // returns RuneError for empty string or invalid UTF8
return unicode.IsLetter(r) && unicode.IsUpper(r) return unicode.IsLetter(r) && unicode.IsUpper(r)
} }
// isZero returns true if v contains the zero value for its type
func isZero(v reflect.Value) bool {
t := v.Type()
if t.Kind() == reflect.Slice || t.Kind() == reflect.Map {
return v.IsNil()
}
if !t.Comparable() {
return false
}
return v.Interface() == reflect.Zero(t).Interface()
}

View File

@ -89,3 +89,24 @@ func TestCardinalityString(t *testing.T) {
assert.Equal(t, "unsupported", unsupported.String()) assert.Equal(t, "unsupported", unsupported.String())
assert.Equal(t, "unknown(42)", cardinality(42).String()) assert.Equal(t, "unknown(42)", cardinality(42).String())
} }
func TestIsZero(t *testing.T) {
var zero int
var notZero = 3
var nilSlice []int
var nonNilSlice = []int{1, 2, 3}
var nilMap map[string]string
var nonNilMap = map[string]string{"foo": "bar"}
var uncomparable = func() {}
assert.True(t, isZero(reflect.ValueOf(zero)))
assert.False(t, isZero(reflect.ValueOf(notZero)))
assert.True(t, isZero(reflect.ValueOf(nilSlice)))
assert.False(t, isZero(reflect.ValueOf(nonNilSlice)))
assert.True(t, isZero(reflect.ValueOf(nilMap)))
assert.False(t, isZero(reflect.ValueOf(nonNilMap)))
assert.False(t, isZero(reflect.ValueOf(uncomparable)))
}

View File

@ -1,6 +1,7 @@
package arg package arg
import ( import (
"reflect"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -48,6 +49,17 @@ func TestMinimalSubcommand(t *testing.T) {
assert.Equal(t, []string{"list"}, p.SubcommandNames()) assert.Equal(t, []string{"list"}, p.SubcommandNames())
} }
func TestSubcommandNamesBeforeParsing(t *testing.T) {
type listCmd struct{}
var args struct {
List *listCmd `arg:"subcommand"`
}
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
assert.Nil(t, p.Subcommand())
assert.Nil(t, p.SubcommandNames())
}
func TestNoSuchSubcommand(t *testing.T) { func TestNoSuchSubcommand(t *testing.T) {
type listCmd struct { type listCmd struct {
} }
@ -179,6 +191,36 @@ func TestSubcommandsWithOptions(t *testing.T) {
} }
} }
func TestSubcommandsWithEnvVars(t *testing.T) {
type getCmd struct {
Name string `arg:"env"`
}
type listCmd struct {
Limit int `arg:"env"`
}
type cmd struct {
Verbose bool
Get *getCmd `arg:"subcommand"`
List *listCmd `arg:"subcommand"`
}
{
var args cmd
setenv(t, "LIMIT", "123")
err := parse("list", &args)
require.NoError(t, err)
require.NotNil(t, args.List)
assert.Equal(t, 123, args.List.Limit)
}
{
var args cmd
setenv(t, "LIMIT", "not_an_integer")
err := parse("list", &args)
assert.Error(t, err)
}
}
func TestNestedSubcommands(t *testing.T) { func TestNestedSubcommands(t *testing.T) {
type child struct{} type child struct{}
type parent struct { type parent struct {
@ -353,3 +395,19 @@ func TestSubcommandsWithMultiplePositionals(t *testing.T) {
assert.Equal(t, 5, args.Limit) assert.Equal(t, 5, args.Limit)
} }
} }
func TestValForNilStruct(t *testing.T) {
type subcmd struct{}
var cmd struct {
Sub *subcmd `arg:"subcommand"`
}
p, err := NewParser(Config{}, &cmd)
require.NoError(t, err)
typ := reflect.TypeOf(cmd)
subField, _ := typ.FieldByName("Sub")
v := p.val(path{fields: []reflect.StructField{subField, subField}})
assert.False(t, v.IsValid())
}

View File

@ -11,7 +11,11 @@ import (
const colWidth = 25 const colWidth = 25
// to allow monkey patching in tests // to allow monkey patching in tests
var stderr = os.Stderr var (
stdout io.Writer = os.Stdout
stderr io.Writer = os.Stderr
osExit = os.Exit
)
// Fail prints usage information to stderr and exits with non-zero status // Fail prints usage information to stderr and exits with non-zero status
func (p *Parser) Fail(msg string) { func (p *Parser) Fail(msg string) {

View File

@ -33,9 +33,10 @@ func (n *NameDotName) MarshalText() (text []byte, err error) {
} }
func TestWriteUsage(t *testing.T) { func TestWriteUsage(t *testing.T) {
expectedUsage := "Usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--values VALUES] [--workers WORKERS] [--testenv TESTENV] [--file FILE] INPUT [OUTPUT [OUTPUT ...]]\n" expectedUsage := "Usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--values VALUES] [--workers WORKERS] [--testenv TESTENV] [--file FILE] INPUT [OUTPUT [OUTPUT ...]]"
expectedHelp := `Usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--values VALUES] [--workers WORKERS] [--testenv TESTENV] [--file FILE] INPUT [OUTPUT [OUTPUT ...]] expectedHelp := `
Usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--values VALUES] [--workers WORKERS] [--testenv TESTENV] [--file FILE] INPUT [OUTPUT [OUTPUT ...]]
Positional arguments: Positional arguments:
INPUT INPUT
@ -56,6 +57,7 @@ Options:
--file FILE, -f FILE File with mandatory extension [default: scratch.txt] --file FILE, -f FILE File with mandatory extension [default: scratch.txt]
--help, -h display this help and exit --help, -h display this help and exit
` `
var args struct { var args struct {
Input string `arg:"positional"` Input string `arg:"positional"`
Output []string `arg:"positional" help:"list of outputs"` Output []string `arg:"positional" help:"list of outputs"`
@ -79,13 +81,13 @@ Options:
os.Args[0] = "example" os.Args[0] = "example"
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, usage.String())
var help bytes.Buffer var help bytes.Buffer
p.WriteHelp(&help) p.WriteHelp(&help)
assert.Equal(t, expectedHelp, help.String()) assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
} }
type MyEnum int type MyEnum int
@ -99,7 +101,10 @@ func (n *MyEnum) MarshalText() ([]byte, error) {
} }
func TestUsageWithDefaults(t *testing.T) { func TestUsageWithDefaults(t *testing.T) {
expectedHelp := `Usage: example [--label LABEL] [--content CONTENT] expectedUsage := "Usage: example [--label LABEL] [--content CONTENT]"
expectedHelp := `
Usage: example [--label LABEL] [--content CONTENT]
Options: Options:
--label LABEL [default: cat] --label LABEL [default: cat]
@ -118,7 +123,11 @@ Options:
var help bytes.Buffer var help bytes.Buffer
p.WriteHelp(&help) p.WriteHelp(&help)
assert.Equal(t, expectedHelp, help.String()) assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
} }
func TestUsageCannotMarshalToString(t *testing.T) { func TestUsageCannotMarshalToString(t *testing.T) {
@ -132,7 +141,10 @@ func TestUsageCannotMarshalToString(t *testing.T) {
} }
func TestUsageLongPositionalWithHelp_legacyForm(t *testing.T) { func TestUsageLongPositionalWithHelp_legacyForm(t *testing.T) {
expectedHelp := `Usage: example VERYLONGPOSITIONALWITHHELP expectedUsage := "Usage: example VERYLONGPOSITIONALWITHHELP"
expectedHelp := `
Usage: example VERYLONGPOSITIONALWITHHELP
Positional arguments: Positional arguments:
VERYLONGPOSITIONALWITHHELP VERYLONGPOSITIONALWITHHELP
@ -145,17 +157,23 @@ Options:
VeryLongPositionalWithHelp string `arg:"positional,help:this positional argument is very long but cannot include commas"` VeryLongPositionalWithHelp string `arg:"positional,help:this positional argument is very long but cannot include commas"`
} }
p, err := NewParser(Config{}, &args) p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err) require.NoError(t, err)
os.Args[0] = "example"
var help bytes.Buffer var help bytes.Buffer
p.WriteHelp(&help) p.WriteHelp(&help)
assert.Equal(t, expectedHelp, help.String()) assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
} }
func TestUsageLongPositionalWithHelp_newForm(t *testing.T) { func TestUsageLongPositionalWithHelp_newForm(t *testing.T) {
expectedHelp := `Usage: example VERYLONGPOSITIONALWITHHELP expectedUsage := "Usage: example VERYLONGPOSITIONALWITHHELP"
expectedHelp := `
Usage: example VERYLONGPOSITIONALWITHHELP
Positional arguments: Positional arguments:
VERYLONGPOSITIONALWITHHELP VERYLONGPOSITIONALWITHHELP
@ -168,17 +186,23 @@ Options:
VeryLongPositionalWithHelp string `arg:"positional" help:"this positional argument is very long, and includes: commas, colons etc"` VeryLongPositionalWithHelp string `arg:"positional" help:"this positional argument is very long, and includes: commas, colons etc"`
} }
p, err := NewParser(Config{}, &args) p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err) require.NoError(t, err)
os.Args[0] = "example"
var help bytes.Buffer var help bytes.Buffer
p.WriteHelp(&help) p.WriteHelp(&help)
assert.Equal(t, expectedHelp, help.String()) assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
} }
func TestUsageWithProgramName(t *testing.T) { func TestUsageWithProgramName(t *testing.T) {
expectedHelp := `Usage: myprogram expectedUsage := "Usage: myprogram"
expectedHelp := `
Usage: myprogram
Options: Options:
--help, -h display this help and exit --help, -h display this help and exit
@ -190,9 +214,14 @@ Options:
require.NoError(t, err) require.NoError(t, err)
os.Args[0] = "example" os.Args[0] = "example"
var help bytes.Buffer var help bytes.Buffer
p.WriteHelp(&help) p.WriteHelp(&help)
assert.Equal(t, expectedHelp, help.String()) assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
} }
type versioned struct{} type versioned struct{}
@ -203,7 +232,10 @@ func (versioned) Version() string {
} }
func TestUsageWithVersion(t *testing.T) { func TestUsageWithVersion(t *testing.T) {
expectedHelp := `example 3.2.1 expectedUsage := "example 3.2.1\nUsage: example"
expectedHelp := `
example 3.2.1
Usage: example Usage: example
Options: Options:
@ -216,12 +248,11 @@ Options:
var help bytes.Buffer var help bytes.Buffer
p.WriteHelp(&help) p.WriteHelp(&help)
actual := help.String() assert.Equal(t, expectedHelp[1:], help.String())
if expectedHelp != actual {
t.Logf("Expected:\n%s", expectedHelp) var usage bytes.Buffer
t.Logf("Actual:\n%s", actual) p.WriteUsage(&usage)
t.Fail() assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
} }
type described struct{} type described struct{}
@ -232,7 +263,10 @@ func (described) Description() string {
} }
func TestUsageWithDescription(t *testing.T) { func TestUsageWithDescription(t *testing.T) {
expectedHelp := `this program does this and that expectedUsage := "Usage: example"
expectedHelp := `
this program does this and that
Usage: example Usage: example
Options: Options:
@ -244,16 +278,18 @@ Options:
var help bytes.Buffer var help bytes.Buffer
p.WriteHelp(&help) p.WriteHelp(&help)
actual := help.String() assert.Equal(t, expectedHelp[1:], help.String())
if expectedHelp != actual {
t.Logf("Expected:\n%s", expectedHelp) var usage bytes.Buffer
t.Logf("Actual:\n%s", actual) p.WriteUsage(&usage)
t.Fail() assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
} }
func TestRequiredMultiplePositionals(t *testing.T) { func TestRequiredMultiplePositionals(t *testing.T) {
expectedHelp := `Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...] expectedUsage := "Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]"
expectedHelp := `
Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]
Positional arguments: Positional arguments:
REQUIREDMULTIPLE required multiple positional REQUIREDMULTIPLE required multiple positional
@ -270,11 +306,18 @@ Options:
var help bytes.Buffer var help bytes.Buffer
p.WriteHelp(&help) p.WriteHelp(&help)
assert.Equal(t, expectedHelp, help.String()) assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
} }
func TestUsageWithNestedSubcommands(t *testing.T) { func TestUsageWithNestedSubcommands(t *testing.T) {
expectedHelp := `Usage: example child nested [--enable] OUTPUT expectedUsage := "Usage: example child nested [--enable] OUTPUT"
expectedHelp := `
Usage: example child nested [--enable] OUTPUT
Positional arguments: Positional arguments:
OUTPUT OUTPUT
@ -307,11 +350,18 @@ Global options:
var help bytes.Buffer var help bytes.Buffer
p.WriteHelp(&help) p.WriteHelp(&help)
assert.Equal(t, expectedHelp, help.String()) assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
} }
func TestUsageWithoutLongNames(t *testing.T) { func TestUsageWithoutLongNames(t *testing.T) {
expectedHelp := `Usage: example [-a PLACEHOLDER] -b SHORTONLY2 expectedUsage := "Usage: example [-a PLACEHOLDER] -b SHORTONLY2"
expectedHelp := `
Usage: example [-a PLACEHOLDER] -b SHORTONLY2
Options: Options:
-a PLACEHOLDER some help [default: some val] -a PLACEHOLDER some help [default: some val]
@ -324,13 +374,21 @@ Options:
} }
p, err := NewParser(Config{Program: "example"}, &args) p, err := NewParser(Config{Program: "example"}, &args)
assert.NoError(t, err) assert.NoError(t, err)
var help bytes.Buffer var help bytes.Buffer
p.WriteHelp(&help) p.WriteHelp(&help)
assert.Equal(t, expectedHelp, help.String()) assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
} }
func TestUsageWithShortFirst(t *testing.T) { func TestUsageWithShortFirst(t *testing.T) {
expectedHelp := `Usage: example [-c CAT] [--dog DOG] expectedUsage := "Usage: example [-c CAT] [--dog DOG]"
expectedHelp := `
Usage: example [-c CAT] [--dog DOG]
Options: Options:
-c CAT -c CAT
@ -343,13 +401,21 @@ Options:
} }
p, err := NewParser(Config{Program: "example"}, &args) p, err := NewParser(Config{Program: "example"}, &args)
assert.NoError(t, err) assert.NoError(t, err)
var help bytes.Buffer var help bytes.Buffer
p.WriteHelp(&help) p.WriteHelp(&help)
assert.Equal(t, expectedHelp, help.String()) assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
} }
func TestUsageWithEnvOptions(t *testing.T) { func TestUsageWithEnvOptions(t *testing.T) {
expectedHelp := `Usage: example [-s SHORT] expectedUsage := "Usage: example [-s SHORT]"
expectedHelp := `
Usage: example [-s SHORT]
Options: Options:
-s SHORT [env: SHORT] -s SHORT [env: SHORT]
@ -363,7 +429,42 @@ Options:
p, err := NewParser(Config{Program: "example"}, &args) p, err := NewParser(Config{Program: "example"}, &args)
assert.NoError(t, err) assert.NoError(t, err)
var help bytes.Buffer var help bytes.Buffer
p.WriteHelp(&help) p.WriteHelp(&help)
assert.Equal(t, expectedHelp, help.String()) assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestFail(t *testing.T) {
originalStderr := stderr
originalExit := osExit
defer func() {
stderr = originalStderr
osExit = originalExit
}()
var b bytes.Buffer
stderr = &b
var exitCode int
osExit = func(code int) { exitCode = code }
expectedStdout := `
Usage: example [--foo FOO]
error: something went wrong
`
var args struct {
Foo int
}
p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err)
p.Fail("something went wrong")
assert.Equal(t, expectedStdout[1:], b.String())
assert.Equal(t, -1, exitCode)
} }