package build_test import ( "fmt" "slices" "strings" "testing" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/execute/build" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/execute/tsctests" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tsoptions" "gotest.tools/v3/assert" ) func TestBuildOrderGenerator(t *testing.T) { t.Parallel() testCases := []*buildOrderTestCase{ {"specify two roots", []string{"A", "G"}, []string{"D", "E", "C", "B", "A", "G"}, false}, {"multiple parts of the same graph in various orders", []string{"A"}, []string{"D", "E", "C", "B", "A"}, false}, {"multiple parts of the same graph in various orders", []string{"A", "C", "D"}, []string{"D", "E", "C", "B", "A"}, false}, {"multiple parts of the same graph in various orders", []string{"D", "C", "A"}, []string{"D", "E", "C", "B", "A"}, false}, {"other orderings", []string{"F"}, []string{"E", "F"}, false}, {"other orderings", []string{"E"}, []string{"E"}, false}, {"other orderings", []string{"F", "C", "A"}, []string{"E", "F", "D", "C", "B", "A"}, false}, {"returns circular order", []string{"H"}, []string{"E", "J", "I", "H"}, true}, {"returns circular order", []string{"A", "H"}, []string{"D", "E", "C", "B", "A", "J", "I", "H"}, true}, } for _, testcase := range testCases { testcase.run(t) } } type buildOrderTestCase struct { name string projects []string expected []string circular bool } func (b *buildOrderTestCase) configName(project string) string { return fmt.Sprintf("/home/src/workspaces/project/%s/tsconfig.json", project) } func (b *buildOrderTestCase) projectName(config string) string { str := strings.TrimPrefix(config, "/home/src/workspaces/project/") str = strings.TrimSuffix(str, "/tsconfig.json") return str } func (b *buildOrderTestCase) run(t *testing.T) { t.Helper() t.Run(b.name+" - "+strings.Join(b.projects, ","), func(t *testing.T) { t.Parallel() files := make(map[string]any) deps := map[string][]string{ "A": {"B", "C"}, "B": {"C", "D"}, "C": {"D", "E"}, "F": {"E"}, "H": {"I"}, "I": {"J"}, "J": {"H", "E"}, } reverseDeps := map[string][]string{} for project, deps := range deps { for _, dep := range deps { reverseDeps[dep] = append(reverseDeps[dep], project) } } verifyDeps := func(orchestrator *build.Orchestrator, buildOrder []string, hasDownStream bool) { for index, project := range buildOrder { upstream := core.Map(orchestrator.Upstream(b.configName(project)), b.projectName) expectedUpstream := deps[project] assert.Assert(t, len(upstream) <= len(expectedUpstream), fmt.Sprintf("Expected upstream for %s to be at most %d, got %d", project, len(expectedUpstream), len(upstream))) for _, expected := range expectedUpstream { if slices.Contains(buildOrder[:index], expected) { assert.Assert(t, slices.Contains(upstream, expected), fmt.Sprintf("Expected upstream for %s to contain %s", project, expected)) } else { assert.Assert(t, !slices.Contains(upstream, expected), fmt.Sprintf("Expected upstream for %s to not contain %s", project, expected)) } } downstream := core.Map(orchestrator.Downstream(b.configName(project)), b.projectName) expectedDownstream := core.IfElse(hasDownStream, reverseDeps[project], nil) assert.Assert(t, len(downstream) <= len(expectedDownstream), fmt.Sprintf("Expected downstream for %s to be at most %d, got %d", project, len(expectedDownstream), len(downstream))) for _, expected := range expectedDownstream { if slices.Contains(buildOrder[index+1:], expected) { assert.Assert(t, slices.Contains(downstream, expected), fmt.Sprintf("Expected downstream for %s to contain %s", project, expected)) } else { assert.Assert(t, !slices.Contains(downstream, expected), fmt.Sprintf("Expected downstream for %s to not contain %s", project, expected)) } } } } for _, project := range []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J"} { files[fmt.Sprintf("/home/src/workspaces/project/%s/%s.ts", project, project)] = "export {}" referencesStr := "" if deps, ok := deps[project]; ok { referencesStr = fmt.Sprintf(`, "references": [%s]`, strings.Join(core.Map(deps, func(dep string) string { return fmt.Sprintf(`{ "path": "../%s" }`, dep) }), ",")) } files[b.configName(project)] = fmt.Sprintf(`{ "compilerOptions": { "composite": true }, "files": ["./%s.ts"], %s }`, project, referencesStr) } sys := tsctests.NewTscSystem(files, true, "/home/src/workspaces/project") args := append([]string{"--build", "--dry"}, b.projects...) buildCommand := tsoptions.ParseBuildCommandLine(args, sys) orchestrator := build.NewOrchestrator(build.Options{ Sys: sys, Command: buildCommand, }) orchestrator.GenerateGraph(nil) buildOrder := core.Map(orchestrator.Order(), b.projectName) assert.DeepEqual(t, buildOrder, b.expected) verifyDeps(orchestrator, buildOrder, false) if !b.circular { for project, projectDeps := range deps { child := b.configName(project) childIndex := slices.Index(buildOrder, child) if childIndex == -1 { continue } for _, dep := range projectDeps { parent := b.configName(dep) parentIndex := slices.Index(buildOrder, parent) assert.Assert(t, childIndex > parentIndex, fmt.Sprintf("Expecting child %s to be built after parent %s", project, dep)) } } } orchestrator.GenerateGraphReusingOldTasks() buildOrder2 := core.Map(orchestrator.Order(), b.projectName) assert.DeepEqual(t, buildOrder2, b.expected) argsWatch := append([]string{"--build", "--watch"}, b.projects...) buildCommandWatch := tsoptions.ParseBuildCommandLine(argsWatch, sys) orchestrator = build.NewOrchestrator(build.Options{ Sys: sys, Command: buildCommandWatch, }) orchestrator.GenerateGraph(nil) buildOrder3 := core.Map(orchestrator.Order(), b.projectName) verifyDeps(orchestrator, buildOrder3, true) }) }