Prepare coalesced resolution replay
This commit is contained in:
@@ -91,7 +91,7 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
|
||||
}
|
||||
|
||||
working = document.Clone(r.input)
|
||||
for _, snapshot := range coalescedResolutionSnapshots(r.resolution) {
|
||||
for _, snapshot := range r.resolutionReplay {
|
||||
relevant := pathsIntersect(snapshot.change.Path, path)
|
||||
var before ValueSnapshot
|
||||
if relevant {
|
||||
|
||||
@@ -520,3 +520,49 @@ func TestExplanationReplaysResolutionAdditionsAndOverlappingIntent(t *testing.T)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExplanationReplayDescriptorsRemainDetachedAcrossQueries(t *testing.T) {
|
||||
type config struct {
|
||||
Count int `yaml:"count"`
|
||||
}
|
||||
result, err := testLoad(Bytes("resolution-explanation.yml", []byte("count: 1\n")), Options[config]{
|
||||
Definition: Definition[config]{Resolve: func(value *config, resolution *Resolution) error {
|
||||
value.Count = 2
|
||||
resolution.Record(Keys("count"), ChangeNormalization, "normalized", Keys("count"))
|
||||
return nil
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first, ok := result.Explain(Keys("count"))
|
||||
if !ok {
|
||||
t.Fatal("missing explanation")
|
||||
}
|
||||
var firstResolution *ValueChange
|
||||
for index := range first.History {
|
||||
if first.History[index].Origin.Layer == LayerResolution {
|
||||
firstResolution = &first.History[index]
|
||||
break
|
||||
}
|
||||
}
|
||||
if firstResolution == nil || len(firstResolution.Reasons) != 1 || len(firstResolution.Dependencies) != 1 {
|
||||
t.Fatalf("resolution history = %#v", first.History)
|
||||
}
|
||||
firstResolution.Reasons[0] = "mutated"
|
||||
firstResolution.Dependencies[0].segments[0].key = "mutated"
|
||||
|
||||
second, ok := result.Explain(Keys("count"))
|
||||
if !ok {
|
||||
t.Fatal("missing repeated explanation")
|
||||
}
|
||||
for _, change := range second.History {
|
||||
if change.Origin.Layer == LayerResolution {
|
||||
if !reflect.DeepEqual(change.Reasons, []string{"normalized"}) || len(change.Dependencies) != 1 || change.Dependencies[0].String() != "/count" {
|
||||
t.Fatalf("repeated explanation observed caller mutation: %#v", change)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("resolution change missing from repeated explanation: %#v", second.History)
|
||||
}
|
||||
|
||||
@@ -179,28 +179,50 @@ func mergeHistoryAtPath(root **document.Node, path Path, incoming *document.Node
|
||||
// intent into that event rather than replay a child assignment a second time.
|
||||
func coalescedResolutionSnapshots(snapshots []resolutionSnapshot) []resolutionSnapshot {
|
||||
groups := make([]resolutionSnapshot, 0, len(snapshots))
|
||||
reasons := make([]map[string]struct{}, 0, len(snapshots))
|
||||
dependencies := make([]map[string]struct{}, 0, len(snapshots))
|
||||
var coverage pathPrefixIndex
|
||||
for _, snapshot := range snapshots {
|
||||
merged := false
|
||||
for i := range groups {
|
||||
if !pathContains(groups[i].change.Path, snapshot.change.Path) {
|
||||
continue
|
||||
pointer := snapshot.change.Path.String()
|
||||
groupIndex := -1
|
||||
coverage.visitAncestors(pointer, func(id int) {
|
||||
if groupIndex < 0 || id < groupIndex {
|
||||
groupIndex = id
|
||||
}
|
||||
})
|
||||
if groupIndex >= 0 {
|
||||
group := &groups[groupIndex]
|
||||
for _, reason := range snapshot.change.Reasons {
|
||||
groups[i].change.Reasons = appendUniqueString(groups[i].change.Reasons, reason)
|
||||
if _, found := reasons[groupIndex][reason]; !found {
|
||||
reasons[groupIndex][reason] = struct{}{}
|
||||
group.change.Reasons = append(group.change.Reasons, reason)
|
||||
}
|
||||
}
|
||||
for _, dependency := range snapshot.change.Dependencies {
|
||||
groups[i].change.Dependencies = appendUniquePath(groups[i].change.Dependencies, dependency)
|
||||
dependencyPointer := dependency.String()
|
||||
if _, found := dependencies[groupIndex][dependencyPointer]; !found {
|
||||
dependencies[groupIndex][dependencyPointer] = struct{}{}
|
||||
group.change.Dependencies = append(group.change.Dependencies, dependency.clone())
|
||||
}
|
||||
}
|
||||
if snapshot.change.Origin.Operation == ChangeNormalization {
|
||||
groups[i].change.Origin.Operation = ChangeNormalization
|
||||
group.change.Origin.Operation = ChangeNormalization
|
||||
}
|
||||
merged = true
|
||||
break
|
||||
continue
|
||||
}
|
||||
if !merged {
|
||||
snapshot.change = cloneChange(snapshot.change)
|
||||
groups = append(groups, snapshot)
|
||||
snapshot.change = cloneChange(snapshot.change)
|
||||
groups = append(groups, snapshot)
|
||||
reasonSet := make(map[string]struct{}, len(snapshot.change.Reasons))
|
||||
for _, reason := range snapshot.change.Reasons {
|
||||
reasonSet[reason] = struct{}{}
|
||||
}
|
||||
reasons = append(reasons, reasonSet)
|
||||
dependencySet := make(map[string]struct{}, len(snapshot.change.Dependencies))
|
||||
for _, dependency := range snapshot.change.Dependencies {
|
||||
dependencySet[dependency.String()] = struct{}{}
|
||||
}
|
||||
dependencies = append(dependencies, dependencySet)
|
||||
coverage.insert(pointer, len(groups)-1)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
@@ -324,6 +324,7 @@ func TestInheritanceHistoryPrecedesApplicationResolution(t *testing.T) {
|
||||
replaceHistoryPath(&final, path, replacement)
|
||||
inspection.final = final
|
||||
inspection.resolution = []resolutionSnapshot{snapshot}
|
||||
inspection.resolutionReplay = coalescedResolutionSnapshots(inspection.resolution)
|
||||
inspection.metadata.history[path.String()] = append(inspection.metadata.history[path.String()], snapshot.change)
|
||||
inspection.metadata.fields[path.String()] = FieldInfo{Explicit: true, Origin: snapshot.change.Origin}
|
||||
history := inspection.Metadata().History(path)
|
||||
|
||||
@@ -85,6 +85,7 @@ type Inspection struct {
|
||||
inputOrigins map[string]Origin
|
||||
final *document.Node
|
||||
resolution []resolutionSnapshot
|
||||
resolutionReplay []resolutionSnapshot
|
||||
applications []historyApplication
|
||||
inheritance []inheritanceOperation
|
||||
secrets map[string]bool
|
||||
|
||||
@@ -108,6 +108,7 @@ func publishLoadResult[T any](schema *schemaDescriptor, definition Definition[T]
|
||||
inputOrigins: cloneOriginIndex(decoded.origins),
|
||||
final: final,
|
||||
resolution: resolutionSnapshots,
|
||||
resolutionReplay: coalescedResolutionSnapshots(resolutionSnapshots),
|
||||
applications: applications,
|
||||
inheritance: inheritanceOperations(resolved.inheritance),
|
||||
secrets: secrets,
|
||||
|
||||
@@ -1,11 +1,64 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
)
|
||||
|
||||
func TestCoalescedResolutionSnapshotsMatchOrderedScan(t *testing.T) {
|
||||
parsedDependency := parseOverrideTestPath(t, "/dependencies/item")
|
||||
snapshots := []resolutionSnapshot{
|
||||
{change: Change{Path: Keys("a", "b"), Reasons: []string{"child", "shared", "child"}, Dependencies: []Path{Keys("dependencies", "item")}, Origin: Origin{Operation: ChangeDerivedDefault}}},
|
||||
{change: Change{Path: Keys("a", "c"), Reasons: []string{"other"}, Dependencies: []Path{Keys("other")}, Origin: Origin{Operation: ChangeDerivedDefault}}},
|
||||
{change: Change{Path: Keys("a"), Reasons: []string{"parent"}, Dependencies: []Path{Keys("parent")}, Origin: Origin{Operation: ChangeDerivedDefault}}},
|
||||
{change: Change{Path: Keys("a", "b", "grand"), Reasons: []string{"deep", "shared"}, Dependencies: []Path{parsedDependency, Keys("deep")}, Origin: Origin{Operation: ChangeDerivedDefault}}},
|
||||
{change: Change{Path: Path{}, Reasons: []string{"root"}, Origin: Origin{Operation: ChangeDerivedDefault}}},
|
||||
{change: Change{Path: Keys("outside"), Reasons: []string{"root child"}, Origin: Origin{Operation: ChangeDerivedDefault}}},
|
||||
{change: Change{Path: Keys("a", "b", "normalization"), Reasons: []string{"normalized"}, Origin: Origin{Operation: ChangeNormalization}}},
|
||||
{change: Change{Path: Keys("a", "none"), Reasons: []string{"parent child"}, Origin: Origin{Operation: ChangeDerivedDefault}}},
|
||||
}
|
||||
want := coalescedResolutionSnapshotsByScan(snapshots)
|
||||
got := coalescedResolutionSnapshots(snapshots)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("coalesced snapshots differ from ordered scan:\n got: %#v\nwant: %#v", got, want)
|
||||
}
|
||||
if len(got) != 4 || got[0].change.Origin.Operation != ChangeNormalization ||
|
||||
!reflect.DeepEqual(got[0].change.Reasons, []string{"child", "shared", "child", "deep", "normalized"}) ||
|
||||
len(got[0].change.Dependencies) != 2 || got[3].change.Path.String() != "" {
|
||||
t.Fatalf("coalesced ordering or unions changed: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func coalescedResolutionSnapshotsByScan(snapshots []resolutionSnapshot) []resolutionSnapshot {
|
||||
groups := make([]resolutionSnapshot, 0, len(snapshots))
|
||||
for _, snapshot := range snapshots {
|
||||
merged := false
|
||||
for i := range groups {
|
||||
if !pathContains(groups[i].change.Path, snapshot.change.Path) {
|
||||
continue
|
||||
}
|
||||
for _, reason := range snapshot.change.Reasons {
|
||||
groups[i].change.Reasons = appendUniqueString(groups[i].change.Reasons, reason)
|
||||
}
|
||||
for _, dependency := range snapshot.change.Dependencies {
|
||||
groups[i].change.Dependencies = appendUniquePath(groups[i].change.Dependencies, dependency)
|
||||
}
|
||||
if snapshot.change.Origin.Operation == ChangeNormalization {
|
||||
groups[i].change.Origin.Operation = ChangeNormalization
|
||||
}
|
||||
merged = true
|
||||
break
|
||||
}
|
||||
if !merged {
|
||||
snapshot.change = cloneChange(snapshot.change)
|
||||
groups = append(groups, snapshot)
|
||||
}
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
func TestResolutionCoverageIndexMatchesOrderedScan(t *testing.T) {
|
||||
paths := []Path{
|
||||
Keys("group"),
|
||||
|
||||
Reference in New Issue
Block a user