From bea5e28f263502e11fd12e25bdce1782ed4672f2 Mon Sep 17 00:00:00 2001 From: Sebastian Conde Lorenzo Date: Wed, 1 Jul 2026 15:31:54 +0200 Subject: [PATCH 1/3] First commit --- internal/framework/framework.go | 1 + internal/framework/jest.go | 67 +++++++++++ internal/framework/jest_test.go | 167 +++++++++++++++++++++++---- internal/framework/minitest.go | 10 ++ internal/framework/pytest.go | 10 ++ internal/framework/rspec.go | 10 ++ internal/planner/planner.go | 15 +-- internal/planner/planner_test.go | 10 ++ internal/runner/test_helpers_test.go | 10 ++ 9 files changed, 266 insertions(+), 34 deletions(-) diff --git a/internal/framework/framework.go b/internal/framework/framework.go index b2447f7..57c97be 100644 --- a/internal/framework/framework.go +++ b/internal/framework/framework.go @@ -10,6 +10,7 @@ import ( type Framework interface { Name() string TestPattern() string + DiscoverTestFiles(ctx context.Context, testFiles discovery.TestFileSet) ([]string, error) DiscoverTests(ctx context.Context, testFiles discovery.TestFileSet) ([]testoptimization.Test, error) RunTests(ctx context.Context, testFiles []string, envMap map[string]string) error SetPlatformEnv(platformEnv map[string]string) diff --git a/internal/framework/jest.go b/internal/framework/jest.go index df07b80..49d5bc0 100644 --- a/internal/framework/jest.go +++ b/internal/framework/jest.go @@ -3,6 +3,7 @@ package framework import ( "context" "errors" + "fmt" "log/slog" "maps" "os" @@ -81,6 +82,34 @@ func (j *Jest) DiscoverTests(ctx context.Context, testFiles discovery.TestFileSe return nil, ErrFullTestDiscoveryUnsupported } +func (j *Jest) DiscoverTestFiles(ctx context.Context, testFiles discovery.TestFileSet) ([]string, error) { + if testFiles.Empty() { + return []string{}, nil + } + if testFiles.UseExplicitFiles() { + return slices.Clone(testFiles.ExplicitFiles), nil + } + + command, baseArgs := j.getJestCommand() + args := slices.Clone(baseArgs) + args = append(args, "--listTests") + if settings.GetTestsLocation() != "" && testFiles.Pattern != "" { + args = append(args, "--testMatch", testFiles.Pattern) + } + + slog.Info("Discovering Jest test files with command", "command", command, "args", args) + output, err := j.executor.CombinedOutput(ctx, command, args, j.platformEnv) + if err != nil { + message := strings.TrimSpace(string(output)) + if message == "" { + return nil, fmt.Errorf("failed to discover Jest test files: %w", err) + } + return nil, fmt.Errorf("failed to discover Jest test files: %s: %w", message, err) + } + + return parseJestListTestsOutput(output), nil +} + func (j *Jest) RunTests(ctx context.Context, testFiles []string, envMap map[string]string) error { command, baseArgs := j.getJestCommand() args := slices.Clone(baseArgs) @@ -113,3 +142,41 @@ func (j *Jest) getJestCommand() (string, []string) { func jestTestFileExtensionPattern() string { return "{" + strings.Join(jestTestFileExtensions, ",") + "}" } + +func parseJestListTestsOutput(output []byte) []string { + cwd, _ := os.Getwd() + if resolvedCwd, err := filepath.EvalSymlinks(cwd); err == nil { + cwd = resolvedCwd + } + testFiles := make([]string, 0) + for _, line := range strings.Split(string(output), "\n") { + testFile := strings.TrimSpace(line) + if testFile == "" { + continue + } + + if filepath.IsAbs(testFile) && cwd != "" { + pathForRel := testFile + if resolvedPath, err := filepath.EvalSymlinks(testFile); err == nil { + pathForRel = resolvedPath + } + relativePath, err := filepath.Rel(cwd, pathForRel) + if err != nil || strings.HasPrefix(relativePath, ".."+string(filepath.Separator)) || relativePath == ".." { + continue + } + testFile = relativePath + } + + normalizedTestFile := utils.NormalizePath(testFile) + if normalizedTestFile == "" { + continue + } + if _, err := os.Stat(normalizedTestFile); err != nil { + continue + } + testFiles = append(testFiles, normalizedTestFile) + } + + slices.Sort(testFiles) + return slices.Compact(testFiles) +} diff --git a/internal/framework/jest_test.go b/internal/framework/jest_test.go index 9732e1e..5872e6b 100644 --- a/internal/framework/jest_test.go +++ b/internal/framework/jest_test.go @@ -6,11 +6,35 @@ import ( "os" "path/filepath" "slices" + "strings" "testing" "github.com/DataDog/ddtest/internal/discovery" ) +type jestCommandExecutor struct { + output []byte + err error + onExecution func(name string, args []string) + capturedEnvMap map[string]string +} + +func (m *jestCommandExecutor) CombinedOutput(ctx context.Context, name string, args []string, envMap map[string]string) ([]byte, error) { + m.capturedEnvMap = envMap + if m.onExecution != nil { + m.onExecution(name, args) + } + return m.output, m.err +} + +func (m *jestCommandExecutor) Run(ctx context.Context, name string, args []string, envMap map[string]string) error { + m.capturedEnvMap = envMap + if m.onExecution != nil { + m.onExecution(name, args) + } + return m.err +} + func TestNewJest(t *testing.T) { jest := NewJest() if jest == nil { @@ -79,23 +103,23 @@ func TestJest_HasUnskippableMarker(t *testing.T) { } } -func TestJest_DiscoverTestFiles_DefaultPatterns(t *testing.T) { +func TestJest_DiscoverTestFiles_UsesLocalJestListTests(t *testing.T) { tempDir := t.TempDir() oldWd, _ := os.Getwd() defer func() { _ = os.Chdir(oldWd) }() if err := os.Chdir(tempDir); err != nil { t.Fatalf("failed to chdir: %v", err) } + if err := os.MkdirAll(filepath.Dir(binJestPath), 0755); err != nil { + t.Fatalf("failed to create jest bin dir: %v", err) + } + if err := os.WriteFile(binJestPath, []byte("#!/usr/bin/env node\n"), 0755); err != nil { + t.Fatalf("failed to create jest bin: %v", err) + } filesToCreate := []string{ + "src/b.test.ts", "src/foo.test.js", - "src/foo.spec.ts", - "src/__tests__/bar.jsx", - "src/not-test.js", - "node_modules/pkg/bad.test.js", - "dist/bad.spec.js", - "coverage/bad.test.ts", - ".next/bad.test.tsx", } for _, file := range filesToCreate { if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil { @@ -106,26 +130,44 @@ func TestJest_DiscoverTestFiles_DefaultPatterns(t *testing.T) { } } - jest := NewJest() - files, err := discovery.DiscoverTestFiles(jest.TestPattern(), "") + var capturedName string + var capturedArgs []string + mockExecutor := &jestCommandExecutor{ + output: []byte(filepath.Join(tempDir, "src", "b.test.ts") + "\n" + + filepath.Join(tempDir, "src", "foo.test.js") + "\n" + + "warning: ignored because it is not a file\n"), + onExecution: func(name string, args []string) { + capturedName = name + capturedArgs = slices.Clone(args) + }, + } + jest := &Jest{ + executor: mockExecutor, + platformEnv: map[string]string{"NODE_OPTIONS": "-r dd-trace/ci/init"}, + } + files, err := jest.DiscoverTestFiles(context.Background(), discovery.TestFileSet{Pattern: jest.TestPattern()}) if err != nil { - t.Fatalf("generic discovery failed: %v", err) + t.Fatalf("DiscoverTestFiles failed: %v", err) } - expected := []string{ - ".next/bad.test.tsx", - "coverage/bad.test.ts", - "dist/bad.spec.js", - "src/__tests__/bar.jsx", - "src/foo.spec.ts", - "src/foo.test.js", + if capturedName != binJestPath { + t.Errorf("expected command %q, got %q", binJestPath, capturedName) } - if !slices.Equal(files, expected) { - t.Errorf("expected files %v, got %v", expected, files) + expectedArgs := []string{"--listTests"} + if !slices.Equal(capturedArgs, expectedArgs) { + t.Errorf("expected args %v, got %v", expectedArgs, capturedArgs) + } + if mockExecutor.capturedEnvMap["NODE_OPTIONS"] != "-r dd-trace/ci/init" { + t.Errorf("expected NODE_OPTIONS from platform env, got %q", mockExecutor.capturedEnvMap["NODE_OPTIONS"]) + } + + expectedFiles := []string{"src/b.test.ts", "src/foo.test.js"} + if !slices.Equal(files, expectedFiles) { + t.Errorf("expected files %v, got %v", expectedFiles, files) } } -func TestJest_DiscoverTestFiles_WithTestsLocation(t *testing.T) { +func TestJest_DiscoverTestFiles_WithTestsLocationUsesTestMatch(t *testing.T) { tempDir := t.TempDir() oldWd, _ := os.Getwd() defer func() { _ = os.Chdir(oldWd) }() @@ -144,10 +186,28 @@ func TestJest_DiscoverTestFiles_WithTestsLocation(t *testing.T) { setTestsLocation(t, "custom/*.check.js") - jest := NewJest() - files, err := discovery.DiscoverTestFiles(jest.TestPattern(), "") + var capturedName string + var capturedArgs []string + mockExecutor := &jestCommandExecutor{ + output: []byte(filepath.Join(tempDir, "custom", "b.check.js") + "\n" + + filepath.Join(tempDir, "custom", "a.check.js") + "\n"), + onExecution: func(name string, args []string) { + capturedName = name + capturedArgs = slices.Clone(args) + }, + } + jest := &Jest{executor: mockExecutor, platformEnv: make(map[string]string)} + files, err := jest.DiscoverTestFiles(context.Background(), discovery.TestFileSet{Pattern: jest.TestPattern()}) if err != nil { - t.Fatalf("generic discovery failed: %v", err) + t.Fatalf("DiscoverTestFiles failed: %v", err) + } + + if capturedName != "npx" { + t.Errorf("expected command %q, got %q", "npx", capturedName) + } + expectedArgs := []string{"jest", "--listTests", "--testMatch", "custom/*.check.js"} + if !slices.Equal(capturedArgs, expectedArgs) { + t.Errorf("expected args %v, got %v", expectedArgs, capturedArgs) } expected := []string{"custom/a.check.js", "custom/b.check.js"} @@ -156,6 +216,65 @@ func TestJest_DiscoverTestFiles_WithTestsLocation(t *testing.T) { } } +func TestJest_DiscoverTestFiles_WithOverride(t *testing.T) { + tempDir := t.TempDir() + oldWd, _ := os.Getwd() + defer func() { _ = os.Chdir(oldWd) }() + if err := os.Chdir(tempDir); err != nil { + t.Fatalf("failed to chdir: %v", err) + } + if err := os.MkdirAll("src", 0755); err != nil { + t.Fatalf("failed to create src dir: %v", err) + } + if err := os.WriteFile(filepath.Join("src", "a.test.js"), []byte("test"), 0644); err != nil { + t.Fatalf("failed to create test file: %v", err) + } + + var capturedName string + var capturedArgs []string + mockExecutor := &jestCommandExecutor{ + output: []byte(filepath.Join(tempDir, "src", "a.test.js") + "\n"), + onExecution: func(name string, args []string) { + capturedName = name + capturedArgs = slices.Clone(args) + }, + } + jest := &Jest{ + executor: mockExecutor, + commandOverride: []string{"pnpm", "jest", "--runInBand"}, + platformEnv: make(map[string]string), + } + + if _, err := jest.DiscoverTestFiles(context.Background(), discovery.TestFileSet{Pattern: jest.TestPattern()}); err != nil { + t.Fatalf("DiscoverTestFiles failed: %v", err) + } + + if capturedName != "pnpm" { + t.Errorf("expected command %q, got %q", "pnpm", capturedName) + } + expectedArgs := []string{"jest", "--runInBand", "--listTests"} + if !slices.Equal(capturedArgs, expectedArgs) { + t.Errorf("expected args %v, got %v", expectedArgs, capturedArgs) + } +} + +func TestJest_DiscoverTestFiles_CommandError(t *testing.T) { + mockExecutor := &jestCommandExecutor{ + output: []byte("invalid jest config"), + err: errors.New("exit status 1"), + } + jest := &Jest{executor: mockExecutor, platformEnv: make(map[string]string)} + + _, err := jest.DiscoverTestFiles(context.Background(), discovery.TestFileSet{Pattern: jest.TestPattern()}) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "failed to discover Jest test files") || + !strings.Contains(err.Error(), "invalid jest config") { + t.Fatalf("unexpected error: %v", err) + } +} + func TestJest_RunTests_UsesLocalJestBinary(t *testing.T) { tempDir := t.TempDir() oldWd, _ := os.Getwd() diff --git a/internal/framework/minitest.go b/internal/framework/minitest.go index 9f4407d..b507fd4 100644 --- a/internal/framework/minitest.go +++ b/internal/framework/minitest.go @@ -79,6 +79,16 @@ func (m *Minitest) TestPattern() string { return filepath.Join(minitestRootDir, "**", minitestTestFilePattern) } +func (m *Minitest) DiscoverTestFiles(ctx context.Context, testFiles discovery.TestFileSet) ([]string, error) { + if testFiles.Empty() { + return []string{}, nil + } + if testFiles.UseExplicitFiles() { + return testFiles.ExplicitFiles, nil + } + return discovery.DiscoverTestFiles(testFiles.Pattern, settings.GetTestsExcludePattern()) +} + func (m *Minitest) RunTests(ctx context.Context, testFiles []string, envMap map[string]string) error { command, args, isRails := m.getMinitestCommand() slog.Info("Running tests with command", "command", command, "args", args) diff --git a/internal/framework/pytest.go b/internal/framework/pytest.go index 03f1b98..0ee6c69 100644 --- a/internal/framework/pytest.go +++ b/internal/framework/pytest.go @@ -94,6 +94,16 @@ func (p *PyTest) DiscoverTests(ctx context.Context, testFiles discovery.TestFile return discovery.DiscoverTests(ctx, p.executor, "python", args, p.platformEnv) } +func (p *PyTest) DiscoverTestFiles(ctx context.Context, testFiles discovery.TestFileSet) ([]string, error) { + if testFiles.Empty() { + return []string{}, nil + } + if testFiles.UseExplicitFiles() { + return testFiles.ExplicitFiles, nil + } + return discovery.DiscoverTestFiles(testFiles.Pattern, settings.GetTestsExcludePattern()) +} + // braceExpand collapses a list into a single glob token. // A single item is returned as-is; multiple items are wrapped: {a,b,c}. func braceExpand(items []string) string { diff --git a/internal/framework/rspec.go b/internal/framework/rspec.go index 300bedc..61e1688 100644 --- a/internal/framework/rspec.go +++ b/internal/framework/rspec.go @@ -75,6 +75,16 @@ func (r *RSpec) TestPattern() string { return filepath.Join(rspecRootDir, "**", rspecTestFilePattern) } +func (r *RSpec) DiscoverTestFiles(ctx context.Context, testFiles discovery.TestFileSet) ([]string, error) { + if testFiles.Empty() { + return []string{}, nil + } + if testFiles.UseExplicitFiles() { + return testFiles.ExplicitFiles, nil + } + return discovery.DiscoverTestFiles(testFiles.Pattern, settings.GetTestsExcludePattern()) +} + func (r *RSpec) RunTests(ctx context.Context, testFiles []string, envMap map[string]string) error { command, baseArgs := r.getRSpecCommand() args := append(baseArgs, "--format", "progress") diff --git a/internal/planner/planner.go b/internal/planner/planner.go index 147b259..3c90b79 100644 --- a/internal/planner/planner.go +++ b/internal/planner/planner.go @@ -411,16 +411,11 @@ func (tp *TestPlanner) PreparePlanningData(ctx context.Context) error { startTime := time.Now() slog.Info("Discovering test files (fast)...", "framework", testFramework.Name()) var res []string - if resolvedTestFiles.UseExplicitFiles() { - res = resolvedTestFiles.ExplicitFiles - } else { - var discErr error - res, discErr = discovery.DiscoverTestFiles(resolvedTestFiles.Pattern, settings.GetTestsExcludePattern()) - if discErr != nil { - fastDiscoveryErr = discErr - slog.Warn("Fast test discovery failed", "error", discErr) - return nil // Don't fail the entire process if full discovery succeeded - } + res, discErr := testFramework.DiscoverTestFiles(ctx, resolvedTestFiles) + if discErr != nil { + fastDiscoveryErr = discErr + slog.Warn("Fast test discovery failed", "error", discErr) + return nil // Don't fail the entire process if full discovery succeeded } discoveredTestFiles = res fastDiscoveryDuration = time.Since(startTime) diff --git a/internal/planner/planner_test.go b/internal/planner/planner_test.go index 2bb5c41..3546080 100644 --- a/internal/planner/planner_test.go +++ b/internal/planner/planner_test.go @@ -254,6 +254,16 @@ func mockTestFilesPattern(files []string) string { return "{" + strings.Join(normalized, ",") + "}" } +func (m *MockFramework) DiscoverTestFiles(ctx context.Context, testFiles discovery.TestFileSet) ([]string, error) { + if testFiles.Empty() { + return []string{}, nil + } + if testFiles.UseExplicitFiles() { + return testFiles.ExplicitFiles, nil + } + return discovery.DiscoverTestFiles(testFiles.Pattern, settings.GetTestsExcludePattern()) +} + func (m *MockFramework) DiscoverTests(ctx context.Context, testFiles discovery.TestFileSet) ([]testoptimization.Test, error) { m.mu.Lock() m.DiscoverTestsFiles = append(m.DiscoverTestsFiles, testFiles) diff --git a/internal/runner/test_helpers_test.go b/internal/runner/test_helpers_test.go index e63a06e..da37fa2 100644 --- a/internal/runner/test_helpers_test.go +++ b/internal/runner/test_helpers_test.go @@ -96,6 +96,16 @@ func (m *MockFramework) TestPattern() string { return m.TestPatternValue } +func (m *MockFramework) DiscoverTestFiles(ctx context.Context, testFiles discovery.TestFileSet) ([]string, error) { + if testFiles.Empty() { + return []string{}, nil + } + if testFiles.UseExplicitFiles() { + return testFiles.ExplicitFiles, nil + } + return discovery.DiscoverTestFiles(testFiles.Pattern, settings.GetTestsExcludePattern()) +} + func (m *MockFramework) DiscoverTests(ctx context.Context, testFiles discovery.TestFileSet) ([]testoptimization.Test, error) { if m.DiscoverTestsErr != nil { return nil, m.DiscoverTestsErr From 727b0355dc51134eb532e1bce8ce55fc7a765a5e Mon Sep 17 00:00:00 2001 From: Sebastian Conde Lorenzo Date: Wed, 1 Jul 2026 16:43:07 +0200 Subject: [PATCH 2/3] Jest test file discovery is now performed uninstrumented --- internal/framework/jest.go | 55 +++++++++++++++++++++++++++++++-- internal/framework/jest_test.go | 43 ++++++++++++++++++++++++-- 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/internal/framework/jest.go b/internal/framework/jest.go index 49d5bc0..71d7abd 100644 --- a/internal/framework/jest.go +++ b/internal/framework/jest.go @@ -18,7 +18,14 @@ import ( "github.com/DataDog/ddtest/internal/utils" ) -const binJestPath = "node_modules/.bin/jest" +const ( + binJestPath = "node_modules/.bin/jest" + nodeOptionsEnvVar = "NODE_OPTIONS" + ddTraceCIInitModule = "dd-trace/ci/init" + nodeRequireShortArg = "-r" + nodeRequireLongArg = "--require" + nodeRequireLongArgWith = nodeRequireLongArg + "=" +) var ErrFullTestDiscoveryUnsupported = errors.New("full test discovery is not supported") @@ -98,7 +105,7 @@ func (j *Jest) DiscoverTestFiles(ctx context.Context, testFiles discovery.TestFi } slog.Info("Discovering Jest test files with command", "command", command, "args", args) - output, err := j.executor.CombinedOutput(ctx, command, args, j.platformEnv) + output, err := j.executor.CombinedOutput(ctx, command, args, j.discoveryEnv()) if err != nil { message := strings.TrimSpace(string(output)) if message == "" { @@ -124,6 +131,23 @@ func (j *Jest) RunTests(ctx context.Context, testFiles []string, envMap map[stri return j.executor.Run(ctx, command, args, mergedEnv) } +func (j *Jest) discoveryEnv() map[string]string { + envMap := make(map[string]string, len(j.platformEnv)+1) + maps.Copy(envMap, j.platformEnv) + + nodeOptions, ok := envMap[nodeOptionsEnvVar] + if !ok { + var found bool + nodeOptions, found = os.LookupEnv(nodeOptionsEnvVar) + if !found { + return envMap + } + } + + envMap[nodeOptionsEnvVar] = stripNodeOptionsRequire(nodeOptions, ddTraceCIInitModule) + return envMap +} + // Decide between user custom command, local jest binary and npx jest func (j *Jest) getJestCommand() (string, []string) { if len(j.commandOverride) > 0 { @@ -143,6 +167,33 @@ func jestTestFileExtensionPattern() string { return "{" + strings.Join(jestTestFileExtensions, ",") + "}" } +func stripNodeOptionsRequire(nodeOptions string, module string) string { + fields := strings.Fields(nodeOptions) + stripped := make([]string, 0, len(fields)) + for i := 0; i < len(fields); i++ { + field := fields[i] + + if field == nodeRequireShortArg || field == nodeRequireLongArg { + if i+1 < len(fields) && fields[i+1] == module { + i++ + continue + } + } + + if strings.HasPrefix(field, nodeRequireShortArg) && strings.TrimPrefix(field, nodeRequireShortArg) == module { + continue + } + + if strings.HasPrefix(field, nodeRequireLongArgWith) && strings.TrimPrefix(field, nodeRequireLongArgWith) == module { + continue + } + + stripped = append(stripped, field) + } + + return strings.Join(stripped, " ") +} + func parseJestListTestsOutput(output []byte) []string { cwd, _ := os.Getwd() if resolvedCwd, err := filepath.EvalSymlinks(cwd); err == nil { diff --git a/internal/framework/jest_test.go b/internal/framework/jest_test.go index 5872e6b..7fec920 100644 --- a/internal/framework/jest_test.go +++ b/internal/framework/jest_test.go @@ -143,7 +143,7 @@ func TestJest_DiscoverTestFiles_UsesLocalJestListTests(t *testing.T) { } jest := &Jest{ executor: mockExecutor, - platformEnv: map[string]string{"NODE_OPTIONS": "-r dd-trace/ci/init"}, + platformEnv: map[string]string{"NODE_OPTIONS": "-r dd-trace/ci/init --max-old-space-size=4096", "CUSTOM_ENV": "value"}, } files, err := jest.DiscoverTestFiles(context.Background(), discovery.TestFileSet{Pattern: jest.TestPattern()}) if err != nil { @@ -157,8 +157,11 @@ func TestJest_DiscoverTestFiles_UsesLocalJestListTests(t *testing.T) { if !slices.Equal(capturedArgs, expectedArgs) { t.Errorf("expected args %v, got %v", expectedArgs, capturedArgs) } - if mockExecutor.capturedEnvMap["NODE_OPTIONS"] != "-r dd-trace/ci/init" { - t.Errorf("expected NODE_OPTIONS from platform env, got %q", mockExecutor.capturedEnvMap["NODE_OPTIONS"]) + if mockExecutor.capturedEnvMap["NODE_OPTIONS"] != "--max-old-space-size=4096" { + t.Errorf("expected NODE_OPTIONS without dd-trace init, got %q", mockExecutor.capturedEnvMap["NODE_OPTIONS"]) + } + if mockExecutor.capturedEnvMap["CUSTOM_ENV"] != "value" { + t.Errorf("expected CUSTOM_ENV from platform env, got %q", mockExecutor.capturedEnvMap["CUSTOM_ENV"]) } expectedFiles := []string{"src/b.test.ts", "src/foo.test.js"} @@ -167,6 +170,40 @@ func TestJest_DiscoverTestFiles_UsesLocalJestListTests(t *testing.T) { } } +func TestJest_DiscoverTestFiles_StripsInheritedNodeOptions(t *testing.T) { + t.Setenv("NODE_OPTIONS", "--require dd-trace/ci/init --max-old-space-size=4096") + + tempDir := t.TempDir() + oldWd, _ := os.Getwd() + defer func() { _ = os.Chdir(oldWd) }() + if err := os.Chdir(tempDir); err != nil { + t.Fatalf("failed to chdir: %v", err) + } + if err := os.MkdirAll("src", 0755); err != nil { + t.Fatalf("failed to create src dir: %v", err) + } + if err := os.WriteFile(filepath.Join("src", "a.test.js"), []byte("test"), 0644); err != nil { + t.Fatalf("failed to create test file: %v", err) + } + + mockExecutor := &jestCommandExecutor{ + output: []byte(filepath.Join(tempDir, "src", "a.test.js") + "\n"), + } + jest := &Jest{executor: mockExecutor, platformEnv: make(map[string]string)} + + files, err := jest.DiscoverTestFiles(context.Background(), discovery.TestFileSet{Pattern: jest.TestPattern()}) + if err != nil { + t.Fatalf("DiscoverTestFiles failed: %v", err) + } + + if mockExecutor.capturedEnvMap["NODE_OPTIONS"] != "--max-old-space-size=4096" { + t.Errorf("expected inherited NODE_OPTIONS without dd-trace init, got %q", mockExecutor.capturedEnvMap["NODE_OPTIONS"]) + } + if !slices.Equal(files, []string{"src/a.test.js"}) { + t.Errorf("expected discovered file, got %v", files) + } +} + func TestJest_DiscoverTestFiles_WithTestsLocationUsesTestMatch(t *testing.T) { tempDir := t.TempDir() oldWd, _ := os.Getwd() From 492899ac9dda2367d5ff6897d2bf3c6659b71986 Mon Sep 17 00:00:00 2001 From: Sebastian Conde Lorenzo Date: Thu, 2 Jul 2026 14:22:42 +0200 Subject: [PATCH 3/3] Remove Jest testMatch discovery override --- internal/framework/jest.go | 35 ++++++++++++-- internal/framework/jest_test.go | 81 ++++++++++++++++++++++++++++++--- 2 files changed, 105 insertions(+), 11 deletions(-) diff --git a/internal/framework/jest.go b/internal/framework/jest.go index 71d7abd..2e605a5 100644 --- a/internal/framework/jest.go +++ b/internal/framework/jest.go @@ -100,9 +100,6 @@ func (j *Jest) DiscoverTestFiles(ctx context.Context, testFiles discovery.TestFi command, baseArgs := j.getJestCommand() args := slices.Clone(baseArgs) args = append(args, "--listTests") - if settings.GetTestsLocation() != "" && testFiles.Pattern != "" { - args = append(args, "--testMatch", testFiles.Pattern) - } slog.Info("Discovering Jest test files with command", "command", command, "args", args) output, err := j.executor.CombinedOutput(ctx, command, args, j.discoveryEnv()) @@ -114,7 +111,12 @@ func (j *Jest) DiscoverTestFiles(ctx context.Context, testFiles discovery.TestFi return nil, fmt.Errorf("failed to discover Jest test files: %s: %w", message, err) } - return parseJestListTestsOutput(output), nil + discoveredFiles := parseJestListTestsOutput(output) + if settings.GetTestsLocation() == "" && settings.GetTestsExcludePattern() == "" { + return discoveredFiles, nil + } + + return filterJestTestFiles(discoveredFiles, testFiles) } func (j *Jest) RunTests(ctx context.Context, testFiles []string, envMap map[string]string) error { @@ -167,6 +169,31 @@ func jestTestFileExtensionPattern() string { return "{" + strings.Join(jestTestFileExtensions, ",") + "}" } +func filterJestTestFiles(testFiles []string, selectedTestFiles discovery.TestFileSet) ([]string, error) { + if settings.GetTestsLocation() == "" { + selectedTestFiles.Pattern = "" + } + + testFileMatcher, err := discovery.NewTestFileSetMatcher(selectedTestFiles, settings.GetTestsExcludePattern()) + if err != nil { + return nil, err + } + + filteredFiles := make([]string, 0, len(testFiles)) + for _, testFile := range testFiles { + normalizedTestFile := utils.NormalizePath(testFile) + if normalizedTestFile == "" { + continue + } + if testFileMatcher.MatchNormalizedPath(normalizedTestFile) { + filteredFiles = append(filteredFiles, normalizedTestFile) + } + } + + slices.Sort(filteredFiles) + return slices.Compact(filteredFiles), nil +} + func stripNodeOptionsRequire(nodeOptions string, module string) string { fields := strings.Fields(nodeOptions) stripped := make([]string, 0, len(fields)) diff --git a/internal/framework/jest_test.go b/internal/framework/jest_test.go index 7fec920..fe15bd1 100644 --- a/internal/framework/jest_test.go +++ b/internal/framework/jest_test.go @@ -204,7 +204,7 @@ func TestJest_DiscoverTestFiles_StripsInheritedNodeOptions(t *testing.T) { } } -func TestJest_DiscoverTestFiles_WithTestsLocationUsesTestMatch(t *testing.T) { +func TestJest_DiscoverTestFiles_WithTestsLocationFiltersListTestsOutput(t *testing.T) { tempDir := t.TempDir() oldWd, _ := os.Getwd() defer func() { _ = os.Chdir(oldWd) }() @@ -212,7 +212,12 @@ func TestJest_DiscoverTestFiles_WithTestsLocationUsesTestMatch(t *testing.T) { t.Fatalf("failed to chdir: %v", err) } - for _, file := range []string{"custom/a.check.js", "custom/b.check.js", "src/c.test.js"} { + for _, file := range []string{ + "custom/unit/a.check.js", + "custom/unit/b.check.js", + "custom/not-listed.check.js", + "src/c.test.js", + } { if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil { t.Fatalf("failed to create dir for %s: %v", file, err) } @@ -221,13 +226,14 @@ func TestJest_DiscoverTestFiles_WithTestsLocationUsesTestMatch(t *testing.T) { } } - setTestsLocation(t, "custom/*.check.js") + setTestsLocation(t, "./custom/**/*.check.js") var capturedName string var capturedArgs []string mockExecutor := &jestCommandExecutor{ - output: []byte(filepath.Join(tempDir, "custom", "b.check.js") + "\n" + - filepath.Join(tempDir, "custom", "a.check.js") + "\n"), + output: []byte(filepath.Join(tempDir, "custom", "unit", "b.check.js") + "\n" + + filepath.Join(tempDir, "src", "c.test.js") + "\n" + + filepath.Join(tempDir, "custom", "unit", "a.check.js") + "\n"), onExecution: func(name string, args []string) { capturedName = name capturedArgs = slices.Clone(args) @@ -242,12 +248,73 @@ func TestJest_DiscoverTestFiles_WithTestsLocationUsesTestMatch(t *testing.T) { if capturedName != "npx" { t.Errorf("expected command %q, got %q", "npx", capturedName) } - expectedArgs := []string{"jest", "--listTests", "--testMatch", "custom/*.check.js"} + expectedArgs := []string{"jest", "--listTests"} if !slices.Equal(capturedArgs, expectedArgs) { t.Errorf("expected args %v, got %v", expectedArgs, capturedArgs) } - expected := []string{"custom/a.check.js", "custom/b.check.js"} + expected := []string{"custom/unit/a.check.js", "custom/unit/b.check.js"} + if !slices.Equal(files, expected) { + t.Errorf("expected files %v, got %v", expected, files) + } +} + +func TestJest_DiscoverTestFiles_WithTestsLocationReturnsInvalidPatternError(t *testing.T) { + setTestsLocation(t, "custom/[") + + mockExecutor := &jestCommandExecutor{ + output: []byte("custom/a.check.js\n"), + } + jest := &Jest{executor: mockExecutor, platformEnv: make(map[string]string)} + + _, err := jest.DiscoverTestFiles(context.Background(), discovery.TestFileSet{Pattern: jest.TestPattern()}) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "invalid tests location pattern") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestJest_DiscoverTestFiles_WithTestsExcludePatternFiltersListTestsOutput(t *testing.T) { + tempDir := t.TempDir() + oldWd, _ := os.Getwd() + defer func() { _ = os.Chdir(oldWd) }() + if err := os.Chdir(tempDir); err != nil { + t.Fatalf("failed to chdir: %v", err) + } + + for _, file := range []string{"src/a.test.js", "src/system/b.test.js"} { + if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil { + t.Fatalf("failed to create dir for %s: %v", file, err) + } + if err := os.WriteFile(file, []byte("test"), 0644); err != nil { + t.Fatalf("failed to create %s: %v", file, err) + } + } + + setTestsExcludePattern(t, "src/system/**/*.test.js") + + var capturedArgs []string + mockExecutor := &jestCommandExecutor{ + output: []byte(filepath.Join(tempDir, "src", "system", "b.test.js") + "\n" + + filepath.Join(tempDir, "src", "a.test.js") + "\n"), + onExecution: func(name string, args []string) { + capturedArgs = slices.Clone(args) + }, + } + jest := &Jest{executor: mockExecutor, platformEnv: make(map[string]string)} + files, err := jest.DiscoverTestFiles(context.Background(), discovery.TestFileSet{Pattern: jest.TestPattern()}) + if err != nil { + t.Fatalf("DiscoverTestFiles failed: %v", err) + } + + expectedArgs := []string{"jest", "--listTests"} + if !slices.Equal(capturedArgs, expectedArgs) { + t.Errorf("expected args %v, got %v", expectedArgs, capturedArgs) + } + + expected := []string{"src/a.test.js"} if !slices.Equal(files, expected) { t.Errorf("expected files %v, got %v", expected, files) }