Reuse frozen inspection mapping lookups
This commit is contained in:
@@ -371,7 +371,7 @@ Roadmap candidate 7. Owners: engine metadata, metadata cloning, inheritance hist
|
|||||||
Validate: `go test ./internal/engine -run 'Inheritance|Inherited|History|Metadata|Independent'`,
|
Validate: `go test ./internal/engine -run 'Inheritance|Inherited|History|Metadata|Independent'`,
|
||||||
engine race tests, and the everyday check.
|
engine race tests, and the everyday check.
|
||||||
|
|
||||||
## Stage 15: Reuse frozen inspection mapping lookups
|
## Stage 15: Reuse frozen inspection mapping lookups — Complete
|
||||||
|
|
||||||
Roadmap candidate 7. Owners: publication, inspection, document/snapshot/history helpers.
|
Roadmap candidate 7. Owners: publication, inspection, document/snapshot/history helpers.
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ type documentLookup struct {
|
|||||||
byNode map[*document.Node]map[string]int
|
byNode map[*document.Node]map[string]int
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildDocumentLookup(root *document.Node) *documentLookup {
|
func buildDocumentLookup(roots ...*document.Node) *documentLookup {
|
||||||
lookup := &documentLookup{byNode: make(map[*document.Node]map[string]int)}
|
lookup := &documentLookup{byNode: make(map[*document.Node]map[string]int)}
|
||||||
visited := make(map[*document.Node]struct{})
|
visited := make(map[*document.Node]struct{})
|
||||||
var visit func(*document.Node)
|
var visit func(*document.Node)
|
||||||
@@ -35,7 +35,9 @@ func buildDocumentLookup(root *document.Node) *documentLookup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
visit(root)
|
for _, root := range roots {
|
||||||
|
visit(root)
|
||||||
|
}
|
||||||
return lookup
|
return lookup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
|
|||||||
if r == nil || r.final == nil || r.baseline == nil || !r.metadata.hasHistoricalPath(path) {
|
if r == nil || r.final == nil || r.baseline == nil || !r.metadata.hasHistoricalPath(path) {
|
||||||
return Explanation{}, false
|
return Explanation{}, false
|
||||||
}
|
}
|
||||||
explanation := Explanation{Path: path.clone(), Effective: valueSnapshot(r.final, path, r.secrets)}
|
explanation := Explanation{Path: path.clone(), Effective: valueSnapshotWithLookup(r.final, path, r.secrets, r.immutableLookup)}
|
||||||
if info, ok := r.metadata.Lookup(path); ok {
|
if info, ok := r.metadata.Lookup(path); ok {
|
||||||
explanation.Origin = cloneOrigin(info.Origin)
|
explanation.Origin = cloneOrigin(info.Origin)
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
|
|||||||
}
|
}
|
||||||
replayIndex := new(mappingIndex)
|
replayIndex := new(mappingIndex)
|
||||||
for _, application := range r.applications {
|
for _, application := range r.applications {
|
||||||
operationPath, relevant := applicationOperationPath(application, path)
|
operationPath, relevant := applicationOperationPath(application, path, r.immutableLookup)
|
||||||
var before ValueSnapshot
|
var before ValueSnapshot
|
||||||
if relevant {
|
if relevant {
|
||||||
before = valueSnapshot(working, path, r.secrets)
|
before = valueSnapshot(working, path, r.secrets)
|
||||||
@@ -69,7 +69,7 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
|
|||||||
explanation.History = append(explanation.History, ValueChange{
|
explanation.History = append(explanation.History, ValueChange{
|
||||||
Before: before,
|
Before: before,
|
||||||
After: after,
|
After: after,
|
||||||
Origin: historyApplicationOrigin(application, operationPath),
|
Origin: historyApplicationOrigin(application, operationPath, r.immutableLookup),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
appendInheritanceExplanation(&explanation, &working, path, r.inheritance, r.secrets)
|
appendInheritanceExplanation(&explanation, &working, path, r.inheritance, r.secrets)
|
||||||
@@ -78,13 +78,13 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
|
|||||||
// decoding applies them to the final input tree. Show that reconciliation
|
// decoding applies them to the final input tree. Show that reconciliation
|
||||||
// explicitly, including canonical scalar conversions, before domain changes.
|
// explicitly, including canonical scalar conversions, before domain changes.
|
||||||
replayed, _ := documentAtPath(working, path)
|
replayed, _ := documentAtPath(working, path)
|
||||||
decoded, _ := documentAtPath(r.input, path)
|
decoded, _ := documentAtPathWithLookup(r.input, path, r.immutableLookup)
|
||||||
if !configurationValuesEqual(replayed, decoded) {
|
if !configurationValuesEqual(replayed, decoded) {
|
||||||
origin := cloneOrigin(r.inputOrigins[path.String()])
|
origin := cloneOrigin(r.inputOrigins[path.String()])
|
||||||
origin.Operation = ChangeDecoding
|
origin.Operation = ChangeDecoding
|
||||||
explanation.History = append(explanation.History, ValueChange{
|
explanation.History = append(explanation.History, ValueChange{
|
||||||
Before: valueSnapshot(working, path, r.secrets),
|
Before: valueSnapshot(working, path, r.secrets),
|
||||||
After: valueSnapshot(r.input, path, r.secrets),
|
After: valueSnapshotWithLookup(r.input, path, r.secrets, r.immutableLookup),
|
||||||
Origin: origin,
|
Origin: origin,
|
||||||
Reasons: []string{"applied retained defaults and canonical typed decoding"},
|
Reasons: []string{"applied retained defaults and canonical typed decoding"},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||||
@@ -566,3 +567,49 @@ func TestExplanationReplayDescriptorsRemainDetachedAcrossQueries(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Fatalf("resolution change missing from repeated explanation: %#v", second.History)
|
t.Fatalf("resolution change missing from repeated explanation: %#v", second.History)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConcurrentReadOnlyInspectionQueries(t *testing.T) {
|
||||||
|
type config struct {
|
||||||
|
Group struct {
|
||||||
|
Value string `yaml:"value"`
|
||||||
|
} `yaml:"group"`
|
||||||
|
}
|
||||||
|
result, err := testLoad(Bytes("concurrent-inspection.yml", []byte("group: {value: input}\n")), Options[config]{
|
||||||
|
Definition: Definition[config]{Resolve: func(value *config, resolution *Resolution) error {
|
||||||
|
value.Group.Value = "resolved"
|
||||||
|
resolution.Record(Keys("group", "value"), ChangeNormalization, "normalized", Keys("group", "value"))
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
paths := []Path{Keys("group"), Keys("group", "value")}
|
||||||
|
var wait sync.WaitGroup
|
||||||
|
errors := make(chan string, 8)
|
||||||
|
for worker := 0; worker < cap(errors); worker++ {
|
||||||
|
wait.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wait.Done()
|
||||||
|
for iteration := 0; iteration < 8; iteration++ {
|
||||||
|
for _, path := range paths {
|
||||||
|
explanation, found := result.Explain(path)
|
||||||
|
if !found || !explanation.Effective.Present {
|
||||||
|
errors <- "missing effective explanation for " + path.String()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
metadata := result.Metadata()
|
||||||
|
if _, found := metadata.Lookup(path); !found || len(metadata.History(path)) == 0 {
|
||||||
|
errors <- "missing metadata for " + path.String()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wait.Wait()
|
||||||
|
close(errors)
|
||||||
|
for message := range errors {
|
||||||
|
t.Error(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func cloneHistoryApplications(values []historyApplication) []historyApplication
|
|||||||
return cloned
|
return cloned
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationOperationPath(application historyApplication, query Path) (Path, bool) {
|
func applicationOperationPath(application historyApplication, query Path, lookup *documentLookup) (Path, bool) {
|
||||||
if !application.path.valid() || application.value == nil {
|
if !application.path.valid() || application.value == nil {
|
||||||
return Path{}, false
|
return Path{}, false
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ func applicationOperationPath(application historyApplication, query Path) (Path,
|
|||||||
}
|
}
|
||||||
for length := 1; length <= len(query.segments); length++ {
|
for length := 1; length <= len(query.segments); length++ {
|
||||||
prefix := Path{segments: clonePathSegments(query.segments[:length], 0)}
|
prefix := Path{segments: clonePathSegments(query.segments[:length], 0)}
|
||||||
node, present := documentAtPath(application.value, prefix)
|
node, present := documentAtPathWithLookup(application.value, prefix, lookup)
|
||||||
if !present {
|
if !present {
|
||||||
return Path{}, false
|
return Path{}, false
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ func applicationOperationPath(application historyApplication, query Path) (Path,
|
|||||||
return Path{}, false
|
return Path{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func historyApplicationOrigin(application historyApplication, operationPath Path) Origin {
|
func historyApplicationOrigin(application historyApplication, operationPath Path, lookup *documentLookup) Origin {
|
||||||
if application.origin.Layer != "" {
|
if application.origin.Layer != "" {
|
||||||
return cloneOrigin(application.origin)
|
return cloneOrigin(application.origin)
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@ func historyApplicationOrigin(application historyApplication, operationPath Path
|
|||||||
if !ok {
|
if !ok {
|
||||||
return origin
|
return origin
|
||||||
}
|
}
|
||||||
if node, present := documentAtPath(application.value, relative); present {
|
if node, present := documentAtPathWithLookup(application.value, relative, lookup); present {
|
||||||
origin.Locations = []Location{publicLocation(node.Location)}
|
origin.Locations = []Location{publicLocation(node.Location)}
|
||||||
}
|
}
|
||||||
return origin
|
return origin
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ type Inspection struct {
|
|||||||
input *document.Node
|
input *document.Node
|
||||||
inputOrigins map[string]Origin
|
inputOrigins map[string]Origin
|
||||||
final *document.Node
|
final *document.Node
|
||||||
|
immutableLookup *documentLookup
|
||||||
resolution []resolutionSnapshot
|
resolution []resolutionSnapshot
|
||||||
resolutionReplay []resolutionSnapshot
|
resolutionReplay []resolutionSnapshot
|
||||||
applications []historyApplication
|
applications []historyApplication
|
||||||
|
|||||||
@@ -98,6 +98,11 @@ func publishLoadResult[T any](schema *schemaDescriptor, definition Definition[T]
|
|||||||
final, applications, resolutionSnapshots := resolved.final, resolved.applications, resolved.snapshots
|
final, applications, resolutionSnapshots := resolved.final, resolved.applications, resolved.snapshots
|
||||||
finalizeMetadataPaths(&metadata, final, applications, decoded.baseline, decoded.final)
|
finalizeMetadataPaths(&metadata, final, applications, decoded.baseline, decoded.final)
|
||||||
secrets := classifySecretPaths(schema, definition.Secret, metadata, final, applications, decoded.baseline, decoded.final)
|
secrets := classifySecretPaths(schema, definition.Secret, metadata, final, applications, decoded.baseline, decoded.final)
|
||||||
|
retainedTrees := make([]*document.Node, 0, len(applications)+2)
|
||||||
|
retainedTrees = append(retainedTrees, decoded.final, final)
|
||||||
|
for _, application := range applications {
|
||||||
|
retainedTrees = append(retainedTrees, application.value)
|
||||||
|
}
|
||||||
inspection := &Inspection{
|
inspection := &Inspection{
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
sources: cloneSourceInfos(accumulator.sources),
|
sources: cloneSourceInfos(accumulator.sources),
|
||||||
@@ -107,6 +112,7 @@ func publishLoadResult[T any](schema *schemaDescriptor, definition Definition[T]
|
|||||||
input: decoded.final,
|
input: decoded.final,
|
||||||
inputOrigins: cloneOriginIndex(decoded.origins),
|
inputOrigins: cloneOriginIndex(decoded.origins),
|
||||||
final: final,
|
final: final,
|
||||||
|
immutableLookup: buildDocumentLookup(retainedTrees...),
|
||||||
resolution: resolutionSnapshots,
|
resolution: resolutionSnapshots,
|
||||||
resolutionReplay: coalescedResolutionSnapshots(resolutionSnapshots),
|
resolutionReplay: coalescedResolutionSnapshots(resolutionSnapshots),
|
||||||
applications: applications,
|
applications: applications,
|
||||||
|
|||||||
@@ -102,7 +102,11 @@ func TestDocumentLookupMatchesPathScan(t *testing.T) {
|
|||||||
{Key: "01", Value: &document.Node{Kind: document.String, Text: "leading zero key"}},
|
{Key: "01", Value: &document.Node{Kind: document.String, Text: "leading zero key"}},
|
||||||
{Key: "items", Value: sequence},
|
{Key: "items", Value: sequence},
|
||||||
}}
|
}}
|
||||||
lookup := buildDocumentLookup(root)
|
applicationRoot := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{
|
||||||
|
{Key: "shared", Value: sequence},
|
||||||
|
{Key: "application", Value: &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{Key: "value", Value: &document.Node{Kind: document.String, Text: "application"}}}}},
|
||||||
|
}}
|
||||||
|
lookup := buildDocumentLookup(root, applicationRoot)
|
||||||
paths := []Path{
|
paths := []Path{
|
||||||
Path{},
|
Path{},
|
||||||
Keys("0"),
|
Keys("0"),
|
||||||
@@ -123,6 +127,13 @@ func TestDocumentLookupMatchesPathScan(t *testing.T) {
|
|||||||
t.Errorf("indexed lookup %s = (%p, %v), want (%p, %v)", path.String(), gotNode, gotFound, wantNode, wantFound)
|
t.Errorf("indexed lookup %s = (%p, %v), want (%p, %v)", path.String(), gotNode, gotFound, wantNode, wantFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, path := range []Path{Path{}, Keys("shared"), Keys("shared").Index(1).Key("name"), Keys("application", "value")} {
|
||||||
|
wantNode, wantFound := documentAtPath(applicationRoot, path)
|
||||||
|
gotNode, gotFound := documentAtPathWithLookup(applicationRoot, path, lookup)
|
||||||
|
if gotNode != wantNode || gotFound != wantFound {
|
||||||
|
t.Errorf("indexed application lookup %s = (%p, %v), want (%p, %v)", path.String(), gotNode, gotFound, wantNode, wantFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApplyResolutionMetadataPreservesOverlappingDeltaOrder(t *testing.T) {
|
func TestApplyResolutionMetadataPreservesOverlappingDeltaOrder(t *testing.T) {
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ import (
|
|||||||
// Snapshot projection is the shared redaction boundary for effective YAML
|
// Snapshot projection is the shared redaction boundary for effective YAML
|
||||||
// and explanation values. It borrows frozen trees and returns a detached tree.
|
// and explanation values. It borrows frozen trees and returns a detached tree.
|
||||||
func valueSnapshot(root *document.Node, path Path, secrets map[string]bool) ValueSnapshot {
|
func valueSnapshot(root *document.Node, path Path, secrets map[string]bool) ValueSnapshot {
|
||||||
node, present := documentAtPath(root, path)
|
return valueSnapshotWithLookup(root, path, secrets, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueSnapshotWithLookup(root *document.Node, path Path, secrets map[string]bool, lookup *documentLookup) ValueSnapshot {
|
||||||
|
node, present := documentAtPathWithLookup(root, path, lookup)
|
||||||
if !present {
|
if !present {
|
||||||
return ValueSnapshot{}
|
return ValueSnapshot{}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user