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 LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,4 @@ google.golang.org/grpc,https://github.com/grpc/grpc-go,Apache-2.0,"Google LLC"
google.golang.org/protobuf,https://github.com/protocolbuffers/protobuf-go,BSD-3-Clause,"The Go Authors"
gopkg.in/ini.v1,https://github.com/go-ini/ini,Apache-2.0,"Unknown"
gopkg.in/yaml.v3,https://github.com/go-yaml/yaml,MIT,"Kirill Simonov"
github.com/bmatcuk/doublestar/v4,https://github.com/bmatcuk/doublestar,MIT,Bob Matcuk
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ In CI‑node mode, DDTest also fans out across local CPUs on that node and furth
| `--ci-node` | `DD_TEST_OPTIMIZATION_RUNNER_CI_NODE` | `-1` (off) | Restrict this run to the slice assigned to node **N** (0‑indexed). Also parallelizes within the node across its CPUs. |
| `--worker-env` | `DD_TEST_OPTIMIZATION_RUNNER_WORKER_ENV` | `""` | Template env vars per local worker (e.g., isolate DBs): `--worker-env "DATABASE_NAME_TEST=app_test{{nodeIndex}}"`. |
| `--command` | `DD_TEST_OPTIMIZATION_RUNNER_COMMAND` | `""` | Override the default test command used by the framework. When provided, takes precedence over auto-detection (e.g., `--command "bundle exec custom-rspec"`). |
| `--tests-location` | `DD_TEST_OPTIMIZATION_RUNNER_TESTS_LOCATION` | `""` | Custom glob pattern to discover test files (e.g., `--tests-location "custom/spec/**/*_spec.rb"`). Defaults to `spec/**/*_spec.rb` for RSpec, `test/**/*_test.rb` for Minitest. |

#### Note about the `--command` flag

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.24.5

