From f3312f7aefbd8fe2428615c663bf8376b9647a90 Mon Sep 17 00:00:00 2001 From: Haelnorr Date: Sat, 10 Jan 2026 18:22:16 +1100 Subject: [PATCH] added more int parsing and tests --- env/boolean_test.go | 91 +++++++++++++++++++++++ env/duration_test.go | 42 +++++++++++ env/int.go | 45 ++++++++++++ env/int_test.go | 170 ++++++++++++++++++++++++++++++++++++++++++ env/string_test.go | 43 +++++++++++ env/uint.go | 81 ++++++++++++++++++++ env/uint_test.go | 171 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 643 insertions(+) create mode 100644 env/boolean_test.go create mode 100644 env/duration_test.go create mode 100644 env/int_test.go create mode 100644 env/string_test.go create mode 100644 env/uint.go create mode 100644 env/uint_test.go diff --git a/env/boolean_test.go b/env/boolean_test.go new file mode 100644 index 0000000..61ff43f --- /dev/null +++ b/env/boolean_test.go @@ -0,0 +1,91 @@ +package env + +import ( + "os" + "testing" +) + +func TestBool(t *testing.T) { + tests := []struct { + name string + key string + value string + defaultValue bool + expected bool + shouldSet bool + }{ + // Truthy values + {"true lowercase", "TEST_BOOL", "true", false, true, true}, + {"true uppercase", "TEST_BOOL", "TRUE", false, true, true}, + {"true mixed case", "TEST_BOOL", "TrUe", false, true, true}, + {"t", "TEST_BOOL", "t", false, true, true}, + {"T", "TEST_BOOL", "T", false, true, true}, + {"yes", "TEST_BOOL", "yes", false, true, true}, + {"YES", "TEST_BOOL", "YES", false, true, true}, + {"y", "TEST_BOOL", "y", false, true, true}, + {"Y", "TEST_BOOL", "Y", false, true, true}, + {"on", "TEST_BOOL", "on", false, true, true}, + {"ON", "TEST_BOOL", "ON", false, true, true}, + {"1", "TEST_BOOL", "1", false, true, true}, + {"enable", "TEST_BOOL", "enable", false, true, true}, + {"ENABLE", "TEST_BOOL", "ENABLE", false, true, true}, + {"enabled", "TEST_BOOL", "enabled", false, true, true}, + {"ENABLED", "TEST_BOOL", "ENABLED", false, true, true}, + {"active", "TEST_BOOL", "active", false, true, true}, + {"ACTIVE", "TEST_BOOL", "ACTIVE", false, true, true}, + {"affirmative", "TEST_BOOL", "affirmative", false, true, true}, + {"AFFIRMATIVE", "TEST_BOOL", "AFFIRMATIVE", false, true, true}, + + // Falsy values + {"false lowercase", "TEST_BOOL", "false", true, false, true}, + {"false uppercase", "TEST_BOOL", "FALSE", true, false, true}, + {"false mixed case", "TEST_BOOL", "FaLsE", true, false, true}, + {"f", "TEST_BOOL", "f", true, false, true}, + {"F", "TEST_BOOL", "F", true, false, true}, + {"no", "TEST_BOOL", "no", true, false, true}, + {"NO", "TEST_BOOL", "NO", true, false, true}, + {"n", "TEST_BOOL", "n", true, false, true}, + {"N", "TEST_BOOL", "N", true, false, true}, + {"off", "TEST_BOOL", "off", true, false, true}, + {"OFF", "TEST_BOOL", "OFF", true, false, true}, + {"0", "TEST_BOOL", "0", true, false, true}, + {"disable", "TEST_BOOL", "disable", true, false, true}, + {"DISABLE", "TEST_BOOL", "DISABLE", true, false, true}, + {"disabled", "TEST_BOOL", "disabled", true, false, true}, + {"DISABLED", "TEST_BOOL", "DISABLED", true, false, true}, + {"inactive", "TEST_BOOL", "inactive", true, false, true}, + {"INACTIVE", "TEST_BOOL", "INACTIVE", true, false, true}, + {"negative", "TEST_BOOL", "negative", true, false, true}, + {"NEGATIVE", "TEST_BOOL", "NEGATIVE", true, false, true}, + + // Whitespace handling + {"true with spaces", "TEST_BOOL", " true ", false, true, true}, + {"false with spaces", "TEST_BOOL", " false ", true, false, true}, + + // Default values + {"not set default true", "TEST_BOOL_NOTSET", "", true, true, false}, + {"not set default false", "TEST_BOOL_NOTSET", "", false, false, false}, + + // Invalid values should return default + {"invalid value default true", "TEST_BOOL", "invalid", true, true, true}, + {"invalid value default false", "TEST_BOOL", "invalid", false, false, true}, + {"empty string default true", "TEST_BOOL", "", true, true, true}, + {"empty string default false", "TEST_BOOL", "", false, false, true}, + {"random text default true", "TEST_BOOL", "maybe", true, true, true}, + {"random text default false", "TEST_BOOL", "maybe", false, false, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldSet { + os.Setenv(tt.key, tt.value) + defer os.Unsetenv(tt.key) + } + + result := Bool(tt.key, tt.defaultValue) + if result != tt.expected { + t.Errorf("Bool() = %v, want %v", result, tt.expected) + } + }) + } +} diff --git a/env/duration_test.go b/env/duration_test.go new file mode 100644 index 0000000..e46f29b --- /dev/null +++ b/env/duration_test.go @@ -0,0 +1,42 @@ +package env + +import ( + "os" + "testing" + "time" +) + +func TestDuration(t *testing.T) { + tests := []struct { + name string + key string + value string + defaultValue time.Duration + expected time.Duration + shouldSet bool + }{ + {"valid positive duration", "TEST_DURATION", "100", 0, 100 * time.Nanosecond, true}, + {"valid zero", "TEST_DURATION", "0", 10 * time.Second, 0, true}, + {"large value", "TEST_DURATION", "1000000000", 0, 1 * time.Second, true}, + {"valid negative duration", "TEST_DURATION", "-100", 0, -100 * time.Nanosecond, true}, + {"not set", "TEST_DURATION_NOTSET", "", 5 * time.Minute, 5 * time.Minute, false}, + {"invalid value", "TEST_DURATION", "not_a_number", 30 * time.Second, 30 * time.Second, true}, + {"empty string", "TEST_DURATION", "", 1 * time.Hour, 1 * time.Hour, true}, + {"float value", "TEST_DURATION", "10.5", 2 * time.Second, 2 * time.Second, true}, + {"very large value", "TEST_DURATION", "9223372036854775807", 0, 9223372036854775807 * time.Nanosecond, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldSet { + os.Setenv(tt.key, tt.value) + defer os.Unsetenv(tt.key) + } + + result := Duration(tt.key, tt.defaultValue) + if result != tt.expected { + t.Errorf("Duration() = %v, want %v", result, tt.expected) + } + }) + } +} diff --git a/env/int.go b/env/int.go index 18a3a3b..33dcf53 100644 --- a/env/int.go +++ b/env/int.go @@ -20,6 +20,51 @@ func Int(key string, defaultValue int) int { return intVal } +// Get an environment variable as an int8, specifying a default value if its +// not set or can't be parsed properly into an int8 +func Int8(key string, defaultValue int8) int8 { + val, exists := os.LookupEnv(key) + if !exists { + return defaultValue + } + + intVal, err := strconv.ParseInt(val, 10, 8) + if err != nil { + return defaultValue + } + return int8(intVal) +} + +// Get an environment variable as an int16, specifying a default value if its +// not set or can't be parsed properly into an int16 +func Int16(key string, defaultValue int16) int16 { + val, exists := os.LookupEnv(key) + if !exists { + return defaultValue + } + + intVal, err := strconv.ParseInt(val, 10, 16) + if err != nil { + return defaultValue + } + return int16(intVal) +} + +// Get an environment variable as an int32, specifying a default value if its +// not set or can't be parsed properly into an int32 +func Int32(key string, defaultValue int32) int32 { + val, exists := os.LookupEnv(key) + if !exists { + return defaultValue + } + + intVal, err := strconv.ParseInt(val, 10, 32) + if err != nil { + return defaultValue + } + return int32(intVal) +} + // Get an environment variable as an int64, specifying a default value if its // not set or can't be parsed properly into an int64 func Int64(key string, defaultValue int64) int64 { diff --git a/env/int_test.go b/env/int_test.go new file mode 100644 index 0000000..06feac0 --- /dev/null +++ b/env/int_test.go @@ -0,0 +1,170 @@ +package env + +import ( + "os" + "testing" +) + +func TestInt(t *testing.T) { + tests := []struct { + name string + key string + value string + defaultValue int + expected int + shouldSet bool + }{ + {"valid positive int", "TEST_INT", "42", 0, 42, true}, + {"valid negative int", "TEST_INT", "-42", 0, -42, true}, + {"valid zero", "TEST_INT", "0", 10, 0, true}, + {"not set", "TEST_INT_NOTSET", "", 100, 100, false}, + {"invalid value", "TEST_INT", "not_a_number", 50, 50, true}, + {"empty string", "TEST_INT", "", 75, 75, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldSet { + os.Setenv(tt.key, tt.value) + defer os.Unsetenv(tt.key) + } + + result := Int(tt.key, tt.defaultValue) + if result != tt.expected { + t.Errorf("Int() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestInt8(t *testing.T) { + tests := []struct { + name string + key string + value string + defaultValue int8 + expected int8 + shouldSet bool + }{ + {"valid positive int8", "TEST_INT8", "42", 0, 42, true}, + {"valid negative int8", "TEST_INT8", "-42", 0, -42, true}, + {"max int8", "TEST_INT8", "127", 0, 127, true}, + {"min int8", "TEST_INT8", "-128", 0, -128, true}, + {"overflow", "TEST_INT8", "128", 10, 10, true}, + {"not set", "TEST_INT8_NOTSET", "", 50, 50, false}, + {"invalid value", "TEST_INT8", "not_a_number", 25, 25, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldSet { + os.Setenv(tt.key, tt.value) + defer os.Unsetenv(tt.key) + } + + result := Int8(tt.key, tt.defaultValue) + if result != tt.expected { + t.Errorf("Int8() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestInt16(t *testing.T) { + tests := []struct { + name string + key string + value string + defaultValue int16 + expected int16 + shouldSet bool + }{ + {"valid positive int16", "TEST_INT16", "1000", 0, 1000, true}, + {"valid negative int16", "TEST_INT16", "-1000", 0, -1000, true}, + {"max int16", "TEST_INT16", "32767", 0, 32767, true}, + {"min int16", "TEST_INT16", "-32768", 0, -32768, true}, + {"overflow", "TEST_INT16", "32768", 100, 100, true}, + {"not set", "TEST_INT16_NOTSET", "", 500, 500, false}, + {"invalid value", "TEST_INT16", "invalid", 250, 250, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldSet { + os.Setenv(tt.key, tt.value) + defer os.Unsetenv(tt.key) + } + + result := Int16(tt.key, tt.defaultValue) + if result != tt.expected { + t.Errorf("Int16() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestInt32(t *testing.T) { + tests := []struct { + name string + key string + value string + defaultValue int32 + expected int32 + shouldSet bool + }{ + {"valid positive int32", "TEST_INT32", "100000", 0, 100000, true}, + {"valid negative int32", "TEST_INT32", "-100000", 0, -100000, true}, + {"max int32", "TEST_INT32", "2147483647", 0, 2147483647, true}, + {"min int32", "TEST_INT32", "-2147483648", 0, -2147483648, true}, + {"overflow", "TEST_INT32", "2147483648", 1000, 1000, true}, + {"not set", "TEST_INT32_NOTSET", "", 5000, 5000, false}, + {"invalid value", "TEST_INT32", "abc123", 2500, 2500, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldSet { + os.Setenv(tt.key, tt.value) + defer os.Unsetenv(tt.key) + } + + result := Int32(tt.key, tt.defaultValue) + if result != tt.expected { + t.Errorf("Int32() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestInt64(t *testing.T) { + tests := []struct { + name string + key string + value string + defaultValue int64 + expected int64 + shouldSet bool + }{ + {"valid positive int64", "TEST_INT64", "1000000000", 0, 1000000000, true}, + {"valid negative int64", "TEST_INT64", "-1000000000", 0, -1000000000, true}, + {"max int64", "TEST_INT64", "9223372036854775807", 0, 9223372036854775807, true}, + {"min int64", "TEST_INT64", "-9223372036854775808", 0, -9223372036854775808, true}, + {"overflow", "TEST_INT64", "9223372036854775808", 10000, 10000, true}, + {"not set", "TEST_INT64_NOTSET", "", 50000, 50000, false}, + {"invalid value", "TEST_INT64", "not_valid", 25000, 25000, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldSet { + os.Setenv(tt.key, tt.value) + defer os.Unsetenv(tt.key) + } + + result := Int64(tt.key, tt.defaultValue) + if result != tt.expected { + t.Errorf("Int64() = %v, want %v", result, tt.expected) + } + }) + } +} diff --git a/env/string_test.go b/env/string_test.go new file mode 100644 index 0000000..bed0299 --- /dev/null +++ b/env/string_test.go @@ -0,0 +1,43 @@ +package env + +import ( + "os" + "testing" +) + +func TestString(t *testing.T) { + tests := []struct { + name string + key string + value string + defaultValue string + expected string + shouldSet bool + }{ + {"valid string", "TEST_STRING", "hello", "default", "hello", true}, + {"empty string", "TEST_STRING", "", "default", "", true}, + {"string with spaces", "TEST_STRING", "hello world", "default", "hello world", true}, + {"string with special chars", "TEST_STRING", "test@123!$%", "default", "test@123!$%", true}, + {"multiline string", "TEST_STRING", "line1\nline2\nline3", "default", "line1\nline2\nline3", true}, + {"unicode string", "TEST_STRING", "Hello δΈ–η•Œ 🌍", "default", "Hello δΈ–η•Œ 🌍", true}, + {"not set", "TEST_STRING_NOTSET", "", "default_value", "default_value", false}, + {"numeric string", "TEST_STRING", "12345", "default", "12345", true}, + {"boolean string", "TEST_STRING", "true", "default", "true", true}, + {"path string", "TEST_STRING", "/usr/local/bin", "default", "/usr/local/bin", true}, + {"url string", "TEST_STRING", "https://example.com", "default", "https://example.com", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldSet { + os.Setenv(tt.key, tt.value) + defer os.Unsetenv(tt.key) + } + + result := String(tt.key, tt.defaultValue) + if result != tt.expected { + t.Errorf("String() = %v, want %v", result, tt.expected) + } + }) + } +} diff --git a/env/uint.go b/env/uint.go new file mode 100644 index 0000000..fe4dff6 --- /dev/null +++ b/env/uint.go @@ -0,0 +1,81 @@ +package env + +import ( + "os" + "strconv" +) + +// Get an environment variable as a uint, specifying a default value if its +// not set or can't be parsed properly into a uint +func UInt(key string, defaultValue uint) uint { + val, exists := os.LookupEnv(key) + if !exists { + return defaultValue + } + + intVal, err := strconv.ParseUint(val, 10, 0) + if err != nil { + return defaultValue + } + return uint(intVal) +} + +// Get an environment variable as a uint8, specifying a default value if its +// not set or can't be parsed properly into a uint8 +func UInt8(key string, defaultValue uint8) uint8 { + val, exists := os.LookupEnv(key) + if !exists { + return defaultValue + } + + intVal, err := strconv.ParseUint(val, 10, 8) + if err != nil { + return defaultValue + } + return uint8(intVal) +} + +// Get an environment variable as a uint16, specifying a default value if its +// not set or can't be parsed properly into a uint16 +func UInt16(key string, defaultValue uint16) uint16 { + val, exists := os.LookupEnv(key) + if !exists { + return defaultValue + } + + intVal, err := strconv.ParseUint(val, 10, 16) + if err != nil { + return defaultValue + } + return uint16(intVal) +} + +// Get an environment variable as a uint32, specifying a default value if its +// not set or can't be parsed properly into a uint32 +func UInt32(key string, defaultValue uint32) uint32 { + val, exists := os.LookupEnv(key) + if !exists { + return defaultValue + } + + intVal, err := strconv.ParseUint(val, 10, 32) + if err != nil { + return defaultValue + } + return uint32(intVal) +} + +// Get an environment variable as a uint64, specifying a default value if its +// not set or can't be parsed properly into a uint64 +func UInt64(key string, defaultValue uint64) uint64 { + val, exists := os.LookupEnv(key) + if !exists { + return defaultValue + } + + intVal, err := strconv.ParseUint(val, 10, 64) + if err != nil { + return defaultValue + } + return intVal +} diff --git a/env/uint_test.go b/env/uint_test.go new file mode 100644 index 0000000..ca26c50 --- /dev/null +++ b/env/uint_test.go @@ -0,0 +1,171 @@ +package env + +import ( + "os" + "testing" +) + +func TestUInt(t *testing.T) { + tests := []struct { + name string + key string + value string + defaultValue uint + expected uint + shouldSet bool + }{ + {"valid uint", "TEST_UINT", "42", 0, 42, true}, + {"valid zero", "TEST_UINT", "0", 10, 0, true}, + {"large value", "TEST_UINT", "4294967295", 0, 4294967295, true}, + {"not set", "TEST_UINT_NOTSET", "", 100, 100, false}, + {"invalid value", "TEST_UINT", "not_a_number", 50, 50, true}, + {"negative value", "TEST_UINT", "-42", 75, 75, true}, + {"empty string", "TEST_UINT", "", 25, 25, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldSet { + os.Setenv(tt.key, tt.value) + defer os.Unsetenv(tt.key) + } + + result := UInt(tt.key, tt.defaultValue) + if result != tt.expected { + t.Errorf("UInt() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestUInt8(t *testing.T) { + tests := []struct { + name string + key string + value string + defaultValue uint8 + expected uint8 + shouldSet bool + }{ + {"valid uint8", "TEST_UINT8", "42", 0, 42, true}, + {"valid zero", "TEST_UINT8", "0", 10, 0, true}, + {"max uint8", "TEST_UINT8", "255", 0, 255, true}, + {"overflow", "TEST_UINT8", "256", 10, 10, true}, + {"not set", "TEST_UINT8_NOTSET", "", 50, 50, false}, + {"invalid value", "TEST_UINT8", "abc", 25, 25, true}, + {"negative value", "TEST_UINT8", "-1", 30, 30, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldSet { + os.Setenv(tt.key, tt.value) + defer os.Unsetenv(tt.key) + } + + result := UInt8(tt.key, tt.defaultValue) + if result != tt.expected { + t.Errorf("UInt8() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestUInt16(t *testing.T) { + tests := []struct { + name string + key string + value string + defaultValue uint16 + expected uint16 + shouldSet bool + }{ + {"valid uint16", "TEST_UINT16", "1000", 0, 1000, true}, + {"valid zero", "TEST_UINT16", "0", 100, 0, true}, + {"max uint16", "TEST_UINT16", "65535", 0, 65535, true}, + {"overflow", "TEST_UINT16", "65536", 100, 100, true}, + {"not set", "TEST_UINT16_NOTSET", "", 500, 500, false}, + {"invalid value", "TEST_UINT16", "invalid", 250, 250, true}, + {"negative value", "TEST_UINT16", "-100", 300, 300, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldSet { + os.Setenv(tt.key, tt.value) + defer os.Unsetenv(tt.key) + } + + result := UInt16(tt.key, tt.defaultValue) + if result != tt.expected { + t.Errorf("UInt16() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestUInt32(t *testing.T) { + tests := []struct { + name string + key string + value string + defaultValue uint32 + expected uint32 + shouldSet bool + }{ + {"valid uint32", "TEST_UINT32", "100000", 0, 100000, true}, + {"valid zero", "TEST_UINT32", "0", 1000, 0, true}, + {"max uint32", "TEST_UINT32", "4294967295", 0, 4294967295, true}, + {"overflow", "TEST_UINT32", "4294967296", 1000, 1000, true}, + {"not set", "TEST_UINT32_NOTSET", "", 5000, 5000, false}, + {"invalid value", "TEST_UINT32", "xyz", 2500, 2500, true}, + {"negative value", "TEST_UINT32", "-1000", 3000, 3000, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldSet { + os.Setenv(tt.key, tt.value) + defer os.Unsetenv(tt.key) + } + + result := UInt32(tt.key, tt.defaultValue) + if result != tt.expected { + t.Errorf("UInt32() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestUInt64(t *testing.T) { + tests := []struct { + name string + key string + value string + defaultValue uint64 + expected uint64 + shouldSet bool + }{ + {"valid uint64", "TEST_UINT64", "1000000000", 0, 1000000000, true}, + {"valid zero", "TEST_UINT64", "0", 10000, 0, true}, + {"max uint64", "TEST_UINT64", "18446744073709551615", 0, 18446744073709551615, true}, + {"overflow", "TEST_UINT64", "18446744073709551616", 10000, 10000, true}, + {"not set", "TEST_UINT64_NOTSET", "", 50000, 50000, false}, + {"invalid value", "TEST_UINT64", "not_valid", 25000, 25000, true}, + {"negative value", "TEST_UINT64", "-5000", 30000, 30000, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldSet { + os.Setenv(tt.key, tt.value) + defer os.Unsetenv(tt.key) + } + + result := UInt64(tt.key, tt.defaultValue) + if result != tt.expected { + t.Errorf("UInt64() = %v, want %v", result, tt.expected) + } + }) + } +}