diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 67ec15f8..02dd0e61 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: test: strategy: matrix: - go-version: [1.14.x, 1.18.x, 1.19.x] + go-version: [1.18.x, 1.19.x, 1.20.x, 1.21.x] os: [ubuntu-latest] runs-on: ${{ matrix.os }} timeout-minutes: 10 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 4e9fe058..3eff0426 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -21,7 +21,7 @@ jobs: linters: strategy: matrix: - go-version: [1.19.x] + go-version: [1.21.x] os: [ubuntu-latest] runs-on: ${{ matrix.os }} timeout-minutes: 10 @@ -35,5 +35,5 @@ jobs: - name: lint uses: golangci/golangci-lint-action@v3 with: - version: v1.50 + version: v1.55.1 args: --print-resources-usage --timeout=10m --verbose diff --git a/Makefile b/Makefile index 6f247ec0..dfcee7d2 100644 --- a/Makefile +++ b/Makefile @@ -55,8 +55,8 @@ ci: prepare if [ `arch` == 'x86_64' ]; then \ sudo apt-get -y -q update; \ sudo apt-get -y -q install build-essential; \ - wget -q https://github.com/tinygo-org/tinygo/releases/download/v0.26.0/tinygo_0.26.0_amd64.deb; \ - sudo dpkg -i tinygo_0.26.0_amd64.deb; \ + wget -q https://github.com/tinygo-org/tinygo/releases/download/v0.30.0/tinygo_0.30.0_amd64.deb; \ + sudo dpkg -i tinygo_0.30.0_amd64.deb; \ export PATH=$$PATH:/usr/local/tinygo/bin; \ fi go test -v ./... ./_generated diff --git a/_generated/allownil_test.go b/_generated/allownil_test.go new file mode 100644 index 00000000..cf18722f --- /dev/null +++ b/_generated/allownil_test.go @@ -0,0 +1,57 @@ +package _generated + +import ( + "bytes" + "reflect" + "testing" + + "github.com/tinylib/msgp/msgp" +) + +func TestAllownil(t *testing.T) { + tt := &NamedStructAN{ + A: []string{}, + B: nil, + } + var buf bytes.Buffer + + err := msgp.Encode(&buf, tt) + if err != nil { + t.Fatal(err) + } + in := buf.Bytes() + + for _, tnew := range []*NamedStructAN{{}, {A: []string{}}, {B: []string{}}} { + err = msgp.Decode(bytes.NewBuffer(in), tnew) + if err != nil { + t.Error(err) + } + + if !reflect.DeepEqual(tt, tnew) { + t.Logf("in: %#v", tt) + t.Logf("out: %#v", tnew) + t.Fatal("objects not equal") + } + } + + in, err = tt.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + for _, tanother := range []*NamedStructAN{{}, {A: []string{}}, {B: []string{}}} { + var left []byte + left, err = tanother.UnmarshalMsg(in) + if err != nil { + t.Error(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left", len(left)) + } + + if !reflect.DeepEqual(tt, tanother) { + t.Logf("in: %#v", tt) + t.Logf("out: %#v", tanother) + t.Fatal("objects not equal") + } + } +} diff --git a/_generated/def.go b/_generated/def.go index 0855f617..0360729a 100644 --- a/_generated/def.go +++ b/_generated/def.go @@ -46,27 +46,31 @@ type TestType struct { ValueA string `msg:"value_a"` ValueB []byte `msg:"value_b"` } `msg:"object"` - Child *TestType `msg:"child"` - Time time.Time `msg:"time"` - Any interface{} `msg:"any"` - Appended msgp.Raw `msg:"appended"` - Num msgp.Number `msg:"num"` - Byte byte - Rune rune - RunePtr *rune - RunePtrPtr **rune - RuneSlice []rune - Slice1 []string - Slice2 []string - SlicePtr *[]string + Child *TestType `msg:"child"` + Time time.Time `msg:"time"` + Any interface{} `msg:"any"` + Appended msgp.Raw `msg:"appended"` + Num msgp.Number `msg:"num"` + Byte byte + Rune rune + RunePtr *rune + RunePtrPtr **rune + RuneSlice []rune + Slice1 []string + Slice2 []string + SlicePtr *[]string + MapStringEmpty map[string]struct{} + MapStringEmpty2 map[string]EmptyStruct } //msgp:tuple Object type Object struct { - ObjectNo string `msg:"objno"` - Slice1 []string `msg:"slice1"` - Slice2 []string `msg:"slice2"` - MapMap map[string]map[string]string + ObjectNo string `msg:"objno"` + Slice1 []string `msg:"slice1"` + Slice2 []string `msg:"slice2"` + MapMap map[string]map[string]string + MapStringEmpty map[string]struct{} + MapStringEmpty2 map[string]EmptyStruct } //msgp:tuple TestBench @@ -266,8 +270,11 @@ type NonMsgStructTags struct { B string `json:"b"` C []string `json:"c"` VeryNested []struct { - A []string `json:"a"` - B []string `msg:"bbbb" xml:"-"` + A []string `json:"a"` + B []string `msg:"bbbb" xml:"-"` + C map[string]struct{} `msg:"cccc"` } } } + +type EmptyStruct struct{} diff --git a/_generated/gen_test.go b/_generated/gen_test.go index 6d911748..8fa2b7a8 100644 --- a/_generated/gen_test.go +++ b/_generated/gen_test.go @@ -68,6 +68,15 @@ func (a *TestType) Equal(b *TestType) bool { if !bytes.Equal(aa, ab) { return false } + if len(a.MapStringEmpty) == 0 && len(b.MapStringEmpty) == 0 { + a.MapStringEmpty = nil + b.MapStringEmpty = nil + } + if len(a.MapStringEmpty2) == 0 && len(b.MapStringEmpty2) == 0 { + a.MapStringEmpty2 = nil + b.MapStringEmpty2 = nil + } + a.Time, b.Time = time.Time{}, time.Time{} aa, ab = nil, nil ok := reflect.DeepEqual(a, b) @@ -97,9 +106,11 @@ func Test1EncodeDecode(t *testing.T) { ValueA: "here's the first inner value", ValueB: []byte("here's the second inner value"), }, - Child: nil, - Time: time.Now(), - Appended: msgp.Raw([]byte{}), // 'nil' + Child: nil, + Time: time.Now(), + Appended: msgp.Raw([]byte{}), // 'nil' + MapStringEmpty: map[string]struct{}{"Key": {}, "Key2": {}}, + MapStringEmpty2: map[string]EmptyStruct{"Key3": {}, "Key4": {}}, } var buf bytes.Buffer @@ -117,8 +128,8 @@ func Test1EncodeDecode(t *testing.T) { } if !tt.Equal(tnew) { - t.Logf("in: %v", tt) - t.Logf("out: %v", tnew) + t.Logf("in: %#v", tt) + t.Logf("out: %#v", tnew) t.Fatal("objects not equal") } diff --git a/_generated/issue94.go b/_generated/issue94.go index 4384d5d0..696ca4cc 100644 --- a/_generated/issue94.go +++ b/_generated/issue94.go @@ -6,16 +6,6 @@ import ( //go:generate msgp -// Issue 94: shims were not propogated recursively, -// which caused shims that weren't at the top level -// to be silently ignored. -// -// The following line will generate an error after -// the code is generated if the generated code doesn't -// have the right identifier in it. - -//go:generate ./search.sh $GOFILE timetostr - //msgp:shim time.Time as:string using:timetostr/strtotime type T struct { T time.Time diff --git a/_generated/issue94_test.go b/_generated/issue94_test.go new file mode 100644 index 00000000..38fec306 --- /dev/null +++ b/_generated/issue94_test.go @@ -0,0 +1,25 @@ +package _generated + +import ( + "bytes" + "os" + "testing" +) + +// Issue 94: shims were not propogated recursively, +// which caused shims that weren't at the top level +// to be silently ignored. +// +// The following line will generate an error after +// the code is generated if the generated code doesn't +// have the right identifier in it. +func TestIssue94(t *testing.T) { + b, err := os.ReadFile("issue94_gen.go") + if err != nil { + t.Fatal(err) + } + const want = "timetostr" + if !bytes.Contains(b, []byte(want)) { + t.Errorf("generated code did not contain %q", want) + } +} diff --git a/_generated/omitzero.go b/_generated/omitzero.go new file mode 100644 index 00000000..1732ffb7 --- /dev/null +++ b/_generated/omitzero.go @@ -0,0 +1,86 @@ +package _generated + +import "time" + +//go:generate msgp + +// check some specific cases for omitzero + +type OmitZero0 struct { + AStruct OmitZeroA `msg:"astruct,omitempty"` // leave this one omitempty + BStruct OmitZeroA `msg:"bstruct,omitzero"` // and compare to this + AStructPtr *OmitZeroA `msg:"astructptr,omitempty"` // a pointer case omitempty + BStructPtr *OmitZeroA `msg:"bstructptr,omitzero"` // a pointer case omitzero + AExt OmitZeroExt `msg:"aext,omitzero"` // external type case + AExtPtr *OmitZeroExtPtr `msg:"aextptr,omitzero"` // external type pointer case + + // more + APtrNamedStr *NamedStringOZ `msg:"aptrnamedstr,omitzero"` + ANamedStruct NamedStructOZ `msg:"anamedstruct,omitzero"` + APtrNamedStruct *NamedStructOZ `msg:"aptrnamedstruct,omitzero"` + EmbeddableStruct `msg:",flatten,omitzero"` // embed flat + EmbeddableStructOZ `msg:"embeddablestruct2,omitzero"` // embed non-flat + ATime time.Time `msg:"atime,omitzero"` + + OmitZeroTuple OmitZeroTuple `msg:"ozt"` // the inside of a tuple should ignore both omitempty and omitzero +} + +type OmitZeroA struct { + A string `msg:"a,omitempty"` + B NamedStringOZ `msg:"b,omitzero"` + C NamedStringOZ `msg:"c,omitzero"` +} + +func (o *OmitZeroA) IsZero() bool { + if o == nil { + return true + } + return *o == (OmitZeroA{}) +} + +type NamedStructOZ struct { + A string `msg:"a,omitempty"` + B string `msg:"b,omitempty"` +} + +func (ns *NamedStructOZ) IsZero() bool { + if ns == nil { + return true + } + return *ns == (NamedStructOZ{}) +} + +type NamedStringOZ string + +func (ns *NamedStringOZ) IsZero() bool { + if ns == nil { + return true + } + return *ns == "" +} + +type EmbeddableStructOZ struct { + SomeEmbed string `msg:"someembed2,omitempty"` +} + +func (es EmbeddableStructOZ) IsZero() bool { return es == (EmbeddableStructOZ{}) } + +type EmbeddableStructOZ2 struct { + SomeEmbed2 string `msg:"someembed2,omitempty"` +} + +func (es EmbeddableStructOZ2) IsZero() bool { return es == (EmbeddableStructOZ2{}) } + +//msgp:tuple OmitZeroTuple + +// OmitZeroTuple is flagged for tuple output, it should ignore all omitempty and omitzero functionality +// since it's fundamentally incompatible. +type OmitZeroTuple struct { + FieldA string `msg:"fielda,omitempty"` + FieldB NamedStringOZ `msg:"fieldb,omitzero"` + FieldC NamedStringOZ `msg:"fieldc,omitzero"` +} + +type OmitZero1 struct { + T1 OmitZeroTuple `msg:"t1"` +} diff --git a/_generated/omitzero_ext.go b/_generated/omitzero_ext.go new file mode 100644 index 00000000..f711fd96 --- /dev/null +++ b/_generated/omitzero_ext.go @@ -0,0 +1,114 @@ +package _generated + +import ( + "github.com/tinylib/msgp/msgp" +) + +// this has "external" types that will show up +// as generic IDENT during code generation + +type OmitZeroExt struct { + a int // custom type +} + +// IsZero will return true if a is not positive +func (o OmitZeroExt) IsZero() bool { return o.a <= 0 } + +// EncodeMsg implements msgp.Encodable +func (o OmitZeroExt) EncodeMsg(en *msgp.Writer) (err error) { + if o.a > 0 { + return en.WriteInt(o.a) + } + return en.WriteNil() +} + +// DecodeMsg implements msgp.Decodable +func (o *OmitZeroExt) DecodeMsg(dc *msgp.Reader) (err error) { + if dc.IsNil() { + err = dc.ReadNil() + if err != nil { + return + } + o.a = 0 + return + } + o.a, err = dc.ReadInt() + return err +} + +// MarshalMsg implements msgp.Marshaler +func (o OmitZeroExt) MarshalMsg(b []byte) (ret []byte, err error) { + ret = msgp.Require(b, o.Msgsize()) + if o.a > 0 { + return msgp.AppendInt(ret, o.a), nil + } + return msgp.AppendNil(ret), nil +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (o *OmitZeroExt) UnmarshalMsg(bts []byte) (ret []byte, err error) { + if msgp.IsNil(bts) { + bts, err = msgp.ReadNilBytes(bts) + return bts, err + } + o.a, bts, err = msgp.ReadIntBytes(bts) + return bts, err +} + +// Msgsize implements msgp.Msgsizer +func (o OmitZeroExt) Msgsize() (s int) { + return msgp.IntSize +} + +type OmitZeroExtPtr struct { + a int // custom type +} + +// IsZero will return true if a is nil or not positive +func (o *OmitZeroExtPtr) IsZero() bool { return o == nil || o.a <= 0 } + +// EncodeMsg implements msgp.Encodable +func (o *OmitZeroExtPtr) EncodeMsg(en *msgp.Writer) (err error) { + if o.a > 0 { + return en.WriteInt(o.a) + } + return en.WriteNil() +} + +// DecodeMsg implements msgp.Decodable +func (o *OmitZeroExtPtr) DecodeMsg(dc *msgp.Reader) (err error) { + if dc.IsNil() { + err = dc.ReadNil() + if err != nil { + return + } + o.a = 0 + return + } + o.a, err = dc.ReadInt() + return err +} + +// MarshalMsg implements msgp.Marshaler +func (o *OmitZeroExtPtr) MarshalMsg(b []byte) (ret []byte, err error) { + ret = msgp.Require(b, o.Msgsize()) + if o.a > 0 { + return msgp.AppendInt(ret, o.a), nil + } + return msgp.AppendNil(ret), nil +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (o *OmitZeroExtPtr) UnmarshalMsg(bts []byte) (ret []byte, err error) { + if msgp.IsNil(bts) { + bts, err = msgp.ReadNilBytes(bts) + return bts, err + } + o.a, bts, err = msgp.ReadIntBytes(bts) + return bts, err +} + +// Msgsize implements msgp.Msgsizer +func (o *OmitZeroExtPtr) Msgsize() (s int) { + return msgp.IntSize +} diff --git a/_generated/omitzero_test.go b/_generated/omitzero_test.go new file mode 100644 index 00000000..749f6ba5 --- /dev/null +++ b/_generated/omitzero_test.go @@ -0,0 +1,77 @@ +package _generated + +import ( + "bytes" + "testing" +) + +func TestOmitZero(t *testing.T) { + + t.Run("OmitZeroExt_not_empty", func(t *testing.T) { + + z := OmitZero0{AExt: OmitZeroExt{a: 1}} + b, err := z.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + if !bytes.Contains(b, []byte("aext")) { + t.Errorf("expected to find aext in bytes %X", b) + } + z = OmitZero0{} + _, err = z.UnmarshalMsg(b) + if err != nil { + t.Fatal(err) + } + if z.AExt.a != 1 { + t.Errorf("z.AExt.a expected 1 but got %d", z.AExt.a) + } + + }) + + t.Run("OmitZeroExt_negative", func(t *testing.T) { + + z := OmitZero0{AExt: OmitZeroExt{a: -1}} // negative value should act as empty, via IsEmpty() call + b, err := z.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + if bytes.Contains(b, []byte("aext")) { + t.Errorf("expected to not find aext in bytes %X", b) + } + z = OmitZero0{} + _, err = z.UnmarshalMsg(b) + if err != nil { + t.Fatal(err) + } + if z.AExt.a != 0 { + t.Errorf("z.AExt.a expected 0 but got %d", z.AExt.a) + } + + }) + + t.Run("OmitZeroTuple", func(t *testing.T) { + + // make sure tuple encoding isn't affected by omitempty or omitzero + + z := OmitZero0{OmitZeroTuple: OmitZeroTuple{FieldA: "", FieldB: "", FieldC: "fcval"}} + b, err := z.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + // verify the exact binary encoding, that the values follow each other without field names + if !bytes.Contains(b, []byte{0xA0, 0xA0, 0xA5, 'f', 'c', 'v', 'a', 'l'}) { + t.Errorf("failed to find expected bytes in %X", b) + } + z = OmitZero0{} + _, err = z.UnmarshalMsg(b) + if err != nil { + t.Fatal(err) + } + if z.OmitZeroTuple.FieldA != "" || + z.OmitZeroTuple.FieldB != "" || + z.OmitZeroTuple.FieldC != "fcval" { + t.Errorf("z.OmitZeroTuple unexpected value: %#v", z.OmitZeroTuple) + } + + }) +} diff --git a/_generated/search.sh b/_generated/search.sh deleted file mode 100755 index aa6d6477..00000000 --- a/_generated/search.sh +++ /dev/null @@ -1,12 +0,0 @@ -#! /bin/sh - -FILE=$(echo $1 | sed s/.go/_gen.go/) -echo "searching" $FILE "for" $2 -grep -q $2 $FILE -if [ $? -eq 0 ] -then - echo "OK" -else - echo "whoops!" - exit 1 -fi diff --git a/gen/decode.go b/gen/decode.go index aa845a1f..1a1d639b 100644 --- a/gen/decode.go +++ b/gen/decode.go @@ -84,15 +84,17 @@ func (d *decodeGen) structAsTuple(s *Struct) { if !d.p.ok() { return } - anField := s.Fields[i].HasTagPart("allownil") && s.Fields[i].FieldElem.AllowNil() + fieldElem := s.Fields[i].FieldElem + anField := s.Fields[i].HasTagPart("allownil") && fieldElem.AllowNil() if anField { d.p.print("\nif dc.IsNil() {") d.p.print("\nerr = dc.ReadNil()") d.p.wrapErrCheck(d.ctx.ArgsStr()) d.p.printf("\n%s = nil\n} else {", s.Fields[i].FieldElem.Varname()) } + SetIsAllowNil(fieldElem, anField) d.ctx.PushString(s.Fields[i].FieldName) - next(d, s.Fields[i].FieldElem) + next(d, fieldElem) d.ctx.Pop() if anField { d.p.printf("\n}") // close if statement @@ -112,14 +114,16 @@ func (d *decodeGen) structAsMap(s *Struct) { for i := range s.Fields { d.ctx.PushString(s.Fields[i].FieldName) d.p.printf("\ncase \"%s\":", s.Fields[i].FieldTag) - anField := s.Fields[i].HasTagPart("allownil") && s.Fields[i].FieldElem.AllowNil() + fieldElem := s.Fields[i].FieldElem + anField := s.Fields[i].HasTagPart("allownil") && fieldElem.AllowNil() if anField { d.p.print("\nif dc.IsNil() {") d.p.print("\nerr = dc.ReadNil()") d.p.wrapErrCheck(d.ctx.ArgsStr()) - d.p.printf("\n%s = nil\n} else {", s.Fields[i].FieldElem.Varname()) + d.p.printf("\n%s = nil\n} else {", fieldElem.Varname()) } - next(d, s.Fields[i].FieldElem) + SetIsAllowNil(fieldElem, anField) + next(d, fieldElem) d.ctx.Pop() if !d.p.ok() { return @@ -155,7 +159,8 @@ func (d *decodeGen) gBase(b *BaseElem) { switch b.Value { case Bytes: if b.Convert { - d.p.printf("\n%s, err = dc.ReadBytes([]byte(%s))", tmp, vname) + lowered := b.ToBase() + "(" + vname + ")" + d.p.printf("\n%s, err = dc.ReadBytes(%s)", tmp, lowered) } else { d.p.printf("\n%s, err = dc.ReadBytes(%s)", vname, vname) } @@ -196,6 +201,7 @@ func (d *decodeGen) gMap(m *Map) { // for element in map, read string/value // pair and assign + d.needsField() d.p.printf("\nfor %s > 0 {\n%s--", sz, sz) d.p.declare(m.Keyidx, "string") d.p.declare(m.Validx, m.Value.TypeName()) @@ -214,7 +220,11 @@ func (d *decodeGen) gSlice(s *Slice) { sz := randIdent() d.p.declare(sz, u32) d.assignAndCheck(sz, arrayHeader) - d.p.resizeSlice(sz, s) + if s.isAllowNil { + d.p.resizeSliceNoNil(sz, s) + } else { + d.p.resizeSlice(sz, s) + } d.p.rangeBlock(d.ctx, s.Index, s.Varname(), d, s.Els) } diff --git a/gen/elem.go b/gen/elem.go index 174d8a5a..6d5d56c6 100644 --- a/gen/elem.go +++ b/gen/elem.go @@ -190,11 +190,13 @@ type Elem interface { // This is true for slices and maps. AllowNil() bool - // IfZeroExpr returns the expression to compare to zero/empty - // for this type. It is meant to be used in an if statement + // IfZeroExpr returns the expression to compare to an empty value + // for this type, per the rules of the `omitempty` feature. + // It is meant to be used in an if statement // and may include the simple statement form followed by // semicolon and then the expression. // Returns "" if zero/empty not supported for this Elem. + // Note that this is NOT used by the `omitzero` feature. IfZeroExpr() string hidden() @@ -258,9 +260,10 @@ func (a *Array) IfZeroExpr() string { return "" } // Map is a map[string]Elem type Map struct { common - Keyidx string // key variable name - Validx string // value variable name - Value Elem // value element + Keyidx string // key variable name + Validx string // value variable name + Value Elem // value element + isAllowNil bool KeyType string // key type } @@ -303,6 +306,9 @@ func (m *Map) IfZeroExpr() string { return m.Varname() + " == nil" } // AllowNil is true for maps. func (m *Map) AllowNil() bool { return true } +// SetIsAllowNil sets whether the map is allowed to be nil. +func (m *Map) SetIsAllowNil(b bool) { m.isAllowNil = b } + func (m *Map) KeyStringExpr() string { if m.KeyType == "string" { return m.Keyidx @@ -329,8 +335,9 @@ func (m *Map) KeySizeExpr() string { type Slice struct { common - Index string - Els Elem // The type of each element + Index string + isAllowNil bool + Els Elem // The type of each element } func (s *Slice) SetVarname(a string) { @@ -371,6 +378,19 @@ func (s *Slice) IfZeroExpr() string { return s.Varname() + " == nil" } // AllowNil is true for slices. func (s *Slice) AllowNil() bool { return true } +// SetIsAllowNil sets whether the slice is allowed to be nil. +func (s *Slice) SetIsAllowNil(b bool) { s.isAllowNil = b } + +// SetIsAllowNil will set whether the element is allowed to be nil. +func SetIsAllowNil(e Elem, b bool) { + type i interface { + SetIsAllowNil(b bool) + } + if x, ok := e.(i); ok { + x.SetIsAllowNil(b) + } +} + type Ptr struct { common Value Elem diff --git a/gen/encode.go b/gen/encode.go index 9fe34374..3369b08d 100644 --- a/gen/encode.go +++ b/gen/encode.go @@ -92,12 +92,14 @@ func (e *encodeGen) tuple(s *Struct) { if !e.p.ok() { return } - anField := s.Fields[i].HasTagPart("allownil") && s.Fields[i].FieldElem.AllowNil() + fieldElem := s.Fields[i].FieldElem + anField := s.Fields[i].HasTagPart("allownil") && fieldElem.AllowNil() if anField { - e.p.printf("\nif %s { // allownil: if nil", s.Fields[i].FieldElem.IfZeroExpr()) + e.p.printf("\nif %s { // allownil: if nil", fieldElem.IfZeroExpr()) e.p.printf("\nerr = en.WriteNil(); if err != nil { return; }") e.p.printf("\n} else {") } + SetIsAllowNil(fieldElem, anField) e.ctx.PushString(s.Fields[i].FieldName) next(e, s.Fields[i].FieldElem) e.ctx.Pop() @@ -129,12 +131,13 @@ func (e *encodeGen) structmap(s *Struct) { } omitempty := s.AnyHasTagPart("omitempty") + omitzero := s.AnyHasTagPart("omitzero") var fieldNVar string - if omitempty { + if omitempty || omitzero { fieldNVar = oeIdentPrefix + "Len" - e.p.printf("\n// omitempty: check for empty values") + e.p.printf("\n// check for omitted fields") e.p.printf("\n%s := uint32(%d)", fieldNVar, nfields) e.p.printf("\n%s", bm.typeDecl()) e.p.printf("\n_ = %s", bm.varname) @@ -147,6 +150,11 @@ func (e *encodeGen) structmap(s *Struct) { e.p.printf("\n%s--", fieldNVar) e.p.printf("\n%s", bm.setStmt(i)) e.p.printf("\n}") + } else if sf.HasTagPart("omitzero") { + e.p.printf("\nif %s.IsZero() {", sf.FieldElem.Varname()) + e.p.printf("\n%s--", fieldNVar) + e.p.printf("\n%s", bm.setStmt(i)) + e.p.printf("\n}") } } @@ -164,11 +172,12 @@ func (e *encodeGen) structmap(s *Struct) { } else { - // non-omitempty version + // non-omit version data = msgp.AppendMapHeader(nil, uint32(nfields)) e.p.printf("\n// map header, size %d", nfields) e.Fuse(data) if len(s.Fields) == 0 { + e.p.printf("\n_ = %s", s.vname) e.fuseHook() } @@ -179,23 +188,26 @@ func (e *encodeGen) structmap(s *Struct) { return } - // if field is omitempty, wrap with if statement based on the emptymask - oeField := omitempty && s.Fields[i].HasTagPart("omitempty") && s.Fields[i].FieldElem.IfZeroExpr() != "" + // if field is omitempty or omitzero, wrap with if statement based on the emptymask + oeField := (omitempty || omitzero) && + ((s.Fields[i].HasTagPart("omitempty") && s.Fields[i].FieldElem.IfZeroExpr() != "") || + s.Fields[i].HasTagPart("omitzero")) if oeField { - e.p.printf("\nif %s == 0 { // if not empty", bm.readExpr(i)) + e.p.printf("\nif %s == 0 { // if not omitted", bm.readExpr(i)) } data = msgp.AppendString(nil, s.Fields[i].FieldTag) e.p.printf("\n// write %q", s.Fields[i].FieldTag) e.Fuse(data) e.fuseHook() - - anField := !oeField && s.Fields[i].HasTagPart("allownil") && s.Fields[i].FieldElem.AllowNil() + fieldElem := s.Fields[i].FieldElem + anField := !oeField && s.Fields[i].HasTagPart("allownil") && fieldElem.AllowNil() if anField { e.p.printf("\nif %s { // allownil: if nil", s.Fields[i].FieldElem.IfZeroExpr()) e.p.printf("\nerr = en.WriteNil(); if err != nil { return; }") e.p.printf("\n} else {") } + SetIsAllowNil(fieldElem, anField) e.ctx.PushString(s.Fields[i].FieldName) next(e, s.Fields[i].FieldElem) diff --git a/gen/marshal.go b/gen/marshal.go index 72937118..d440099b 100644 --- a/gen/marshal.go +++ b/gen/marshal.go @@ -98,14 +98,16 @@ func (m *marshalGen) tuple(s *Struct) { if !m.p.ok() { return } - anField := s.Fields[i].HasTagPart("allownil") && s.Fields[i].FieldElem.AllowNil() + fieldElem := s.Fields[i].FieldElem + anField := s.Fields[i].HasTagPart("allownil") && fieldElem.AllowNil() if anField { - m.p.printf("\nif %s { // allownil: if nil", s.Fields[i].FieldElem.IfZeroExpr()) + m.p.printf("\nif %s { // allownil: if nil", fieldElem.IfZeroExpr()) m.p.printf("\no = msgp.AppendNil(o)") m.p.printf("\n} else {") } m.ctx.PushString(s.Fields[i].FieldName) - next(m, s.Fields[i].FieldElem) + SetIsAllowNil(fieldElem, anField) + next(m, fieldElem) m.ctx.Pop() if anField { m.p.printf("\n}") // close if statement @@ -124,12 +126,13 @@ func (m *marshalGen) mapstruct(s *Struct) { } omitempty := s.AnyHasTagPart("omitempty") + omitzero := s.AnyHasTagPart("omitzero") var fieldNVar string - if omitempty { + if omitempty || omitzero { fieldNVar = oeIdentPrefix + "Len" - m.p.printf("\n// omitempty: check for empty values") + m.p.printf("\n// check for omitted fields") m.p.printf("\n%s := uint32(%d)", fieldNVar, nfields) m.p.printf("\n%s", bm.typeDecl()) m.p.printf("\n_ = %s", bm.varname) @@ -142,6 +145,11 @@ func (m *marshalGen) mapstruct(s *Struct) { m.p.printf("\n%s--", fieldNVar) m.p.printf("\n%s", bm.setStmt(i)) m.p.printf("\n}") + } else if sf.HasTagPart("omitzero") { + m.p.printf("\nif %s.IsZero() {", sf.FieldElem.Varname()) + m.p.printf("\n%s--", fieldNVar) + m.p.printf("\n%s", bm.setStmt(i)) + m.p.printf("\n}") } } @@ -164,6 +172,7 @@ func (m *marshalGen) mapstruct(s *Struct) { m.p.printf("\n// map header, size %d", len(s.Fields)) m.Fuse(data) if len(s.Fields) == 0 { + m.p.printf("\n_ = %s", s.vname) m.fuseHook() } @@ -174,10 +183,12 @@ func (m *marshalGen) mapstruct(s *Struct) { return } - // if field is omitempty, wrap with if statement based on the emptymask - oeField := s.Fields[i].HasTagPart("omitempty") && s.Fields[i].FieldElem.IfZeroExpr() != "" + // if field is omitempty or omitzero, wrap with if statement based on the emptymask + oeField := (omitempty || omitzero) && + ((s.Fields[i].HasTagPart("omitempty") && s.Fields[i].FieldElem.IfZeroExpr() != "") || + s.Fields[i].HasTagPart("omitzero")) if oeField { - m.p.printf("\nif %s == 0 { // if not empty", bm.readExpr(i)) + m.p.printf("\nif %s == 0 { // if not omitted", bm.readExpr(i)) } data = msgp.AppendString(nil, s.Fields[i].FieldTag) @@ -186,15 +197,16 @@ func (m *marshalGen) mapstruct(s *Struct) { m.Fuse(data) m.fuseHook() - anField := !oeField && s.Fields[i].HasTagPart("allownil") && s.Fields[i].FieldElem.AllowNil() + fieldElem := s.Fields[i].FieldElem + anField := !oeField && s.Fields[i].HasTagPart("allownil") && fieldElem.AllowNil() if anField { - m.p.printf("\nif %s { // allownil: if nil", s.Fields[i].FieldElem.IfZeroExpr()) + m.p.printf("\nif %s { // allownil: if nil", fieldElem.IfZeroExpr()) m.p.printf("\no = msgp.AppendNil(o)") m.p.printf("\n} else {") } - m.ctx.PushString(s.Fields[i].FieldName) - next(m, s.Fields[i].FieldElem) + SetIsAllowNil(fieldElem, anField) + next(m, fieldElem) m.ctx.Pop() if oeField || anField { diff --git a/gen/spec.go b/gen/spec.go index f3a9ee31..3d6e04c6 100644 --- a/gen/spec.go +++ b/gen/spec.go @@ -360,6 +360,13 @@ func (p *printer) resizeSlice(size string, s *Slice) { p.printf("\nif cap(%[1]s) >= int(%[2]s) { %[1]s = (%[1]s)[:%[2]s] } else { %[1]s = make(%[3]s, %[2]s) }", s.Varname(), size, s.TypeName()) } +// resizeSliceNoNil will resize a slice and will not allow nil slices. +func (p *printer) resizeSliceNoNil(size string, s *Slice) { + p.printf("\nif %[1]s != nil && cap(%[1]s) >= int(%[2]s) {", s.Varname(), size) + p.printf("\n%[1]s = (%[1]s)[:%[2]s]", s.Varname(), size) + p.printf("\n} else { %[1]s = make(%[3]s, %[2]s) }", s.Varname(), size, s.TypeName()) +} + func (p *printer) arrayCheck(want string, got string) { p.printf("\nif %[1]s != %[2]s { err = msgp.ArrayError{Wanted: %[2]s, Got: %[1]s}; return }", got, want) } diff --git a/gen/unmarshal.go b/gen/unmarshal.go index bb643a56..c179208f 100644 --- a/gen/unmarshal.go +++ b/gen/unmarshal.go @@ -84,11 +84,13 @@ func (u *unmarshalGen) tuple(s *Struct) { return } u.ctx.PushString(s.Fields[i].FieldName) - anField := s.Fields[i].HasTagPart("allownil") && s.Fields[i].FieldElem.AllowNil() + fieldElem := s.Fields[i].FieldElem + anField := s.Fields[i].HasTagPart("allownil") && fieldElem.AllowNil() if anField { - u.p.printf("\nif msgp.IsNil(bts) {\nbts = bts[1:]\n%s = nil\n} else {", s.Fields[i].FieldElem.Varname()) + u.p.printf("\nif msgp.IsNil(bts) {\nbts = bts[1:]\n%s = nil\n} else {", fieldElem.Varname()) } - next(u, s.Fields[i].FieldElem) + SetIsAllowNil(fieldElem, anField) + next(u, fieldElem) u.ctx.Pop() if anField { u.p.printf("\n}") @@ -113,11 +115,13 @@ func (u *unmarshalGen) mapstruct(s *Struct) { u.p.printf("\ncase \"%s\":", s.Fields[i].FieldTag) u.ctx.PushString(s.Fields[i].FieldName) - anField := s.Fields[i].HasTagPart("allownil") && s.Fields[i].FieldElem.AllowNil() + fieldElem := s.Fields[i].FieldElem + anField := s.Fields[i].HasTagPart("allownil") && fieldElem.AllowNil() if anField { - u.p.printf("\nif msgp.IsNil(bts) {\nbts = bts[1:]\n%s = nil\n} else {", s.Fields[i].FieldElem.Varname()) + u.p.printf("\nif msgp.IsNil(bts) {\nbts = bts[1:]\n%s = nil\n} else {", fieldElem.Varname()) } - next(u, s.Fields[i].FieldElem) + SetIsAllowNil(fieldElem, anField) + next(u, fieldElem) u.ctx.Pop() if anField { u.p.printf("\n}") @@ -193,7 +197,11 @@ func (u *unmarshalGen) gSlice(s *Slice) { sz := randIdent() u.p.declare(sz, u32) u.assignAndCheck(sz, arrayHeader) - u.p.resizeSlice(sz, s) + if s.isAllowNil { + u.p.resizeSliceNoNil(sz, s) + } else { + u.p.resizeSlice(sz, s) + } u.p.rangeBlock(u.ctx, s.Index, s.Varname(), u, s.Els) } @@ -208,6 +216,10 @@ func (u *unmarshalGen) gMap(m *Map) { // allocate or clear map u.p.resizeMap(sz, m) + // We likely need a field. + // Add now to not be inside for scope. + u.needsField() + // loop and get key,value u.p.printf("\nfor %s > 0 {", sz) u.p.printf("\nvar %s string; var %s %s; %s--", m.Keyidx, m.Validx, m.Value.TypeName(), sz) diff --git a/go.mod b/go.mod index 84ec98e5..a4a43b16 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,13 @@ module github.com/tinylib/msgp -go 1.15 +go 1.18 require ( github.com/philhofer/fwd v1.1.2 - golang.org/x/tools v0.4.0 + golang.org/x/tools v0.14.0 +) + +require ( + golang.org/x/mod v0.13.0 // indirect + golang.org/x/sys v0.13.0 // indirect ) diff --git a/go.sum b/go.sum index 60bca288..1cff9254 100644 --- a/go.sum +++ b/go.sum @@ -1,36 +1,8 @@ github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4= -golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= diff --git a/msgp/advise_other.go b/msgp/advise_other.go index 1b6ed572..07f524af 100644 --- a/msgp/advise_other.go +++ b/msgp/advise_other.go @@ -1,5 +1,5 @@ -//go:build (!linux && !tinygo) || appengine -// +build !linux,!tinygo appengine +//go:build (!linux && !tinygo && !windows) || appengine +// +build !linux,!tinygo,!windows appengine package msgp diff --git a/msgp/write_bytes.go b/msgp/write_bytes.go index 676a6efe..8199ac28 100644 --- a/msgp/write_bytes.go +++ b/msgp/write_bytes.go @@ -1,6 +1,7 @@ package msgp import ( + "errors" "math" "reflect" "time" @@ -342,10 +343,10 @@ func AppendMapStrIntf(b []byte, m map[string]interface{}) ([]byte, error) { // provided []byte. 'i' must be one of the following: // - 'nil' // - A bool, float, string, []byte, int, uint, or complex -// - A map[string]interface{} or map[string]string +// - A map[string]T where T is another supported type // - A []T, where T is another supported type // - A *T, where T is another supported type -// - A type that satisfieds the msgp.Marshaler interface +// - A type that satisfies the msgp.Marshaler interface // - A type that satisfies the msgp.Extension interface func AppendIntf(b []byte, i interface{}) ([]byte, error) { if i == nil { @@ -414,6 +415,21 @@ func AppendIntf(b []byte, i interface{}) ([]byte, error) { var err error v := reflect.ValueOf(i) switch v.Kind() { + case reflect.Map: + if v.Type().Key().Kind() != reflect.String { + return b, errors.New("msgp: map keys must be strings") + } + ks := v.MapKeys() + b = AppendMapHeader(b, uint32(len(ks))) + for _, key := range ks { + val := v.MapIndex(key) + b = AppendString(b, key.String()) + b, err = AppendIntf(b, val.Interface()) + if err != nil { + return nil, err + } + } + return b, nil case reflect.Array, reflect.Slice: l := v.Len() b = AppendArrayHeader(b, uint32(l)) diff --git a/msgp/write_bytes_test.go b/msgp/write_bytes_test.go index 69866fe1..93e13ff6 100644 --- a/msgp/write_bytes_test.go +++ b/msgp/write_bytes_test.go @@ -3,6 +3,8 @@ package msgp import ( "bytes" "math" + "reflect" + "strings" "testing" "time" ) @@ -348,3 +350,134 @@ func BenchmarkAppendTime(b *testing.B) { AppendTime(buf[0:0], t) } } + +// TestEncodeDecode does a back-and-forth test of encoding and decoding and compare the value with a given output. +func TestEncodeDecode(t *testing.T) { + for _, tc := range []struct { + name string + input interface{} + output interface{} + encodeError string + }{ + { + name: "nil", + input: nil, + }, + { + name: "bool", + input: true, + }, + { + name: "int", + input: int64(42), + }, + { + name: "float", + input: 3.14159, + }, + { + name: "string", + input: "hello", + }, + { + name: "bytes", + input: []byte("hello"), + }, + { + name: "array-empty", + input: []interface{}{}, + }, + { + name: "array", + input: []interface{}{int64(1), int64(2), int64(3)}, + }, + { + name: "map-empty", + input: map[string]interface{}{}, + }, + { + name: "map", + input: map[string]interface{}{"a": int64(1), "b": int64(2)}, + }, + { + name: "map-interface", + input: map[string]interface{}{"a": int64(1), "b": "2"}, + }, + { + name: "map-string", + input: map[string]string{"a": "1", "b": "2"}, + output: map[string]interface{}{"a": "1", "b": "2"}, + }, + { + name: "map-array", + input: map[string][]int64{"a": {1, 2}, "b": {3}}, + output: map[string]interface{}{"a": []interface{}{int64(1), int64(2)}, "b": []interface{}{int64(3)}}, + }, + { + name: "map-map", + input: map[string]map[string]int64{"a": {"a": 1, "b": 2}, "b": {"c": 3}}, + output: map[string]interface{}{"a": map[string]interface{}{"a": int64(1), "b": int64(2)}, "b": map[string]interface{}{"c": int64(3)}}, + }, + { + name: "array-map", + input: []interface{}{map[string]interface{}{"a": int64(1), "b": "2"}, map[string]int64{"c": 3}}, + output: []interface{}{map[string]interface{}{"a": int64(1), "b": "2"}, map[string]interface{}{"c": int64(3)}}, + }, + { + name: "array-array", + input: []interface{}{[]int64{1, 2}, []interface{}{int64(3)}}, + output: []interface{}{[]interface{}{int64(1), int64(2)}, []interface{}{int64(3)}}, + }, + { + name: "array-array-map", + input: []interface{}{[]interface{}{int64(1), int64(2)}, map[string]interface{}{"c": int64(3)}}, + }, + { + name: "map-array-map", + input: map[string]interface{}{"a": []interface{}{int64(1), int64(2)}, "b": map[string]interface{}{"c": int64(3)}}, + }, + { + name: "map-invalid-keys", + input: map[interface{}]interface{}{int64(1): int64(2)}, + encodeError: "msgp: map keys must be strings", + }, + { + name: "map-nested-invalid-keys", + input: map[string]interface{}{"a": map[int64]string{1: "2"}}, + encodeError: "msgp: map keys must be strings", + }, + { + name: "invalid-type", + input: struct{}{}, + encodeError: "msgp: type \"struct {}\" not supported", + }, + } { + t.Run(tc.name, func(t *testing.T) { + // If no output is given, use the input as output + if tc.output == nil { + tc.output = tc.input + } + + buf, err := AppendIntf(nil, tc.input) + if tc.encodeError != "" { + if err == nil || !strings.Contains(err.Error(), tc.encodeError) { + t.Fatalf("expected encode error '%s' but got '%s'", tc.encodeError, err) + } + return + } + + if tc.encodeError == "" && err != nil { + t.Fatalf("expected no encode error but got '%s'", err.Error()) + } + + out, _, _ := ReadIntfBytes(buf) + if err != nil { + t.Fatalf("expected no decode error but got '%s'", err.Error()) + } + + if !reflect.DeepEqual(tc.output, out) { + t.Fatalf("expected '%v' but got '%v'", tc.input, out) + } + }) + } +} diff --git a/tinygotest/tinygo_test.go b/tinygotest/tinygo_test.go index dd4d96bd..2659abed 100644 --- a/tinygotest/tinygo_test.go +++ b/tinygotest/tinygo_test.go @@ -1,5 +1,4 @@ -//go:build (amd64 && go1.18) || (darwin && go1.18) -// +build amd64,go1.18 darwin,go1.18 +//go:build amd64 || darwin package tinygotest @@ -12,6 +11,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "testing" ) @@ -148,6 +148,9 @@ var buildOnlyTargets = []string{ func run(t *testing.T, dir, exe string, args ...string) { t.Helper() + if runtime.GOOS == "windows" { + exe += ".exe" + } cmd := exec.Command("./" + exe) wd, err := os.Getwd() if err != nil { @@ -202,12 +205,19 @@ func tinygoBuild(t *testing.T, dir string, targets ...string) { if tgt == "wasm" { ext = ".wasm" } + dst := tgt + ext + if tgt == "" { + dst = "nativebin" + if runtime.GOOS == "windows" { + dst += ".exe" + } + } var args []string if tgt == "" { // empty target means the native platform - args = []string{"build", "-o=nativebin", "."} + args = []string{"build", "-o=" + dst, "."} } else { - args = []string{"build", "-target=" + tgt, "-o=" + tgt + ext, "."} + args = []string{"build", "-target=" + tgt, "-o=" + dst, "."} } t.Logf("%s: tinygo %v", dir, args) @@ -218,6 +228,10 @@ func tinygoBuild(t *testing.T, dir string, targets ...string) { t.Logf("%s: tinygo build %v; output: %s", dir, args, b) } if err != nil { + // See https://github.com/tinygo-org/tinygo/issues/3977 + if strings.Contains(string(b), "could not find wasm-opt") { + t.Skipf("skipping wasm test because wasm-opt is not installed") + } t.Fatal(err) } }