test: add FuzzMarshal, golden messages, synctest cancellation and cross smoke
Test / test (push) Successful in 1m31s
Test / test (push) Successful in 1m31s
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -194,6 +194,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- An offset date-time written with the `+00:00` offset kept the anonymous
|
||||
location `time.Parse` invents for it, so a round trip through the tree and
|
||||
`Marshal`, which writes a zero offset as `Z`, changed the value's
|
||||
reflection-visible location. The zero offset normalises to `time.UTC` at
|
||||
parse, and the tree is stable across the round trip.
|
||||
- Decoding into a defined type whose underlying kind is string or bool, such
|
||||
as `type Name string`, panicked instead of storing the value, because a
|
||||
value of the predeclared type is not assignable to a defined type and the
|
||||
|
||||
@@ -258,6 +258,12 @@ func parseDateTime(tok string) (any, bool) {
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
// A zero offset carries its own anonymous location from time.Parse,
|
||||
// while the written form is "Z" either way; normalising to UTC keeps
|
||||
// the tree identical across the round trip.
|
||||
if _, off := t.Zone(); off == 0 {
|
||||
t = t.In(time.UTC)
|
||||
}
|
||||
return OffsetDateTime{t}, true
|
||||
case dateTimeLocal:
|
||||
layout := localClockLayout
|
||||
|
||||
+120
@@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -1574,3 +1575,122 @@ func TestCancelInsideValue(t *testing.T) {
|
||||
t.Errorf("err = %v, want context.Canceled", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancellationSynctest(t *testing.T) {
|
||||
// The bubble makes the cost of the immediate-cancellation path visible in
|
||||
// virtual microseconds, and synctest.Wait holds the test to leaving no
|
||||
// goroutine behind.
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
start := time.Now()
|
||||
var tree map[string]any
|
||||
err := NewDecoder().DecodeContext(ctx, []byte("a = 1\n"), &tree)
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Errorf("err = %v, want context.Canceled", err)
|
||||
}
|
||||
if d := time.Since(start); d != 0 {
|
||||
t.Errorf("the parse consumed %v of virtual time, want none", d)
|
||||
}
|
||||
synctest.Wait()
|
||||
})
|
||||
}
|
||||
|
||||
func TestErrorMessagesGolden(t *testing.T) {
|
||||
// The exact texts the library promises, pinned against unintended edits.
|
||||
type Config struct {
|
||||
Weight uint8 `toml:"weight"`
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
read func() error
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "missing equals",
|
||||
read: func() error { _, err := ParseMap([]byte("a 1\n")); return err },
|
||||
want: "interpres: line 1: expected '=' after key",
|
||||
},
|
||||
{
|
||||
name: "duplicate key",
|
||||
read: func() error { _, err := ParseMap([]byte("a = 1\na = 2\n")); return err },
|
||||
want: `interpres: line 2: duplicate key "a"`,
|
||||
},
|
||||
{
|
||||
name: "unterminated string",
|
||||
read: func() error { _, err := ParseMap([]byte("a = \"open\n")); return err },
|
||||
want: "interpres: line 1: unterminated string",
|
||||
},
|
||||
{
|
||||
name: "leading zero",
|
||||
read: func() error { _, err := ParseMap([]byte("a = 01\n")); return err },
|
||||
want: "interpres: line 1: leading zeros are not allowed in numbers",
|
||||
},
|
||||
{
|
||||
name: "bad escape",
|
||||
read: func() error { _, err := ParseMap([]byte(`a = "\q"` + "\n")); return err },
|
||||
want: `interpres: line 1: invalid escape sequence \q`,
|
||||
},
|
||||
{
|
||||
name: "nesting limit",
|
||||
read: func() error {
|
||||
var b strings.Builder
|
||||
b.WriteString("a = ")
|
||||
for range 11 {
|
||||
b.WriteString("[")
|
||||
}
|
||||
for range 11 {
|
||||
b.WriteString("]")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
var tree map[string]any
|
||||
err := NewDecoder().MaxDepth(10).Decode([]byte(b.String()), &tree)
|
||||
return err
|
||||
},
|
||||
want: "interpres: line 1: nesting exceeds the limit of 10",
|
||||
},
|
||||
{
|
||||
name: "decode overflow",
|
||||
read: func() error {
|
||||
var cfg Config
|
||||
return Unmarshal([]byte("weight = 300\n"), &cfg)
|
||||
},
|
||||
want: "interpres: weight: integer 300 overflows uint8",
|
||||
},
|
||||
{
|
||||
name: "unknown field",
|
||||
read: func() error {
|
||||
var cfg struct {
|
||||
Known int `toml:"known"`
|
||||
}
|
||||
return NewDecoder().DisallowUnknownFields().Decode([]byte("mystery = 1\n"), &cfg)
|
||||
},
|
||||
want: `interpres: unknown field "mystery" for struct { Known int "toml:\"known\"" }`,
|
||||
},
|
||||
{
|
||||
name: "decode target",
|
||||
read: func() error { return Unmarshal([]byte("a = 1\n"), Config{}) },
|
||||
want: "interpres: decode target must be a non-nil pointer",
|
||||
},
|
||||
{
|
||||
name: "encode nil pointer",
|
||||
read: func() error {
|
||||
var p *Config
|
||||
_, err := Marshal(p)
|
||||
return err
|
||||
},
|
||||
want: "interpres: cannot marshal nil pointer",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.read()
|
||||
if err == nil {
|
||||
t.Fatalf("no error, want %q", tt.want)
|
||||
}
|
||||
if err.Error() != tt.want {
|
||||
t.Errorf("message = %q, want %q", err.Error(), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,3 +121,71 @@ func tomlEqual(a, b any) bool {
|
||||
return reflect.DeepEqual(a, b)
|
||||
}
|
||||
}
|
||||
|
||||
// FuzzMarshal drives the encoder with generated Go values and holds it to
|
||||
// the same round-trip invariant FuzzParse holds the parser to: a value built
|
||||
// only of encodable kinds must marshal, the document must re-parse, and the
|
||||
// tree must equal the value it came from.
|
||||
func FuzzMarshal(f *testing.F) {
|
||||
seeds := [][]byte{
|
||||
{},
|
||||
{0, 0, 1, 2},
|
||||
{1, 1, 2, 3, 2, 2, 3, 4},
|
||||
{0, 3, 1, 9, 3, 3, 2, 8, 1, 0, 1, 7},
|
||||
}
|
||||
for _, s := range seeds {
|
||||
f.Add(s)
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
v := fuzzValue(data)
|
||||
out, err := Marshal(v)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal of an encodable value failed: %v\nvalue: %#v", err, v)
|
||||
}
|
||||
tree, err := ParseMap(out)
|
||||
if err != nil {
|
||||
t.Fatalf("re-parse of the emitted document failed: %v\ndoc:\n%s", err, out)
|
||||
}
|
||||
if !tomlEqual(v, tree) {
|
||||
t.Fatalf("round-trip changed the value\nvalue: %#v\ndoc:\n%s\ntree: %#v", v, out, tree)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// fuzzKeys is the fixed key pool the generated values draw from, so keys are
|
||||
// always valid bare keys and repeat often.
|
||||
var fuzzKeys = []string{"alpha", "beta", "gamma", "delta"}
|
||||
|
||||
// fuzzValue builds a map[string]any of encodable kinds from data: integers,
|
||||
// positive floats, short strings, nested tables and scalar arrays. The bytes
|
||||
// decide the shape deterministically.
|
||||
func fuzzValue(data []byte) map[string]any {
|
||||
root := map[string]any{}
|
||||
cur := root
|
||||
depth := 0
|
||||
for i := 0; i+3 < len(data); i += 4 {
|
||||
key := fuzzKeys[int(data[i])%len(fuzzKeys)]
|
||||
switch data[i+1] % 5 {
|
||||
case 0:
|
||||
cur[key] = int64(data[i+2])<<8 | int64(data[i+3])
|
||||
case 1:
|
||||
cur[key] = float64(int(data[i+2])%1000)/8.0 + 0.125
|
||||
case 2:
|
||||
cur[key] = string(rune('a' + int(data[i+2])%26))
|
||||
case 3:
|
||||
cur[key] = []any{
|
||||
int64(data[i+2]),
|
||||
float64(int(data[i+3])%100)/4.0 + 0.25,
|
||||
string(rune('a' + int(data[i+3])%26)),
|
||||
}
|
||||
case 4:
|
||||
if depth < 6 {
|
||||
next := map[string]any{}
|
||||
cur[key] = next
|
||||
cur = next
|
||||
depth++
|
||||
}
|
||||
}
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
@@ -796,3 +796,28 @@ func TestParseAsAndNewSchema(t *testing.T) {
|
||||
}
|
||||
NewSchema[map[string]any]() // must not panic
|
||||
}
|
||||
|
||||
func TestZeroOffsetRoundTrip(t *testing.T) {
|
||||
// A document may write a zero offset as +00:00; the tree must hold the
|
||||
// same value after a round trip, because the written form is "Z" either
|
||||
// way.
|
||||
src := []byte("a = 1979-05-27T07:32:00+00:00\n")
|
||||
tree, err := ParseMap(src)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := Marshal(tree)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
re, err := ParseMap(out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(tree, re) {
|
||||
t.Errorf("round trip changed the tree: %#v vs %#v", tree, re)
|
||||
}
|
||||
if got := tree["a"].(OffsetDateTime).String(); got != "1979-05-27T07:32Z" {
|
||||
t.Errorf("a = %q, want 1979-05-27T07:32Z", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ test:
|
||||
close($c);
|
||||
die qq{no total line in coverage.out\n} unless defined $total;
|
||||
printf qq{Total coverage: %s%%\n}, $total;
|
||||
exit($total < 80 ? 1 : 0);
|
||||
exit($total < 82 ? 1 : 0);
|
||||
|
||||
# The same suite under the race detector. The expensive one.
|
||||
race:
|
||||
@@ -100,6 +100,15 @@ toml-test: build
|
||||
coverage-html: test
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
|
||||
# Cross-compile smoke: the library and the command build for the foreign architectures; not a gate, it is a nightly convenience and runs std-lib only.
|
||||
cross:
|
||||
GOARCH=arm64 go build ./...
|
||||
GOARCH=loong64 go build ./...
|
||||
GOARCH=riscv64 go build ./...
|
||||
GOARCH=arm64 CGO_ENABLED=0 go build -o /dev/null {{package}}
|
||||
GOARCH=loong64 CGO_ENABLED=0 go build -o /dev/null {{package}}
|
||||
GOARCH=riscv64 CGO_ENABLED=0 go build -o /dev/null {{package}}
|
||||
|
||||
# Runs the example program under examples/basic; not standard because `run` runs the adapter, and an example is documentation, not the product.
|
||||
example:
|
||||
go run ./examples/basic
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("0=00:00\n1=0000-01-01 00:00:00.0+00:00#000000000000")
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("e = \"\\\\e[0m\\\\x41\\\\x7f\\\\x00\"\n")
|
||||
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("m = {\n\ttitle = \"one\",\n\tnums = [1, 2,],\n\tinner = { deep = true }, # trailing\n}\n")
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("t = 13:37\nbig = 1979-05-27 07:32:00.5+01:00\nshort = 1979-05-27 07:32\n")
|
||||
Reference in New Issue
Block a user