Reuse frozen inspection mapping lookups
This commit is contained in:
@@ -7,7 +7,7 @@ type documentLookup struct {
|
||||
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)}
|
||||
visited := make(map[*document.Node]struct{})
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
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 {
|
||||
explanation.Origin = cloneOrigin(info.Origin)
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
|
||||
}
|
||||
replayIndex := new(mappingIndex)
|
||||
for _, application := range r.applications {
|
||||
operationPath, relevant := applicationOperationPath(application, path)
|
||||
operationPath, relevant := applicationOperationPath(application, path, r.immutableLookup)
|
||||
var before ValueSnapshot
|
||||
if relevant {
|
||||
before = valueSnapshot(working, path, r.secrets)
|
||||
@@ -69,7 +69,7 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
|
||||
explanation.History = append(explanation.History, ValueChange{
|
||||
Before: before,
|
||||
After: after,
|
||||
Origin: historyApplicationOrigin(application, operationPath),
|
||||
Origin: historyApplicationOrigin(application, operationPath, r.immutableLookup),
|
||||
})
|
||||
}
|
||||
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
|
||||
// explicitly, including canonical scalar conversions, before domain changes.
|
||||
replayed, _ := documentAtPath(working, path)
|
||||
decoded, _ := documentAtPath(r.input, path)
|
||||
decoded, _ := documentAtPathWithLookup(r.input, path, r.immutableLookup)
|
||||
if !configurationValuesEqual(replayed, decoded) {
|
||||
origin := cloneOrigin(r.inputOrigins[path.String()])
|
||||
origin.Operation = ChangeDecoding
|
||||
explanation.History = append(explanation.History, ValueChange{
|
||||
Before: valueSnapshot(working, path, r.secrets),
|
||||
After: valueSnapshot(r.input, path, r.secrets),
|
||||
After: valueSnapshotWithLookup(r.input, path, r.secrets, r.immutableLookup),
|
||||
Origin: origin,
|
||||
Reasons: []string{"applied retained defaults and canonical typed decoding"},
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
return Path{}, false
|
||||
}
|
||||
@@ -80,7 +80,7 @@ func applicationOperationPath(application historyApplication, query Path) (Path,
|
||||
}
|
||||
for length := 1; length <= len(query.segments); length++ {
|
||||
prefix := Path{segments: clonePathSegments(query.segments[:length], 0)}
|
||||
node, present := documentAtPath(application.value, prefix)
|
||||
node, present := documentAtPathWithLookup(application.value, prefix, lookup)
|
||||
if !present {
|
||||
return Path{}, false
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func applicationOperationPath(application historyApplication, query Path) (Path,
|
||||
return Path{}, false
|
||||
}
|
||||
|
||||
func historyApplicationOrigin(application historyApplication, operationPath Path) Origin {
|
||||
func historyApplicationOrigin(application historyApplication, operationPath Path, lookup *documentLookup) Origin {
|
||||
if application.origin.Layer != "" {
|
||||
return cloneOrigin(application.origin)
|
||||
}
|
||||
@@ -100,7 +100,7 @@ func historyApplicationOrigin(application historyApplication, operationPath Path
|
||||
if !ok {
|
||||
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)}
|
||||
}
|
||||
return origin
|
||||
|
||||
@@ -84,6 +84,7 @@ type Inspection struct {
|
||||
input *document.Node
|
||||
inputOrigins map[string]Origin
|
||||
final *document.Node
|
||||
immutableLookup *documentLookup
|
||||
resolution []resolutionSnapshot
|
||||
resolutionReplay []resolutionSnapshot
|
||||
applications []historyApplication
|
||||
|
||||
@@ -98,6 +98,11 @@ func publishLoadResult[T any](schema *schemaDescriptor, definition Definition[T]
|
||||
final, applications, resolutionSnapshots := resolved.final, resolved.applications, resolved.snapshots
|
||||
finalizeMetadataPaths(&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{
|
||||
metadata: metadata,
|
||||
sources: cloneSourceInfos(accumulator.sources),
|
||||
@@ -107,6 +112,7 @@ func publishLoadResult[T any](schema *schemaDescriptor, definition Definition[T]
|
||||
input: decoded.final,
|
||||
inputOrigins: cloneOriginIndex(decoded.origins),
|
||||
final: final,
|
||||
immutableLookup: buildDocumentLookup(retainedTrees...),
|
||||
resolution: resolutionSnapshots,
|
||||
resolutionReplay: coalescedResolutionSnapshots(resolutionSnapshots),
|
||||
applications: applications,
|
||||
|
||||
@@ -102,7 +102,11 @@ func TestDocumentLookupMatchesPathScan(t *testing.T) {
|
||||
{Key: "01", Value: &document.Node{Kind: document.String, Text: "leading zero key"}},
|
||||
{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{
|
||||
Path{},
|
||||
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)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -12,7 +12,11 @@ import (
|
||||
// Snapshot projection is the shared redaction boundary for effective YAML
|
||||
// and explanation values. It borrows frozen trees and returns a detached tree.
|
||||
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 {
|
||||
return ValueSnapshot{}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user