Skip to content

Commit 6599e2a

Browse files
committed
implement Partial Messages
1 parent 668912e commit 6599e2a

File tree

4 files changed

+1874
-0
lines changed

4 files changed

+1874
-0
lines changed

partialmessages/invariants.go

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
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.ExtendFromEncodedPartialMessage("", response1)
186+
if len(response2) > 0 {
187+
reconstructed.ExtendFromEncodedPartialMessage("", response2)
188+
}
189+
190+
// The reconstructed message should be equivalent to the full message
191+
if !checker.Equal(fullMessage, reconstructed) {
192+
t.Errorf("Reconstructed message from partial responses should equal full message")
193+
}
194+
})
195+
196+
t.Run("PartialMessageBytesFromMetadata with empty metadata requests all parts", func(t *testing.T) {
197+
fullMessage, err := checker.FullMessage()
198+
if err != nil {
199+
t.Fatal(err)
200+
}
201+
202+
// Request with empty metadata should return all available parts
203+
response, rest, err := fullMessage.PartialMessageBytesFromMetadata(nil)
204+
if err != nil {
205+
t.Fatal(err)
206+
}
207+
208+
// Should get some response from a full message
209+
if len(response) == 0 {
210+
t.Error("Full message should return data when requested with empty metadata")
211+
}
212+
213+
// Should have no remaining parts since full message can fulfill everything
214+
if len(rest) > 0 {
215+
t.Error("Full message should have no remaining parts when fulfilling empty metadata request")
216+
}
217+
218+
// Test the same with empty slice instead of nil
219+
response2, rest2, err := fullMessage.PartialMessageBytesFromMetadata([]byte{})
220+
if err != nil {
221+
t.Fatal(err)
222+
}
223+
224+
// Results should be the same
225+
if len(response2) != len(response) || len(rest2) != len(rest) {
226+
t.Error("Empty metadata (nil) and empty metadata (empty slice) should produce same results")
227+
}
228+
})
229+
230+
t.Run("Available parts, missing parts, and partial message bytes consistency", func(t *testing.T) {
231+
fullMessage, err := checker.FullMessage()
232+
if err != nil {
233+
t.Fatal(err)
234+
}
235+
236+
// Get the available parts
237+
availableParts, err := fullMessage.AvailableParts()
238+
if err != nil {
239+
t.Fatal(err)
240+
}
241+
242+
// Assert available parts is non-zero length
243+
if len(availableParts) == 0 {
244+
t.Error("Full message should have non-zero available parts")
245+
}
246+
247+
// Split the full message into parts
248+
parts, err := checker.SplitIntoParts(fullMessage)
249+
if err != nil {
250+
t.Fatal(err)
251+
}
252+
253+
var partialMessageResponses [][]byte
254+
255+
// Test each part and empty message
256+
testMessages := make([]P, len(parts)+1)
257+
copy(testMessages, parts)
258+
testMessages[len(parts)] = checker.EmptyMessage()
259+
260+
for i, testMsg := range testMessages {
261+
// Assert that ShouldRequest returns true for the available parts
262+
if !testMsg.ShouldRequest("", availableParts) {
263+
t.Errorf("Message %d should request the available parts", i)
264+
}
265+
266+
// Get the MissingParts() and have the full message fulfill the request
267+
missingParts, err := testMsg.MissingParts()
268+
if err != nil {
269+
t.Fatal(err)
270+
}
271+
272+
response, rest, err := fullMessage.PartialMessageBytesFromMetadata(missingParts)
273+
if err != nil {
274+
t.Fatal(err)
275+
}
276+
277+
// Assert that the rest is nil
278+
if len(rest) > 0 {
279+
t.Errorf("Full message should fulfill all missing parts with no rest for message %d", i)
280+
}
281+
282+
// Store each partial message bytes
283+
if len(response) > 0 {
284+
partialMessageResponses = append(partialMessageResponses, response)
285+
}
286+
287+
// Call ExtendFromEncodedPartialMessage
288+
testMsg, err = checker.ExtendFromBytes(testMsg, response)
289+
if err != nil {
290+
t.Fatal(err)
291+
}
292+
293+
// Assert the extended form is now equal to the full message
294+
if !checker.Equal(fullMessage, testMsg) {
295+
t.Errorf("Extended message %d should equal full message", i)
296+
}
297+
}
298+
299+
// Assert that none of the partial message bytes are equal to each other.
300+
for i := range partialMessageResponses {
301+
for j := i + 1; j < len(partialMessageResponses); j++ {
302+
if bytes.Equal(partialMessageResponses[i], partialMessageResponses[j]) {
303+
t.Errorf("Partial message bytes %d and %d should not be equal", i, j)
304+
}
305+
}
306+
}
307+
})
308+
}

0 commit comments

Comments
 (0)