Separate inheritance graph analysis and application
This commit is contained in:
@@ -84,7 +84,7 @@ func expandSiblingDefinitionsWithLimits(scope *document.Node, scopePath Path, li
|
|||||||
return expandSiblingDefinitionsWithOrigins(scope, scopePath, limits, nil)
|
return expandSiblingDefinitionsWithOrigins(scope, scopePath, limits, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func expandSiblingDefinitionsWithOrigins(scope *document.Node, scopePath Path, limits projectionLimits, inputOrigins map[string]Origin, budget ...*retainedNodeCounter) (*inheritanceScope, []Diagnostic) {
|
func expandSiblingDefinitionsWithFrames(scope *document.Node, scopePath Path, limits projectionLimits, inputOrigins map[string]Origin, budget ...*retainedNodeCounter) (*inheritanceScope, []Diagnostic) {
|
||||||
if scope == nil || scope.Kind != document.Mapping {
|
if scope == nil || scope.Kind != document.Mapping {
|
||||||
return nil, []Diagnostic{{
|
return nil, []Diagnostic{{
|
||||||
Code: CodeInheritanceInvalid, Severity: SeverityError, Path: scopePath,
|
Code: CodeInheritanceInvalid, Severity: SeverityError, Path: scopePath,
|
||||||
|
|||||||
341
internal/engine/inheritance_schedule.go
Normal file
341
internal/engine/inheritance_schedule.go
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||||
|
)
|
||||||
|
|
||||||
|
type inheritanceTemplate struct {
|
||||||
|
name string
|
||||||
|
entry document.Entry
|
||||||
|
path Path
|
||||||
|
parent int
|
||||||
|
hasParent bool
|
||||||
|
hasDirective bool
|
||||||
|
cancelled bool
|
||||||
|
directive document.Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
type inheritanceGraph struct {
|
||||||
|
scope *document.Node
|
||||||
|
scopePath Path
|
||||||
|
nodes []inheritanceTemplate
|
||||||
|
edges []dependencyEdge
|
||||||
|
byName map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
// depthFirstTemplateOrder preserves the currently wired parent-first order
|
||||||
|
// while the loading path adopts the graph/application boundary.
|
||||||
|
func (graph *inheritanceGraph) depthFirstTemplateOrder() ([]int, []Diagnostic) {
|
||||||
|
const (
|
||||||
|
unvisited = uint8(0)
|
||||||
|
active = uint8(1)
|
||||||
|
finished = uint8(2)
|
||||||
|
)
|
||||||
|
state := make([]uint8, len(graph.nodes))
|
||||||
|
order := make([]int, 0, len(graph.nodes))
|
||||||
|
for start := range graph.nodes {
|
||||||
|
if state[start] != unvisited {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
stack := []int{start}
|
||||||
|
for len(stack) != 0 {
|
||||||
|
index := stack[len(stack)-1]
|
||||||
|
if state[index] == unvisited {
|
||||||
|
state[index] = active
|
||||||
|
node := graph.nodes[index]
|
||||||
|
if node.hasParent {
|
||||||
|
parent := node.parent
|
||||||
|
switch state[parent] {
|
||||||
|
case active:
|
||||||
|
referencePath := node.path.Key(inheritanceKey)
|
||||||
|
location := publicLocation(node.directive.Value.Location)
|
||||||
|
return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceCycle, referencePath, location,
|
||||||
|
[]Location{publicLocation(graph.nodes[parent].entry.Value.Location)}, "sibling inheritance contains a cycle")}
|
||||||
|
case unvisited:
|
||||||
|
stack = append(stack, parent)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state[index] = finished
|
||||||
|
order = append(order, index)
|
||||||
|
stack = stack[:len(stack)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return order, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (graph *inheritanceGraph) depthFirstControls() []inheritanceControl {
|
||||||
|
visited := make([]bool, len(graph.nodes))
|
||||||
|
controls := make([]inheritanceControl, 0, len(graph.nodes))
|
||||||
|
type frame struct {
|
||||||
|
index int
|
||||||
|
entered bool
|
||||||
|
}
|
||||||
|
for start := range graph.nodes {
|
||||||
|
if visited[start] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
stack := []frame{{index: start}}
|
||||||
|
for len(stack) != 0 {
|
||||||
|
current := &stack[len(stack)-1]
|
||||||
|
node := graph.nodes[current.index]
|
||||||
|
if !current.entered {
|
||||||
|
current.entered = true
|
||||||
|
visited[current.index] = true
|
||||||
|
if node.hasDirective {
|
||||||
|
controls = append(controls, inheritanceControl{
|
||||||
|
path: node.path.Key(inheritanceKey),
|
||||||
|
locations: []Location{publicLocation(node.directive.Value.Location)},
|
||||||
|
cancelled: node.cancelled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if node.hasParent && !visited[node.parent] {
|
||||||
|
stack = append(stack, frame{index: node.parent})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stack = stack[:len(stack)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return controls
|
||||||
|
}
|
||||||
|
|
||||||
|
// analyzeInheritanceGraph retains only direct mapping siblings as graph nodes.
|
||||||
|
// It validates declarations separately from applying their templates, leaving
|
||||||
|
// nested scopes for the existing destination-local walk.
|
||||||
|
func analyzeInheritanceGraph(scope *document.Node, scopePath Path) (*inheritanceGraph, []Diagnostic) {
|
||||||
|
if scope == nil || scope.Kind != document.Mapping {
|
||||||
|
return nil, []Diagnostic{{
|
||||||
|
Code: CodeInheritanceInvalid, Severity: SeverityError, Path: scopePath,
|
||||||
|
Message: "inheritance scope must be a mapping",
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
graph := &inheritanceGraph{
|
||||||
|
scope: scope, scopePath: scopePath,
|
||||||
|
byName: make(map[string]int, len(scope.Mapping)),
|
||||||
|
}
|
||||||
|
entries := make(map[string]document.Entry, len(scope.Mapping))
|
||||||
|
for _, entry := range scope.Mapping {
|
||||||
|
entries[entry.Key] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
names := make([]string, 0, len(scope.Mapping))
|
||||||
|
for name, entry := range entries {
|
||||||
|
if entry.Value.Kind == document.Mapping {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.Sort(names)
|
||||||
|
for _, name := range names {
|
||||||
|
index := len(graph.nodes)
|
||||||
|
graph.byName[name] = index
|
||||||
|
graph.nodes = append(graph.nodes, inheritanceTemplate{
|
||||||
|
name: name, entry: entries[name], path: scopePath.Key(name),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range names {
|
||||||
|
index := graph.byName[name]
|
||||||
|
node := &graph.nodes[index]
|
||||||
|
reference, found := inheritanceDirective(node.entry.Value)
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
node.hasDirective = true
|
||||||
|
node.directive = reference
|
||||||
|
referencePath := node.path.Key(inheritanceKey)
|
||||||
|
location := publicLocation(reference.Value.Location)
|
||||||
|
switch reference.Value.Kind {
|
||||||
|
case document.Null:
|
||||||
|
node.cancelled = true
|
||||||
|
case document.String:
|
||||||
|
parent, exists := entries[reference.Value.Text]
|
||||||
|
if !exists {
|
||||||
|
return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceMissing, referencePath, location, nil,
|
||||||
|
"referenced sibling does not exist")}
|
||||||
|
}
|
||||||
|
if parent.Value.Kind != document.Mapping {
|
||||||
|
related := []Location{publicLocation(parent.Value.Location)}
|
||||||
|
return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceInvalid, referencePath, location, related,
|
||||||
|
"referenced sibling is not a mapping")}
|
||||||
|
}
|
||||||
|
node.parent = graph.byName[reference.Value.Text]
|
||||||
|
node.hasParent = true
|
||||||
|
graph.edges = append(graph.edges, dependencyEdge{prerequisite: node.parent, dependent: index})
|
||||||
|
default:
|
||||||
|
return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceInvalid, referencePath, location, nil,
|
||||||
|
"inheritance reference must be a sibling name or null")}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return graph, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (graph *inheritanceGraph) schedule() ([]int, []Diagnostic) {
|
||||||
|
keys := make([]dependencyKey, len(graph.nodes))
|
||||||
|
for i, node := range graph.nodes {
|
||||||
|
keys[i] = dependencyKey{"definition", graph.scopePath.String(), node.path.String()}
|
||||||
|
}
|
||||||
|
order, cycle := scheduleDependencies(keys, graph.edges)
|
||||||
|
if len(cycle) == 0 {
|
||||||
|
return order, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parent := graph.nodes[cycle[0]]
|
||||||
|
child := graph.nodes[cycle[1]]
|
||||||
|
referencePath := child.path.Key(inheritanceKey)
|
||||||
|
location := publicLocation(child.directive.Value.Location)
|
||||||
|
return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceCycle, referencePath, location,
|
||||||
|
[]Location{publicLocation(parent.entry.Value.Location)}, "sibling inheritance contains a cycle")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandSiblingDefinitionsWithOrigins(scope *document.Node, scopePath Path, limits projectionLimits, inputOrigins map[string]Origin, budget ...*retainedNodeCounter) (*inheritanceScope, []Diagnostic) {
|
||||||
|
graph, diagnostics := analyzeInheritanceGraph(scope, scopePath)
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
return nil, diagnostics
|
||||||
|
}
|
||||||
|
order, diagnostics := graph.depthFirstTemplateOrder()
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
return nil, diagnostics
|
||||||
|
}
|
||||||
|
result, diagnostics := applyInheritanceTemplates(graph, order, limits, inputOrigins, budget...)
|
||||||
|
if len(diagnostics) == 0 {
|
||||||
|
result.controls = graph.depthFirstControls()
|
||||||
|
}
|
||||||
|
return result, diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyInheritanceTemplates builds detached templates in the supplied
|
||||||
|
// dependency-ready order while retaining the existing origins, merge history,
|
||||||
|
// and node-accounting evidence. The root keeps its authored mapping order.
|
||||||
|
func applyInheritanceTemplates(graph *inheritanceGraph, order []int, limits projectionLimits, inputOrigins map[string]Origin, budget ...*retainedNodeCounter) (*inheritanceScope, []Diagnostic) {
|
||||||
|
resolved := make([]*document.Node, len(graph.nodes))
|
||||||
|
resolvedOrigins := make([]map[string]Origin, len(graph.nodes))
|
||||||
|
result := &inheritanceScope{}
|
||||||
|
counter := newRetainedNodeCounter(historyNodeLimit)
|
||||||
|
if len(budget) != 0 {
|
||||||
|
counter = budget[0]
|
||||||
|
}
|
||||||
|
retained := 1 // scope mapping node
|
||||||
|
for _, entry := range graph.scope.Mapping {
|
||||||
|
if entry.Value.Kind == document.Mapping {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
remaining := limits.nodes - retained - 1
|
||||||
|
if limits.nodes >= 0 && remaining < 0 {
|
||||||
|
return nil, []Diagnostic{inheritanceBudgetDiagnostic(graph.scopePath.Key(entry.Key), entry.Value.Location)}
|
||||||
|
}
|
||||||
|
count, within := inheritanceLowerBoundCount(entry.Value, remaining)
|
||||||
|
if !within {
|
||||||
|
return nil, []Diagnostic{inheritanceBudgetDiagnostic(graph.scopePath.Key(entry.Key), entry.Value.Location)}
|
||||||
|
}
|
||||||
|
retained += 1 + count
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, index := range order {
|
||||||
|
node := graph.nodes[index]
|
||||||
|
payload := withoutInheritanceDirective(node.entry.Value)
|
||||||
|
operationStart, replacementStart := len(result.operations), len(result.replacements)
|
||||||
|
childOrigins := directInheritanceOrigins(payload, node.path, inputOrigins)
|
||||||
|
if node.hasDirective {
|
||||||
|
control := inheritanceControl{
|
||||||
|
path: node.path.Key(inheritanceKey),
|
||||||
|
locations: []Location{publicLocation(node.directive.Value.Location)},
|
||||||
|
cancelled: node.cancelled,
|
||||||
|
}
|
||||||
|
result.controls = append(result.controls, control)
|
||||||
|
controlOrigins := directInheritanceOrigins(node.directive.Value, control.path, inputOrigins)
|
||||||
|
result.operations = append(result.operations, inheritanceOperation{
|
||||||
|
control: &control, controlOrigin: controlOrigins[control.path.String()],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if limits.nodes >= 0 {
|
||||||
|
remaining := limits.nodes - retained - 1 // the sibling key
|
||||||
|
within := inheritanceLowerBoundWithin(payload, remaining)
|
||||||
|
if node.hasParent {
|
||||||
|
within = inheritanceOverlayLowerBoundWithin(resolved[node.parent], payload, remaining)
|
||||||
|
}
|
||||||
|
if !within {
|
||||||
|
return nil, []Diagnostic{inheritanceBudgetDiagnostic(node.path, node.entry.Value.Location)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.hasParent {
|
||||||
|
parent := resolved[node.parent]
|
||||||
|
merged := document.Clone(parent)
|
||||||
|
transfer := inheritanceTransfer{
|
||||||
|
from: graph.nodes[node.parent].path, to: node.path,
|
||||||
|
reference: node.path.Key(inheritanceKey),
|
||||||
|
locations: []Location{publicLocation(node.directive.Value.Location)},
|
||||||
|
}
|
||||||
|
mergedOrigins := transferredInheritanceOrigins(resolvedOrigins[node.parent], transfer.from, node.path, transfer)
|
||||||
|
mergeInheritanceOrigin(mergedOrigins, node.path, childOrigins[node.path.String()])
|
||||||
|
mergeRecursiveMappings(merged, payload, node.path, new(mappingIndex), recursiveMergeHooks{
|
||||||
|
added: func(childPath Path, incoming *document.Node) {
|
||||||
|
copyInheritanceSubtree(mergedOrigins, childOrigins, incoming, childPath)
|
||||||
|
},
|
||||||
|
combined: func(childPath Path, _ *document.Node) {
|
||||||
|
mergeInheritanceOrigin(mergedOrigins, childPath, childOrigins[childPath.String()])
|
||||||
|
},
|
||||||
|
replaced: func(childPath Path, existing, incoming *document.Node) {
|
||||||
|
previous := displacedOrigins(existing, mergedOrigins, childPath)
|
||||||
|
result.replacements = append(result.replacements, inheritanceReplacement{
|
||||||
|
path: childPath.clone(), before: existing, after: incoming,
|
||||||
|
previous: previous, effective: cloneOrigin(childOrigins[childPath.String()]),
|
||||||
|
})
|
||||||
|
deleteOriginSubtree(mergedOrigins, childPath, existing)
|
||||||
|
copyInheritanceSubtree(mergedOrigins, childOrigins, incoming, childPath)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
merged.Location = node.entry.Value.Location
|
||||||
|
resolved[index], resolvedOrigins[index] = merged, mergedOrigins
|
||||||
|
result.transfers = append(result.transfers, transfer)
|
||||||
|
result.operations = append(result.operations, inheritanceOperation{
|
||||||
|
transfer: &transfer, parent: parent, effective: merged,
|
||||||
|
parentOrigins: resolvedOrigins[node.parent], effectiveOrigins: mergedOrigins,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
resolved[index] = document.Clone(payload)
|
||||||
|
resolvedOrigins[index] = childOrigins
|
||||||
|
}
|
||||||
|
|
||||||
|
count, _ := inheritanceLowerBoundCount(resolved[index], limits.nodes)
|
||||||
|
retained += 1 + count
|
||||||
|
for _, operation := range result.operations[operationStart:] {
|
||||||
|
if !counter.add(operation.parent) || !counter.add(operation.effective) {
|
||||||
|
return nil, []Diagnostic{inheritanceHistoryBudgetDiagnostic(node.path, node.entry.Value.Location)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, replacement := range result.replacements[replacementStart:] {
|
||||||
|
if !counter.add(replacement.before) || !counter.add(replacement.after) {
|
||||||
|
return nil, []Diagnostic{inheritanceHistoryBudgetDiagnostic(node.path, node.entry.Value.Location)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root := *graph.scope
|
||||||
|
root.Mapping = make([]document.Entry, len(graph.scope.Mapping))
|
||||||
|
result.origins = directInheritanceOrigins(&document.Node{Kind: document.Mapping, Location: graph.scope.Location}, graph.scopePath, inputOrigins)
|
||||||
|
for i, entry := range graph.scope.Mapping {
|
||||||
|
root.Mapping[i] = entry
|
||||||
|
if index, found := graph.byName[entry.Key]; found {
|
||||||
|
root.Mapping[i].Value = resolved[index]
|
||||||
|
for pointer, origin := range resolvedOrigins[index] {
|
||||||
|
result.origins[pointer] = origin
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
root.Mapping[i].Value = document.Clone(entry.Value)
|
||||||
|
for pointer, origin := range directInheritanceOrigins(entry.Value, graph.scopePath.Key(entry.Key), inputOrigins) {
|
||||||
|
result.origins[pointer] = origin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.root = &root
|
||||||
|
if !inheritanceLowerBoundWithin(result.root, limits.nodes) {
|
||||||
|
return nil, []Diagnostic{inheritanceBudgetDiagnostic(graph.scopePath, graph.scope.Location)}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
135
internal/engine/inheritance_schedule_test.go
Normal file
135
internal/engine/inheritance_schedule_test.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInheritanceGraphSchedulesDirectRelationshipsAndAppliesTemplates(t *testing.T) {
|
||||||
|
root := parseCompositionDocument(t, "schedule", `
|
||||||
|
child:
|
||||||
|
extends: base
|
||||||
|
own: child-value
|
||||||
|
base:
|
||||||
|
inherited: base-value
|
||||||
|
cleared:
|
||||||
|
extends: null
|
||||||
|
own: cleared-value
|
||||||
|
scalar: kept
|
||||||
|
`)
|
||||||
|
original := document.Clone(root)
|
||||||
|
graph, diagnostics := analyzeInheritanceGraph(root, Path{})
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatalf("analyze diagnostics = %#v", diagnostics)
|
||||||
|
}
|
||||||
|
if len(graph.edges) != 1 {
|
||||||
|
t.Fatalf("edges = %#v, want one direct parent edge", graph.edges)
|
||||||
|
}
|
||||||
|
order, diagnostics := graph.schedule()
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatalf("schedule diagnostics = %#v", diagnostics)
|
||||||
|
}
|
||||||
|
orderNames := make([]string, len(order))
|
||||||
|
for i, index := range order {
|
||||||
|
orderNames[i] = graph.nodes[index].name
|
||||||
|
}
|
||||||
|
if want := []string{"base", "child", "cleared"}; !reflect.DeepEqual(orderNames, want) {
|
||||||
|
t.Fatalf("mapping schedule = %v, want %v", orderNames, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, diagnostics := applyInheritanceTemplates(graph, order, defaultProjectionLimits(), nil)
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatalf("apply diagnostics = %#v", diagnostics)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(root, original) {
|
||||||
|
t.Fatal("applying templates mutated the input mapping")
|
||||||
|
}
|
||||||
|
if len(result.controls) != 2 || result.controls[0].path.String() != "/child/extends" || result.controls[0].cancelled ||
|
||||||
|
result.controls[1].path.String() != "/cleared/extends" || !result.controls[1].cancelled {
|
||||||
|
t.Fatalf("controls = %#v, want scheduled ordinary and null relationships", result.controls)
|
||||||
|
}
|
||||||
|
if len(result.transfers) != 1 || result.transfers[0].from.String() != "/base" || result.transfers[0].to.String() != "/child" {
|
||||||
|
t.Fatalf("transfers = %#v, want the direct parent transfer", result.transfers)
|
||||||
|
}
|
||||||
|
if got, want := result.root.Mapping[0].Key, "child"; got != want {
|
||||||
|
t.Fatalf("root order begins with %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
child := result.root.Mapping[0].Value
|
||||||
|
inherited, found := documentAtPath(child, Path{}.Key("inherited"))
|
||||||
|
if !found || inherited.Text != "base-value" {
|
||||||
|
t.Fatalf("inherited value = %#v, found %t", inherited, found)
|
||||||
|
}
|
||||||
|
childValue, found := documentAtPath(child, Path{}.Key("own"))
|
||||||
|
if !found || childValue.Text != "child-value" {
|
||||||
|
t.Fatalf("child value = %#v, found %t", childValue, found)
|
||||||
|
}
|
||||||
|
cleared := result.root.Mapping[2].Value
|
||||||
|
clearedValue, found := documentAtPath(cleared, Path{}.Key("own"))
|
||||||
|
if !found || clearedValue.Text != "cleared-value" {
|
||||||
|
t.Fatalf("null relationship value = %#v, found %t", clearedValue, found)
|
||||||
|
}
|
||||||
|
if _, found := inheritanceDirective(cleared); found {
|
||||||
|
t.Fatal("null relationship directive remained in the template")
|
||||||
|
}
|
||||||
|
|
||||||
|
child.Mapping[0].Value.Text = "changed-result"
|
||||||
|
inherited, found = documentAtPath(root.Mapping[1].Value, Path{}.Key("inherited"))
|
||||||
|
if !found || inherited.Text != "base-value" {
|
||||||
|
t.Fatalf("mutating result changed input: inherited value = %#v, found %t", inherited, found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInheritanceGraphReportsCycleAtAuthoredRelationship(t *testing.T) {
|
||||||
|
root := parseCompositionDocument(t, "cycle-source", `alpha:
|
||||||
|
extends: beta
|
||||||
|
beta:
|
||||||
|
extends: alpha
|
||||||
|
`)
|
||||||
|
graph, diagnostics := analyzeInheritanceGraph(root, Path{})
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatalf("analyze diagnostics = %#v", diagnostics)
|
||||||
|
}
|
||||||
|
_, diagnostics = graph.schedule()
|
||||||
|
if len(diagnostics) != 1 || diagnostics[0].Code != CodeInheritanceCycle {
|
||||||
|
t.Fatalf("cycle diagnostics = %#v", diagnostics)
|
||||||
|
}
|
||||||
|
diagnostic := diagnostics[0]
|
||||||
|
if diagnostic.Primary == nil || diagnostic.Primary.SourceID != "cycle-source" {
|
||||||
|
t.Fatalf("cycle primary = %#v, want authored source", diagnostic.Primary)
|
||||||
|
}
|
||||||
|
if diagnostic.Primary.Line != 4 {
|
||||||
|
t.Fatalf("cycle primary line = %d, want line 4", diagnostic.Primary.Line)
|
||||||
|
}
|
||||||
|
if len(diagnostic.Related) != 1 || diagnostic.Related[0].SourceID != "cycle-source" {
|
||||||
|
t.Fatalf("cycle related sites = %#v, want the referenced mapping", diagnostic.Related)
|
||||||
|
}
|
||||||
|
if diagnostic.Message != "sibling inheritance contains a cycle" {
|
||||||
|
t.Fatalf("cycle message exposes unexpected details: %q", diagnostic.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInheritanceGraphValidatesRelationshipKinds(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
text string
|
||||||
|
code Code
|
||||||
|
}{
|
||||||
|
{name: "missing sibling", text: "child:\n extends: absent\n", code: CodeInheritanceMissing},
|
||||||
|
{name: "non-mapping sibling", text: "base: scalar\nchild:\n extends: base\n", code: CodeInheritanceInvalid},
|
||||||
|
{name: "invalid relationship", text: "child:\n extends: 3\n", code: CodeInheritanceInvalid},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
root := parseCompositionDocument(t, "invalid", test.text)
|
||||||
|
_, diagnostics := analyzeInheritanceGraph(root, Path{})
|
||||||
|
if len(diagnostics) != 1 || diagnostics[0].Code != test.code {
|
||||||
|
t.Fatalf("diagnostics = %#v, want code %q", diagnostics, test.code)
|
||||||
|
}
|
||||||
|
if diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != "invalid" {
|
||||||
|
t.Fatalf("primary site = %#v, want authored declaration", diagnostics[0].Primary)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user