Skip to content

Commit bea5e28

Browse files
committed
First commit
1 parent ce99c6a commit bea5e28

9 files changed

Lines changed: 266 additions & 34 deletions

File tree

internal/framework/framework.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
type Framework interface {
1111
Name() string
1212
TestPattern() string
13+
DiscoverTestFiles(ctx context.Context, testFiles discovery.TestFileSet) ([]string, error)
1314
DiscoverTests(ctx context.Context, testFiles discovery.TestFileSet) ([]testoptimization.Test, error)
1415
RunTests(ctx context.Context, testFiles []string, envMap map[string]string) error
1516
SetPlatformEnv(platformEnv map[string]string)

internal/framework/jest.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package framework
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"log/slog"
78
"maps"
89
"os"
@@ -81,6 +82,34 @@ func (j *Jest) DiscoverTests(ctx context.Context, testFiles discovery.TestFileSe
8182
return nil, ErrFullTestDiscoveryUnsupported
8283
}
8384

85+
func (j *Jest) DiscoverTestFiles(ctx context.Context, testFiles discovery.TestFileSet) ([]string, error) {
86+
if testFiles.Empty() {
87+
return []string{}, nil
88+
}
89+
if testFiles.UseExplicitFiles() {
90+
return slices.Clone(testFiles.ExplicitFiles), nil
91+
}
92+
93+
command, baseArgs := j.getJestCommand()
94+
args := slices.Clone(baseArgs)
95+
args = append(args, "--listTests")
96+
if settings.GetTestsLocation() != "" && testFiles.Pattern != "" {
97+
args = append(args, "--testMatch", testFiles.Pattern)
98+
}
99+
100+
slog.Info("Discovering Jest test files with command", "command", command, "args", args)
101+
output, err := j.executor.CombinedOutput(ctx, command, args, j.platformEnv)
102+
if err != nil {
103+
message := strings.TrimSpace(string(output))
104+
if message == "" {
105+
return nil, fmt.Errorf("failed to discover Jest test files: %w", err)
106+
}
107+
return nil, fmt.Errorf("failed to discover Jest test files: %s: %w", message, err)
108+
}
109+
110+
return parseJestListTestsOutput(output), nil
111+
}
112+
84113
func (j *Jest) RunTests(ctx context.Context, testFiles []string, envMap map[string]string) error {
85114
command, baseArgs := j.getJestCommand()
86115
args := slices.Clone(baseArgs)
@@ -113,3 +142,41 @@ func (j *Jest) getJestCommand() (string, []string) {
113142
func jestTestFileExtensionPattern() string {
114143
return "{" + strings.Join(jestTestFileExtensions, ",") + "}"
115144
}
145+
146+
func parseJestListTestsOutput(output []byte) []string {
147+
cwd, _ := os.Getwd()
148+
if resolvedCwd, err := filepath.EvalSymlinks(cwd); err == nil {
149+
cwd = resolvedCwd
150+
}
151+
testFiles := make([]string, 0)
152+
for _, line := range strings.Split(string(output), "\n") {
153+
testFile := strings.TrimSpace(line)
154+
if testFile == "" {
155+
continue
156+
}
157+
158+
if filepath.IsAbs(testFile) && cwd != "" {
159+
pathForRel := testFile
160+
if resolvedPath, err := filepath.EvalSymlinks(testFile); err == nil {
161+
pathForRel = resolvedPath
162+
}
163+
relativePath, err := filepath.Rel(cwd, pathForRel)
164+
if err != nil || strings.HasPrefix(relativePath, ".."+string(filepath.Separator)) || relativePath == ".." {
165+
continue
166+
}
167+
testFile = relativePath
168+
}
169+
170+
normalizedTestFile := utils.NormalizePath(testFile)
171+
if normalizedTestFile == "" {
172+
continue
173+
}
174+
if _, err := os.Stat(normalizedTestFile); err != nil {
175+
continue
176+
}
177+
testFiles = append(testFiles, normalizedTestFile)
178+
}
179+
180+
slices.Sort(testFiles)
181+
return slices.Compact(testFiles)
182+
}

internal/framework/jest_test.go

Lines changed: 143 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,35 @@ import (
66
"os"
77
"path/filepath"
88
"slices"
9+
"strings"
910
"testing"
1011

1112
"github.com/DataDog/ddtest/internal/discovery"
1213
)
1314

15+
type jestCommandExecutor struct {
16+
output []byte
17+
err error
18+
onExecution func(name string, args []string)
19+
capturedEnvMap map[string]string
20+
}
21+
22+
func (m *jestCommandExecutor) CombinedOutput(ctx context.Context, name string, args []string, envMap map[string]string) ([]byte, error) {
23+
m.capturedEnvMap = envMap
24+
if m.onExecution != nil {
25+
m.onExecution(name, args)
26+
}
27+
return m.output, m.err
28+
}
29+
30+
func (m *jestCommandExecutor) Run(ctx context.Context, name string, args []string, envMap map[string]string) error {
31+
m.capturedEnvMap = envMap
32+
if m.onExecution != nil {
33+
m.onExecution(name, args)
34+
}
35+
return m.err
36+
}
37+
1438
func TestNewJest(t *testing.T) {
1539
jest := NewJest()
1640
if jest == nil {
@@ -79,23 +103,23 @@ func TestJest_HasUnskippableMarker(t *testing.T) {
79103
}
80104
}
81105

82-
func TestJest_DiscoverTestFiles_DefaultPatterns(t *testing.T) {
106+
func TestJest_DiscoverTestFiles_UsesLocalJestListTests(t *testing.T) {
83107
tempDir := t.TempDir()
84108
oldWd, _ := os.Getwd()
85109
defer func() { _ = os.Chdir(oldWd) }()
86110
if err := os.Chdir(tempDir); err != nil {
87111
t.Fatalf("failed to chdir: %v", err)
88112
}
113+
if err := os.MkdirAll(filepath.Dir(binJestPath), 0755); err != nil {
114+
t.Fatalf("failed to create jest bin dir: %v", err)
115+
}
116+
if err := os.WriteFile(binJestPath, []byte("#!/usr/bin/env node\n"), 0755); err != nil {
117+
t.Fatalf("failed to create jest bin: %v", err)
118+
}
89119

90120
filesToCreate := []string{
121+
"src/b.test.ts",
91122
"src/foo.test.js",
92-
"src/foo.spec.ts",
93-
"src/__tests__/bar.jsx",
94-
"src/not-test.js",
95-
"node_modules/pkg/bad.test.js",
96-
"dist/bad.spec.js",
97-
"coverage/bad.test.ts",
98-
".next/bad.test.tsx",
99123
}
100124
for _, file := range filesToCreate {
101125
if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil {
@@ -106,26 +130,44 @@ func TestJest_DiscoverTestFiles_DefaultPatterns(t *testing.T) {
106130
}
107131
}
108132

109-
jest := NewJest()
110-
files, err := discovery.DiscoverTestFiles(jest.TestPattern(), "")
133+
var capturedName string
134+
var capturedArgs []string
135+
mockExecutor := &jestCommandExecutor{
136+
output: []byte(filepath.Join(tempDir, "src", "b.test.ts") + "\n" +
137+
filepath.Join(tempDir, "src", "foo.test.js") + "\n" +
138+
"warning: ignored because it is not a file\n"),
139+
onExecution: func(name string, args []string) {
140+
capturedName = name
141+
capturedArgs = slices.Clone(args)
142+
},
143+
}
144+
jest := &Jest{
145+
executor: mockExecutor,
146+
platformEnv: map[string]string{"NODE_OPTIONS": "-r dd-trace/ci/init"},
147+
}
148+
files, err := jest.DiscoverTestFiles(context.Background(), discovery.TestFileSet{Pattern: jest.TestPattern()})
111149
if err != nil {
112-
t.Fatalf("generic discovery failed: %v", err)
150+
t.Fatalf("DiscoverTestFiles failed: %v", err)
113151
}
114152

115-
expected := []string{
116-
".next/bad.test.tsx",
117-
"coverage/bad.test.ts",
118-
"dist/bad.spec.js",
119-
"src/__tests__/bar.jsx",
120-
"src/foo.spec.ts",
121-
"src/foo.test.js",
153+
if capturedName != binJestPath {
154+
t.Errorf("expected command %q, got %q", binJestPath, capturedName)
122155
}
123-
if !slices.Equal(files, expected) {
124-
t.Errorf("expected files %v, got %v", expected, files)
156+
expectedArgs := []string{"--listTests"}
157+
if !slices.Equal(capturedArgs, expectedArgs) {
158+
t.Errorf("expected args %v, got %v", expectedArgs, capturedArgs)
159+
}
160+
if mockExecutor.capturedEnvMap["NODE_OPTIONS"] != "-r dd-trace/ci/init" {
161+
t.Errorf("expected NODE_OPTIONS from platform env, got %q", mockExecutor.capturedEnvMap["NODE_OPTIONS"])
162+
}
163+
164+
expectedFiles := []string{"src/b.test.ts", "src/foo.test.js"}
165+
if !slices.Equal(files, expectedFiles) {
166+
t.Errorf("expected files %v, got %v", expectedFiles, files)
125167
}
126168
}
127169

128-
func TestJest_DiscoverTestFiles_WithTestsLocation(t *testing.T) {
170+
func TestJest_DiscoverTestFiles_WithTestsLocationUsesTestMatch(t *testing.T) {
129171
tempDir := t.TempDir()
130172
oldWd, _ := os.Getwd()
131173
defer func() { _ = os.Chdir(oldWd) }()
@@ -144,10 +186,28 @@ func TestJest_DiscoverTestFiles_WithTestsLocation(t *testing.T) {
144186

145187
setTestsLocation(t, "custom/*.check.js")
146188

147-
jest := NewJest()
148-
files, err := discovery.DiscoverTestFiles(jest.TestPattern(), "")
189+
var capturedName string
190+
var capturedArgs []string
191+
mockExecutor := &jestCommandExecutor{
192+
output: []byte(filepath.Join(tempDir, "custom", "b.check.js") + "\n" +
193+
filepath.Join(tempDir, "custom", "a.check.js") + "\n"),
194+
onExecution: func(name string, args []string) {
195+
capturedName = name
196+
capturedArgs = slices.Clone(args)
197+
},
198+
}
199+
jest := &Jest{executor: mockExecutor, platformEnv: make(map[string]string)}
200+
files, err := jest.DiscoverTestFiles(context.Background(), discovery.TestFileSet{Pattern: jest.TestPattern()})
149201
if err != nil {
150-
t.Fatalf("generic discovery failed: %v", err)
202+
t.Fatalf("DiscoverTestFiles failed: %v", err)
203+
}
204+
205+
if capturedName != "npx" {
206+
t.Errorf("expected command %q, got %q", "npx", capturedName)
207+
}
208+
expectedArgs := []string{"jest", "--listTests", "--testMatch", "custom/*.check.js"}
209+
if !slices.Equal(capturedArgs, expectedArgs) {
210+
t.Errorf("expected args %v, got %v", expectedArgs, capturedArgs)
151211
}
152212

153213
expected := []string{"custom/a.check.js", "custom/b.check.js"}
@@ -156,6 +216,65 @@ func TestJest_DiscoverTestFiles_WithTestsLocation(t *testing.T) {
156216
}
157217
}
158218

219+
func TestJest_DiscoverTestFiles_WithOverride(t *testing.T) {
220+
tempDir := t.TempDir()
221+
oldWd, _ := os.Getwd()
222+
defer func() { _ = os.Chdir(oldWd) }()
223+
if err := os.Chdir(tempDir); err != nil {
224+
t.Fatalf("failed to chdir: %v", err)
225+
}
226+
if err := os.MkdirAll("src", 0755); err != nil {
227+
t.Fatalf("failed to create src dir: %v", err)
228+
}
229+
if err := os.WriteFile(filepath.Join("src", "a.test.js"), []byte("test"), 0644); err != nil {
230+
t.Fatalf("failed to create test file: %v", err)
231+
}
232+
233+
var capturedName string
234+
var capturedArgs []string
235+
mockExecutor := &jestCommandExecutor{
236+
output: []byte(filepath.Join(tempDir, "src", "a.test.js") + "\n"),
237+
onExecution: func(name string, args []string) {
238+
capturedName = name
239+
capturedArgs = slices.Clone(args)
240+
},
241+
}
242+
jest := &Jest{
243+
executor: mockExecutor,
244+
commandOverride: []string{"pnpm", "jest", "--runInBand"},
245+
platformEnv: make(map[string]string),
246+
}
247+
248+
if _, err := jest.DiscoverTestFiles(context.Background(), discovery.TestFileSet{Pattern: jest.TestPattern()}); err != nil {
249+
t.Fatalf("DiscoverTestFiles failed: %v", err)
250+
}
251+
252+
if capturedName != "pnpm" {
253+
t.Errorf("expected command %q, got %q", "pnpm", capturedName)
254+
}
255+
expectedArgs := []string{"jest", "--runInBand", "--listTests"}
256+
if !slices.Equal(capturedArgs, expectedArgs) {
257+
t.Errorf("expected args %v, got %v", expectedArgs, capturedArgs)
258+
}
259+
}
260+
261+
func TestJest_DiscoverTestFiles_CommandError(t *testing.T) {
262+
mockExecutor := &jestCommandExecutor{
263+
output: []byte("invalid jest config"),
264+
err: errors.New("exit status 1"),
265+
}
266+
jest := &Jest{executor: mockExecutor, platformEnv: make(map[string]string)}
267+
268+
_, err := jest.DiscoverTestFiles(context.Background(), discovery.TestFileSet{Pattern: jest.TestPattern()})
269+
if err == nil {
270+
t.Fatal("expected error")
271+
}
272+
if !strings.Contains(err.Error(), "failed to discover Jest test files") ||
273+
!strings.Contains(err.Error(), "invalid jest config") {
274+
t.Fatalf("unexpected error: %v", err)
275+
}
276+
}
277+
159278
func TestJest_RunTests_UsesLocalJestBinary(t *testing.T) {
160279
tempDir := t.TempDir()
161280
oldWd, _ := os.Getwd()

internal/framework/minitest.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ func (m *Minitest) TestPattern() string {
7979
return filepath.Join(minitestRootDir, "**", minitestTestFilePattern)
8080
}
8181

82+
func (m *Minitest) DiscoverTestFiles(ctx context.Context, testFiles discovery.TestFileSet) ([]string, error) {
83+
if testFiles.Empty() {
84+
return []string{}, nil
85+
}
86+
if testFiles.UseExplicitFiles() {
87+
return testFiles.ExplicitFiles, nil
88+
}
89+
return discovery.DiscoverTestFiles(testFiles.Pattern, settings.GetTestsExcludePattern())
90+
}
91+
8292
func (m *Minitest) RunTests(ctx context.Context, testFiles []string, envMap map[string]string) error {
8393
command, args, isRails := m.getMinitestCommand()
8494
slog.Info("Running tests with command", "command", command, "args", args)

internal/framework/pytest.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,16 @@ func (p *PyTest) DiscoverTests(ctx context.Context, testFiles discovery.TestFile
9494
return discovery.DiscoverTests(ctx, p.executor, "python", args, p.platformEnv)
9595
}
9696

97+
func (p *PyTest) DiscoverTestFiles(ctx context.Context, testFiles discovery.TestFileSet) ([]string, error) {
98+
if testFiles.Empty() {
99+
return []string{}, nil
100+
}
101+
if testFiles.UseExplicitFiles() {
102+
return testFiles.ExplicitFiles, nil
103+
}
104+
return discovery.DiscoverTestFiles(testFiles.Pattern, settings.GetTestsExcludePattern())
105+
}
106+
97107
// braceExpand collapses a list into a single glob token.
98108
// A single item is returned as-is; multiple items are wrapped: {a,b,c}.
99109
func braceExpand(items []string) string {

internal/framework/rspec.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ func (r *RSpec) TestPattern() string {
7575
return filepath.Join(rspecRootDir, "**", rspecTestFilePattern)
7676
}
7777

78+
func (r *RSpec) DiscoverTestFiles(ctx context.Context, testFiles discovery.TestFileSet) ([]string, error) {
79+
if testFiles.Empty() {
80+
return []string{}, nil
81+
}
82+
if testFiles.UseExplicitFiles() {
83+
return testFiles.ExplicitFiles, nil
84+
}
85+
return discovery.DiscoverTestFiles(testFiles.Pattern, settings.GetTestsExcludePattern())
86+
}
87+
7888
func (r *RSpec) RunTests(ctx context.Context, testFiles []string, envMap map[string]string) error {
7989
command, baseArgs := r.getRSpecCommand()
8090
args := append(baseArgs, "--format", "progress")

internal/planner/planner.go

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -411,16 +411,11 @@ func (tp *TestPlanner) PreparePlanningData(ctx context.Context) error {
411411
startTime := time.Now()
412412
slog.Info("Discovering test files (fast)...", "framework", testFramework.Name())
413413
var res []string
414-
if resolvedTestFiles.UseExplicitFiles() {
415-
res = resolvedTestFiles.ExplicitFiles
416-
} else {
417-
var discErr error
418-
res, discErr = discovery.DiscoverTestFiles(resolvedTestFiles.Pattern, settings.GetTestsExcludePattern())
419-
if discErr != nil {
420-
fastDiscoveryErr = discErr
421-
slog.Warn("Fast test discovery failed", "error", discErr)
422-
return nil // Don't fail the entire process if full discovery succeeded
423-
}
414+
res, discErr := testFramework.DiscoverTestFiles(ctx, resolvedTestFiles)
415+
if discErr != nil {
416+
fastDiscoveryErr = discErr
417+
slog.Warn("Fast test discovery failed", "error", discErr)
418+
return nil // Don't fail the entire process if full discovery succeeded
424419
}
425420
discoveredTestFiles = res
426421
fastDiscoveryDuration = time.Since(startTime)

0 commit comments

Comments
 (0)