diff --git a/xload/async.go b/xload/async.go index 46ac31a..72b4a0e 100644 --- a/xload/async.go +++ b/xload/async.go @@ -13,7 +13,31 @@ import ( type loadAndSet func(context.Context, reflect.Value) error type loadAndSetPointer func(context.Context, reflect.Value, reflect.Value, bool) error -func processConcurrently(ctx context.Context, v any, opts *options) error { +func processConcurrently(ctx context.Context, v any, o *options) error { + if !o.detectCollisions { + return doProcessConcurrently(ctx, v, o) + } + + syncKeyUsage := &collisionSyncMap{} + ldr := o.loader + o.loader = LoaderFunc(func(ctx context.Context, key string) (string, error) { + v, err := ldr.Load(ctx, key) + + if err == nil { + syncKeyUsage.add(key) + } + + return v, err + }) + + if err := doProcessConcurrently(ctx, v, o); err != nil { + return err + } + + return syncKeyUsage.err() +} + +func doProcessConcurrently(ctx context.Context, v any, opts *options) error { doneCh := make(chan struct{}, 1) defer close(doneCh) diff --git a/xload/collision.go b/xload/collision.go new file mode 100644 index 0000000..35e3b46 --- /dev/null +++ b/xload/collision.go @@ -0,0 +1,63 @@ +package xload + +import ( + "sync" +) + +type collisionSyncMap sync.Map + +func (cm *collisionSyncMap) add(key string) { + m := (*sync.Map)(cm) + v, loaded := m.LoadOrStore(key, 1) + + if loaded { + m.Store(key, v.(int)+1) + } +} + +func (cm *collisionSyncMap) err() error { + var collidedKeys []string + + m := (*sync.Map)(cm) + m.Range(func(key, v any) bool { + if key == "" { + return true + } + + if count, _ := v.(int); count > 1 { + collidedKeys = append(collidedKeys, key.(string)) + } + + return true + }) + + return keysToErr(collidedKeys) +} + +type collisionMap map[string]int + +func (cm collisionMap) add(key string) { cm[key]++ } + +func (cm collisionMap) err() error { + var collidedKeys []string + + for key, count := range cm { + if key == "" { + continue + } + + if count > 1 { + collidedKeys = append(collidedKeys, key) + } + } + + return keysToErr(collidedKeys) +} + +func keysToErr(collidedKeys []string) error { + if len(collidedKeys) == 0 { + return nil + } + + return &ErrCollision{keys: collidedKeys} +} diff --git a/xload/collision_test.go b/xload/collision_test.go new file mode 100644 index 0000000..6c31d9c --- /dev/null +++ b/xload/collision_test.go @@ -0,0 +1,33 @@ +package xload + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_collisionSyncMap_err(t *testing.T) { + tests := []struct { + name string + cm func() *collisionSyncMap + wantErr assert.ErrorAssertionFunc + }{ + { + name: "empty keys", + cm: func() *collisionSyncMap { + m := &collisionSyncMap{} + m.add("") + m.add("") + m.add("") + return m + }, + wantErr: assert.NoError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.wantErr(t, tt.cm().err(), "err()") + }) + } +} diff --git a/xload/errors.go b/xload/errors.go index 1dc79ec..cd43287 100644 --- a/xload/errors.go +++ b/xload/errors.go @@ -61,3 +61,19 @@ type ErrInvalidPrefixAndKey struct { func (e ErrInvalidPrefixAndKey) Error() string { return fmt.Sprintf("`%s` key=%s has both prefix and key", e.field, e.key) } + +// ErrCollision is returned when key collisions are detected. +// Collision can happen when two or more fields have the same full key. +type ErrCollision struct{ keys []string } + +func (e *ErrCollision) Error() string { + return fmt.Sprintf("xload: key collisions detected for keys: %v", e.keys) +} + +// Keys returns the collided keys. +func (e *ErrCollision) Keys() []string { + keysCopy := make([]string, len(e.keys)) + copy(keysCopy, e.keys) + + return keysCopy +} diff --git a/xload/errors_test.go b/xload/errors_test.go index 1b4018c..21be574 100644 --- a/xload/errors_test.go +++ b/xload/errors_test.go @@ -36,3 +36,14 @@ func TestErrUnknownTagOption_Error(t *testing.T) { }) } } + +func TestErrCollision(t *testing.T) { + ks := []string{ + "KEY_A", + "KEY_B", + } + err := &ErrCollision{keys: ks} + + assert.ElementsMatch(t, ks, err.Keys()) + assert.Equal(t, "xload: key collisions detected for keys: [KEY_A KEY_B]", err.Error()) +} diff --git a/xload/example_test.go b/xload/example_test.go index e000b39..db81b1d 100644 --- a/xload/example_test.go +++ b/xload/example_test.go @@ -306,3 +306,23 @@ func ExampleLoad_extendingStructs() { panic(err) } } + +func ExampleLoad_skipCollisionDetection() { + type AppConf struct { + Host string `env:"HOST"` + // Hostname is also loaded from HOST, and no error is returned + Hostname string `env:"HOST"` + Timeout time.Duration `env:"TIMEOUT"` + } + + var conf AppConf + + err := xload.Load( + context.Background(), + &conf, + xload.SkipCollisionDetection, + ) + if err != nil { + panic(err) + } +} diff --git a/xload/load.go b/xload/load.go index 6496301..25f438e 100644 --- a/xload/load.go +++ b/xload/load.go @@ -41,11 +41,34 @@ func Load(ctx context.Context, v any, opts ...Option) error { return processConcurrently(ctx, v, o) } - return process(ctx, v, o.tagName, o.loader) + return process(ctx, v, o) +} + +func process(ctx context.Context, v any, o *options) error { + if !o.detectCollisions { + return doProcess(ctx, v, o.tagName, o.loader) + } + + keyUsage := make(collisionMap) + loaderWithKeyUsage := LoaderFunc(func(ctx context.Context, key string) (string, error) { + v, err := o.loader.Load(ctx, key) + + if err == nil { + keyUsage.add(key) + } + + return v, err + }) + + if err := doProcess(ctx, v, o.tagName, loaderWithKeyUsage); err != nil { + return err + } + + return keyUsage.err() } //nolint:funlen,nestif -func process(ctx context.Context, obj any, tagKey string, loader Loader) error { +func doProcess(ctx context.Context, obj any, tagKey string, loader Loader) error { v := reflect.ValueOf(obj) if v.Kind() != reflect.Ptr { @@ -141,7 +164,7 @@ func process(ctx context.Context, obj any, tagKey string, loader Loader) error { pld = PrefixLoader(meta.prefix, loader) } - err := process(ctx, fVal.Interface(), tagKey, pld) + err := doProcess(ctx, fVal.Interface(), tagKey, pld) if err != nil { return err } diff --git a/xload/load_struct_test.go b/xload/load_struct_test.go index b4884a0..f06572a 100644 --- a/xload/load_struct_test.go +++ b/xload/load_struct_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/gotidy/ptr" + "github.com/stretchr/testify/assert" ) type House struct { @@ -175,16 +176,72 @@ func TestLoad_Structs(t *testing.T) { input: &struct { Name string `env:",prefix=CLUSTER"` }{}, - err: &ErrInvalidPrefix{field: "Name", kind: strKind}, - loader: MapLoader{}, + wantErr: errContains(&ErrInvalidPrefix{field: "Name", kind: strKind}), + loader: MapLoader{}, }, { name: "struct with key and prefix", input: &struct { Address Address `env:"ADDRESS,prefix=CLUSTER"` }{}, - err: &ErrInvalidPrefixAndKey{field: "Address", key: "ADDRESS"}, - loader: MapLoader{}, + wantErr: errContains(&ErrInvalidPrefixAndKey{field: "Address", key: "ADDRESS"}), + loader: MapLoader{}, + }, + + // key collision + { + name: "key collision", + input: &struct { + Address1 Address `env:",prefix=ADDRESS_"` + Address2 *Address `env:",prefix=ADDRESS_"` + }{}, + wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + tErr := &ErrCollision{} + assert.ErrorAs(t, err, &tErr) + return assert.ElementsMatch(t, tErr.Keys(), []string{ + "ADDRESS_CITY", + "ADDRESS_LATITUDE", + "ADDRESS_LONGITUTE", + "ADDRESS_STREET", + }) + }, + loader: MapLoader{ + "ADDRESS_STREET": "street1", + "ADDRESS_CITY": "city1", + "ADDRESS_LONGITUTE": "1.1", + "ADDRESS_LATITUDE": "-2.2", + }, + }, + { + name: "key collision with detection disabled", + opts: []Option{SkipCollisionDetection}, + input: &struct { + Address1 Address `env:",prefix=ADDRESS_"` + Address2 *Address `env:",prefix=ADDRESS_"` + }{}, + want: &struct { + Address1 Address + Address2 *Address + }{ + Address{ + Street: "street1", + City: "city1", + Longitute: ptr.Float64(1.1), + Latitude: ptr.Float64(-2.2), + }, + &Address{ + Street: "street1", + City: "city1", + Longitute: ptr.Float64(1.1), + Latitude: ptr.Float64(-2.2), + }, + }, + loader: MapLoader{ + "ADDRESS_STREET": "street1", + "ADDRESS_CITY": "city1", + "ADDRESS_LONGITUTE": "1.1", + "ADDRESS_LATITUDE": "-2.2", + }, }, } @@ -219,8 +276,8 @@ func TestLoad_Decoder(t *testing.T) { input: &struct { Time time.Time `env:"TIME"` }{}, - loader: MapLoader{"TIME": "invalid"}, - err: errors.New("cannot parse"), + loader: MapLoader{"TIME": "invalid"}, + wantErr: errContains(errors.New("cannot parse")), }, } @@ -303,15 +360,15 @@ func TestLoad_JSON(t *testing.T) { input: &struct { Plot Plot `env:"PLOT"` }{}, - err: errors.New("invalid character"), - loader: MapLoader{"PLOT": `invalid`}, + wantErr: errContains(errors.New("invalid character")), + loader: MapLoader{"PLOT": `invalid`}, }, { name: "json: loader error", input: &struct { Plot Plot `env:"PLOT"` }{}, - err: errors.New("loader error"), + wantErr: errContains(errors.New("loader error")), loader: LoaderFunc(func(ctx context.Context, key string) (string, error) { return "", errors.New("loader error") }), @@ -321,8 +378,8 @@ func TestLoad_JSON(t *testing.T) { input: &struct { Plot Plot `env:"PLOT,required"` }{}, - err: &ErrRequired{key: "PLOT"}, - loader: MapLoader{}, + wantErr: errContains(&ErrRequired{key: "PLOT"}), + loader: MapLoader{}, }, } diff --git a/xload/load_test.go b/xload/load_test.go index 1db4855..c686da2 100644 --- a/xload/load_test.go +++ b/xload/load_test.go @@ -14,11 +14,18 @@ import ( ) type testcase struct { - name string - input any - want any - loader Loader - err error + name string + input any + want any + loader Loader + opts []Option + wantErr assert.ErrorAssertionFunc +} + +func errContains(want error) assert.ErrorAssertionFunc { + return func(t assert.TestingT, err error, msgArgs ...interface{}) bool { + return assert.ErrorContains(t, err, want.Error(), msgArgs...) + } } func TestLoad_Default(t *testing.T) { @@ -48,16 +55,16 @@ func TestLoad_Errors(t *testing.T) { input: struct { Host string `env:"HOST"` }{}, - loader: MapLoader{}, - err: ErrNotPointer, + loader: MapLoader{}, + wantErr: errContains(ErrNotPointer), }, // not a struct { - name: "not a struct", - input: ptr.String(""), - loader: MapLoader{}, - err: ErrNotStruct, + name: "not a struct", + input: ptr.String(""), + loader: MapLoader{}, + wantErr: errContains(ErrNotStruct), }, // private fields @@ -105,7 +112,7 @@ func TestLoad_Errors(t *testing.T) { loader: LoaderFunc(func(ctx context.Context, k string) (string, error) { return "", errors.New("loader error") }), - err: errors.New("loader error"), + wantErr: errContains(errors.New("loader error")), }, // unknown tag option @@ -114,8 +121,8 @@ func TestLoad_Errors(t *testing.T) { input: &struct { Host string `env:"HOST,unknown"` }{}, - loader: MapLoader{}, - err: &ErrUnknownTagOption{key: "HOST", opt: "unknown"}, + loader: MapLoader{}, + wantErr: errContains(&ErrUnknownTagOption{key: "HOST", opt: "unknown"}), }, } @@ -158,8 +165,8 @@ func TestLoad_NativeTypes(t *testing.T) { input: &struct { Bool bool `env:"BOOL"` }{}, - loader: MapLoader{"BOOL": "invalid"}, - err: errors.New("invalid syntax"), + loader: MapLoader{"BOOL": "invalid"}, + wantErr: errContains(errors.New("invalid syntax")), }, // integer values @@ -198,8 +205,8 @@ func TestLoad_NativeTypes(t *testing.T) { input: &struct { Int int `env:"INT"` }{}, - loader: MapLoader{"INT": "invalid"}, - err: errors.New("unable to cast"), + loader: MapLoader{"INT": "invalid"}, + wantErr: errContains(errors.New("unable to cast")), }, // unsigned integer values @@ -238,8 +245,8 @@ func TestLoad_NativeTypes(t *testing.T) { input: &struct { Uint uint `env:"UINT"` }{}, - loader: MapLoader{"UINT": "invalid"}, - err: errors.New("unable to cast"), + loader: MapLoader{"UINT": "invalid"}, + wantErr: errContains(errors.New("unable to cast")), }, // floating-point values @@ -266,8 +273,8 @@ func TestLoad_NativeTypes(t *testing.T) { input: &struct { Float float32 `env:"FLOAT"` }{}, - loader: MapLoader{"FLOAT": "invalid"}, - err: errors.New("unable to cast"), + loader: MapLoader{"FLOAT": "invalid"}, + wantErr: errContains(errors.New("unable to cast")), }, // duration values @@ -294,8 +301,8 @@ func TestLoad_NativeTypes(t *testing.T) { input: &struct { Duration time.Duration `env:"DURATION"` }{}, - loader: MapLoader{"DURATION": "invalid"}, - err: errors.New("invalid duration"), + loader: MapLoader{"DURATION": "invalid"}, + wantErr: errContains(errors.New("invalid duration")), }, // string values @@ -374,8 +381,8 @@ func TestLoad_NativeTypes(t *testing.T) { input: &struct { Int64Slice []int64 `env:"INT64_SLICE"` }{}, - loader: MapLoader{"INT64_SLICE": "invalid,2"}, - err: errors.New("unable to cast"), + loader: MapLoader{"INT64_SLICE": "invalid,2"}, + wantErr: errContains(errors.New("unable to cast")), }, // map values @@ -410,24 +417,24 @@ func TestLoad_NativeTypes(t *testing.T) { input: &struct { StringMap map[string]string `env:"STRING_MAP"` }{}, - loader: MapLoader{"STRING_MAP": "key1::value1,key2::value2"}, - err: &ErrInvalidMapValue{key: "STRING_MAP"}, + loader: MapLoader{"STRING_MAP": "key1::value1,key2::value2"}, + wantErr: errContains(&ErrInvalidMapValue{key: "STRING_MAP"}), }, { name: "map: invalid value", input: &struct { Int64Map map[string]int64 `env:"INT64_MAP"` }{}, - loader: MapLoader{"INT64_MAP": "key1=1,key2=invalid"}, - err: errors.New("unable to cast"), + loader: MapLoader{"INT64_MAP": "key1=1,key2=invalid"}, + wantErr: errContains(errors.New("unable to cast")), }, { name: "map: invalid key", input: &struct { Int64Map map[int]int64 `env:"INT64_MAP"` }{}, - loader: MapLoader{"INT64_MAP": "key1=1,key2=2"}, - err: errors.New("unable to cast"), + loader: MapLoader{"INT64_MAP": "key1=1,key2=2"}, + wantErr: errContains(errors.New("unable to cast")), }, // unknown key type @@ -436,8 +443,8 @@ func TestLoad_NativeTypes(t *testing.T) { input: &struct { Unknown interface{} `env:"UNKNOWN"` }{}, - loader: MapLoader{"UNKNOWN": "1+2i"}, - err: &ErrUnknownFieldType{field: "Unknown", key: "UNKNOWN", kind: anyKind}, + loader: MapLoader{"UNKNOWN": "1+2i"}, + wantErr: errContains(&ErrUnknownFieldType{field: "Unknown", key: "UNKNOWN", kind: anyKind}), }, { name: "nested unknown key type", @@ -446,8 +453,8 @@ func TestLoad_NativeTypes(t *testing.T) { Unknown interface{} `env:"UNKNOWN"` } `env:",prefix=NESTED_"` }{}, - loader: MapLoader{"NESTED_UNKNOWN": "1+2i"}, - err: &ErrUnknownFieldType{field: "Unknown", key: "UNKNOWN", kind: anyKind}, + loader: MapLoader{"NESTED_UNKNOWN": "1+2i"}, + wantErr: errContains(&ErrUnknownFieldType{field: "Unknown", key: "UNKNOWN", kind: anyKind}), }, } @@ -509,8 +516,8 @@ func TestLoad_ArrayTypes(t *testing.T) { input: &struct { Int64Slice []int64 `env:"INT64_SLICE"` }{}, - loader: MapLoader{"INT64_SLICE": "invalid,2"}, - err: errors.New("unable to cast"), + loader: MapLoader{"INT64_SLICE": "invalid,2"}, + wantErr: errContains(errors.New("unable to cast")), }, } @@ -576,24 +583,24 @@ func TestLoad_MapTypes(t *testing.T) { input: &struct { StringMap map[string]string `env:"STRING_MAP"` }{}, - loader: MapLoader{"STRING_MAP": "key1::value1,key2::value2"}, - err: &ErrInvalidMapValue{key: "STRING_MAP"}, + loader: MapLoader{"STRING_MAP": "key1::value1,key2::value2"}, + wantErr: errContains(&ErrInvalidMapValue{key: "STRING_MAP"}), }, { name: "map: invalid value", input: &struct { Int64Map map[string]int64 `env:"INT64_MAP"` }{}, - loader: MapLoader{"INT64_MAP": "key1=1,key2=invalid"}, - err: errors.New("unable to cast"), + loader: MapLoader{"INT64_MAP": "key1=1,key2=invalid"}, + wantErr: errContains(errors.New("unable to cast")), }, { name: "map: invalid key", input: &struct { Int64Map map[int]int64 `env:"INT64_MAP"` }{}, - loader: MapLoader{"INT64_MAP": "key1=1,key2=2"}, - err: errors.New("unable to cast"), + loader: MapLoader{"INT64_MAP": "key1=1,key2=2"}, + wantErr: errContains(errors.New("unable to cast")), }, } @@ -672,32 +679,32 @@ func TestOption_Required(t *testing.T) { input: &struct { Name string `env:"NAME,required"` }{}, - err: &ErrRequired{key: "NAME"}, - loader: MapLoader{}, + wantErr: errContains(&ErrRequired{key: "NAME"}), + loader: MapLoader{}, }, { name: "required custom decoder", input: &struct { Name CustomGob `env:"NAME,required"` }{}, - err: &ErrRequired{key: "NAME"}, - loader: MapLoader{}, + wantErr: errContains(&ErrRequired{key: "NAME"}), + loader: MapLoader{}, }, { name: "required option: empty value", input: &struct { Name *string `env:"NAME,required"` }{}, - err: &ErrRequired{key: "NAME"}, - loader: MapLoader{"NAME": ""}, + wantErr: errContains(&ErrRequired{key: "NAME"}), + loader: MapLoader{"NAME": ""}, }, { name: "missing key", input: &struct { Name string `env:",required"` }{}, - err: ErrMissingKey, - loader: MapLoader{}, + wantErr: errContains(ErrMissingKey), + loader: MapLoader{}, }, } @@ -711,10 +718,9 @@ func runTestcases(t *testing.T, testcases []testcase) { tc := tc t.Run("Load_"+tc.name, func(t *testing.T) { - err := Load(context.Background(), tc.input, WithLoader(tc.loader)) - if tc.err != nil { - assert.Error(t, err) - assert.ErrorContains(t, err, tc.err.Error()) + err := Load(context.Background(), tc.input, append(tc.opts, WithLoader(tc.loader))...) + if tc.wantErr != nil { + tc.wantErr(t, err) return } @@ -724,10 +730,9 @@ func runTestcases(t *testing.T, testcases []testcase) { }) t.Run("LoadAsync_"+tc.name, func(t *testing.T) { - err := Load(context.Background(), tc.input, Concurrency(5), WithLoader(tc.loader)) - if tc.err != nil { - assert.Error(t, err) - assert.ErrorContains(t, err, tc.err.Error()) + err := Load(context.Background(), tc.input, append(tc.opts, Concurrency(5), WithLoader(tc.loader))...) + if tc.wantErr != nil { + tc.wantErr(t, err) return } diff --git a/xload/loader_test.go b/xload/loader_test.go index e53bc7f..4da4626 100644 --- a/xload/loader_test.go +++ b/xload/loader_test.go @@ -46,7 +46,7 @@ func TestSerialLoader(t *testing.T) { return "", errors.New("error loading field") }), ), - err: errors.New("error loading field"), + wantErr: errContains(errors.New("error loading field")), }, } diff --git a/xload/options.go b/xload/options.go index 56d6595..f67e02a 100644 --- a/xload/options.go +++ b/xload/options.go @@ -5,23 +5,48 @@ const defaultKey = "env" // Option configures the xload behaviour. type Option interface{ apply(*options) } +// FieldTagName allows customising the struct tag name to use. +type FieldTagName string + +func (k FieldTagName) apply(opts *options) { opts.tagName = string(k) } + +// Concurrency allows customising the number of goroutines to use. +// Default is 1. +type Concurrency int + +func (c Concurrency) apply(opts *options) { opts.concurrency = int(c) } + +// WithLoader allows customising the loader to use. +func WithLoader(loader Loader) Option { + return optionFunc(func(opts *options) { opts.loader = loader }) +} + +// SkipCollisionDetection disables detecting any key collisions while trying to load full keys. +var SkipCollisionDetection = &applier{f: func(o *options) { o.detectCollisions = false }} + // optionFunc allows using a function as an Option. type optionFunc func(*options) func (f optionFunc) apply(opts *options) { f(opts) } +type applier struct{ f func(*options) } + +func (a *applier) apply(opts *options) { a.f(opts) } + // options holds the configuration. type options struct { - tagName string - loader Loader - concurrency int + tagName string + loader Loader + concurrency int + detectCollisions bool } func newOptions(opts ...Option) *options { o := &options{ - tagName: defaultKey, - loader: OSLoader(), - concurrency: 1, + tagName: defaultKey, + loader: OSLoader(), + concurrency: 1, + detectCollisions: true, } for _, opt := range opts { @@ -30,19 +55,3 @@ func newOptions(opts ...Option) *options { return o } - -// FieldTagName allows customising the struct tag name to use. -type FieldTagName string - -func (k FieldTagName) apply(opts *options) { opts.tagName = string(k) } - -// Concurrency allows customising the number of goroutines to use. -// Default is 1. -type Concurrency int - -func (c Concurrency) apply(opts *options) { opts.concurrency = int(c) } - -// WithLoader allows customising the loader to use. -func WithLoader(loader Loader) Option { - return optionFunc(func(opts *options) { opts.loader = loader }) -} diff --git a/xload/options_test.go b/xload/options_test.go index b9c6d42..7682c3a 100644 --- a/xload/options_test.go +++ b/xload/options_test.go @@ -8,15 +8,20 @@ import ( func Test_defaultOptions(t *testing.T) { want := &options{ - tagName: defaultKey, - loader: OSLoader(), - concurrency: 1, + tagName: defaultKey, + loader: OSLoader(), + concurrency: 1, + detectCollisions: true, } opts := newOptions() - assert.Equal(t, want.tagName, opts.tagName) - assert.Equal(t, want.concurrency, opts.concurrency) - assert.IsType(t, want.loader, opts.loader) + t.Run("Loader", func(t *testing.T) { + assert.IsType(t, want.loader, opts.loader) + want.loader = nil + opts.loader = nil + }) + + assert.Equal(t, want, opts) } func TestOptions(t *testing.T) { @@ -29,27 +34,39 @@ func TestOptions(t *testing.T) { name: "field tag name", opts: []Option{FieldTagName("custom")}, want: &options{ - tagName: "custom", - loader: OSLoader(), - concurrency: 1, + tagName: "custom", + loader: OSLoader(), + concurrency: 1, + detectCollisions: true, }, }, { name: "loader", opts: []Option{MapLoader{"A": "1"}}, want: &options{ - tagName: defaultKey, - loader: MapLoader{"A": "1"}, - concurrency: 1, + tagName: defaultKey, + loader: MapLoader{"A": "1"}, + concurrency: 1, + detectCollisions: true, }, }, { name: "concurrency", opts: []Option{Concurrency(2)}, + want: &options{ + tagName: defaultKey, + loader: OSLoader(), + concurrency: 2, + detectCollisions: true, + }, + }, + { + name: "detectCollisions", + opts: []Option{SkipCollisionDetection}, want: &options{ tagName: defaultKey, loader: OSLoader(), - concurrency: 2, + concurrency: 1, }, }, } @@ -58,9 +75,13 @@ func TestOptions(t *testing.T) { t.Run(tc.name, func(t *testing.T) { opts := newOptions(tc.opts...) - assert.Equal(t, tc.want.tagName, opts.tagName) - assert.Equal(t, tc.want.concurrency, opts.concurrency) - assert.IsType(t, tc.want.loader, opts.loader) + t.Run("Loader", func(t *testing.T) { + assert.IsType(t, tc.want.loader, opts.loader) + tc.want.loader = nil + opts.loader = nil + }) + + assert.Equal(t, tc.want, opts) }) } }