Skip to content

Commit 4b757ca

Browse files
committed
allow serialization to return error for generic types
1 parent 36373f8 commit 4b757ca

14 files changed

+568
-108
lines changed

.cspell.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
"coverhtml",
1414
"coverprofile",
1515
"cpuprofile",
16+
"deser",
1617
"elif",
1718
"errgroup",
1819
"extsort",
1920
"fset",
21+
"godoc",
2022
"golangci",
2123
"GOPROXY",
2224
"lanrat",

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ ALL_SOURCES := $(shell find . -type f -name '*.go')
99
.PHONY: fmt lint test cover coverhtml examples readme
1010

1111
test:
12-
go test -timeout=90s -v ./...
12+
go test -timeout=90s $(shell go list ./... | grep -v "/examples")
1313
@echo "< ALL TESTS PASS >"
1414

1515
update-deps: go.mod

README.md

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -115,25 +115,19 @@ type Person struct {
115115
Age int
116116
}
117117

118-
func personToBytes(p Person) []byte {
118+
func personToBytes(p Person) ([]byte, error) {
119119
var buf bytes.Buffer
120120
enc := gob.NewEncoder(&buf)
121121
err := enc.Encode(p)
122-
if err != nil {
123-
panic(err)
124-
}
125-
return buf.Bytes()
122+
return buf.Bytes(), err
126123
}
127124

128-
func personFromBytes(data []byte) Person {
125+
func personFromBytes(data []byte) (Person, error) {
129126
var p Person
130127
buf := bytes.NewReader(data)
131128
dec := gob.NewDecoder(buf)
132129
err := dec.Decode(&p)
133-
if err != nil {
134-
panic(err)
135-
}
136-
return p
130+
return p, err
137131
}
138132

139133
func comparePersonsByAge(a, b Person) int {

benchmark_comparison_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -309,15 +309,15 @@ func generateRandomStringInts(size int) []string {
309309
return data
310310
}
311311

312-
func intFromBytes(b []byte) int {
312+
func intFromBytes(b []byte) (int, error) {
313313
if len(b) < 8 {
314-
return 0
314+
return 0, nil
315315
}
316-
return int(binary.LittleEndian.Uint64(b))
316+
return int(binary.LittleEndian.Uint64(b)), nil
317317
}
318318

319-
func intToBytes(i int) []byte {
319+
func intToBytes(i int) ([]byte, error) {
320320
b := make([]byte, 8)
321321
binary.LittleEndian.PutUint64(b, uint64(i))
322-
return b
322+
return b, nil
323323
}

error_edge_cases_test.go

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
package extsort_test
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"testing"
8+
9+
"github.com/lanrat/extsort"
10+
)
11+
12+
// TestSerializationErrorInFirstChunk tests error handling when the first chunk fails serialization
13+
func TestSerializationErrorInFirstChunk(t *testing.T) {
14+
inputChan := make(chan *ErrorEdgeItem, 1)
15+
16+
// Only item fails serialization
17+
inputChan <- &ErrorEdgeItem{Key: 1, FailSerialize: true}
18+
close(inputChan)
19+
20+
config := &extsort.Config{ChunkSize: 1}
21+
sort, outChan, errChan := extsort.Generic(
22+
inputChan,
23+
edgeFromBytes,
24+
edgeToBytes,
25+
edgeCompare,
26+
config,
27+
)
28+
29+
sort.Sort(context.Background())
30+
31+
// Should get no output
32+
outputCount := 0
33+
for range outChan {
34+
outputCount++
35+
}
36+
37+
if outputCount != 0 {
38+
t.Errorf("Expected 0 output items, got %d", outputCount)
39+
}
40+
41+
// Should get error
42+
err := <-errChan
43+
if err == nil {
44+
t.Fatal("Expected serialization error but got nil")
45+
}
46+
47+
var serErr *extsort.SerializationError
48+
if !errors.As(err, &serErr) {
49+
t.Errorf("Expected SerializationError, got %T", err)
50+
}
51+
52+
t.Logf("First chunk serialization error handled correctly: %v", err)
53+
}
54+
55+
// TestSerializationErrorInLastChunk tests error handling when the last chunk fails serialization
56+
func TestSerializationErrorInLastChunk(t *testing.T) {
57+
inputChan := make(chan *ErrorEdgeItem, 3)
58+
59+
// Good items followed by failing item
60+
inputChan <- &ErrorEdgeItem{Key: 1, FailSerialize: false}
61+
inputChan <- &ErrorEdgeItem{Key: 2, FailSerialize: false}
62+
inputChan <- &ErrorEdgeItem{Key: 3, FailSerialize: true} // Last chunk fails
63+
close(inputChan)
64+
65+
config := &extsort.Config{ChunkSize: 1} // Each item in its own chunk
66+
sort, outChan, errChan := extsort.Generic(
67+
inputChan,
68+
edgeFromBytes,
69+
edgeToBytes,
70+
edgeCompare,
71+
config,
72+
)
73+
74+
sort.Sort(context.Background())
75+
76+
// Should get no output due to error
77+
outputCount := 0
78+
for range outChan {
79+
outputCount++
80+
}
81+
82+
// Should get error even though first chunks were fine
83+
err := <-errChan
84+
if err == nil {
85+
t.Fatal("Expected serialization error but got nil")
86+
}
87+
88+
t.Logf("Last chunk serialization error handled correctly: %v", err)
89+
}
90+
91+
// TestEmptyChunkWithError tests error handling when chunks become empty due to errors
92+
func TestEmptyChunkWithError(t *testing.T) {
93+
inputChan := make(chan *ErrorEdgeItem, 1)
94+
95+
inputChan <- &ErrorEdgeItem{Key: 1, FailSerialize: true}
96+
close(inputChan)
97+
98+
// Use larger chunk size - the single failing item should still cause error
99+
config := &extsort.Config{ChunkSize: 10}
100+
sort, outChan, errChan := extsort.Generic(
101+
inputChan,
102+
edgeFromBytes,
103+
edgeToBytes,
104+
edgeCompare,
105+
config,
106+
)
107+
108+
sort.Sort(context.Background())
109+
110+
// Drain output
111+
for range outChan {
112+
// Should be empty
113+
}
114+
115+
// Should still get error
116+
err := <-errChan
117+
if err == nil {
118+
t.Fatal("Expected error even with large chunk size")
119+
}
120+
121+
t.Logf("Empty chunk error handling works: %v", err)
122+
}
123+
124+
// TestDeserializationErrorDuringMerge tests deserialization errors during the merge phase
125+
func TestDeserializationErrorDuringMerge(t *testing.T) {
126+
inputChan := make(chan *ErrorEdgeItem, 2)
127+
128+
// Items that serialize fine
129+
inputChan <- &ErrorEdgeItem{Key: 1, FailSerialize: false}
130+
inputChan <- &ErrorEdgeItem{Key: 2, FailSerialize: false}
131+
close(inputChan)
132+
133+
config := &extsort.Config{ChunkSize: 1} // Force multiple chunks to trigger merge
134+
sort, outChan, errChan := extsort.Generic(
135+
inputChan,
136+
edgeFailingFromBytes, // This will fail during merge
137+
edgeToBytes,
138+
edgeCompare,
139+
config,
140+
)
141+
142+
sort.Sort(context.Background())
143+
144+
// Should get no output due to deserialization failure during merge
145+
outputCount := 0
146+
for range outChan {
147+
outputCount++
148+
}
149+
150+
err := <-errChan
151+
if err == nil {
152+
t.Fatal("Expected deserialization error during merge")
153+
}
154+
155+
var deserErr *extsort.DeserializationError
156+
if !errors.As(err, &deserErr) {
157+
t.Errorf("Expected DeserializationError, got %T", err)
158+
}
159+
160+
t.Logf("Merge phase deserialization error handled: %v", err)
161+
}
162+
163+
// TestConcurrentErrorHandling tests error handling with multiple workers
164+
func TestConcurrentErrorHandling(t *testing.T) {
165+
inputChan := make(chan *ErrorEdgeItem, 4)
166+
167+
// Simpler test - just a few items with one that fails early
168+
inputChan <- &ErrorEdgeItem{Key: 1, FailSerialize: false}
169+
inputChan <- &ErrorEdgeItem{Key: 2, FailSerialize: true} // This will fail
170+
inputChan <- &ErrorEdgeItem{Key: 3, FailSerialize: false}
171+
close(inputChan)
172+
173+
config := &extsort.Config{
174+
ChunkSize: 1, // Each item in its own chunk for predictable behavior
175+
NumWorkers: 2, // Multiple workers
176+
}
177+
sort, outChan, errChan := extsort.Generic(
178+
inputChan,
179+
edgeFromBytes,
180+
edgeToBytes,
181+
edgeCompare,
182+
config,
183+
)
184+
185+
sort.Sort(context.Background())
186+
187+
// Drain output
188+
outputCount := 0
189+
for range outChan {
190+
outputCount++
191+
}
192+
193+
// Should get error from one of the workers
194+
err := <-errChan
195+
if err == nil {
196+
t.Fatal("Expected error with concurrent processing")
197+
}
198+
199+
t.Logf("Concurrent error handling works: %v (got %d outputs)", err, outputCount)
200+
}
201+
202+
// Helper types for edge case testing
203+
204+
type ErrorEdgeItem struct {
205+
Key int `json:"key"`
206+
FailSerialize bool `json:"failSerialize"`
207+
}
208+
209+
func edgeToBytes(item *ErrorEdgeItem) ([]byte, error) {
210+
if item.FailSerialize {
211+
return nil, errors.New("edge case serialization failure")
212+
}
213+
return json.Marshal(item)
214+
}
215+
216+
func edgeFromBytes(data []byte) (*ErrorEdgeItem, error) {
217+
var item ErrorEdgeItem
218+
err := json.Unmarshal(data, &item)
219+
return &item, err
220+
}
221+
222+
func edgeFailingFromBytes(data []byte) (*ErrorEdgeItem, error) {
223+
return nil, errors.New("edge case deserialization failure")
224+
}
225+
226+
func edgeCompare(a, b *ErrorEdgeItem) int {
227+
if a.Key < b.Key {
228+
return -1
229+
} else if a.Key > b.Key {
230+
return 1
231+
}
232+
return 0
233+
}

error_scenarios_test.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ func TestSerializationError(t *testing.T) {
1818
for i := 0; i < 4; i++ {
1919
inputChan <- val{Key: i, Order: i}
2020
}
21-
inputChan <- &errorVal{shouldFail: true} // This will fail ToBytes()
21+
errorItem := &errorVal{shouldFail: true}
22+
t.Logf("Adding errorVal with shouldFail=true: %+v", errorItem)
23+
inputChan <- errorItem // This will fail ToBytes()
2224
close(inputChan)
2325

26+
// Force multiple chunks to trigger serialization
27+
config := &extsort.Config{ChunkSize: 1}
2428
sort, outChan, errChan := extsort.New(inputChan, fromBytesForTest, func(a, b extsort.SortType) bool {
2529
// Handle mixed types safely
2630
av, aok := a.(val)
@@ -29,14 +33,18 @@ func TestSerializationError(t *testing.T) {
2933
return av.Key < bv.Key
3034
}
3135
return false // Fallback for error types
32-
}, nil)
36+
}, config)
3337

3438
sort.Sort(context.Background())
3539

36-
// Drain output
37-
for range outChan {
40+
// Drain output and count results
41+
resultCount := 0
42+
for result := range outChan {
43+
resultCount++
44+
t.Logf("Output %d: %T = %+v", resultCount, result, result)
3845
// Consume output
3946
}
47+
t.Logf("Got %d results from output channel", resultCount)
4048

4149
// Should now get a proper error instead of panic
4250
if err := <-errChan; err != nil {

examples/custom_types/custom_types.go

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,19 @@ type Person struct {
1414
Age int
1515
}
1616

17-
func personToBytes(p Person) []byte {
17+
func personToBytes(p Person) ([]byte, error) {
1818
var buf bytes.Buffer
1919
enc := gob.NewEncoder(&buf)
2020
err := enc.Encode(p)
21-
if err != nil {
22-
panic(err)
23-
}
24-
return buf.Bytes()
21+
return buf.Bytes(), err
2522
}
2623

27-
func personFromBytes(data []byte) Person {
24+
func personFromBytes(data []byte) (Person, error) {
2825
var p Person
2926
buf := bytes.NewReader(data)
3027
dec := gob.NewDecoder(buf)
3128
err := dec.Decode(&p)
32-
if err != nil {
33-
panic(err)
34-
}
35-
return p
29+
return p, err
3630
}
3731

3832
func comparePersonsByAge(a, b Person) int {

0 commit comments

Comments
 (0)