|
| 1 | +package enumify |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "math" |
| 6 | + "math/rand" |
| 7 | + "strings" |
| 8 | + "testing" |
| 9 | + "unicode" |
| 10 | + |
| 11 | + "github.com/stretchr/testify/require" |
| 12 | +) |
| 13 | + |
| 14 | +var ( |
| 15 | + DefaultInvalid = []any{"foo", "123", "INVALID", 257, -1, 314.314, struct{}{}, true, false} |
| 16 | +) |
| 17 | + |
| 18 | +const ( |
| 19 | + DBCharLimit = 16 |
| 20 | +) |
| 21 | + |
| 22 | +type TestSuite[T ~uint8, Names []string | [][]string] struct { |
| 23 | + Values []T // the ordered enum values |
| 24 | + Names Names // the names of the enum values |
| 25 | + Invalid []any // any invalid values to test |
| 26 | + ICase bool // whether the enum is case insensitive (adds multi-case tests) defaults to true |
| 27 | + ISpace bool // whether the enum is space insensitive (adds space repr tests) defaults to true |
| 28 | + parser Parser[T] // parsing function created by ParseFactory |
| 29 | + unknowns string // the string representation of the unknown value |
| 30 | +} |
| 31 | + |
| 32 | +//============================================================================ |
| 33 | +// Testing Utilities |
| 34 | +//============================================================================ |
| 35 | + |
| 36 | +func (s *TestSuite[T, Names]) Run(t *testing.T) { |
| 37 | + t.Run("Interface", s.TestInterface) |
| 38 | + t.Run("Stringer", s.TestStringer) |
| 39 | + t.Run("StringBounds", s.TestStringBounds) |
| 40 | + t.Run("Parse", s.TestParse) |
| 41 | + t.Run("Database", s.TestDatabase) |
| 42 | +} |
| 43 | + |
| 44 | +func (s *TestSuite[T, Names]) TestInterface(t *testing.T) { |
| 45 | + enum := T(0) |
| 46 | + require.Implements(t, (*Enum)(nil), &enum, "%T must implement the Enum interface", enum) |
| 47 | +} |
| 48 | + |
| 49 | +func (s *TestSuite[T, Names]) TestStringer(t *testing.T) { |
| 50 | + names := s.Strings() |
| 51 | + for i, enum := range s.Values { |
| 52 | + e, ok := any(enum).(fmt.Stringer) |
| 53 | + require.True(t, ok, "expected %T to be a fmt.Stringer", enum) |
| 54 | + require.Equal(t, names[i], e.String(), "expected %T to have string representation %q, got %q", e, names[i], e.String()) |
| 55 | + } |
| 56 | + |
| 57 | + // Test Zero Values |
| 58 | + zero := T(0) |
| 59 | + e, ok := any(zero).(fmt.Stringer) |
| 60 | + require.True(t, ok, "expected %T to be a fmt.Stringer", zero) |
| 61 | + require.Equal(t, s.Unknowns(), e.String(), "expected %T to have string representation %q, got %q", e, s.Unknowns(), e.String()) |
| 62 | +} |
| 63 | + |
| 64 | +func (s *TestSuite[T, Names]) TestStringBounds(t *testing.T) { |
| 65 | + max := uint8(0) // max starts at 0 and anything greater in Values is set to max |
| 66 | + min := uint8(255) // min starts at 255 and anything less in Values is set to min |
| 67 | + |
| 68 | + // Discover the maximum and minimum values of the enum. |
| 69 | + for _, e := range s.Values { |
| 70 | + if uint8(e) > max { |
| 71 | + max = uint8(e) |
| 72 | + } |
| 73 | + if uint8(e) < min { |
| 74 | + min = uint8(e) |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + // Create a value above the maximum value. |
| 79 | + above := T(max + 1) |
| 80 | + aboves, ok := any(above).(fmt.Stringer) |
| 81 | + require.True(t, ok, "expected %T to be a fmt.Stringer", above) |
| 82 | + require.Equal(t, s.Unknowns(), aboves.String(), "expected %T to have string representation %q for unknown value above maximum enum value %d", above, s.Unknowns(), max) |
| 83 | + |
| 84 | + // Test zero value |
| 85 | + if min > 0 { |
| 86 | + zero := T(0) |
| 87 | + zeros, ok := any(zero).(fmt.Stringer) |
| 88 | + require.True(t, ok, "expected %T to be a fmt.Stringer", zero) |
| 89 | + require.Equal(t, s.Unknowns(), zeros.String(), "expected %T to have string representation %q for unknown value at zero", zero, s.Unknowns()) |
| 90 | + } |
| 91 | +} |
| 92 | + |
| 93 | +func (s *TestSuite[T, Names]) TestParse(t *testing.T) { |
| 94 | + t.Run("Valid", func(t *testing.T) { |
| 95 | + testCases := s.ValidCases() |
| 96 | + for _, val := range testCases { |
| 97 | + actual, err := s.Parser()(val) |
| 98 | + require.NoError(t, err, "expected parsing valid %T value %q to not error", T(0), val) |
| 99 | + require.Contains(t, s.Values, actual, "expected parsing valid %T value %q to return valid enum", T(0), val) |
| 100 | + } |
| 101 | + }) |
| 102 | + |
| 103 | + t.Run("Invalid", func(t *testing.T) { |
| 104 | + testCases := s.InvalidCases() |
| 105 | + for _, val := range testCases { |
| 106 | + actual, err := s.Parser()(val) |
| 107 | + require.Error(t, err, "expected parsing invalid %T value %q to error", T(0), val) |
| 108 | + require.Equal(t, T(0), actual, "expected parsing invalid %T value %q to return unknown value %T", T(0), val, actual) |
| 109 | + } |
| 110 | + }) |
| 111 | +} |
| 112 | + |
| 113 | +func (s *TestSuite[T, Names]) TestDatabase(t *testing.T) { |
| 114 | + // TODO: implement scan and value tests |
| 115 | + t.Run("VARCHAR", func(t *testing.T) { |
| 116 | + // Ensure that all string representations are less than or equal to the db VARCHAR limit |
| 117 | + for _, enum := range s.Values { |
| 118 | + s, ok := any(enum).(fmt.Stringer) |
| 119 | + require.True(t, ok, "expected %T to be a fmt.Stringer", enum) |
| 120 | + require.LessOrEqual(t, len(s.String()), DBCharLimit, "expected %T value %q to be less than or equal to %d characters", enum, s.String(), DBCharLimit) |
| 121 | + } |
| 122 | + }) |
| 123 | +} |
| 124 | + |
| 125 | +//============================================================================ |
| 126 | +// Helper Functions |
| 127 | +//============================================================================ |
| 128 | + |
| 129 | +func (s *TestSuite[T, Names]) Parser() Parser[T] { |
| 130 | + if s.parser == nil { |
| 131 | + s.parser = ParseFactory[T](s.Names) |
| 132 | + } |
| 133 | + return s.parser |
| 134 | +} |
| 135 | + |
| 136 | +func (s *TestSuite[T, Names]) Unknowns() string { |
| 137 | + if s.unknowns == "" { |
| 138 | + s.unknowns = s.Strings()[0] |
| 139 | + } |
| 140 | + return s.unknowns |
| 141 | +} |
| 142 | + |
| 143 | +func (s *TestSuite[T, Names]) Strings() []string { |
| 144 | + var names []string |
| 145 | + switch col := any(s.Names).(type) { |
| 146 | + case []string: |
| 147 | + names = col |
| 148 | + case [][]string: |
| 149 | + names = col[0] |
| 150 | + } |
| 151 | + return names |
| 152 | +} |
| 153 | + |
| 154 | +func (s *TestSuite[T, Names]) ValidCases() []any { |
| 155 | + cases := make([]any, 0, len(s.Values)*18) |
| 156 | + names := s.Strings() |
| 157 | + |
| 158 | + // Add the numeric representations |
| 159 | + // Set 1: the enum values themselves |
| 160 | + // Set 2-6: uint, uint8, uint16, uint32, uint64 |
| 161 | + // Set 7-11: int, int8, int16, int32, int64 |
| 162 | + // Set 12-13: float32, float64 |
| 163 | + for _, val := range s.Values { |
| 164 | + cases = append(cases, val) |
| 165 | + cases = append(cases, uint(val), uint8(val), uint16(val), uint32(val), uint64(val)) |
| 166 | + cases = append(cases, int(val), int8(val), int16(val), int32(val), int64(val)) |
| 167 | + cases = append(cases, float32(val), float64(val)) |
| 168 | + } |
| 169 | + |
| 170 | + // Add the string representations |
| 171 | + // Set 14: the enum values as strings |
| 172 | + // Set 15: lowercase if case-insensitive |
| 173 | + // Set 16: mixed case if case-insensitive |
| 174 | + // Set 17: uppercase if case-insensitive |
| 175 | + // Set 18: spaces added if space-insensitive |
| 176 | + for _, name := range names { |
| 177 | + cases = append(cases, name) |
| 178 | + if s.ICase { |
| 179 | + cases = append(cases, strings.ToLower(name)) |
| 180 | + cases = append(cases, mixedCase(name)) |
| 181 | + cases = append(cases, strings.ToUpper(name)) |
| 182 | + } |
| 183 | + if s.ISpace { |
| 184 | + cases = append(cases, addSpaces(name)) |
| 185 | + } |
| 186 | + } |
| 187 | + |
| 188 | + return cases |
| 189 | +} |
| 190 | + |
| 191 | +func (s *TestSuite[T, Names]) InvalidCases() []any { |
| 192 | + if len(s.Invalid) > 0 { |
| 193 | + return s.Invalid |
| 194 | + } |
| 195 | + |
| 196 | + cases := make([]any, 0, len(DefaultInvalid)+len(s.Values)+16) |
| 197 | + cases = append(cases, DefaultInvalid...) |
| 198 | + cases = append(cases, math.MaxInt, math.MinInt, math.MaxInt8, math.MinInt8, math.MaxInt16, math.MinInt16, math.MaxInt32, math.MinInt32, math.MaxInt64, math.MinInt64) |
| 199 | + cases = append(cases, math.MaxUint16, math.MaxUint32) |
| 200 | + cases = append(cases, float32(-1.0), float64(-1.0), math.MaxFloat32, math.MaxFloat64) |
| 201 | + |
| 202 | + for _, name := range s.Strings() { |
| 203 | + cases = append(cases, mangle(name)) |
| 204 | + if !s.ICase { |
| 205 | + cases = append(cases, mixedCase(name)) |
| 206 | + } |
| 207 | + if !s.ISpace { |
| 208 | + cases = append(cases, addSpaces(name)) |
| 209 | + } |
| 210 | + } |
| 211 | + return cases |
| 212 | +} |
| 213 | + |
| 214 | +func mixedCase(s string) string { |
| 215 | + sb := strings.Builder{} |
| 216 | + for _, r := range s { |
| 217 | + // Flip a coin and make the character upper or lower case |
| 218 | + if rand.Intn(2) == 0 { |
| 219 | + sb.WriteRune(unicode.ToLower(r)) |
| 220 | + } else { |
| 221 | + sb.WriteRune(unicode.ToUpper(r)) |
| 222 | + } |
| 223 | + } |
| 224 | + return sb.String() |
| 225 | +} |
| 226 | + |
| 227 | +func addSpaces(s string) string { |
| 228 | + fn := strings.Repeat(" ", rand.Intn(8)) |
| 229 | + bn := strings.Repeat(" ", rand.Intn(4)) |
| 230 | + return fn + s + bn |
| 231 | +} |
| 232 | + |
| 233 | +func mangle(s string) string { |
| 234 | + sb := strings.Builder{} |
| 235 | + mangled := false |
| 236 | + for _, c := range s { |
| 237 | + // 40% chance to mangle the character |
| 238 | + if rand.Intn(100) < 40 { |
| 239 | + r := rune(rand.Intn(93) + 33) // 33-126 |
| 240 | + sb.WriteRune(r) |
| 241 | + mangled = r != c && unicode.ToLower(r) != unicode.ToLower(c) |
| 242 | + } else { |
| 243 | + sb.WriteRune(c) |
| 244 | + } |
| 245 | + } |
| 246 | + |
| 247 | + if !mangled { |
| 248 | + for i := 0; i < rand.Intn(4)+1; i++ { |
| 249 | + sb.WriteRune(rune(rand.Intn(93) + 33)) // 33-126 |
| 250 | + } |
| 251 | + } |
| 252 | + return sb.String() |
| 253 | +} |
0 commit comments