Schedule ordinary import contributions by dependency

This commit is contained in:
2026-10-19 19:29:35 +00:00
parent 91ab37a39d
commit 5ed0f55970
7 changed files with 159 additions and 13 deletions

View File

@@ -148,6 +148,42 @@ func (graph importInclusionGraph) scheduleCycle(closingEdge int, location Locati
}, true
}
func scheduleOrdinaryContributions(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 || (contribution.role != importRoleMain && contribution.role != importRoleMapping) {
continue
}
positions[contribution.identity] = len(keys)
slots = append(slots, index)
keys = append(keys, contribution.identity.dependencyKey())
}
if len(keys) < 2 {
return scope
}
edges := make([]dependencyEdge, 0, len(graph.edges))
for _, edge := range graph.edges {
importer, importerOK := positions[edge.importer]
imported, importedOK := positions[edge.imported]
if importerOK && importedOK {
edges = append(edges, dependencyEdge{prerequisite: imported, dependent: importer})
}
}
order, _ := scheduleDependencies(keys, edges)
if len(order) != len(keys) {
return scope
}
scheduled := scope
scheduled.contributions = append([]acquiredContribution(nil), scope.contributions...)
for position, scheduledIndex := range order {
scheduled.contributions[slots[position]] = scope.contributions[slots[scheduledIndex]]
}
return scheduled
}
func importKeyFor(access *filesystemAccess, canonical string, synthetic bool) importNodeKey {
kind := "host"
resourceKind := "file"

View File

@@ -220,6 +220,9 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
}
}
if len(diagnostics) == 0 {
if !mainIsItem {
state.scope = scheduleOrdinaryContributions(state.scope, state.importGraph)
}
diagnostics = state.composePendingItems(&state.scope)
}
@@ -256,7 +259,7 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
protectionRule = &scoped
}
}
if diagnostic := importedVersionClaim(documents, completed, protectionRule, 0); diagnostic != nil {
if diagnostic := importedVersionClaim(documents, localScope, completed, protectionRule); diagnostic != nil {
if rule != nil {
diagnostic.Path = rule.Path.clone()
}
@@ -297,7 +300,7 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
}
composed, diagnostics := composeDocumentsWithPolicy(policy, documents...)
if composed != nil {
if diagnostic := importedVersionClaim(documents, composed, rule, 0); diagnostic != nil {
if diagnostic := importedVersionClaim(documents, state.scope, composed, rule); diagnostic != nil {
return nil, cloneSourceInfos(state.sources), []Diagnostic{*diagnostic}
}
}
@@ -618,10 +621,17 @@ func (s *importTraversal) composePendingItems(scope *acquisitionScope) []Diagnos
return nil
}
func importedVersionClaim(documents []*document.Node, composed *composedDocument, rule *VersionRule, mainIndex int) *Diagnostic {
func importedVersionClaim(documents []*document.Node, scope acquisitionScope, composed *composedDocument, rule *VersionRule) *Diagnostic {
if composed == nil || rule == nil {
return nil
}
mainIndex := -1
for index, contribution := range scope.contributions {
if contribution.role == importRoleMain {
mainIndex = index
break
}
}
ranges := indexSequenceRanges(composed.sequenceRanges)
for index, contribution := range documents {
if index == mainIndex {

View File

@@ -91,6 +91,80 @@ settings:
}
}
func TestLoadOrdinaryImportsUseDependencyReadyCompositionOrder(t *testing.T) {
type task struct {
ID string `yaml:"id"`
}
type config struct {
Tasks []task `yaml:"tasks"`
}
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
imports:
- path: z-parent.yml
- path: a-independent.yml
tasks: [{id: main}]
`)
zParentPath := writeImportFile(t, directory, "z-parent.yml", `configkit:
imports:
- path: b-dependency.yml
tasks: [{id: z-parent}]
`)
aIndependentPath := writeImportFile(t, directory, "a-independent.yml", "tasks: [{id: a-independent}]\n")
bDependencyPath := writeImportFile(t, directory, "b-dependency.yml", "tasks: [{id: b-dependency}]\n")
input, diagnostics := assembleBaseInput[config](File(mainPath), Options[config]{
Imports: true,
sequencePolicy: sequencePolicy{identityFields: map[string]string{"/tasks": "id"}},
})
if len(diagnostics) != 0 {
t.Fatalf("assemble imports: %#v", diagnostics)
}
tasks, ok := documentAtPath(input.document, Keys("tasks"))
if !ok || tasks.Kind != document.Sequence {
t.Fatalf("tasks document = %#v, %v", tasks, ok)
}
wantOrder := []string{"a-independent", "b-dependency", "z-parent", "main"}
if len(tasks.Sequence) != len(wantOrder) {
t.Fatalf("tasks = %#v", tasks.Sequence)
}
for index, want := range wantOrder {
id, found := documentAtPath(tasks, Path{}.Index(index).Key("id"))
if !found || id.Text != want {
t.Errorf("task %d ID = %#v, want %q", index, id, want)
}
}
wantAcquisition := []string{
canonicalTestPath(t, mainPath), canonicalTestPath(t, zParentPath),
canonicalTestPath(t, bDependencyPath), canonicalTestPath(t, aIndependentPath),
}
for index, want := range wantAcquisition {
if got := input.sources[index].Path; got != want {
t.Errorf("acquired source %d = %q, want %q", index, got, want)
}
}
}
func TestAcquiredContextsUseSelectedFileWhenSourceRecordsAreReordered(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "settings: {root: main}\n")
importedPath := writeImportFile(t, directory, "nested/imported.yml", "settings: {nested: imported}\n")
sources := []SourceInfo{
{ID: "source-2", Path: canonicalTestPath(t, importedPath), Kind: SourceKindFile},
{ID: "source-1", Path: canonicalTestPath(t, mainPath), Kind: SourceKindFile},
}
contexts, diagnostic := contextsForAcquiredSources(File(mainPath), nil, nil, sources)
if diagnostic != nil {
t.Fatalf("source contexts: %#v", diagnostic)
}
if got := contexts["source-1"].BaseDir; got != "." {
t.Errorf("main source base = %q, want root-relative current directory", got)
}
if got := contexts["source-2"].BaseDir; got != "nested" {
t.Errorf("imported source base = %q, want nested", got)
}
}
func TestLoadImportsFromProfilesDirectoryAsBaseInput(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: profiles/shared.yml}]}\n")

View File

@@ -161,7 +161,7 @@ func TestRegisteredSequencePolicyAcrossLoadingLifecycle(t *testing.T) {
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: fragment.yml}]}\nitems: [{id: authored}]\n")
writeImportFile(t, directory, "fragment.yml", "items: [{id: imported}]\n")
result, err := testLoad[config](File(mainPath), Options[config]{Imports: true, Definition: definition})
if err != nil || len(result.Config.Items) != 2 || result.Config.Items[1].ID != "imported" {
if err != nil || len(result.Config.Items) != 2 || result.Config.Items[0].ID != "imported" || result.Config.Items[1].ID != "authored" {
t.Fatalf("registered imported list = %#v, %v", result, err)
}
})

View File

@@ -23,9 +23,16 @@ func contextsForAcquiredSources(source Source, context *FilesystemContext, baseS
filesystem = context.FS.namespace.filesystem
rootPath, basePath = context.Root, context.BaseDir
} else if source.kind == sourceFile {
filePath := sources[0].Path
filePath := source.path
if baseSource != nil {
filePath = baseSource.Path
} else {
canonical, err := canonicalExistingPath(filePath)
if err != nil {
value := importDiagnostic(CodeImportInvalid, "selected source could not be canonicalized", nil)
return nil, &value
}
filePath = canonical
}
basePath = filepath.Dir(filePath)
rootPath = basePath