Retain import inclusion graph during acquisition
This commit is contained in:
@@ -8,6 +8,7 @@ import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
type composedDocument struct {
|
||||
root *document.Node
|
||||
acquisition []acquisitionScope
|
||||
importGraph importInclusionGraph
|
||||
claims []compositionClaim
|
||||
sequenceRanges []sequenceRange
|
||||
origins map[string]Origin
|
||||
@@ -51,6 +52,7 @@ func cloneComposedDocument(base *composedDocument) *composedDocument {
|
||||
return &composedDocument{
|
||||
root: document.Clone(base.root), claims: cloneCompositionClaims(base.claims),
|
||||
acquisition: cloneAcquisitionScopes(base.acquisition),
|
||||
importGraph: base.importGraph.clone(),
|
||||
sequenceRanges: cloneSequenceRanges(base.sequenceRanges),
|
||||
origins: cloneOriginIndex(base.origins), overlays: cloneOverlayRecords(base.overlays),
|
||||
applications: cloneHistoryApplications(base.applications),
|
||||
|
||||
190
internal/engine/import_graph.go
Normal file
190
internal/engine/import_graph.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type importNodeKey struct {
|
||||
namespaceKind string
|
||||
namespaceAnchor string
|
||||
resourceKind string
|
||||
resourcePath string
|
||||
}
|
||||
|
||||
func (key importNodeKey) dependencyKey() dependencyKey {
|
||||
return dependencyKey{key.namespaceKind, key.namespaceAnchor, key.resourceKind, key.resourcePath}
|
||||
}
|
||||
|
||||
type importContributionRole string
|
||||
|
||||
const (
|
||||
importRoleMain importContributionRole = "main"
|
||||
importRoleMapping importContributionRole = "mapping-import"
|
||||
importRoleItem importContributionRole = "item-import"
|
||||
)
|
||||
|
||||
type importGraphNode struct {
|
||||
key importNodeKey
|
||||
canonical string
|
||||
sourceID string
|
||||
role importContributionRole
|
||||
}
|
||||
|
||||
type importInclusionEdge struct {
|
||||
importer importNodeKey
|
||||
imported importNodeKey
|
||||
location Location
|
||||
}
|
||||
|
||||
// importInclusionGraph retains source inclusion independently of mapping-scope
|
||||
// ownership. Scheduling edges point from each imported prerequisite to its
|
||||
// importer, matching the shared dependency scheduler's convention.
|
||||
type importInclusionGraph struct {
|
||||
nodes []importGraphNode
|
||||
edges []importInclusionEdge
|
||||
index map[importNodeKey]int
|
||||
}
|
||||
|
||||
func newImportInclusionGraph() importInclusionGraph {
|
||||
return importInclusionGraph{index: make(map[importNodeKey]int)}
|
||||
}
|
||||
|
||||
func (graph *importInclusionGraph) addNode(node importGraphNode) int {
|
||||
if graph.index == nil {
|
||||
graph.index = make(map[importNodeKey]int)
|
||||
}
|
||||
if index, exists := graph.index[node.key]; exists {
|
||||
return index
|
||||
}
|
||||
index := len(graph.nodes)
|
||||
graph.index[node.key] = index
|
||||
graph.nodes = append(graph.nodes, node)
|
||||
return index
|
||||
}
|
||||
|
||||
func (graph *importInclusionGraph) addEdge(importer, imported importNodeKey, location Location) int {
|
||||
index := len(graph.edges)
|
||||
graph.edges = append(graph.edges, importInclusionEdge{
|
||||
importer: importer, imported: imported, location: location,
|
||||
})
|
||||
return index
|
||||
}
|
||||
|
||||
func (graph importInclusionGraph) clone() importInclusionGraph {
|
||||
cloned := importInclusionGraph{
|
||||
nodes: append([]importGraphNode(nil), graph.nodes...),
|
||||
edges: make([]importInclusionEdge, len(graph.edges)),
|
||||
index: make(map[importNodeKey]int, len(graph.index)),
|
||||
}
|
||||
for index, edge := range graph.edges {
|
||||
cloned.edges[index] = importInclusionEdge{
|
||||
importer: edge.importer, imported: edge.imported, location: edge.location,
|
||||
}
|
||||
}
|
||||
for key, index := range graph.index {
|
||||
cloned.index[key] = index
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func (graph importInclusionGraph) scheduleCycle(closingEdge int, location Location) (Diagnostic, bool) {
|
||||
keys := make([]dependencyKey, len(graph.nodes))
|
||||
for index, node := range graph.nodes {
|
||||
keys[index] = node.key.dependencyKey()
|
||||
}
|
||||
edges := make([]dependencyEdge, 0, len(graph.edges))
|
||||
for _, edge := range graph.edges {
|
||||
importer, importerOK := graph.index[edge.importer]
|
||||
imported, importedOK := graph.index[edge.imported]
|
||||
if !importerOK || !importedOK {
|
||||
continue
|
||||
}
|
||||
edges = append(edges, dependencyEdge{prerequisite: imported, dependent: importer})
|
||||
}
|
||||
_, cycle := scheduleDependencies(keys, edges)
|
||||
if len(cycle) < 2 {
|
||||
return Diagnostic{}, false
|
||||
}
|
||||
|
||||
// Scheduler edges reverse inclusion edges. Reverse its closed cycle to
|
||||
// render the authored import route while keeping the canonical start node.
|
||||
route := make([]int, 0, len(cycle))
|
||||
route = append(route, cycle[0])
|
||||
for index := len(cycle) - 2; index > 0; index-- {
|
||||
route = append(route, cycle[index])
|
||||
}
|
||||
route = append(route, cycle[0])
|
||||
paths := make([]string, len(route))
|
||||
for index, node := range route {
|
||||
paths[index] = graph.nodes[node].canonical
|
||||
}
|
||||
|
||||
related := make([]Location, 0, len(route)-2)
|
||||
for index := 0; index+1 < len(route); index++ {
|
||||
importerKey := graph.nodes[route[index]].key
|
||||
importedKey := graph.nodes[route[index+1]].key
|
||||
edgeIndex := -1
|
||||
for candidate, edge := range graph.edges {
|
||||
if edge.importer == importerKey && edge.imported == importedKey {
|
||||
edgeIndex = candidate
|
||||
if candidate == closingEdge {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if edgeIndex == closingEdge {
|
||||
continue
|
||||
}
|
||||
if edgeIndex >= 0 {
|
||||
related = append(related, graph.edges[edgeIndex].location)
|
||||
}
|
||||
}
|
||||
return Diagnostic{
|
||||
Code: CodeImportCycle, Severity: SeverityError,
|
||||
Message: "import cycle detected: " + strings.Join(paths, " -> "),
|
||||
Primary: cloneLocation(&location), Related: related,
|
||||
}, true
|
||||
}
|
||||
|
||||
func importKeyFor(access *filesystemAccess, canonical string, synthetic bool) importNodeKey {
|
||||
kind := "host"
|
||||
resourceKind := "file"
|
||||
resourcePath := filepath.ToSlash(canonical)
|
||||
if access.native != nil {
|
||||
kind = "native"
|
||||
}
|
||||
if synthetic {
|
||||
resourceKind = "synthetic"
|
||||
resourcePath = ""
|
||||
}
|
||||
return importNodeKey{
|
||||
namespaceKind: kind,
|
||||
// This traversal owns the main input position; grouped inputs receive
|
||||
// their explicit positions when their acquisition graphs are combined.
|
||||
namespaceAnchor: fmt.Sprintf("%020d/%020d", 0, 0),
|
||||
resourceKind: resourceKind, resourcePath: resourcePath,
|
||||
}
|
||||
}
|
||||
|
||||
func importKeyForMainSource(source *SourceInfo) importNodeKey {
|
||||
if source == nil {
|
||||
return importNodeKey{}
|
||||
}
|
||||
namespaceKind := "host"
|
||||
if source.Kind == SourceKindFilesystem {
|
||||
namespaceKind = "native"
|
||||
}
|
||||
synthetic := source.Kind != SourceKindFile && source.Kind != SourceKindFilesystem
|
||||
resourceKind := "file"
|
||||
resourcePath := filepath.ToSlash(source.Path)
|
||||
if synthetic {
|
||||
resourceKind = "synthetic"
|
||||
resourcePath = ""
|
||||
}
|
||||
return importNodeKey{
|
||||
namespaceKind: namespaceKind, namespaceAnchor: fmt.Sprintf("%020d/%020d", 0, 0),
|
||||
resourceKind: resourceKind, resourcePath: resourcePath,
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ func defaultImportLimits() importLimits {
|
||||
type importRoute struct {
|
||||
canonical string
|
||||
locations []Location
|
||||
key importNodeKey
|
||||
}
|
||||
|
||||
type importAccounting struct {
|
||||
@@ -59,6 +60,7 @@ type importTraversal struct {
|
||||
active map[resourceIdentity]int
|
||||
visited map[resourceIdentity]importRoute
|
||||
stack []importRoute
|
||||
importGraph importInclusionGraph
|
||||
}
|
||||
|
||||
func loadImportedDocuments(source Source, context *FilesystemContext, main parsedSource, rule *VersionRule, limits importLimits) (*composedDocument, []SourceInfo, []Diagnostic) {
|
||||
@@ -139,11 +141,12 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
|
||||
if mainPath != "" {
|
||||
mainInfo.Path = mainPath
|
||||
}
|
||||
mainKey := access.identity(mainPath)
|
||||
mainResource := access.identity(mainPath)
|
||||
mainNodeKey := importKeyFor(access, mainPath, mainPath == "")
|
||||
if mainPath == "" {
|
||||
mainKey = access.syntheticIdentity()
|
||||
mainResource = access.syntheticIdentity()
|
||||
}
|
||||
if previous, repeated := visited[mainKey]; repeated {
|
||||
if previous, repeated := visited[mainResource]; repeated {
|
||||
_ = access.close()
|
||||
return nil, sourceInfos(main.source), []Diagnostic{importRepeatedDiagnostic(Location{SourceID: main.source.ID}, previous, importRoute{})}
|
||||
}
|
||||
@@ -152,9 +155,10 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
|
||||
accounting.nodes += countInputNodes(main.document)
|
||||
state := importTraversal{
|
||||
access: access, accounting: accounting, limits: limits, rule: rule, versionState: versionState,
|
||||
scope: acquisitionScope{destination: destination.clone()},
|
||||
scope: acquisitionScope{owner: mainNodeKey, destination: destination.clone()},
|
||||
sources: []SourceInfo{mainInfo},
|
||||
active: make(map[resourceIdentity]int), visited: visited,
|
||||
importGraph: newImportInclusionGraph(),
|
||||
}
|
||||
if mainIsItem {
|
||||
state.itemScope = true
|
||||
@@ -176,8 +180,10 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
|
||||
mainIdentity = "<selected-source>"
|
||||
}
|
||||
mainRoute := importRoute{canonical: mainIdentity}
|
||||
state.active[mainKey] = 0
|
||||
state.visited[mainKey] = mainRoute
|
||||
state.importGraph.addNode(importGraphNode{key: mainNodeKey, canonical: mainIdentity, sourceID: mainInfo.ID, role: importRoleMain})
|
||||
mainRoute.key = mainNodeKey
|
||||
state.active[mainResource] = 0
|
||||
state.visited[mainResource] = mainRoute
|
||||
state.stack = append(state.stack, mainRoute)
|
||||
var body *document.Node
|
||||
var declarations []importDeclaration
|
||||
@@ -191,7 +197,8 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
|
||||
if len(diagnostics) == 0 {
|
||||
if mainIsItem {
|
||||
state.scope.contributions = append(state.scope.contributions, acquiredContribution{
|
||||
source: cloneSourceInfo(mainInfo), payload: body, document: body, processing: processing,
|
||||
source: cloneSourceInfo(mainInfo), identity: mainNodeKey, role: importRoleMain,
|
||||
scopeOwner: mainNodeKey, payload: body, document: body, processing: processing,
|
||||
})
|
||||
diagnostics = state.walkDeclarations(basePath, declarations, 0, Path{})
|
||||
} else {
|
||||
@@ -205,12 +212,16 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
|
||||
accounting.nodes += placementNodes
|
||||
state.scope.contributions = append(state.scope.contributions, acquiredContribution{
|
||||
source: cloneSourceInfo(mainInfo), payload: body, document: placed,
|
||||
identity: mainNodeKey, role: importRoleMain, scopeOwner: mainNodeKey,
|
||||
destination: destination.clone(), processing: processing,
|
||||
})
|
||||
diagnostics = state.walkDeclarations(basePath, declarations, 0, destination)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(diagnostics) == 0 {
|
||||
diagnostics = state.composePendingItems(&state.scope)
|
||||
}
|
||||
|
||||
closeErr := access.close()
|
||||
if len(diagnostics) != 0 {
|
||||
@@ -267,13 +278,16 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
|
||||
if composed != nil {
|
||||
synthetic := acquisitionScope{contributions: []acquiredContribution{{
|
||||
source: cloneSourceInfo(mainInfo), payload: completed.root, document: placed,
|
||||
identity: mainNodeKey, role: importRoleMain, scopeOwner: mainNodeKey,
|
||||
destination: destination.clone(), processing: processing, nested: completed,
|
||||
}}}
|
||||
synthetic.owner = mainNodeKey
|
||||
projectNestedEvidence(composed, synthetic)
|
||||
projectAcquiredDestinations(composed, synthetic, state.sources)
|
||||
composed.acquisition = []acquisitionScope{cloneAcquisitionScope(synthetic)}
|
||||
composed.inputBytes = accounting.bytes
|
||||
composed.inputNodes = accounting.nodes
|
||||
composed.importGraph = state.importGraph.clone()
|
||||
}
|
||||
return composed, cloneSourceInfos(state.sources), failures
|
||||
}
|
||||
@@ -293,6 +307,7 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
|
||||
composed.acquisition = []acquisitionScope{cloneAcquisitionScope(state.scope)}
|
||||
composed.inputBytes = accounting.bytes
|
||||
composed.inputNodes = accounting.nodes
|
||||
composed.importGraph = state.importGraph.clone()
|
||||
}
|
||||
return composed, cloneSourceInfos(state.sources), diagnostics
|
||||
}
|
||||
@@ -386,11 +401,18 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
|
||||
|
||||
currentRoute := s.currentRoute(location)
|
||||
identity := s.access.identity(canonical)
|
||||
nodeKey := importKeyFor(s.access, canonical, false)
|
||||
importerKey := s.currentImportNode()
|
||||
if activeIndex, ok := s.active[identity]; ok {
|
||||
closingEdge := s.importGraph.addEdge(importerKey, nodeKey, location)
|
||||
if diagnostic, ok := s.importGraph.scheduleCycle(closingEdge, location); ok {
|
||||
return []Diagnostic{diagnostic}
|
||||
}
|
||||
cycle := append([]importRoute(nil), s.stack[activeIndex:]...)
|
||||
return []Diagnostic{importCycleDiagnostic(canonical, location, cycle)}
|
||||
}
|
||||
if previous, ok := s.visited[identity]; ok {
|
||||
s.importGraph.addEdge(importerKey, nodeKey, location)
|
||||
return []Diagnostic{importRepeatedDiagnostic(location, previous, currentRoute)}
|
||||
}
|
||||
if s.accounting.sources+1 > s.limits.sources {
|
||||
@@ -467,12 +489,18 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
|
||||
if declaration.placement == placementModeSequenceItem {
|
||||
mode = placementModeSequenceItem
|
||||
}
|
||||
role := importRoleMapping
|
||||
if mode == placementModeSequenceItem {
|
||||
route := importRoute{canonical: canonical, locations: currentRoute.locations}
|
||||
role = importRoleItem
|
||||
}
|
||||
s.importGraph.addNode(importGraphNode{key: nodeKey, canonical: canonical, sourceID: sourceID, role: role})
|
||||
s.importGraph.addEdge(importerKey, nodeKey, location)
|
||||
if mode == placementModeSequenceItem {
|
||||
route := importRoute{canonical: canonical, locations: currentRoute.locations, key: nodeKey}
|
||||
s.active[identity] = len(s.stack)
|
||||
s.visited[identity] = route
|
||||
s.stack = append(s.stack, route)
|
||||
diagnostics := s.visitItemSource(canonical, declaration, destination, relativeDestination, depth, info, body, declarations, processing)
|
||||
diagnostics := s.visitItemSource(canonical, nodeKey, declaration, destination, relativeDestination, depth, info, body, declarations, processing)
|
||||
delete(s.active, identity)
|
||||
s.stack = s.stack[:len(s.stack)-1]
|
||||
return diagnostics
|
||||
@@ -486,9 +514,10 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
|
||||
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &location)}
|
||||
}
|
||||
s.accounting.nodes += placementNodes
|
||||
route := importRoute{canonical: canonical, locations: currentRoute.locations}
|
||||
route := importRoute{canonical: canonical, locations: currentRoute.locations, key: nodeKey}
|
||||
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
|
||||
source: cloneSourceInfo(info), payload: body, document: placed,
|
||||
identity: nodeKey, role: role, scopeOwner: s.scope.owner,
|
||||
destination: placementDestination.clone(), relativeDestination: relativeDestination.clone(), processing: processing,
|
||||
})
|
||||
s.active[identity] = len(s.stack)
|
||||
@@ -500,6 +529,17 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
func (s *importTraversal) currentImportNode() importNodeKey {
|
||||
if len(s.stack) != 0 {
|
||||
return s.stack[len(s.stack)-1].key
|
||||
}
|
||||
key := s.scope.owner
|
||||
if _, exists := s.importGraph.index[key]; !exists {
|
||||
s.importGraph.addNode(importGraphNode{key: key, canonical: "<selected-source>", role: importRoleMain})
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func (s *importTraversal) sourceDestination(destination Path) Path {
|
||||
if !s.itemScope {
|
||||
return destination.clone()
|
||||
@@ -509,10 +549,10 @@ func (s *importTraversal) sourceDestination(destination Path) Path {
|
||||
return s.itemFallback.clone()
|
||||
}
|
||||
|
||||
func (s *importTraversal) visitItemSource(canonical string, declaration importDeclaration, destination, relativeDestination Path, depth int, info SourceInfo, body *document.Node, declarations []importDeclaration, processing *sourceVersionProcessingResult) []Diagnostic {
|
||||
func (s *importTraversal) visitItemSource(canonical string, identity importNodeKey, declaration importDeclaration, destination, relativeDestination Path, depth int, info SourceInfo, body *document.Node, declarations []importDeclaration, processing *sourceVersionProcessingResult) []Diagnostic {
|
||||
parentScope := s.scope
|
||||
parentItemScope, parentPrefix, parentFallback := s.itemScope, s.itemPrefix, s.itemFallback
|
||||
s.scope = acquisitionScope{}
|
||||
s.scope = acquisitionScope{owner: identity}
|
||||
s.itemScope = true
|
||||
if parentItemScope {
|
||||
s.itemPrefix = appendDestination(parentPrefix, destination)
|
||||
@@ -522,6 +562,7 @@ func (s *importTraversal) visitItemSource(canonical string, declaration importDe
|
||||
}
|
||||
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
|
||||
source: cloneSourceInfo(info), payload: body, document: body,
|
||||
identity: identity, role: importRoleItem, scopeOwner: identity,
|
||||
processing: processing,
|
||||
})
|
||||
diagnostics := s.walkDeclarations(s.access.dir(canonical), declarations, depth, Path{})
|
||||
@@ -530,35 +571,53 @@ func (s *importTraversal) visitItemSource(canonical string, declaration importDe
|
||||
if len(diagnostics) != 0 {
|
||||
return projectItemDiagnosticPaths(diagnostics, parentScope, destination)
|
||||
}
|
||||
documents := make([]*document.Node, 0, len(itemScope.contributions))
|
||||
for _, contribution := range itemScope.contributions {
|
||||
documents = append(documents, contribution.document)
|
||||
}
|
||||
completed, diagnostics := composeDocuments(documents...)
|
||||
if len(diagnostics) != 0 {
|
||||
return projectItemDiagnosticPaths(diagnostics, parentScope, destination)
|
||||
}
|
||||
itemScope.destination = Path{}
|
||||
completed.acquisition = []acquisitionScope{cloneAcquisitionScope(itemScope)}
|
||||
projectNestedEvidence(completed, itemScope)
|
||||
pl := destination
|
||||
_, placed, err := placeDocumentWithMode(completed.root, pl, placementModeSequenceItem, body.Location, maxDocumentDepth, maxDocumentNodes)
|
||||
if err != nil {
|
||||
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source depth or node limit after destination placement", &declaration.location)}
|
||||
}
|
||||
placementNodes := destinationWrapperNodeCountForMode(pl, placementModeSequenceItem)
|
||||
if s.accounting.nodes+placementNodes > s.limits.nodes {
|
||||
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &declaration.location)}
|
||||
}
|
||||
s.accounting.nodes += placementNodes
|
||||
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
|
||||
source: cloneSourceInfo(info), payload: completed.root, document: placed,
|
||||
destination: pl.clone(), relativeDestination: relativeDestination.clone(), processing: processing,
|
||||
nested: completed,
|
||||
source: cloneSourceInfo(info), payload: body,
|
||||
identity: identity, role: importRoleItem, scopeOwner: parentScope.owner,
|
||||
destination: destination.clone(), relativeDestination: relativeDestination.clone(), processing: processing,
|
||||
pendingItem: &itemScope,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *importTraversal) composePendingItems(scope *acquisitionScope) []Diagnostic {
|
||||
for index := range scope.contributions {
|
||||
contribution := &scope.contributions[index]
|
||||
if contribution.pendingItem == nil {
|
||||
continue
|
||||
}
|
||||
itemScope := contribution.pendingItem
|
||||
if diagnostics := s.composePendingItems(itemScope); len(diagnostics) != 0 {
|
||||
return projectItemDiagnosticPaths(diagnostics, *scope, contribution.destination)
|
||||
}
|
||||
documents := make([]*document.Node, 0, len(itemScope.contributions))
|
||||
for _, nested := range itemScope.contributions {
|
||||
documents = append(documents, nested.document)
|
||||
}
|
||||
completed, diagnostics := composeDocuments(documents...)
|
||||
if len(diagnostics) != 0 {
|
||||
return projectItemDiagnosticPaths(diagnostics, *scope, contribution.destination)
|
||||
}
|
||||
completed.acquisition = []acquisitionScope{cloneAcquisitionScope(*itemScope)}
|
||||
projectNestedEvidence(completed, *itemScope)
|
||||
_, placed, err := placeDocumentWithMode(completed.root, contribution.destination, placementModeSequenceItem, contribution.payload.Location, maxDocumentDepth, maxDocumentNodes)
|
||||
if err != nil {
|
||||
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source depth or node limit after destination placement", contribution.source.ImportedFrom)}
|
||||
}
|
||||
placementNodes := destinationWrapperNodeCountForMode(contribution.destination, placementModeSequenceItem)
|
||||
if s.accounting.nodes+placementNodes > s.limits.nodes {
|
||||
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", contribution.source.ImportedFrom)}
|
||||
}
|
||||
s.accounting.nodes += placementNodes
|
||||
contribution.payload = completed.root
|
||||
contribution.document = placed
|
||||
contribution.nested = completed
|
||||
contribution.pendingItem = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func importedVersionClaim(documents []*document.Node, composed *composedDocument, rule *VersionRule, mainIndex int) *Diagnostic {
|
||||
if composed == nil || rule == nil {
|
||||
return nil
|
||||
|
||||
@@ -656,6 +656,22 @@ func TestLoadImportIdentityReportsCyclesAndRepeatedRoutes(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cycle through an item scope", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
||||
imports:
|
||||
- path: item.yml
|
||||
destination: [items]
|
||||
placement: sequence-item
|
||||
`)
|
||||
writeImportFile(t, directory, "item.yml", "configkit: {imports: [{path: main.yml}]}\n")
|
||||
loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportCycle)
|
||||
diagnostic := loadError.Diagnostics()[0]
|
||||
if diagnostic.Primary == nil || len(diagnostic.Related) != 1 || !strings.Contains(diagnostic.Message, "main.yml") || !strings.Contains(diagnostic.Message, "item.yml") {
|
||||
t.Fatalf("nested item cycle diagnostic = %#v", diagnostic)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("diamond", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports: [{path: left.yml}, {path: right.yml}]\n")
|
||||
@@ -686,6 +702,61 @@ func TestLoadImportIdentityReportsCyclesAndRepeatedRoutes(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestItemAcquisitionRetainsInclusionGraphAndScopeOwnership(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
||||
imports:
|
||||
- path: item.yml
|
||||
destination: [items]
|
||||
placement: sequence-item
|
||||
`)
|
||||
itemPath := writeImportFile(t, directory, "item.yml", "configkit: {imports: [{path: helper.yml}]}\nname: item\n")
|
||||
helperPath := writeImportFile(t, directory, "helper.yml", "id: supplied\n")
|
||||
|
||||
input, diagnostics := assembleBaseInput[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatalf("assemble input: %#v", diagnostics)
|
||||
}
|
||||
graph := input.composition.importGraph
|
||||
if len(graph.nodes) != 3 || len(graph.edges) != 2 {
|
||||
t.Fatalf("inclusion graph has %d nodes and %d edges, want 3 and 2", len(graph.nodes), len(graph.edges))
|
||||
}
|
||||
for index, want := range []struct {
|
||||
path string
|
||||
role importContributionRole
|
||||
}{
|
||||
{path: canonicalTestPath(t, mainPath), role: importRoleMain},
|
||||
{path: canonicalTestPath(t, itemPath), role: importRoleItem},
|
||||
{path: canonicalTestPath(t, helperPath), role: importRoleMapping},
|
||||
} {
|
||||
node := graph.nodes[index]
|
||||
if node.canonical != want.path || node.role != want.role || node.sourceID != "source-"+strconv.Itoa(index+1) {
|
||||
t.Errorf("graph node %d = %#v, want %s, %q, source-%d", index, node, want.role, want.path, index+1)
|
||||
}
|
||||
}
|
||||
rootScope := input.composition.acquisition[0]
|
||||
if rootScope.owner != graph.nodes[0].key || len(rootScope.contributions) != 2 {
|
||||
t.Fatalf("root acquisition scope = %#v", rootScope)
|
||||
}
|
||||
item := rootScope.contributions[1]
|
||||
if item.identity != graph.nodes[1].key || item.role != importRoleItem || item.scopeOwner != rootScope.owner || item.nested == nil {
|
||||
t.Fatalf("outer item contribution = %#v", item)
|
||||
}
|
||||
itemScope := item.nested.acquisition[0]
|
||||
if itemScope.owner != item.identity || len(itemScope.contributions) != 2 {
|
||||
t.Fatalf("nested item scope = %#v", itemScope)
|
||||
}
|
||||
for _, contribution := range itemScope.contributions {
|
||||
if contribution.scopeOwner != item.identity {
|
||||
t.Errorf("nested contribution scope owner = %#v, want item %v", contribution.scopeOwner, item.identity)
|
||||
}
|
||||
}
|
||||
if graph.edges[0].importer != graph.nodes[0].key || graph.edges[0].imported != graph.nodes[1].key || graph.edges[0].location.SourceID != "source-1" ||
|
||||
graph.edges[1].importer != graph.nodes[1].key || graph.edges[1].imported != graph.nodes[2].key || graph.edges[1].location.SourceID != "source-2" {
|
||||
t.Fatalf("inclusion edges = %#v", graph.edges)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) {
|
||||
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
|
||||
t.Skip("initial filesystem contract covers Linux and macOS")
|
||||
|
||||
@@ -23,37 +23,51 @@ type assembledInput struct {
|
||||
// boundary for later scoped assembly and evidence.
|
||||
type acquiredContribution struct {
|
||||
source SourceInfo
|
||||
identity importNodeKey
|
||||
role importContributionRole
|
||||
scopeOwner importNodeKey
|
||||
payload *document.Node
|
||||
document *document.Node
|
||||
destination Path
|
||||
relativeDestination Path
|
||||
processing *sourceVersionProcessingResult
|
||||
nested *composedDocument
|
||||
pendingItem *acquisitionScope
|
||||
}
|
||||
|
||||
// acquisitionScope is the ordered strict mapping scope produced by one input
|
||||
// and its ordinary imports.
|
||||
type acquisitionScope struct {
|
||||
owner importNodeKey
|
||||
destination Path
|
||||
contributions []acquiredContribution
|
||||
}
|
||||
|
||||
func cloneAcquisitionScope(scope acquisitionScope) acquisitionScope {
|
||||
cloned := acquisitionScope{destination: scope.destination.clone()}
|
||||
cloned := acquisitionScope{owner: scope.owner, destination: scope.destination.clone()}
|
||||
if scope.contributions != nil {
|
||||
cloned.contributions = make([]acquiredContribution, len(scope.contributions))
|
||||
for index, contribution := range scope.contributions {
|
||||
cloned.contributions[index] = acquiredContribution{
|
||||
source: cloneSourceInfo(contribution.source), payload: contribution.payload,
|
||||
source: cloneSourceInfo(contribution.source), identity: contribution.identity,
|
||||
role: contribution.role, scopeOwner: contribution.scopeOwner, payload: contribution.payload,
|
||||
document: contribution.document, destination: contribution.destination.clone(),
|
||||
relativeDestination: contribution.relativeDestination.clone(), processing: contribution.processing,
|
||||
nested: cloneComposedDocument(contribution.nested),
|
||||
nested: cloneComposedDocument(contribution.nested), pendingItem: cloneAcquisitionScopePointer(contribution.pendingItem),
|
||||
}
|
||||
}
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func cloneAcquisitionScopePointer(scope *acquisitionScope) *acquisitionScope {
|
||||
if scope == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := cloneAcquisitionScope(*scope)
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func cloneAcquisitionScopes(scopes []acquisitionScope) []acquisitionScope {
|
||||
if scopes == nil {
|
||||
return nil
|
||||
@@ -145,10 +159,13 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d
|
||||
}
|
||||
localPayload := input.document
|
||||
input.document = placed
|
||||
identity := importKeyForMainSource(parsed.source)
|
||||
input.acquisition = []acquisitionScope{{
|
||||
owner: identity,
|
||||
destination: destination.clone(),
|
||||
contributions: []acquiredContribution{{
|
||||
source: cloneSourceInfo(*parsed.source), payload: localPayload,
|
||||
identity: identity, role: importRoleMain, scopeOwner: identity,
|
||||
document: placed, destination: destination.clone(), processing: versionProcessing,
|
||||
}},
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user