Schedule underlay groups as dependency forests
This commit is contained in:
@@ -941,6 +941,9 @@ own strict mapping scope, with helpers before the bodies that import them. Those
|
||||
local scopes do not use application collection policies and are not flattened into
|
||||
the outer schedule. Item positions and diagnostic paths follow the resulting
|
||||
assembly order.
|
||||
Each underlay group likewise acquires its inputs before dependency-ready assembly
|
||||
in one strict scope. Independent groups remain separate precedence tiers; a higher
|
||||
tier still replaces a lower tier's sequence values wholesale.
|
||||
Assembly order never permits replacement. Required missing paths fail; `optional:
|
||||
true` suppresses only target nonexistence, not invalid paths, containment failures,
|
||||
directories, permissions, parse errors, or composition errors.
|
||||
|
||||
@@ -89,6 +89,15 @@ func (graph importInclusionGraph) clone() importInclusionGraph {
|
||||
return cloned
|
||||
}
|
||||
|
||||
func mergeImportInclusionGraph(target *importInclusionGraph, source importInclusionGraph) {
|
||||
for _, node := range source.nodes {
|
||||
target.addNode(node)
|
||||
}
|
||||
for _, edge := range source.edges {
|
||||
target.addEdge(edge.importer, edge.imported, edge.location)
|
||||
}
|
||||
}
|
||||
|
||||
func (graph importInclusionGraph) scheduleCycle(closingEdge int, location Location) (Diagnostic, bool) {
|
||||
keys := make([]dependencyKey, len(graph.nodes))
|
||||
for index, node := range graph.nodes {
|
||||
@@ -149,21 +158,41 @@ func (graph importInclusionGraph) scheduleCycle(closingEdge int, location Locati
|
||||
}
|
||||
|
||||
func scheduleAcquisitionScope(scope acquisitionScope, graph importInclusionGraph) acquisitionScope {
|
||||
positions := make(map[importNodeKey]int, len(scope.contributions))
|
||||
slots := make([]int, 0, len(scope.contributions))
|
||||
keys := make([]dependencyKey, 0, len(scope.contributions))
|
||||
for index, contribution := range scope.contributions {
|
||||
if contribution.scopeOwner != scope.owner {
|
||||
continue
|
||||
}
|
||||
positions[contribution.identity] = len(keys)
|
||||
slots = append(slots, index)
|
||||
keys = append(keys, contribution.identity.dependencyKey())
|
||||
}
|
||||
if len(keys) < 2 {
|
||||
return scope
|
||||
}
|
||||
scheduled := scope
|
||||
scheduled.contributions = scheduleContributionSlots(scope.contributions, slots, graph)
|
||||
return scheduled
|
||||
}
|
||||
|
||||
func scheduleAcquisitionGroup(scopes []acquisitionScope, graph importInclusionGraph) acquisitionScope {
|
||||
var contributions []acquiredContribution
|
||||
for _, scope := range scopes {
|
||||
contributions = append(contributions, scope.contributions...)
|
||||
}
|
||||
slots := make([]int, len(contributions))
|
||||
for index := range slots {
|
||||
slots[index] = index
|
||||
}
|
||||
return acquisitionScope{contributions: scheduleContributionSlots(contributions, slots, graph)}
|
||||
}
|
||||
|
||||
func scheduleContributionSlots(contributions []acquiredContribution, slots []int, graph importInclusionGraph) []acquiredContribution {
|
||||
if len(slots) < 2 {
|
||||
return append([]acquiredContribution(nil), contributions...)
|
||||
}
|
||||
positions := make(map[importNodeKey]int, len(slots))
|
||||
keys := make([]dependencyKey, len(slots))
|
||||
for position, slot := range slots {
|
||||
contribution := contributions[slot]
|
||||
positions[contribution.identity] = position
|
||||
keys[position] = contribution.identity.dependencyKey()
|
||||
}
|
||||
edges := make([]dependencyEdge, 0, len(graph.edges))
|
||||
for _, edge := range graph.edges {
|
||||
importer, importerOK := positions[edge.importer]
|
||||
@@ -174,16 +203,86 @@ func scheduleAcquisitionScope(scope acquisitionScope, graph importInclusionGraph
|
||||
}
|
||||
order, _ := scheduleDependencies(keys, edges)
|
||||
if len(order) != len(keys) {
|
||||
return scope
|
||||
return append([]acquiredContribution(nil), contributions...)
|
||||
}
|
||||
scheduled := scope
|
||||
scheduled.contributions = append([]acquiredContribution(nil), scope.contributions...)
|
||||
scheduled := append([]acquiredContribution(nil), contributions...)
|
||||
for position, scheduledIndex := range order {
|
||||
scheduled.contributions[slots[position]] = scope.contributions[slots[scheduledIndex]]
|
||||
scheduled[slots[position]] = contributions[slots[scheduledIndex]]
|
||||
}
|
||||
return scheduled
|
||||
}
|
||||
|
||||
type importNamespaceAnchors struct {
|
||||
kind string
|
||||
host string
|
||||
native string
|
||||
input string
|
||||
}
|
||||
|
||||
func explicitNamespaceAnchor(group, input int) string {
|
||||
return fmt.Sprintf("%020d/%020d", group, input)
|
||||
}
|
||||
|
||||
func reanchorImportKey(key importNodeKey, anchors importNamespaceAnchors) importNodeKey {
|
||||
if anchors.kind != "" {
|
||||
key.namespaceKind = anchors.kind
|
||||
}
|
||||
switch {
|
||||
case key.resourceKind == "synthetic":
|
||||
key.namespaceAnchor = anchors.input
|
||||
case key.namespaceKind == "native":
|
||||
key.namespaceAnchor = anchors.native
|
||||
default:
|
||||
key.namespaceAnchor = anchors.host
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func reanchorImportGraph(graph importInclusionGraph, anchors importNamespaceAnchors) (importInclusionGraph, map[importNodeKey]importNodeKey) {
|
||||
reanchored := newImportInclusionGraph()
|
||||
keys := make(map[importNodeKey]importNodeKey, len(graph.nodes))
|
||||
for _, node := range graph.nodes {
|
||||
old := node.key
|
||||
node.key = reanchorImportKey(old, anchors)
|
||||
keys[old] = node.key
|
||||
reanchored.addNode(node)
|
||||
}
|
||||
for _, edge := range graph.edges {
|
||||
reanchored.addEdge(keys[edge.importer], keys[edge.imported], edge.location)
|
||||
}
|
||||
return reanchored, keys
|
||||
}
|
||||
|
||||
func reanchorAcquisitionScope(scope *acquisitionScope, keys map[importNodeKey]importNodeKey, anchors importNamespaceAnchors) {
|
||||
if scope == nil {
|
||||
return
|
||||
}
|
||||
if key, exists := keys[scope.owner]; exists {
|
||||
scope.owner = key
|
||||
} else {
|
||||
scope.owner = reanchorImportKey(scope.owner, anchors)
|
||||
}
|
||||
for index := range scope.contributions {
|
||||
contribution := &scope.contributions[index]
|
||||
if key, exists := keys[contribution.identity]; exists {
|
||||
contribution.identity = key
|
||||
} else {
|
||||
contribution.identity = reanchorImportKey(contribution.identity, anchors)
|
||||
}
|
||||
if key, exists := keys[contribution.scopeOwner]; exists {
|
||||
contribution.scopeOwner = key
|
||||
} else {
|
||||
contribution.scopeOwner = reanchorImportKey(contribution.scopeOwner, anchors)
|
||||
}
|
||||
reanchorAcquisitionScope(contribution.pendingItem, keys, anchors)
|
||||
if contribution.nested != nil {
|
||||
for nestedIndex := range contribution.nested.acquisition {
|
||||
reanchorAcquisitionScope(&contribution.nested.acquisition[nestedIndex], keys, anchors)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func importKeyFor(access *filesystemAccess, canonical string, synthetic bool) importNodeKey {
|
||||
kind := "host"
|
||||
resourceKind := "file"
|
||||
|
||||
@@ -80,12 +80,16 @@ func loadImportedDocumentsTrackedWithVersion(source Source, context *FilesystemC
|
||||
}
|
||||
|
||||
func loadImportedDocumentsTrackedWithProcessing(source Source, context *FilesystemContext, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute, hostToken *namespaceToken, versionState *sourceVersionState, processing *sourceVersionProcessingResult, policy sequencePolicy) (*composedDocument, []SourceInfo, []Diagnostic) {
|
||||
return loadImportedDocumentsTrackedWithProcessingDeferred(source, context, main, destination, rule, limits, accounting, visited, hostToken, versionState, processing, policy, false)
|
||||
}
|
||||
|
||||
func loadImportedDocumentsTrackedWithProcessingDeferred(source Source, context *FilesystemContext, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute, hostToken *namespaceToken, versionState *sourceVersionState, processing *sourceVersionProcessingResult, policy sequencePolicy, deferAssembly bool) (*composedDocument, []SourceInfo, []Diagnostic) {
|
||||
if nativeContext(source, context) {
|
||||
access, basePath, mainPath, diagnostic := nativeFilesystemAccess(source, context)
|
||||
if diagnostic != nil {
|
||||
return nil, sourceInfos(main.source), []Diagnostic{*diagnostic}
|
||||
}
|
||||
return loadImportedWithAccessTrackedAndProcessingPolicy(access, basePath, mainPath, main, destination, rule, limits, accounting, visited, versionState, processing, policy)
|
||||
return loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access, basePath, mainPath, main, destination, rule, limits, accounting, visited, versionState, processing, policy, deferAssembly)
|
||||
}
|
||||
rootPath, basePath, mainPath, diagnostic := importFilesystem(source, context, main)
|
||||
if diagnostic != nil {
|
||||
@@ -100,7 +104,7 @@ func loadImportedDocumentsTrackedWithProcessing(source Source, context *Filesyst
|
||||
if hostToken != nil {
|
||||
access.token = hostToken
|
||||
}
|
||||
return loadImportedWithAccessTrackedAndProcessingPolicy(access, basePath, mainPath, main, destination, rule, limits, accounting, visited, versionState, processing, policy)
|
||||
return loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access, basePath, mainPath, main, destination, rule, limits, accounting, visited, versionState, processing, policy, deferAssembly)
|
||||
}
|
||||
|
||||
func loadImportedWithAccess(access *filesystemAccess, basePath, mainPath string, main parsedSource, rule *VersionRule, limits importLimits, accounting *importAccounting) (*composedDocument, []SourceInfo, []Diagnostic) {
|
||||
@@ -124,6 +128,10 @@ func loadImportedWithAccessTrackedAndProcessing(access *filesystemAccess, basePa
|
||||
}
|
||||
|
||||
func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess, basePath, mainPath string, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute, versionState *sourceVersionState, processing *sourceVersionProcessingResult, policy sequencePolicy) (*composedDocument, []SourceInfo, []Diagnostic) {
|
||||
return loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access, basePath, mainPath, main, destination, rule, limits, accounting, visited, versionState, processing, policy, false)
|
||||
}
|
||||
|
||||
func loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access *filesystemAccess, basePath, mainPath string, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute, versionState *sourceVersionState, processing *sourceVersionProcessingResult, policy sequencePolicy, deferAssembly bool) (*composedDocument, []SourceInfo, []Diagnostic) {
|
||||
if !access.contains(basePath) || (mainPath != "" && !access.contains(mainPath)) {
|
||||
_ = access.close()
|
||||
return nil, sourceInfos(main.source), []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "selected source and import base must stay inside the configuration root", nil)}
|
||||
@@ -221,8 +229,10 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
|
||||
}
|
||||
if len(diagnostics) == 0 {
|
||||
state.scheduleAcquisitionScopes(&state.scope)
|
||||
if !deferAssembly {
|
||||
diagnostics = state.composePendingItems(&state.scope)
|
||||
}
|
||||
}
|
||||
|
||||
closeErr := access.close()
|
||||
if len(diagnostics) != 0 {
|
||||
@@ -234,6 +244,27 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
|
||||
if closeErr != nil {
|
||||
return nil, cloneSourceInfos(state.sources), []Diagnostic{importDiagnostic(CodeImportRead, "configuration root could not be closed", nil)}
|
||||
}
|
||||
if deferAssembly {
|
||||
acquisition := state.scope
|
||||
if mainIsItem {
|
||||
localScope := state.scope
|
||||
acquisition = acquisitionScope{
|
||||
owner: mainNodeKey, destination: destination.clone(),
|
||||
contributions: []acquiredContribution{{
|
||||
source: cloneSourceInfo(mainInfo), payload: body, identity: mainNodeKey,
|
||||
role: importRoleMain, scopeOwner: mainNodeKey, destination: destination.clone(),
|
||||
processing: processing, pendingItem: &localScope,
|
||||
}},
|
||||
}
|
||||
}
|
||||
if diagnostic := reservePendingItemPlacements(&acquisition, limits, accounting); diagnostic != nil {
|
||||
return nil, cloneSourceInfos(state.sources), []Diagnostic{*diagnostic}
|
||||
}
|
||||
return &composedDocument{
|
||||
acquisition: []acquisitionScope{cloneAcquisitionScope(acquisition)},
|
||||
importGraph: state.importGraph.clone(), inputBytes: accounting.bytes, inputNodes: accounting.nodes,
|
||||
}, cloneSourceInfos(state.sources), nil
|
||||
}
|
||||
if mainIsItem {
|
||||
localScope := state.scope
|
||||
documents := make([]*document.Node, 0, len(localScope.contributions))
|
||||
@@ -313,6 +344,29 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
|
||||
return composed, cloneSourceInfos(state.sources), diagnostics
|
||||
}
|
||||
|
||||
func reservePendingItemPlacements(scope *acquisitionScope, limits importLimits, accounting *importAccounting) *Diagnostic {
|
||||
if scope == nil {
|
||||
return nil
|
||||
}
|
||||
for index := range scope.contributions {
|
||||
contribution := &scope.contributions[index]
|
||||
if contribution.pendingItem == nil {
|
||||
continue
|
||||
}
|
||||
placementNodes := destinationWrapperNodeCountForMode(contribution.destination, placementModeSequenceItem)
|
||||
if accounting.nodes+placementNodes > limits.nodes {
|
||||
diagnostic := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", contribution.source.ImportedFrom)
|
||||
return &diagnostic
|
||||
}
|
||||
accounting.nodes += placementNodes
|
||||
contribution.placementCharged = true
|
||||
if diagnostic := reservePendingItemPlacements(contribution.pendingItem, limits, accounting); diagnostic != nil {
|
||||
return diagnostic
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *importTraversal) checkInitialLimits() *Diagnostic {
|
||||
if s.accounting.sources > s.limits.sources {
|
||||
value := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source-count limit", nil)
|
||||
@@ -609,6 +663,9 @@ func (s *importTraversal) composePendingItems(scope *acquisitionScope) []Diagnos
|
||||
if len(diagnostics) != 0 {
|
||||
return projectItemDiagnosticPaths(diagnostics, *scope, contribution.destination, index)
|
||||
}
|
||||
if diagnostic := importedVersionClaim(documents, *itemScope, completed, s.rule); diagnostic != nil {
|
||||
return []Diagnostic{*diagnostic}
|
||||
}
|
||||
completed.acquisition = []acquisitionScope{cloneAcquisitionScope(*itemScope)}
|
||||
projectNestedEvidence(completed, *itemScope)
|
||||
_, placed, err := placeDocumentWithMode(completed.root, contribution.destination, placementModeSequenceItem, contribution.payload.Location, maxDocumentDepth, maxDocumentNodes)
|
||||
@@ -616,10 +673,12 @@ func (s *importTraversal) composePendingItems(scope *acquisitionScope) []Diagnos
|
||||
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source depth or node limit after destination placement", contribution.source.ImportedFrom)}
|
||||
}
|
||||
placementNodes := destinationWrapperNodeCountForMode(contribution.destination, placementModeSequenceItem)
|
||||
if !contribution.placementCharged {
|
||||
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
|
||||
|
||||
@@ -33,6 +33,7 @@ type Options[T any] struct {
|
||||
destination Path
|
||||
placementMode placementMode
|
||||
sequencePolicy sequencePolicy
|
||||
deferAssembly bool
|
||||
}
|
||||
|
||||
// FilesystemContext supplies filesystem roots for imports and overlays.
|
||||
|
||||
@@ -33,6 +33,7 @@ type acquiredContribution struct {
|
||||
processing *sourceVersionProcessingResult
|
||||
nested *composedDocument
|
||||
pendingItem *acquisitionScope
|
||||
placementCharged bool
|
||||
}
|
||||
|
||||
// acquisitionScope is the ordered strict mapping scope produced by one input
|
||||
@@ -54,6 +55,7 @@ func cloneAcquisitionScope(scope acquisitionScope) acquisitionScope {
|
||||
document: contribution.document, destination: contribution.destination.clone(),
|
||||
relativeDestination: contribution.relativeDestination.clone(), processing: contribution.processing,
|
||||
nested: cloneComposedDocument(contribution.nested), pendingItem: cloneAcquisitionScopePointer(contribution.pendingItem),
|
||||
placementCharged: contribution.placementCharged,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,12 +137,18 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d
|
||||
}
|
||||
}
|
||||
if options.Imports {
|
||||
if options.deferAssembly {
|
||||
input.composition, input.sources, diagnostics = loadImportedDocumentsTrackedWithProcessingDeferred(source, options.Filesystem, parsed, destination, options.Definition.Version, limits, accounting, visited, hostToken, versionState, versionProcessing, options.sequencePolicy, true)
|
||||
} else {
|
||||
input.composition, input.sources, diagnostics = loadImportedDocumentsTrackedWithProcessing(source, options.Filesystem, parsed, destination, options.Definition.Version, limits, accounting, visited, hostToken, versionState, versionProcessing, options.sequencePolicy)
|
||||
}
|
||||
if len(diagnostics) != 0 {
|
||||
return input, diagnostics
|
||||
}
|
||||
if input.composition != nil {
|
||||
input.acquisition = cloneAcquisitionScopes(input.composition.acquisition)
|
||||
input.document = input.composition.root
|
||||
}
|
||||
} else {
|
||||
input.sources = sourceInfos(parsed.source)
|
||||
if versionProcessing != nil {
|
||||
|
||||
@@ -158,14 +158,23 @@ func acquireUnderlayGroupWithPolicyVersion(group loadGroup, index int, rule *Ver
|
||||
initialBytes, initialNodes := accounting.bytes, accounting.nodes
|
||||
visited := make(map[resourceIdentity]importRoute)
|
||||
hostToken := &namespaceToken{}
|
||||
nativeAnchors := make(map[*namespaceToken]string)
|
||||
for inputIndex, contribution := range group.inputs {
|
||||
if token := nativeTokenForLoadInput(contribution); token != nil {
|
||||
if _, exists := nativeAnchors[token]; !exists {
|
||||
nativeAnchors[token] = explicitNamespaceAnchor(index, inputIndex+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
var scopes []acquisitionScope
|
||||
inclusionGraph := newImportInclusionGraph()
|
||||
var sources []SourceInfo
|
||||
var contexts map[string]ResourceContext
|
||||
for _, contribution := range group.inputs {
|
||||
for inputIndex, contribution := range group.inputs {
|
||||
options := Options[struct{}]{
|
||||
Imports: contribution.imports, Filesystem: contribution.filesystem,
|
||||
Definition: Definition[struct{}]{Version: rule}, destination: contribution.destination.clone(),
|
||||
placementMode: contribution.placementMode, sequencePolicy: policy,
|
||||
placementMode: contribution.placementMode, sequencePolicy: policy, deferAssembly: true,
|
||||
}
|
||||
acquired, diagnostics := assembleBaseInputTrackedForRole(contribution.source, options, contribution.destination, accounting, visited, hostToken, versionState, sourceVersionContribution)
|
||||
for _, source := range acquired.sources {
|
||||
@@ -177,17 +186,38 @@ func acquireUnderlayGroupWithPolicyVersion(group loadGroup, index int, rule *Ver
|
||||
if len(diagnostics) != 0 {
|
||||
return nil, sources, contexts, projectPriorInputDiagnosticPaths(diagnostics, scopes)
|
||||
}
|
||||
if acquired.document == nil {
|
||||
if acquired.document == nil && len(acquired.acquisition) == 0 {
|
||||
continue
|
||||
}
|
||||
scopes = append(scopes, cloneAcquisitionScopes(acquired.acquisition)...)
|
||||
anchors := importNamespaceAnchors{
|
||||
kind: "host", host: explicitNamespaceAnchor(index, 0),
|
||||
input: explicitNamespaceAnchor(index, inputIndex+1),
|
||||
}
|
||||
if token := nativeTokenForLoadInput(contribution); token != nil {
|
||||
anchors.kind = "native"
|
||||
anchors.native = nativeAnchors[token]
|
||||
}
|
||||
inputScopes := cloneAcquisitionScopes(acquired.acquisition)
|
||||
inputGraph := newImportInclusionGraph()
|
||||
if acquired.composition != nil {
|
||||
inputGraph = acquired.composition.importGraph
|
||||
}
|
||||
inputGraph, keyMap := reanchorImportGraph(inputGraph, anchors)
|
||||
for scopeIndex := range inputScopes {
|
||||
reanchorAcquisitionScope(&inputScopes[scopeIndex], keyMap, anchors)
|
||||
}
|
||||
scopes = append(scopes, inputScopes...)
|
||||
mergeImportInclusionGraph(&inclusionGraph, inputGraph)
|
||||
}
|
||||
groupScope := scheduleAcquisitionGroup(scopes, inclusionGraph)
|
||||
traversal := importTraversal{accounting: accounting, limits: defaultImportLimits(), rule: rule, importGraph: inclusionGraph}
|
||||
if diagnostics := traversal.composePendingItems(&groupScope); len(diagnostics) != 0 {
|
||||
return nil, sources, contexts, diagnostics
|
||||
}
|
||||
var documents []*document.Node
|
||||
for _, scope := range scopes {
|
||||
for _, contribution := range scope.contributions {
|
||||
for _, contribution := range groupScope.contributions {
|
||||
documents = append(documents, contribution.document)
|
||||
}
|
||||
}
|
||||
if len(documents) == 0 {
|
||||
return nil, sources, contexts, nil
|
||||
}
|
||||
@@ -197,6 +227,18 @@ func acquireUnderlayGroupWithPolicyVersion(group loadGroup, index int, rule *Ver
|
||||
}
|
||||
if composition != nil && rule != nil {
|
||||
ranges := indexSequenceRanges(composition.sequenceRanges)
|
||||
for sourceIndex, document := range documents {
|
||||
if groupScope.contributions[sourceIndex].role == importRoleMain {
|
||||
continue
|
||||
}
|
||||
if node := fragmentVersionClaimWithIndex(document, rule, sourceIndex, ranges); node != nil {
|
||||
location := publicLocation(node.Location)
|
||||
return nil, sources, contexts, []Diagnostic{{
|
||||
Code: CodeImportVersion, Severity: SeverityError, Path: rule.Path.clone(),
|
||||
Message: "imported fragments cannot contribute to the schema-version path", Primary: &location,
|
||||
}}
|
||||
}
|
||||
}
|
||||
for sourceIndex, document := range documents {
|
||||
if node := fragmentVersionClaimWithIndex(document, rule, sourceIndex, ranges); node != nil {
|
||||
location := publicLocation(node.Location)
|
||||
@@ -209,13 +251,14 @@ func acquireUnderlayGroupWithPolicyVersion(group loadGroup, index int, rule *Ver
|
||||
}
|
||||
composition.inputBytes = accounting.bytes - initialBytes
|
||||
composition.inputNodes = accounting.nodes - initialNodes
|
||||
composition.acquisition = cloneAcquisitionScopes(scopes)
|
||||
var projectionScope acquisitionScope
|
||||
for _, scope := range scopes {
|
||||
projectionScope.contributions = append(projectionScope.contributions, scope.contributions...)
|
||||
groupScope.owner = importNodeKey{namespaceKind: "group", namespaceAnchor: explicitNamespaceAnchor(index, 0), resourceKind: "scope"}
|
||||
for contributionIndex := range groupScope.contributions {
|
||||
groupScope.contributions[contributionIndex].scopeOwner = groupScope.owner
|
||||
}
|
||||
projectNestedEvidence(composition, projectionScope)
|
||||
projectAcquiredDestinations(composition, projectionScope, sources)
|
||||
composition.acquisition = []acquisitionScope{cloneAcquisitionScope(groupScope)}
|
||||
composition.importGraph = inclusionGraph.clone()
|
||||
projectNestedEvidence(composition, groupScope)
|
||||
projectAcquiredDestinations(composition, groupScope, sources)
|
||||
for pointer, origin := range composition.origins {
|
||||
origin.Layer = LayerUnderlay
|
||||
composition.origins[pointer] = origin
|
||||
@@ -225,3 +268,13 @@ func acquireUnderlayGroupWithPolicyVersion(group loadGroup, index int, rule *Ver
|
||||
}
|
||||
return composition, sources, contexts, nil
|
||||
}
|
||||
|
||||
func nativeTokenForLoadInput(input loadInput) *namespaceToken {
|
||||
if input.source.kind == sourceFilesystem && input.source.namespace != nil {
|
||||
return input.source.namespace.token
|
||||
}
|
||||
if input.filesystem != nil && input.filesystem.FS != nil && input.filesystem.FS.namespace != nil {
|
||||
return input.filesystem.FS.namespace.token
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -19,6 +19,186 @@ func testGroupedLoad[T any](request loadRequest[T]) (*testResult[T], error) {
|
||||
return testLoaded(loaded), err
|
||||
}
|
||||
|
||||
func TestUnderlayGroupSchedulesInterleavedImportBranches(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
zRoot := writeImportFile(t, directory, "z-root.yml", `configkit:
|
||||
imports:
|
||||
- path: d-parent.yml
|
||||
items: [{id: z}]
|
||||
`)
|
||||
dParent := writeImportFile(t, directory, "d-parent.yml", `configkit:
|
||||
imports:
|
||||
- path: a-dependency.yml
|
||||
items: [{id: d}]
|
||||
`)
|
||||
aDependency := writeImportFile(t, directory, "a-dependency.yml", "items: [{id: a}]\n")
|
||||
bRoot := writeImportFile(t, directory, "b-root.yml", `configkit:
|
||||
imports:
|
||||
- path: c-helper.yml
|
||||
items: [{id: b}]
|
||||
`)
|
||||
cHelper := writeImportFile(t, directory, "c-helper.yml", "items: [{id: c}]\n")
|
||||
|
||||
request := loadRequest[map[string]any]{
|
||||
main: loadInput{source: NoSource()},
|
||||
underlays: []loadGroup{{inputs: []loadInput{
|
||||
{source: NoSource()},
|
||||
{source: File(zRoot), imports: true},
|
||||
{source: File(bRoot), imports: true},
|
||||
}}},
|
||||
definition: Definition[map[string]any]{sequenceRegistrations: []sequenceRegistration{{path: Keys("items"), identityField: "id"}}},
|
||||
}
|
||||
result, err := testGroupedLoad(request)
|
||||
if err != nil {
|
||||
t.Fatalf("grouped load: %v", err)
|
||||
}
|
||||
items := result.Config["items"].([]any)
|
||||
wantIDs := []string{"a", "c", "b", "d", "z"}
|
||||
if len(items) != len(wantIDs) {
|
||||
t.Fatalf("scheduled items = %#v", items)
|
||||
}
|
||||
for index, want := range wantIDs {
|
||||
if got := items[index].(map[string]any)["id"]; got != want {
|
||||
t.Errorf("scheduled item %d ID = %#v, want %q", index, got, want)
|
||||
}
|
||||
}
|
||||
wantPaths := []string{zRoot, dParent, aDependency, bRoot, cHelper}
|
||||
for index, want := range wantPaths {
|
||||
if got := result.Sources()[index].Path; got != canonicalTestPath(t, want) {
|
||||
t.Errorf("acquired source %d path = %q, want %q", index, got, canonicalTestPath(t, want))
|
||||
}
|
||||
}
|
||||
if got := result.Sources(); len(got) != len(wantPaths) {
|
||||
t.Fatalf("NoSource added a source record: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnderlayNamespaceAnchorsUseInputPositionsAndFilesystemIdentity(t *testing.T) {
|
||||
firstNamespace := NewFilesystem("same-label", fstest.MapFS{
|
||||
"root.yml": &fstest.MapFile{Data: []byte("items: [{id: first}]\n")},
|
||||
"later.yml": &fstest.MapFile{Data: []byte("items: [{id: copied}]\n")},
|
||||
})
|
||||
secondNamespace := NewFilesystem("same-label", fstest.MapFS{
|
||||
"root.yml": &fstest.MapFile{Data: []byte("items: [{id: second}]\n")},
|
||||
})
|
||||
request := loadRequest[map[string]any]{
|
||||
main: loadInput{source: NoSource()},
|
||||
underlays: []loadGroup{{inputs: []loadInput{
|
||||
{source: firstNamespace.File("root.yml")},
|
||||
{source: secondNamespace.File("root.yml")},
|
||||
{source: firstNamespace.File("later.yml")},
|
||||
}}},
|
||||
definition: Definition[map[string]any]{sequenceRegistrations: []sequenceRegistration{{path: Keys("items"), identityField: "id"}}},
|
||||
}
|
||||
result, err := testGroupedLoad(request)
|
||||
if err != nil {
|
||||
t.Fatalf("grouped load with native namespaces: %v", err)
|
||||
}
|
||||
items := result.Config["items"].([]any)
|
||||
wantIDs := []string{"copied", "first", "second"}
|
||||
if len(items) != len(wantIDs) {
|
||||
t.Fatalf("scheduled native items = %#v", items)
|
||||
}
|
||||
for index, want := range wantIDs {
|
||||
if got := items[index].(map[string]any)["id"]; got != want {
|
||||
t.Errorf("scheduled native item %d ID = %#v, want %q", index, got, want)
|
||||
}
|
||||
}
|
||||
if sources := result.Sources(); len(sources) != len(wantIDs) {
|
||||
t.Fatalf("source records = %#v", sources)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnderlayItemConflictUsesScheduledGroupIndex(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
zItem := writeImportFile(t, directory, "z-item.yml", "id: duplicate\n")
|
||||
aItem := writeImportFile(t, directory, "a-item.yml", "id: duplicate\n")
|
||||
request := loadRequest[map[string]any]{
|
||||
main: loadInput{source: NoSource()},
|
||||
underlays: []loadGroup{{inputs: []loadInput{
|
||||
{source: File(zItem), destination: Keys("items"), placementMode: placementModeSequenceItem},
|
||||
{source: File(aItem), destination: Keys("items"), placementMode: placementModeSequenceItem},
|
||||
}}},
|
||||
definition: Definition[map[string]any]{sequenceRegistrations: []sequenceRegistration{{path: Keys("items"), identityField: "id"}}},
|
||||
}
|
||||
_, err := testGroupedLoad(request)
|
||||
loadError, ok := err.(*LoadError)
|
||||
if !ok || len(loadError.Diagnostics()) != 1 {
|
||||
t.Fatalf("expected item conflict: %v", err)
|
||||
}
|
||||
diagnostic := loadError.Diagnostics()[0]
|
||||
if diagnostic.Code != CodeComposeConflict || diagnostic.Path.String() != "/items" {
|
||||
t.Fatalf("group item conflict = %s %s, want compose.conflict /items", diagnostic.Code, diagnostic.Path.String())
|
||||
}
|
||||
files := make(map[string]string)
|
||||
for _, source := range loadError.Sources() {
|
||||
files[source.ID] = source.Path
|
||||
}
|
||||
locations := append([]Location(nil), diagnostic.Related...)
|
||||
if diagnostic.Primary != nil {
|
||||
locations = append(locations, *diagnostic.Primary)
|
||||
}
|
||||
wantLocations := map[string]Location{
|
||||
canonicalTestPath(t, zItem): {Line: 1, Column: 5},
|
||||
canonicalTestPath(t, aItem): {Line: 1, Column: 5},
|
||||
}
|
||||
gotPaths := make(map[string]bool, len(locations))
|
||||
for _, location := range locations {
|
||||
want := wantLocations[files[location.SourceID]]
|
||||
gotPaths[files[location.SourceID]] = location.Line == want.Line && location.Column == want.Column
|
||||
}
|
||||
if len(locations) != 2 || !gotPaths[canonicalTestPath(t, zItem)] || !gotPaths[canonicalTestPath(t, aItem)] {
|
||||
t.Fatalf("group item conflict sites = %#v; source paths = %#v", locations, files)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnderlayItemHelperConflictUsesScheduledGroupIndex(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
zBad := writeImportFile(t, directory, "z-bad.yml", "configkit: {imports: [{path: helper.yml}]}\nid: duplicate\n")
|
||||
helper := writeImportFile(t, directory, "helper.yml", "id: duplicate\n")
|
||||
aGood := writeImportFile(t, directory, "a-good.yml", "id: good\n")
|
||||
request := loadRequest[map[string]any]{
|
||||
main: loadInput{source: NoSource()},
|
||||
underlays: []loadGroup{{inputs: []loadInput{
|
||||
{source: File(zBad), imports: true, destination: Keys("items"), placementMode: placementModeSequenceItem},
|
||||
{source: File(aGood), destination: Keys("items"), placementMode: placementModeSequenceItem},
|
||||
}}},
|
||||
definition: Definition[map[string]any]{sequenceRegistrations: []sequenceRegistration{{path: Keys("items"), identityField: "id"}}},
|
||||
}
|
||||
_, err := testGroupedLoad(request)
|
||||
loadError, ok := err.(*LoadError)
|
||||
if !ok || len(loadError.Diagnostics()) != 1 {
|
||||
t.Fatalf("expected item helper conflict: %v", err)
|
||||
}
|
||||
diagnostic := loadError.Diagnostics()[0]
|
||||
if diagnostic.Code != CodeComposeConflict || diagnostic.Path.String() != "/items/1/id" {
|
||||
t.Fatalf("group item helper conflict = %s %s, want compose.conflict /items/1/id", diagnostic.Code, diagnostic.Path.String())
|
||||
}
|
||||
files := make(map[string]string)
|
||||
for _, source := range loadError.Sources() {
|
||||
files[source.ID] = source.Path
|
||||
}
|
||||
if len(files) != 3 {
|
||||
t.Fatalf("group acquisition stopped before all sources were read: %#v", files)
|
||||
}
|
||||
locations := append([]Location(nil), diagnostic.Related...)
|
||||
if diagnostic.Primary != nil {
|
||||
locations = append(locations, *diagnostic.Primary)
|
||||
}
|
||||
wantLocations := map[string]Location{
|
||||
canonicalTestPath(t, zBad): {Line: 2, Column: 5},
|
||||
canonicalTestPath(t, helper): {Line: 1, Column: 5},
|
||||
}
|
||||
gotPaths := make(map[string]bool, len(locations))
|
||||
for _, location := range locations {
|
||||
want := wantLocations[files[location.SourceID]]
|
||||
gotPaths[files[location.SourceID]] = location.Line == want.Line && location.Column == want.Column
|
||||
}
|
||||
if len(locations) != 2 || !gotPaths[canonicalTestPath(t, zBad)] || !gotPaths[canonicalTestPath(t, helper)] {
|
||||
t.Fatalf("group item helper conflict sites = %#v; source paths = %#v", locations, files)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupedLoadPrecedenceSourceOrderAndSingleLifecycle(t *testing.T) {
|
||||
filesystem := NewFilesystem("bundle", fstest.MapFS{
|
||||
"main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: imported.yml}]}\nversion: v1\nport: 5\nsettings: {shared: main}\n")},
|
||||
|
||||
Reference in New Issue
Block a user