Skip to content

Commit

Permalink
feat!: extract BatchReturn to batch package, add methods & tests (#286)
Browse files Browse the repository at this point in the history
  • Loading branch information
rvagg authored Sep 6, 2024
1 parent 9fa04b1 commit aff1b8d
Show file tree
Hide file tree
Showing 40 changed files with 768 additions and 3,141 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ gen:
$(GO_BIN) run ./gen/gen.go
$(GO_BIN) run ./manifest/gen/gen.go
$(GO_BIN) run ./proof/gen/gen.go
$(GO_BIN) run ./batch/gen/gen.go
$(GO_BIN) run ./builtin/v8/gen/gen.go
$(GO_BIN) run ./builtin/v9/gen/gen.go
$(GO_BIN) run ./builtin/v10/gen/gen.go
Expand Down
78 changes: 78 additions & 0 deletions batch/batch_return.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package batch

import (
"errors"

"github.com/filecoin-project/go-state-types/exitcode"
)

type BatchReturn struct {
SuccessCount uint64
FailCodes []FailCode
}

type FailCode struct {
Idx uint64
Code exitcode.ExitCode
}

func (b BatchReturn) Size() int {
return int(b.SuccessCount) + len(b.FailCodes)
}

func (b BatchReturn) AllOk() bool {
return len(b.FailCodes) == 0
}

func (b BatchReturn) Codes() []exitcode.ExitCode {
codes := make([]exitcode.ExitCode, b.Size())
i := 0
for _, fc := range b.FailCodes {
if fc.Idx > uint64(i) {
for ; i < int(fc.Idx); i++ {
codes[i] = exitcode.Ok
}
}
codes[i] = fc.Code
i++
}
for ; i < len(codes); i++ {
codes[i] = exitcode.Ok
}
return codes
}

func (b BatchReturn) CodeAt(n uint64) (exitcode.ExitCode, error) {
if n >= uint64(b.Size()) {
return exitcode.Ok, errors.New("index out of bounds")
}
for _, fc := range b.FailCodes {
if fc.Idx == n {
return fc.Code, nil
}
if fc.Idx > n {
return exitcode.Ok, nil
}
}
return exitcode.Ok, nil
}

func (b BatchReturn) Validate() error {
size := uint64(b.Size())
var gaps uint64
for i, fc := range b.FailCodes {
if fc.Idx >= size {
// will also catch the case where the gaps aren't accounted for in total size
return errors.New("index out of bounds")
}
if i > 0 {
if fc.Idx <= b.FailCodes[i-1].Idx {
return errors.New("fail codes are not in strictly increasing order")
}
gaps += fc.Idx - b.FailCodes[i-1].Idx - 1
} else {
gaps += fc.Idx
}
}
return nil
}
243 changes: 243 additions & 0 deletions batch/batch_return_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
package batch

import (
"bytes"
"encoding/hex"
"testing"

"github.com/filecoin-project/go-state-types/exitcode"
"github.com/stretchr/testify/require"
)

// Tests to match with Rust fil_actors_runtime::serialization
func TestSerializationBatchReturn(t *testing.T) {
testCases := []struct {
name string
params BatchReturn
hex string
}{
{
name: "empty",
params: BatchReturn{},
// [0,[]]
hex: "820080",
},
{
name: "single success",
params: BatchReturn{SuccessCount: 1},
// [1,[]]
hex: "820180",
},
{
name: "single failure",
params: BatchReturn{FailCodes: []FailCode{{Idx: 0, Code: exitcode.ErrIllegalArgument}}},
// [0,[[0,16]]]
hex: "820081820010",
},
{
name: "multiple success",
params: BatchReturn{SuccessCount: 2, FailCodes: []FailCode{
{Idx: 1, Code: exitcode.SysErrOutOfGas},
{Idx: 2, Code: exitcode.ErrIllegalState},
{Idx: 4, Code: exitcode.ErrIllegalArgument},
}},
// [2,[[1,7],[2,20],[4,16]]]
hex: "820283820107820214820410",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := require.New(t)

var buf bytes.Buffer
req.NoError(tc.params.MarshalCBOR(&buf))
req.Equal(tc.hex, hex.EncodeToString(buf.Bytes()))
var br BatchReturn
req.NoError(br.UnmarshalCBOR(&buf))
req.Equal(tc.params, br)
req.NoError(br.Validate())
})
}
}

