Prepare coalesced resolution replay
This commit is contained in:
@@ -331,7 +331,7 @@ Roadmap candidate 4. Owner: engine `override.go`.
|
|||||||
Validate: `go test ./internal/engine -run 'Override|CLI|Provenance|Diagnostic'`
|
Validate: `go test ./internal/engine -run 'Override|CLI|Provenance|Diagnostic'`
|
||||||
and the everyday check. No policy changes.
|
and the everyday check. No policy changes.
|
||||||
|
|
||||||
## Stage 13: Coalesce resolution replay once with indexed ancestors
|
## Stage 13: Coalesce resolution replay once with indexed ancestors — Complete
|
||||||
|
|
||||||
Roadmap candidate 7. Owners: engine `history.go`, `load.go`, `load_result.go`.
|
Roadmap candidate 7. Owners: engine `history.go`, `load.go`, `load_result.go`.
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
working = document.Clone(r.input)
|
working = document.Clone(r.input)
|
||||||
for _, snapshot := range coalescedResolutionSnapshots(r.resolution) {
|
for _, snapshot := range r.resolutionReplay {
|
||||||
relevant := pathsIntersect(snapshot.change.Path, path)
|
relevant := pathsIntersect(snapshot.change.Path, path)
|
||||||
var before ValueSnapshot
|
var before ValueSnapshot
|
||||||
if relevant {
|
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.
|
// intent into that event rather than replay a child assignment a second time.
|
||||||
func coalescedResolutionSnapshots(snapshots []resolutionSnapshot) []resolutionSnapshot {
|
func coalescedResolutionSnapshots(snapshots []resolutionSnapshot) []resolutionSnapshot {
|
||||||
groups := make([]resolutionSnapshot, 0, len(snapshots))
|
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 {
|
for _, snapshot := range snapshots {
|
||||||
merged := false
|
pointer := snapshot.change.Path.String()
|
||||||
for i := range groups {
|
groupIndex := -1
|
||||||
if !pathContains(groups[i].change.Path, snapshot.change.Path) {
|
coverage.visitAncestors(pointer, func(id int) {
|
||||||
continue
|
if groupIndex < 0 || id < groupIndex {
|
||||||
|
groupIndex = id
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
if groupIndex >= 0 {
|
||||||
|
group := &groups[groupIndex]
|
||||||
for _, reason := range snapshot.change.Reasons {
|
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 {
|
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 {
|
if snapshot.change.Origin.Operation == ChangeNormalization {
|
||||||
groups[i].change.Origin.Operation = ChangeNormalization
|
group.change.Origin.Operation = ChangeNormalization
|
||||||
}
|
}
|
||||||
merged = true
|
continue
|
||||||
break
|
|
||||||
}
|
}
|
||||||
if !merged {
|
snapshot.change = cloneChange(snapshot.change)
|
||||||
snapshot.change = cloneChange(snapshot.change)
|
groups = append(groups, snapshot)
|
||||||
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
|
return groups
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,6 +324,7 @@ func TestInheritanceHistoryPrecedesApplicationResolution(t *testing.T) {
|
|||||||
replaceHistoryPath(&final, path, replacement)
|
replaceHistoryPath(&final, path, replacement)
|
||||||
inspection.final = final
|
inspection.final = final
|
||||||
inspection.resolution = []resolutionSnapshot{snapshot}
|
inspection.resolution = []resolutionSnapshot{snapshot}
|
||||||
|
inspection.resolutionReplay = coalescedResolutionSnapshots(inspection.resolution)
|
||||||
inspection.metadata.history[path.String()] = append(inspection.metadata.history[path.String()], snapshot.change)
|
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}
|
inspection.metadata.fields[path.String()] = FieldInfo{Explicit: true, Origin: snapshot.change.Origin}
|
||||||
history := inspection.Metadata().History(path)
|
history := inspection.Metadata().History(path)
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ type Inspection struct {
|
|||||||
inputOrigins map[string]Origin
|
inputOrigins map[string]Origin
|
||||||
final *document.Node
|
final *document.Node
|
||||||
resolution []resolutionSnapshot
|
resolution []resolutionSnapshot
|
||||||
|
resolutionReplay []resolutionSnapshot
|
||||||
applications []historyApplication
|
applications []historyApplication
|
||||||
inheritance []inheritanceOperation
|
inheritance []inheritanceOperation
|
||||||
secrets map[string]bool
|
secrets map[string]bool
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ func publishLoadResult[T any](schema *schemaDescriptor, definition Definition[T]
|
|||||||
inputOrigins: cloneOriginIndex(decoded.origins),
|
inputOrigins: cloneOriginIndex(decoded.origins),
|
||||||
final: final,
|
final: final,
|
||||||
resolution: resolutionSnapshots,
|
resolution: resolutionSnapshots,
|
||||||
|
resolutionReplay: coalescedResolutionSnapshots(resolutionSnapshots),
|
||||||
applications: applications,
|
applications: applications,
|
||||||
inheritance: inheritanceOperations(resolved.inheritance),
|
inheritance: inheritanceOperations(resolved.inheritance),
|
||||||
secrets: secrets,
|
secrets: secrets,
|
||||||
|
|||||||
@@ -1,11 +1,64 @@
|
|||||||
package engine
|
package engine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"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) {
|
func TestResolutionCoverageIndexMatchesOrderedScan(t *testing.T) {
|
||||||
paths := []Path{
|
paths := []Path{
|
||||||
Keys("group"),
|
Keys("group"),
|
||||||
|
|||||||
Reference in New Issue
Block a user