219 lines
8.3 KiB
Go
219 lines
8.3 KiB
Go
package configkit
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
type layeredItem struct {
|
|
Value string `yaml:"value"`
|
|
}
|
|
|
|
type layeredConfig struct {
|
|
Shared struct {
|
|
Equal string `yaml:"equal"`
|
|
Imported string `yaml:"imported"`
|
|
Profile string `yaml:"profile"`
|
|
} `yaml:"shared"`
|
|
Values []int `yaml:"values"`
|
|
Items map[string]layeredItem `yaml:"items"`
|
|
Value any `yaml:"value"`
|
|
Nullable *string `yaml:"nullable"`
|
|
Derived int `yaml:"derived"`
|
|
Empty map[string]string `yaml:"empty"`
|
|
}
|
|
|
|
func TestLayeredProvenanceAgreesWithResolvedSnapshot(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", `_configkit:
|
|
imports: [{path: imported.yml}]
|
|
shared: {equal: same}
|
|
values: [1, 2]
|
|
items:
|
|
keep: {value: 'yes'}
|
|
remove: {value: 'no'}
|
|
value: {child: base}
|
|
nullable: base
|
|
empty: {}
|
|
`)
|
|
importPath := writeImportFile(t, directory, "imported.yml", "shared: {imported: base}\n")
|
|
profilePath := writeImportFile(t, directory, "profiles/testing.yml", `shared: {profile: selected}
|
|
values: [3, 4]
|
|
value: selected
|
|
nullable: null
|
|
empty: {}
|
|
`)
|
|
defaultNullable := "default"
|
|
definition := Definition[layeredConfig]{
|
|
Defaults: func() layeredConfig {
|
|
defaults := layeredConfig{
|
|
Values: []int{0, 0}, Value: map[string]any{"child": "default"},
|
|
Nullable: &defaultNullable, Empty: map[string]string{"retained": "default"},
|
|
}
|
|
defaults.Shared.Equal = "same"
|
|
return defaults
|
|
},
|
|
Resolve: func(configuration *layeredConfig, resolution *Resolution) error {
|
|
Normalize(resolution, Keys("values"), &configuration.Values, []int{5},
|
|
"bounded values", Keys("shared", "imported"), Keys("shared", "imported"))
|
|
Normalize(resolution, Keys("values"), &configuration.Values, []int{5},
|
|
"bounded values", Keys("shared", "imported"))
|
|
delete(configuration.Items, "remove")
|
|
resolution.Record(Keys("items"), ChangeNormalization, "removed disabled items")
|
|
Default(resolution, Keys("derived"), &configuration.Derived, 7,
|
|
"derived from equal", Keys("shared", "equal"), Keys("shared", "equal"))
|
|
resolution.Warning(Keys("values"), "values were bounded", Keys("shared", "imported"))
|
|
return nil
|
|
},
|
|
}
|
|
|
|
result, err := Load[layeredConfig](File(mainPath), Options[layeredConfig]{
|
|
Imports: true, Profile: "testing", Definition: definition,
|
|
})
|
|
if err != nil {
|
|
var loadError *LoadError
|
|
_ = errors.As(err, &loadError)
|
|
t.Fatalf("%v: %#v", err, loadError.Diagnostics())
|
|
}
|
|
wantSources := []string{canonicalTestPath(t, mainPath), canonicalTestPath(t, importPath), canonicalTestPath(t, profilePath)}
|
|
sources := result.Sources()
|
|
gotSources := make([]string, len(sources))
|
|
for index, source := range sources {
|
|
gotSources[index] = source.Path
|
|
}
|
|
if !reflect.DeepEqual(gotSources, wantSources) || sources[2].Layer != LayerProfile {
|
|
t.Fatalf("sources = %#v", sources)
|
|
}
|
|
if result.Config.Shared.Equal != "same" || result.Config.Shared.Imported != "base" || result.Config.Shared.Profile != "selected" ||
|
|
!reflect.DeepEqual(result.Config.Values, []int{5}) || result.Config.Nullable != nil || result.Config.Derived != 7 ||
|
|
result.Config.Empty["retained"] != "default" {
|
|
t.Fatalf("configuration = %#v", result.Config)
|
|
}
|
|
|
|
metadata := result.Metadata()
|
|
equal, _ := metadata.Lookup(Keys("shared", "equal"))
|
|
derived, _ := metadata.Lookup(Keys("derived"))
|
|
nullable, _ := metadata.Lookup(Keys("nullable"))
|
|
empty, _ := metadata.Lookup(Keys("empty"))
|
|
firstValue, _ := metadata.Lookup(Keys("values").Index(0))
|
|
if !equal.Explicit || equal.Origin.Layer != LayerBase || derived.Explicit || derived.Origin.Operation != ChangeDerivedDefault ||
|
|
!nullable.Explicit || !nullable.Null || nullable.Origin.Layer != LayerProfile ||
|
|
!empty.Explicit || empty.Origin.Layer != LayerProfile || len(empty.Origin.Locations) != 2 ||
|
|
!firstValue.Explicit || firstValue.Origin.Operation != ChangeNormalization {
|
|
t.Fatalf("metadata = equal %#v derived %#v nullable %#v empty %#v value %#v", equal, derived, nullable, empty, firstValue)
|
|
}
|
|
if _, ok := metadata.Lookup(Keys("value", "child")); ok {
|
|
t.Fatal("profile-removed child remains effective")
|
|
}
|
|
if _, ok := metadata.Lookup(Keys("values").Index(1)); ok {
|
|
t.Fatal("resolution-removed sequence index remains effective")
|
|
}
|
|
if _, ok := metadata.Lookup(Keys("items", "remove", "value")); ok {
|
|
t.Fatal("resolution-removed mapping child remains effective")
|
|
}
|
|
|
|
assertHistoryLayersAndPaths(t, metadata.History(Keys("values").Index(1)),
|
|
[]Layer{LayerDefault, LayerBase, LayerProfile, LayerResolution},
|
|
[]string{"/values/1", "/values/1", "/values", "/values"})
|
|
assertHistoryLayersAndPaths(t, metadata.History(Keys("value", "child")),
|
|
[]Layer{LayerDefault, LayerBase, LayerProfile},
|
|
[]string{"/value/child", "/value/child", "/value"})
|
|
removedHistory := metadata.History(Keys("items", "remove", "value"))
|
|
assertHistoryLayersAndPaths(t, removedHistory,
|
|
[]Layer{LayerBase, LayerResolution},
|
|
[]string{"/items/remove/value", "/items"})
|
|
if !reflect.DeepEqual(removedHistory[1].Reasons, []string{"removed disabled items"}) {
|
|
t.Fatalf("removed history = %#v", removedHistory)
|
|
}
|
|
valuesHistory := metadata.History(Keys("values").Index(0))
|
|
lastValue := valuesHistory[len(valuesHistory)-1]
|
|
if !reflect.DeepEqual(lastValue.Reasons, []string{"bounded values"}) || len(lastValue.Dependencies) != 1 || lastValue.Dependencies[0].String() != "/shared/imported" {
|
|
t.Fatalf("canonical resolution history = %#v", lastValue)
|
|
}
|
|
|
|
schema, issue := buildSchema(reflect.TypeFor[layeredConfig]())
|
|
if issue != nil {
|
|
t.Fatal(issue)
|
|
}
|
|
projected, projectionIssue := projectConfiguration(schema, reflect.ValueOf(result.Config), defaultProjectionLimits())
|
|
if projectionIssue != nil || !documentNodesEqual(projected, result.final) {
|
|
t.Fatalf("typed projection and private snapshot disagree: %#v", projectionIssue)
|
|
}
|
|
|
|
metadata.history[Keys("values").Index(0).String()][0].Origin.Layer = "mutated"
|
|
sources[1].ImportedFrom.SourceID = "mutated"
|
|
diagnostics := result.Diagnostics()
|
|
diagnostics[0].Related[0].SourceID = "mutated"
|
|
result.Config.Values[0] = 99
|
|
freshHistory := result.Metadata().History(Keys("values").Index(0))
|
|
if freshHistory[0].Origin.Layer == "mutated" || result.Sources()[1].ImportedFrom.SourceID == "mutated" ||
|
|
result.Diagnostics()[0].Related[0].SourceID == "mutated" {
|
|
t.Fatal("returned inspection records alias private state")
|
|
}
|
|
snapshotValue, ok := documentAtPath(result.final, Keys("values").Index(0))
|
|
if !ok || snapshotValue.Text != "5" {
|
|
t.Fatal("caller configuration mutation changed the private final snapshot")
|
|
}
|
|
}
|
|
|
|
func TestSelectedProfileLoadsRemainIndependentConcurrently(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", "values: [1, 2]\n")
|
|
writeImportFile(t, directory, "profiles/testing.yml", "values: [3, 4]\n")
|
|
options := Options[layeredConfig]{Profile: "testing"}
|
|
|
|
const loads = 16
|
|
results := make(chan *Result[layeredConfig], loads)
|
|
errors := make(chan error, loads)
|
|
var group sync.WaitGroup
|
|
for index := 0; index < loads; index++ {
|
|
group.Add(1)
|
|
go func() {
|
|
defer group.Done()
|
|
result, err := Load[layeredConfig](File(mainPath), options)
|
|
if err != nil {
|
|
errors <- err
|
|
return
|
|
}
|
|
results <- result
|
|
}()
|
|
}
|
|
group.Wait()
|
|
close(results)
|
|
close(errors)
|
|
for err := range errors {
|
|
t.Fatal(err)
|
|
}
|
|
var collected []*Result[layeredConfig]
|
|
for result := range results {
|
|
collected = append(collected, result)
|
|
}
|
|
if len(collected) != loads {
|
|
t.Fatalf("completed loads = %d", len(collected))
|
|
}
|
|
collected[0].Config.Values[0] = 99
|
|
collected[0].metadata.fields[Keys("values").String()] = FieldInfo{}
|
|
for _, result := range collected[1:] {
|
|
if !reflect.DeepEqual(result.Config.Values, []int{3, 4}) {
|
|
t.Fatalf("concurrent result contaminated = %#v", result.Config.Values)
|
|
}
|
|
if info, ok := result.Metadata().Lookup(Keys("values")); !ok || info.Origin.Layer != LayerProfile {
|
|
t.Fatalf("concurrent metadata contaminated = %#v, %v", info, ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertHistoryLayersAndPaths(t *testing.T, history []Change, layers []Layer, paths []string) {
|
|
t.Helper()
|
|
if len(history) != len(layers) || len(history) != len(paths) {
|
|
t.Fatalf("history = %#v", history)
|
|
}
|
|
for index, change := range history {
|
|
if change.Origin.Layer != layers[index] || change.Path.String() != paths[index] {
|
|
t.Fatalf("history[%d] = %#v", index, change)
|
|
}
|
|
}
|
|
}
|