Skip to content

Commit

Permalink
chore: add side-by-side tests with official proto.Marshal and Unmarshal
Browse files Browse the repository at this point in the history
This test ensures that our encoding is compatible with official proto modules. It doesn't verify that binary form is equivalent because we do encode zero values, unlike official encoder.

Also add simple fuzzing.

Closes #2

Signed-off-by: Dmitriy Matrenichev <dmitry.matrenichev@siderolabs.com>
  • Loading branch information
DmitriyMV committed Jul 27, 2022
1 parent 2519db3 commit ab9b1ff
Show file tree
Hide file tree
Showing 10 changed files with 991 additions and 10 deletions.
3 changes: 2 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2022-07-21T21:54:08Z by kres latest.
# Generated on 2022-07-26T15:06:36Z by kres latest.

**
!messages
!array_test.go
!benchmarks_test.go
!example_test.go
Expand Down
9 changes: 9 additions & 0 deletions .kres.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
kind: golang.Generate
spec:
vtProtobufEnabled: false
baseSpecPath: /messages/
specs:
- source: /messages/messages.proto
genGateway: false
external: false
29 changes: 26 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2022-07-21T21:54:08Z by kres latest.
# Generated on 2022-07-26T15:20:24Z by kres latest.

ARG TOOLCHAIN

# cleaned up specs and compiled versions
FROM scratch AS generate
# collects proto specs
FROM scratch AS proto-specs
ADD /messages/messages.proto /messages/

# base toolchain image
FROM ${TOOLCHAIN} AS toolchain
Expand All @@ -26,6 +27,15 @@ RUN go install mvdan.cc/gofumpt@${GOFUMPT_VERSION} \
ARG GOIMPORTS_VERSION
RUN go install golang.org/x/tools/cmd/goimports@${GOIMPORTS_VERSION} \
&& mv /go/bin/goimports /bin/goimports
ARG PROTOBUF_GO_VERSION
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v${PROTOBUF_GO_VERSION}
RUN mv /go/bin/protoc-gen-go /bin
ARG GRPC_GO_VERSION
RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v${GRPC_GO_VERSION}
RUN mv /go/bin/protoc-gen-go-grpc /bin
ARG GRPC_GATEWAY_VERSION
RUN go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v${GRPC_GATEWAY_VERSION}
RUN mv /go/bin/protoc-gen-grpc-gateway /bin
ARG DEEPCOPY_VERSION
RUN go install github.com/siderolabs/deep-copy@${DEEPCOPY_VERSION} \
&& mv /go/bin/deep-copy /bin/deep-copy
Expand All @@ -37,6 +47,7 @@ COPY ./go.mod .
COPY ./go.sum .
RUN --mount=type=cache,target=/go/pkg go mod download
RUN --mount=type=cache,target=/go/pkg go mod verify
COPY ./messages ./messages
COPY ./array_test.go ./array_test.go
COPY ./benchmarks_test.go ./benchmarks_test.go
COPY ./example_test.go ./example_test.go
Expand All @@ -54,6 +65,14 @@ COPY ./type_cache.go ./type_cache.go
COPY ./unmarshal.go ./unmarshal.go
RUN --mount=type=cache,target=/go/pkg go list -mod=readonly all >/dev/null

# runs protobuf compiler
FROM tools AS proto-compile
COPY --from=proto-specs / /
RUN protoc -I/messages/ --go_out=paths=source_relative:/messages/ --go-grpc_out=paths=source_relative:/messages/ /messages/messages.proto
RUN rm /messages/messages.proto
RUN goimports -w -local github.com/siderolabs/protoenc /messages/
RUN gofumpt -w /messages/

# runs gofumpt
FROM base AS lint-gofumpt
RUN FILES="$(gofumpt -l .)" && test -z "${FILES}" || (echo -e "Source code is not formatted with 'gofumpt -w .':\n${FILES}"; exit 1)
Expand All @@ -78,6 +97,10 @@ FROM base AS unit-tests-run
ARG TESTPKGS
RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg --mount=type=cache,target=/tmp go test -v -covermode=atomic -coverprofile=coverage.txt -coverpkg=${TESTPKGS} -count 1 ${TESTPKGS}

# cleaned up specs and compiled versions
FROM scratch AS generate
COPY --from=proto-compile /messages/ /messages/

FROM scratch AS unit-tests
COPY --from=unit-tests-run /src/coverage.txt /coverage.txt

5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2022-07-22T07:58:20Z by kres c84ca98-dirty.
# Generated on 2022-07-26T15:06:36Z by kres latest.

# common variables

Expand Down Expand Up @@ -112,6 +112,9 @@ fmt: ## Formats the source code
lint-goimports: ## Runs goimports linter.
@$(MAKE) target-$@

