Skip to content

Commit 64f2bac

Browse files
committed
Fuzz tests
1 parent fc0e0f0 commit 64f2bac

File tree

2 files changed

+233
-0
lines changed

2 files changed

+233
-0
lines changed

table_fuzz_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package table
2+
3+
import (
4+
"math/rand"
5+
"testing"
6+
)
7+
8+
func TestTable_FuzzMassive(t *testing.T) {
9+
const (
10+
maxRows = 50_000 // scale to your RAM target
11+
maxCols = 10
12+
maxValLen = 8
13+
seed = 42 // make this configurable for reproducibility
14+
)
15+
16+
rnd := rand.New(rand.NewSource(seed))
17+
18+
tbl := &Table{}
19+
20+
// === Insert Phase ===
21+
for i := 0; i < maxRows; i++ {
22+
row := randomRow(rnd, maxCols, maxValLen)
23+
if rnd.Float64() < 0.05 {
24+
// 5% chance insert as hole row
25+
tbl.InsertHoles([][]string{row})
26+
} else {
27+
tbl.Insert([][]string{row})
28+
}
29+
if i%1_000_000 == 0 && i > 0 {
30+
t.Logf("Inserted %d rows", i)
31+
}
32+
}
33+
34+
// === Fuzz workload ===
35+
for i := 0; i < 1000; i++ {
36+
col := rnd.Intn(maxCols)
37+
val := randString(rnd, maxValLen)
38+
count := tbl.Count(col, val)
39+
40+
// Must match GetAll
41+
all := tbl.GetAll(col, val)
42+
if len(all) != count {
43+
t.Fatalf("Count mismatch: Count()=%d vs GetAll()=%d for col=%d val=%q",
44+
count, len(all), col, val)
45+
}
46+
47+
// Holes must match GetAllHoles >= GetAll
48+
allHoles := tbl.GetAllHoles(col, val)
49+
if len(allHoles) < len(all) {
50+
t.Fatalf("GetAllHoles() should never be smaller than GetAll()")
51+
}
52+
53+
// Do a query
54+
query := tbl.QueryBy(map[int]string{col: val})
55+
for _, row := range query {
56+
if row == nil {
57+
t.Fatalf("QueryBy should skip holes but found nil row")
58+
}
59+
if col >= len(row) || row[col] != val {
60+
t.Fatalf("QueryBy returned invalid row: %+v", row)
61+
}
62+
}
63+
64+
// Delete 10% of values we hit
65+
if rnd.Float64() < 0.1 {
66+
tbl.DeleteBy(map[int]string{col: val})
67+
}
68+
}
69+
70+
// === Holes check ===
71+
all := tbl.All()
72+
allHoles := tbl.AllHoles()
73+
if len(allHoles) < len(all) {
74+
t.Fatalf("AllHoles must never be smaller than All")
75+
}
76+
77+
// === Compact and recheck ===
78+
tbl.Compact()
79+
allAfter := tbl.All()
80+
allHolesAfter := tbl.AllHoles()
81+
if len(allHolesAfter) != len(allAfter) {
82+
t.Logf("After compact: holes should be gone: all=%d allHoles=%d",
83+
len(allAfter), len(allHolesAfter))
84+
}
85+
86+
t.Logf("Fuzz done: final rows=%d holes=%d",
87+
len(allAfter), len(allHolesAfter)-len(allAfter))
88+
}

