From 59d9bf6a839415047658eda0023d8f7de23d8965 Mon Sep 17 00:00:00 2001 From: Simon Wikstrand Date: Wed, 1 Oct 2025 20:31:23 +0200 Subject: [PATCH] thread safety and write function --- .github/workflows/test-coverage-main.yaml | 2 +- .github/workflows/test-coverage-pr.yaml | 2 +- configura.go | 383 ++++++++++++++-------- configura_test.go | 266 +++++++-------- configura_thread_safety_test.go | 174 ++++++++++ 5 files changed, 552 insertions(+), 275 deletions(-) create mode 100644 configura_thread_safety_test.go diff --git a/.github/workflows/test-coverage-main.yaml b/.github/workflows/test-coverage-main.yaml index 48f2880..2a4eb34 100644 --- a/.github/workflows/test-coverage-main.yaml +++ b/.github/workflows/test-coverage-main.yaml @@ -20,7 +20,7 @@ jobs: run: go mod vendor - name: Test with the Go CLI id: test - run: go test -coverprofile=unit.coverage.out ./... + run: go test -race -v -coverprofile=unit.coverage.out ./... - name: Upload coverage report id: coverage env: diff --git a/.github/workflows/test-coverage-pr.yaml b/.github/workflows/test-coverage-pr.yaml index 98dcaa2..0f26853 100644 --- a/.github/workflows/test-coverage-pr.yaml +++ b/.github/workflows/test-coverage-pr.yaml @@ -19,7 +19,7 @@ jobs: - name: Test with the Go CLI if: always() id: test - run: go test -coverprofile=unit.coverage.out ./... + run: go test -race -v -coverprofile=unit.coverage.out ./... - name: Upload coverage report if: always() id: coverage diff --git a/configura.go b/configura.go index b56e4c1..6fc75f2 100644 --- a/configura.go +++ b/configura.go @@ -3,6 +3,7 @@ package configura import ( "errors" "maps" + "sync" ) var ErrMissingVariable = errors.New("missing configuration variables") @@ -35,208 +36,302 @@ type Config interface { ConfigurationKeysRegistered(keys ...any) error } +// Write is a generic function that writes configuration values to the provided configuration struct. +// It uses type assertions to determine the type of the values and writes them to the appropriate map in the +// configuration struct. +func Write[T constraint](cfg Config, values map[Variable[T]]T) error { + if cfg == nil { + return errors.New("Config cannot be nil") + } + + typecastCfg, ok := cfg.(*ConfigImpl) // Type assertion to *ConfigImpl + if !ok { + return errors.New("invalid configuration type, expected *ConfigImpl") + } + + typecastCfg.rwLock.Lock() + defer typecastCfg.rwLock.Unlock() + switch v := any(values).(type) { + case map[Variable[string]]string: + maps.Copy(typecastCfg.regString, v) + case map[Variable[int]]int: + maps.Copy(typecastCfg.regInt, v) + case map[Variable[int8]]int8: + maps.Copy(typecastCfg.regInt8, v) + case map[Variable[int16]]int16: + maps.Copy(typecastCfg.regInt16, v) + case map[Variable[int32]]int32: + maps.Copy(typecastCfg.regInt32, v) + case map[Variable[int64]]int64: + maps.Copy(typecastCfg.regInt64, v) + case map[Variable[uint]]uint: + maps.Copy(typecastCfg.regUint, v) + case map[Variable[uint8]]uint8: + maps.Copy(typecastCfg.regUint8, v) + case map[Variable[uint16]]uint16: + maps.Copy(typecastCfg.regUint16, v) + case map[Variable[uint32]]uint32: + maps.Copy(typecastCfg.regUint32, v) + case map[Variable[uint64]]uint64: + maps.Copy(typecastCfg.regUint64, v) + case map[Variable[uintptr]]uintptr: + maps.Copy(typecastCfg.regUintptr, v) + case map[Variable[[]byte]][]byte: + maps.Copy(typecastCfg.regBytes, v) + case map[Variable[[]rune]][]rune: + maps.Copy(typecastCfg.regRunes, v) + case map[Variable[float32]]float32: + maps.Copy(typecastCfg.regFloat32, v) + case map[Variable[float64]]float64: + maps.Copy(typecastCfg.regFloat64, v) + case map[Variable[bool]]bool: + maps.Copy(typecastCfg.regBool, v) + default: + return errors.New("unsupported values type") + } + + return nil +} + // LoadEnvironment is a generic function that loads an environment variable into the provided configuration, // using the specified key and fallback value. It uses type assertions to determine the type of the key // and fallback value, and registers the variable in the appropriate map of the configuration struct. func LoadEnvironment[T constraint](config *ConfigImpl, key Variable[T], fallback T) { + config.rwLock.Lock() + defer config.rwLock.Unlock() switch any(key).(type) { case Variable[string]: - config.RegString[any(key).(Variable[string])] = String(any(key).(Variable[string]), any(fallback).(string)) + config.regString[any(key).(Variable[string])] = String(any(key).(Variable[string]), any(fallback).(string)) case Variable[int]: - config.RegInt[any(key).(Variable[int])] = Int(any(key).(Variable[int]), any(fallback).(int)) + config.regInt[any(key).(Variable[int])] = Int(any(key).(Variable[int]), any(fallback).(int)) case Variable[int8]: - config.RegInt8[any(key).(Variable[int8])] = Int8(any(key).(Variable[int8]), any(fallback).(int8)) + config.regInt8[any(key).(Variable[int8])] = Int8(any(key).(Variable[int8]), any(fallback).(int8)) case Variable[int16]: - config.RegInt16[any(key).(Variable[int16])] = Int16(any(key).(Variable[int16]), any(fallback).(int16)) + config.regInt16[any(key).(Variable[int16])] = Int16(any(key).(Variable[int16]), any(fallback).(int16)) case Variable[int32]: - config.RegInt32[any(key).(Variable[int32])] = Int32(any(key).(Variable[int32]), any(fallback).(int32)) + config.regInt32[any(key).(Variable[int32])] = Int32(any(key).(Variable[int32]), any(fallback).(int32)) case Variable[int64]: - config.RegInt64[any(key).(Variable[int64])] = Int64(any(key).(Variable[int64]), any(fallback).(int64)) + config.regInt64[any(key).(Variable[int64])] = Int64(any(key).(Variable[int64]), any(fallback).(int64)) case Variable[uint]: - config.RegUint[any(key).(Variable[uint])] = Uint(any(key).(Variable[uint]), any(fallback).(uint)) + config.regUint[any(key).(Variable[uint])] = Uint(any(key).(Variable[uint]), any(fallback).(uint)) case Variable[uint8]: - config.RegUint8[any(key).(Variable[uint8])] = Uint8(any(key).(Variable[uint8]), any(fallback).(uint8)) + config.regUint8[any(key).(Variable[uint8])] = Uint8(any(key).(Variable[uint8]), any(fallback).(uint8)) case Variable[uint16]: - config.RegUint16[any(key).(Variable[uint16])] = Uint16(any(key).(Variable[uint16]), any(fallback).(uint16)) + config.regUint16[any(key).(Variable[uint16])] = Uint16(any(key).(Variable[uint16]), any(fallback).(uint16)) case Variable[uint32]: - config.RegUint32[any(key).(Variable[uint32])] = Uint32(any(key).(Variable[uint32]), any(fallback).(uint32)) + config.regUint32[any(key).(Variable[uint32])] = Uint32(any(key).(Variable[uint32]), any(fallback).(uint32)) case Variable[uint64]: - config.RegUint64[any(key).(Variable[uint64])] = Uint64(any(key).(Variable[uint64]), any(fallback).(uint64)) + config.regUint64[any(key).(Variable[uint64])] = Uint64(any(key).(Variable[uint64]), any(fallback).(uint64)) case Variable[uintptr]: - config.RegUintptr[any(key).(Variable[uintptr])] = Uintptr(any(key).(Variable[uintptr]), any(fallback).(uintptr)) + config.regUintptr[any(key).(Variable[uintptr])] = Uintptr(any(key).(Variable[uintptr]), any(fallback).(uintptr)) case Variable[[]byte]: - config.RegBytes[any(key).(Variable[[]byte])] = Bytes(any(key).(Variable[[]byte]), any(fallback).([]byte)) + config.regBytes[any(key).(Variable[[]byte])] = Bytes(any(key).(Variable[[]byte]), any(fallback).([]byte)) case Variable[[]rune]: - config.RegRunes[any(key).(Variable[[]rune])] = Runes(any(key).(Variable[[]rune]), any(fallback).([]rune)) + config.regRunes[any(key).(Variable[[]rune])] = Runes(any(key).(Variable[[]rune]), any(fallback).([]rune)) case Variable[float32]: - config.RegFloat32[any(key).(Variable[float32])] = Float32(any(key).(Variable[float32]), any(fallback).(float32)) + config.regFloat32[any(key).(Variable[float32])] = Float32(any(key).(Variable[float32]), any(fallback).(float32)) case Variable[float64]: - config.RegFloat64[any(key).(Variable[float64])] = Float64(any(key).(Variable[float64]), any(fallback).(float64)) + config.regFloat64[any(key).(Variable[float64])] = Float64(any(key).(Variable[float64]), any(fallback).(float64)) case Variable[bool]: - config.RegBool[any(key).(Variable[bool])] = Bool(any(key).(Variable[bool]), any(fallback).(bool)) + config.regBool[any(key).(Variable[bool])] = Bool(any(key).(Variable[bool]), any(fallback).(bool)) } } // ConfigImpl is a concrete implementation of the Config interface, holding maps for each type of configuration // variable. It provides methods to retrieve values for each type and checks if all required keys are registered. type ConfigImpl struct { - RegString map[Variable[string]]string - RegInt map[Variable[int]]int - RegInt8 map[Variable[int8]]int8 - RegInt16 map[Variable[int16]]int16 - RegInt32 map[Variable[int32]]int32 - RegInt64 map[Variable[int64]]int64 - RegUint map[Variable[uint]]uint - RegUint8 map[Variable[uint8]]uint8 - RegUint16 map[Variable[uint16]]uint16 - RegUint32 map[Variable[uint32]]uint32 - RegUint64 map[Variable[uint64]]uint64 - RegUintptr map[Variable[uintptr]]uintptr - RegBytes map[Variable[[]byte]][]byte - RegRunes map[Variable[[]rune]][]rune - RegFloat32 map[Variable[float32]]float32 - RegFloat64 map[Variable[float64]]float64 - RegBool map[Variable[bool]]bool + rwLock sync.RWMutex + regString map[Variable[string]]string + regInt map[Variable[int]]int + regInt8 map[Variable[int8]]int8 + regInt16 map[Variable[int16]]int16 + regInt32 map[Variable[int32]]int32 + regInt64 map[Variable[int64]]int64 + regUint map[Variable[uint]]uint + regUint8 map[Variable[uint8]]uint8 + regUint16 map[Variable[uint16]]uint16 + regUint32 map[Variable[uint32]]uint32 + regUint64 map[Variable[uint64]]uint64 + regUintptr map[Variable[uintptr]]uintptr + regBytes map[Variable[[]byte]][]byte + regRunes map[Variable[[]rune]][]rune + regFloat32 map[Variable[float32]]float32 + regFloat64 map[Variable[float64]]float64 + regBool map[Variable[bool]]bool } func NewConfigImpl() *ConfigImpl { return &ConfigImpl{ - RegString: make(map[Variable[string]]string), - RegInt: make(map[Variable[int]]int), - RegInt8: make(map[Variable[int8]]int8), - RegInt16: make(map[Variable[int16]]int16), - RegInt32: make(map[Variable[int32]]int32), - RegInt64: make(map[Variable[int64]]int64), - RegUint: make(map[Variable[uint]]uint), - RegUint8: make(map[Variable[uint8]]uint8), - RegUint16: make(map[Variable[uint16]]uint16), - RegUint32: make(map[Variable[uint32]]uint32), - RegUint64: make(map[Variable[uint64]]uint64), - RegUintptr: make(map[Variable[uintptr]]uintptr), - RegBytes: make(map[Variable[[]byte]][]byte), - RegRunes: make(map[Variable[[]rune]][]rune), - RegFloat32: make(map[Variable[float32]]float32), - RegFloat64: make(map[Variable[float64]]float64), - RegBool: make(map[Variable[bool]]bool), + regString: make(map[Variable[string]]string), + regInt: make(map[Variable[int]]int), + regInt8: make(map[Variable[int8]]int8), + regInt16: make(map[Variable[int16]]int16), + regInt32: make(map[Variable[int32]]int32), + regInt64: make(map[Variable[int64]]int64), + regUint: make(map[Variable[uint]]uint), + regUint8: make(map[Variable[uint8]]uint8), + regUint16: make(map[Variable[uint16]]uint16), + regUint32: make(map[Variable[uint32]]uint32), + regUint64: make(map[Variable[uint64]]uint64), + regUintptr: make(map[Variable[uintptr]]uintptr), + regBytes: make(map[Variable[[]byte]][]byte), + regRunes: make(map[Variable[[]rune]][]rune), + regFloat32: make(map[Variable[float32]]float32), + regFloat64: make(map[Variable[float64]]float64), + regBool: make(map[Variable[bool]]bool), } } var _ Config = (*ConfigImpl)(nil) -func (c ConfigImpl) String(key Variable[string]) string { - if value, exists := c.RegString[key]; exists { +func (c *ConfigImpl) String(key Variable[string]) string { + c.rwLock.RLock() + defer c.rwLock.RUnlock() + if value, exists := c.regString[key]; exists { return value } return "" } -func (c ConfigImpl) Int(key Variable[int]) int { - if value, exists := c.RegInt[key]; exists { +func (c *ConfigImpl) Int(key Variable[int]) int { + c.rwLock.RLock() + defer c.rwLock.RUnlock() + if value, exists := c.regInt[key]; exists { return value } return 0 } -func (c ConfigImpl) Int8(key Variable[int8]) int8 { - if value, exists := c.RegInt8[key]; exists { +func (c *ConfigImpl) Int8(key Variable[int8]) int8 { + c.rwLock.RLock() + defer c.rwLock.RUnlock() + if value, exists := c.regInt8[key]; exists { return value } return 0 } -func (c ConfigImpl) Int16(key Variable[int16]) int16 { - if value, exists := c.RegInt16[key]; exists { +func (c *ConfigImpl) Int16(key Variable[int16]) int16 { + c.rwLock.RLock() + defer c.rwLock.RUnlock() + if value, exists := c.regInt16[key]; exists { return value } return 0 } -func (c ConfigImpl) Int32(key Variable[int32]) int32 { - if value, exists := c.RegInt32[key]; exists { +func (c *ConfigImpl) Int32(key Variable[int32]) int32 { + c.rwLock.RLock() + defer c.rwLock.RUnlock() + if value, exists := c.regInt32[key]; exists { return value } return 0 } -func (c ConfigImpl) Int64(key Variable[int64]) int64 { - if value, exists := c.RegInt64[key]; exists { +func (c *ConfigImpl) Int64(key Variable[int64]) int64 { + c.rwLock.RLock() + defer c.rwLock.RUnlock() + if value, exists := c.regInt64[key]; exists { return value } return 0 } -func (c ConfigImpl) Uint(key Variable[uint]) uint { - if value, exists := c.RegUint[key]; exists { +func (c *ConfigImpl) Uint(key Variable[uint]) uint { + c.rwLock.RLock() + defer c.rwLock.RUnlock() + if value, exists := c.regUint[key]; exists { return value } return 0 } -func (c ConfigImpl) Uint8(key Variable[uint8]) uint8 { - if value, exists := c.RegUint8[key]; exists { +func (c *ConfigImpl) Uint8(key Variable[uint8]) uint8 { + c.rwLock.RLock() + defer c.rwLock.RUnlock() + if value, exists := c.regUint8[key]; exists { return value } return 0 } -func (c ConfigImpl) Uint16(key Variable[uint16]) uint16 { - if value, exists := c.RegUint16[key]; exists { +func (c *ConfigImpl) Uint16(key Variable[uint16]) uint16 { + c.rwLock.RLock() + defer c.rwLock.RUnlock() + if value, exists := c.regUint16[key]; exists { return value } return 0 } -func (c ConfigImpl) Uint32(key Variable[uint32]) uint32 { - if value, exists := c.RegUint32[key]; exists { +func (c *ConfigImpl) Uint32(key Variable[uint32]) uint32 { + c.rwLock.RLock() + defer c.rwLock.RUnlock() + if value, exists := c.regUint32[key]; exists { return value } return 0 } -func (c ConfigImpl) Uint64(key Variable[uint64]) uint64 { - if value, exists := c.RegUint64[key]; exists { +func (c *ConfigImpl) Uint64(key Variable[uint64]) uint64 { + c.rwLock.RLock() + defer c.rwLock.RUnlock() + if value, exists := c.regUint64[key]; exists { return value } return 0 } -func (c ConfigImpl) Uintptr(key Variable[uintptr]) uintptr { - if value, exists := c.RegUintptr[key]; exists { +func (c *ConfigImpl) Uintptr(key Variable[uintptr]) uintptr { + c.rwLock.RLock() + defer c.rwLock.RUnlock() + if value, exists := c.regUintptr[key]; exists { return value } return 0 } -func (c ConfigImpl) Bytes(key Variable[[]byte]) []byte { - if value, exists := c.RegBytes[key]; exists { +func (c *ConfigImpl) Bytes(key Variable[[]byte]) []byte { + c.rwLock.RLock() + defer c.rwLock.RUnlock() + if value, exists := c.regBytes[key]; exists { return value } return nil } -func (c ConfigImpl) Runes(key Variable[[]rune]) []rune { - if value, exists := c.RegRunes[key]; exists { +func (c *ConfigImpl) Runes(key Variable[[]rune]) []rune { + c.rwLock.RLock() + defer c.rwLock.RUnlock() + if value, exists := c.regRunes[key]; exists { return value } return nil } -func (c ConfigImpl) Float32(key Variable[float32]) float32 { - if value, exists := c.RegFloat32[key]; exists { +func (c *ConfigImpl) Float32(key Variable[float32]) float32 { + c.rwLock.RLock() + defer c.rwLock.RUnlock() + if value, exists := c.regFloat32[key]; exists { return value } return 0.0 } -func (c ConfigImpl) Float64(key Variable[float64]) float64 { - if value, exists := c.RegFloat64[key]; exists { +func (c *ConfigImpl) Float64(key Variable[float64]) float64 { + c.rwLock.RLock() + defer c.rwLock.RUnlock() + if value, exists := c.regFloat64[key]; exists { return value } return 0.0 } -func (c ConfigImpl) Bool(key Variable[bool]) bool { - if value, exists := c.RegBool[key]; exists { +func (c *ConfigImpl) Bool(key Variable[bool]) bool { + c.rwLock.RLock() + defer c.rwLock.RUnlock() + if value, exists := c.regBool[key]; exists { return value } return false @@ -276,61 +371,63 @@ var _ error = (*missingVariableError)(nil) // checkKey checks if the provided key exists in the configuration. It uses type assertion to determine the type of the // key and checks the corresponding map in the configuration struct. -func (c ConfigImpl) checkKey(key any) (string, bool) { +func (c *ConfigImpl) checkKey(key any) (string, bool) { var exists bool var keyName string - switch any(key).(type) { + c.rwLock.RLock() + defer c.rwLock.RUnlock() + switch k := key.(type) { case Variable[string]: - _, exists = c.RegString[key.(Variable[string])] - keyName = string(key.(Variable[string])) + _, exists = c.regString[k] + keyName = string(k) case Variable[int]: - _, exists = c.RegInt[key.(Variable[int])] - keyName = string(key.(Variable[int])) + _, exists = c.regInt[k] + keyName = string(k) case Variable[int8]: - _, exists = c.RegInt8[key.(Variable[int8])] - keyName = string(key.(Variable[int8])) + _, exists = c.regInt8[k] + keyName = string(k) case Variable[int16]: - _, exists = c.RegInt16[key.(Variable[int16])] - keyName = string(key.(Variable[int16])) + _, exists = c.regInt16[k] + keyName = string(k) case Variable[int32]: - _, exists = c.RegInt32[key.(Variable[int32])] - keyName = string(key.(Variable[int32])) + _, exists = c.regInt32[k] + keyName = string(k) case Variable[int64]: - _, exists = c.RegInt64[key.(Variable[int64])] - keyName = string(key.(Variable[int64])) + _, exists = c.regInt64[k] + keyName = string(k) case Variable[uint]: - _, exists = c.RegUint[key.(Variable[uint])] - keyName = string(key.(Variable[uint])) + _, exists = c.regUint[k] + keyName = string(k) case Variable[uint8]: - _, exists = c.RegUint8[key.(Variable[uint8])] - keyName = string(key.(Variable[uint8])) + _, exists = c.regUint8[k] + keyName = string(k) case Variable[uint16]: - _, exists = c.RegUint16[key.(Variable[uint16])] - keyName = string(key.(Variable[uint16])) + _, exists = c.regUint16[k] + keyName = string(k) case Variable[uint32]: - _, exists = c.RegUint32[key.(Variable[uint32])] - keyName = string(key.(Variable[uint32])) + _, exists = c.regUint32[k] + keyName = string(k) case Variable[uint64]: - _, exists = c.RegUint64[key.(Variable[uint64])] - keyName = string(key.(Variable[uint64])) + _, exists = c.regUint64[k] + keyName = string(k) case Variable[uintptr]: - _, exists = c.RegUintptr[key.(Variable[uintptr])] - keyName = string(key.(Variable[uintptr])) + _, exists = c.regUintptr[k] + keyName = string(k) case Variable[[]byte]: - _, exists = c.RegBytes[key.(Variable[[]byte])] - keyName = string(key.(Variable[[]byte])) + _, exists = c.regBytes[k] + keyName = string(k) case Variable[[]rune]: - _, exists = c.RegRunes[key.(Variable[[]rune])] - keyName = string(key.(Variable[[]rune])) + _, exists = c.regRunes[k] + keyName = string(k) case Variable[float32]: - _, exists = c.RegFloat32[key.(Variable[float32])] - keyName = string(key.(Variable[float32])) + _, exists = c.regFloat32[k] + keyName = string(k) case Variable[float64]: - _, exists = c.RegFloat64[key.(Variable[float64])] - keyName = string(key.(Variable[float64])) + _, exists = c.regFloat64[k] + keyName = string(k) case Variable[bool]: - _, exists = c.RegBool[key.(Variable[bool])] - keyName = string(key.(Variable[bool])) + _, exists = c.regBool[k] + keyName = string(k) } return keyName, exists @@ -338,7 +435,7 @@ func (c ConfigImpl) checkKey(key any) (string, bool) { // ConfigurationKeysRegistered checks if all provided keys are registered in the configuration. To ensure that the // client of the package have taken all required keys into consideration when building the configuration object. -func (c ConfigImpl) ConfigurationKeysRegistered(keys ...any) error { +func (c *ConfigImpl) ConfigurationKeysRegistered(keys ...any) error { var missingKeys []string for _, key := range keys { if keyName, ok := c.checkKey(key); !ok { @@ -364,28 +461,34 @@ func Fallback[T comparable](value T, fallback T) T { } // Merge combines multiple Config instances into a single Config instance. +// To ensure a consistent view of the source configurations, it locks all +// configuration types for reading during the merge operation. func Merge(cfgs ...Config) Config { merged := NewConfigImpl() + merged.rwLock.Lock() + defer merged.rwLock.Unlock() for _, cfg := range cfgs { - if strMap, ok := cfg.(*ConfigImpl); ok { - maps.Copy(merged.RegString, strMap.RegString) - maps.Copy(merged.RegInt, strMap.RegInt) - maps.Copy(merged.RegInt8, strMap.RegInt8) - maps.Copy(merged.RegInt16, strMap.RegInt16) - maps.Copy(merged.RegInt32, strMap.RegInt32) - maps.Copy(merged.RegInt64, strMap.RegInt64) - maps.Copy(merged.RegUint, strMap.RegUint) - maps.Copy(merged.RegUint8, strMap.RegUint8) - maps.Copy(merged.RegUint16, strMap.RegUint16) - maps.Copy(merged.RegUint32, strMap.RegUint32) - maps.Copy(merged.RegUint64, strMap.RegUint64) - maps.Copy(merged.RegUintptr, strMap.RegUintptr) - maps.Copy(merged.RegBytes, strMap.RegBytes) - maps.Copy(merged.RegRunes, strMap.RegRunes) - maps.Copy(merged.RegFloat32, strMap.RegFloat32) - maps.Copy(merged.RegFloat64, strMap.RegFloat64) - maps.Copy(merged.RegBool, strMap.RegBool) + if c, ok := cfg.(*ConfigImpl); ok { + c.rwLock.RLock() + defer c.rwLock.RUnlock() + maps.Copy(merged.regString, c.regString) + maps.Copy(merged.regInt, c.regInt) + maps.Copy(merged.regInt8, c.regInt8) + maps.Copy(merged.regInt16, c.regInt16) + maps.Copy(merged.regInt32, c.regInt32) + maps.Copy(merged.regInt64, c.regInt64) + maps.Copy(merged.regUint, c.regUint) + maps.Copy(merged.regUint8, c.regUint8) + maps.Copy(merged.regUint16, c.regUint16) + maps.Copy(merged.regUint32, c.regUint32) + maps.Copy(merged.regUint64, c.regUint64) + maps.Copy(merged.regUintptr, c.regUintptr) + maps.Copy(merged.regBytes, c.regBytes) + maps.Copy(merged.regRunes, c.regRunes) + maps.Copy(merged.regFloat32, c.regFloat32) + maps.Copy(merged.regFloat64, c.regFloat64) + maps.Copy(merged.regBool, c.regBool) } else { panic("unsupported config type") } diff --git a/configura_test.go b/configura_test.go index 0c4687b..25b13c3 100644 --- a/configura_test.go +++ b/configura_test.go @@ -64,7 +64,7 @@ func (s *ConfigSuite) TestString() { assert.Equal(s.T(), "", s.config.String(key)) }) s.Run("KeyExists", func() { - s.config.RegString[key] = "hello" + Write(s.config, map[Variable[string]]string{key: "hello"}) assert.Equal(s.T(), "hello", s.config.String(key)) }) } @@ -75,7 +75,7 @@ func (s *ConfigSuite) TestInt() { assert.Equal(s.T(), 0, s.config.Int(key)) }) s.Run("KeyExists", func() { - s.config.RegInt[key] = 123 + Write(s.config, map[Variable[int]]int{key: 123}) assert.Equal(s.T(), 123, s.config.Int(key)) }) } @@ -86,7 +86,7 @@ func (s *ConfigSuite) TestInt8() { assert.Equal(s.T(), int8(0), s.config.Int8(key)) }) s.Run("KeyExists", func() { - s.config.RegInt8[key] = int8(12) + Write(s.config, map[Variable[int8]]int8{key: 12}) assert.Equal(s.T(), int8(12), s.config.Int8(key)) }) } @@ -97,7 +97,7 @@ func (s *ConfigSuite) TestInt16() { assert.Equal(s.T(), int16(0), s.config.Int16(key)) }) s.Run("KeyExists", func() { - s.config.RegInt16[key] = int16(1234) + Write(s.config, map[Variable[int16]]int16{key: 1234}) assert.Equal(s.T(), int16(1234), s.config.Int16(key)) }) } @@ -108,7 +108,7 @@ func (s *ConfigSuite) TestInt32() { assert.Equal(s.T(), int32(0), s.config.Int32(key)) }) s.Run("KeyExists", func() { - s.config.RegInt32[key] = int32(123456) + Write(s.config, map[Variable[int32]]int32{key: 123456}) assert.Equal(s.T(), int32(123456), s.config.Int32(key)) }) } @@ -119,7 +119,7 @@ func (s *ConfigSuite) TestInt64() { assert.Equal(s.T(), int64(0), s.config.Int64(key)) }) s.Run("KeyExists", func() { - s.config.RegInt64[key] = int64(1234567890) + Write(s.config, map[Variable[int64]]int64{key: 1234567890}) assert.Equal(s.T(), int64(1234567890), s.config.Int64(key)) }) } @@ -130,7 +130,7 @@ func (s *ConfigSuite) TestUint() { assert.Equal(s.T(), uint(0), s.config.Uint(key)) }) s.Run("KeyExists", func() { - s.config.RegUint[key] = uint(123) + Write(s.config, map[Variable[uint]]uint{key: 123}) assert.Equal(s.T(), uint(123), s.config.Uint(key)) }) } @@ -141,7 +141,7 @@ func (s *ConfigSuite) TestUint8() { assert.Equal(s.T(), uint8(0), s.config.Uint8(key)) }) s.Run("KeyExists", func() { - s.config.RegUint8[key] = uint8(12) + Write(s.config, map[Variable[uint8]]uint8{key: 12}) assert.Equal(s.T(), uint8(12), s.config.Uint8(key)) }) } @@ -152,7 +152,7 @@ func (s *ConfigSuite) TestUint16() { assert.Equal(s.T(), uint16(0), s.config.Uint16(key)) }) s.Run("KeyExists", func() { - s.config.RegUint16[key] = uint16(1234) + Write(s.config, map[Variable[uint16]]uint16{key: 1234}) assert.Equal(s.T(), uint16(1234), s.config.Uint16(key)) }) } @@ -163,7 +163,7 @@ func (s *ConfigSuite) TestUint32() { assert.Equal(s.T(), uint32(0), s.config.Uint32(key)) }) s.Run("KeyExists", func() { - s.config.RegUint32[key] = uint32(123456) + Write(s.config, map[Variable[uint32]]uint32{key: 123456}) assert.Equal(s.T(), uint32(123456), s.config.Uint32(key)) }) } @@ -174,7 +174,7 @@ func (s *ConfigSuite) TestUint64() { assert.Equal(s.T(), uint64(0), s.config.Uint64(key)) }) s.Run("KeyExists", func() { - s.config.RegUint64[key] = uint64(1234567890) + Write(s.config, map[Variable[uint64]]uint64{key: 1234567890}) assert.Equal(s.T(), uint64(1234567890), s.config.Uint64(key)) }) } @@ -185,7 +185,7 @@ func (s *ConfigSuite) TestUintptr() { assert.Equal(s.T(), uintptr(0), s.config.Uintptr(key)) }) s.Run("KeyExists", func() { - s.config.RegUintptr[key] = uintptr(0xdeadbeef) + Write(s.config, map[Variable[uintptr]]uintptr{key: 0xdeadbeef}) assert.Equal(s.T(), uintptr(0xdeadbeef), s.config.Uintptr(key)) }) } @@ -197,7 +197,7 @@ func (s *ConfigSuite) TestBytes() { }) s.Run("KeyExists", func() { val := []byte("hello") - s.config.RegBytes[key] = val + Write(s.config, map[Variable[[]byte]][]byte{key: val}) assert.Equal(s.T(), val, s.config.Bytes(key)) }) } @@ -209,7 +209,7 @@ func (s *ConfigSuite) TestRunes() { }) s.Run("KeyExists", func() { val := []rune("hello😊") - s.config.RegRunes[key] = val + Write(s.config, map[Variable[[]rune]][]rune{key: val}) assert.Equal(s.T(), val, s.config.Runes(key)) }) } @@ -220,7 +220,7 @@ func (s *ConfigSuite) TestFloat32() { assert.Equal(s.T(), float32(0.0), s.config.Float32(key)) }) s.Run("KeyExists", func() { - s.config.RegFloat32[key] = float32(3.14) + Write(s.config, map[Variable[float32]]float32{key: 3.14}) assert.Equal(s.T(), float32(3.14), s.config.Float32(key)) }) } @@ -231,7 +231,7 @@ func (s *ConfigSuite) TestFloat64() { assert.Equal(s.T(), float64(0.0), s.config.Float64(key)) }) s.Run("KeyExists", func() { - s.config.RegFloat64[key] = float64(3.14159) + Write(s.config, map[Variable[float64]]float64{key: 3.14159}) assert.Equal(s.T(), float64(3.14159), s.config.Float64(key)) }) } @@ -242,12 +242,12 @@ func (s *ConfigSuite) TestBool() { assert.False(s.T(), s.config.Bool(key)) }) s.Run("KeyExistsTrue", func() { - s.config.RegBool[key] = true + Write(s.config, map[Variable[bool]]bool{key: true}) assert.True(s.T(), s.config.Bool(key)) }) s.Run("KeyExistsFalse", func() { falseKey := Variable[bool]("TEST_BOOL_FALSE_VAL") - s.config.RegBool[falseKey] = false + Write(s.config, map[Variable[bool]]bool{falseKey: false}) assert.False(s.T(), s.config.Bool(falseKey)) }) } @@ -279,17 +279,17 @@ func (s *LoadEnvironmentSuite) TestLoadString() { s.Run("EnvVarSet", func() { s.setEnvVar(string(key), envVal) LoadEnvironment(cfg, key, fallback) - assert.Equal(s.T(), envVal, cfg.RegString[key]) + assert.Equal(s.T(), envVal, cfg.String(key)) }) s.Run("EnvVarNotSet", func() { - s.unsetEnvVar(string(key)) // Ensure it's unset for this specific sub-test - LoadEnvironment(cfg, key, fallback) // Use fresh config or reset - assert.Equal(s.T(), fallback, cfg.RegString[key]) // This line would fail if cfg is not reset or key re-added + s.unsetEnvVar(string(key)) // Ensure it's unset for this specific sub-test + LoadEnvironment(cfg, key, fallback) // Use fresh config or reset + assert.Equal(s.T(), fallback, cfg.String(key)) // This line would fail if cfg is not reset or key re-added // Corrected approach for EnvVarNotSet freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegString[key]) + assert.Equal(s.T(), fallback, freshCfg.String(key)) }) } @@ -303,19 +303,19 @@ func (s *LoadEnvironmentSuite) TestLoadInt() { s.Run("EnvVarSetValid", func() { s.setEnvVar(string(key), envValStr) LoadEnvironment(cfg, key, fallback) - assert.Equal(s.T(), envValInt, cfg.RegInt[key]) + assert.Equal(s.T(), envValInt, cfg.Int(key)) }) s.Run("EnvVarNotSet", func() { s.unsetEnvVar(string(key)) freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegInt[key]) + assert.Equal(s.T(), fallback, freshCfg.Int(key)) }) s.Run("EnvVarSetInvalid", func() { s.setEnvVar(string(key), "not-an-int") freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegInt[key]) + assert.Equal(s.T(), fallback, freshCfg.Int(key)) }) } @@ -329,25 +329,25 @@ func (s *LoadEnvironmentSuite) TestLoadInt8() { s.Run("EnvVarSetValid", func() { s.setEnvVar(string(key), envValStr) LoadEnvironment(cfg, key, fallback) - assert.Equal(s.T(), envValInt8, cfg.RegInt8[key]) + assert.Equal(s.T(), envValInt8, cfg.Int8(key)) }) s.Run("EnvVarNotSet", func() { s.unsetEnvVar(string(key)) freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegInt8[key]) + assert.Equal(s.T(), fallback, freshCfg.Int8(key)) }) s.Run("EnvVarSetInvalid", func() { s.setEnvVar(string(key), "not-an-int8") freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegInt8[key]) + assert.Equal(s.T(), fallback, freshCfg.Int8(key)) }) s.Run("EnvVarSetOutOfBounds", func() { s.setEnvVar(string(key), "129") // Out of bounds for int8 freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegInt8[key]) + assert.Equal(s.T(), fallback, freshCfg.Int8(key)) }) } @@ -361,25 +361,25 @@ func (s *LoadEnvironmentSuite) TestLoadInt16() { s.Run("EnvVarSetValid", func() { s.setEnvVar(string(key), envValStr) LoadEnvironment(cfg, key, fallback) - assert.Equal(s.T(), envValInt16, cfg.RegInt16[key]) + assert.Equal(s.T(), envValInt16, cfg.Int16(key)) }) s.Run("EnvVarNotSet", func() { s.unsetEnvVar(string(key)) freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegInt16[key]) + assert.Equal(s.T(), fallback, freshCfg.Int16(key)) }) s.Run("EnvVarSetInvalid", func() { s.setEnvVar(string(key), "not-an-int16") freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegInt16[key]) + assert.Equal(s.T(), fallback, freshCfg.Int16(key)) }) s.Run("EnvVarSetOutOfBounds", func() { s.setEnvVar(string(key), "32768") // Out of bounds for int16 freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegInt16[key]) + assert.Equal(s.T(), fallback, freshCfg.Int16(key)) }) } @@ -393,25 +393,25 @@ func (s *LoadEnvironmentSuite) TestLoadInt32() { s.Run("EnvVarSetValid", func() { s.setEnvVar(string(key), envValStr) LoadEnvironment(cfg, key, fallback) - assert.Equal(s.T(), envValInt32, cfg.RegInt32[key]) + assert.Equal(s.T(), envValInt32, cfg.Int32(key)) }) s.Run("EnvVarNotSet", func() { s.unsetEnvVar(string(key)) freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegInt32[key]) + assert.Equal(s.T(), fallback, freshCfg.Int32(key)) }) s.Run("EnvVarSetInvalid", func() { s.setEnvVar(string(key), "not-an-int32") freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegInt32[key]) + assert.Equal(s.T(), fallback, freshCfg.Int32(key)) }) s.Run("EnvVarSetOutOfBounds", func() { s.setEnvVar(string(key), "2147483648") // Out of bounds for int32 freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegInt32[key]) + assert.Equal(s.T(), fallback, freshCfg.Int32(key)) }) } @@ -425,25 +425,25 @@ func (s *LoadEnvironmentSuite) TestLoadInt64() { s.Run("EnvVarSetValid", func() { s.setEnvVar(string(key), envValStr) LoadEnvironment(cfg, key, fallback) - assert.Equal(s.T(), envValInt64, cfg.RegInt64[key]) + assert.Equal(s.T(), envValInt64, cfg.Int64(key)) }) s.Run("EnvVarNotSet", func() { s.unsetEnvVar(string(key)) freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegInt64[key]) + assert.Equal(s.T(), fallback, freshCfg.Int64(key)) }) s.Run("EnvVarSetInvalid", func() { s.setEnvVar(string(key), "not-an-int64") freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegInt64[key]) + assert.Equal(s.T(), fallback, freshCfg.Int64(key)) }) s.Run("EnvVarSetOutOfBounds", func() { s.setEnvVar(string(key), "9223372036854775808") // Out of bounds for int64 freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegInt64[key]) + assert.Equal(s.T(), fallback, freshCfg.Int64(key)) }) } @@ -457,25 +457,25 @@ func (s *LoadEnvironmentSuite) TestLoadUint() { s.Run("EnvVarSetValid", func() { s.setEnvVar(string(key), envValStr) LoadEnvironment(cfg, key, fallback) - assert.Equal(s.T(), envValUint, cfg.RegUint[key]) + assert.Equal(s.T(), envValUint, cfg.Uint(key)) }) s.Run("EnvVarNotSet", func() { s.unsetEnvVar(string(key)) freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegUint[key]) + assert.Equal(s.T(), fallback, freshCfg.Uint(key)) }) s.Run("EnvVarSetInvalid", func() { s.setEnvVar(string(key), "not-a-uint") freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegUint[key]) + assert.Equal(s.T(), fallback, freshCfg.Uint(key)) }) s.Run("EnvVarSetNegative", func() { s.setEnvVar(string(key), "-1") // Negative, invalid for uint freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegUint[key]) + assert.Equal(s.T(), fallback, freshCfg.Uint(key)) }) } @@ -489,25 +489,25 @@ func (s *LoadEnvironmentSuite) TestLoadUint8() { s.Run("EnvVarSetValid", func() { s.setEnvVar(string(key), envValStr) LoadEnvironment(cfg, key, fallback) - assert.Equal(s.T(), envValUint8, cfg.RegUint8[key]) + assert.Equal(s.T(), envValUint8, cfg.Uint8(key)) }) s.Run("EnvVarNotSet", func() { s.unsetEnvVar(string(key)) freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegUint8[key]) + assert.Equal(s.T(), fallback, freshCfg.Uint8(key)) }) s.Run("EnvVarSetInvalid", func() { s.setEnvVar(string(key), "not-a-uint8") freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegUint8[key]) + assert.Equal(s.T(), fallback, freshCfg.Uint8(key)) }) s.Run("EnvVarSetOutOfBounds", func() { s.setEnvVar(string(key), "256") // Out of bounds for uint8 freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegUint8[key]) + assert.Equal(s.T(), fallback, freshCfg.Uint8(key)) }) } @@ -521,25 +521,25 @@ func (s *LoadEnvironmentSuite) TestLoadUint16() { s.Run("EnvVarSetValid", func() { s.setEnvVar(string(key), envValStr) LoadEnvironment(cfg, key, fallback) - assert.Equal(s.T(), envValUint16, cfg.RegUint16[key]) + assert.Equal(s.T(), envValUint16, cfg.Uint16(key)) }) s.Run("EnvVarNotSet", func() { s.unsetEnvVar(string(key)) freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegUint16[key]) + assert.Equal(s.T(), fallback, freshCfg.Uint16(key)) }) s.Run("EnvVarSetInvalid", func() { s.setEnvVar(string(key), "not-a-uint16") freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegUint16[key]) + assert.Equal(s.T(), fallback, freshCfg.Uint16(key)) }) s.Run("EnvVarSetOutOfBounds", func() { s.setEnvVar(string(key), "65536") // Out of bounds for uint16 freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegUint16[key]) + assert.Equal(s.T(), fallback, freshCfg.Uint16(key)) }) } @@ -553,25 +553,25 @@ func (s *LoadEnvironmentSuite) TestLoadUint32() { s.Run("EnvVarSetValid", func() { s.setEnvVar(string(key), envValStr) LoadEnvironment(cfg, key, fallback) - assert.Equal(s.T(), envValUint32, cfg.RegUint32[key]) + assert.Equal(s.T(), envValUint32, cfg.Uint32(key)) }) s.Run("EnvVarNotSet", func() { s.unsetEnvVar(string(key)) freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegUint32[key]) + assert.Equal(s.T(), fallback, freshCfg.Uint32(key)) }) s.Run("EnvVarSetInvalid", func() { s.setEnvVar(string(key), "not-a-uint32") freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegUint32[key]) + assert.Equal(s.T(), fallback, freshCfg.Uint32(key)) }) s.Run("EnvVarSetOutOfBounds", func() { s.setEnvVar(string(key), "4294967296") // Out of bounds for uint32 freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegUint32[key]) + assert.Equal(s.T(), fallback, freshCfg.Uint32(key)) }) } @@ -585,25 +585,25 @@ func (s *LoadEnvironmentSuite) TestLoadUint64() { s.Run("EnvVarSetValid", func() { s.setEnvVar(string(key), envValStr) LoadEnvironment(cfg, key, fallback) - assert.Equal(s.T(), envValUint64, cfg.RegUint64[key]) + assert.Equal(s.T(), envValUint64, cfg.Uint64(key)) }) s.Run("EnvVarNotSet", func() { s.unsetEnvVar(string(key)) freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegUint64[key]) + assert.Equal(s.T(), fallback, freshCfg.Uint64(key)) }) s.Run("EnvVarSetInvalid", func() { s.setEnvVar(string(key), "not-a-uint64") freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegUint64[key]) + assert.Equal(s.T(), fallback, freshCfg.Uint64(key)) }) s.Run("EnvVarSetOutOfBounds", func() { s.setEnvVar(string(key), "18446744073709551616") // Out of bounds for uint64 freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegUint64[key]) + assert.Equal(s.T(), fallback, freshCfg.Uint64(key)) }) } @@ -623,19 +623,19 @@ func (s *LoadEnvironmentSuite) TestLoadUintptr() { decimalEnvValStr := strconv.FormatUint(uint64(envValUintptr), 10) s.setEnvVar(string(key), decimalEnvValStr) LoadEnvironment(cfg, key, fallback) - assert.Equal(s.T(), envValUintptr, cfg.RegUintptr[key]) + assert.Equal(s.T(), envValUintptr, cfg.Uintptr(key)) }) s.Run("EnvVarNotSet", func() { s.unsetEnvVar(string(key)) freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegUintptr[key]) + assert.Equal(s.T(), fallback, freshCfg.Uintptr(key)) }) s.Run("EnvVarSetInvalid", func() { s.setEnvVar(string(key), "not-a-uintptr") freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegUintptr[key]) + assert.Equal(s.T(), fallback, freshCfg.Uintptr(key)) }) } @@ -649,19 +649,19 @@ func (s *LoadEnvironmentSuite) TestLoadFloat32() { s.Run("EnvVarSetValid", func() { s.setEnvVar(string(key), envValStr) LoadEnvironment(cfg, key, fallback) - assert.InDelta(s.T(), envValFloat32, cfg.RegFloat32[key], 0.0001) + assert.InDelta(s.T(), envValFloat32, cfg.Float32(key), 0.0001) }) s.Run("EnvVarNotSet", func() { s.unsetEnvVar(string(key)) freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.InDelta(s.T(), fallback, freshCfg.RegFloat32[key], 0.0001) + assert.InDelta(s.T(), fallback, freshCfg.Float32(key), 0.0001) }) s.Run("EnvVarSetInvalid", func() { s.setEnvVar(string(key), "not-a-float") freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.InDelta(s.T(), fallback, freshCfg.RegFloat32[key], 0.0001) + assert.InDelta(s.T(), fallback, freshCfg.Float32(key), 0.0001) }) } @@ -675,19 +675,19 @@ func (s *LoadEnvironmentSuite) TestLoadFloat64() { s.Run("EnvVarSetValid", func() { s.setEnvVar(string(key), envValStr) LoadEnvironment(cfg, key, fallback) - assert.InDelta(s.T(), envValFloat64, cfg.RegFloat64[key], 0.0000001) + assert.InDelta(s.T(), envValFloat64, cfg.Float64(key), 0.0000001) }) s.Run("EnvVarNotSet", func() { s.unsetEnvVar(string(key)) freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.InDelta(s.T(), fallback, freshCfg.RegFloat64[key], 0.0000001) + assert.InDelta(s.T(), fallback, freshCfg.Float64(key), 0.0000001) }) s.Run("EnvVarSetInvalid", func() { s.setEnvVar(string(key), "not-a-float64") freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.InDelta(s.T(), fallback, freshCfg.RegFloat64[key], 0.0000001) + assert.InDelta(s.T(), fallback, freshCfg.Float64(key), 0.0000001) }) } @@ -721,7 +721,7 @@ func (s *LoadEnvironmentSuite) TestLoadBool() { s.unsetEnvVar(string(key)) } LoadEnvironment(currentCfg, key, tc.fallback) - assert.Equal(s.T(), tc.expectedReg, currentCfg.RegBool[key], "Mismatch in registered bool value") + assert.Equal(s.T(), tc.expectedReg, currentCfg.Bool(key), "Mismatch in registered bool value") }) } } @@ -735,13 +735,13 @@ func (s *LoadEnvironmentSuite) TestLoadBytes() { s.Run("EnvVarSet", func() { s.setEnvVar(string(key), string(envVal)) LoadEnvironment(cfg, key, fallback) - assert.Equal(s.T(), envVal, cfg.RegBytes[key]) + assert.Equal(s.T(), envVal, cfg.Bytes(key)) }) s.Run("EnvVarNotSet", func() { s.unsetEnvVar(string(key)) freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegBytes[key]) + assert.Equal(s.T(), fallback, freshCfg.Bytes(key)) }) } @@ -755,13 +755,13 @@ func (s *LoadEnvironmentSuite) TestLoadRunes() { s.Run("EnvVarSet", func() { s.setEnvVar(string(key), string(envValStr)) // Store string in env LoadEnvironment(cfg, key, fallback) - assert.Equal(s.T(), envVal, cfg.RegRunes[key]) + assert.Equal(s.T(), envVal, cfg.Runes(key)) }) s.Run("EnvVarNotSet", func() { s.unsetEnvVar(string(key)) freshCfg := NewConfigImpl() LoadEnvironment(freshCfg, key, fallback) - assert.Equal(s.T(), fallback, freshCfg.RegRunes[key]) + assert.Equal(s.T(), fallback, freshCfg.Runes(key)) }) } @@ -797,10 +797,10 @@ func (s *CheckKeySuite) TestCheckKey() { missingIntKey := Variable[int]("MISSING_INT") uintptrKey := Variable[uintptr]("MY_UINTPTR_UNINIT_MAP_SCENARIO") - cfg.RegString[strKey] = "value" - cfg.RegInt[intKey] = 123 - cfg.RegBool[boolKey] = true - cfg.RegFloat32[float32Key] = 3.14 + cfg.regString[strKey] = "value" + cfg.regInt[intKey] = 123 + cfg.regBool[boolKey] = true + cfg.regFloat32[float32Key] = 3.14 s.Run("ExistingKeys", func() { name, exists := cfg.checkKey(strKey) @@ -857,8 +857,8 @@ func (s *ConfigurationKeysRegisteredSuite) TestConfigurationKeysRegistered() { intKey1 := Variable[int]("INT_KEY_1") floatKeyMissing := Variable[float32]("FLOAT_KEY_MISSING") - cfg.RegString[strKey1] = "val1" - cfg.RegInt[intKey1] = 100 + cfg.regString[strKey1] = "val1" + cfg.regInt[intKey1] = 100 s.Run("AllCheckedKeysExist", func() { err := cfg.ConfigurationKeysRegistered(strKey1, intKey1) @@ -930,23 +930,23 @@ func (s *MergeSuite) TestMergeEmpty() { cfgImpl, ok := mergedCfg.(*ConfigImpl) s.Require().True(ok, "Merged config should be of type *ConfigImpl") - s.Empty(cfgImpl.RegString, "RegString should be empty") - s.Empty(cfgImpl.RegInt, "RegInt should be empty") - s.Empty(cfgImpl.RegInt8, "RegInt8 should be empty") - s.Empty(cfgImpl.RegInt16, "RegInt16 should be empty") - s.Empty(cfgImpl.RegInt32, "RegInt32 should be empty") - s.Empty(cfgImpl.RegInt64, "RegInt64 should be empty") - s.Empty(cfgImpl.RegUint, "RegUint should be empty") - s.Empty(cfgImpl.RegUint8, "RegUint8 should be empty") - s.Empty(cfgImpl.RegUint16, "RegUint16 should be empty") - s.Empty(cfgImpl.RegUint32, "RegUint32 should be empty") - s.Empty(cfgImpl.RegUint64, "RegUint64 should be empty") - s.Empty(cfgImpl.RegUintptr, "RegUintptr should be empty") - s.Empty(cfgImpl.RegBytes, "RegBytes should be empty") - s.Empty(cfgImpl.RegRunes, "RegRunes should be empty") - s.Empty(cfgImpl.RegFloat32, "RegFloat32 should be empty") - s.Empty(cfgImpl.RegFloat64, "RegFloat64 should be empty") - s.Empty(cfgImpl.RegBool, "RegBool should be empty") + s.Empty(cfgImpl.regString, "RegString should be empty") + s.Empty(cfgImpl.regInt, "RegInt should be empty") + s.Empty(cfgImpl.regInt8, "RegInt8 should be empty") + s.Empty(cfgImpl.regInt16, "RegInt16 should be empty") + s.Empty(cfgImpl.regInt32, "RegInt32 should be empty") + s.Empty(cfgImpl.regInt64, "RegInt64 should be empty") + s.Empty(cfgImpl.regUint, "RegUint should be empty") + s.Empty(cfgImpl.regUint8, "RegUint8 should be empty") + s.Empty(cfgImpl.regUint16, "RegUint16 should be empty") + s.Empty(cfgImpl.regUint32, "RegUint32 should be empty") + s.Empty(cfgImpl.regUint64, "RegUint64 should be empty") + s.Empty(cfgImpl.regUintptr, "RegUintptr should be empty") + s.Empty(cfgImpl.regBytes, "RegBytes should be empty") + s.Empty(cfgImpl.regRunes, "RegRunes should be empty") + s.Empty(cfgImpl.regFloat32, "RegFloat32 should be empty") + s.Empty(cfgImpl.regFloat64, "RegFloat64 should be empty") + s.Empty(cfgImpl.regBool, "RegBool should be empty") } // TestMergeSingle tests merging a single configuration. @@ -967,10 +967,10 @@ func (s *MergeSuite) TestMergeSingle() { // Ensure it has copied values correctly cfgImpl, ok := mergedCfg.(*ConfigImpl) s.Require().True(ok, "Merged config should be of type *ConfigImpl") - s.Equal("value1", cfgImpl.RegString[keyStr]) - s.Equal(123, cfgImpl.RegInt[keyInt]) - s.Len(cfgImpl.RegString, 1) - s.Len(cfgImpl.RegInt, 1) + s.Equal("value1", cfgImpl.regString[keyStr]) + s.Equal(123, cfgImpl.regInt[keyInt]) + s.Len(cfgImpl.regString, 1) + s.Len(cfgImpl.regInt, 1) } // TestMergeTwoDistinct tests merging two configurations with distinct keys. @@ -991,10 +991,10 @@ func (s *MergeSuite) TestMergeTwoDistinct() { cfgImpl, ok := mergedCfg.(*ConfigImpl) s.Require().True(ok, "Merged config should be of type *ConfigImpl") - s.Len(cfgImpl.RegString, 1, "RegString should have 1 entry") - s.Equal("value1", cfgImpl.RegString[keyStr1]) - s.Len(cfgImpl.RegInt, 1, "RegInt should have 1 entry") - s.Equal(100, cfgImpl.RegInt[keyInt1]) + s.Len(cfgImpl.regString, 1, "RegString should have 1 entry") + s.Equal("value1", cfgImpl.regString[keyStr1]) + s.Len(cfgImpl.regInt, 1, "RegInt should have 1 entry") + s.Equal(100, cfgImpl.regInt[keyInt1]) } // TestMergeTwoOverride tests merging two configurations where the second overrides the first. @@ -1019,12 +1019,12 @@ func (s *MergeSuite) TestMergeTwoOverride() { cfgImpl, ok := mergedCfg.(*ConfigImpl) s.Require().True(ok, "Merged config should be of type *ConfigImpl") - s.Len(cfgImpl.RegString, 1) - s.Equal("overridden_value", cfgImpl.RegString[keyStr]) - s.Len(cfgImpl.RegInt, 1) - s.Equal(111, cfgImpl.RegInt[keyInt]) - s.Len(cfgImpl.RegBool, 1) - s.True(cfgImpl.RegBool[keyBool]) + s.Len(cfgImpl.regString, 1) + s.Equal("overridden_value", cfgImpl.regString[keyStr]) + s.Len(cfgImpl.regInt, 1) + s.Equal(111, cfgImpl.regInt[keyInt]) + s.Len(cfgImpl.regBool, 1) + s.True(cfgImpl.regBool[keyBool]) } // TestMergeMultiple tests merging multiple (three) configurations with overrides. @@ -1055,9 +1055,9 @@ func (s *MergeSuite) TestMergeMultiple() { cfgImpl, ok := mergedCfg.(*ConfigImpl) s.Require().True(ok) - s.Len(cfgImpl.RegString, 2) // S1, SHARED_KEY - s.Len(cfgImpl.RegInt, 1) // I1 - s.Len(cfgImpl.RegBool, 1) // B1 + s.Len(cfgImpl.regString, 2) // S1, SHARED_KEY + s.Len(cfgImpl.regInt, 1) // I1 + s.Len(cfgImpl.regBool, 1) // B1 } // TestMergeAllTypes ensures all supported types are merged correctly. @@ -1159,23 +1159,23 @@ func (s *MergeSuite) TestMergeAllTypes() { cfgImpl, ok := mergedCfg.(*ConfigImpl) s.Require().True(ok) - s.Len(cfgImpl.RegString, 1) - s.Len(cfgImpl.RegInt, 1) - s.Len(cfgImpl.RegInt8, 1) - s.Len(cfgImpl.RegInt16, 1) - s.Len(cfgImpl.RegInt32, 1) - s.Len(cfgImpl.RegInt64, 1) - s.Len(cfgImpl.RegUint, 1) - s.Len(cfgImpl.RegUint8, 1) - s.Len(cfgImpl.RegUint16, 1) - s.Len(cfgImpl.RegUint32, 1) - s.Len(cfgImpl.RegUint64, 1) - s.Len(cfgImpl.RegUintptr, 1) - s.Len(cfgImpl.RegBytes, 1) - s.Len(cfgImpl.RegRunes, 1) - s.Len(cfgImpl.RegFloat32, 1) - s.Len(cfgImpl.RegFloat64, 1) - s.Len(cfgImpl.RegBool, 1) + s.Len(cfgImpl.regString, 1) + s.Len(cfgImpl.regInt, 1) + s.Len(cfgImpl.regInt8, 1) + s.Len(cfgImpl.regInt16, 1) + s.Len(cfgImpl.regInt32, 1) + s.Len(cfgImpl.regInt64, 1) + s.Len(cfgImpl.regUint, 1) + s.Len(cfgImpl.regUint8, 1) + s.Len(cfgImpl.regUint16, 1) + s.Len(cfgImpl.regUint32, 1) + s.Len(cfgImpl.regUint64, 1) + s.Len(cfgImpl.regUintptr, 1) + s.Len(cfgImpl.regBytes, 1) + s.Len(cfgImpl.regRunes, 1) + s.Len(cfgImpl.regFloat32, 1) + s.Len(cfgImpl.regFloat64, 1) + s.Len(cfgImpl.regBool, 1) } func (s *FallbackSuite) TestFallbackString() { diff --git a/configura_thread_safety_test.go b/configura_thread_safety_test.go new file mode 100644 index 0000000..fafaab8 --- /dev/null +++ b/configura_thread_safety_test.go @@ -0,0 +1,174 @@ +package configura + +import ( + "fmt" + "sync" + "testing" + + "github.com/stretchr/testify/suite" +) + +type ThreadSafetySuite struct { + suite.Suite + config Config +} + +func (s *ThreadSafetySuite) SetupTest() { + s.config = NewConfigImpl() +} + +func (s *ThreadSafetySuite) runConcurrently(goroutines int, f func(i int)) { + var wg sync.WaitGroup + wg.Add(goroutines) + + for i := 0; i < goroutines; i++ { + go func(i int) { + defer wg.Done() + f(i) + }(i) + } + + wg.Wait() +} + +func (s *ThreadSafetySuite) TestConcurrentAccess() { + type testCase struct { + name string + action func(i int) + } + + testCases := []testCase{ + { + name: "String", + action: func(i int) { + key := Variable[string](fmt.Sprintf("KEY_%d", i)) + _ = s.config.String(key) + }, + }, + { + name: "Int", + action: func(i int) { + key := Variable[int](fmt.Sprintf("KEY_%d", i)) + _ = s.config.Int(key) + }, + }, + { + name: "Int8", + action: func(i int) { + key := Variable[int8](fmt.Sprintf("KEY_%d", i)) + _ = s.config.Int8(key) + }, + }, + { + name: "Int16", + action: func(i int) { + key := Variable[int16](fmt.Sprintf("KEY_%d", i)) + _ = s.config.Int16(key) + }, + }, + { + name: "Int32", + action: func(i int) { + key := Variable[int32](fmt.Sprintf("KEY_%d", i)) + _ = s.config.Int32(key) + }, + }, + { + name: "Int64", + action: func(i int) { + key := Variable[int64](fmt.Sprintf("KEY_%d", i)) + _ = s.config.Int64(key) + }, + }, + { + name: "Uint", + action: func(i int) { + key := Variable[uint](fmt.Sprintf("KEY_%d", i)) + _ = s.config.Uint(key) + }, + }, + { + name: "Uint8", + action: func(i int) { + key := Variable[uint8](fmt.Sprintf("KEY_%d", i)) + _ = s.config.Uint8(key) + }, + }, + { + name: "Uint16", + action: func(i int) { + key := Variable[uint16](fmt.Sprintf("KEY_%d", i)) + _ = s.config.Uint16(key) + }, + }, + { + name: "Uint32", + action: func(i int) { + key := Variable[uint32](fmt.Sprintf("KEY_%d", i)) + _ = s.config.Uint32(key) + }, + }, + { + name: "Uint64", + action: func(i int) { + key := Variable[uint64](fmt.Sprintf("KEY_%d", i)) + _ = s.config.Uint64(key) + }, + }, + { + name: "Uintptr", + action: func(i int) { + key := Variable[uintptr](fmt.Sprintf("KEY_%d", i)) + _ = s.config.Uintptr(key) + }, + }, + { + name: "Bytes", + action: func(i int) { + key := Variable[[]byte](fmt.Sprintf("KEY_%d", i)) + _ = s.config.Bytes(key) + }, + }, + { + name: "Runes", + action: func(i int) { + key := Variable[[]rune](fmt.Sprintf("KEY_%d", i)) + _ = s.config.Runes(key) + }, + }, + { + name: "Float32", + action: func(i int) { + key := Variable[float32](fmt.Sprintf("KEY_%d", i)) + _ = s.config.Float32(key) + }, + }, + { + name: "Float64", + action: func(i int) { + key := Variable[float64](fmt.Sprintf("KEY_%d", i)) + _ = s.config.Float64(key) + }, + }, + { + name: "Bool", + action: func(i int) { + key := Variable[bool](fmt.Sprintf("KEY_%d", i)) + _ = s.config.Bool(key) + }, + }, + } + + const numGoroutines = 100 + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() + s.runConcurrently(numGoroutines, tc.action) + }) + } +} + +func TestThreadSafety(t *testing.T) { + suite.Run(t, new(ThreadSafetySuite)) +}