diff options
| author | 2023-04-28 01:36:44 +0800 | |
|---|---|---|
| committer | 2023-04-28 01:36:44 +0800 | |
| commit | dd84b9d64fb98746a230cd24233ff50a562c39c9 (patch) | |
| tree | b583261ef00b3afe72ec4d6dacb31e57779a6faf /cli/internal/scope/scope_test.go | |
| parent | 0b46fcd72ac34382387b2bcf9095233efbcc52f4 (diff) | |
| download | HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.tar.gz HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.zip | |
Diffstat (limited to 'cli/internal/scope/scope_test.go')
| -rw-r--r-- | cli/internal/scope/scope_test.go | 550 |
1 files changed, 550 insertions, 0 deletions
diff --git a/cli/internal/scope/scope_test.go b/cli/internal/scope/scope_test.go new file mode 100644 index 0000000..216984d --- /dev/null +++ b/cli/internal/scope/scope_test.go @@ -0,0 +1,550 @@ +package scope + +import ( + "fmt" + "io" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/hashicorp/go-hclog" + "github.com/pyr-sh/dag" + "github.com/vercel/turbo/cli/internal/context" + "github.com/vercel/turbo/cli/internal/fs" + "github.com/vercel/turbo/cli/internal/lockfile" + "github.com/vercel/turbo/cli/internal/packagemanager" + "github.com/vercel/turbo/cli/internal/turbopath" + "github.com/vercel/turbo/cli/internal/ui" + "github.com/vercel/turbo/cli/internal/util" + "github.com/vercel/turbo/cli/internal/workspace" +) + +type mockSCM struct { + changed []string + contents map[string][]byte +} + +func (m *mockSCM) ChangedFiles(_fromCommit string, _toCommit string, _relativeTo string) ([]string, error) { + return m.changed, nil +} + +func (m *mockSCM) PreviousContent(fromCommit string, filePath string) ([]byte, error) { + contents, ok := m.contents[filePath] + if !ok { + return nil, fmt.Errorf("No contents found") + } + return contents, nil +} + +type mockLockfile struct { + globalChange bool + versions map[string]string + allDeps map[string]map[string]string +} + +func (m *mockLockfile) ResolvePackage(workspacePath turbopath.AnchoredUnixPath, name string, version string) (lockfile.Package, error) { + resolvedVersion, ok := m.versions[name] + if ok { + key := fmt.Sprintf("%s%s", name, version) + return lockfile.Package{Key: key, Version: resolvedVersion, Found: true}, nil + } + return lockfile.Package{Found: false}, nil +} + +func (m *mockLockfile) AllDependencies(key string) (map[string]string, bool) { + deps, ok := m.allDeps[key] + return deps, ok +} + +func (m *mockLockfile) Encode(w io.Writer) error { + return nil +} + +func (m *mockLockfile) GlobalChange(other lockfile.Lockfile) bool { + return m.globalChange || (other != nil && other.(*mockLockfile).globalChange) +} + +func (m *mockLockfile) Patches() []turbopath.AnchoredUnixPath { + return nil +} + +func (m *mockLockfile) Subgraph(workspaces []turbopath.AnchoredSystemPath, packages []string) (lockfile.Lockfile, error) { + return nil, nil +} + +var _ (lockfile.Lockfile) = (*mockLockfile)(nil) + +func TestResolvePackages(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("cwd: %v", err) + } + root, err := fs.GetCwd(cwd) + if err != nil { + t.Fatalf("cwd: %v", err) + } + tui := ui.Default() + logger := hclog.Default() + // Dependency graph: + // + // app0 - + // \ + // app1 -> libA + // \ + // > libB -> libD + // / + // app2 < + // \ + // > libC + // / + // app2-a < + // + // Filesystem layout: + // + // app/ + // app0 + // app1 + // app2 + // app2-a + // libs/ + // libA + // libB + // libC + // libD + graph := dag.AcyclicGraph{} + graph.Add("app0") + graph.Add("app1") + graph.Add("app2") + graph.Add("app2-a") + graph.Add("libA") + graph.Add("libB") + graph.Add("libC") + graph.Add("libD") + graph.Connect(dag.BasicEdge("libA", "libB")) + graph.Connect(dag.BasicEdge("libB", "libD")) + graph.Connect(dag.BasicEdge("app0", "libA")) + graph.Connect(dag.BasicEdge("app1", "libA")) + graph.Connect(dag.BasicEdge("app2", "libB")) + graph.Connect(dag.BasicEdge("app2", "libC")) + graph.Connect(dag.BasicEdge("app2-a", "libC")) + workspaceInfos := workspace.Catalog{ + PackageJSONs: map[string]*fs.PackageJSON{ + "//": { + Dir: turbopath.AnchoredSystemPath("").ToSystemPath(), + UnresolvedExternalDeps: map[string]string{"global": "2"}, + TransitiveDeps: []lockfile.Package{{Key: "global2", Version: "2", Found: true}}, + }, + "app0": { + Dir: turbopath.AnchoredUnixPath("app/app0").ToSystemPath(), + Name: "app0", + UnresolvedExternalDeps: map[string]string{"app0-dep": "2"}, + TransitiveDeps: []lockfile.Package{ + {Key: "app0-dep2", Version: "2", Found: true}, + {Key: "app0-util2", Version: "2", Found: true}, + }, + }, + "app1": { + Dir: turbopath.AnchoredUnixPath("app/app1").ToSystemPath(), + Name: "app1", + }, + "app2": { + Dir: turbopath.AnchoredUnixPath("app/app2").ToSystemPath(), + Name: "app2", + }, + "app2-a": { + Dir: turbopath.AnchoredUnixPath("app/app2-a").ToSystemPath(), + Name: "app2-a", + }, + "libA": { + Dir: turbopath.AnchoredUnixPath("libs/libA").ToSystemPath(), + Name: "libA", + }, + "libB": { + Dir: turbopath.AnchoredUnixPath("libs/libB").ToSystemPath(), + Name: "libB", + UnresolvedExternalDeps: map[string]string{"external": "1"}, + TransitiveDeps: []lockfile.Package{ + {Key: "external-dep-a1", Version: "1", Found: true}, + {Key: "external-dep-b1", Version: "1", Found: true}, + {Key: "external1", Version: "1", Found: true}, + }, + }, + "libC": { + Dir: turbopath.AnchoredUnixPath("libs/libC").ToSystemPath(), + Name: "libC", + }, + "libD": { + Dir: turbopath.AnchoredUnixPath("libs/libD").ToSystemPath(), + Name: "libD", + }, + }, + } + packageNames := []string{} + for name := range workspaceInfos.PackageJSONs { + packageNames = append(packageNames, name) + } + + // global -> globalDep + // app0-dep -> app0-dep :) + + makeLockfile := func(f func(*mockLockfile)) *mockLockfile { + l := mockLockfile{ + globalChange: false, + versions: map[string]string{ + "global": "2", + "app0-dep": "2", + "app0-util": "2", + "external": "1", + "external-dep-a": "1", + "external-dep-b": "1", + }, + allDeps: map[string]map[string]string{ + "global2": map[string]string{}, + "app0-dep2": map[string]string{ + "app0-util": "2", + }, + "app0-util2": map[string]string{}, + "external1": map[string]string{ + "external-dep-a": "1", + "external-dep-b": "1", + }, + "external-dep-a1": map[string]string{}, + "external-dep-b1": map[string]string{}, + }, + } + if f != nil { + f(&l) + } + return &l + } + + testCases := []struct { + name string + changed []string + expected []string + expectAllPackages bool + scope []string + since string + ignore string + globalDeps []string + includeDependencies bool + includeDependents bool + lockfile string + currLockfile *mockLockfile + prevLockfile *mockLockfile + inferPkgPath string + }{ + { + name: "Just scope and dependencies", + changed: []string{}, + includeDependencies: true, + scope: []string{"app2"}, + expected: []string{"app2", "libB", "libC", "libD"}, + }, + { + name: "Only turbo.json changed", + changed: []string{"turbo.json"}, + expected: []string{"//", "app0", "app1", "app2", "app2-a", "libA", "libB", "libC", "libD"}, + since: "dummy", + includeDependencies: true, + }, + { + name: "Only root package.json changed", + changed: []string{"package.json"}, + expected: []string{"//", "app0", "app1", "app2", "app2-a", "libA", "libB", "libC", "libD"}, + since: "dummy", + includeDependencies: true, + }, + { + name: "Only package-lock.json changed", + changed: []string{"package-lock.json"}, + expected: []string{"//", "app0", "app1", "app2", "app2-a", "libA", "libB", "libC", "libD"}, + since: "dummy", + includeDependencies: true, + lockfile: "package-lock.json", + }, + { + name: "Only yarn.lock changed", + changed: []string{"yarn.lock"}, + expected: []string{"//", "app0", "app1", "app2", "app2-a", "libA", "libB", "libC", "libD"}, + since: "dummy", + includeDependencies: true, + lockfile: "yarn.lock", + }, + { + name: "Only pnpm-lock.yaml changed", + changed: []string{"pnpm-lock.yaml"}, + expected: []string{"//", "app0", "app1", "app2", "app2-a", "libA", "libB", "libC", "libD"}, + since: "dummy", + includeDependencies: true, + lockfile: "pnpm-lock.yaml", + }, + { + name: "One package changed", + changed: []string{"libs/libB/src/index.ts"}, + expected: []string{"libB"}, + since: "dummy", + }, + { + name: "One package manifest changed", + changed: []string{"libs/libB/package.json"}, + expected: []string{"libB"}, + since: "dummy", + }, + { + name: "An ignored package changed", + changed: []string{"libs/libB/src/index.ts"}, + expected: []string{}, + since: "dummy", + ignore: "libs/libB/**/*.ts", + }, + { + // nothing in scope depends on the change + name: "unrelated library changed", + changed: []string{"libs/libC/src/index.ts"}, + expected: []string{}, + since: "dummy", + scope: []string{"app1"}, + includeDependencies: true, // scope implies include-dependencies + }, + { + // a dependent lib changed, scope implies include-dependencies, + // so all deps of app1 get built + name: "dependency of scope changed", + changed: []string{"libs/libA/src/index.ts"}, + expected: []string{"libA", "libB", "libD", "app1"}, + since: "dummy", + scope: []string{"app1"}, + includeDependencies: true, // scope implies include-dependencies + }, + { + // a dependent lib changed, user explicitly asked to not build dependencies. + // Since the package matching the scope had a changed dependency, we run it. + // We don't include its dependencies because the user asked for no dependencies. + // note: this is not yet supported by the CLI, as you cannot specify --include-dependencies=false + name: "dependency of scope changed, user asked to not include depedencies", + changed: []string{"libs/libA/src/index.ts"}, + expected: []string{"app1"}, + since: "dummy", + scope: []string{"app1"}, + includeDependencies: false, + }, + { + // a nested dependent lib changed, user explicitly asked to not build dependencies + // note: this is not yet supported by the CLI, as you cannot specify --include-dependencies=false + name: "nested dependency of scope changed, user asked to not include dependencies", + changed: []string{"libs/libB/src/index.ts"}, + expected: []string{"app1"}, + since: "dummy", + scope: []string{"app1"}, + includeDependencies: false, + }, + { + name: "global dependency changed, even though it was ignored, forcing a build of everything", + changed: []string{"libs/libB/src/index.ts"}, + expected: []string{"//", "app0", "app1", "app2", "app2-a", "libA", "libB", "libC", "libD"}, + since: "dummy", + ignore: "libs/libB/**/*.ts", + globalDeps: []string{"libs/**/*.ts"}, + }, + { + name: "an app changed, user asked for dependencies to build", + changed: []string{"app/app2/src/index.ts"}, + since: "dummy", + includeDependencies: true, + expected: []string{"app2", "libB", "libC", "libD"}, + }, + { + name: "a library changed, user asked for dependents to be built", + changed: []string{"libs/libB"}, + since: "dummy", + includeDependents: true, + expected: []string{"app0", "app1", "app2", "libA", "libB"}, + }, + { + // no changes, no base to compare against, defaults to everything + name: "no changes or scope specified, build everything", + since: "", + expected: []string{"//", "app0", "app1", "app2", "app2-a", "libA", "libB", "libC", "libD"}, + expectAllPackages: true, + }, + { + // a dependent library changed, no deps beyond the scope are build + // "libB" is still built because it is a dependent within the scope, but libB's dependents + // are skipped + name: "a dependent library changed, build up to scope", + changed: []string{"libs/libD/src/index.ts"}, + since: "dummy", + scope: []string{"libB"}, + expected: []string{"libB", "libD"}, + includeDependencies: true, // scope implies include-dependencies + }, + { + name: "library change, no scope", + changed: []string{"libs/libA/src/index.ts"}, + expected: []string{"libA", "app0", "app1"}, + includeDependents: true, + since: "dummy", + }, + { + // make sure multiple apps with the same prefix are handled separately. + // prevents this issue: https://github.com/vercel/turbo/issues/1528 + name: "Two apps with an overlapping prefix changed", + changed: []string{"app/app2/src/index.js", "app/app2-a/src/index.js"}, + expected: []string{"app2", "app2-a"}, + since: "dummy", + }, + { + name: "Global lockfile change invalidates all packages", + changed: []string{"dummy.lock"}, + expected: []string{"//", "app0", "app1", "app2", "app2-a", "libA", "libB", "libC", "libD"}, + lockfile: "dummy.lock", + currLockfile: makeLockfile(nil), + prevLockfile: makeLockfile(func(ml *mockLockfile) { + ml.globalChange = true + }), + since: "dummy", + }, + { + name: "Dependency of workspace root change invalidates all packages", + changed: []string{"dummy.lock"}, + expected: []string{"//", "app0", "app1", "app2", "app2-a", "libA", "libB", "libC", "libD"}, + lockfile: "dummy.lock", + currLockfile: makeLockfile(nil), + prevLockfile: makeLockfile(func(ml *mockLockfile) { + ml.versions["global"] = "3" + ml.allDeps["global3"] = map[string]string{} + }), + since: "dummy", + }, + { + name: "Version change invalidates package", + changed: []string{"dummy.lock"}, + expected: []string{"//", "app0"}, + lockfile: "dummy.lock", + currLockfile: makeLockfile(nil), + prevLockfile: makeLockfile(func(ml *mockLockfile) { + ml.versions["app0-util"] = "3" + ml.allDeps["app0-dep2"] = map[string]string{"app0-util": "3"} + ml.allDeps["app0-util3"] = map[string]string{} + }), + since: "dummy", + }, + { + name: "Transitive dep invalidates package", + changed: []string{"dummy.lock"}, + expected: []string{"//", "libB"}, + lockfile: "dummy.lock", + currLockfile: makeLockfile(nil), + prevLockfile: makeLockfile(func(ml *mockLockfile) { + ml.versions["external-dep-a"] = "2" + ml.allDeps["external1"] = map[string]string{"external-dep-a": "2", "external-dep-b": "1"} + ml.allDeps["external-dep-a2"] = map[string]string{} + }), + since: "dummy", + }, + { + name: "Transitive dep invalidates package and dependents", + changed: []string{"dummy.lock"}, + expected: []string{"//", "app0", "app1", "app2", "libA", "libB"}, + lockfile: "dummy.lock", + includeDependents: true, + currLockfile: makeLockfile(nil), + prevLockfile: makeLockfile(func(ml *mockLockfile) { + ml.versions["external-dep-a"] = "2" + ml.allDeps["external1"] = map[string]string{"external-dep-a": "2", "external-dep-b": "1"} + ml.allDeps["external-dep-a2"] = map[string]string{} + }), + since: "dummy", + }, + { + name: "Infer app2 from directory", + inferPkgPath: "app/app2", + expected: []string{"app2"}, + }, + { + name: "Infer app2 from a subdirectory", + inferPkgPath: "app/app2/src", + expected: []string{"app2"}, + }, + { + name: "Infer from a directory with no packages", + inferPkgPath: "wrong", + expected: []string{}, + }, + { + name: "Infer from a parent directory", + inferPkgPath: "app", + expected: []string{"app0", "app1", "app2", "app2-a"}, + }, + { + name: "library change, no scope, inferred libs", + changed: []string{"libs/libA/src/index.ts"}, + expected: []string{"libA"}, + since: "dummy", + inferPkgPath: "libs", + }, + { + name: "library change, no scope, inferred app", + changed: []string{"libs/libA/src/index.ts"}, + expected: []string{}, + since: "dummy", + inferPkgPath: "app", + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("test #%v %v", i, tc.name), func(t *testing.T) { + // Convert test data to system separators. + systemSeparatorChanged := make([]string, len(tc.changed)) + for index, path := range tc.changed { + systemSeparatorChanged[index] = filepath.FromSlash(path) + } + scm := &mockSCM{ + changed: systemSeparatorChanged, + contents: make(map[string][]byte, len(systemSeparatorChanged)), + } + for _, path := range systemSeparatorChanged { + scm.contents[path] = nil + } + readLockfile := func(_rootPackageJSON *fs.PackageJSON, content []byte) (lockfile.Lockfile, error) { + return tc.prevLockfile, nil + } + pkgInferenceRoot, err := resolvePackageInferencePath(tc.inferPkgPath) + if err != nil { + t.Errorf("bad inference path (%v): %v", tc.inferPkgPath, err) + } + pkgs, isAllPackages, err := ResolvePackages(&Opts{ + LegacyFilter: LegacyFilter{ + Entrypoints: tc.scope, + Since: tc.since, + IncludeDependencies: tc.includeDependencies, + SkipDependents: !tc.includeDependents, + }, + IgnorePatterns: []string{tc.ignore}, + GlobalDepPatterns: tc.globalDeps, + PackageInferenceRoot: pkgInferenceRoot, + }, root, scm, &context.Context{ + WorkspaceInfos: workspaceInfos, + WorkspaceNames: packageNames, + PackageManager: &packagemanager.PackageManager{Lockfile: tc.lockfile, UnmarshalLockfile: readLockfile}, + WorkspaceGraph: graph, + RootNode: "root", + Lockfile: tc.currLockfile, + }, tui, logger) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + expected := make(util.Set) + for _, pkg := range tc.expected { + expected.Add(pkg) + } + if !reflect.DeepEqual(pkgs, expected) { + t.Errorf("ResolvePackages got %v, want %v", pkgs, expected) + } + if isAllPackages != tc.expectAllPackages { + t.Errorf("isAllPackages got %v, want %v", isAllPackages, tc.expectAllPackages) + } + }) + } +} |
