Schedule underlay groups as dependency forests
This commit is contained in:
@@ -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