Skip to content

Commit aff1b8d

Browse files
authored
feat!: extract BatchReturn to batch package, add methods & tests (#286)
1 parent 9fa04b1 commit aff1b8d

40 files changed

+768
-3141
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ gen:
2424
$(GO_BIN) run ./gen/gen.go
2525
$(GO_BIN) run ./manifest/gen/gen.go
2626
$(GO_BIN) run ./proof/gen/gen.go
27+
$(GO_BIN) run ./batch/gen/gen.go
2728
$(GO_BIN) run ./builtin/v8/gen/gen.go
2829
$(GO_BIN) run ./builtin/v9/gen/gen.go
2930
$(GO_BIN) run ./builtin/v10/gen/gen.go

batch/batch_return.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package batch
2+
3+
import (
4+
"errors"
5+
6+
"github.com/filecoin-project/go-state-types/exitcode"
7+
)
8+
9+
type BatchReturn struct {
10+
SuccessCount uint64
11+
FailCodes []FailCode
12+
}
13+
14+
type FailCode struct {
15+
Idx uint64
16+
Code exitcode.ExitCode
17+
}
18+
19+
func (b BatchReturn) Size() int {
20+
return int(b.SuccessCount) + len(b.FailCodes)
21+
}
22+
23+
func (b BatchReturn) AllOk() bool {
24+
return len(b.FailCodes) == 0
25+
}
26+
27+
func (b BatchReturn) Codes() []exitcode.ExitCode {
28+
codes := make([]exitcode.ExitCode, b.Size())
29+
i := 0
30+
for _, fc := range b.FailCodes {
31+
if fc.Idx > uint64(i) {
32+
for ; i < int(fc.Idx); i++ {
33+
codes[i] = exitcode.Ok
34+
}
35+
}
36+
codes[i] = fc.Code
37+
i++
38+
}
39+
for ; i < len(codes); i++ {
40+
codes[i] = exitcode.Ok
41+
}
42+
return codes
43+
}
44+
45+
func (b BatchReturn) CodeAt(n uint64) (exitcode.ExitCode, error) {
46+
if n >= uint64(b.Size()) {
47+
return exitcode.Ok, errors.New("index out of bounds")
48+
}
49+
for _, fc := range b.FailCodes {
50+
if fc.Idx == n {
51+
return fc.Code, nil
52+
}
53+
if fc.Idx > n {
54+
return exitcode.Ok, nil
55+
}
56+
}
57+
return exitcode.Ok, nil
58+
}
59+
60+
func (b BatchReturn) Validate() error {
61+
size := uint64(b.Size())
62+
var gaps uint64
63+
for i, fc := range b.FailCodes {
64+
if fc.Idx >= size {
65+
// will also catch the case where the gaps aren't accounted for in total size
66+
return errors.New("index out of bounds")
67+
}
68+
if i > 0 {
69+
if fc.Idx <= b.FailCodes[i-1].Idx {
70+
return errors.New("fail codes are not in strictly increasing order")
71+
}
72+
gaps += fc.Idx - b.FailCodes[i-1].Idx - 1
73+
} else {
74+
gaps += fc.Idx
75+
}
76+
}
77+
return nil
78+
}

batch/batch_return_test.go

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
package batch
2+
3+
import (
4+
"bytes"
5+
"encoding/hex"
6+
"testing"
7+
8+
"github.com/filecoin-project/go-state-types/exitcode"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// Tests to match with Rust fil_actors_runtime::serialization
13+
func TestSerializationBatchReturn(t *testing.T) {
14+
testCases := []struct {
15+
name string
16+
params BatchReturn
17+
hex string
18+
}{
19+
{
20+
name: "empty",
21+
params: BatchReturn{},
22+
// [0,[]]
23+
hex: "820080",
24+
},
25+
{
26+
name: "single success",
27+
params: BatchReturn{SuccessCount: 1},
28+
// [1,[]]
29+
hex: "820180",
30+
},
31+
{
32+
name: "single failure",
33+
params: BatchReturn{FailCodes: []FailCode{{Idx: 0, Code: exitcode.ErrIllegalArgument}}},
34+
// [0,[[0,16]]]
35+
hex: "820081820010",
36+
},
37+
{
38+
name: "multiple success",
39+
params: BatchReturn{SuccessCount: 2, FailCodes: []FailCode{
40+
{Idx: 1, Code: exitcode.SysErrOutOfGas},
41+
{Idx: 2, Code: exitcode.ErrIllegalState},
42+
{Idx: 4, Code: exitcode.ErrIllegalArgument},
43+
}},
44+
// [2,[[1,7],[2,20],[4,16]]]
45+
hex: "820283820107820214820410",
46+
},
47+
}
48+
49+
for _, tc := range testCases {
50+
t.Run(tc.name, func(t *testing.T) {
51+
req := require.New(t)
52+
53+
var buf bytes.Buffer
54+
req.NoError(tc.params.MarshalCBOR(&buf))
55+
req.Equal(tc.hex, hex.EncodeToString(buf.Bytes()))
56+
var br BatchReturn
57+
req.NoError(br.UnmarshalCBOR(&buf))
58+
req.Equal(tc.params, br)
59+
req.NoError(br.Validate())
60+
})
61+
}
62+
}
63+
64+
func TestBatchReturn(t *testing.T) {
65+
req := require.New(t)
66+
67+
t.Run("empty", func(t *testing.T) {
68+
br := BatchReturn{}
69+
req.Equal(0, br.Size())
70+
req.True(br.AllOk())
71+
req.Equal([]exitcode.ExitCode{}, br.Codes())
72+
_, err := br.CodeAt(0)
73+
req.Error(err, "index out of bounds")
74+
req.NoError(br.Validate())
75+
})
76+
77+
t.Run("single success", func(t *testing.T) {
78+
br := BatchReturn{SuccessCount: 1}
79+
req.Equal(1, br.Size())
80+
req.True(br.AllOk())
81+
req.Equal([]exitcode.ExitCode{exitcode.Ok}, br.Codes())
82+
ec, err := br.CodeAt(0)
83+
req.NoError(err)
84+
req.Equal(exitcode.Ok, ec)
85+
_, err = br.CodeAt(1)
86+
req.Error(err, "index out of bounds")
87+
req.NoError(br.Validate())
88+
})
89+
90+
t.Run("single failure", func(t *testing.T) {
91+
br := BatchReturn{FailCodes: []FailCode{{Idx: 0, Code: exitcode.ErrIllegalArgument}}}
92+
req.Equal(1, br.Size())
93+
req.False(br.AllOk())
94+
req.Equal([]exitcode.ExitCode{exitcode.ErrIllegalArgument}, br.Codes())
95+
ec, err := br.CodeAt(0)
96+
req.NoError(err)
97+
req.Equal(exitcode.ErrIllegalArgument, ec)
98+
_, err = br.CodeAt(1)
99+
req.Error(err, "index out of bounds")
100+
req.NoError(br.Validate())
101+
})
102+
103+
t.Run("multiple success", func(t *testing.T) {
104+
br := BatchReturn{SuccessCount: 1, FailCodes: []FailCode{{Idx: 1, Code: exitcode.ErrIllegalArgument}}}
105+
req.Equal(2, br.Size())
106+
req.False(br.AllOk())
107+
req.Equal([]exitcode.ExitCode{exitcode.Ok, exitcode.ErrIllegalArgument}, br.Codes())
108+
ec, err := br.CodeAt(0)
109+
req.NoError(err)
110+
req.Equal(exitcode.Ok, ec)
111+
ec, err = br.CodeAt(1)
112+
req.NoError(err)
113+
req.Equal(exitcode.ErrIllegalArgument, ec)
114+
req.Equal(exitcode.Ok, br.Codes()[0])
115+
_, err = br.CodeAt(2)
116+
req.Error(err, "index out of bounds")
117+
req.NoError(br.Validate())
118+
})
119+
120+
t.Run("multiple failure", func(t *testing.T) {
121+
br := BatchReturn{SuccessCount: 1, FailCodes: []FailCode{{Idx: 0, Code: exitcode.ErrForbidden}}}
122+
req.Equal(2, br.Size())
123+
req.False(br.AllOk())
124+
req.Equal([]exitcode.ExitCode{exitcode.ErrForbidden, exitcode.Ok}, br.Codes())
125+
ec, err := br.CodeAt(0)
126+
req.NoError(err)
127+
req.Equal(exitcode.ErrForbidden, ec)
128+
ec, err = br.CodeAt(1)
129+
req.NoError(err)
130+
req.Equal(exitcode.Ok, ec)
131+
_, err = br.CodeAt(2)
132+
req.Error(err, "index out of bounds")
133+
req.NoError(br.Validate())
134+
})
135+
136+
t.Run("mixed", func(t *testing.T) {
137+
br := BatchReturn{SuccessCount: 2, FailCodes: []FailCode{
138+
{Idx: 1, Code: exitcode.SysErrOutOfGas},
139+
{Idx: 2, Code: exitcode.ErrIllegalState},
140+
{Idx: 4, Code: exitcode.ErrIllegalArgument},
141+
}}
142+
req.Equal(5, br.Size())
143+
req.False(br.AllOk())
144+
req.Equal([]exitcode.ExitCode{exitcode.Ok, exitcode.SysErrOutOfGas, exitcode.ErrIllegalState, exitcode.Ok, exitcode.ErrIllegalArgument}, br.Codes())
145+
ec, err := br.CodeAt(0)
146+
req.NoError(err)
147+
req.Equal(exitcode.Ok, ec)
148+
ec, err = br.CodeAt(1)
149+
req.NoError(err)
150+
req.Equal(exitcode.SysErrOutOfGas, ec)
151+
ec, err = br.CodeAt(2)
152+
req.NoError(err)
153+
req.Equal(exitcode.ErrIllegalState, ec)
154+
ec, err = br.CodeAt(3)
155+
req.NoError(err)
156+
req.Equal(exitcode.Ok, ec)
157+
ec, err = br.CodeAt(4)
158+
req.NoError(err)
159+
req.Equal(exitcode.ErrIllegalArgument, ec)
160+
_, err = br.CodeAt(5)
161+
req.Error(err, "index out of bounds")
162+
req.NoError(br.Validate())
163+
})
164+
}
165+
166+
func TestBatchReturn_Validate(t *testing.T) {
167+
tests := []struct {
168+
name string
169+
batchReturn BatchReturn
170+
errorMsg string
171+
}{
172+
{
173+
name: "valid batchreturn",
174+
batchReturn: BatchReturn{
175+
SuccessCount: 5,
176+
FailCodes: []FailCode{
177+
{Idx: 1, Code: exitcode.ErrIllegalArgument},
178+
{Idx: 3, Code: exitcode.ErrIllegalState},
179+
{Idx: 6, Code: exitcode.ErrNotPayable},
180+
},
181+
},
182+
},
183+
{
184+
name: "failcodes not in strictly increasing order",
185+
batchReturn: BatchReturn{
186+
SuccessCount: 5,
187+
FailCodes: []FailCode{
188+
{Idx: 1, Code: exitcode.ErrIllegalArgument},
189+
{Idx: 3, Code: exitcode.ErrIllegalState},
190+
{Idx: 3, Code: exitcode.ErrNotPayable},
191+
},
192+
},
193+
errorMsg: "fail codes are not in strictly increasing order",
194+
},
195+
{
196+
name: "failcodes contain index out of bounds",
197+
batchReturn: BatchReturn{
198+
SuccessCount: 5,
199+
FailCodes: []FailCode{
200+
{Idx: 1, Code: exitcode.ErrIllegalArgument},
201+
{Idx: 3, Code: exitcode.ErrIllegalState},
202+
{Idx: 10, Code: exitcode.ErrNotPayable},
203+
},
204+
},
205+
errorMsg: "index out of bounds",
206+
},
207+
{
208+
name: "gaps between failures exceed successcount",
209+
batchReturn: BatchReturn{
210+
SuccessCount: 2,
211+
FailCodes: []FailCode{
212+
{Idx: 1, Code: exitcode.ErrIllegalArgument},
213+
{Idx: 4, Code: exitcode.ErrIllegalState},
214+
{Idx: 7, Code: exitcode.ErrNotPayable},
215+
},
216+
},
217+
errorMsg: "index out of bounds",
218+
},
219+
{
220+
name: "initial gap exceeds successcount",
221+
batchReturn: BatchReturn{
222+
SuccessCount: 1,
223+
FailCodes: []FailCode{
224+
{Idx: 2, Code: exitcode.ErrIllegalArgument},
225+
},
226+
},
227+
errorMsg: "index out of bounds",
228+
},
229+
}
230+
231+
for _, tt := range tests {
232+
t.Run(tt.name, func(t *testing.T) {
233+
req := require.New(t)
234+
err := tt.batchReturn.Validate()
235+
// req.NoError(err)
236+
if tt.errorMsg != "" {
237+
req.ErrorContains(err, tt.errorMsg)
238+
} else {
239+
req.NoError(err)
240+
}
241+
})
242+
}
243+
}

0 commit comments

Comments
 (0)