From 95a74f9d86da8c604c649d380028785bdba63792 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Thu, 27 Aug 2026 06:26:55 +0000 Subject: [PATCH 1/5] feat(pytest): use commandOverride for RunTests and DiscoverTests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PyTest.RunTests and DiscoverTests hardcoded 'python -m pytest', ignoring commandOverride (from --command or DD_TEST_OPTIMIZATION_RUNNER_COMMAND env var). Use commandOverride when set so callers can run 'pytest' (console script) instead of 'python -m pytest'. This fixes the service inference issue: 'python -m pytest' sets sys.argv[0] to pytest/__main__.py, which detect_service treats as a test path → service='pytest'. 'pytest' (console script) sets argv[0]='pytest' (skipped by detect_service → correct service). --- internal/framework/pytest.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/internal/framework/pytest.go b/internal/framework/pytest.go index 0ee6c69..53077b5 100644 --- a/internal/framework/pytest.go +++ b/internal/framework/pytest.go @@ -75,6 +75,11 @@ func (p *PyTest) DiscoverTests(ctx context.Context, testFiles discovery.TestFile } args := []string{"-m", "pytest"} + command := "python" + if len(p.commandOverride) > 0 { + command = p.commandOverride[0] + args = p.commandOverride[1:] + } if testFiles.UseExplicitFiles() { args = append(args, testFiles.ExplicitFiles...) @@ -91,7 +96,7 @@ func (p *PyTest) DiscoverTests(ctx context.Context, testFiles discovery.TestFile args = append(args, files...) } - return discovery.DiscoverTests(ctx, p.executor, "python", args, p.platformEnv) + return discovery.DiscoverTests(ctx, p.executor, command, args, p.platformEnv) } func (p *PyTest) DiscoverTestFiles(ctx context.Context, testFiles discovery.TestFileSet) ([]string, error) { @@ -128,6 +133,10 @@ func (p *PyTest) HasUnskippableMarker(testFile string) bool { func (p *PyTest) RunTests(ctx context.Context, testFiles []string, envMap map[string]string) error { command := "python" args := []string{"-m", "pytest"} + if len(p.commandOverride) > 0 { + command = p.commandOverride[0] + args = p.commandOverride[1:] + } slog.Info("Running tests with command", "command", command, "args", args) args = append(args, testFiles...) From a980103e7cd96077044430f488049447ba357705 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Thu, 27 Aug 2026 06:30:54 +0000 Subject: [PATCH 2/5] test(pytest): add tests for commandOverride in RunTests and DiscoverTests Two new tests: - TestPyTest_RunTests_WithCommandOverride: verifies RunTests uses commandOverride (command='pytest', args=[]) instead of hardcoded 'python -m pytest'. - TestPyTest_DiscoverTests_WithCommandOverride: verifies DiscoverTests uses commandOverride (command='pytest', args=['tests/test_foo.py']) instead of hardcoded 'python -m pytest'. Both tests pass: the mock executor captures command='pytest' (no '-m pytest' in args) when commandOverride is set. --- internal/framework/pytest_test.go | 72 +++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/internal/framework/pytest_test.go b/internal/framework/pytest_test.go index e717fde..0f10421 100644 --- a/internal/framework/pytest_test.go +++ b/internal/framework/pytest_test.go @@ -204,6 +204,78 @@ func TestPyTest_SupportsFullTestDiscovery(t *testing.T) { } } +func TestPyTest_RunTests_WithCommandOverride(t *testing.T) { + testFiles := []string{"tests/test_user.py"} + + var capturedName string + var capturedArgs []string + mockExecutor := &mockCommandExecutor{ + capturedEnvMap: map[string]string{}, + onExecution: func(name string, args []string) { + capturedName = name + capturedArgs = args + }, + } + + pytest := &PyTest{ + executor: mockExecutor, + platformEnv: map[string]string{}, + commandOverride: []string{"pytest"}, + } + if err := pytest.RunTests(context.Background(), testFiles, nil); err != nil { + t.Fatalf("RunTests failed: %v", err) + } + + if capturedName != "pytest" { + t.Fatalf("expected command 'pytest' (from commandOverride), got %q", capturedName) + } + expectedArgs := []string{"tests/test_user.py"} + if !slices.Equal(capturedArgs, expectedArgs) { + t.Fatalf("expected args %v (no -m pytest), got %v", expectedArgs, capturedArgs) + } +} + +func TestPyTest_DiscoverTests_WithCommandOverride(t *testing.T) { + if err := os.MkdirAll(filepath.Dir(discovery.TestsFilePath), 0755); err != nil { + t.Fatalf("failed to create discovery dir: %v", err) + } + defer cleanupDiscoveryDir() + + explicitFiles := []string{"tests/test_foo.py"} + var capturedName string + var capturedArgs []string + mockExecutor := &mockCommandExecutor{ + onExecution: func(name string, args []string) { + capturedName = name + capturedArgs = args + f, _ := os.Create(discovery.TestsFilePath) + _ = f.Close() + }, + } + + pytest := &PyTest{ + executor: mockExecutor, + platformEnv: map[string]string{}, + commandOverride: []string{"pytest"}, + } + testFiles := discovery.TestFileSet{ExplicitFiles: explicitFiles} + _, _ = pytest.DiscoverTests(context.Background(), testFiles) + + if capturedName != "pytest" { + t.Fatalf("expected command 'pytest' (from commandOverride), got %q", capturedName) + } + for _, expected := range explicitFiles { + if !slices.Contains(capturedArgs, expected) { + t.Errorf("expected file %q in pytest args, got %v", expected, capturedArgs) + } + } + for _, unexpected := range []string{"-m", "pytest"} { + if slices.Contains(capturedArgs, unexpected) { + t.Errorf("unexpected arg %q in pytest args (commandOverride should not add -m pytest)", unexpected) + } + } +} + func TestPyTest_RunTests(t *testing.T) { testFiles := []string{"tests/test_user.py", "tests/test_auth.py"} envMap := map[string]string{ From 8503f127d4ab4e38f56015253ad873f8be1340ca Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Thu, 27 Aug 2026 07:40:36 +0000 Subject: [PATCH 3/5] fix(testoptimization): don't cache settings.json on API error (401) GetSettings() stored the raw HTTP response body (c.settingsRawResponse) before checking the status code. On 401 (no valid API key), the error body ('data') was stored and then written to .testoptimization/cache/http/settings.json by StoreCacheAndExit(). The subprocess ddtrace CI Visibility plugin then read this invalid cache and logged 'Error parsing cached settings file: data' to stderr, breaking test_span_schematization's 'assert err == b""'. Fix: on non-2xx status, clear settingsRawResponse and return error before storing the raw response. Only cache successful responses. --- internal/testoptimization/api/settings_api.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/testoptimization/api/settings_api.go b/internal/testoptimization/api/settings_api.go index 40e1033..c0d4090 100644 --- a/internal/testoptimization/api/settings_api.go +++ b/internal/testoptimization/api/settings_api.go @@ -111,6 +111,8 @@ func (c *transport) GetSettings() (*SettingsResponseData, error) { } if response.StatusCode < 200 || response.StatusCode >= 300 { telemetry.GitRequestsSettingsErrors(c.telemetryClient, response.StatusCode) + c.settingsRawResponse = nil + return nil, fmt.Errorf("unmarshalling settings response: cannot unmarshal response with status code %d", response.StatusCode) } slog.Debug("testoptimization.settings", "responseBody", string(response.Body)) From d1c904d14b020e7cf6a92e1cb27bafbea6fcf885 Mon Sep 17 00:00:00 2001 From: "federico.mon" Date: Thu, 27 Aug 2026 08:20:30 +0000 Subject: [PATCH 4/5] Revert "fix(testoptimization): don't cache settings.json on API error (401)" This reverts commit 8503f127d4ab4e38f56015253ad873f8be1340ca. --- internal/testoptimization/api/settings_api.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/testoptimization/api/settings_api.go b/internal/testoptimization/api/settings_api.go index c0d4090..40e1033 100644 --- a/internal/testoptimization/api/settings_api.go +++ b/internal/testoptimization/api/settings_api.go @@ -111,8 +111,6 @@ func (c *transport) GetSettings() (*SettingsResponseData, error) { } if response.StatusCode < 200 || response.StatusCode >= 300 { telemetry.GitRequestsSettingsErrors(c.telemetryClient, response.StatusCode) - c.settingsRawResponse = nil - return nil, fmt.Errorf("unmarshalling settings response: cannot unmarshal response with status code %d", response.StatusCode) } slog.Debug("testoptimization.settings", "responseBody", string(response.Body)) From deaaec39266dca92238411eda62b51623fbdaf7d Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Thu, 27 Aug 2026 08:21:03 +0000 Subject: [PATCH 5/5] style: gofmt pytest_test.go --- internal/framework/pytest_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/framework/pytest_test.go b/internal/framework/pytest_test.go index 0f10421..1867903 100644 --- a/internal/framework/pytest_test.go +++ b/internal/framework/pytest_test.go @@ -218,8 +218,8 @@ func TestPyTest_RunTests_WithCommandOverride(t *testing.T) { } pytest := &PyTest{ - executor: mockExecutor, - platformEnv: map[string]string{}, + executor: mockExecutor, + platformEnv: map[string]string{}, commandOverride: []string{"pytest"}, } if err := pytest.RunTests(context.Background(), testFiles, nil); err != nil { @@ -254,8 +254,8 @@ func TestPyTest_DiscoverTests_WithCommandOverride(t *testing.T) { } pytest := &PyTest{ - executor: mockExecutor, - platformEnv: map[string]string{}, + executor: mockExecutor, + platformEnv: map[string]string{}, commandOverride: []string{"pytest"}, } testFiles := discovery.TestFileSet{ExplicitFiles: explicitFiles}