Skip to content

Commit 795645f

Browse files
authored
Merge pull request #6880 from onflow/jord/cbor-codec-behaviour
Add test cases documenting CBOR's behaviour with omitted and extra fields
2 parents a550d37 + f03b83e commit 795645f

File tree

1 file changed

+139
-0
lines changed

1 file changed

+139
-0
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package cbor
2+
3+
import (
4+
"testing"
5+
6+
"github.com/fxamacker/cbor/v2"
7+
"github.com/stretchr/testify/assert"
8+
9+
cborcodec "github.com/onflow/flow-go/model/encoding/cbor"
10+
)
11+
12+
// The CBOR network codec uses the [cbor.ExtraDecErrorUnknownField] option, which
13+
// causes decoding to return an error when decoding a message which contains an
14+
// extra field, not present in the target (struct into which we are decoding).
15+
//
16+
// This test validates this behaviour.
17+
func TestBehaviour_DecodeExtraField(t *testing.T) {
18+
t.Run("decoding NON-ZERO VALUE of extra field not present in the target struct is forbidden", func(t *testing.T) {
19+
type model1 struct {
20+
A int
21+
}
22+
type model2 struct {
23+
A int
24+
B int
25+
}
26+
27+
m2 := model2{
28+
A: 100,
29+
B: 200,
30+
}
31+
bz, err := cborcodec.EncMode.Marshal(m2)
32+
assert.NoError(t, err)
33+
34+
var m1 model1
35+
err = defaultDecMode.Unmarshal(bz, &m1)
36+
assert.Error(t, err)
37+
target := &cbor.UnknownFieldError{}
38+
assert.ErrorAs(t, err, &target)
39+
})
40+
41+
t.Run("decoding ZERO VALUE of extra field not present in the target struct is forbidden", func(t *testing.T) {
42+
type model1 struct {
43+
A *int
44+
}
45+
type model2 struct {
46+
A *int
47+
B *int
48+
}
49+
50+
a := 100
51+
m2 := model2{
52+
A: &a,
53+
// B has zero-value
54+
}
55+
bz, err := cborcodec.EncMode.Marshal(m2)
56+
assert.NoError(t, err)
57+
58+
var m1 model1
59+
err = defaultDecMode.Unmarshal(bz, &m1)
60+
assert.Error(t, err)
61+
target := &cbor.UnknownFieldError{}
62+
assert.ErrorAs(t, err, &target)
63+
})
64+
}
65+
66+
// The CBOR network codec uses the [cbor.ExtraDecErrorUnknownField] option, which
67+
// causes decoding to return an error when decoding a message which contains an
68+
// extra field, not present in the target (struct into which we are decoding).
69+
//
70+
// This test validates that, when decoding a message which OMITS a field present
71+
// in the target, no error is returned.
72+
//
73+
// This behaviour is very useful for downwards compatibility: for example if we add
74+
// a new field B to a struct, nodes running the updated software can still decode
75+
// messages emitted by the old software - with the convention that in the decoded
76+
// message, field B has the zero-value.
77+
// However, note that the reverse (i.e. downwards compatibility) is not true *by default*
78+
// Specifically the old software cannot decode the new struct, even if field B has the
79+
// zero value, as demonstrated by the test [TestBehaviour_DecodeExtraField] above.
80+
//
81+
// Nevertheless, downwards compatibility can be improved with suitable conventions
82+
// as demonstrated in the test [TestBehaviour_OmittingNewFieldForDownwardsCompatibility] below
83+
func TestBehaviour_DecodeOmittedField(t *testing.T) {
84+
type model1 struct {
85+
A int
86+
}
87+
type model2 struct {
88+
A int
89+
B int
90+
}
91+
92+
m1 := model1{
93+
A: 100,
94+
}
95+
bz, err := cborcodec.EncMode.Marshal(m1)
96+
assert.NoError(t, err)
97+
98+
var m2 model2
99+
err = defaultDecMode.Unmarshal(bz, &m2)
100+
assert.NoError(t, err)
101+
assert.Equal(t, m2.A, m1.A)
102+
assert.Equal(t, m2.B, int(0))
103+
}
104+
105+
// This test demonstrates a possible convention for improving downwards compatibility,
106+
// when we want to add a new field to an existing struct. Let's say that the struct
107+
// `model1` describes the old data structure, to which we want to add a new integer
108+
// field `B`.
109+
// Note that the following pattern only works out of the box, if field `B` is required
110+
// according to the new protocol convention. In other words, the new software can
111+
// differentiate between the old and the new data model based on whether field `B`
112+
// is present.
113+
// The important aspects are
114+
// 1. define field `B` as a pointer variable. Thereby, the new software can represent
115+
// an old data model with `B` being nil, while the new data model always has `B` ≠ nil.
116+
// 2. In the new software, provide the cbor directive `cbor:",omitempty"`, which instructs
117+
// cbor to omit the field entirely during the encoding step. Thereby the new software
118+
// reproduces the encoding of the old software when dealing with the old data model.
119+
func TestBehaviour_OmittingNewFieldForDownwardsCompatibility(t *testing.T) {
120+
type model1 struct {
121+
A int
122+
}
123+
type model2 struct {
124+
A int
125+
B *int `cbor:",omitempty"`
126+
}
127+
128+
a := 100
129+
m2 := model2{
130+
A: a,
131+
}
132+
// m2.B is `nil`, which cbor will omit in the encoding step according to our directive "omitempty"
133+
bz, err := cborcodec.EncMode.Marshal(m2)
134+
assert.NoError(t, err)
135+
136+
var m1 model1
137+
err = defaultDecMode.Unmarshal(bz, &m1)
138+
assert.NoError(t, err)
139+
}

0 commit comments

Comments
 (0)