Skip to content

Commit 46149c5

Browse files
committed
implement Partial Messages
1 parent 8ca4304 commit 46149c5

File tree

4 files changed

+1749
-0
lines changed

4 files changed

+1749
-0
lines changed

partialmessages/invariants.go

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
package partialmessages
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
)
7+
8+
// InvariantChecker is a test tool to test an implementation of PartialMessage
9+
// upholds its invariants. Use this in your application's tests to validate your
10+
// PartialMessage implementation.
11+
type InvariantChecker[P PartialMessage] interface {
12+
// SplitIntoParts returns a list of partial messages where there are no
13+
// overlaps between messages, and the sum of all messages is the original
14+
// partial message.
15+
SplitIntoParts(in P) ([]P, error)
16+
17+
// FullMessage should return a complete partial message.
18+
FullMessage() (P, error)
19+
20+
// EmptyMessage should return a empty partial message.
21+
EmptyMessage() P
22+
23+
// ExtendFromBytes extends a from data and returns the extended partial
24+
// message or an error. An implementation may mutate a and return a.
25+
ExtendFromBytes(a P, data []byte) (P, error)
26+
27+
Equal(a, b P) bool
28+
}
29+
30+
func TestPartialMessageInvariants[P PartialMessage](t *testing.T, checker InvariantChecker[P]) {
31+
extend := func(a, b P) (P, error) {
32+
encodedB, _, err := b.PartialMessageBytesFromMetadata(nil)
33+
if err != nil {
34+
var out P
35+
return out, err
36+
}
37+
return checker.ExtendFromBytes(a, encodedB)
38+
}
39+
40+
t.Run("A full message should return an empty slice for missing parts", func(t *testing.T) {
41+
fullMessage, err := checker.FullMessage()
42+
if err != nil {
43+
t.Fatal(err)
44+
}
45+
b, err := fullMessage.MissingParts()
46+
if err != nil {
47+
t.Fatal(err)
48+
}
49+
if len(b) != 0 {
50+
t.Errorf("expected empty slice, got %v", b)
51+
}
52+
})
53+
t.Run("A empty message should not return an empty slice for missing parts", func(t *testing.T) {
54+
empty := checker.EmptyMessage()
55+
b, err := empty.MissingParts()
56+
if err != nil {
57+
t.Fatal(err)
58+
}
59+
if len(b) == 0 {
60+
t.Errorf("did not expect empty slice")
61+
}
62+
})
63+
t.Run("Splitting a full message, and then recombining it yields the original message", func(t *testing.T) {
64+
fullMessage, err := checker.FullMessage()
65+
if err != nil {
66+
t.Fatal(err)
67+
}
68+
69+
parts, err := checker.SplitIntoParts(fullMessage)
70+
if err != nil {
71+
t.Fatal(err)
72+
}
73+
74+
recombined := checker.EmptyMessage()
75+
for _, part := range parts {
76+
b, _, err := part.PartialMessageBytesFromMetadata(nil)
77+
if err != nil {
78+
t.Fatal(err)
79+
}
80+
recombined, err = checker.ExtendFromBytes(recombined, b)
81+
if err != nil {
82+
t.Fatal(err)
83+
}
84+
}
85+
86+
if !checker.Equal(fullMessage, recombined) {
87+
t.Errorf("Expected %v, got %v", fullMessage, recombined)
88+
}
89+
})
90+
91+
t.Run("Empty message requesting parts it doesn't have returns nil response", func(t *testing.T) {
92+
emptyMessage := checker.EmptyMessage()
93+
94+
// Get metadata representing all parts from the empty message
95+
allPartsMetadata, err := emptyMessage.MissingParts()
96+
if err != nil {
97+
t.Fatal(err)
98+
}
99+
100+
// Empty message should not be able to fulfill any request
101+
response, rest, err := emptyMessage.PartialMessageBytesFromMetadata(allPartsMetadata)
102+
if err != nil {
103+
t.Fatal(err)
104+
}
105+
106+
if len(response) != 0 {
107+
t.Error("Empty message should return nil response when requesting parts it doesn't have")
108+
}
109+
110+
// The rest should be the same as the original request since nothing was fulfilled
111+
if len(rest) == 0 && len(allPartsMetadata) > 0 {
112+
t.Error("Empty message should return the full request as 'rest' when it cannot fulfill anything")
113+
}
114+
})
115+
116+
t.Run("Partial fulfillment returns correct rest and can be completed by another message", func(t *testing.T) {
117+
fullMessage, err := checker.FullMessage()
118+
if err != nil {
119+
t.Fatal(err)
120+
}
121+
122+
parts, err := checker.SplitIntoParts(fullMessage)
123+
if err != nil {
124+
t.Fatal(err)
125+
}
126+
127+
// Skip this test if we can't split into at least 2 parts
128+
if len(parts) < 2 {
129+
t.Skip("Cannot test partial fulfillment with less than 2 parts")
130+
}
131+
132+
// Get metadata representing all parts needed
133+
emptyMessage := checker.EmptyMessage()
134+
allPartsMetadata, err := emptyMessage.MissingParts()
135+
if err != nil {
136+
t.Fatal(err)
137+
}
138+
139+
// Request all parts from the partial message
140+
response1, rest1, err := parts[0].PartialMessageBytesFromMetadata(allPartsMetadata)
141+
if err != nil {
142+
t.Fatal(err)
143+
}
144+
145+
// Should get some response since partial message has at least one part
146+
if len(response1) == 0 {
147+
t.Error("Partial message should return some data when it has parts to fulfill")
148+
}
149+
150+
// Rest should be non-zero and different from original request since something was fulfilled
151+
if len(rest1) == 0 {
152+
t.Error("Rest should be non-zero when partial fulfillment occurred")
153+
}
154+
if bytes.Equal(rest1, allPartsMetadata) {
155+
t.Error("Rest should be different from original request since partial fulfillment occurred")
156+
}
157+
158+
// Create another partial message with the remaining parts
159+
remainingPartial := checker.EmptyMessage()
160+
for i := 1; i < len(parts); i++ {
161+
remainingPartial, err = extend(remainingPartial, parts[i])
162+
if err != nil {
163+
t.Fatal(err)
164+
}
165+
}
166+
167+
// The remaining partial message should be able to fulfill the "rest" request
168+
response2, rest2, err := remainingPartial.PartialMessageBytesFromMetadata(rest1)
169+
if err != nil {
170+
t.Fatal(err)
171+
}
172+
173+
// response2 should be non-empty since we have remaining parts to fulfill
174+
if len(response2) == 0 {
175+
t.Error("Response2 should be non-empty when fulfilling remaining parts")
176+
}
177+
178+
// After fulfilling the rest, there should be no more parts needed
179+
if len(rest2) > 0 {
180+
t.Errorf("After fulfilling all parts, there should be no remaining request, saw %v", rest2)
181+
}
182+
183+
// Combine both responses and verify we can reconstruct the full message
184+
reconstructed := checker.EmptyMessage()
185+
reconstructed, err = checker.ExtendFromBytes(reconstructed, response1)
186+
if err != nil {
187+
t.Fatal(err)
188+
}
189+
if len(response2) > 0 {
190+
reconstructed, err = checker.ExtendFromBytes(reconstructed, response2)
191+
if err != nil {
192+
t.Fatal(err)
193+
}
194+
}
195+
196+
// The reconstructed message should be equivalent to the full message
197+
if !checker.Equal(fullMessage, reconstructed) {
198+
t.Errorf("Reconstructed message from partial responses should equal full message")
199+
}
200+
})
201+
202+
t.Run("PartialMessageBytesFromMetadata with empty metadata requests all parts", func(t *testing.T) {
203+
fullMessage, err := checker.FullMessage()
204+
if err != nil {
205+
t.Fatal(err)
206+
}
207+
208+
// Request with empty metadata should return all available parts
209+
response, rest, err := fullMessage.PartialMessageBytesFromMetadata(nil)
210+
if err != nil {
211+
t.Fatal(err)
212+
}
213+
214+
// Should get some response from a full message
215+
if len(response) == 0 {
216+
t.Error("Full message should return data when requested with empty metadata")
217+
}
218+
219+
// Should have no remaining parts since full message can fulfill everything
220+
if len(rest) > 0 {
221+
t.Error("Full message should have no remaining parts when fulfilling empty metadata request")
222+
}
223+
224+
// Test the same with empty slice instead of nil
225+
response2, rest2, err := fullMessage.PartialMessageBytesFromMetadata([]byte{})
226+
if err != nil {
227+
t.Fatal(err)
228+
}
229+
230+
// Results should be the same
231+
if len(response2) != len(response) || len(rest2) != len(rest) {
232+
t.Error("Empty metadata (nil) and empty metadata (empty slice) should produce same results")
233+
}
234+
})
235+
236+
t.Run("Available parts, missing parts, and partial message bytes consistency", func(t *testing.T) {
237+
fullMessage, err := checker.FullMessage()
238+
if err != nil {
239+
t.Fatal(err)
240+
}
241+
242+
// Get the available parts
243+
availableParts, err := fullMessage.AvailableParts()
244+
if err != nil {
245+
t.Fatal(err)
246+
}
247+
248+
// Assert available parts is non-zero length
249+
if len(availableParts) == 0 {
250+
t.Error("Full message should have non-zero available parts")
251+
}
252+
253+
// Split the full message into parts
254+
parts, err := checker.SplitIntoParts(fullMessage)
255+
if err != nil {
256+
t.Fatal(err)
257+
}
258+
259+
var partialMessageResponses [][]byte
260+
261+
// Test each part and empty message
262+
testMessages := make([]P, len(parts)+1)
263+
copy(testMessages, parts)
264+
testMessages[len(parts)] = checker.EmptyMessage()
265+
266+
for i, testMsg := range testMessages {
267+
// Assert that ShouldRequest returns true for the available parts
268+
if !testMsg.ShouldRequest("", availableParts) {
269+
t.Errorf("Message %d should request the available parts", i)
270+
}
271+
272+
// Get the MissingParts() and have the full message fulfill the request
273+
missingParts, err := testMsg.MissingParts()
274+
if err != nil {
275+
t.Fatal(err)
276+
}
277+
278+
response, rest, err := fullMessage.PartialMessageBytesFromMetadata(missingParts)
279+
if err != nil {
280+
t.Fatal(err)
281+
}
282+
283+
// Assert that the rest is nil
284+
if len(rest) > 0 {
285+
t.Errorf("Full message should fulfill all missing parts with no rest for message %d", i)
286+
}
287+
288+
// Store each partial message bytes
289+
if len(response) > 0 {
290+
partialMessageResponses = append(partialMessageResponses, response)
291+
}
292+
293+
// Call ExtendFromEncodedPartialMessage
294+
testMsg, err = checker.ExtendFromBytes(testMsg, response)
295+
if err != nil {
296+
t.Fatal(err)
297+
}
298+
299+
// Assert the extended form is now equal to the full message
300+
if !checker.Equal(fullMessage, testMsg) {
301+
t.Errorf("Extended message %d should equal full message", i)
302+
}
303+
}
304+
305+
// Assert that none of the partial message bytes are equal to each other.
306+
for i := range partialMessageResponses {
307+
for j := i + 1; j < len(partialMessageResponses); j++ {
308+
if bytes.Equal(partialMessageResponses[i], partialMessageResponses[j]) {
309+
t.Errorf("Partial message bytes %d and %d should not be equal", i, j)
310+
}
311+
}
312+
}
313+
})
314+
}

0 commit comments

Comments
 (0)