Add internal engine relocation candidate
This commit is contained in:
312
internal/engine/decode_test.go
Normal file
312
internal/engine/decode_test.go
Normal file
@@ -0,0 +1,312 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
)
|
||||
|
||||
type namedSmall int8
|
||||
|
||||
type validatedText string
|
||||
|
||||
func (value validatedText) MarshalText() ([]byte, error) {
|
||||
return []byte(value), nil
|
||||
}
|
||||
|
||||
func (value *validatedText) UnmarshalText(text []byte) error {
|
||||
if string(text) == "bad-secret-canary" {
|
||||
return errors.New("CANARY_TEXT_ERROR: bad-secret-canary")
|
||||
}
|
||||
*value = validatedText(text)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestDecodeAppliesInputOverDetachedDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type nested struct {
|
||||
A string `yaml:"a"`
|
||||
B string `yaml:"b"`
|
||||
}
|
||||
type config struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Workers int `yaml:"workers"`
|
||||
Label string `yaml:"label"`
|
||||
Nested nested `yaml:"nested"`
|
||||
EmptyMapping nested `yaml:"empty_mapping"`
|
||||
Objects map[string]nested `yaml:"objects"`
|
||||
Sequence []string `yaml:"sequence"`
|
||||
Replacement []string `yaml:"replacement"`
|
||||
Pointer *nested `yaml:"pointer"`
|
||||
NilPointer *nested `yaml:"nil_pointer"`
|
||||
Nullable map[string]string `yaml:"nullable"`
|
||||
Dynamic any `yaml:"dynamic"`
|
||||
DynamicShape any `yaml:"dynamic_shape"`
|
||||
DynamicNull any `yaml:"dynamic_null"`
|
||||
Fixed [2]int `yaml:"fixed"`
|
||||
Small namedSmall `yaml:"small"`
|
||||
Delay time.Duration `yaml:"delay"`
|
||||
Custom validatedText `yaml:"custom"`
|
||||
}
|
||||
defaults := config{
|
||||
Enabled: true,
|
||||
Workers: 4,
|
||||
Label: "default",
|
||||
Nested: nested{A: "keep", B: "before"},
|
||||
EmptyMapping: nested{A: "keep-empty", B: "keep-empty"},
|
||||
Objects: map[string]nested{
|
||||
"existing": {A: "keep-map", B: "before"},
|
||||
},
|
||||
Sequence: []string{"default"},
|
||||
Replacement: []string{"default"},
|
||||
Pointer: &nested{A: "keep-pointer", B: "before"},
|
||||
Nullable: map[string]string{"default": "value"},
|
||||
Dynamic: map[string]any{"keep": "default", "count": int64(4)},
|
||||
DynamicShape: nested{A: "keep-dynamic", B: "before"},
|
||||
DynamicNull: "default",
|
||||
Fixed: [2]int{1, 2},
|
||||
Small: 7,
|
||||
Delay: time.Second,
|
||||
Custom: "default",
|
||||
}
|
||||
input := `
|
||||
enabled: false
|
||||
workers: 0
|
||||
label: ""
|
||||
nested: {b: changed}
|
||||
empty_mapping: {}
|
||||
objects:
|
||||
existing: {b: changed}
|
||||
new: {b: added}
|
||||
sequence: []
|
||||
replacement: [one, two]
|
||||
pointer: {b: changed}
|
||||
nil_pointer: {b: added}
|
||||
nullable: null
|
||||
dynamic: {count: 0, fraction: 1.5}
|
||||
dynamic_shape: {b: changed}
|
||||
dynamic_null: null
|
||||
fixed: [3, 4]
|
||||
small: -8
|
||||
delay: 1.5s
|
||||
custom: accepted
|
||||
`
|
||||
schema := mustSchemaFor[config](t)
|
||||
documentRoot := parseDocumentForDecode(t, input)
|
||||
decoded, issues := decodeConfiguration(schema, reflect.ValueOf(defaults), documentRoot)
|
||||
if len(issues) != 0 {
|
||||
t.Fatalf("decode issues = %#v", issues)
|
||||
}
|
||||
got := decoded.value.Interface().(config)
|
||||
if got.Enabled || got.Workers != 0 || got.Label != "" {
|
||||
t.Fatalf("explicit zero values lost: %#v", got)
|
||||
}
|
||||
if got.Nested != (nested{A: "keep", B: "changed"}) || got.EmptyMapping != defaults.EmptyMapping {
|
||||
t.Fatalf("mapping defaults = %#v / %#v", got.Nested, got.EmptyMapping)
|
||||
}
|
||||
if got.Objects["existing"] != (nested{A: "keep-map", B: "changed"}) || got.Objects["new"] != (nested{B: "added"}) {
|
||||
t.Fatalf("map defaults = %#v", got.Objects)
|
||||
}
|
||||
if got.Sequence == nil || len(got.Sequence) != 0 || !reflect.DeepEqual(got.Replacement, []string{"one", "two"}) {
|
||||
t.Fatalf("sequence replacement = %#v / %#v", got.Sequence, got.Replacement)
|
||||
}
|
||||
if got.Pointer == defaults.Pointer || *got.Pointer != (nested{A: "keep-pointer", B: "changed"}) || got.NilPointer == nil || *got.NilPointer != (nested{B: "added"}) {
|
||||
t.Fatalf("pointer defaults = %#v / %#v", got.Pointer, got.NilPointer)
|
||||
}
|
||||
if got.Nullable != nil || got.DynamicNull != nil {
|
||||
t.Fatalf("null values = %#v / %#v", got.Nullable, got.DynamicNull)
|
||||
}
|
||||
dynamic := got.Dynamic.(map[string]any)
|
||||
if dynamic["keep"] != "default" || dynamic["count"] != int64(0) || dynamic["fraction"] != float64(1.5) {
|
||||
t.Fatalf("dynamic mapping = %#v", dynamic)
|
||||
}
|
||||
if got.DynamicShape != (nested{A: "keep-dynamic", B: "changed"}) {
|
||||
t.Fatalf("dynamic concrete mapping = %#v", got.DynamicShape)
|
||||
}
|
||||
if got.Fixed != [2]int{3, 4} || got.Small != -8 || got.Delay != 1500*time.Millisecond || got.Custom != "accepted" {
|
||||
t.Fatalf("scalar decoding = %#v", got)
|
||||
}
|
||||
|
||||
for _, path := range []Path{Keys(), Keys("enabled"), Keys("nested"), Keys("nested", "b"), Keys("empty_mapping")} {
|
||||
if _, ok := decoded.presence[path.String()]; !ok {
|
||||
t.Errorf("explicit path %q missing", path.String())
|
||||
}
|
||||
}
|
||||
if _, ok := decoded.presence[Keys("nested", "a").String()]; ok {
|
||||
t.Fatal("parent mapping made omitted child explicit")
|
||||
}
|
||||
if decoded.origins[Keys("nested", "a").String()].Layer != LayerDefault || decoded.origins[Keys("nested", "b").String()].Layer != LayerBase {
|
||||
t.Fatalf("origins = %#v", decoded.origins)
|
||||
}
|
||||
if decoded.origins[Keys("nested", "b").String()].Locations[0].Line == 0 {
|
||||
t.Fatal("input origin lost source coordinates")
|
||||
}
|
||||
|
||||
defaults.Objects["existing"] = nested{A: "mutated"}
|
||||
defaults.Pointer.B = "mutated"
|
||||
defaults.Sequence[0] = "mutated"
|
||||
if got.Objects["existing"].A != "keep-map" || got.Pointer.B != "changed" || len(got.Sequence) != 0 {
|
||||
t.Fatal("decoded value aliases defaults")
|
||||
}
|
||||
got.Pointer.B = "caller mutation"
|
||||
if mappingValue(t, mappingValue(t, decoded.final, "pointer"), "b").Text != "changed" {
|
||||
t.Fatal("final snapshot aliases returned typed value")
|
||||
}
|
||||
if mappingValue(t, mappingValue(t, decoded.baseline, "nested"), "b").Text != "before" {
|
||||
t.Fatal("baseline snapshot was mutated by decode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeDefaultsOnlyAndRepeatedResultsAreIndependent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type config struct {
|
||||
Values map[string][]string `yaml:"values"`
|
||||
}
|
||||
defaults := config{Values: map[string][]string{"one": {"a"}}}
|
||||
schema := mustSchemaFor[config](t)
|
||||
first, issues := decodeConfiguration(schema, reflect.ValueOf(defaults), nil)
|
||||
if len(issues) != 0 {
|
||||
t.Fatal(issues)
|
||||
}
|
||||
second, issues := decodeConfiguration(schema, reflect.ValueOf(defaults), nil)
|
||||
if len(issues) != 0 {
|
||||
t.Fatal(issues)
|
||||
}
|
||||
firstValue := first.value.Interface().(config)
|
||||
secondValue := second.value.Interface().(config)
|
||||
firstValue.Values["one"][0] = "changed"
|
||||
if secondValue.Values["one"][0] != "a" || defaults.Values["one"][0] != "a" {
|
||||
t.Fatal("repeated defaults-only results share aliases")
|
||||
}
|
||||
if len(first.presence) != 0 || first.origins[Keys("values", "one").String()].Layer != LayerDefault {
|
||||
t.Fatalf("defaults-only metadata = %#v / %#v", first.presence, first.origins)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeCollectsIndependentTypeAndShapeFailures(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type nested struct {
|
||||
Known int `yaml:"known"`
|
||||
}
|
||||
type config struct {
|
||||
Small int8 `yaml:"small"`
|
||||
Unsigned uint8 `yaml:"unsigned"`
|
||||
Number float32 `yaml:"number"`
|
||||
Text string `yaml:"text"`
|
||||
Flag bool `yaml:"flag"`
|
||||
Delay time.Duration `yaml:"delay"`
|
||||
Fixed [2]int `yaml:"fixed"`
|
||||
Nested nested `yaml:"nested"`
|
||||
Custom validatedText `yaml:"custom"`
|
||||
Dynamic any `yaml:"dynamic"`
|
||||
}
|
||||
input := `
|
||||
small: 128
|
||||
unsigned: -1
|
||||
number: 1e999
|
||||
text: 3
|
||||
flag: "true"
|
||||
delay: 10
|
||||
fixed: [1]
|
||||
nested: {first_unknown: 1, second_unknown: 2}
|
||||
custom: bad-secret-canary
|
||||
dynamic: 9223372036854775808
|
||||
`
|
||||
schema := mustSchemaFor[config](t)
|
||||
_, issues := decodeConfiguration(schema, reflect.ValueOf(config{Custom: "ok"}), parseDocumentForDecode(t, input))
|
||||
if len(issues) != 11 {
|
||||
t.Fatalf("issues = %d: %#v", len(issues), issues)
|
||||
}
|
||||
want := map[string]Code{
|
||||
"/small": CodeDecodeRange,
|
||||
"/unsigned": CodeDecodeType,
|
||||
"/number": CodeDecodeRange,
|
||||
"/text": CodeDecodeType,
|
||||
"/flag": CodeDecodeType,
|
||||
"/delay": CodeDecodeType,
|
||||
"/fixed": CodeDecodeArrayLength,
|
||||
"/nested/first_unknown": CodeDecodeUnknownField,
|
||||
"/nested/second_unknown": CodeDecodeUnknownField,
|
||||
"/custom": CodeDecodeText,
|
||||
"/dynamic": CodeDecodeRange,
|
||||
}
|
||||
for _, issue := range issues {
|
||||
if wantCode, ok := want[issue.path.String()]; !ok || wantCode != issue.code {
|
||||
t.Errorf("unexpected issue %#v", issue)
|
||||
}
|
||||
if issue.location.Line == 0 {
|
||||
t.Errorf("issue lost input location: %#v", issue)
|
||||
}
|
||||
if strings.Contains(issue.message, "CANARY") || strings.Contains(issue.message, "bad-secret") {
|
||||
t.Errorf("issue exposed value: %#v", issue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeNullabilityAndNestedMapStrictness(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type nested struct {
|
||||
Value int `yaml:"value"`
|
||||
}
|
||||
type config struct {
|
||||
Scalar int `yaml:"scalar"`
|
||||
Object nested `yaml:"object"`
|
||||
Array [1]int `yaml:"array"`
|
||||
Map map[string]nested `yaml:"map"`
|
||||
}
|
||||
input := "scalar: null\nobject: null\narray: null\nmap:\n item: {unknown: 1}\n"
|
||||
_, issues := decodeConfiguration(mustSchemaFor[config](t), reflect.ValueOf(config{}), parseDocumentForDecode(t, input))
|
||||
want := map[string]Code{
|
||||
"/scalar": CodeDecodeNull,
|
||||
"/object": CodeDecodeNull,
|
||||
"/array": CodeDecodeNull,
|
||||
"/map/item/unknown": CodeDecodeUnknownField,
|
||||
}
|
||||
if len(issues) != len(want) {
|
||||
t.Fatalf("issues = %#v", issues)
|
||||
}
|
||||
for _, issue := range issues {
|
||||
if want[issue.path.String()] != issue.code {
|
||||
t.Errorf("issue = %#v", issue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeRejectsInvalidDefaultsBeforeApplyingInput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type config struct {
|
||||
Name string `yaml:"name"`
|
||||
Extras map[string]any `yaml:",inline"`
|
||||
Float float64 `yaml:"float"`
|
||||
}
|
||||
schema := mustSchemaFor[config](t)
|
||||
for name, defaults := range map[string]config{
|
||||
"inline collision": {Name: "declared", Extras: map[string]any{"name": "duplicate"}},
|
||||
"non-finite": {Float: math.NaN()},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if _, issues := decodeConfiguration(schema, reflect.ValueOf(defaults), parseDocumentForDecode(t, "{}\n")); len(issues) != 1 || !strings.HasPrefix(string(issues[0].code), "schema.") {
|
||||
t.Fatalf("issues = %#v", issues)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func parseDocumentForDecode(t *testing.T, input string) *document.Node {
|
||||
t.Helper()
|
||||
parsed, diagnostics := parseSource(Bytes("decode.yml", []byte(input)), "source-1", defaultParseLimits())
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatalf("parse diagnostics = %#v", diagnostics)
|
||||
}
|
||||
return parsed.document
|
||||
}
|
||||
Reference in New Issue
Block a user