Skip to content

Commit 2ff1597

Browse files
authored
[FEAT] Test Suite (#3)
1 parent 1cb67f6 commit 2ff1597

4 files changed

Lines changed: 339 additions & 8 deletions

File tree

enumify_test.go

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
package enumify_test
22

3+
import (
4+
"database/sql/driver"
5+
"encoding/json"
6+
7+
"go.rtnl.ai/enumify"
8+
)
9+
310
//============================================================================
4-
// Test Enum Type
11+
// Enum type for testing the library
512
//============================================================================
613

14+
// Status is an enum type that is used for library testing. This code is not generated
15+
// but must match what the generated code would produce.
716
type Status uint8
817

918
const (
@@ -27,3 +36,55 @@ var StatusNames2D = [][]string{
2736
{"Unknown", "Draft", "Needs Review", "Published", "Archived"},
2837
{"Unbekannt", "Entwurf", "Überprüfung", "Veröffentlicht", "Archiviert"},
2938
}
39+
40+
func (s Status) String() string {
41+
if s >= Status(len(StatusNames)) {
42+
return StatusNames[StatusUnknown]
43+
}
44+
return StatusNames[s]
45+
}
46+
47+
func (s Status) MarshalJSON() ([]byte, error) {
48+
return json.Marshal(s.String())
49+
}
50+
51+
func (s *Status) UnmarshalJSON(data []byte) (err error) {
52+
var v any
53+
if err := json.Unmarshal(data, &v); err != nil {
54+
return err
55+
}
56+
57+
*s, err = enumify.ParseFactory[Status](StatusNames)(v)
58+
return err
59+
}
60+
61+
func (s Status) MarshalYAML() (any, error) {
62+
return s.String(), nil
63+
}
64+
65+
func (s *Status) UnmarshalYAML(unmarshal func(any) error) (err error) {
66+
var v string
67+
if err = unmarshal(&v); err != nil {
68+
return err
69+
}
70+
*s, err = enumify.ParseFactory[Status](StatusNames)(v)
71+
return err
72+
}
73+
74+
func (s *Status) Scan(src any) (err error) {
75+
switch v := src.(type) {
76+
case nil:
77+
*s = StatusUnknown
78+
return nil
79+
case []byte:
80+
*s, err = enumify.ParseFactory[Status](StatusNames)(string(v))
81+
return err
82+
default:
83+
*s, err = enumify.ParseFactory[Status](StatusNames)(v)
84+
return err
85+
}
86+
}
87+
88+
func (s Status) Value() (driver.Value, error) {
89+
return s.String(), nil
90+
}

parse_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,19 +123,19 @@ func TestParseFactory(t *testing.T) {
123123
{value: uint16(42), expected: `invalid enumify_test.Status value: 42`},
124124
{value: uint32(42), expected: `invalid enumify_test.Status value: 42`},
125125
{value: uint64(42), expected: `invalid enumify_test.Status value: 42`},
126-
{value: int(-1), expected: `invalid enumify_test.Status value: -1`},
126+
{value: int(-42), expected: `invalid enumify_test.Status value: -42`},
127127
{value: int(42), expected: `invalid enumify_test.Status value: 42`},
128-
{value: int8(-1), expected: `invalid enumify_test.Status value: -1`},
128+
{value: int8(-42), expected: `invalid enumify_test.Status value: -42`},
129129
{value: int8(42), expected: `invalid enumify_test.Status value: 42`},
130-
{value: int16(-1), expected: `invalid enumify_test.Status value: -1`},
130+
{value: int16(-42), expected: `invalid enumify_test.Status value: -42`},
131131
{value: int16(42), expected: `invalid enumify_test.Status value: 42`},
132-
{value: int32(-1), expected: `invalid enumify_test.Status value: -1`},
132+
{value: int32(-42), expected: `invalid enumify_test.Status value: -42`},
133133
{value: int32(42), expected: `invalid enumify_test.Status value: 42`},
134-
{value: int64(-1), expected: `invalid enumify_test.Status value: -1`},
134+
{value: int64(-42), expected: `invalid enumify_test.Status value: -42`},
135135
{value: int64(42), expected: `invalid enumify_test.Status value: 42`},
136-
{value: float32(-1), expected: `invalid enumify_test.Status value: -1.000000`},
136+
{value: float32(-42), expected: `invalid enumify_test.Status value: -42.000000`},
137137
{value: float32(42), expected: `invalid enumify_test.Status value: 42.000000`},
138-
{value: float64(-1), expected: `invalid enumify_test.Status value: -1.000000`},
138+
{value: float64(-42), expected: `invalid enumify_test.Status value: -42.000000`},
139139
{value: float64(42), expected: `invalid enumify_test.Status value: 42.000000`},
140140
}
141141

testing.go

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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+
}

testing_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package enumify_test
2+
3+
import (
4+
"testing"
5+
6+
"go.rtnl.ai/enumify"
7+
)
8+
9+
func TestTestSuite(t *testing.T) {
10+
suite := enumify.TestSuite[Status, []string]{
11+
Values: []Status{StatusUnknown, StatusDraft, StatusReview, StatusPublished, StatusArchived},
12+
Names: StatusNames,
13+
ICase: true,
14+
ISpace: true,
15+
}
16+
t.Run("Status", suite.Run)
17+
}

0 commit comments

Comments
 (0)