...

Source file src/github.com/peterbourgon/ff/v3/parse_test.go

Documentation: github.com/peterbourgon/ff/v3

     1  package ff_test
     2  
     3  import (
     4  	"context"
     5  	"flag"
     6  	"os"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/peterbourgon/ff/v3"
    11  	"github.com/peterbourgon/ff/v3/ffcli"
    12  	"github.com/peterbourgon/ff/v3/fftest"
    13  )
    14  
    15  func TestParseBasics(t *testing.T) {
    16  	t.Parallel()
    17  
    18  	for _, testcase := range []struct {
    19  		name string
    20  		env  map[string]string
    21  		file string
    22  		args []string
    23  		opts []ff.Option
    24  		want fftest.Vars
    25  	}{
    26  		{
    27  			name: "empty",
    28  			args: []string{},
    29  			want: fftest.Vars{},
    30  		},
    31  		{
    32  			name: "args only",
    33  			args: []string{"-s", "foo", "-i", "123", "-b", "-d", "24m"},
    34  			want: fftest.Vars{S: "foo", I: 123, B: true, D: 24 * time.Minute},
    35  		},
    36  		{
    37  			name: "file only",
    38  			file: "testdata/1.conf",
    39  			want: fftest.Vars{S: "bar", I: 99, B: true, D: time.Hour},
    40  		},
    41  		{
    42  			name: "env only",
    43  			env:  map[string]string{"TEST_PARSE_S": "baz", "TEST_PARSE_F": "0.99", "TEST_PARSE_D": "100s"},
    44  			opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE")},
    45  			want: fftest.Vars{S: "baz", F: 0.99, D: 100 * time.Second},
    46  		},
    47  		{
    48  			name: "file args",
    49  			file: "testdata/2.conf",
    50  			args: []string{"-s", "foo", "-i", "1234"},
    51  			want: fftest.Vars{S: "foo", I: 1234, D: 3 * time.Second},
    52  		},
    53  		{
    54  			name: "env args",
    55  			env:  map[string]string{"TEST_PARSE_S": "should be overridden", "TEST_PARSE_B": "true"},
    56  			args: []string{"-s", "explicit wins", "-i", "7"},
    57  			opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE")},
    58  			want: fftest.Vars{S: "explicit wins", I: 7, B: true},
    59  		},
    60  		{
    61  			name: "file env",
    62  			env:  map[string]string{"TEST_PARSE_S": "env takes priority", "TEST_PARSE_B": "true"},
    63  			file: "testdata/3.conf",
    64  			opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE")},
    65  			want: fftest.Vars{S: "env takes priority", I: 99, B: true, D: 34 * time.Second},
    66  		},
    67  		{
    68  			name: "file env args",
    69  			file: "testdata/4.conf",
    70  			env:  map[string]string{"TEST_PARSE_S": "from env", "TEST_PARSE_I": "300", "TEST_PARSE_F": "0.15", "TEST_PARSE_B": "true"},
    71  			args: []string{"-s", "from arg", "-i", "100"},
    72  			opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE")},
    73  			want: fftest.Vars{S: "from arg", I: 100, F: 0.15, B: true, D: time.Minute},
    74  		},
    75  		{
    76  			name: "repeated args",
    77  			args: []string{"-s", "foo", "-s", "bar", "-d", "1m", "-d", "1h", "-x", "1", "-x", "2", "-x", "3"},
    78  			want: fftest.Vars{S: "bar", D: time.Hour, X: []string{"1", "2", "3"}},
    79  		},
    80  		{
    81  			name: "priority repeats",
    82  			env:  map[string]string{"TEST_PARSE_S": "s.env", "TEST_PARSE_X": "x.env.1"},
    83  			file: "testdata/5.conf",
    84  			args: []string{"-s", "s.arg.1", "-s", "s.arg.2", "-x", "x.arg.1", "-x", "x.arg.2"},
    85  			opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE")},
    86  			want: fftest.Vars{S: "s.arg.2", X: []string{"x.arg.1", "x.arg.2"}}, // highest prio wins and no others are called
    87  		},
    88  		{
    89  			name: "PlainParser solo bool",
    90  			file: "testdata/solo_bool.conf",
    91  			want: fftest.Vars{S: "x", B: true},
    92  		},
    93  		{
    94  			name: "PlainParser string with spaces",
    95  			file: "testdata/spaces.conf",
    96  			want: fftest.Vars{S: "i am the very model of a modern major general"},
    97  		},
    98  		{
    99  			name: "default comma behavior",
   100  			env:  map[string]string{"TEST_PARSE_S": "one,two,three", "TEST_PARSE_X": "one,two,three"},
   101  			opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE")},
   102  			want: fftest.Vars{S: "one,two,three", X: []string{"one,two,three"}},
   103  		},
   104  		{
   105  			name: "WithEnvVarSplit",
   106  			env:  map[string]string{"TEST_PARSE_S": "one,two,three", "TEST_PARSE_X": "one,two,three"},
   107  			opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE"), ff.WithEnvVarSplit(",")},
   108  			want: fftest.Vars{S: "three", X: []string{"one", "two", "three"}},
   109  		},
   110  		{
   111  			name: "WithEnvVarNoPrefix",
   112  			env:  map[string]string{"TEST_PARSE_S": "foo", "S": "bar"},
   113  			opts: []ff.Option{ff.WithEnvVarNoPrefix()},
   114  			want: fftest.Vars{S: "bar"},
   115  		},
   116  		{
   117  			name: "WithIgnoreUndefined env",
   118  			env:  map[string]string{"TEST_PARSE_UNDEFINED": "one", "TEST_PARSE_S": "one"},
   119  			opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE"), ff.WithIgnoreUndefined(true)},
   120  			want: fftest.Vars{S: "one"},
   121  		},
   122  		{
   123  			name: "WithIgnoreUndefined file true",
   124  			file: "testdata/undefined.conf",
   125  			opts: []ff.Option{ff.WithIgnoreUndefined(true)},
   126  			want: fftest.Vars{S: "one"},
   127  		},
   128  		{
   129  			name: "WithIgnoreUndefined file false",
   130  			file: "testdata/undefined.conf",
   131  			opts: []ff.Option{ff.WithIgnoreUndefined(false)},
   132  			want: fftest.Vars{WantParseErrorString: "config file flag"},
   133  		},
   134  		{
   135  			name: "env var split comma whitespace",
   136  			env:  map[string]string{"TEST_PARSE_S": "one, two, three ", "TEST_PARSE_X": "one, two, three "},
   137  			opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE"), ff.WithEnvVarSplit(",")},
   138  			want: fftest.Vars{S: " three ", X: []string{"one", " two", " three "}},
   139  		},
   140  	} {
   141  		t.Run(testcase.name, func(t *testing.T) {
   142  			if testcase.file != "" {
   143  				testcase.opts = append(testcase.opts, ff.WithConfigFile(testcase.file), ff.WithConfigFileParser(ff.PlainParser))
   144  			}
   145  
   146  			if len(testcase.env) > 0 {
   147  				for k, v := range testcase.env {
   148  					defer os.Setenv(k, os.Getenv(k))
   149  					os.Setenv(k, v)
   150  				}
   151  			}
   152  
   153  			fs, vars := fftest.Pair()
   154  			vars.ParseError = ff.Parse(fs, testcase.args, testcase.opts...)
   155  			fftest.Compare(t, &testcase.want, vars)
   156  		})
   157  	}
   158  }
   159  
   160  func TestParseIssue16(t *testing.T) {
   161  	t.Parallel()
   162  
   163  	for _, testcase := range []struct {
   164  		name string
   165  		data string
   166  		want string
   167  	}{
   168  		{
   169  			name: "hash in value",
   170  			data: "s bar#baz",
   171  			want: "bar#baz",
   172  		},
   173  		{
   174  			name: "EOL comment with space",
   175  			data: "s bar # baz",
   176  			want: "bar",
   177  		},
   178  		{
   179  			name: "EOL comment no space",
   180  			data: "s bar #baz",
   181  			want: "bar",
   182  		},
   183  		{
   184  			name: "only comment with space",
   185  			data: "# foo bar\n",
   186  			want: "",
   187  		},
   188  		{
   189  			name: "only comment no space",
   190  			data: "#foo bar\n",
   191  			want: "",
   192  		},
   193  	} {
   194  		t.Run(testcase.name, func(t *testing.T) {
   195  			filename, cleanup := fftest.TempFile(t, testcase.data)
   196  			defer cleanup()
   197  
   198  			fs, vars := fftest.Pair()
   199  			vars.ParseError = ff.Parse(fs, []string{},
   200  				ff.WithConfigFile(filename),
   201  				ff.WithConfigFileParser(ff.PlainParser),
   202  			)
   203  
   204  			want := fftest.Vars{S: testcase.want}
   205  			fftest.Compare(t, &want, vars)
   206  		})
   207  	}
   208  }
   209  
   210  func TestParseConfigFile(t *testing.T) {
   211  	t.Parallel()
   212  
   213  	for _, testcase := range []struct {
   214  		name         string
   215  		missing      bool
   216  		allowMissing bool
   217  		parseError   error
   218  	}{
   219  		{
   220  			name: "has config file",
   221  		},
   222  		{
   223  			name:       "config file missing",
   224  			missing:    true,
   225  			parseError: os.ErrNotExist,
   226  		},
   227  		{
   228  			name:         "config file missing + allow missing",
   229  			missing:      true,
   230  			allowMissing: true,
   231  		},
   232  	} {
   233  		t.Run(testcase.name, func(t *testing.T) {
   234  			filename := "dummy"
   235  			if !testcase.missing {
   236  				var cleanup func()
   237  				filename, cleanup = fftest.TempFile(t, "")
   238  				defer cleanup()
   239  			}
   240  
   241  			options := []ff.Option{ff.WithConfigFile(filename), ff.WithConfigFileParser(ff.PlainParser)}
   242  			if testcase.allowMissing {
   243  				options = append(options, ff.WithAllowMissingConfigFile(true))
   244  			}
   245  
   246  			fs, vars := fftest.Pair()
   247  			vars.ParseError = ff.Parse(fs, []string{}, options...)
   248  
   249  			want := fftest.Vars{WantParseErrorIs: testcase.parseError}
   250  			fftest.Compare(t, &want, vars)
   251  		})
   252  	}
   253  }
   254  
   255  func TestParseConfigFileVia(t *testing.T) {
   256  	t.Parallel()
   257  
   258  	var (
   259  		rootFS = flag.NewFlagSet("root", flag.ContinueOnError)
   260  		config = rootFS.String("config-file", "", "")
   261  		i      = rootFS.Int("i", 0, "")
   262  		s      = rootFS.String("s", "", "")
   263  		subFS  = flag.NewFlagSet("subcommand", flag.ContinueOnError)
   264  		d      = subFS.Duration("d", time.Second, "")
   265  		b      = subFS.Bool("b", false, "")
   266  	)
   267  
   268  	subCommand := &ffcli.Command{
   269  		Name:    "subcommand",
   270  		FlagSet: subFS,
   271  		Options: []ff.Option{
   272  			ff.WithConfigFileParser(ff.PlainParser),
   273  			ff.WithConfigFileVia(config),
   274  			ff.WithIgnoreUndefined(true),
   275  		},
   276  		Exec: func(ctx context.Context, args []string) error { return nil },
   277  	}
   278  
   279  	root := &ffcli.Command{
   280  		Name:    "root",
   281  		FlagSet: rootFS,
   282  		Options: []ff.Option{
   283  			ff.WithConfigFileParser(ff.PlainParser),
   284  			ff.WithConfigFileFlag("config-file"),
   285  			ff.WithIgnoreUndefined(true),
   286  		},
   287  		Exec:        func(ctx context.Context, args []string) error { return nil },
   288  		Subcommands: []*ffcli.Command{subCommand},
   289  	}
   290  
   291  	err := root.ParseAndRun(context.Background(), []string{"-config-file", "testdata/1.conf", "subcommand", "-b"})
   292  	if err != nil {
   293  		t.Fatal(err)
   294  	}
   295  
   296  	if want, have := time.Hour, *d; want != have {
   297  		t.Errorf("d: want %v, have %v", want, have)
   298  	}
   299  	if want, have := true, *b; want != have {
   300  		t.Errorf("b: want %v, have %v", want, have)
   301  	}
   302  	if want, have := "bar", *s; want != have {
   303  		t.Errorf("s: want %q, have %q", want, have)
   304  	}
   305  	if want, have := 99, *i; want != have {
   306  		t.Errorf("i: want %d, have %d", want, have)
   307  	}
   308  
   309  }
   310  

View as plain text