github.com/grailbio/base@v0.0.11/config/profile_test.go (about)

     1  // Copyright 2019 GRAIL, Inc. All rights reserved.
     2  // Use of this source code is governed by the Apache 2.0
     3  // license that can be found in the LICENSE file.
     4  
     5  package config
     6  
     7  import (
     8  	"strings"
     9  	"testing"
    10  )
    11  
    12  type custom struct {
    13  	x int
    14  	f float64
    15  }
    16  
    17  // paramFields is a convenient structure for testing that has fields of various
    18  // types that we use to test parameter handling.
    19  type paramFields struct {
    20  	c  custom
    21  	p  *custom
    22  	ch chan struct{}
    23  	a  any
    24  }
    25  
    26  func init() {
    27  	Register("test/custom", func(inst *Constructor[custom]) {
    28  		var c custom
    29  		inst.IntVar(&c.x, "x", -1, "the x value")
    30  		inst.FloatVar(&c.f, "f", 0, "the f value")
    31  		inst.New = func() (custom, error) {
    32  			return c, nil
    33  		}
    34  	})
    35  
    36  	Default("test/default", "test/custom")
    37  
    38  	Default("test/default2", "test/default")
    39  
    40  	Register("test/custom-ptr", func(inst *Constructor[*custom]) {
    41  		var c custom
    42  		inst.IntVar(&c.x, "x", -1, "the x value")
    43  		inst.New = func() (*custom, error) {
    44  			return &c, nil
    45  		}
    46  	})
    47  
    48  	Register("test/1", func(inst *Constructor[int]) {
    49  		var c custom
    50  		inst.InstanceVar(&c, "custom", "test/default", "the custom struct")
    51  		x := inst.Int("x", 123, "the x value")
    52  		inst.New = func() (int, error) {
    53  			return *x + c.x, nil
    54  		}
    55  	})
    56  
    57  	Register("test/custom-nil", func(inst *Constructor[*custom]) {
    58  		inst.New = func() (*custom, error) {
    59  			return (*custom)(nil), nil
    60  		}
    61  	})
    62  
    63  	Default("test/default-custom-nil", "test/custom-nil")
    64  
    65  	Register("test/untyped-nil", func(inst *Constructor[any]) {
    66  		inst.New = func() (any, error) {
    67  			return nil, nil
    68  		}
    69  	})
    70  
    71  	Default("test/default-untyped-nil", "test/untyped-nil")
    72  
    73  	Register("test/params/empty", func(inst *Constructor[paramFields]) {
    74  		var pf paramFields
    75  		inst.InstanceVar(&pf.p, "p", "test/custom-nil", "")
    76  		inst.InstanceVar(&pf.ch, "ch", "", "")
    77  		inst.InstanceVar(&pf.a, "a", "", "")
    78  		inst.New = func() (paramFields, error) {
    79  			return pf, nil
    80  		}
    81  	})
    82  
    83  	Register("test/params/nil", func(inst *Constructor[paramFields]) {
    84  		var pf paramFields
    85  		inst.InstanceVar(&pf.p, "p", "nil", "")
    86  		inst.InstanceVar(&pf.ch, "ch", "", "")
    87  		inst.InstanceVar(&pf.a, "a", "nil", "")
    88  		inst.New = func() (paramFields, error) {
    89  			return pf, nil
    90  		}
    91  	})
    92  
    93  	Register("test/params/nil-instance", func(inst *Constructor[paramFields]) {
    94  		var pf paramFields
    95  		inst.InstanceVar(&pf.p, "p", "test/custom-nil", "")
    96  		inst.New = func() (paramFields, error) {
    97  			return pf, nil
    98  		}
    99  	})
   100  
   101  	Register("test/params/empty-non-nilable-recovered", func(inst *Constructor[any]) {
   102  		var r any
   103  		func() {
   104  			defer func() {
   105  				r = recover()
   106  			}()
   107  			var pf paramFields
   108  			inst.InstanceVar(&pf.c, "c", "", "")
   109  		}()
   110  		inst.New = func() (any, error) {
   111  			return r, nil
   112  		}
   113  	})
   114  
   115  	Register("test/params/nil-non-nilable-recovered", func(inst *Constructor[any]) {
   116  		var r any
   117  		func() {
   118  			defer func() {
   119  				r = recover()
   120  			}()
   121  			var pf paramFields
   122  			inst.InstanceVar(&pf.c, "c", "nil", "")
   123  		}()
   124  		inst.New = func() (any, error) {
   125  			return r, nil
   126  		}
   127  	})
   128  
   129  	Register("test/chan", func(inst *Constructor[chan struct{}]) {
   130  		inst.New = func() (chan struct{}, error) {
   131  			return make(chan struct{}), nil
   132  		}
   133  	})
   134  
   135  	Register("test/params/non-nil", func(inst *Constructor[paramFields]) {
   136  		var pf paramFields
   137  		inst.InstanceVar(&pf.c, "c", "test/custom", "")
   138  		inst.InstanceVar(&pf.p, "p", "test/custom-ptr", "")
   139  		inst.InstanceVar(&pf.ch, "ch", "test/chan", "")
   140  		inst.InstanceVar(&pf.a, "a", "test/custom", "")
   141  		inst.New = func() (paramFields, error) {
   142  			return pf, nil
   143  		}
   144  	})
   145  }
   146  
   147  func TestProfileParamDefault(t *testing.T) {
   148  	p := New()
   149  	var x int
   150  	if err := p.Instance("test/1", &x); err != nil {
   151  		t.Fatal(err)
   152  	}
   153  	if got, want := x, 122; got != want {
   154  		t.Errorf("got %v, want %v", got, want)
   155  	}
   156  
   157  	p = New()
   158  	if err := p.Set("test/custom.x", "-100"); err != nil {
   159  		t.Fatal(err)
   160  	}
   161  	if err := p.Instance("test/1", &x); err != nil {
   162  		t.Fatal(err)
   163  	}
   164  	if got, want := x, 23; got != want {
   165  		t.Errorf("got %v, want %v", got, want)
   166  	}
   167  }
   168  
   169  func TestProfileDefaultInstance(t *testing.T) {
   170  	t.Run("basic", func(t *testing.T) {
   171  		p := New()
   172  		if err := p.Instance("test/default", nil); err != nil {
   173  			t.Fatal(err)
   174  		}
   175  	})
   176  	t.Run("override-default", func(t *testing.T) {
   177  		p := New()
   178  		if err := p.Parse(strings.NewReader(`
   179  			instance custom13 test/custom (
   180  				x = 13
   181  			)
   182  			instance test/default custom13
   183  		`)); err != nil {
   184  			t.Fatal(err)
   185  		}
   186  		var c custom
   187  		if err := p.Instance("test/default", &c); err != nil {
   188  			t.Fatal(err)
   189  		}
   190  		if got, want := c.x, 13; got != want {
   191  			t.Errorf("got %v, want %v", got, want)
   192  		}
   193  		if err := p.Instance("test/default2", &c); err != nil {
   194  			t.Fatal(err)
   195  		}
   196  		if got, want := c.x, 13; got != want {
   197  			t.Errorf("got %v, want %v", got, want)
   198  		}
   199  	})
   200  	t.Run("override-second-default", func(t *testing.T) {
   201  		p := New()
   202  		if err := p.Parse(strings.NewReader(`
   203  			instance custom13 test/custom (
   204  				x = 13
   205  			)
   206  			instance test/default2 custom13
   207  		`)); err != nil {
   208  			t.Fatal(err)
   209  		}
   210  		var c custom
   211  		if err := p.Instance("test/default", &c); err != nil {
   212  			t.Fatal(err)
   213  		}
   214  		if got, want := c.x, -1; got != want {
   215  			t.Errorf("got %v, want %v", got, want)
   216  		}
   217  		if err := p.Instance("test/default2", &c); err != nil {
   218  			t.Fatal(err)
   219  		}
   220  		if got, want := c.x, 13; got != want {
   221  			t.Errorf("got %v, want %v", got, want)
   222  		}
   223  	})
   224  	t.Run("override-defaults-differently", func(t *testing.T) {
   225  		p := New()
   226  		if err := p.Parse(strings.NewReader(`
   227  			instance custom13 test/custom (
   228  				x = 13
   229  			)
   230  			instance custom132 test/custom (
   231  				x = 132
   232  			)
   233  			instance test/default custom13
   234  			instance test/default2 custom132
   235  		`)); err != nil {
   236  			t.Fatal(err)
   237  		}
   238  		var c custom
   239  		if err := p.Instance("test/default", &c); err != nil {
   240  			t.Fatal(err)
   241  		}
   242  		if got, want := c.x, 13; got != want {
   243  			t.Errorf("got %v, want %v", got, want)
   244  		}
   245  		if err := p.Instance("test/default2", &c); err != nil {
   246  			t.Fatal(err)
   247  		}
   248  		if got, want := c.x, 132; got != want {
   249  			t.Errorf("got %v, want %v", got, want)
   250  		}
   251  	})
   252  	t.Run("override-default-instance-with-param", func(t *testing.T) {
   253  		t.Skip() // BXDS-2886
   254  		p := New()
   255  		if err := p.Parse(strings.NewReader(`
   256  			instance test/default test/custom (
   257  				x = 13
   258  			)
   259  		`)); err != nil {
   260  			t.Fatal(err)
   261  		}
   262  		var c custom
   263  		if err := p.Instance("test/default", &c); err != nil {
   264  			t.Fatal(err)
   265  		}
   266  		if got, want := c.x, 13; got != want {
   267  			t.Errorf("got %v, want %v", got, want)
   268  		}
   269  		if err := p.Instance("test/default2", &c); err != nil {
   270  			t.Fatal(err)
   271  		}
   272  		if got, want := c.x, 13; got != want {
   273  			t.Errorf("got %v, want %v", got, want)
   274  		}
   275  	})
   276  	t.Run("set-default", func(t *testing.T) {
   277  		p := New()
   278  		if err := p.Parse(strings.NewReader(`
   279  			instance custom13 test/custom (
   280  				x = 13
   281  			)
   282  		`)); err != nil {
   283  			t.Fatal(err)
   284  		}
   285  		if err := p.Set("test/default", "custom13"); err != nil {
   286  			t.Fatal(err)
   287  		}
   288  		var c custom
   289  		if err := p.Instance("test/default", &c); err != nil {
   290  			t.Fatal(err)
   291  		}
   292  		if got, want := c.x, 13; got != want {
   293  			t.Errorf("got %v, want %v", got, want)
   294  		}
   295  		if err := p.Instance("test/default2", &c); err != nil {
   296  			t.Fatal(err)
   297  		}
   298  		if got, want := c.x, 13; got != want {
   299  			t.Errorf("got %v, want %v", got, want)
   300  		}
   301  	})
   302  	t.Run("set-second-default", func(t *testing.T) {
   303  		p := New()
   304  		if err := p.Parse(strings.NewReader(`
   305  			instance custom13 test/custom (
   306  				x = 13
   307  			)
   308  		`)); err != nil {
   309  			t.Fatal(err)
   310  		}
   311  		if err := p.Set("test/default2", "custom13"); err != nil {
   312  			t.Fatal(err)
   313  		}
   314  		var c custom
   315  		if err := p.Instance("test/default", &c); err != nil {
   316  			t.Fatal(err)
   317  		}
   318  		if got, want := c.x, -1; got != want {
   319  			t.Errorf("got %v, want %v", got, want)
   320  		}
   321  		if err := p.Instance("test/default2", &c); err != nil {
   322  			t.Fatal(err)
   323  		}
   324  		if got, want := c.x, 13; got != want {
   325  			t.Errorf("got %v, want %v", got, want)
   326  		}
   327  	})
   328  	t.Run("set-defaults-differently", func(t *testing.T) {
   329  		p := New()
   330  		if err := p.Parse(strings.NewReader(`
   331  			instance custom13 test/custom (
   332  				x = 13
   333  			)
   334  			instance custom132 test/custom (
   335  				x = 132
   336  			)
   337  			instance test/default custom13
   338  			instance test/default2 custom132
   339  		`)); err != nil {
   340  			t.Fatal(err)
   341  		}
   342  		if err := p.Set("test/default", "custom13"); err != nil {
   343  			t.Fatal(err)
   344  		}
   345  		if err := p.Set("test/default2", "custom132"); err != nil {
   346  			t.Fatal(err)
   347  		}
   348  		var c custom
   349  		if err := p.Instance("test/default", &c); err != nil {
   350  			t.Fatal(err)
   351  		}
   352  		if got, want := c.x, 13; got != want {
   353  			t.Errorf("got %v, want %v", got, want)
   354  		}
   355  		if err := p.Instance("test/default2", &c); err != nil {
   356  			t.Fatal(err)
   357  		}
   358  		if got, want := c.x, 132; got != want {
   359  			t.Errorf("got %v, want %v", got, want)
   360  		}
   361  	})
   362  }
   363  
   364  func TestProfile(t *testing.T) {
   365  	p := New()
   366  	err := p.Parse(strings.NewReader(`
   367  param test/custom (
   368  	x = 999
   369  )
   370  
   371  param test/1 (
   372  	custom = test/custom
   373  	x = 1
   374  )
   375  
   376  instance testx test/1 (
   377  	x = 100
   378  )
   379  
   380  instance testf test/custom (
   381  	f = 1
   382  )
   383  
   384  instance test/default testf
   385  `))
   386  	if err != nil {
   387  		t.Fatal(err)
   388  	}
   389  
   390  	var x int
   391  	if err = p.Instance("test/1", &x); err != nil {
   392  		t.Fatal(err)
   393  	}
   394  	if got, want := x, 1000; got != want {
   395  		t.Errorf("got %v, want %v", got, want)
   396  	}
   397  
   398  	if err = p.Instance("testx", &x); err != nil {
   399  		t.Fatal(err)
   400  	}
   401  	if got, want := x, 1099; got != want {
   402  		t.Errorf("got %v, want %v", got, want)
   403  	}
   404  
   405  	var str string
   406  	err = p.Instance("testx", &str)
   407  	if err == nil || !strings.Contains(err.Error(), "instance \"testx\" of type int is not assignable to provided pointer element type string") {
   408  		t.Error(err)
   409  	}
   410  
   411  	var c custom
   412  	if err = p.Instance("testf", &c); err != nil {
   413  		t.Fatal(err)
   414  	}
   415  	if got, want := c.f, 1.; got != want {
   416  		t.Errorf("got %v, want %v", got, want)
   417  	}
   418  
   419  	// Verify that test/default derives from testf.
   420  	if err = p.Instance("test/default", &c); err != nil {
   421  		t.Fatal(err)
   422  	}
   423  	if got, want := c.f, 1.; got != want {
   424  		t.Errorf("got %v, want %v", got, want)
   425  	}
   426  }
   427  
   428  // TestNilInstances verifies that we handle nil/empty instances appropriately.
   429  func TestNilInstances(t *testing.T) {
   430  	var (
   431  		mustSet = func(p *Profile, path, value string) {
   432  			t.Helper()
   433  			if err := p.Set(path, value); err != nil {
   434  				t.Error(err)
   435  			}
   436  		}
   437  		mustInstance = func(p *Profile, name string, pa any) {
   438  			t.Helper()
   439  			if err := p.Instance(name, pa); err != nil {
   440  				t.Fatal(err)
   441  			}
   442  		}
   443  		mustEqual = func(got, want any) {
   444  			t.Helper()
   445  			if got != want {
   446  				t.Errorf("got %v, want %v", got, want)
   447  			}
   448  		}
   449  	)
   450  
   451  	var (
   452  		p  *Profile
   453  		pc *custom
   454  		a  any
   455  		pf paramFields
   456  	)
   457  
   458  	// Verify that top-level instances can be nil.
   459  	p = New()
   460  	mustInstance(p, "test/custom-nil", &pc)
   461  	mustEqual(pc, (*custom)(nil))
   462  	mustInstance(p, "test/default-custom-nil", &pc)
   463  	mustEqual(pc, (*custom)(nil))
   464  	mustInstance(p, "test/untyped-nil", &a)
   465  	mustEqual(a, nil)
   466  	mustInstance(p, "test/default-untyped-nil", &a)
   467  	mustEqual(a, nil)
   468  
   469  	// Verify that empty InstanceVar defaults produce nil parameters.
   470  	p = New()
   471  	mustInstance(p, "test/params/empty", &pf)
   472  	mustEqual(pf.p, (*custom)(nil))
   473  	mustEqual(pf.ch, (chan struct{})(nil))
   474  	mustEqual(pf.a, nil)
   475  
   476  	// Verify that nil InstanceVar defaults produce nil parameters.
   477  	p = New()
   478  	mustInstance(p, "test/params/nil", &pf)
   479  	mustEqual(pf.p, (*custom)(nil))
   480  	mustEqual(pf.ch, (chan struct{})(nil))
   481  	mustEqual(pf.a, nil)
   482  
   483  	// Verify that an InstanceVar default instance whose value is nil produces
   484  	// a nil parameter.
   485  	p = New()
   486  	mustInstance(p, "test/params/nil-instance", &pf)
   487  	mustEqual(pf.p, (*custom)(nil))
   488  
   489  	// Verify that InstanceVar panics setting an empty value default for an
   490  	// element type that cannot be assigned nil.
   491  	p = New()
   492  	// Set c to a valid instance, so that the invalid instance error does not
   493  	// obscure the recovered panic value.
   494  	mustSet(p, "test/params/empty-non-nilable-recovered.c", "test/custom")
   495  	mustInstance(p, "test/params/empty-non-nilable-recovered", &a)
   496  	if a == nil {
   497  		t.Error("expected non-nil-assignable empty default instance to panic")
   498  	}
   499  
   500  	// Verify that InstanceVar panics setting a nil value default for an
   501  	// element type that cannot be assigned nil.
   502  	p = New()
   503  	// Set c to a valid instance, so that the invalid instance error does not
   504  	// obscure the recovered panic value.
   505  	mustSet(p, "test/params/nil-non-nilable-recovered.c", "test/custom")
   506  	mustInstance(p, "test/params/nil-non-nilable-recovered", &a)
   507  	if a == nil {
   508  		t.Error("expected non-nil-assignable nil default instance to panic")
   509  	}
   510  
   511  	// Verify that a non-nil-assignable parameter set to an empty instance is
   512  	// invalid.
   513  	p = New()
   514  	mustSet(p, "test/params/non-nil.c", "")
   515  	if err := p.Instance("test/params/non-nil", &pf); err == nil {
   516  		t.Error("non-nil-assignable set to empty instance should return non-nil error")
   517  	}
   518  
   519  	// Verify that a non-nil-assignable parameter set to nil is invalid.
   520  	p = New()
   521  	mustSet(p, "test/params/non-nil.c", "nil")
   522  	if err := p.Instance("test/params/non-nil", &pf); err == nil {
   523  		t.Error("non-nil-assignable set to nil should return non-nil error")
   524  	}
   525  
   526  	// Verify that nil-assignable parameters can be set to empty, resulting in
   527  	// nil parameter values.
   528  	p = New()
   529  	mustSet(p, "test/params/non-nil.p", "")
   530  	mustSet(p, "test/params/non-nil.ch", "")
   531  	mustSet(p, "test/params/non-nil.a", "")
   532  	mustInstance(p, "test/params/non-nil", &pf)
   533  	mustEqual(pf.p, (*custom)(nil))
   534  	mustEqual(pf.ch, (chan struct{})(nil))
   535  	mustEqual(pf.a, nil)
   536  
   537  	// Verify that nil-assignable parameters can be set to nil, resulting in
   538  	// nil parameter values.
   539  	p = New()
   540  	mustSet(p, "test/params/non-nil.p", "nil")
   541  	mustSet(p, "test/params/non-nil.ch", "nil")
   542  	mustSet(p, "test/params/non-nil.a", "nil")
   543  	mustInstance(p, "test/params/non-nil", &pf)
   544  	mustEqual(pf.p, (*custom)(nil))
   545  	mustEqual(pf.ch, (chan struct{})(nil))
   546  	mustEqual(pf.a, nil)
   547  
   548  	// Verify that a nil-assignable parameter can be set to an instance whose
   549  	// value is nil, resulting in a nil parameter value.
   550  	p = New()
   551  	mustSet(p, "test/params/non-nil.p", "test/custom-nil")
   552  	mustInstance(p, "test/params/non-nil", &pf)
   553  	mustEqual(pf.p, (*custom)(nil))
   554  
   555  	// Verify that top-level instances cannot be set to empty.
   556  	p = New()
   557  	if err := p.Set("test/custom", ""); err == nil {
   558  		t.Error("top-level instance set to empty should return non-nil error")
   559  	}
   560  
   561  	// Verify that top-level instances cannot be set to nil.
   562  	p = New()
   563  	if err := p.Set("test/custom", ""); err == nil {
   564  		t.Error("top-level instance set to nil should return non-nil error")
   565  	}
   566  }
   567  
   568  func TestSetGet(t *testing.T) {
   569  	p := New()
   570  	err := p.Parse(strings.NewReader(`
   571  param test/custom (
   572  	x = 999
   573  )
   574  
   575  param test/1 (
   576  	custom = test/custom
   577  	x = 1
   578  )
   579  
   580  instance testx test/1 (
   581  	x = 100
   582  )
   583  
   584  instance testy test/1
   585  
   586  `))
   587  	if err != nil {
   588  		t.Fatal(err)
   589  	}
   590  
   591  	var (
   592  		mustGet = func(k, want string) {
   593  			t.Helper()
   594  			got, ok := p.Get(k)
   595  			if !ok {
   596  				t.Fatalf("key %v not found", k)
   597  			}
   598  			if got != want {
   599  				t.Fatalf("key %v: got %v, want %v", k, got, want)
   600  			}
   601  		}
   602  		mustSet = func(k, v string) {
   603  			t.Helper()
   604  			if err := p.Set(k, v); err != nil {
   605  				t.Fatalf("set %v %v: %v", k, v, err)
   606  			}
   607  		}
   608  	)
   609  
   610  	mustGet("testy", "test/1")
   611  	mustGet("test/1.x", "1")
   612  	mustGet("testx.x", "100")
   613  	mustGet("testx.custom", "test/custom")
   614  	mustGet("testx.custom.x", "999")
   615  	mustGet("testy.x", "1")
   616  
   617  	mustSet("testx.custom.x", "1900")
   618  	mustGet("testx.custom.x", "1900")
   619  	mustSet("testx.custom.x", "-1900")
   620  	mustGet("testx.custom.x", "-1900")
   621  	mustSet("testx.custom.f", "3.14")
   622  	mustGet("testx.custom.f", "3.14")
   623  	mustSet("testx.custom.f", "-3.14")
   624  	mustGet("testx.custom.f", "-3.14")
   625  
   626  	mustSet("testy", "testx")
   627  	mustGet("testy.x", "100")
   628  
   629  }
   630  
   631  // TestInstanceNames verifies that InstanceNames returns the correct set of
   632  // instance names.
   633  func TestInstanceNames(t *testing.T) {
   634  	p := New()
   635  	names := p.InstanceNames()
   636  	// Because global instances can be added from anywhere, we only verify that
   637  	// the returned names contains the instances added by this file.
   638  	for _, name := range []string{
   639  		"test/1",
   640  		"test/custom",
   641  		"test/default",
   642  	} {
   643  		if _, ok := names[name]; !ok {
   644  			t.Errorf("missing instance name=%v", name)
   645  		}
   646  	}
   647  }