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
7 changes: 5 additions & 2 deletions internal/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import (
"github.com/spf13/viper"
)

// defaultParallelism stores the computed default at init time for CLI flags
var defaultParallelism = settings.DefaultParallelism()

var rootCmd = &cobra.Command{
Use: "ddtest",
Short: "A test runner from Datadog",
Expand Down Expand Up @@ -54,8 +57,8 @@ var runCmd = &cobra.Command{
func init() {
rootCmd.PersistentFlags().String("platform", "ruby", "Platform that runs tests")
rootCmd.PersistentFlags().String("framework", "rspec", "Test framework to use")
rootCmd.PersistentFlags().Int("min-parallelism", 1, "Minimum number of parallel test processes")
rootCmd.PersistentFlags().Int("max-parallelism", 1, "Maximum number of parallel test processes")
rootCmd.PersistentFlags().Int("min-parallelism", defaultParallelism, "Minimum number of parallel test processes (default: number of CPUs)")
rootCmd.PersistentFlags().Int("max-parallelism", defaultParallelism, "Maximum number of parallel test processes (default: number of CPUs)")
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")
Expand Down
4 changes: 2 additions & 2 deletions internal/runner/parallelism.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ func calculateParallelRunnersWithParams(skippablePercentage float64, minParallel
}

if maxParallelism < minParallelism {
slog.Warn("max_parallelism is less than min_parallelism, using min_parallelism",
slog.Warn("max_parallelism is less than min_parallelism, clamping min to max",
"max_parallelism", maxParallelism, "min_parallelism", minParallelism)
return minParallelism
minParallelism = maxParallelism
}

percentage := math.Max(0.0, math.Min(100.0, skippablePercentage)) // Clamp to [0, 100]
Expand Down
4 changes: 3 additions & 1 deletion internal/runner/parallelism_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ func TestCalculateParallelRunners_MinParallelismLessThanOne(t *testing.T) {
}

func TestCalculateParallelRunners_MaxLessThanMin(t *testing.T) {
// When max < min, min is clamped to max. This ensures that a user who only
// sets --max-parallelism to a lower value gets the expected behavior.
result := testCalculateParallelRunners(50.0, 5, 3) // max < min
expected := 5 // Should return min_parallelism
expected := 3 // Should clamp min to max and return max
if result != expected {
t.Errorf("calculateParallelRunners(50.0) = %d, expected %d when max < min", result, expected)
}
Expand Down
27 changes: 27 additions & 0 deletions internal/runner/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,15 @@ func TestTestRunner_Setup_WithParallelRunners(t *testing.T) {
// Create .testoptimization directory
_ = os.MkdirAll(constants.PlanDirectory, 0755)

// Set parallelism to 1 to test single runner behavior
_ = os.Setenv("DD_TEST_OPTIMIZATION_RUNNER_MIN_PARALLELISM", "1")
_ = os.Setenv("DD_TEST_OPTIMIZATION_RUNNER_MAX_PARALLELISM", "1")
defer func() {
_ = os.Unsetenv("DD_TEST_OPTIMIZATION_RUNNER_MIN_PARALLELISM")
_ = os.Unsetenv("DD_TEST_OPTIMIZATION_RUNNER_MAX_PARALLELISM")
}()
settings.Init()

// Setup mocks for a test with 40% skippable percentage
mockFramework := &MockFramework{
FrameworkName: "rspec",
Expand Down Expand Up @@ -299,6 +308,15 @@ func TestTestRunner_Setup_WithCIProvider(t *testing.T) {
// Create .testoptimization directory
_ = os.MkdirAll(constants.PlanDirectory, 0755)

// Set parallelism to 1 to test single runner behavior
_ = os.Setenv("DD_TEST_OPTIMIZATION_RUNNER_MIN_PARALLELISM", "1")
_ = os.Setenv("DD_TEST_OPTIMIZATION_RUNNER_MAX_PARALLELISM", "1")
defer func() {
_ = os.Unsetenv("DD_TEST_OPTIMIZATION_RUNNER_MIN_PARALLELISM")
_ = os.Unsetenv("DD_TEST_OPTIMIZATION_RUNNER_MAX_PARALLELISM")
}()
settings.Init()

// Setup mocks for test with CI provider
mockFramework := &MockFramework{
FrameworkName: "rspec",
Expand Down Expand Up @@ -455,6 +473,15 @@ func TestTestRunner_Setup_WithTestSplit(t *testing.T) {
// Create .testoptimization directory
_ = os.MkdirAll(constants.PlanDirectory, 0755)

// Set parallelism to 1 to test single runner behavior
_ = os.Setenv("DD_TEST_OPTIMIZATION_RUNNER_MIN_PARALLELISM", "1")
_ = os.Setenv("DD_TEST_OPTIMIZATION_RUNNER_MAX_PARALLELISM", "1")
defer func() {
_ = os.Unsetenv("DD_TEST_OPTIMIZATION_RUNNER_MIN_PARALLELISM")
_ = os.Unsetenv("DD_TEST_OPTIMIZATION_RUNNER_MAX_PARALLELISM")
}()
settings.Init()

// Setup mocks for single runner scenario
mockFramework := &MockFramework{
FrameworkName: "rspec",
Expand Down
11 changes: 9 additions & 2 deletions internal/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@ import (
"encoding/json"
"fmt"
"os"
"runtime"
"strings"

"github.com/spf13/viper"
)

// DefaultParallelism returns the default parallelism value, which is the number
// of available CPUs. This respects container CPU limits in cloud CI environments.
func DefaultParallelism() int {
return runtime.NumCPU()
}

type Config struct {
Platform string `mapstructure:"platform"`
Framework string `mapstructure:"framework"`
Expand Down Expand Up @@ -42,8 +49,8 @@ func Init() {
func setDefaults() {
viper.SetDefault("platform", "ruby")
viper.SetDefault("framework", "rspec")
viper.SetDefault("min_parallelism", 1)
viper.SetDefault("max_parallelism", 1)
viper.SetDefault("min_parallelism", DefaultParallelism())
viper.SetDefault("max_parallelism", DefaultParallelism())
viper.SetDefault("worker_env", "")
viper.SetDefault("ci_node", -1)
viper.SetDefault("command", "")
Expand Down
41 changes: 29 additions & 12 deletions internal/settings/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,24 @@ package settings

import (
"os"
"runtime"
"testing"

"github.com/spf13/viper"
)

func TestDefaultParallelism(t *testing.T) {
result := DefaultParallelism()
expected := runtime.NumCPU()

if result != expected {
t.Errorf("expected DefaultParallelism() to return %d (runtime.NumCPU()), got %d", expected, result)
}
if result < 1 {
t.Errorf("expected DefaultParallelism() to be at least 1, got %d", result)
}
}

func TestInit(t *testing.T) {
// Clear any existing config
config = nil
Expand All @@ -25,11 +38,12 @@ func TestInit(t *testing.T) {
if config.Framework != "rspec" {
t.Errorf("expected default framework to be 'rspec', got %q", config.Framework)
}
if config.MinParallelism != 1 {
t.Errorf("expected default min_parallelism to be 1, got %d", config.MinParallelism)
expectedParallelism := runtime.NumCPU()
if config.MinParallelism != expectedParallelism {
t.Errorf("expected default min_parallelism to be %d (CPU count), got %d", expectedParallelism, config.MinParallelism)
}
if config.MaxParallelism != 1 {
t.Errorf("expected default max_parallelism to be 1, got %d", config.MaxParallelism)
if config.MaxParallelism != expectedParallelism {
t.Errorf("expected default max_parallelism to be %d (CPU count), got %d", expectedParallelism, config.MaxParallelism)
}
if config.WorkerEnv != "" {
t.Errorf("expected default worker_env to be empty, got %q", config.WorkerEnv)
Expand Down Expand Up @@ -59,11 +73,12 @@ func TestSetDefaults(t *testing.T) {
if viper.GetString("framework") != "rspec" {
t.Errorf("expected default framework to be 'rspec', got %q", viper.GetString("framework"))
}
if viper.GetInt("min_parallelism") != 1 {
t.Errorf("expected default min_parallelism to be 1, got %d", viper.GetInt("min_parallelism"))
expectedParallelism := runtime.NumCPU()
if viper.GetInt("min_parallelism") != expectedParallelism {
t.Errorf("expected default min_parallelism to be %d (CPU count), got %d", expectedParallelism, viper.GetInt("min_parallelism"))
}
if viper.GetInt("max_parallelism") != 1 {
t.Errorf("expected default max_parallelism to be 1, got %d", viper.GetInt("max_parallelism"))
if viper.GetInt("max_parallelism") != expectedParallelism {
t.Errorf("expected default max_parallelism to be %d (CPU count), got %d", expectedParallelism, viper.GetInt("max_parallelism"))
}
if viper.GetString("worker_env") != "" {
t.Errorf("expected default worker_env to be empty, got %q", viper.GetString("worker_env"))
Expand Down Expand Up @@ -199,9 +214,10 @@ func TestGetMinParallelism(t *testing.T) {
config = nil
viper.Reset()

expectedParallelism := runtime.NumCPU()
minParallelism := GetMinParallelism()
if minParallelism != 1 {
t.Errorf("expected min_parallelism to be 1, got %d", minParallelism)
if minParallelism != expectedParallelism {
t.Errorf("expected min_parallelism to be %d (CPU count), got %d", expectedParallelism, minParallelism)
}

// Test with custom value
Expand All @@ -217,9 +233,10 @@ func TestGetMaxParallelism(t *testing.T) {
config = nil
viper.Reset()

expectedParallelism := runtime.NumCPU()
maxParallelism := GetMaxParallelism()
if maxParallelism != 1 {
t.Errorf("expected max_parallelism to be 1, got %d", maxParallelism)
if maxParallelism != expectedParallelism {
t.Errorf("expected max_parallelism to be %d (CPU count), got %d", expectedParallelism, maxParallelism)
}

// Test with custom value
Expand Down
Loading