Cover integer version decoding boundaries
This commit is contained in:
@@ -58,7 +58,7 @@ the scalar, destination-type, and semantic-path boundary matrix.
|
||||
|
||||
## Stage 2 — Close scalar and path-boundary gaps
|
||||
|
||||
Status: not started. Depends on: Stage 1.
|
||||
Status: complete (2026-09-12). Depends on: Stage 1.
|
||||
|
||||
Scope: focused tests in `load_test.go`, `decode_test.go`, and relevant path/decoder
|
||||
helpers only where those tests expose defects. Avoid broad schema refactoring.
|
||||
@@ -82,6 +82,14 @@ loads. Expand a stage-1 test instead of duplicating the same behavior.
|
||||
|
||||
Validation: `go test . -run 'Version|Decode|Path'`, then the everyday checker.
|
||||
|
||||
Completion record: boundary coverage now includes exact zero, negative, negative
|
||||
zero, unbounded textual integer, rejected scalar/container kinds, parsed numeric
|
||||
tokens in maps and sequences, escaped keys, named and nested-pointer strings, and
|
||||
ordinary numeric, dynamic, codec, duration, and secret destinations. Existing
|
||||
parser-policy tests continue to own non-JSON number rejection. The focused command
|
||||
and `./scripts/check.sh` pass with Go 1.26.7 on Linux. Stage 3 should verify retained
|
||||
history, rendering, composition ownership, detachment, and redaction invariants.
|
||||
|
||||
## Stage 3 — Verify provenance and pipeline invariants
|
||||
|
||||
Status: not started. Depends on: Stage 2.
|
||||
|
||||
217
load_test.go
217
load_test.go
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLoadSingleSourceKindsAndDefaultsOnly(t *testing.T) {
|
||||
@@ -216,6 +217,222 @@ func TestLoadDecodesIntegerVersionOnlyAtConfiguredStringPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadVersionScalarAndPathBoundaries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type config struct {
|
||||
Version string `yaml:"version"`
|
||||
}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
value string
|
||||
want string
|
||||
}{
|
||||
{name: "zero", value: "0", want: "0"},
|
||||
{name: "negative", value: "-7", want: "-7"},
|
||||
{name: "negative zero", value: "-0", want: "-0"},
|
||||
{name: "beyond int64", value: "123456789012345678901234567890", want: "123456789012345678901234567890"},
|
||||
{name: "empty string", value: `""`, want: ""},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var checked Version
|
||||
rule := &VersionRule{Path: Keys("version"), Check: func(version Version, _ *Validation) error {
|
||||
checked = version
|
||||
return nil
|
||||
}}
|
||||
result, err := Load(Bytes("version.yml", []byte("version: "+test.value+"\n")), Options[config]{Definition: Definition[config]{Version: rule}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Config.Version != test.want || checked.Text != test.want || !checked.Present || checked.Null {
|
||||
t.Fatalf("configuration = %#v, checked = %#v", result.Config, checked)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{name: "fraction", value: "4.0"},
|
||||
{name: "exponent", value: "4e0"},
|
||||
{name: "boolean", value: "true"},
|
||||
{name: "mapping", value: "{}"},
|
||||
{name: "sequence", value: "[]"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
called := false
|
||||
rule := &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error {
|
||||
called = true
|
||||
return nil
|
||||
}}
|
||||
result, err := Load(Bytes("version.yml", []byte("version: "+test.value+"\n")), Options[config]{Definition: Definition[config]{Version: rule}})
|
||||
if result != nil || called {
|
||||
t.Fatalf("result = %#v, checker called = %v", result, called)
|
||||
}
|
||||
requireDiagnostic(t, err, CodeVersionType, "/version")
|
||||
})
|
||||
}
|
||||
|
||||
type namedVersion string
|
||||
type nested struct {
|
||||
Versions map[string]**namedVersion `yaml:"versions"`
|
||||
Sibling namedVersion `yaml:"sibling"`
|
||||
}
|
||||
parsed, err := ParsePath("/versions/0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rule := &VersionRule{Path: parsed, Check: func(version Version, _ *Validation) error {
|
||||
if version.Text != "4" {
|
||||
t.Errorf("version = %#v", version)
|
||||
}
|
||||
return nil
|
||||
}}
|
||||
result, err := Load(Bytes("nested.yml", []byte("versions:\n \"0\": 4\nsibling: quoted\n")), Options[nested]{Definition: Definition[nested]{Version: rule}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Config.Versions["0"] == nil || *result.Config.Versions["0"] == nil || **result.Config.Versions["0"] != "4" || result.Config.Sibling != "quoted" {
|
||||
t.Fatalf("nested configuration = %#v", result.Config)
|
||||
}
|
||||
type sequenceConfig struct {
|
||||
Versions []namedVersion `yaml:"versions"`
|
||||
}
|
||||
sequenceResult, err := Load(Bytes("sequence.yml", []byte("versions: [6]\n")), Options[sequenceConfig]{Definition: Definition[sequenceConfig]{Version: &VersionRule{Path: parsed, Check: func(Version, *Validation) error { return nil }}}})
|
||||
if err != nil || len(sequenceResult.Config.Versions) != 1 || sequenceResult.Config.Versions[0] != "6" {
|
||||
t.Fatalf("sequence configuration = %#v, %v", sequenceResult, err)
|
||||
}
|
||||
|
||||
type escaped struct {
|
||||
Versions map[string]namedVersion `yaml:"versions"`
|
||||
}
|
||||
escapedPath, err := ParsePath("/versions/a~1b")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
escapedResult, err := Load(Bytes("escaped.yml", []byte("versions:\n a/b: 9\n")), Options[escaped]{Definition: Definition[escaped]{Version: &VersionRule{Path: escapedPath, Check: func(Version, *Validation) error { return nil }}}})
|
||||
if err != nil || escapedResult.Config.Versions["a/b"] != "9" {
|
||||
t.Fatalf("escaped configuration = %#v, %v", escapedResult, err)
|
||||
}
|
||||
|
||||
siblingRule := &VersionRule{Path: Keys("versions", "target"), Check: func(Version, *Validation) error { return nil }}
|
||||
if result, err := Load(Bytes("sibling.yml", []byte("versions:\n target: 4\n other: 5\n")), Options[escaped]{Definition: Definition[escaped]{Version: siblingRule}}); result != nil {
|
||||
t.Fatalf("same-type sibling result = %#v, %v", result, err)
|
||||
} else {
|
||||
requireDiagnostic(t, err, CodeDecodeType, "/versions/other")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadVersionDestinationTypesKeepOrdinaryDecoding(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
check := func(Version, *Validation) error { return nil }
|
||||
t.Run("integer destination", func(t *testing.T) {
|
||||
type config struct {
|
||||
Version int8 `yaml:"version"`
|
||||
}
|
||||
result, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
if err != nil || result.Config.Version != 4 {
|
||||
t.Fatalf("result = %#v, %v", result, err)
|
||||
}
|
||||
result, err = Load(Bytes("version.yml", []byte("version: 999\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
if result != nil {
|
||||
t.Fatalf("range result = %#v", result)
|
||||
}
|
||||
requireDiagnostic(t, err, CodeDecodeRange, "/version")
|
||||
result, err = Load(Bytes("version.yml", []byte("version: \"4\"\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
if result != nil {
|
||||
t.Fatalf("quoted integer result = %#v", result)
|
||||
}
|
||||
requireDiagnostic(t, err, CodeDecodeType, "/version")
|
||||
})
|
||||
|
||||
t.Run("dynamic destination", func(t *testing.T) {
|
||||
type config struct {
|
||||
Version any `yaml:"version"`
|
||||
}
|
||||
result, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
if err != nil || result.Config.Version != int64(4) {
|
||||
t.Fatalf("result = %#v, %v", result, err)
|
||||
}
|
||||
result, err = Load(Bytes("version.yml", []byte("version: 123456789012345678901234567890\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
if result != nil {
|
||||
t.Fatalf("range result = %#v", result)
|
||||
}
|
||||
requireDiagnostic(t, err, CodeDecodeRange, "/version")
|
||||
})
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
load func() error
|
||||
}{
|
||||
{name: "text codec", load: func() error {
|
||||
type config struct {
|
||||
Version validatedText `yaml:"version"`
|
||||
}
|
||||
_, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
return err
|
||||
}},
|
||||
{name: "duration", load: func() error {
|
||||
type config struct {
|
||||
Version time.Duration `yaml:"version"`
|
||||
}
|
||||
_, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
return err
|
||||
}},
|
||||
{name: "secret", load: func() error {
|
||||
type config struct {
|
||||
Version Secret `yaml:"version"`
|
||||
}
|
||||
_, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
return err
|
||||
}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
requireDiagnostic(t, test.load(), CodeDecodeType, "/version")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadVersionPolicyStopsLaterHooks(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type config struct {
|
||||
Version string `yaml:"version"`
|
||||
}
|
||||
var defaultsCalled, resolutionCalled, validationCalled bool
|
||||
rule := &VersionRule{Path: Keys("version"), Check: func(version Version, validation *Validation) error {
|
||||
if version.Text != "4" {
|
||||
t.Errorf("version = %#v", version)
|
||||
}
|
||||
validation.Error(Keys("version"), "unsupported configuration version")
|
||||
return nil
|
||||
}}
|
||||
result, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{
|
||||
Defaults: func() config { defaultsCalled = true; return config{} },
|
||||
Version: rule,
|
||||
Resolve: func(*config, *Resolution) error { resolutionCalled = true; return nil },
|
||||
Validate: func(*config, *Validation) { validationCalled = true },
|
||||
}})
|
||||
if result != nil || defaultsCalled || resolutionCalled || validationCalled {
|
||||
t.Fatalf("result = %#v, hooks = %v/%v/%v", result, defaultsCalled, resolutionCalled, validationCalled)
|
||||
}
|
||||
requireDiagnostic(t, err, CodeVersionError, "/version")
|
||||
}
|
||||
|
||||
func requireDiagnostic(t *testing.T, err error, code Code, path string) {
|
||||
t.Helper()
|
||||
var loadError *LoadError
|
||||
if !errors.As(err, &loadError) {
|
||||
t.Fatalf("error = %T %v", err, err)
|
||||
}
|
||||
diagnostics := loadError.Diagnostics()
|
||||
if len(diagnostics) != 1 || diagnostics[0].Code != code || diagnostics[0].Path.String() != path || diagnostics[0].Primary == nil {
|
||||
t.Fatalf("diagnostics = %#v", diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadVersionReportSuppressesReturnedErrorTextAndDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user