func TestBatchReturn(t *testing.T) {
req := require.New(t)

t.Run("empty", func(t *testing.T) {
br := BatchReturn{}
req.Equal(0, br.Size())
req.True(br.AllOk())
req.Equal([]exitcode.ExitCode{}, br.Codes())
_, err := br.CodeAt(0)
req.Error(err, "index out of bounds")
req.NoError(br.Validate())
})

t.Run("single success", func(t *testing.T) {
br := BatchReturn{SuccessCount: 1}
req.Equal(1, br.Size())
req.True(br.AllOk())
req.Equal([]exitcode.ExitCode{exitcode.Ok}, br.Codes())
ec, err := br.CodeAt(0)
req.NoError(err)
req.Equal(exitcode.Ok, ec)
_, err = br.CodeAt(1)
req.Error(err, "index out of bounds")
req.NoError(br.Validate())
})

t.Run("single failure", func(t *testing.T) {
br := BatchReturn{FailCodes: []FailCode{{Idx: 0, Code: exitcode.ErrIllegalArgument}}}
req.Equal(1, br.Size())
req.False(br.AllOk())
req.Equal([]exitcode.ExitCode{exitcode.ErrIllegalArgument}, br.Codes())
ec, err := br.CodeAt(0)
req.NoError(err)
req.Equal(exitcode.ErrIllegalArgument, ec)
_, err = br.CodeAt(1)
req.Error(err, "index out of bounds")
req.NoError(br.Validate())
})

t.Run("multiple success", func(t *testing.T) {
br := BatchReturn{SuccessCount: 1, FailCodes: []FailCode{{Idx: 1, Code: exitcode.ErrIllegalArgument}}}
req.Equal(2, br.Size())
req.False(br.AllOk())
req.Equal([]exitcode.ExitCode{exitcode.Ok, exitcode.ErrIllegalArgument}, br.Codes())
ec, err := br.CodeAt(0)
req.NoError(err)
req.Equal(exitcode.Ok, ec)
ec, err = br.CodeAt(1)
req.NoError(err)
req.Equal(exitcode.ErrIllegalArgument, ec)
req.Equal(exitcode.Ok, br.Codes()[0])
_, err = br.CodeAt(2)
req.Error(err, "index out of bounds")
req.NoError(br.Validate())
})

t.Run("multiple failure", func(t *testing.T) {
br := BatchReturn{SuccessCount: 1, FailCodes: []FailCode{{Idx: 0, Code: exitcode.ErrForbidden}}}
req.Equal(2, br.Size())
req.False(br.AllOk())
req.Equal([]exitcode.ExitCode{exitcode.ErrForbidden, exitcode.Ok}, br.Codes())
ec, err := br.CodeAt(0)
req.NoError(err)
req.Equal(exitcode.ErrForbidden, ec)
ec, err = br.CodeAt(1)
req.NoError(err)
req.Equal(exitcode.Ok, ec)
_, err = br.CodeAt(2)
req.Error(err, "index out of bounds")
req.NoError(br.Validate())
})

t.Run("mixed", func(t *testing.T) {
br := BatchReturn{SuccessCount: 2, FailCodes: []FailCode{
{Idx: 1, Code: exitcode.SysErrOutOfGas},
{Idx: 2, Code: exitcode.ErrIllegalState},
{Idx: 4, Code: exitcode.ErrIllegalArgument},
}}
req.Equal(5, br.Size())
req.False(br.AllOk())
req.Equal([]exitcode.ExitCode{exitcode.Ok, exitcode.SysErrOutOfGas, exitcode.ErrIllegalState, exitcode.Ok, exitcode.ErrIllegalArgument}, br.Codes())
ec, err := br.CodeAt(0)
req.NoError(err)
req.Equal(exitcode.Ok, ec)
ec, err = br.CodeAt(1)
req.NoError(err)
req.Equal(exitcode.SysErrOutOfGas, ec)
ec, err = br.CodeAt(2)
req.NoError(err)
req.Equal(exitcode.ErrIllegalState, ec)
ec, err = br.CodeAt(3)
req.NoError(err)
req.Equal(exitcode.Ok, ec)
ec, err = br.CodeAt(4)
req.NoError(err)
req.Equal(exitcode.ErrIllegalArgument, ec)
_, err = br.CodeAt(5)
req.Error(err, "index out of bounds")
req.NoError(br.Validate())
})
}

func TestBatchReturn_Validate(t *testing.T) {
tests := []struct {
name string
batchReturn BatchReturn
errorMsg string
}{
{
name: "valid batchreturn",
batchReturn: BatchReturn{
SuccessCount: 5,
FailCodes: []FailCode{
{Idx: 1, Code: exitcode.ErrIllegalArgument},
{Idx: 3, Code: exitcode.ErrIllegalState},
{Idx: 6, Code: exitcode.ErrNotPayable},
},
},
},
{
name: "failcodes not in strictly increasing order",
batchReturn: BatchReturn{
SuccessCount: 5,
FailCodes: []FailCode{
{Idx: 1, Code: exitcode.ErrIllegalArgument},
{Idx: 3, Code: exitcode.ErrIllegalState},
{Idx: 3, Code: exitcode.ErrNotPayable},
},
},
errorMsg: "fail codes are not in strictly increasing order",
},
{
name: "failcodes contain index out of bounds",
batchReturn: BatchReturn{
SuccessCount: 5,
FailCodes: []FailCode{
{Idx: 1, Code: exitcode.ErrIllegalArgument},
{Idx: 3, Code: exitcode.ErrIllegalState},
{Idx: 10, Code: exitcode.ErrNotPayable},
},
},
errorMsg: "index out of bounds",
},
{
name: "gaps between failures exceed successcount",
batchReturn: BatchReturn{
SuccessCount: 2,
FailCodes: []FailCode{
{Idx: 1, Code: exitcode.ErrIllegalArgument},
{Idx: 4, Code: exitcode.ErrIllegalState},
{Idx: 7, Code: exitcode.ErrNotPayable},
},
},
errorMsg: "index out of bounds",
},
{
name: "initial gap exceeds successcount",
batchReturn: BatchReturn{
SuccessCount: 1,
FailCodes: []FailCode{
{Idx: 2, Code: exitcode.ErrIllegalArgument},
},
},
errorMsg: "index out of bounds",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := require.New(t)
err := tt.batchReturn.Validate()
// req.NoError(err)
if tt.errorMsg != "" {
req.ErrorContains(err, tt.errorMsg)
} else {
req.NoError(err)
}
})
}
}
Loading

0 comments on commit aff1b8d

Please sign in to comment.