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  }