diff --git a/.gitignore b/.gitignore index 90cf10ce9137c..33d4051c62b2e 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ cli/npm/turbo-sunos-64/bin cli/npm/turbo-windows-32/turbo.exe cli/npm/turbo-windows-64/turbo.exe cli/npm/turbo-windows-arm64/turbo.exe +cli/scripts/turbo-* !/npm/turbo-windows-32/bin !/npm/turbo-windows-64/bin !/npm/turbo-install/bin diff --git a/cli/internal/run/run.go b/cli/internal/run/run.go index 33513097cc6bb..8c5eef596adaa 100644 --- a/cli/internal/run/run.go +++ b/cli/internal/run/run.go @@ -24,11 +24,11 @@ import ( "turbo/internal/ui" "turbo/internal/util" "turbo/internal/util/browser" + "turbo/internal/util/filter" "github.com/pyr-sh/dag" "github.com/fatih/color" - glob "github.com/gobwas/glob" "github.com/hashicorp/go-hclog" "github.com/mitchellh/cli" "github.com/pkg/errors" @@ -139,14 +139,14 @@ func (c *RunCommand) Run(args []string) int { c.logWarning(c.Config.Logger, "", err) } - ignoreGlobs, err := convertStringsToGlobs(runOptions.ignore) + ignoreGlob, err := filter.Compile(runOptions.ignore) if err != nil { c.logError(c.Config.Logger, "", fmt.Errorf("invalid ignore globs: %w", err)) return 1 } - globalDeps, err := convertStringsToGlobs(runOptions.globalDeps) + globalDepsGlob, err := filter.Compile(runOptions.globalDeps) if err != nil { - c.logError(c.Config.Logger, "", fmt.Errorf("invalid global deps: %w", err)) + c.logError(c.Config.Logger, "", fmt.Errorf("invalid global deps glob: %w", err)) return 1 } hasRepoGlobalFileChanged := false @@ -156,23 +156,23 @@ func (c *RunCommand) Run(args []string) int { } ignoreSet := make(util.Set) - - for _, f := range changedFiles { - for _, g := range globalDeps { - if g.Match(f) { + if globalDepsGlob != nil { + for _, f := range changedFiles { + if globalDepsGlob.Match(f) { hasRepoGlobalFileChanged = true break } } } - for _, f := range changedFiles { - for _, g := range ignoreGlobs { - if g.Match(f) { + if ignoreGlob != nil { + for _, f := range changedFiles { + if ignoreGlob.Match(f) { ignoreSet.Add(f) } } } + filteredChangedFiles := make(util.Set) // Ignore any changed files in the ignore set for _, c := range changedFiles { @@ -197,7 +197,6 @@ func (c *RunCommand) Run(args []string) int { // Scoped packages // Unwind scope globs scopePkgs, err := getScopedPackages(ctx, runOptions.scope) - if err != nil { c.logError(c.Config.Logger, "", fmt.Errorf("Invalid scope: %w", err)) return 1 @@ -229,37 +228,40 @@ func (c *RunCommand) Run(args []string) int { c.Ui.Output(fmt.Sprintf(ui.Dim("• Packages changed since %s: %s"), runOptions.since, strings.Join(filteredPkgs.UnsafeListOfStrings(), ", "))) } else if scopePkgs.Len() > 0 { filteredPkgs = scopePkgs - c.Ui.Output(fmt.Sprintf(ui.Dim("• Packages in scope: %v"), strings.Join(scopePkgs.UnsafeListOfStrings(), ", "))) } else { for _, f := range ctx.PackageNames { filteredPkgs.Add(f) } } - if runOptions.deps { + if runOptions.includeDependents { // perf??? this is duplicative from the step above - for _, changed := range filteredPkgs { - descenders, err := ctx.TopologicalGraph.Descendents(changed) + for _, pkg := range filteredPkgs { + descenders, err := ctx.TopologicalGraph.Descendents(pkg) if err != nil { c.logError(c.Config.Logger, "", fmt.Errorf("error calculating affected packages: %w", err)) return 1 } - // filteredPkgs.Add(changed) + c.Config.Logger.Debug("dependents", "pkg", pkg, "value", descenders.List()) for _, d := range descenders { - filteredPkgs.Add(d) + // we need to exlcude the fake root node + // since it is not a real package + if d != ctx.RootNode { + filteredPkgs.Add(d) + } } } c.Config.Logger.Debug("running with dependents") } - if runOptions.ancestors { - for _, changed := range filteredPkgs { - ancestors, err := ctx.TopologicalGraph.Ancestors(changed) + if runOptions.includeDependencies { + for _, pkg := range filteredPkgs { + ancestors, err := ctx.TopologicalGraph.Ancestors(pkg) if err != nil { log.Printf("error getting dependency %v", err) return 1 } - c.Config.Logger.Debug("dependencies", ancestors) + c.Config.Logger.Debug("dependencies", "pkg", pkg, "value", ancestors.List()) for _, d := range ancestors { // we need to exlcude the fake root node // since it is not a real package @@ -270,9 +272,10 @@ func (c *RunCommand) Run(args []string) int { } c.Config.Logger.Debug(ui.Dim("running with dependencies")) } - c.Config.Logger.Debug("execution scope", "packages", strings.Join(filteredPkgs.UnsafeListOfStrings(), ", ")) c.Config.Logger.Debug("global hash", "value", ctx.GlobalHash) - + packagesInScope := filteredPkgs.UnsafeListOfStrings() + sort.Strings(packagesInScope) + c.Ui.Output(fmt.Sprintf(ui.Dim("• Packages in scope: %v"), strings.Join(packagesInScope, ", "))) c.Config.Logger.Debug("local cache folder", "path", runOptions.cacheFolder) fs.EnsureDir(runOptions.cacheFolder) turboCache := cache.New(c.Config) @@ -682,9 +685,9 @@ func (c *RunCommand) Run(args []string) int { type RunOptions struct { // Whether to include dependent impacted consumers in execution (defaults to true) - deps bool - // Whether to include ancestors (pkg.dependencies) in execution (defaults to false) - ancestors bool + includeDependents bool + // Whether to include includeDependencies (pkg.dependencies) in execution (defaults to false) + includeDependencies bool // List of globs of file paths to ignore from exection scope calculation ignore []string // Whether to stream log outputs @@ -720,17 +723,17 @@ type RunOptions struct { func getDefaultRunOptions() *RunOptions { return &RunOptions{ - bail: true, - deps: true, - parallel: false, - concurrency: 10, - dotGraph: "", - ancestors: false, - cache: true, - profile: "", // empty string does no tracing - forceExecution: false, - stream: true, - only: false, + bail: true, + includeDependents: true, + parallel: false, + concurrency: 10, + dotGraph: "", + includeDependencies: false, + cache: true, + profile: "", // empty string does no tracing + forceExecution: false, + stream: true, + only: false, } } @@ -750,23 +753,23 @@ func parseRunArgs(args []string, cwd string) (*RunOptions, error) { } else if strings.HasPrefix(arg, "--") { switch { case strings.HasPrefix(arg, "--since="): - if len(arg[len("--since="):]) > 1 { + if len(arg[len("--since="):]) > 0 { runOptions.since = arg[len("--since="):] } case strings.HasPrefix(arg, "--scope="): - if len(arg[len("--scope="):]) > 1 { + if len(arg[len("--scope="):]) > 0 { runOptions.scope = append(runOptions.scope, arg[len("--scope="):]) } case strings.HasPrefix(arg, "--ignore="): - if len(arg[len("--ignore="):]) > 1 { + if len(arg[len("--ignore="):]) > 0 { runOptions.ignore = append(runOptions.ignore, arg[len("--ignore="):]) } case strings.HasPrefix(arg, "--global-deps="): - if len(arg[len("--global-deps="):]) > 1 { + if len(arg[len("--global-deps="):]) > 0 { runOptions.globalDeps = append(runOptions.globalDeps, arg[len("--global-deps="):]) } case strings.HasPrefix(arg, "--cwd="): - if len(arg[len("--cwd="):]) > 1 { + if len(arg[len("--cwd="):]) > 0 { runOptions.cwd = arg[len("--cwd="):] } else { runOptions.cwd = cwd @@ -774,14 +777,14 @@ func parseRunArgs(args []string, cwd string) (*RunOptions, error) { case strings.HasPrefix(arg, "--parallel"): runOptions.parallel = true case strings.HasPrefix(arg, "--profile="): // this one must com before the next - if len(arg[len("--profile="):]) > 1 { + if len(arg[len("--profile="):]) > 0 { runOptions.profile = arg[len("--profile="):] } case strings.HasPrefix(arg, "--profile"): runOptions.profile = fmt.Sprintf("%v-profile.json", time.Now().UnixNano()) case strings.HasPrefix(arg, "--no-deps"): - runOptions.deps = false + runOptions.includeDependents = false case strings.HasPrefix(arg, "--no-cache"): runOptions.cache = true case strings.HasPrefix(arg, "--cacheFolder"): @@ -797,7 +800,7 @@ func parseRunArgs(args []string, cwd string) (*RunOptions, error) { runOptions.stream = true case strings.HasPrefix(arg, "--graph="): // this one must com before the next - if len(arg[len("--graph="):]) > 1 { + if len(arg[len("--graph="):]) > 0 { runOptions.dotGraph = arg[len("--graph="):] } case strings.HasPrefix(arg, "--graph"): @@ -816,8 +819,10 @@ func parseRunArgs(args []string, cwd string) (*RunOptions, error) { } } case strings.HasPrefix(arg, "--includeDependencies"): + log.Printf("[WARNING] The --includeDependencies flag has renamed to --include-dependencies for consistency. Please use `--include-dependencies` instead") + runOptions.includeDependencies = true case strings.HasPrefix(arg, "--include-dependencies"): - runOptions.ancestors = true + runOptions.includeDependencies = true case strings.HasPrefix(arg, "--only"): runOptions.only = true case strings.HasPrefix(arg, "--team"): @@ -845,32 +850,22 @@ func parseRunArgs(args []string, cwd string) (*RunOptions, error) { return runOptions, nil } -// convertStringsToGlobs converts string glob patterns to an array glob.Glob instances. -func convertStringsToGlobs(patterns []string) (globss []glob.Glob, err error) { - var globs = make([]glob.Glob, 0, len(patterns)) - for _, pattern := range patterns { - g, err := glob.Compile(pattern) - if err != nil { - return nil, err - } - globs = append(globs, g) - } - - return globs, nil -} - // getScopedPackages returns a set of package names in scope for a given list of glob patterns func getScopedPackages(ctx *context.Context, scopePatterns []string) (scopePkgs util.Set, err error) { - scopeGlobs, err := convertStringsToGlobs(scopePatterns) if err != nil { return nil, fmt.Errorf("invalid glob pattern %w", err) } var scopedPkgs = make(util.Set) + if len(scopePatterns) == 0 { + return scopePkgs, nil + } + glob, err := filter.Compile(scopePatterns) + if err != nil { + return nil, err + } for _, f := range ctx.PackageNames { - for _, g := range scopeGlobs { - if g.Match(f) { - scopedPkgs.Add(f) - } + if glob.Match(f) { + scopedPkgs.Add(f) } } diff --git a/cli/internal/run/run_test.go b/cli/internal/run/run_test.go index a139f3cc2686c..ffa03a8226d9e 100644 --- a/cli/internal/run/run_test.go +++ b/cli/internal/run/run_test.go @@ -20,116 +20,116 @@ func TestParseConfig(t *testing.T) { "string flags", []string{"foo"}, &RunOptions{ - deps: true, - stream: true, - bail: true, - dotGraph: "", - concurrency: 10, - ancestors: false, - cache: true, - forceExecution: false, - profile: "", - cacheFolder: filepath.FromSlash("node_modules/.cache/turbo"), + includeDependents: true, + stream: true, + bail: true, + dotGraph: "", + concurrency: 10, + includeDependencies: false, + cache: true, + forceExecution: false, + profile: "", + cacheFolder: filepath.FromSlash("node_modules/.cache/turbo"), }, }, { "cwd", []string{"foo", "--cwd=zop"}, &RunOptions{ - deps: true, - stream: true, - bail: true, - dotGraph: "", - concurrency: 10, - ancestors: false, - cache: true, - forceExecution: false, - profile: "", - cwd: "zop", - cacheFolder: filepath.FromSlash("zop/node_modules/.cache/turbo"), + includeDependents: true, + stream: true, + bail: true, + dotGraph: "", + concurrency: 10, + includeDependencies: false, + cache: true, + forceExecution: false, + profile: "", + cwd: "zop", + cacheFolder: filepath.FromSlash("zop/node_modules/.cache/turbo"), }, }, { "scope", []string{"foo", "--scope=foo", "--scope=blah"}, &RunOptions{ - deps: true, - stream: true, - bail: true, - dotGraph: "", - concurrency: 10, - ancestors: false, - cache: true, - forceExecution: false, - profile: "", - scope: []string{"foo", "blah"}, - cacheFolder: filepath.FromSlash("node_modules/.cache/turbo"), + includeDependents: true, + stream: true, + bail: true, + dotGraph: "", + concurrency: 10, + includeDependencies: false, + cache: true, + forceExecution: false, + profile: "", + scope: []string{"foo", "blah"}, + cacheFolder: filepath.FromSlash("node_modules/.cache/turbo"), }, }, { "concurrency", []string{"foo", "--concurrency=12"}, &RunOptions{ - deps: true, - stream: true, - bail: true, - dotGraph: "", - concurrency: 12, - ancestors: false, - cache: true, - forceExecution: false, - profile: "", - cacheFolder: filepath.FromSlash("node_modules/.cache/turbo"), + includeDependents: true, + stream: true, + bail: true, + dotGraph: "", + concurrency: 12, + includeDependencies: false, + cache: true, + forceExecution: false, + profile: "", + cacheFolder: filepath.FromSlash("node_modules/.cache/turbo"), }, }, { "graph", []string{"foo", "--graph=g.png"}, &RunOptions{ - deps: true, - stream: true, - bail: true, - dotGraph: "g.png", - concurrency: 10, - ancestors: false, - cache: true, - forceExecution: false, - profile: "", - cacheFolder: filepath.FromSlash("node_modules/.cache/turbo"), + includeDependents: true, + stream: true, + bail: true, + dotGraph: "g.png", + concurrency: 10, + includeDependencies: false, + cache: true, + forceExecution: false, + profile: "", + cacheFolder: filepath.FromSlash("node_modules/.cache/turbo"), }, }, { "passThroughArgs", []string{"foo", "--graph=g.png", "--", "--boop", "zoop"}, &RunOptions{ - deps: true, - stream: true, - bail: true, - dotGraph: "g.png", - concurrency: 10, - ancestors: false, - cache: true, - forceExecution: false, - profile: "", - cacheFolder: filepath.FromSlash("node_modules/.cache/turbo"), - passThroughArgs: []string{"--boop", "zoop"}, + includeDependents: true, + stream: true, + bail: true, + dotGraph: "g.png", + concurrency: 10, + includeDependencies: false, + cache: true, + forceExecution: false, + profile: "", + cacheFolder: filepath.FromSlash("node_modules/.cache/turbo"), + passThroughArgs: []string{"--boop", "zoop"}, }, }, { "Empty passThroughArgs", []string{"foo", "--graph=g.png", "--"}, &RunOptions{ - deps: true, - stream: true, - bail: true, - dotGraph: "g.png", - concurrency: 10, - ancestors: false, - cache: true, - forceExecution: false, - profile: "", - cacheFolder: filepath.FromSlash("node_modules/.cache/turbo"), - passThroughArgs: []string{}, + includeDependents: true, + stream: true, + bail: true, + dotGraph: "g.png", + concurrency: 10, + includeDependencies: false, + cache: true, + forceExecution: false, + profile: "", + cacheFolder: filepath.FromSlash("node_modules/.cache/turbo"), + passThroughArgs: []string{}, }, }, } diff --git a/cli/internal/util/filter/filter.go b/cli/internal/util/filter/filter.go new file mode 100644 index 0000000000000..143af42b49877 --- /dev/null +++ b/cli/internal/util/filter/filter.go @@ -0,0 +1,134 @@ +// Copyright (c) 2015-2020 InfluxData Inc. MIT License (MIT) +// https://github.com/influxdata/telegraf +package filter + +import ( + "strings" + + "github.com/gobwas/glob" +) + +type Filter interface { + Match(string) bool +} + +// Compile takes a list of string filters and returns a Filter interface +// for matching a given string against the filter list. The filter list +// supports glob matching too, ie: +// +// f, _ := Compile([]string{"cpu", "mem", "net*"}) +// f.Match("cpu") // true +// f.Match("network") // true +// f.Match("memory") // false +// +func Compile(filters []string) (Filter, error) { + // return if there is nothing to compile + if len(filters) == 0 { + return nil, nil + } + + // check if we can compile a non-glob filter + noGlob := true + for _, filter := range filters { + if hasMeta(filter) { + noGlob = false + break + } + } + + switch { + case noGlob: + // return non-globbing filter if not needed. + return compileFilterNoGlob(filters), nil + case len(filters) == 1: + return glob.Compile(filters[0]) + default: + return glob.Compile("{" + strings.Join(filters, ",") + "}") + } +} + +// hasMeta reports whether path contains any magic glob characters. +func hasMeta(s string) bool { + return strings.ContainsAny(s, "*?[") +} + +type filter struct { + m map[string]struct{} +} + +func (f *filter) Match(s string) bool { + _, ok := f.m[s] + return ok +} + +type filtersingle struct { + s string +} + +func (f *filtersingle) Match(s string) bool { + return f.s == s +} + +func compileFilterNoGlob(filters []string) Filter { + if len(filters) == 1 { + return &filtersingle{s: filters[0]} + } + out := filter{m: make(map[string]struct{})} + for _, filter := range filters { + out.m[filter] = struct{}{} + } + return &out +} + +type IncludeExcludeFilter struct { + include Filter + exclude Filter + includeDefault bool + excludeDefault bool +} + +func NewIncludeExcludeFilter( + include []string, + exclude []string, +) (Filter, error) { + return NewIncludeExcludeFilterDefaults(include, exclude, true, false) +} + +func NewIncludeExcludeFilterDefaults( + include []string, + exclude []string, + includeDefault bool, + excludeDefault bool, +) (Filter, error) { + in, err := Compile(include) + if err != nil { + return nil, err + } + + ex, err := Compile(exclude) + if err != nil { + return nil, err + } + + return &IncludeExcludeFilter{in, ex, includeDefault, excludeDefault}, nil +} + +func (f *IncludeExcludeFilter) Match(s string) bool { + if f.include != nil { + if !f.include.Match(s) { + return false + } + } else if !f.includeDefault { + return false + } + + if f.exclude != nil { + if f.exclude.Match(s) { + return false + } + } else if f.excludeDefault { + return false + } + + return true +} diff --git a/cli/internal/util/filter/filter_test.go b/cli/internal/util/filter/filter_test.go new file mode 100644 index 0000000000000..727a4b685d9ff --- /dev/null +++ b/cli/internal/util/filter/filter_test.go @@ -0,0 +1,116 @@ +// Copyright (c) 2015-2020 InfluxData Inc. MIT License (MIT) +// https://github.com/influxdata/telegraf +package filter + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCompile(t *testing.T) { + f, err := Compile([]string{}) + assert.NoError(t, err) + assert.Nil(t, f) + + f, err = Compile([]string{"cpu"}) + assert.NoError(t, err) + assert.True(t, f.Match("cpu")) + assert.False(t, f.Match("cpu0")) + assert.False(t, f.Match("mem")) + + f, err = Compile([]string{"cpu*"}) + assert.NoError(t, err) + assert.True(t, f.Match("cpu")) + assert.True(t, f.Match("cpu0")) + assert.False(t, f.Match("mem")) + + f, err = Compile([]string{"cpu", "mem"}) + assert.NoError(t, err) + assert.True(t, f.Match("cpu")) + assert.False(t, f.Match("cpu0")) + assert.True(t, f.Match("mem")) + + f, err = Compile([]string{"cpu", "mem", "net*"}) + assert.NoError(t, err) + assert.True(t, f.Match("cpu")) + assert.False(t, f.Match("cpu0")) + assert.True(t, f.Match("mem")) + assert.True(t, f.Match("network")) +} + +func TestIncludeExclude(t *testing.T) { + tags := []string{} + labels := []string{"best", "com_influxdata", "timeseries", "com_influxdata_telegraf", "ever"} + + filter, err := NewIncludeExcludeFilter([]string{}, []string{"com_influx*"}) + if err != nil { + t.Fatalf("Failed to create include/exclude filter - %v", err) + } + + for i := range labels { + if filter.Match(labels[i]) { + tags = append(tags, labels[i]) + } + } + + assert.Equal(t, []string{"best", "timeseries", "ever"}, tags) +} + +var benchbool bool + +func BenchmarkFilterSingleNoGlobFalse(b *testing.B) { + f, _ := Compile([]string{"cpu"}) + var tmp bool + for n := 0; n < b.N; n++ { + tmp = f.Match("network") + } + benchbool = tmp +} + +func BenchmarkFilterSingleNoGlobTrue(b *testing.B) { + f, _ := Compile([]string{"cpu"}) + var tmp bool + for n := 0; n < b.N; n++ { + tmp = f.Match("cpu") + } + benchbool = tmp +} + +func BenchmarkFilter(b *testing.B) { + f, _ := Compile([]string{"cpu", "mem", "net*"}) + var tmp bool + for n := 0; n < b.N; n++ { + tmp = f.Match("network") + } + benchbool = tmp +} + +func BenchmarkFilterNoGlob(b *testing.B) { + f, _ := Compile([]string{"cpu", "mem", "net"}) + var tmp bool + for n := 0; n < b.N; n++ { + tmp = f.Match("net") + } + benchbool = tmp +} + +func BenchmarkFilter2(b *testing.B) { + f, _ := Compile([]string{"aa", "bb", "c", "ad", "ar", "at", "aq", + "aw", "az", "axxx", "ab", "cpu", "mem", "net*"}) + var tmp bool + for n := 0; n < b.N; n++ { + tmp = f.Match("network") + } + benchbool = tmp +} + +func BenchmarkFilter2NoGlob(b *testing.B) { + f, _ := Compile([]string{"aa", "bb", "c", "ad", "ar", "at", "aq", + "aw", "az", "axxx", "ab", "cpu", "mem", "net"}) + var tmp bool + for n := 0; n < b.N; n++ { + tmp = f.Match("net") + } + benchbool = tmp +} diff --git a/cli/scripts/e2e/e2e.ts b/cli/scripts/e2e/e2e.ts index bc5b6471c0aaa..894ff4d81ceaf 100644 --- a/cli/scripts/e2e/e2e.ts +++ b/cli/scripts/e2e/e2e.ts @@ -19,6 +19,9 @@ const basicPipeline = { }, }; +// This is injected by github actions +process.env.TURBO_TOKEN = ""; + for (let npmClient of ["yarn", "pnpm", "npm"] as const) { const repo = new Monorepo("basics"); repo.init(npmClient, basicPipeline); @@ -74,6 +77,7 @@ function runSmokeTests( }, `Could not read log file from cache ${logFilePath}`); assert.ok(text.includes("testing c"), "Contains correct output"); + repo.newBranch("my-feature-branch"); repo.commitFiles({ [path.join("packages", "a", "test.js")]: `console.log('testingz a');`, @@ -85,21 +89,94 @@ function runSmokeTests( options ); const testCLine = (sinceResults.stdout + sinceResults.stderr).split("\n"); + assert.equal( `• Packages changed since main: a`, testCLine[0], "Calculates changed packages (--since)" ); + assert.equal(`• Packages in scope: a`, testCLine[1], "Packages in scope"); assert.equal( `• Running test in 1 packages`, - testCLine[1], + testCLine[2], "Runs only in changed packages" ); assert.ok( - testCLine[2].startsWith(`a:test: cache miss, executing`), + testCLine[3].startsWith(`a:test: cache miss, executing`), "Cache miss in changed package" ); + // Check cache hit after another run + const since2Results = repo.turbo( + "run", + ["test", "--since=main", "--stream", "-vvv"], + options + ); + const testCLine2 = (since2Results.stdout + since2Results.stderr).split( + "\n" + ); + assert.equal( + `• Packages changed since main: a`, + testCLine2[0], + "Calculates changed packages (--since) after a second run" + ); + assert.equal( + `• Packages in scope: a`, + testCLine2[1], + "Packages in scope after a second run" + ); + assert.equal( + `• Running test in 1 packages`, + testCLine2[2], + "Runs only in changed packages after a second run" + ); + + assert.ok( + testCLine2[3].startsWith(`a:test: cache hit, replaying output`), + "Cache hit in changed package after a second run" + ); + + // Check that hashes are different and trigger a cascade + repo.commitFiles({ + [path.join("packages", "b", "test.js")]: `console.log('testingz b');`, + }); + + const hashChangeResults = repo.turbo("run", ["test", "--stream"], options); + const hashChangeResultsOut = + hashChangeResults.stdout + hashChangeResults.stderr; + console.log("------------------------------------------------------"); + console.log(hashChangeResultsOut); + console.log("------------------------------------------------------"); + const testCLine3 = hashChangeResultsOut.split("\n"); + + assert.equal( + `• Packages in scope: a, b, c`, + testCLine3[0], + "Packages in scope after a third run" + ); + assert.equal( + `• Running test in 3 packages`, + testCLine3[1], + "Runs correct number of packages" + ); + assert.ok( + testCLine3.findIndex((l) => + l.startsWith("a:test: cache miss, executing") + ) >= 0, + `A was impacted.` + ); + assert.ok( + testCLine3.findIndex((l) => + l.startsWith("b:test: cache miss, executing") + ) >= 0, + `B was impacted.` + ); + assert.ok( + testCLine3.findIndex((l) => + l.startsWith("c:test: cache hit, replaying output") + ) >= 0, + `C was unchanged` + ); repo.cleanup(); }); }