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"}},
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