github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/tiltfile/config/config_test.go (about) 1 package config 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "strings" 8 "testing" 9 10 "github.com/stretchr/testify/require" 11 12 "github.com/tilt-dev/tilt/internal/tiltfile/include" 13 "github.com/tilt-dev/tilt/internal/tiltfile/io" 14 "github.com/tilt-dev/tilt/internal/tiltfile/starkit" 15 "github.com/tilt-dev/tilt/internal/tiltfile/value" 16 "github.com/tilt-dev/tilt/pkg/model" 17 ) 18 19 func TestSetResources(t *testing.T) { 20 for _, tc := range []struct { 21 name string 22 callConfigParse bool 23 args []string 24 tiltfileResources []model.ManifestName 25 expectedResources []model.ManifestName 26 }{ 27 {"neither", false, nil, nil, []model.ManifestName{"a", "b"}}, 28 {"neither, with config.parse", true, nil, nil, []model.ManifestName{"a", "b"}}, 29 {"args only", false, []string{"a"}, nil, []model.ManifestName{"a"}}, 30 {"args only, with config.parse", true, []string{"a"}, nil, []model.ManifestName{"a", "b"}}, 31 {"tiltfile only", false, nil, []model.ManifestName{"b"}, []model.ManifestName{"b"}}, 32 {"tiltfile only, with config.parse", true, nil, []model.ManifestName{"b"}, []model.ManifestName{"b"}}, 33 {"both", false, []string{"a"}, []model.ManifestName{"b"}, []model.ManifestName{"b"}}, 34 {"both, with config.parse", true, []string{"a"}, []model.ManifestName{"b"}, []model.ManifestName{"b"}}, 35 } { 36 t.Run(tc.name, func(t *testing.T) { 37 f := NewFixture(t, tc.args, "") 38 39 setResources := "" 40 if len(tc.tiltfileResources) > 0 { 41 var rs []string 42 for _, mn := range tc.tiltfileResources { 43 rs = append(rs, fmt.Sprintf("'%s'", mn)) 44 } 45 setResources = fmt.Sprintf("config.set_enabled_resources([%s])", strings.Join(rs, ", ")) 46 } 47 48 configParse := "" 49 if tc.callConfigParse { 50 configParse = ` 51 config.define_string_list('resources', args=True) 52 config.parse()` 53 } 54 55 tiltfile := fmt.Sprintf("%s\n%s\n", setResources, configParse) 56 57 f.File("Tiltfile", tiltfile) 58 59 result, err := f.ExecFile("Tiltfile") 60 require.NoError(t, err) 61 62 manifests := []model.Manifest{{Name: "a"}, {Name: "b"}} 63 actual, err := MustState(result).EnabledResources(f.Tiltfile(), manifests) 64 require.NoError(t, err) 65 66 require.Equal(t, tc.expectedResources, actual) 67 }) 68 } 69 } 70 71 func TestClearEnabledResources(t *testing.T) { 72 args := strings.Split("united states canada mexico panama haiti jamaica peru", " ") 73 74 f := NewFixture(t, args, "") 75 76 f.File("Tiltfile", "config.clear_enabled_resources()") 77 78 result, err := f.ExecFile("Tiltfile") 79 require.NoError(t, err) 80 81 manifests := []model.Manifest{{Name: "a"}, {Name: "b"}} 82 actual, err := MustState(result).EnabledResources(f.Tiltfile(), manifests) 83 require.NoError(t, err) 84 85 require.Len(t, actual, 0) 86 } 87 88 func TestClearEnabledResourcesWithArgs(t *testing.T) { 89 args := strings.Split("united states canada mexico panama haiti jamaica peru", " ") 90 91 f := NewFixture(t, args, "") 92 93 f.File("Tiltfile", "config.clear_enabled_resources('foo')") 94 95 _, err := f.ExecFile("Tiltfile") 96 require.Error(t, err) 97 98 require.Contains(t, err.Error(), "got 1 arguments, want at most 0") 99 } 100 101 func TestParsePositional(t *testing.T) { 102 args := strings.Split("united states canada mexico panama haiti jamaica peru", " ") 103 104 f := NewFixture(t, args, "") 105 106 f.File("Tiltfile", ` 107 config.define_string_list('foo', args=True) 108 cfg = config.parse() 109 print(cfg['foo']) 110 `) 111 112 _, err := f.ExecFile("Tiltfile") 113 require.NoError(t, err) 114 115 require.Contains(t, f.PrintOutput(), value.StringSliceToList(args).String()) 116 } 117 118 func TestParseKeyword(t *testing.T) { 119 foo := strings.Split("republic dominican cuba caribbean greenland el salvador too", " ") 120 var args []string 121 for _, s := range foo { 122 args = append(args, []string{"--foo", s}...) 123 } 124 125 f := NewFixture(t, args, "") 126 127 f.File("Tiltfile", ` 128 config.define_string_list('foo') 129 cfg = config.parse() 130 print(cfg['foo']) 131 `) 132 133 _, err := f.ExecFile("Tiltfile") 134 require.NoError(t, err) 135 136 require.Contains(t, f.PrintOutput(), value.StringSliceToList(foo).String()) 137 } 138 139 func TestParsePositionalAndMultipleInterspersedKeyword(t *testing.T) { 140 args := []string{"--bar", "puerto rico", "--baz", "colombia", "--bar", "venezuela", "--baz", "honduras", "--baz", "guyana", "and", "still"} 141 f := NewFixture(t, args, "") 142 143 f.File("Tiltfile", ` 144 config.define_string_list('foo', args=True) 145 config.define_string_list('bar') 146 config.define_string_list('baz') 147 cfg = config.parse() 148 print("foo:", cfg['foo']) 149 print("bar:", cfg['bar']) 150 print("baz:", cfg['baz']) 151 `) 152 153 _, err := f.ExecFile("Tiltfile") 154 require.NoError(t, err) 155 156 require.Contains(t, f.PrintOutput(), `foo: ["and", "still"]`) 157 require.Contains(t, f.PrintOutput(), `bar: ["puerto rico", "venezuela"]`) 158 require.Contains(t, f.PrintOutput(), `baz: ["colombia", "honduras", "guyana"]`) 159 } 160 161 func TestParseKeywordAfterPositional(t *testing.T) { 162 args := []string{"--bar", "puerto rico", "colombia"} 163 f := NewFixture(t, args, "") 164 165 f.File("Tiltfile", ` 166 config.define_string_list('foo', args=True) 167 config.define_string('bar') 168 cfg = config.parse() 169 print("foo:", cfg['foo']) 170 print("bar:", cfg['bar']) 171 `) 172 173 _, err := f.ExecFile("Tiltfile") 174 require.NoError(t, err) 175 176 require.Contains(t, f.PrintOutput(), `foo: ["colombia"]`) 177 require.Contains(t, f.PrintOutput(), `bar: puerto rico`) 178 } 179 180 func TestMultiplePositionalDefs(t *testing.T) { 181 f := NewFixture(t, nil, "") 182 183 f.File("Tiltfile", ` 184 config.define_string_list('foo', args=True) 185 config.define_string_list('bar', args=True) 186 `) 187 188 _, err := f.ExecFile("Tiltfile") 189 require.Error(t, err) 190 require.Equal(t, "both bar and foo are defined as positional args", err.Error()) 191 } 192 193 func TestMultipleArgsSameName(t *testing.T) { 194 f := NewFixture(t, nil, "") 195 196 f.File("Tiltfile", ` 197 config.define_string_list('foo') 198 config.define_string_list('foo') 199 `) 200 201 _, err := f.ExecFile("Tiltfile") 202 require.Error(t, err) 203 require.Equal(t, "foo defined multiple times", err.Error()) 204 } 205 206 func TestUndefinedArg(t *testing.T) { 207 f := NewFixture(t, []string{"--bar", "hello"}, "") 208 209 f.File("Tiltfile", ` 210 config.define_string_list('foo') 211 config.parse() 212 `) 213 214 expected := `invalid Tiltfile config args: unknown flag: --bar 215 Usage: 216 --foo list[string] ` + ` 217 ` 218 219 _, err := f.ExecFile("Tiltfile") 220 require.Error(t, err) 221 require.EqualError(t, err, expected) 222 } 223 224 func TestUnprovidedArg(t *testing.T) { 225 f := NewFixture(t, nil, "") 226 227 f.File("Tiltfile", ` 228 config.define_string_list('foo') 229 cfg = config.parse() 230 print("foo:",cfg['foo']) 231 `) 232 233 _, err := f.ExecFile("Tiltfile") 234 require.Error(t, err) 235 require.Contains(t, err.Error(), `key "foo" not in dict`) 236 } 237 238 func TestUnprovidedPositionalArg(t *testing.T) { 239 f := NewFixture(t, nil, "") 240 f.File("Tiltfile", ` 241 config.define_string_list('foo', args=True) 242 cfg = config.parse() 243 print("foo:",cfg['foo']) 244 `) 245 246 _, err := f.ExecFile("Tiltfile") 247 require.Error(t, err) 248 require.Contains(t, err.Error(), `key "foo" not in dict`) 249 } 250 251 func TestProvidedButUnexpectedPositionalArgs(t *testing.T) { 252 f := NewFixture(t, []string{"do", "re", "mi"}, "") 253 254 f.File("Tiltfile", ` 255 cfg = config.parse() 256 `) 257 258 _, err := f.ExecFile("Tiltfile") 259 require.Error(t, err) 260 require.Equal(t, 261 "invalid Tiltfile config args: positional CLI args (\"do re mi\") were specified, but none were expected.\n"+ 262 "See https://docs.tilt.dev/tiltfile_config.html#positional-arguments for examples.", 263 err.Error()) 264 } 265 266 func TestUsage(t *testing.T) { 267 f := NewFixture(t, []string{"--bar", "hello"}, "") 268 269 f.File("Tiltfile", ` 270 config.define_string_list('foo', usage='what can I foo for you today?') 271 config.parse() 272 `) 273 274 expected := `invalid Tiltfile config args: unknown flag: --bar 275 Usage: 276 --foo list[string] what can I foo for you today? 277 ` 278 279 _, err := f.ExecFile("Tiltfile") 280 require.Error(t, err) 281 require.EqualError(t, err, expected) 282 } 283 284 // i.e., tilt up foo bar gets you resources foo and bar 285 func TestDefaultTiltBehavior(t *testing.T) { 286 f := NewFixture(t, []string{"foo", "bar"}, "") 287 288 f.File("Tiltfile", ` 289 config.define_string_list('resources', usage='which resources to load in Tilt', args=True) 290 config.set_enabled_resources(config.parse()['resources']) 291 `) 292 293 result, err := f.ExecFile("Tiltfile") 294 require.NoError(t, err) 295 296 manifests := []model.Manifest{{Name: "foo"}, {Name: "bar"}, {Name: "baz"}} 297 actual, err := MustState(result).EnabledResources(f.Tiltfile(), manifests) 298 require.NoError(t, err) 299 require.Equal(t, []model.ManifestName{"foo", "bar"}, actual) 300 } 301 302 func TestSettingsFromConfigAndArgs(t *testing.T) { 303 for _, tc := range []struct { 304 name string 305 args []string 306 config map[string][]string 307 expected map[string][]string 308 }{ 309 { 310 name: "args only", 311 args: []string{"--a", "1", "--a", "2", "--b", "3", "--a", "4", "5", "6"}, 312 config: nil, 313 expected: map[string][]string{ 314 "a": {"1", "2", "4"}, 315 "b": {"3"}, 316 "c": {"5", "6"}, 317 }, 318 }, 319 { 320 name: "config only", 321 args: nil, 322 config: map[string][]string{ 323 "b": {"7", "8"}, 324 "c": {"9"}, 325 }, 326 expected: map[string][]string{ 327 "b": {"7", "8"}, 328 "c": {"9"}, 329 }, 330 }, 331 { 332 name: "args trump config", 333 args: []string{"--a", "1", "--a", "2", "--a", "4", "5", "6"}, 334 config: map[string][]string{ 335 "b": {"7", "8"}, 336 "c": {"9"}, 337 }, 338 expected: map[string][]string{ 339 "a": {"1", "2", "4"}, 340 "b": {"7", "8"}, 341 "c": {"5", "6"}, 342 }, 343 }, 344 } { 345 t.Run(tc.name, func(t *testing.T) { 346 f := NewFixture(t, tc.args, "") 347 348 f.File("Tiltfile", ` 349 config.define_string_list('a') 350 config.define_string_list('b') 351 config.define_string_list('c', args=True) 352 cfg = config.parse() 353 print("a=", cfg.get('a', 'missing')) 354 print("b=", cfg.get('b', 'missing')) 355 print("c=", cfg.get('c', 'missing')) 356 `) 357 if tc.config != nil { 358 b := &bytes.Buffer{} 359 err := json.NewEncoder(b).Encode(tc.config) 360 require.NoError(t, err) 361 f.File(UserConfigFileName, b.String()) 362 } 363 364 _, err := f.ExecFile("Tiltfile") 365 require.NoError(t, err) 366 367 for _, arg := range []string{"a", "b", "c"} { 368 expected := "missing" 369 if vs, ok := tc.expected[arg]; ok { 370 var s []string 371 for _, v := range vs { 372 s = append(s, fmt.Sprintf(`"%s"`, v)) 373 } 374 expected = fmt.Sprintf("[%s]", strings.Join(s, ", ")) 375 } 376 require.Contains(t, f.PrintOutput(), fmt.Sprintf("%s= %s", arg, expected)) 377 } 378 }) 379 } 380 } 381 382 func TestUndefinedArgInConfigFile(t *testing.T) { 383 f := NewFixture(t, nil, "") 384 385 f.File("Tiltfile", ` 386 config.define_string_list('foo') 387 cfg = config.parse() 388 print("foo:",cfg.get('foo', [])) 389 `) 390 391 f.File(UserConfigFileName, `{"bar": "1"}`) 392 393 _, err := f.ExecFile("Tiltfile") 394 require.Error(t, err) 395 require.Contains(t, err.Error(), "specified unknown setting name 'bar'") 396 } 397 398 func TestWrongTypeArgInConfigFile(t *testing.T) { 399 f := NewFixture(t, nil, "") 400 401 f.File("Tiltfile", ` 402 config.define_string_list('foo') 403 cfg = config.parse() 404 print("foo:",cfg.get('foo', [])) 405 `) 406 407 f.File(UserConfigFileName, `{"foo": "1"}`) 408 409 _, err := f.ExecFile("Tiltfile") 410 require.Error(t, err) 411 require.Contains(t, err.Error(), "specified invalid value for setting foo: expected array") 412 } 413 414 func TestConfigParseFromMultipleDirs(t *testing.T) { 415 f := NewFixture(t, nil, "") 416 417 f.File("Tiltfile", ` 418 config.define_string_list('foo') 419 cfg = config.parse() 420 include('inc/Tiltfile') 421 `) 422 423 f.File("inc/Tiltfile", ` 424 cfg = config.parse() 425 `) 426 427 _, err := f.ExecFile("Tiltfile") 428 require.Error(t, err) 429 require.Contains(t, err.Error(), "config.parse can only be called from one Tiltfile working directory per run") 430 require.Contains(t, err.Error(), f.Path()) 431 require.Contains(t, err.Error(), f.JoinPath("inc")) 432 } 433 434 func TestDefineSettingAfterParse(t *testing.T) { 435 f := NewFixture(t, nil, "") 436 437 f.File("Tiltfile", ` 438 cfg = config.parse() 439 config.define_string_list('foo') 440 `) 441 442 _, err := f.ExecFile("Tiltfile") 443 require.Error(t, err) 444 require.Contains(t, err.Error(), "config.define_string_list cannot be called after config.parse is called") 445 } 446 447 func TestConfigFileRecordedRead(t *testing.T) { 448 f := NewFixture(t, nil, "") 449 450 f.File("Tiltfile", ` 451 cfg = config.parse()`) 452 453 result, err := f.ExecFile("Tiltfile") 454 require.NoError(t, err) 455 456 rs, err := io.GetState(result) 457 require.NoError(t, err) 458 require.Contains(t, rs.Paths, f.JoinPath(UserConfigFileName)) 459 } 460 461 func TestSubCommand(t *testing.T) { 462 f := NewFixture(t, nil, "foo") 463 464 f.File("Tiltfile", ` 465 print(config.tilt_subcommand) 466 `) 467 468 _, err := f.ExecFile("Tiltfile") 469 require.NoError(t, err) 470 471 require.Equal(t, "foo\n", f.PrintOutput()) 472 } 473 474 func TestTiltfilePath(t *testing.T) { 475 f := NewFixture(t, nil, "foo") 476 477 f.File("foo/Tiltfile", ` 478 print(config.main_path) 479 `) 480 f.File("Tiltfile", ` 481 include('./foo/Tiltfile') 482 print(config.main_path) 483 `) 484 485 _, err := f.ExecFile("Tiltfile") 486 require.NoError(t, err) 487 488 val := f.JoinPath("Tiltfile") 489 require.Equal(t, fmt.Sprintf("%s\n%s\n", val, val), f.PrintOutput()) 490 } 491 492 func TestTiltfileDir(t *testing.T) { 493 f := NewFixture(t, nil, "foo") 494 495 f.File("foo/Tiltfile", ` 496 print(config.main_dir) 497 `) 498 f.File("Tiltfile", ` 499 include('./foo/Tiltfile') 500 print(config.main_dir) 501 `) 502 503 _, err := f.ExecFile("Tiltfile") 504 require.NoError(t, err) 505 506 val := f.Path() 507 require.Equal(t, fmt.Sprintf("%s\n%s\n", val, val), f.PrintOutput()) 508 } 509 510 func NewFixture(tb testing.TB, args []string, tiltSubcommand model.TiltSubcommand) *starkit.Fixture { 511 ext := NewPlugin(tiltSubcommand) 512 513 ret := starkit.NewFixture(tb, ext, io.NewPlugin(), include.IncludeFn{}) 514 ret.UseRealFS() 515 ret.Tiltfile().Spec.Args = args 516 return ret 517 } 518 519 type typeTestCase struct { 520 name string 521 define string 522 args []string 523 configFile string 524 expectedVal string 525 expectedError string 526 } 527 528 func newTypeTestCase(name string, define string) typeTestCase { 529 return typeTestCase{ 530 name: name, 531 define: define, 532 } 533 } 534 535 func (ttc typeTestCase) withExpectedVal(expectedVal string) typeTestCase { 536 ttc.expectedVal = expectedVal 537 return ttc 538 } 539 540 func (ttc typeTestCase) withExpectedError(expectedError string) typeTestCase { 541 ttc.expectedError = expectedError 542 return ttc 543 } 544 545 func (ttc typeTestCase) withArgs(args ...string) typeTestCase { 546 ttc.args = args 547 return ttc 548 } 549 550 func (ttc typeTestCase) withConfigFile(cfg string) typeTestCase { 551 ttc.configFile = cfg 552 return ttc 553 } 554 555 func TestTypes(t *testing.T) { 556 for _, tc := range []struct { 557 name string 558 define string 559 args []string 560 configFile string 561 expectedVal string 562 expectedError string 563 }{ 564 newTypeTestCase("string_list from args", "config.define_string_list('foo')").withArgs("--foo", "1", "--foo", "2").withExpectedVal("['1', '2']"), 565 newTypeTestCase("string_list from config", "config.define_string_list('foo')").withConfigFile(`{"foo": ["1", "2"]}`).withExpectedVal("['1', '2']"), 566 newTypeTestCase("invalid string_list from config", "config.define_string_list('foo')").withConfigFile(`{"foo": [1, 2]}`).withExpectedError("expected string, got float64"), 567 568 newTypeTestCase("string from args", "config.define_string('foo')").withArgs("--foo", "bar").withExpectedVal("'bar'"), 569 newTypeTestCase("string from config", "config.define_string('foo')").withConfigFile(`{"foo": "bar"}`).withExpectedVal("'bar'"), 570 newTypeTestCase("string defined multiple times", "config.define_string('foo')").withArgs("--foo", "bar", "--foo", "baz").withExpectedError("string settings can only be specified once"), 571 newTypeTestCase("invalid string from config", "config.define_string('foo')").withConfigFile(`{"foo": 5}`).withExpectedError("expected string, found float64"), 572 573 newTypeTestCase("bool from args w/ implicit value", "config.define_bool('foo')").withArgs("--foo").withExpectedVal("True"), 574 newTypeTestCase("bool from config", "config.define_bool('foo')").withConfigFile(`{"foo": true}`).withExpectedVal("True"), 575 newTypeTestCase("bool defined multiple times", "config.define_bool('foo')").withArgs("--foo", "--foo").withExpectedError("bool settings can only be specified once"), 576 newTypeTestCase("invalid bool from config", "config.define_bool('foo')").withConfigFile(`{"foo": 5}`).withExpectedError("expected bool, found float64"), 577 578 newTypeTestCase("obj from args", "config.define_object('foo')"). 579 withArgs(`--foo`, `["a", "b", "c"]`). 580 withExpectedVal(`["a", "b", "c"]`), 581 582 newTypeTestCase("obj from config", "config.define_object('foo')"). 583 withConfigFile(`{"foo": ["a", "b", "c"]}`). 584 withExpectedVal(`["a", "b", "c"]`), 585 } { 586 t.Run(tc.name, func(t *testing.T) { 587 f := NewFixture(t, tc.args, "") 588 589 tf := fmt.Sprintf(` 590 %s 591 592 cfg = config.parse() 593 `, tc.define) 594 if tc.expectedVal != "" { 595 tf += fmt.Sprintf(` 596 observed = cfg['foo'] 597 expected = %s 598 599 def test(): 600 if expected != observed: 601 print('expected: %%s' %% expected) 602 print('observed: %%s' %% observed) 603 fail('did not get expected value out of config') 604 605 test() 606 `, tc.expectedVal) 607 } 608 f.File("Tiltfile", tf) 609 610 if tc.configFile != "" { 611 f.File("tilt_config.json", tc.configFile) 612 } 613 614 _, err := f.ExecFile("Tiltfile") 615 if tc.expectedError == "" { 616 require.NoError(t, err) 617 } else { 618 require.Error(t, err) 619 require.Contains(t, err.Error(), tc.expectedError) 620 } 621 }) 622 } 623 624 }