table_union_fuzz_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package table
2+
3+
import (
4+
"math/rand"
5+
"reflect"
6+
"sort"
7+
"strings"
8+
"testing"
9+
)
10+
11+
// Utility to sort slice-of-rows for comparison.
12+
func sortRows(rows [][]string) {
13+
sort.Slice(rows, func(i, j int) bool {
14+
return strings.Join(rows[i], "|") < strings.Join(rows[j], "|")
15+
})
16+
}
17+
18+
// Merges two sorted slices of rows (with possible duplicates).
19+
func unionRows(a, b [][]string) [][]string {
20+
out := append(append([][]string(nil), a...), b...)
21+
sortRows(out)
22+
return out
23+
}
24+
25+
func TestFuzzUnion(t *testing.T) {
26+
const (
27+
maxCols = 5
28+
totalIters = 5000
29+
lookupIters = 50
30+
maxValLen = 8
31+
seed = 42
32+
)
33+
34+
r := rand.New(rand.NewSource(seed))
35+
A := &Table{}
36+
B := &Table{}
37+
var C *Table
38+
39+
for iter := 0; iter < totalIters; iter++ {
40+
// Randomly choose insert vs delete (80% inserts)
41+
if r.Float64() < 0.8 {
42+
row := randomRow(r, maxCols, maxValLen)
43+
if r.Float64() < 0.5 {
44+
A.Insert([][]string{row})
45+
} else {
46+
B.Insert([][]string{row})
47+
}
48+
} else {
49+
// Random delete: pick non-empty table A or B
50+
var T *Table
51+
if r.Float64() < 0.5 {
52+
T = A
53+
} else {
54+
T = B
55+
}
56+
// Pick a random existing row and delete by one of its columns
57+
all := T.All()
58+
if len(all) > 0 {
59+
row := all[r.Intn(len(all))]
60+
col := r.Intn(len(row))
61+
val := row[col]
62+
T.DeleteBy(map[int]string{col: val})
63+
}
64+
}
65+
66+
// Rebuild C = A ∪ B
67+
C = &Table{}
68+
C.Insert(A.All())
69+
C.Insert(B.All())
70+
C.Compact()
71+
72+
// Now test random single-column lookups
73+
for li := 0; li < lookupIters; li++ {
74+
col := r.Intn(maxCols)
75+
// choose a test value: either random or drawn from C
76+
var val string
77+
if r.Float64() < 0.5 {
78+
val = randString(r, 3)
79+
} else {
80+
h := C.All()
81+
if len(h) > 0 {
82+
row := h[r.Intn(len(h))]
83+
if col < len(row) {
84+
val = row[col]
85+
}
86+
}
87+
}
88+
89+
aRows := A.GetAll(col, val)
90+
bRows := B.GetAll(col, val)
91+
exp := unionRows(aRows, bRows)
92+
93+
cRows := C.GetAll(col, val)
94+
sortRows(cRows)
95+
96+
if !equalRows(cRows, exp) {
97+
t.Fatalf("Invariant broken at iter %d lookup %d:\n"+
98+
"col=%d val=%q\nA rows = %v\nB rows = %v\n"+
99+
"C rows = %v\nexpected union = %v",
100+
iter, li, col, val, aRows, bRows, cRows, exp)
101+
}
102+
}
103+
104+
// Occasional compaction to reclaim holes
105+
if iter%1000 == 0 {
106+
A.Compact()
107+
B.Compact()
108+
C.Compact()
109+
}
110+
}
111+
}
112+
113+
// equalRows assumes both are sorted by sortRows().
114+
func equalRows(a, b [][]string) bool {
115+
if len(a) != len(b) {
116+
return false
117+
}
118+
for i := range a {
119+
if !reflect.DeepEqual(a[i], b[i]) {
120+
return false
121+
}
122+
}
123+
return true
124+
}
125+
126+
func randomRow(rnd *rand.Rand, maxCols, maxValLen int) []string {
127+
n := rnd.Intn(maxCols) + 1
128+
row := make([]string, n)
129+
for i := range row {
130+
if rnd.Float64() < 0.05 {
131+
continue // randomly keep some cols empty
132+
}
133+
row[i] = randString(rnd, maxValLen)
134+
}
135+
return row
136+
}
137+
138+
func randString(rnd *rand.Rand, length int) string {
139+
chars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
140+
s := make([]byte, length)
141+
for i := range s {
142+
s[i] = chars[rnd.Intn(len(chars))]
143+
}
144+
return string(s)
145+
}

0 commit comments

Comments
 (0)