require (
github.com/DataDog/dd-trace-go/v2 v2.1.0
github.com/bmatcuk/doublestar/v4 v4.9.1
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.20.1
github.com/tinylib/msgp v1.2.5
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lpr
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
Expand Down
5 changes: 5 additions & 0 deletions internal/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func init() {
rootCmd.PersistentFlags().Int("max-parallelism", 1, "Maximum number of parallel test processes")
rootCmd.PersistentFlags().String("worker-env", "", "Worker environment configuration")
rootCmd.PersistentFlags().String("command", "", "Test command that ddtest should wrap")
rootCmd.PersistentFlags().String("tests-location", "", "Glob pattern used to discover test files")
if err := viper.BindPFlag("platform", rootCmd.PersistentFlags().Lookup("platform")); err != nil {
fmt.Fprintf(os.Stderr, "Error binding platform flag: %v\n", err)
os.Exit(1)
Expand All @@ -82,6 +83,10 @@ func init() {
fmt.Fprintf(os.Stderr, "Error binding command flag: %v\n", err)
os.Exit(1)
}
if err := viper.BindPFlag("tests_location", rootCmd.PersistentFlags().Lookup("tests-location")); err != nil {
fmt.Fprintf(os.Stderr, "Error binding tests-location flag: %v\n", err)
os.Exit(1)
}

rootCmd.AddCommand(planCmd)
rootCmd.AddCommand(runCmd)
Expand Down
19 changes: 19 additions & 0 deletions internal/cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ func TestRootCommandFlags(t *testing.T) {
return
}

testsLocationFlag := rootCmd.PersistentFlags().Lookup("tests-location")
if testsLocationFlag == nil {
t.Error("tests-location flag should be defined")
return
}

// Check default values
if platformFlag.DefValue != "ruby" {
t.Errorf("expected platform default to be 'ruby', got %q", platformFlag.DefValue)
Expand All @@ -45,6 +51,10 @@ func TestRootCommandFlags(t *testing.T) {
if commandFlag.DefValue != "" {
t.Errorf("expected command default to be empty, got %q", commandFlag.DefValue)
}

if testsLocationFlag.DefValue != "" {
t.Errorf("expected tests-location default to be empty, got %q", testsLocationFlag.DefValue)
}
}

func TestCommandHierarchy(t *testing.T) {
Expand Down Expand Up @@ -107,6 +117,9 @@ func TestFlagBinding(t *testing.T) {
if err := viper.BindPFlag("command", rootCmd.PersistentFlags().Lookup("command")); err != nil {
t.Fatalf("Error binding command flag: %v", err)
}
if err := viper.BindPFlag("tests_location", rootCmd.PersistentFlags().Lookup("tests-location")); err != nil {
t.Fatalf("Error binding tests-location flag: %v", err)
}

// Set flag values
if err := rootCmd.PersistentFlags().Set("platform", "python"); err != nil {
Expand All @@ -118,6 +131,9 @@ func TestFlagBinding(t *testing.T) {
if err := rootCmd.PersistentFlags().Set("command", "bundle exec pytest"); err != nil {
t.Fatalf("Error setting command flag: %v", err)
}
if err := rootCmd.PersistentFlags().Set("tests-location", "spec/**/*_spec.rb"); err != nil {
t.Fatalf("Error setting tests-location flag: %v", err)
}

// Check that viper picks up the flag values
if viper.GetString("platform") != "python" {
Expand All @@ -129,6 +145,9 @@ func TestFlagBinding(t *testing.T) {
if viper.GetString("command") != "bundle exec pytest" {
t.Errorf("expected viper command to be 'bundle exec pytest', got %q", viper.GetString("command"))
}
if viper.GetString("tests_location") != "spec/**/*_spec.rb" {
t.Errorf("expected viper tests_location to be 'spec/**/*_spec.rb', got %q", viper.GetString("tests_location"))
}
}

func TestCommandUsage(t *testing.T) {
Expand Down
35 changes: 8 additions & 27 deletions internal/framework/framework.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"path/filepath"
"time"

"github.com/bmatcuk/doublestar/v4"

"github.com/DataDog/ddtest/internal/constants"
"github.com/DataDog/ddtest/internal/ext"
"github.com/DataDog/ddtest/internal/testoptimization"
Expand Down Expand Up @@ -71,36 +73,15 @@ func parseDiscoveryFile(filePath string) ([]testoptimization.Test, error) {
return tests, nil
}

// discoverTestFilesByPattern searches for test files matching a pattern in a given directory
func discoverTestFilesByPattern(rootDir string, pattern string) ([]string, error) {
var testFiles []string

err := filepath.WalkDir(rootDir, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}

// Skip directories
if d.IsDir() {
return nil
}

// Check if the file matches the pattern
matched, err := filepath.Match(pattern, filepath.Base(path))
if err != nil {
return err
}

if matched {
testFiles = append(testFiles, path)
}

return nil
})
func defaultTestPattern(rootDir, filePattern string) string {
return filepath.Join(rootDir, "**", filePattern)
}

func globTestFiles(pattern string) ([]string, error) {
matches, err := doublestar.FilepathGlob(pattern, doublestar.WithFilesOnly())
if err != nil {
return nil, err
}

return testFiles, nil
return matches, nil
}
35 changes: 24 additions & 11 deletions internal/framework/minitest.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/DataDog/ddtest/internal/ext"
"github.com/DataDog/ddtest/internal/settings"
"github.com/DataDog/ddtest/internal/testoptimization"
)

Expand Down Expand Up @@ -35,7 +36,18 @@ func (m *Minitest) Name() string {
func (m *Minitest) DiscoverTests(ctx context.Context) ([]testoptimization.Test, error) {
cleanupDiscoveryFile(TestsDiscoveryFilePath)

name, args, envMap := m.createDiscoveryCommand()
pattern := m.testPattern()
name, args, envMap, isRails := m.createDiscoveryCommand()
slog.Debug("Using test discovery pattern", "pattern", pattern)
if isRails {
args = append(args, pattern)
} else {
if envMap == nil {
envMap = make(map[string]string)
}
envMap["TEST"] = pattern
}

slog.Info("Discovering tests with command", "command", name, "args", args)
_, err := executeDiscoveryCommand(ctx, m.executor, name, args, envMap, m.Name())
if err != nil {
Expand All @@ -52,13 +64,7 @@ func (m *Minitest) DiscoverTests(ctx context.Context) ([]testoptimization.Test,
}

func (m *Minitest) DiscoverTestFiles() ([]string, error) {
// Check if the test directory exists
if _, err := os.Stat(minitestRootDir); os.IsNotExist(err) {
slog.Debug("Minitest directory does not exist", "directory", minitestRootDir)
return []string{}, nil
}

testFiles, err := discoverTestFilesByPattern(minitestRootDir, minitestTestFilePattern)
testFiles, err := globTestFiles(m.testPattern())
if err != nil {
return nil, err
}
Expand All @@ -67,6 +73,13 @@ func (m *Minitest) DiscoverTestFiles() ([]string, error) {
return testFiles, nil
}

func (m *Minitest) testPattern() string {
if custom := settings.GetTestsLocation(); custom != "" {
return custom
}
return defaultTestPattern(minitestRootDir, minitestTestFilePattern)
}

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)
Expand Down Expand Up @@ -150,12 +163,12 @@ func (m *Minitest) getMinitestCommand() (string, []string, bool) {
return "bundle", []string{"exec", "rake", "test"}, false
}

func (m *Minitest) createDiscoveryCommand() (string, []string, map[string]string) {
command, args, _ := m.getMinitestCommand()
func (m *Minitest) createDiscoveryCommand() (string, []string, map[string]string, bool) {
command, args, isRails := m.getMinitestCommand()

envMap := map[string]string{
"DD_TEST_OPTIMIZATION_DISCOVERY_ENABLED": "1",
"DD_TEST_OPTIMIZATION_DISCOVERY_FILE": TestsDiscoveryFilePath,
}
return command, args, envMap
return command, args, envMap, isRails
}
Loading
Loading