generate: ## Generate .proto definitions.
@$(MAKE) local-$@ DEST=./

.PHONY: base
base: ## Prepare base toolchain
@$(MAKE) target-$@
Expand Down
19 changes: 14 additions & 5 deletions marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,21 +98,30 @@ func (m *marshaller) encodeFields(val reflect.Value, fieldsData []FieldData) {
noneEncoded := true

for _, fieldData = range fieldsData {
field := val.FieldByIndex(fieldData.FieldIndex)
num := fieldData.Num
field := fieldByIndex(val, fieldData)

if field.CanSet() {
m.encodeValue(num, field)
if field.IsValid() {
m.encodeValue(fieldData.Num, field)

noneEncoded = false
}
}

if noneEncoded {
panic("struct has no marshallable fields")
panic(fmt.Errorf("struct '%s' has no marshallable fields", val.Type().Name()))
}
}

// fieldByIndex returns the field of the struct by its index if the field is exported.
// Otherwise, it returns empty reflect.Value.
func fieldByIndex(structVal reflect.Value, data FieldData) reflect.Value {
if data.Field.IsExported() && structVal.IsValid() {
return structVal.FieldByIndex(data.FieldIndex)
}

return reflect.Value{}
}

//nolint:cyclop
func (m *marshaller) encodeValue(num protowire.Number, val reflect.Value) {
if m.tryEncodePredefined(num, val) {
Expand Down
67 changes: 67 additions & 0 deletions messages/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package messages_test

import (
"encoding/hex"
"strings"
"testing"

"github.com/stretchr/testify/require"

"github.com/siderolabs/protoenc"
)

func FuzzBasicMessage(f *testing.F) {
testcases := [][]byte{
hexToBytes(f, "08 01 18 02 29 03 00 00 00 00 00 00 00 32 0b 73 6f 6d 65 20 73 74 72 69 6e 67 3a 0a 73 6f 6d 65 20 62 79 74 65 73"),
hexToBytes(f, "08 00 18 00 29 00 00 00 00 00 00 00 00 32 00 3a 0a 73 6f 6d 65 20 62 79 74 65 73"),
}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}

f.Fuzz(func(t *testing.T, data []byte) {
var ourBasicMessage BasicMessage
err := protoenc.Unmarshal(data, &ourBasicMessage)
if err != nil {
errText := err.Error()

switch {
case strings.Contains(errText, "index out of range"),
strings.Contains(errText, "assignment to entry in nil map"),
strings.Contains(errText, "invalid memory address or nil pointer dereference"):
t.FailNow()
}
}
})
}

// hexToBytes converts a hex string to a byte slice, removing any whitespace.
func hexToBytes(f *testing.F, s string) []byte {
f.Helper()

s = strings.ReplaceAll(s, "|", "")
s = strings.ReplaceAll(s, "[", "")
s = strings.ReplaceAll(s, "]", "")
s = strings.ReplaceAll(s, " ", "")

b, err := hex.DecodeString(s)
require.NoError(f, err)

return b
}

func TestName(t *testing.T) {
ourBasicMessage := BasicMessage{
Int64: 0,
UInt64: 0,
Fixed64: protoenc.FixedU64(0),
SomeString: "",
SomeBytes: nil,
}
encoded1 := must(protoenc.Marshal(&ourBasicMessage))(t)
t.Logf("\n%s", hex.Dump(encoded1))
}
79 changes: 79 additions & 0 deletions messages/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package messages_test

import (
"reflect"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"

"github.com/siderolabs/protoenc"
)

func shouldBeEqual[T any](t *testing.T, left, right T) {
t.Helper()

opts := makeOpts[T]()

if !cmp.Equal(left, right, opts...) {
t.Log(cmp.Diff(left, right, opts...))
t.FailNow()
}
}

func makeOpts[T any]() []cmp.Option {
var zero T

typ := reflect.TypeOf(zero)
for typ.Kind() == reflect.Ptr {
typ = typ.Elem()
}

if typ.Kind() == reflect.Struct {
return []cmp.Option{cmpopts.IgnoreUnexported(reflect.New(typ).Elem().Interface())}
}

return nil
}

type msg[T any] interface {
*T
proto.Message
}

func protoUnmarshal[T any, V msg[T]](t *testing.T, data []byte) V {
t.Helper()

var msg T

err := proto.Unmarshal(data, V(&msg))
require.NoError(t, err)

return &msg
}

func ourUnmarshal[T any](t *testing.T, data []byte) T {
t.Helper()

var msg T

err := protoenc.Unmarshal(data, &msg)
require.NoError(t, err)

return msg
}

func must[T any](v T, err error) func(t *testing.T) T {
return func(t *testing.T) T {
t.Helper()
require.NoError(t, err)

return v
}
}
Loading

0 comments on commit ab9b1ff

Please sign in to comment.