Validate source equivalence and layered scaling
This commit is contained in:
@@ -461,6 +461,28 @@ checker and full race suite.
|
|||||||
|
|
||||||
## Stage 9: Validate cross-feature consistency and scaling
|
## Stage 9: Validate cross-feature consistency and scaling
|
||||||
|
|
||||||
|
**Completed 2026-09-29.** A compact public matrix now checks equivalent host,
|
||||||
|
native, byte, reader, and mapping inputs; tier-local repetition versus cross-tier
|
||||||
|
reuse; reader consumption; and safe diagnostics/effective/history output after
|
||||||
|
secret replacement. Engine tests verify exact occurrence accounting across
|
||||||
|
imports, shared glob/source limits, and the mixed-source CLI ceiling. Rendering
|
||||||
|
now indexes source labels once per explanation instead of scanning all records
|
||||||
|
for every location. Loading and passive context ownership were reviewed against
|
||||||
|
ADR 0008; no handles or background work are retained by queries.
|
||||||
|
|
||||||
|
On Linux amd64 with `-benchmem` and 100 ms samples, the 512-entry assembly-only
|
||||||
|
composition measured about 3.5 ms/op (the earlier post-index baseline was about
|
||||||
|
3.3 ms), and recursive overlay about 2.2 ms/op (about 12 ms at the first-stage
|
||||||
|
handoff before subsequent provenance work). The 128-application owned assembly
|
||||||
|
remained about 0.30 ms/op versus 6.35 ms/op for copy-preserving application.
|
||||||
|
End-to-end layered measurements were about 7.15 ms/op for 512 broad catalog
|
||||||
|
entries, 2.59 ms/op for 128 small tiers, 3.27 ms/op for 128 alternating subtree
|
||||||
|
replacements, 1.86 ms/op for 64 imported native defaults, and 1.36 ms/op for
|
||||||
|
64 retained-history applications. Allocations grew roughly with these fixture
|
||||||
|
sizes. I/O, sorting, decoding, callbacks, and retained output contribute to
|
||||||
|
full-path cost; these observations are not timing gates or an unconditional
|
||||||
|
complexity guarantee. The everyday checker and full race suite passed.
|
||||||
|
|
||||||
1. Consolidate a small semantic equivalence matrix across host, native, bytes,
|
1. Consolidate a small semantic equivalence matrix across host, native, bytes,
|
||||||
readers, and mappings, using equivalent logical inputs and contexts. Cover
|
readers, and mappings, using equivalent logical inputs and contexts. Cover
|
||||||
strict conflict versus intentional precedence and version-path preservation;
|
strict conflict versus intentional precedence and version-path preservation;
|
||||||
|
|||||||
164
facade_equivalence_test.go
Normal file
164
facade_equivalence_test.go
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package configkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"testing/fstest"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type equivalentConfig struct {
|
||||||
|
Version uint64 `yaml:"version"`
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
Count int `yaml:"count"`
|
||||||
|
Items []string `yaml:"items"`
|
||||||
|
Optional *string `yaml:"optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEquivalentSourceFormsPreserveInputSemantics(t *testing.T) {
|
||||||
|
const yaml = "version: 1\nname: retained\nenabled: false\ncount: 0\nitems: []\noptional: null\n"
|
||||||
|
hostPath := filepath.Join(t.TempDir(), "main.yml")
|
||||||
|
if err := os.WriteFile(hostPath, []byte(yaml), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
namespace := configkit.NewFilesystem("bundle", fstest.MapFS{"main.yml": &fstest.MapFile{Data: []byte(yaml)}})
|
||||||
|
context := &configkit.FilesystemContext{Root: ".", BaseDir: ".", FS: &namespace}
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
source configkit.Source
|
||||||
|
context *configkit.FilesystemContext
|
||||||
|
kind configkit.SourceKind
|
||||||
|
}{
|
||||||
|
{"host", configkit.File(hostPath), nil, configkit.SourceKindFile},
|
||||||
|
{"native", namespace.File("main.yml"), nil, configkit.SourceKindFilesystem},
|
||||||
|
{"bytes", configkit.Bytes("memory", []byte(yaml)), context, configkit.SourceKindBytes},
|
||||||
|
{"reader", configkit.Reader("stream", strings.NewReader(yaml)), context, configkit.SourceKindReader},
|
||||||
|
{"mapping", configkit.Mapping("programmatic", map[string]any{
|
||||||
|
"version": uint64(1), "name": "retained", "enabled": false, "count": 0,
|
||||||
|
"items": []any{}, "optional": nil,
|
||||||
|
}), context, configkit.SourceKindMapping},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
checks := 0
|
||||||
|
result, err := configkit.LoadLayered(configkit.Input{Source: test.source, Filesystem: test.context}, configkit.LayeredOptions[equivalentConfig]{
|
||||||
|
Definition: configkit.Definition[equivalentConfig]{Version: &configkit.VersionRule{
|
||||||
|
Path: configkit.Keys("version"), Check: func(version configkit.Version, validation *configkit.Validation) error {
|
||||||
|
checks++
|
||||||
|
if !version.Present || version.Text != "1" {
|
||||||
|
t.Errorf("version = %#v", version)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := equivalentConfig{Version: 1, Name: "retained", Items: []string{}}
|
||||||
|
if !reflect.DeepEqual(result.Config, want) || checks != 1 {
|
||||||
|
t.Fatalf("config = %#v, checks %d", result.Config, checks)
|
||||||
|
}
|
||||||
|
for _, field := range []string{"version", "name", "enabled", "count", "items", "optional"} {
|
||||||
|
info, ok := result.Metadata().Lookup(configkit.Keys(field))
|
||||||
|
if !ok || !info.Explicit || info.Origin.Layer != configkit.LayerBase || info.Null != (field == "optional") {
|
||||||
|
t.Errorf("%s input info = %#v, %v", field, info, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sources := result.Sources()
|
||||||
|
if len(sources) != 1 || sources[0].Kind != test.kind || sources[0].ID != "source-1" || sources[0].UnderlayIndex != 0 {
|
||||||
|
t.Fatalf("sources = %#v", sources)
|
||||||
|
}
|
||||||
|
resource, ok := result.SourceContext("source-1")
|
||||||
|
if !ok || resource.BaseDir != "." || resource.FS == nil {
|
||||||
|
t.Fatalf("source context = %#v, %v", resource, ok)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepeatedImportsAcrossTiersAndReaderConsumption(t *testing.T) {
|
||||||
|
namespace := configkit.NewFilesystem("bundle", fstest.MapFS{
|
||||||
|
"left.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: shared.yml}]}\n")},
|
||||||
|
"right.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: shared.yml}]}\n")},
|
||||||
|
"shared.yml": &fstest.MapFile{Data: []byte("name: retained\n")},
|
||||||
|
})
|
||||||
|
type config struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
}
|
||||||
|
left := configkit.Input{Source: namespace.File("left.yml"), Imports: true}
|
||||||
|
right := configkit.Input{Source: namespace.File("right.yml"), Imports: true}
|
||||||
|
_, err := configkit.LoadLayered(configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[config]{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{left, right}}},
|
||||||
|
})
|
||||||
|
if got := layeredErrorCode(t, err); got != configkit.CodeImportRepeated {
|
||||||
|
t.Fatalf("same-tier repeated import = %s", got)
|
||||||
|
}
|
||||||
|
result, err := configkit.LoadLayered(configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[config]{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{left}}, {Inputs: []configkit.Input{right}}},
|
||||||
|
})
|
||||||
|
if err != nil || result.Config.Name != "retained" || len(result.Sources()) != 4 {
|
||||||
|
t.Fatalf("cross-tier repeated import = %#v, %v", result, err)
|
||||||
|
}
|
||||||
|
reader := configkit.Reader("single-use", strings.NewReader("name: retained\n"))
|
||||||
|
_, err = configkit.LoadLayered(configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[config]{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: reader}}}, {Inputs: []configkit.Input{{Source: reader}}}},
|
||||||
|
})
|
||||||
|
if got := layeredErrorCode(t, err); got != configkit.CodeSourceReaderUsed {
|
||||||
|
t.Fatalf("reader re-consumption = %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplacedSecretRemainsAbsentFromSafeOutput(t *testing.T) {
|
||||||
|
type config struct {
|
||||||
|
Secret string `yaml:"secret"`
|
||||||
|
}
|
||||||
|
result, err := configkit.LoadLayered(configkit.Input{Source: configkit.Bytes("main", []byte("secret: new-secret-canary\n"))}, configkit.LayeredOptions[config]{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("lower", map[string]any{"secret": "old-secret-canary"})}}}},
|
||||||
|
Definition: configkit.Definition[config]{Secret: func(path configkit.Path) bool { return path.String() == "/secret" }},
|
||||||
|
})
|
||||||
|
if err != nil || result.Config.Secret != "new-secret-canary" {
|
||||||
|
t.Fatalf("secret load = %#v, %v", result, err)
|
||||||
|
}
|
||||||
|
var effective, history bytes.Buffer
|
||||||
|
if err := result.WriteEffective(&effective); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := result.WriteExplanation(&history, configkit.Keys("secret")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if strings.Contains(effective.String()+history.String(), "old-secret-canary") || strings.Contains(effective.String()+history.String(), "new-secret-canary") {
|
||||||
|
t.Fatalf("secret output leaked: %q %q", effective.String(), history.String())
|
||||||
|
}
|
||||||
|
_, err = configkit.LoadLayered(configkit.Input{Source: configkit.Bytes("main", []byte("secret: new-secret-canary\n"))}, configkit.LayeredOptions[config]{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("lower", map[string]any{"secret": "old-secret-canary"})}}}},
|
||||||
|
Definition: configkit.Definition[config]{Validate: func(config *config, validation *configkit.Validation) {
|
||||||
|
validation.Error(configkit.Keys("secret"), "invalid secret")
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
loadError, ok := err.(*configkit.LoadError)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("validation error = %#v", err)
|
||||||
|
}
|
||||||
|
var diagnostics bytes.Buffer
|
||||||
|
if err := configkit.WriteDiagnostics(&diagnostics, loadError.Diagnostics()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if strings.Contains(diagnostics.String(), "secret-canary") {
|
||||||
|
t.Fatalf("diagnostic leaked secret: %q", diagnostics.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func layeredErrorCode(t *testing.T, err error) configkit.Code {
|
||||||
|
t.Helper()
|
||||||
|
loadError, ok := err.(*configkit.LoadError)
|
||||||
|
if !ok || len(loadError.Diagnostics()) == 0 {
|
||||||
|
t.Fatalf("load error = %#v", err)
|
||||||
|
}
|
||||||
|
return loadError.Diagnostics()[0].Code
|
||||||
|
}
|
||||||
@@ -23,10 +23,14 @@ func (r *Inspection) WriteExplanation(output io.Writer, path Path) error {
|
|||||||
return errExplanationUnknownPath
|
return errExplanationUnknownPath
|
||||||
}
|
}
|
||||||
var rendered strings.Builder
|
var rendered strings.Builder
|
||||||
|
labels := make(map[string]string, len(r.sources))
|
||||||
|
for _, source := range r.sources {
|
||||||
|
labels[source.ID] = source.Label
|
||||||
|
}
|
||||||
rendered.WriteString("path: ")
|
rendered.WriteString("path: ")
|
||||||
rendered.WriteString(strconv.Quote(explanation.Path.String()))
|
rendered.WriteString(strconv.Quote(explanation.Path.String()))
|
||||||
rendered.WriteByte('\n')
|
rendered.WriteByte('\n')
|
||||||
writeExplanationOrigin(&rendered, "effective source", explanation.Origin, r.sources)
|
writeExplanationOrigin(&rendered, "effective source", explanation.Origin, labels)
|
||||||
writeExplanationSnapshot(&rendered, "effective (canonical typed)", explanation.Effective, 0)
|
writeExplanationSnapshot(&rendered, "effective (canonical typed)", explanation.Effective, 0)
|
||||||
if explanation.SecretReference != nil {
|
if explanation.SecretReference != nil {
|
||||||
rendered.WriteString("secret reference: ")
|
rendered.WriteString("secret reference: ")
|
||||||
@@ -43,7 +47,7 @@ func (r *Inspection) WriteExplanation(output io.Writer, path Path) error {
|
|||||||
} else {
|
} else {
|
||||||
rendered.WriteString("canonical typed\n")
|
rendered.WriteString("canonical typed\n")
|
||||||
}
|
}
|
||||||
writeExplanationOrigin(&rendered, " source", change.Origin, r.sources)
|
writeExplanationOrigin(&rendered, " source", change.Origin, labels)
|
||||||
writeExplanationSnapshot(&rendered, " before", change.Before, 4)
|
writeExplanationSnapshot(&rendered, " before", change.Before, 4)
|
||||||
writeExplanationSnapshot(&rendered, " after", change.After, 4)
|
writeExplanationSnapshot(&rendered, " after", change.After, 4)
|
||||||
for _, reason := range change.Reasons {
|
for _, reason := range change.Reasons {
|
||||||
@@ -65,7 +69,7 @@ func (r *Inspection) WriteExplanation(output io.Writer, path Path) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeExplanationOrigin(output *strings.Builder, label string, origin Origin, sources []SourceInfo) {
|
func writeExplanationOrigin(output *strings.Builder, label string, origin Origin, labels map[string]string) {
|
||||||
output.WriteString(label)
|
output.WriteString(label)
|
||||||
output.WriteString(": layer=")
|
output.WriteString(": layer=")
|
||||||
output.WriteString(strconv.Quote(string(origin.Layer)))
|
output.WriteString(strconv.Quote(string(origin.Layer)))
|
||||||
@@ -77,7 +81,7 @@ func writeExplanationOrigin(output *strings.Builder, label string, origin Origin
|
|||||||
for _, location := range origin.Locations {
|
for _, location := range origin.Locations {
|
||||||
output.WriteString(" location: source=")
|
output.WriteString(" location: source=")
|
||||||
output.WriteString(strconv.Quote(location.SourceID))
|
output.WriteString(strconv.Quote(location.SourceID))
|
||||||
if label := sourceLabel(sources, location.SourceID); label != "" {
|
if label := labels[location.SourceID]; label != "" {
|
||||||
output.WriteString(" label=")
|
output.WriteString(" label=")
|
||||||
output.WriteString(strconv.Quote(label))
|
output.WriteString(strconv.Quote(label))
|
||||||
}
|
}
|
||||||
@@ -89,15 +93,6 @@ func writeExplanationOrigin(output *strings.Builder, label string, origin Origin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sourceLabel(sources []SourceInfo, sourceID string) string {
|
|
||||||
for _, source := range sources {
|
|
||||||
if source.ID == sourceID {
|
|
||||||
return source.Label
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeExplanationSnapshot(output *strings.Builder, label string, snapshot ValueSnapshot, indent int) {
|
func writeExplanationSnapshot(output *strings.Builder, label string, snapshot ValueSnapshot, indent int) {
|
||||||
if !snapshot.Present {
|
if !snapshot.Present {
|
||||||
output.WriteString(label)
|
output.WriteString(label)
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, a
|
|||||||
}
|
}
|
||||||
|
|
||||||
func acquireUnderlayGroup(group loadGroup, index int, rule *VersionRule, accounting *importAccounting) (*composedDocument, []SourceInfo, map[string]ResourceContext, []Diagnostic) {
|
func acquireUnderlayGroup(group loadGroup, index int, rule *VersionRule, accounting *importAccounting) (*composedDocument, []SourceInfo, map[string]ResourceContext, []Diagnostic) {
|
||||||
|
initialBytes, initialNodes := accounting.bytes, accounting.nodes
|
||||||
visited := make(map[resourceIdentity]importRoute)
|
visited := make(map[resourceIdentity]importRoute)
|
||||||
hostToken := &namespaceToken{}
|
hostToken := &namespaceToken{}
|
||||||
var documents []*document.Node
|
var documents []*document.Node
|
||||||
@@ -168,6 +169,8 @@ func acquireUnderlayGroup(group loadGroup, index int, rule *VersionRule, account
|
|||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return nil, sources, contexts, diagnostics
|
return nil, sources, contexts, diagnostics
|
||||||
}
|
}
|
||||||
|
composition.inputBytes = accounting.bytes - initialBytes
|
||||||
|
composition.inputNodes = accounting.nodes - initialNodes
|
||||||
for pointer, origin := range composition.origins {
|
for pointer, origin := range composition.origins {
|
||||||
origin.Layer = LayerUnderlay
|
origin.Layer = LayerUnderlay
|
||||||
composition.origins[pointer] = origin
|
composition.origins[pointer] = origin
|
||||||
|
|||||||
@@ -208,3 +208,73 @@ func groupedErrorCode(t *testing.T, err error) Code {
|
|||||||
}
|
}
|
||||||
return loadError.Diagnostics()[0].Code
|
return loadError.Diagnostics()[0].Code
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGroupedAccountingChargesMixedOccurrencesOnce(t *testing.T) {
|
||||||
|
const (
|
||||||
|
one = "configkit: {imports: [{path: child-one.yml}]}\nleft: first\n"
|
||||||
|
child = "extra: imported\n"
|
||||||
|
two = "configkit: {imports: [{path: child-two.yml}]}\nright: second\n"
|
||||||
|
)
|
||||||
|
filesystem := NewFilesystem("bundle", fstest.MapFS{
|
||||||
|
"one.yml": &fstest.MapFile{Data: []byte(one)},
|
||||||
|
"child-one.yml": &fstest.MapFile{Data: []byte(child)},
|
||||||
|
"two.yml": &fstest.MapFile{Data: []byte(two)},
|
||||||
|
"child-two.yml": &fstest.MapFile{Data: []byte(child)},
|
||||||
|
})
|
||||||
|
accounting := &importAccounting{}
|
||||||
|
first, sources, _, diagnostics := acquireUnderlayGroup(loadGroup{inputs: []loadInput{{source: filesystem.File("one.yml"), imports: true}}}, 1, nil, accounting)
|
||||||
|
if len(diagnostics) != 0 || first == nil || len(sources) != 2 || accounting.sources != 2 || accounting.bytes != int64(len(one)+len(child)) {
|
||||||
|
t.Fatalf("first acquisition = %#v, %#v, %#v, accounting %#v", first, sources, diagnostics, accounting)
|
||||||
|
}
|
||||||
|
second, sources, _, diagnostics := acquireUnderlayGroup(loadGroup{inputs: []loadInput{{source: filesystem.File("two.yml"), imports: true}}}, 2, nil, accounting)
|
||||||
|
if len(diagnostics) != 0 || second == nil || len(sources) != 2 || sources[0].ID != "source-3" || sources[1].ID != "source-4" || accounting.sources != 4 || accounting.bytes != int64(len(one)+len(two)+2*len(child)) {
|
||||||
|
t.Fatalf("second acquisition = %#v, %#v, %#v, accounting %#v", second, sources, diagnostics, accounting)
|
||||||
|
}
|
||||||
|
var wantNodes int
|
||||||
|
for _, text := range []string{one, child, two, child} {
|
||||||
|
parsed, diagnostics := parseSource(Bytes("fixture", []byte(text)), "source-1", defaultParseLimits())
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatal(diagnostics)
|
||||||
|
}
|
||||||
|
wantNodes += countInputNodes(parsed.document)
|
||||||
|
}
|
||||||
|
if accounting.nodes != wantNodes {
|
||||||
|
t.Fatalf("charged nodes = %d, want %d", accounting.nodes, wantNodes)
|
||||||
|
}
|
||||||
|
if first.inputBytes != int64(len(one)+len(child)) || second.inputBytes != int64(len(two)+len(child)) {
|
||||||
|
t.Fatalf("composition costs = %d, %d, accounting %d", first.inputBytes, second.inputBytes, accounting.bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroupedGlobEntryBudgetIsShared(t *testing.T) {
|
||||||
|
filesystem := NewFilesystem("bundle", fstest.MapFS{
|
||||||
|
"main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{glob: 'parts/*.yml'}]}\n")},
|
||||||
|
"parts/a.yml": &fstest.MapFile{Data: []byte("left: one\n")},
|
||||||
|
"parts/b.yml": &fstest.MapFile{Data: []byte("right: two\n")},
|
||||||
|
})
|
||||||
|
accounting := &importAccounting{entries: maxGlobEntries - 1}
|
||||||
|
_, _, _, diagnostics := acquireUnderlayGroup(loadGroup{inputs: []loadInput{{source: filesystem.File("main.yml"), imports: true}}}, 1, nil, accounting)
|
||||||
|
if len(diagnostics) == 0 || diagnostics[0].Code != CodeImportLimitExceeded || accounting.entries <= maxGlobEntries {
|
||||||
|
t.Fatalf("shared glob budget = %#v, accounting %#v", diagnostics, accounting)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroupedMixedSourcesShareCLIRecordLimit(t *testing.T) {
|
||||||
|
filesystem := NewFilesystem("bundle", fstest.MapFS{"main.yml": &fstest.MapFile{Data: []byte("value: base\n")}})
|
||||||
|
inputs := make([]Input, maxImportSources-1)
|
||||||
|
inputs[0] = Input{Source: Bytes("empty-bytes", []byte("{}\n"))}
|
||||||
|
for index := 1; index < len(inputs); index++ {
|
||||||
|
inputs[index] = Input{Source: Mapping("empty-map", map[string]any{})}
|
||||||
|
}
|
||||||
|
type config struct {
|
||||||
|
Value string `yaml:"value"`
|
||||||
|
}
|
||||||
|
_, err := LoadLayered(Input{Source: filesystem.File("main.yml")}, testSecretBinding(), LayeredOptions[config]{
|
||||||
|
Underlays: []Underlay{{Inputs: inputs}},
|
||||||
|
Overrides: []Override{{Path: Keys("value"), Value: "cli", Flag: "--value"}},
|
||||||
|
})
|
||||||
|
loadError, ok := err.(*LoadError)
|
||||||
|
if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != CodeOverrideLimitExceeded || len(loadError.Sources()) != maxImportSources {
|
||||||
|
t.Fatalf("mixed source/CLI limit = %#v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
156
internal/engine/underlay_bench_test.go
Normal file
156
internal/engine/underlay_bench_test.go
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"testing/fstest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type benchmarkCatalogConfig struct {
|
||||||
|
Catalog map[string]string `yaml:"catalog"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type benchmarkNodeConfig struct {
|
||||||
|
Node string `yaml:"node"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUnderlayBroadCatalog(b *testing.B) {
|
||||||
|
for _, width := range []int{128, 256, 512} {
|
||||||
|
b.Run(strconv.Itoa(width), func(b *testing.B) {
|
||||||
|
inputs := make([]Input, 2)
|
||||||
|
for half := range inputs {
|
||||||
|
catalog := make(map[string]any, width/2)
|
||||||
|
for index := range width / 2 {
|
||||||
|
catalog["item-"+strconv.Itoa(half*width/2+index)] = "value"
|
||||||
|
}
|
||||||
|
inputs[half] = Input{Source: Mapping("catalog-"+strconv.Itoa(half), map[string]any{"catalog": catalog})}
|
||||||
|
}
|
||||||
|
options := LayeredOptions[benchmarkCatalogConfig]{Underlays: []Underlay{{Inputs: inputs}}}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for range b.N {
|
||||||
|
if _, err := LoadLayered(Input{Source: NoSource()}, testSecretBinding(), options); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUnderlayManySmallTiers(b *testing.B) {
|
||||||
|
for _, count := range []int{32, 64, 128} {
|
||||||
|
b.Run(strconv.Itoa(count), func(b *testing.B) {
|
||||||
|
groups := make([]Underlay, count)
|
||||||
|
for index := range groups {
|
||||||
|
groups[index].Inputs = []Input{{Source: Mapping("item", map[string]any{
|
||||||
|
"catalog": map[string]any{"item-" + strconv.Itoa(index): "value"},
|
||||||
|
})}}
|
||||||
|
}
|
||||||
|
options := LayeredOptions[benchmarkCatalogConfig]{Underlays: groups}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for range b.N {
|
||||||
|
if _, err := LoadLayered(Input{Source: NoSource()}, testSecretBinding(), options); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUnderlaySubtreeReplacement(b *testing.B) {
|
||||||
|
for _, count := range []int{32, 64, 128} {
|
||||||
|
b.Run(strconv.Itoa(count), func(b *testing.B) {
|
||||||
|
groups := make([]Underlay, count)
|
||||||
|
for index := range groups {
|
||||||
|
var node any = "temporary"
|
||||||
|
if index%2 == 0 {
|
||||||
|
node = map[string]any{"left": "one", "right": "two", "nested": map[string]any{"enabled": false}}
|
||||||
|
}
|
||||||
|
groups[index].Inputs = []Input{{Source: Mapping("replacement", map[string]any{"node": node})}}
|
||||||
|
}
|
||||||
|
options := LayeredOptions[benchmarkNodeConfig]{Underlays: groups}
|
||||||
|
main := Input{Source: Mapping("main", map[string]any{"node": "final"})}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for range b.N {
|
||||||
|
if _, err := LoadLayered(main, testSecretBinding(), options); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUnderlayNativeImports(b *testing.B) {
|
||||||
|
for _, count := range []int{16, 32, 64} {
|
||||||
|
b.Run(strconv.Itoa(count), func(b *testing.B) {
|
||||||
|
files := fstest.MapFS{
|
||||||
|
"main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{glob: 'defaults/*.yml'}]}\n")},
|
||||||
|
}
|
||||||
|
for index := range count {
|
||||||
|
name := "item-" + strconv.Itoa(index)
|
||||||
|
files["defaults/"+name+".yml"] = &fstest.MapFile{Data: []byte("catalog: {" + name + ": value}\n")}
|
||||||
|
}
|
||||||
|
filesystem := NewFilesystem("bundle", files)
|
||||||
|
main := Input{Source: filesystem.File("main.yml"), Imports: true}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for range b.N {
|
||||||
|
if _, err := LoadLayered[benchmarkCatalogConfig](main, testSecretBinding()); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUnderlayRetainedHistory(b *testing.B) {
|
||||||
|
for _, count := range []int{16, 32, 64} {
|
||||||
|
b.Run(strconv.Itoa(count), func(b *testing.B) {
|
||||||
|
groups := make([]Underlay, count)
|
||||||
|
for index := range groups {
|
||||||
|
groups[index].Inputs = []Input{{Source: Mapping("value", map[string]any{
|
||||||
|
"catalog": map[string]any{"shared": strconv.Itoa(index)},
|
||||||
|
})}}
|
||||||
|
}
|
||||||
|
options := LayeredOptions[benchmarkCatalogConfig]{Underlays: groups}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for range b.N {
|
||||||
|
result, err := LoadLayered(Input{Source: NoSource()}, testSecretBinding(), options)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, ok := result.Inspection.Explain(Keys("catalog", "shared")); !ok {
|
||||||
|
b.Fatal("retained path was not explained")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUnderlayRenderedHistory(b *testing.B) {
|
||||||
|
for _, count := range []int{32, 64, 128} {
|
||||||
|
b.Run(strconv.Itoa(count), func(b *testing.B) {
|
||||||
|
groups := make([]Underlay, count)
|
||||||
|
for index := range groups {
|
||||||
|
groups[index].Inputs = []Input{{Source: Mapping("value", map[string]any{
|
||||||
|
"catalog": map[string]any{"shared": strconv.Itoa(index)},
|
||||||
|
})}}
|
||||||
|
}
|
||||||
|
result, err := LoadLayered(Input{Source: NoSource()}, testSecretBinding(), LayeredOptions[benchmarkCatalogConfig]{Underlays: groups})
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for range b.N {
|
||||||
|
if err := result.Inspection.WriteExplanation(io.Discard, Keys("catalog", "shared")); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user