Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/framework/framework.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
147 changes: 146 additions & 1 deletion internal/framework/jest.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package framework
import (
"context"
"errors"
"fmt"
"log/slog"
"maps"
"os"
Expand All @@ -17,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")

Expand Down Expand Up @@ -81,6 +89,36 @@ 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")

slog.Info("Discovering Jest test files with command", "command", command, "args", args)
output, err := j.executor.CombinedOutput(ctx, command, args, j.discoveryEnv())
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)
}

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 {
command, baseArgs := j.getJestCommand()
args := slices.Clone(baseArgs)
Expand All @@ -95,6 +133,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 {
Expand All @@ -113,3 +168,93 @@ func (j *Jest) getJestCommand() (string, []string) {
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))
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 {
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)
}
Loading
Loading