From e7950ecc955b3804ee5d4eaa82c6c7a0b7def8ba Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Tue, 10 Jun 2025 16:18:05 +0200 Subject: [PATCH 01/58] atr + xdist test improvements --- tests/contrib/pytest/test_pytest_xdist_atr.py | 144 +++++++++++++++++- 1 file changed, 136 insertions(+), 8 deletions(-) diff --git a/tests/contrib/pytest/test_pytest_xdist_atr.py b/tests/contrib/pytest/test_pytest_xdist_atr.py index 76d201140d9..ce31cfe5049 100644 --- a/tests/contrib/pytest/test_pytest_xdist_atr.py +++ b/tests/contrib/pytest/test_pytest_xdist_atr.py @@ -141,14 +141,39 @@ def setup_sitecustomize(self): in the xdist worker processes. """ sitecustomize_content = """ -# sitecustomize.py +# sitecustomize.py - Cross-process ATR mocking for xdist +import os from unittest import mock -from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings -import ddtrace.internal.ci_visibility.recorder # Ensure parent module is loaded +from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings, EarlyFlakeDetectionSettings, TestManagementSettings + +# Ensure environment variables are set for agentless mode +os.environ["DD_CIVISIBILITY_AGENTLESS_ENABLED"] = "1" +os.environ["DD_API_KEY"] = "foobar.baz" +os.environ["DD_INSTRUMENTATION_TELEMETRY_ENABLED"] = "0" + +# Mock ddconfig to enable agentless mode +from ddtrace import config as ddconfig +ddconfig._ci_visibility_agentless_enabled = True + +# Ensure parent module is loaded +import ddtrace.internal.ci_visibility.recorder + +# Create ATR-enabled settings for worker processes +atr_enabled_settings = TestVisibilityAPISettings( + coverage_enabled=False, + skipping_enabled=False, + require_git=False, + itr_enabled=False, + flaky_test_retries_enabled=True, + known_tests_enabled=False, + early_flake_detection=EarlyFlakeDetectionSettings(), + test_management=TestManagementSettings(), +) +# Apply the settings mock globally for all processes _GLOBAL_SITECUSTOMIZE_PATCH_OBJECT = mock.patch( "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", - return_value=TestVisibilityAPISettings(flaky_test_retries_enabled=True) + return_value=atr_enabled_settings ) _GLOBAL_SITECUSTOMIZE_PATCH_OBJECT.start() """ @@ -169,14 +194,86 @@ def test_pytest_xdist_atr_no_ddtrace_does_not_retry(self): assert rec.ret == 1 def test_pytest_xdist_atr_env_var_disables_retrying(self): + # Create a test-specific sitecustomize with ATR disabled settings + atr_disabled_sitecustomize = """ +# sitecustomize.py - Cross-process ATR disabled mocking for xdist +import os +from unittest import mock +from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings, EarlyFlakeDetectionSettings, TestManagementSettings + +# Ensure environment variables are set for agentless mode +os.environ["DD_CIVISIBILITY_AGENTLESS_ENABLED"] = "1" +os.environ["DD_API_KEY"] = "foobar.baz" +os.environ["DD_INSTRUMENTATION_TELEMETRY_ENABLED"] = "0" +os.environ["DD_CIVISIBILITY_FLAKY_RETRY_ENABLED"] = "0" # Disable ATR via env var + +# Mock ddconfig to enable agentless mode +from ddtrace import config as ddconfig +ddconfig._ci_visibility_agentless_enabled = True + +# Ensure parent module is loaded +import ddtrace.internal.ci_visibility.recorder + +# Create ATR-disabled settings for worker processes (environment variable takes precedence) +atr_disabled_settings = TestVisibilityAPISettings( + coverage_enabled=False, + skipping_enabled=False, + require_git=False, + itr_enabled=False, + flaky_test_retries_enabled=False, + known_tests_enabled=False, + early_flake_detection=EarlyFlakeDetectionSettings(), + test_management=TestManagementSettings(), +) + +# Apply the settings mock globally for all processes +_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT = mock.patch( + "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", + return_value=atr_disabled_settings +) +_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT.start() +""" + self.testdir.makepyfile(sitecustomize=atr_disabled_sitecustomize) + self.testdir.makepyfile(test_pass=_TEST_PASS_CONTENT) self.testdir.makepyfile(test_fail=_TEST_FAIL_CONTENT) self.testdir.makepyfile(test_errors=_TEST_ERRORS_CONTENT) self.testdir.makepyfile(test_pass_on_retries=_TEST_PASS_ON_RETRIES_CONTENT) self.testdir.makepyfile(test_skip=_TEST_SKIP_CONTENT) - with mock.patch("ddtrace.internal.ci_visibility.recorder.ddconfig", _get_default_civisibility_ddconfig()): - rec = self.inline_run("--ddtrace", "-s", extra_env={"DD_CIVISIBILITY_FLAKY_RETRY_ENABLED": "0"}) + # Create ATR-disabled settings for the main process + from ddtrace.internal.ci_visibility._api_client import ( + TestVisibilityAPISettings, + EarlyFlakeDetectionSettings, + TestManagementSettings, + ) + + atr_disabled_settings = TestVisibilityAPISettings( + coverage_enabled=False, + skipping_enabled=False, + require_git=False, + itr_enabled=False, + flaky_test_retries_enabled=False, + known_tests_enabled=False, + early_flake_detection=EarlyFlakeDetectionSettings(), + test_management=TestManagementSettings(), + ) + + # Use the proper CI Visibility test environment setup + mock_ddconfig = _get_default_civisibility_ddconfig() + mock_ddconfig._ci_visibility_agentless_enabled = True + + with mock.patch("ddtrace.internal.ci_visibility.recorder.ddconfig", mock_ddconfig), mock.patch( + "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", + return_value=atr_disabled_settings, + ): + extra_env = { + "DD_API_KEY": "foobar.baz", + "DD_CIVISIBILITY_AGENTLESS_ENABLED": "1", + "DD_INSTRUMENTATION_TELEMETRY_ENABLED": "0", + "DD_CIVISIBILITY_FLAKY_RETRY_ENABLED": "0", + } + rec = self.inline_run("--ddtrace", "-s", extra_env=extra_env) assert rec.ret == 1 def test_pytest_xdist_atr_fails_session_when_test_fails(self): @@ -193,8 +290,39 @@ def test_pytest_xdist_atr_passes_session_when_test_pass(self): self.testdir.makepyfile(test_pass_on_retries=_TEST_PASS_ON_RETRIES_CONTENT) self.testdir.makepyfile(test_skip=_TEST_SKIP_CONTENT) - rec = self.inline_run("--ddtrace") - assert rec.ret == 0 + # Create ATR-enabled settings for the main process + from ddtrace.internal.ci_visibility._api_client import ( + TestVisibilityAPISettings, + EarlyFlakeDetectionSettings, + TestManagementSettings, + ) + + atr_enabled_settings = TestVisibilityAPISettings( + coverage_enabled=False, + skipping_enabled=False, + require_git=False, + itr_enabled=False, + flaky_test_retries_enabled=True, + known_tests_enabled=False, + early_flake_detection=EarlyFlakeDetectionSettings(), + test_management=TestManagementSettings(), + ) + + # Use the proper CI Visibility test environment setup + mock_ddconfig = _get_default_civisibility_ddconfig() + mock_ddconfig._ci_visibility_agentless_enabled = True + + with mock.patch("ddtrace.internal.ci_visibility.recorder.ddconfig", mock_ddconfig), mock.patch( + "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", + return_value=atr_enabled_settings, + ): + extra_env = { + "DD_API_KEY": "foobar.baz", + "DD_CIVISIBILITY_AGENTLESS_ENABLED": "1", + "DD_INSTRUMENTATION_TELEMETRY_ENABLED": "0", + } + rec = self.inline_run("--ddtrace", extra_env=extra_env) + assert rec.ret == 0 def test_pytest_xdist_atr_does_not_retry_failed_setup_or_teardown(self): # NOTE: This feature only works for regular pytest tests. For tests inside unittest classes, setup and teardown From 6e6758294e444ec8bb28a038728ff7a0ee19db69 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Tue, 10 Jun 2025 16:24:57 +0200 Subject: [PATCH 02/58] xdist + ITR tests --- tests/contrib/pytest/test_pytest_xdist_itr.py | 486 ++++++++++++++++++ 1 file changed, 486 insertions(+) create mode 100644 tests/contrib/pytest/test_pytest_xdist_itr.py diff --git a/tests/contrib/pytest/test_pytest_xdist_itr.py b/tests/contrib/pytest/test_pytest_xdist_itr.py new file mode 100644 index 00000000000..27e62d02a17 --- /dev/null +++ b/tests/contrib/pytest/test_pytest_xdist_itr.py @@ -0,0 +1,486 @@ +"""Tests Intelligent Test Runner (ITR) functionality interacting with pytest-xdist. + +The tests in this module validate the interaction between ITR and pytest-xdist. +""" +import os # Just for the RIOT env var check +from unittest import mock + +import pytest + +from ddtrace.contrib.internal.pytest._utils import _USE_PLUGIN_V2 +from ddtrace.contrib.internal.pytest._utils import _pytest_version_supports_itr +from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings +from ddtrace.internal.ci_visibility._api_client import ITRData +from ddtrace.internal.ci_visibility._api_client import TestManagementSettings + +# Create ITR-enabled settings for the main process +# Create ITR-disabled settings for the main process +from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings +from tests.ci_visibility.api_client._util import _make_fqdn_test_ids +from tests.ci_visibility.util import _get_default_civisibility_ddconfig +from tests.contrib.pytest.test_pytest import PytestTestCaseBase + + +###### +# Skip these tests if they are not running under riot +riot_env_value = os.getenv("RIOT", None) +if not riot_env_value: + pytest.importorskip("xdist", reason="ITR + xdist tests, not running under riot") +###### + + +pytestmark = pytest.mark.skipif( + not (_USE_PLUGIN_V2 and _pytest_version_supports_itr()), + reason="ITR requires v2 of the plugin and pytest >=7.0", +) + +_TEST_PASS_CONTENT = """ +import unittest + +def test_func_pass(): + assert True + +class SomeTestCase(unittest.TestCase): + def test_class_func_pass(self): + assert True +""" + +_TEST_FAIL_CONTENT = """ +import pytest +import unittest + +def test_func_fail(): + assert False + +class SomeTestCase(unittest.TestCase): + def test_class_func_fail(self): + assert False +""" + +_TEST_SKIP_CONTENT = """ +import pytest +import unittest + +@pytest.mark.skip +def test_func_skip_mark(): + assert True + +def test_func_skip_inside(): + pytest.skip() + +class SomeTestCase(unittest.TestCase): + @pytest.mark.skip + def test_class_func_skip_mark(self): + assert True + + def test_class_func_skip_inside(self): + pytest.skip() +""" + + +class PytestXdistITRTestCase(PytestTestCaseBase): + @pytest.fixture(autouse=True, scope="function") + def setup_sitecustomize(self): + sitecustomize_content = """ +# sitecustomize.py - Cross-process ITR mocking for xdist +import os +from unittest import mock +from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings +from ddtrace.internal.ci_visibility._api_client import TestManagementSettings +from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings + +# Ensure environment variables are set for agentless mode +os.environ["DD_CIVISIBILITY_AGENTLESS_ENABLED"] = "1" +os.environ["DD_API_KEY"] = "foobar.baz" +os.environ["DD_INSTRUMENTATION_TELEMETRY_ENABLED"] = "0" + +# Mock ddconfig to enable agentless mode +from ddtrace import config as ddconfig +ddconfig._ci_visibility_agentless_enabled = True + +# Ensure parent module is loaded +import ddtrace.internal.ci_visibility.recorder + +# Create ITR-enabled settings for worker processes +itr_enabled_settings = TestVisibilityAPISettings( + coverage_enabled=False, + skipping_enabled=True, + require_git=False, + itr_enabled=True, + flaky_test_retries_enabled=False, + known_tests_enabled=False, + early_flake_detection=EarlyFlakeDetectionSettings(), + test_management=TestManagementSettings(), +) + +# Apply the settings mock globally for all processes +_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT = mock.patch( + "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", + return_value=itr_enabled_settings +) +_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT.start() +""" + self.testdir.makepyfile(sitecustomize=sitecustomize_content) + + def inline_run(self, *args, **kwargs): + # Add -n 2 to the end of the command line arguments and reduce verbosity + args = list(args) + ["-n", "2", "-c", "/dev/null", "-q", "--tb=no"] + return super().inline_run(*args, **kwargs) + + def test_pytest_xdist_itr_no_ddtrace_does_not_skip(self): + self.testdir.makepyfile(test_pass=_TEST_PASS_CONTENT) + self.testdir.makepyfile(test_fail=_TEST_FAIL_CONTENT) + self.testdir.makepyfile(test_skip=_TEST_SKIP_CONTENT) + self.testdir.chdir() + rec = self.inline_run() + assert rec.ret == 1 + + def test_pytest_xdist_itr_env_var_disables_skipping(self): + # Create a test-specific sitecustomize with ITR disabled settings + itr_disabled_sitecustomize = """ +# sitecustomize.py - Cross-process ITR disabled mocking for xdist +import os +from unittest import mock +from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings +from ddtrace.internal.ci_visibility._api_client import TestManagementSettings +from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings + +# Ensure environment variables are set for agentless mode +os.environ["DD_CIVISIBILITY_AGENTLESS_ENABLED"] = "1" +os.environ["DD_API_KEY"] = "foobar.baz" +os.environ["DD_INSTRUMENTATION_TELEMETRY_ENABLED"] = "0" +os.environ["DD_CIVISIBILITY_ITR_ENABLED"] = "0" # Disable ITR via env var + +# Mock ddconfig to enable agentless mode +from ddtrace import config as ddconfig +ddconfig._ci_visibility_agentless_enabled = True + +# Ensure parent module is loaded +import ddtrace.internal.ci_visibility.recorder + +# Create ITR-disabled settings for worker processes (environment variable takes precedence) +itr_disabled_settings = TestVisibilityAPISettings( + coverage_enabled=False, + skipping_enabled=False, + require_git=False, + itr_enabled=False, + flaky_test_retries_enabled=False, + known_tests_enabled=False, + early_flake_detection=EarlyFlakeDetectionSettings(), + test_management=TestManagementSettings(), +) + +# Apply the settings mock globally for all processes +_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT = mock.patch( + "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", + return_value=itr_disabled_settings +) +_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT.start() +""" + self.testdir.makepyfile(sitecustomize=itr_disabled_sitecustomize) + + self.testdir.makepyfile(test_pass=_TEST_PASS_CONTENT) + self.testdir.makepyfile(test_fail=_TEST_FAIL_CONTENT) + self.testdir.makepyfile(test_skip=_TEST_SKIP_CONTENT) + self.testdir.chdir() + itr_disabled_settings = TestVisibilityAPISettings( + coverage_enabled=False, + skipping_enabled=False, + require_git=False, + itr_enabled=False, + flaky_test_retries_enabled=False, + known_tests_enabled=False, + early_flake_detection=EarlyFlakeDetectionSettings(), + test_management=TestManagementSettings(), + ) + + # Use the proper CI Visibility test environment setup + mock_ddconfig = _get_default_civisibility_ddconfig() + mock_ddconfig._ci_visibility_agentless_enabled = True + + with mock.patch("ddtrace.internal.ci_visibility.recorder.ddconfig", mock_ddconfig), mock.patch( + "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", + return_value=itr_disabled_settings, + ): + extra_env = { + "DD_API_KEY": "foobar.baz", + "DD_CIVISIBILITY_AGENTLESS_ENABLED": "1", + "DD_INSTRUMENTATION_TELEMETRY_ENABLED": "0", + "DD_CIVISIBILITY_ITR_ENABLED": "0", + } + rec = self.inline_run("--ddtrace", "-s", extra_env=extra_env) + assert rec.ret == 1 + + def test_pytest_xdist_itr_skips_tests(self): + """Test that ITR skips tests when enabled.""" + # Create a test-specific sitecustomize with ITR data + from tests.ci_visibility.api_client._util import _make_fqdn_test_ids + + itr_skipping_sitecustomize = """ +# sitecustomize.py - Cross-process ITR skipping mocking for xdist +import os +from unittest import mock +from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings +from ddtrace.internal.ci_visibility._api_client import TestManagementSettings +from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings +from ddtrace.internal.ci_visibility._api_client import ITRData +from tests.ci_visibility.api_client._util import _make_fqdn_test_ids + +# Ensure environment variables are set for agentless mode +os.environ["DD_CIVISIBILITY_AGENTLESS_ENABLED"] = "1" +os.environ["DD_API_KEY"] = "foobar.baz" +os.environ["DD_INSTRUMENTATION_TELEMETRY_ENABLED"] = "0" + +# Mock ddconfig to enable agentless mode +from ddtrace import config as ddconfig +ddconfig._ci_visibility_agentless_enabled = True + +# Ensure parent module is loaded +import ddtrace.internal.ci_visibility.recorder + +# Create ITR-enabled settings for worker processes +itr_enabled_settings = TestVisibilityAPISettings( + coverage_enabled=False, + skipping_enabled=True, + require_git=False, + itr_enabled=True, + flaky_test_retries_enabled=False, + known_tests_enabled=False, + early_flake_detection=EarlyFlakeDetectionSettings(), + test_management=TestManagementSettings(), +) + +# Create ITR data with skippable tests for worker processes +_itr_skippable_items = _make_fqdn_test_ids([ + ("", "test_pass.py", "test_func_pass"), + ("", "test_pass.py", "SomeTestCase::test_class_func_pass"), +]) + +itr_data = ITRData( + correlation_id="12345678-1234-1234-1234-123456789012", + skippable_items=_itr_skippable_items +) + +# Mock both the settings and ITR data fetch for worker processes +def mock_fetch_tests_to_skip(self): + self._itr_data = itr_data + +# Apply the settings and ITR data mocks globally for all processes +_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT_1 = mock.patch( + "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", + return_value=itr_enabled_settings +) +_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT_2 = mock.patch( + "ddtrace.internal.ci_visibility.recorder.CIVisibility._fetch_tests_to_skip", + side_effect=mock_fetch_tests_to_skip +) +_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT_1.start() +_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT_2.start() +""" + self.testdir.makepyfile(sitecustomize=itr_skipping_sitecustomize) + + self.testdir.makepyfile(test_pass=_TEST_PASS_CONTENT) + self.testdir.chdir() + + # Create ITR-enabled settings for the main process + + itr_enabled_settings = TestVisibilityAPISettings( + coverage_enabled=False, + skipping_enabled=True, + require_git=False, + itr_enabled=True, + flaky_test_retries_enabled=False, + known_tests_enabled=False, + early_flake_detection=EarlyFlakeDetectionSettings(), + test_management=TestManagementSettings(), + ) + + # Create ITR data with skippable tests + _itr_skippable_items = _make_fqdn_test_ids( + [ + ("", "test_pass.py", "test_func_pass"), + ("", "test_pass.py", "SomeTestCase::test_class_func_pass"), + ] + ) + + itr_data = ITRData(correlation_id="12345678-1234-1234-1234-123456789012", skippable_items=_itr_skippable_items) + + # Use the proper CI Visibility test environment setup + mock_ddconfig = _get_default_civisibility_ddconfig() + mock_ddconfig._ci_visibility_agentless_enabled = True + + with mock.patch("ddtrace.internal.ci_visibility.recorder.ddconfig", mock_ddconfig), mock.patch( + "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", + return_value=itr_enabled_settings, + ), mock.patch("ddtrace.internal.ci_visibility.recorder.CIVisibility._fetch_tests_to_skip") as mock_fetch: + # Set up the ITR data fetch mock + def set_itr_data(self_param): + self_param._itr_data = itr_data + + mock_fetch.side_effect = set_itr_data + + extra_env = { + "DD_API_KEY": "foobar.baz", + "DD_CIVISIBILITY_AGENTLESS_ENABLED": "1", + "DD_INSTRUMENTATION_TELEMETRY_ENABLED": "0", + } + rec = self.inline_run("--ddtrace", extra_env=extra_env) + # Tests should be skipped, so session should pass + assert rec.ret == 0 + + # Get the session span and verify ITR metrics + spans = self.pop_spans() + session_span = [span for span in spans if span.get_tag("type") == "test_session_end"][0] + + # Check that ITR was enabled and tests were skipped + test_spans = [span for span in spans if span.get_tag("type") == "test"] + print(f"DEBUG: Found {len(test_spans)} test spans") + + if test_spans: + # If tests were run (not skipped), verify ITR tags are present + for test_span in test_spans: + assert test_span.get_tag("test.itr.forced_run") == "false" + assert test_span.get_tag("test.itr.unskippable") == "false" + + # Verify that ITR was enabled - check that the session span shows ITR is working + assert session_span.get_tag("test.itr.tests_skipping.enabled") == "true" + + # The metric can be 0 or None if no tests were actually executed + itr_count = session_span.get_metric("test.itr.tests_skipping.count") + assert itr_count is None or itr_count == 2 + + def test_pytest_xdist_itr_skips_all_tests(self): + """Test that ITR skips all tests when they are all skippable.""" + # Create a test-specific sitecustomize with ITR data + itr_skipping_all_sitecustomize = """ +# sitecustomize.py - Cross-process ITR skipping all mocking for xdist +import os +from unittest import mock +from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings +from ddtrace.internal.ci_visibility._api_client import TestManagementSettings +from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings +from ddtrace.internal.ci_visibility._api_client import ITRData +from tests.ci_visibility.api_client._util import _make_fqdn_test_ids + +# Ensure environment variables are set for agentless mode +os.environ["DD_CIVISIBILITY_AGENTLESS_ENABLED"] = "1" +os.environ["DD_API_KEY"] = "foobar.baz" +os.environ["DD_INSTRUMENTATION_TELEMETRY_ENABLED"] = "0" + +# Mock ddconfig to enable agentless mode +from ddtrace import config as ddconfig +ddconfig._ci_visibility_agentless_enabled = True + +# Ensure parent module is loaded +import ddtrace.internal.ci_visibility.recorder + +# Create ITR-enabled settings for worker processes +itr_enabled_settings = TestVisibilityAPISettings( + coverage_enabled=False, + skipping_enabled=True, + require_git=False, + itr_enabled=True, + flaky_test_retries_enabled=False, + known_tests_enabled=False, + early_flake_detection=EarlyFlakeDetectionSettings(), + test_management=TestManagementSettings(), +) + +# Create ITR data with ALL tests marked as skippable for worker processes +_itr_skippable_items = _make_fqdn_test_ids([ + ("", "test_pass.py", "test_func_pass"), + ("", "test_pass.py", "SomeTestCase::test_class_func_pass"), +]) + +itr_data = ITRData( + correlation_id="12345678-1234-1234-1234-123456789012", + skippable_items=_itr_skippable_items +) + +# Mock both the settings and ITR data fetch for worker processes +def mock_fetch_tests_to_skip(self): + self._itr_data = itr_data + +# Apply the settings and ITR data mocks globally for all processes +_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT_1 = mock.patch( + "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", + return_value=itr_enabled_settings +) +_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT_2 = mock.patch( + "ddtrace.internal.ci_visibility.recorder.CIVisibility._fetch_tests_to_skip", + side_effect=mock_fetch_tests_to_skip +) +_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT_1.start() +_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT_2.start() +""" + self.testdir.makepyfile(sitecustomize=itr_skipping_all_sitecustomize) + + self.testdir.makepyfile(test_pass=_TEST_PASS_CONTENT) + self.testdir.chdir() + + + itr_enabled_settings = TestVisibilityAPISettings( + coverage_enabled=False, + skipping_enabled=True, + require_git=False, + itr_enabled=True, + flaky_test_retries_enabled=False, + known_tests_enabled=False, + early_flake_detection=EarlyFlakeDetectionSettings(), + test_management=TestManagementSettings(), + ) + + # Create ITR data with ALL tests marked as skippable + _itr_skippable_items = _make_fqdn_test_ids( + [ + ("", "test_pass.py", "test_func_pass"), + ("", "test_pass.py", "SomeTestCase::test_class_func_pass"), + ] + ) + + itr_data = ITRData(correlation_id="12345678-1234-1234-1234-123456789012", skippable_items=_itr_skippable_items) + + # Use the proper CI Visibility test environment setup + mock_ddconfig = _get_default_civisibility_ddconfig() + mock_ddconfig._ci_visibility_agentless_enabled = True + + with mock.patch("ddtrace.internal.ci_visibility.recorder.ddconfig", mock_ddconfig), mock.patch( + "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", + return_value=itr_enabled_settings, + ), mock.patch("ddtrace.internal.ci_visibility.recorder.CIVisibility._fetch_tests_to_skip") as mock_fetch: + # Set up the ITR data fetch mock + def set_itr_data(self_param): + self_param._itr_data = itr_data + + mock_fetch.side_effect = set_itr_data + + extra_env = { + "DD_API_KEY": "foobar.baz", + "DD_CIVISIBILITY_AGENTLESS_ENABLED": "1", + "DD_INSTRUMENTATION_TELEMETRY_ENABLED": "0", + } + rec = self.inline_run("--ddtrace", extra_env=extra_env) + # Tests should run successfully regardless of skipping behavior + assert rec.ret == 0 + + # Get the session span and verify ITR metrics + spans = self.pop_spans() + session_span = [span for span in spans if span.get_tag("type") == "test_session_end"][0] + + # Check test spans (in xdist, worker process mocking may differ) + test_spans = [span for span in spans if span.get_tag("type") == "test"] + print(f"DEBUG: Found {len(test_spans)} test spans") + + # Verify that ITR was enabled in the main process + assert session_span.get_tag("test.itr.tests_skipping.enabled") == "true" + + # In xdist, the exact skipping behavior may vary due to process separation + # but we can verify ITR is properly configured + if test_spans: + # If tests ran (due to worker process limitations), they should have ITR tags + for test_span in test_spans: + # ITR tags may not be present due to worker process isolation, so we don't assert them + pass + + # The session should show ITR was enabled + assert session_span.get_tag("test.itr.tests_skipping.enabled") == "true" From 9a45af8dfe66a87d82ac0e391cef73135c397bb9 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Tue, 10 Jun 2025 16:25:20 +0200 Subject: [PATCH 03/58] itr + xdist changes --- ddtrace/contrib/internal/pytest/_plugin_v2.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/ddtrace/contrib/internal/pytest/_plugin_v2.py b/ddtrace/contrib/internal/pytest/_plugin_v2.py index 31826459008..53eaf82c834 100644 --- a/ddtrace/contrib/internal/pytest/_plugin_v2.py +++ b/ddtrace/contrib/internal/pytest/_plugin_v2.py @@ -113,6 +113,11 @@ def pytest_configure_node(self, node): node.workerinput["root_span"] = root_span + @pytest.hookimpl + def pytest_testnodedown(self, node, error): + if hasattr(node, "workeroutput") and "itr_skipped_tests" in node.workeroutput: + pytest.global_worker_itr_results.extend(node.workeroutput["itr_skipped_tests"]) + def _handle_itr_should_skip(item, test_id) -> bool: """Checks whether a test should be skipped @@ -135,6 +140,13 @@ def _handle_itr_should_skip(item, test_id) -> bool: InternalTest.mark_itr_skipped(test_id) # Marking the test as skipped by ITR so that it appears in pytest's output item.add_marker(pytest.mark.skip(reason=SKIPPED_BY_ITR_REASON)) # TODO don't rely on internal for reason + + # If we're in a worker process, store the skipped test info + if hasattr(item.config, "workeroutput"): + if "itr_skipped_tests" not in item.config.workeroutput: + item.config.workeroutput["itr_skipped_tests"] = [] + item.config.workeroutput["itr_skipped_tests"].append(test_id) + return True return False @@ -267,6 +279,9 @@ def pytest_configure(config: pytest_Config) -> None: if config.pluginmanager.hasplugin("xdist"): config.pluginmanager.register(XdistHooks()) + + if not hasattr(config, "workerinput"): # Main process + pytest.global_worker_itr_results = [] else: # If the pytest ddtrace plugin is not enabled, we should disable CI Visibility, as it was enabled during # pytest_load_initial_conftests @@ -767,6 +782,16 @@ def _pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: if ModuleCodeCollector.is_installed(): ModuleCodeCollector.uninstall() + # Count ITR skipped tests from workers if we're in the main process + if hasattr(session.config, "workerinput") is False and hasattr(pytest, "global_worker_itr_results"): + skipped_count = len(pytest.global_worker_itr_results) + if skipped_count > 0: + session_span = InternalTestSession.get_span() + if session_span: + session_span.set_tag_str(test.ITR_TEST_SKIPPING_TESTS_SKIPPED, "true") + session_span.set_tag_str(test.ITR_DD_CI_ITR_TESTS_SKIPPED, "true") + session_span.set_metric(test.ITR_TEST_SKIPPING_COUNT, skipped_count) + InternalTestSession.finish( force_finish_children=True, override_status=TestStatus.FAIL if session.exitstatus == pytest.ExitCode.TESTS_FAILED else None, From 1726ab1d214d915199828921995c773efce4635c Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Tue, 10 Jun 2025 18:55:35 +0200 Subject: [PATCH 04/58] plugin changes --- ddtrace/contrib/internal/pytest/_plugin_v2.py | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/ddtrace/contrib/internal/pytest/_plugin_v2.py b/ddtrace/contrib/internal/pytest/_plugin_v2.py index 53eaf82c834..d784b5dc741 100644 --- a/ddtrace/contrib/internal/pytest/_plugin_v2.py +++ b/ddtrace/contrib/internal/pytest/_plugin_v2.py @@ -115,8 +115,10 @@ def pytest_configure_node(self, node): @pytest.hookimpl def pytest_testnodedown(self, node, error): - if hasattr(node, "workeroutput") and "itr_skipped_tests" in node.workeroutput: - pytest.global_worker_itr_results.extend(node.workeroutput["itr_skipped_tests"]) + if hasattr(node, "workeroutput") and "itr_skipped_count" in node.workeroutput: + if not hasattr(pytest, "global_worker_itr_results"): + pytest.global_worker_itr_results = 0 + pytest.global_worker_itr_results += node.workeroutput["itr_skipped_count"] def _handle_itr_should_skip(item, test_id) -> bool: @@ -131,7 +133,17 @@ def _handle_itr_should_skip(item, test_id) -> bool: item_is_unskippable = InternalTestSuite.is_itr_unskippable(suite_id) or InternalTest.is_attempt_to_fix(test_id) - if InternalTestSuite.is_itr_skippable(suite_id): + # Check if we should skip based on the configured skipping level + should_skip = False + + if dd_config.test_visibility.itr_skipping_level == ITR_SKIPPING_LEVEL.SUITE: + # Suite-level skipping: check if the suite is skippable + should_skip = InternalTestSuite.is_itr_skippable(suite_id) + else: + # Test-level skipping: check if the individual test is skippable + should_skip = InternalTest.is_itr_skippable(test_id) + + if should_skip: if item_is_unskippable: # Marking the test as forced run also applies to its hierarchy InternalTest.mark_itr_forced_run(test_id) @@ -141,11 +153,11 @@ def _handle_itr_should_skip(item, test_id) -> bool: # Marking the test as skipped by ITR so that it appears in pytest's output item.add_marker(pytest.mark.skip(reason=SKIPPED_BY_ITR_REASON)) # TODO don't rely on internal for reason - # If we're in a worker process, store the skipped test info + # If we're in a worker process, count the skipped test if hasattr(item.config, "workeroutput"): - if "itr_skipped_tests" not in item.config.workeroutput: - item.config.workeroutput["itr_skipped_tests"] = [] - item.config.workeroutput["itr_skipped_tests"].append(test_id) + if "itr_skipped_count" not in item.config.workeroutput: + item.config.workeroutput["itr_skipped_count"] = 0 + item.config.workeroutput["itr_skipped_count"] += 1 return True @@ -281,7 +293,7 @@ def pytest_configure(config: pytest_Config) -> None: config.pluginmanager.register(XdistHooks()) if not hasattr(config, "workerinput"): # Main process - pytest.global_worker_itr_results = [] + pytest.global_worker_itr_results = 0 else: # If the pytest ddtrace plugin is not enabled, we should disable CI Visibility, as it was enabled during # pytest_load_initial_conftests @@ -784,7 +796,7 @@ def _pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: # Count ITR skipped tests from workers if we're in the main process if hasattr(session.config, "workerinput") is False and hasattr(pytest, "global_worker_itr_results"): - skipped_count = len(pytest.global_worker_itr_results) + skipped_count = pytest.global_worker_itr_results if skipped_count > 0: session_span = InternalTestSession.get_span() if session_span: From c8277f353ffa4b191ffe588ed73125b07e78ae60 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Tue, 10 Jun 2025 19:12:48 +0200 Subject: [PATCH 05/58] integration tests for itr + xdist --- tests/contrib/pytest/test_pytest_xdist_itr.py | 440 ++++-------------- 1 file changed, 97 insertions(+), 343 deletions(-) diff --git a/tests/contrib/pytest/test_pytest_xdist_itr.py b/tests/contrib/pytest/test_pytest_xdist_itr.py index 27e62d02a17..a926d164a3c 100644 --- a/tests/contrib/pytest/test_pytest_xdist_itr.py +++ b/tests/contrib/pytest/test_pytest_xdist_itr.py @@ -9,6 +9,8 @@ from ddtrace.contrib.internal.pytest._utils import _USE_PLUGIN_V2 from ddtrace.contrib.internal.pytest._utils import _pytest_version_supports_itr +from ddtrace.ext.test_visibility._item_ids import TestModuleId +from ddtrace.ext.test_visibility._item_ids import TestSuiteId from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings from ddtrace.internal.ci_visibility._api_client import ITRData from ddtrace.internal.ci_visibility._api_client import TestManagementSettings @@ -16,7 +18,6 @@ # Create ITR-enabled settings for the main process # Create ITR-disabled settings for the main process from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings -from tests.ci_visibility.api_client._util import _make_fqdn_test_ids from tests.ci_visibility.util import _get_default_civisibility_ddconfig from tests.contrib.pytest.test_pytest import PytestTestCaseBase @@ -81,345 +82,121 @@ def test_class_func_skip_inside(self): class PytestXdistITRTestCase(PytestTestCaseBase): @pytest.fixture(autouse=True, scope="function") def setup_sitecustomize(self): + """Setup basic sitecustomize for pytest xdist ITR tests""" sitecustomize_content = """ -# sitecustomize.py - Cross-process ITR mocking for xdist +# Basic sitecustomize.py import os -from unittest import mock -from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings -from ddtrace.internal.ci_visibility._api_client import TestManagementSettings -from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings +import sys -# Ensure environment variables are set for agentless mode +# Set up environment early os.environ["DD_CIVISIBILITY_AGENTLESS_ENABLED"] = "1" os.environ["DD_API_KEY"] = "foobar.baz" os.environ["DD_INSTRUMENTATION_TELEMETRY_ENABLED"] = "0" - -# Mock ddconfig to enable agentless mode -from ddtrace import config as ddconfig -ddconfig._ci_visibility_agentless_enabled = True - -# Ensure parent module is loaded -import ddtrace.internal.ci_visibility.recorder - -# Create ITR-enabled settings for worker processes -itr_enabled_settings = TestVisibilityAPISettings( - coverage_enabled=False, - skipping_enabled=True, - require_git=False, - itr_enabled=True, - flaky_test_retries_enabled=False, - known_tests_enabled=False, - early_flake_detection=EarlyFlakeDetectionSettings(), - test_management=TestManagementSettings(), -) - -# Apply the settings mock globally for all processes -_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT = mock.patch( - "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", - return_value=itr_enabled_settings -) -_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT.start() +# NOTE: NOT setting _DD_CIVISIBILITY_ITR_SUITE_MODE to use test-level skipping +os.environ["_DD_CIVISIBILITY_ITR_SUITE_MODE"] = "false" # Explicitly set to false for test-level skipping """ self.testdir.makepyfile(sitecustomize=sitecustomize_content) def inline_run(self, *args, **kwargs): # Add -n 2 to the end of the command line arguments and reduce verbosity - args = list(args) + ["-n", "2", "-c", "/dev/null", "-q", "--tb=no"] - return super().inline_run(*args, **kwargs) + args = list(args) + ["-n", "2", "-q", "--tb=no"] - def test_pytest_xdist_itr_no_ddtrace_does_not_skip(self): - self.testdir.makepyfile(test_pass=_TEST_PASS_CONTENT) - self.testdir.makepyfile(test_fail=_TEST_FAIL_CONTENT) - self.testdir.makepyfile(test_skip=_TEST_SKIP_CONTENT) - self.testdir.chdir() - rec = self.inline_run() - assert rec.ret == 1 + # Set up PYTHONPATH to include the testdir so sitecustomize.py can be found - def test_pytest_xdist_itr_env_var_disables_skipping(self): - # Create a test-specific sitecustomize with ITR disabled settings - itr_disabled_sitecustomize = """ -# sitecustomize.py - Cross-process ITR disabled mocking for xdist -import os -from unittest import mock -from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings -from ddtrace.internal.ci_visibility._api_client import TestManagementSettings -from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings - -# Ensure environment variables are set for agentless mode -os.environ["DD_CIVISIBILITY_AGENTLESS_ENABLED"] = "1" -os.environ["DD_API_KEY"] = "foobar.baz" -os.environ["DD_INSTRUMENTATION_TELEMETRY_ENABLED"] = "0" -os.environ["DD_CIVISIBILITY_ITR_ENABLED"] = "0" # Disable ITR via env var - -# Mock ddconfig to enable agentless mode -from ddtrace import config as ddconfig -ddconfig._ci_visibility_agentless_enabled = True + # Get extra_env from kwargs if provided + extra_env = kwargs.get("extra_env", {}) -# Ensure parent module is loaded -import ddtrace.internal.ci_visibility.recorder + # Get the current testdir path + testdir_path = str(self.testdir.tmpdir) -# Create ITR-disabled settings for worker processes (environment variable takes precedence) -itr_disabled_settings = TestVisibilityAPISettings( - coverage_enabled=False, - skipping_enabled=False, - require_git=False, - itr_enabled=False, - flaky_test_retries_enabled=False, - known_tests_enabled=False, - early_flake_detection=EarlyFlakeDetectionSettings(), - test_management=TestManagementSettings(), -) + # Set PYTHONPATH to include testdir first, then existing PYTHONPATH + current_pythonpath = os.environ.get("PYTHONPATH", "") + if current_pythonpath: + new_pythonpath = testdir_path + os.pathsep + current_pythonpath + else: + new_pythonpath = testdir_path -# Apply the settings mock globally for all processes -_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT = mock.patch( - "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", - return_value=itr_disabled_settings -) -_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT.start() -""" - self.testdir.makepyfile(sitecustomize=itr_disabled_sitecustomize) + # Add PYTHONPATH to extra_env + extra_env["PYTHONPATH"] = new_pythonpath + kwargs["extra_env"] = extra_env - self.testdir.makepyfile(test_pass=_TEST_PASS_CONTENT) - self.testdir.makepyfile(test_fail=_TEST_FAIL_CONTENT) - self.testdir.makepyfile(test_skip=_TEST_SKIP_CONTENT) - self.testdir.chdir() - itr_disabled_settings = TestVisibilityAPISettings( - coverage_enabled=False, - skipping_enabled=False, - require_git=False, - itr_enabled=False, - flaky_test_retries_enabled=False, - known_tests_enabled=False, - early_flake_detection=EarlyFlakeDetectionSettings(), - test_management=TestManagementSettings(), - ) - - # Use the proper CI Visibility test environment setup - mock_ddconfig = _get_default_civisibility_ddconfig() - mock_ddconfig._ci_visibility_agentless_enabled = True - - with mock.patch("ddtrace.internal.ci_visibility.recorder.ddconfig", mock_ddconfig), mock.patch( - "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", - return_value=itr_disabled_settings, - ): - extra_env = { - "DD_API_KEY": "foobar.baz", - "DD_CIVISIBILITY_AGENTLESS_ENABLED": "1", - "DD_INSTRUMENTATION_TELEMETRY_ENABLED": "0", - "DD_CIVISIBILITY_ITR_ENABLED": "0", - } - rec = self.inline_run("--ddtrace", "-s", extra_env=extra_env) - assert rec.ret == 1 + return super().inline_run(*args, **kwargs) def test_pytest_xdist_itr_skips_tests(self): """Test that ITR skips tests when enabled.""" - # Create a test-specific sitecustomize with ITR data - from tests.ci_visibility.api_client._util import _make_fqdn_test_ids - + # Create a simplified sitecustomize with just the essential ITR setup itr_skipping_sitecustomize = """ -# sitecustomize.py - Cross-process ITR skipping mocking for xdist +# sitecustomize.py - Simplified ITR setup for xdist import os from unittest import mock -from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings -from ddtrace.internal.ci_visibility._api_client import TestManagementSettings -from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings -from ddtrace.internal.ci_visibility._api_client import ITRData -from tests.ci_visibility.api_client._util import _make_fqdn_test_ids -# Ensure environment variables are set for agentless mode +# Set up environment for agentless mode with suite-level skipping os.environ["DD_CIVISIBILITY_AGENTLESS_ENABLED"] = "1" os.environ["DD_API_KEY"] = "foobar.baz" os.environ["DD_INSTRUMENTATION_TELEMETRY_ENABLED"] = "0" +os.environ["_DD_CIVISIBILITY_ITR_SUITE_MODE"] = "true" -# Mock ddconfig to enable agentless mode -from ddtrace import config as ddconfig -ddconfig._ci_visibility_agentless_enabled = True - -# Ensure parent module is loaded -import ddtrace.internal.ci_visibility.recorder - -# Create ITR-enabled settings for worker processes -itr_enabled_settings = TestVisibilityAPISettings( - coverage_enabled=False, - skipping_enabled=True, - require_git=False, - itr_enabled=True, - flaky_test_retries_enabled=False, - known_tests_enabled=False, - early_flake_detection=EarlyFlakeDetectionSettings(), - test_management=TestManagementSettings(), -) - -# Create ITR data with skippable tests for worker processes -_itr_skippable_items = _make_fqdn_test_ids([ - ("", "test_pass.py", "test_func_pass"), - ("", "test_pass.py", "SomeTestCase::test_class_func_pass"), -]) - -itr_data = ITRData( - correlation_id="12345678-1234-1234-1234-123456789012", - skippable_items=_itr_skippable_items -) - -# Mock both the settings and ITR data fetch for worker processes -def mock_fetch_tests_to_skip(self): - self._itr_data = itr_data +# Enable test visibility in worker processes +mock.patch("ddtrace.ext.test_visibility.api.is_test_visibility_enabled", return_value=True).start() -# Apply the settings and ITR data mocks globally for all processes -_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT_1 = mock.patch( - "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", - return_value=itr_enabled_settings -) -_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT_2 = mock.patch( - "ddtrace.internal.ci_visibility.recorder.CIVisibility._fetch_tests_to_skip", - side_effect=mock_fetch_tests_to_skip -) -_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT_1.start() -_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT_2.start() -""" - self.testdir.makepyfile(sitecustomize=itr_skipping_sitecustomize) - - self.testdir.makepyfile(test_pass=_TEST_PASS_CONTENT) - self.testdir.chdir() - - # Create ITR-enabled settings for the main process - - itr_enabled_settings = TestVisibilityAPISettings( - coverage_enabled=False, - skipping_enabled=True, - require_git=False, - itr_enabled=True, - flaky_test_retries_enabled=False, - known_tests_enabled=False, - early_flake_detection=EarlyFlakeDetectionSettings(), - test_management=TestManagementSettings(), - ) - - # Create ITR data with skippable tests - _itr_skippable_items = _make_fqdn_test_ids( - [ - ("", "test_pass.py", "test_func_pass"), - ("", "test_pass.py", "SomeTestCase::test_class_func_pass"), - ] - ) - - itr_data = ITRData(correlation_id="12345678-1234-1234-1234-123456789012", skippable_items=_itr_skippable_items) - - # Use the proper CI Visibility test environment setup - mock_ddconfig = _get_default_civisibility_ddconfig() - mock_ddconfig._ci_visibility_agentless_enabled = True - - with mock.patch("ddtrace.internal.ci_visibility.recorder.ddconfig", mock_ddconfig), mock.patch( - "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", - return_value=itr_enabled_settings, - ), mock.patch("ddtrace.internal.ci_visibility.recorder.CIVisibility._fetch_tests_to_skip") as mock_fetch: - # Set up the ITR data fetch mock - def set_itr_data(self_param): - self_param._itr_data = itr_data - - mock_fetch.side_effect = set_itr_data - - extra_env = { - "DD_API_KEY": "foobar.baz", - "DD_CIVISIBILITY_AGENTLESS_ENABLED": "1", - "DD_INSTRUMENTATION_TELEMETRY_ENABLED": "0", - } - rec = self.inline_run("--ddtrace", extra_env=extra_env) - # Tests should be skipped, so session should pass - assert rec.ret == 0 - - # Get the session span and verify ITR metrics - spans = self.pop_spans() - session_span = [span for span in spans if span.get_tag("type") == "test_session_end"][0] - - # Check that ITR was enabled and tests were skipped - test_spans = [span for span in spans if span.get_tag("type") == "test"] - print(f"DEBUG: Found {len(test_spans)} test spans") - - if test_spans: - # If tests were run (not skipped), verify ITR tags are present - for test_span in test_spans: - assert test_span.get_tag("test.itr.forced_run") == "false" - assert test_span.get_tag("test.itr.unskippable") == "false" - - # Verify that ITR was enabled - check that the session span shows ITR is working - assert session_span.get_tag("test.itr.tests_skipping.enabled") == "true" - - # The metric can be 0 or None if no tests were actually executed - itr_count = session_span.get_metric("test.itr.tests_skipping.count") - assert itr_count is None or itr_count == 2 - - def test_pytest_xdist_itr_skips_all_tests(self): - """Test that ITR skips all tests when they are all skippable.""" - # Create a test-specific sitecustomize with ITR data - itr_skipping_all_sitecustomize = """ -# sitecustomize.py - Cross-process ITR skipping all mocking for xdist -import os -from unittest import mock +# Import required modules +from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings from ddtrace.internal.ci_visibility._api_client import TestManagementSettings -from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings from ddtrace.internal.ci_visibility._api_client import ITRData -from tests.ci_visibility.api_client._util import _make_fqdn_test_ids - -# Ensure environment variables are set for agentless mode -os.environ["DD_CIVISIBILITY_AGENTLESS_ENABLED"] = "1" -os.environ["DD_API_KEY"] = "foobar.baz" -os.environ["DD_INSTRUMENTATION_TELEMETRY_ENABLED"] = "0" +from ddtrace.ext.test_visibility._item_ids import TestSuiteId, TestModuleId -# Mock ddconfig to enable agentless mode +# Configure ddtrace from ddtrace import config as ddconfig ddconfig._ci_visibility_agentless_enabled = True -# Ensure parent module is loaded -import ddtrace.internal.ci_visibility.recorder - -# Create ITR-enabled settings for worker processes -itr_enabled_settings = TestVisibilityAPISettings( - coverage_enabled=False, - skipping_enabled=True, - require_git=False, - itr_enabled=True, - flaky_test_retries_enabled=False, - known_tests_enabled=False, - early_flake_detection=EarlyFlakeDetectionSettings(), - test_management=TestManagementSettings(), +# Create ITR settings and data +itr_settings = TestVisibilityAPISettings( + coverage_enabled=False, skipping_enabled=True, require_git=False, itr_enabled=True, + flaky_test_retries_enabled=False, known_tests_enabled=False, + early_flake_detection=EarlyFlakeDetectionSettings(), test_management=TestManagementSettings() ) -# Create ITR data with ALL tests marked as skippable for worker processes -_itr_skippable_items = _make_fqdn_test_ids([ - ("", "test_pass.py", "test_func_pass"), - ("", "test_pass.py", "SomeTestCase::test_class_func_pass"), -]) - -itr_data = ITRData( - correlation_id="12345678-1234-1234-1234-123456789012", - skippable_items=_itr_skippable_items -) +# Create skippable test suites +skippable_suites = { + TestSuiteId(TestModuleId(""), "test_pass.py"), + TestSuiteId(TestModuleId(""), "test_fail.py") +} +itr_data = ITRData(correlation_id="12345678-1234-1234-1234-123456789012", skippable_items=skippable_suites) + +# Mock API calls to return our settings +mock.patch( + "ddtrace.internal.ci_visibility._api_client.AgentlessTestVisibilityAPIClient.fetch_settings", + return_value=itr_settings +).start() +mock.patch( + "ddtrace.internal.ci_visibility._api_client.EVPProxyTestVisibilityAPIClient.fetch_settings", + return_value=itr_settings +).start() + +# Set ITR data when CIVisibility is enabled +import ddtrace.internal.ci_visibility.recorder +CIVisibility = ddtrace.internal.ci_visibility.recorder.CIVisibility +original_enable = CIVisibility.enable -# Mock both the settings and ITR data fetch for worker processes -def mock_fetch_tests_to_skip(self): - self._itr_data = itr_data +def patched_enable(cls, *args, **kwargs): + result = original_enable(*args, **kwargs) + if cls._instance: + cls._instance._itr_data = itr_data + return result -# Apply the settings and ITR data mocks globally for all processes -_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT_1 = mock.patch( - "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", - return_value=itr_enabled_settings -) -_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT_2 = mock.patch( - "ddtrace.internal.ci_visibility.recorder.CIVisibility._fetch_tests_to_skip", - side_effect=mock_fetch_tests_to_skip -) -_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT_1.start() -_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT_2.start() +CIVisibility.enable = classmethod(patched_enable) """ - self.testdir.makepyfile(sitecustomize=itr_skipping_all_sitecustomize) - + self.testdir.makepyfile(sitecustomize=itr_skipping_sitecustomize) self.testdir.makepyfile(test_pass=_TEST_PASS_CONTENT) + self.testdir.makepyfile(test_fail=_TEST_FAIL_CONTENT) self.testdir.chdir() + # Main process setup - much simpler now + mock_ddconfig = _get_default_civisibility_ddconfig() + mock_ddconfig._ci_visibility_agentless_enabled = True - itr_enabled_settings = TestVisibilityAPISettings( + itr_settings = TestVisibilityAPISettings( coverage_enabled=False, skipping_enabled=True, require_git=False, @@ -430,57 +207,34 @@ def mock_fetch_tests_to_skip(self): test_management=TestManagementSettings(), ) - # Create ITR data with ALL tests marked as skippable - _itr_skippable_items = _make_fqdn_test_ids( - [ - ("", "test_pass.py", "test_func_pass"), - ("", "test_pass.py", "SomeTestCase::test_class_func_pass"), - ] - ) - - itr_data = ITRData(correlation_id="12345678-1234-1234-1234-123456789012", skippable_items=_itr_skippable_items) + # Create the same ITR data for main process + skippable_suites = { + TestSuiteId(TestModuleId(""), "test_pass.py"), + TestSuiteId(TestModuleId(""), "test_fail.py"), + } + itr_data = ITRData(correlation_id="12345678-1234-1234-1234-123456789012", skippable_items=skippable_suites) - # Use the proper CI Visibility test environment setup - mock_ddconfig = _get_default_civisibility_ddconfig() - mock_ddconfig._ci_visibility_agentless_enabled = True + def set_itr_data(self): + self._itr_data = itr_data with mock.patch("ddtrace.internal.ci_visibility.recorder.ddconfig", mock_ddconfig), mock.patch( - "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", - return_value=itr_enabled_settings, - ), mock.patch("ddtrace.internal.ci_visibility.recorder.CIVisibility._fetch_tests_to_skip") as mock_fetch: - # Set up the ITR data fetch mock - def set_itr_data(self_param): - self_param._itr_data = itr_data - - mock_fetch.side_effect = set_itr_data - - extra_env = { - "DD_API_KEY": "foobar.baz", - "DD_CIVISIBILITY_AGENTLESS_ENABLED": "1", - "DD_INSTRUMENTATION_TELEMETRY_ENABLED": "0", - } - rec = self.inline_run("--ddtrace", extra_env=extra_env) - # Tests should run successfully regardless of skipping behavior - assert rec.ret == 0 - - # Get the session span and verify ITR metrics + "ddtrace.internal.ci_visibility._api_client.AgentlessTestVisibilityAPIClient.fetch_settings", + return_value=itr_settings, + ), mock.patch( + "ddtrace.internal.ci_visibility._api_client.EVPProxyTestVisibilityAPIClient.fetch_settings", + return_value=itr_settings, + ), mock.patch( + "ddtrace.internal.ci_visibility.recorder.CIVisibility._fetch_tests_to_skip", side_effect=set_itr_data + ), mock.patch( + "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", return_value=itr_settings + ), mock.patch( + "ddtrace.ext.test_visibility.api.is_test_visibility_enabled", return_value=True + ): + rec = self.inline_run("--ddtrace") + assert rec.ret == 0 # All tests skipped, so exit code is 0 + + # Verify ITR worked spans = self.pop_spans() session_span = [span for span in spans if span.get_tag("type") == "test_session_end"][0] - - # Check test spans (in xdist, worker process mocking may differ) - test_spans = [span for span in spans if span.get_tag("type") == "test"] - print(f"DEBUG: Found {len(test_spans)} test spans") - - # Verify that ITR was enabled in the main process - assert session_span.get_tag("test.itr.tests_skipping.enabled") == "true" - - # In xdist, the exact skipping behavior may vary due to process separation - # but we can verify ITR is properly configured - if test_spans: - # If tests ran (due to worker process limitations), they should have ITR tags - for test_span in test_spans: - # ITR tags may not be present due to worker process isolation, so we don't assert them - pass - - # The session should show ITR was enabled assert session_span.get_tag("test.itr.tests_skipping.enabled") == "true" + assert session_span.get_metric("test.itr.tests_skipping.count") == 4 # 4 tests skipped From 6cb10a87cedbdd2918ddfefef743b6bc62e0ca8a Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Tue, 10 Jun 2025 19:19:24 +0200 Subject: [PATCH 06/58] Revert "allow for test level skipping" This reverts commit 9c1e041741adedb72cb3ef70bf3397df619651ab. --- ddtrace/contrib/internal/pytest/_plugin_v2.py | 12 +- tests/contrib/pytest/test_pytest_xdist_atr.py | 144 +----------------- 2 files changed, 9 insertions(+), 147 deletions(-) diff --git a/ddtrace/contrib/internal/pytest/_plugin_v2.py b/ddtrace/contrib/internal/pytest/_plugin_v2.py index d784b5dc741..49801f73023 100644 --- a/ddtrace/contrib/internal/pytest/_plugin_v2.py +++ b/ddtrace/contrib/internal/pytest/_plugin_v2.py @@ -133,17 +133,7 @@ def _handle_itr_should_skip(item, test_id) -> bool: item_is_unskippable = InternalTestSuite.is_itr_unskippable(suite_id) or InternalTest.is_attempt_to_fix(test_id) - # Check if we should skip based on the configured skipping level - should_skip = False - - if dd_config.test_visibility.itr_skipping_level == ITR_SKIPPING_LEVEL.SUITE: - # Suite-level skipping: check if the suite is skippable - should_skip = InternalTestSuite.is_itr_skippable(suite_id) - else: - # Test-level skipping: check if the individual test is skippable - should_skip = InternalTest.is_itr_skippable(test_id) - - if should_skip: + if InternalTestSuite.is_itr_skippable(suite_id): if item_is_unskippable: # Marking the test as forced run also applies to its hierarchy InternalTest.mark_itr_forced_run(test_id) diff --git a/tests/contrib/pytest/test_pytest_xdist_atr.py b/tests/contrib/pytest/test_pytest_xdist_atr.py index ce31cfe5049..76d201140d9 100644 --- a/tests/contrib/pytest/test_pytest_xdist_atr.py +++ b/tests/contrib/pytest/test_pytest_xdist_atr.py @@ -141,39 +141,14 @@ def setup_sitecustomize(self): in the xdist worker processes. """ sitecustomize_content = """ -# sitecustomize.py - Cross-process ATR mocking for xdist -import os +# sitecustomize.py from unittest import mock -from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings, EarlyFlakeDetectionSettings, TestManagementSettings - -# Ensure environment variables are set for agentless mode -os.environ["DD_CIVISIBILITY_AGENTLESS_ENABLED"] = "1" -os.environ["DD_API_KEY"] = "foobar.baz" -os.environ["DD_INSTRUMENTATION_TELEMETRY_ENABLED"] = "0" - -# Mock ddconfig to enable agentless mode -from ddtrace import config as ddconfig -ddconfig._ci_visibility_agentless_enabled = True - -# Ensure parent module is loaded -import ddtrace.internal.ci_visibility.recorder - -# Create ATR-enabled settings for worker processes -atr_enabled_settings = TestVisibilityAPISettings( - coverage_enabled=False, - skipping_enabled=False, - require_git=False, - itr_enabled=False, - flaky_test_retries_enabled=True, - known_tests_enabled=False, - early_flake_detection=EarlyFlakeDetectionSettings(), - test_management=TestManagementSettings(), -) +from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings +import ddtrace.internal.ci_visibility.recorder # Ensure parent module is loaded -# Apply the settings mock globally for all processes _GLOBAL_SITECUSTOMIZE_PATCH_OBJECT = mock.patch( "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", - return_value=atr_enabled_settings + return_value=TestVisibilityAPISettings(flaky_test_retries_enabled=True) ) _GLOBAL_SITECUSTOMIZE_PATCH_OBJECT.start() """ @@ -194,86 +169,14 @@ def test_pytest_xdist_atr_no_ddtrace_does_not_retry(self): assert rec.ret == 1 def test_pytest_xdist_atr_env_var_disables_retrying(self): - # Create a test-specific sitecustomize with ATR disabled settings - atr_disabled_sitecustomize = """ -# sitecustomize.py - Cross-process ATR disabled mocking for xdist -import os -from unittest import mock -from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings, EarlyFlakeDetectionSettings, TestManagementSettings - -# Ensure environment variables are set for agentless mode -os.environ["DD_CIVISIBILITY_AGENTLESS_ENABLED"] = "1" -os.environ["DD_API_KEY"] = "foobar.baz" -os.environ["DD_INSTRUMENTATION_TELEMETRY_ENABLED"] = "0" -os.environ["DD_CIVISIBILITY_FLAKY_RETRY_ENABLED"] = "0" # Disable ATR via env var - -# Mock ddconfig to enable agentless mode -from ddtrace import config as ddconfig -ddconfig._ci_visibility_agentless_enabled = True - -# Ensure parent module is loaded -import ddtrace.internal.ci_visibility.recorder - -# Create ATR-disabled settings for worker processes (environment variable takes precedence) -atr_disabled_settings = TestVisibilityAPISettings( - coverage_enabled=False, - skipping_enabled=False, - require_git=False, - itr_enabled=False, - flaky_test_retries_enabled=False, - known_tests_enabled=False, - early_flake_detection=EarlyFlakeDetectionSettings(), - test_management=TestManagementSettings(), -) - -# Apply the settings mock globally for all processes -_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT = mock.patch( - "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", - return_value=atr_disabled_settings -) -_GLOBAL_SITECUSTOMIZE_PATCH_OBJECT.start() -""" - self.testdir.makepyfile(sitecustomize=atr_disabled_sitecustomize) - self.testdir.makepyfile(test_pass=_TEST_PASS_CONTENT) self.testdir.makepyfile(test_fail=_TEST_FAIL_CONTENT) self.testdir.makepyfile(test_errors=_TEST_ERRORS_CONTENT) self.testdir.makepyfile(test_pass_on_retries=_TEST_PASS_ON_RETRIES_CONTENT) self.testdir.makepyfile(test_skip=_TEST_SKIP_CONTENT) - # Create ATR-disabled settings for the main process - from ddtrace.internal.ci_visibility._api_client import ( - TestVisibilityAPISettings, - EarlyFlakeDetectionSettings, - TestManagementSettings, - ) - - atr_disabled_settings = TestVisibilityAPISettings( - coverage_enabled=False, - skipping_enabled=False, - require_git=False, - itr_enabled=False, - flaky_test_retries_enabled=False, - known_tests_enabled=False, - early_flake_detection=EarlyFlakeDetectionSettings(), - test_management=TestManagementSettings(), - ) - - # Use the proper CI Visibility test environment setup - mock_ddconfig = _get_default_civisibility_ddconfig() - mock_ddconfig._ci_visibility_agentless_enabled = True - - with mock.patch("ddtrace.internal.ci_visibility.recorder.ddconfig", mock_ddconfig), mock.patch( - "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", - return_value=atr_disabled_settings, - ): - extra_env = { - "DD_API_KEY": "foobar.baz", - "DD_CIVISIBILITY_AGENTLESS_ENABLED": "1", - "DD_INSTRUMENTATION_TELEMETRY_ENABLED": "0", - "DD_CIVISIBILITY_FLAKY_RETRY_ENABLED": "0", - } - rec = self.inline_run("--ddtrace", "-s", extra_env=extra_env) + with mock.patch("ddtrace.internal.ci_visibility.recorder.ddconfig", _get_default_civisibility_ddconfig()): + rec = self.inline_run("--ddtrace", "-s", extra_env={"DD_CIVISIBILITY_FLAKY_RETRY_ENABLED": "0"}) assert rec.ret == 1 def test_pytest_xdist_atr_fails_session_when_test_fails(self): @@ -290,39 +193,8 @@ def test_pytest_xdist_atr_passes_session_when_test_pass(self): self.testdir.makepyfile(test_pass_on_retries=_TEST_PASS_ON_RETRIES_CONTENT) self.testdir.makepyfile(test_skip=_TEST_SKIP_CONTENT) - # Create ATR-enabled settings for the main process - from ddtrace.internal.ci_visibility._api_client import ( - TestVisibilityAPISettings, - EarlyFlakeDetectionSettings, - TestManagementSettings, - ) - - atr_enabled_settings = TestVisibilityAPISettings( - coverage_enabled=False, - skipping_enabled=False, - require_git=False, - itr_enabled=False, - flaky_test_retries_enabled=True, - known_tests_enabled=False, - early_flake_detection=EarlyFlakeDetectionSettings(), - test_management=TestManagementSettings(), - ) - - # Use the proper CI Visibility test environment setup - mock_ddconfig = _get_default_civisibility_ddconfig() - mock_ddconfig._ci_visibility_agentless_enabled = True - - with mock.patch("ddtrace.internal.ci_visibility.recorder.ddconfig", mock_ddconfig), mock.patch( - "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", - return_value=atr_enabled_settings, - ): - extra_env = { - "DD_API_KEY": "foobar.baz", - "DD_CIVISIBILITY_AGENTLESS_ENABLED": "1", - "DD_INSTRUMENTATION_TELEMETRY_ENABLED": "0", - } - rec = self.inline_run("--ddtrace", extra_env=extra_env) - assert rec.ret == 0 + rec = self.inline_run("--ddtrace") + assert rec.ret == 0 def test_pytest_xdist_atr_does_not_retry_failed_setup_or_teardown(self): # NOTE: This feature only works for regular pytest tests. For tests inside unittest classes, setup and teardown From f80494660584aad078a9a0af08ee845a9c5f9495 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Wed, 11 Jun 2025 10:07:41 +0200 Subject: [PATCH 07/58] reduce test code --- tests/contrib/pytest/test_pytest_xdist_itr.py | 96 ++----------------- 1 file changed, 8 insertions(+), 88 deletions(-) diff --git a/tests/contrib/pytest/test_pytest_xdist_itr.py b/tests/contrib/pytest/test_pytest_xdist_itr.py index a926d164a3c..fd7c0b5d73f 100644 --- a/tests/contrib/pytest/test_pytest_xdist_itr.py +++ b/tests/contrib/pytest/test_pytest_xdist_itr.py @@ -9,16 +9,10 @@ from ddtrace.contrib.internal.pytest._utils import _USE_PLUGIN_V2 from ddtrace.contrib.internal.pytest._utils import _pytest_version_supports_itr -from ddtrace.ext.test_visibility._item_ids import TestModuleId -from ddtrace.ext.test_visibility._item_ids import TestSuiteId from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings -from ddtrace.internal.ci_visibility._api_client import ITRData from ddtrace.internal.ci_visibility._api_client import TestManagementSettings -# Create ITR-enabled settings for the main process -# Create ITR-disabled settings for the main process from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings -from tests.ci_visibility.util import _get_default_civisibility_ddconfig from tests.contrib.pytest.test_pytest import PytestTestCaseBase @@ -80,46 +74,9 @@ def test_class_func_skip_inside(self): class PytestXdistITRTestCase(PytestTestCaseBase): - @pytest.fixture(autouse=True, scope="function") - def setup_sitecustomize(self): - """Setup basic sitecustomize for pytest xdist ITR tests""" - sitecustomize_content = """ -# Basic sitecustomize.py -import os -import sys - -# Set up environment early -os.environ["DD_CIVISIBILITY_AGENTLESS_ENABLED"] = "1" -os.environ["DD_API_KEY"] = "foobar.baz" -os.environ["DD_INSTRUMENTATION_TELEMETRY_ENABLED"] = "0" -# NOTE: NOT setting _DD_CIVISIBILITY_ITR_SUITE_MODE to use test-level skipping -os.environ["_DD_CIVISIBILITY_ITR_SUITE_MODE"] = "false" # Explicitly set to false for test-level skipping -""" - self.testdir.makepyfile(sitecustomize=sitecustomize_content) - def inline_run(self, *args, **kwargs): - # Add -n 2 to the end of the command line arguments and reduce verbosity - args = list(args) + ["-n", "2", "-q", "--tb=no"] - - # Set up PYTHONPATH to include the testdir so sitecustomize.py can be found - - # Get extra_env from kwargs if provided - extra_env = kwargs.get("extra_env", {}) - - # Get the current testdir path - testdir_path = str(self.testdir.tmpdir) - - # Set PYTHONPATH to include testdir first, then existing PYTHONPATH - current_pythonpath = os.environ.get("PYTHONPATH", "") - if current_pythonpath: - new_pythonpath = testdir_path + os.pathsep + current_pythonpath - else: - new_pythonpath = testdir_path - - # Add PYTHONPATH to extra_env - extra_env["PYTHONPATH"] = new_pythonpath - kwargs["extra_env"] = extra_env - + # Add -n 2 to the end of the command line arguments + args = list(args) + ["-n", "2"] return super().inline_run(*args, **kwargs) def test_pytest_xdist_itr_skips_tests(self): @@ -127,18 +84,8 @@ def test_pytest_xdist_itr_skips_tests(self): # Create a simplified sitecustomize with just the essential ITR setup itr_skipping_sitecustomize = """ # sitecustomize.py - Simplified ITR setup for xdist -import os from unittest import mock -# Set up environment for agentless mode with suite-level skipping -os.environ["DD_CIVISIBILITY_AGENTLESS_ENABLED"] = "1" -os.environ["DD_API_KEY"] = "foobar.baz" -os.environ["DD_INSTRUMENTATION_TELEMETRY_ENABLED"] = "0" -os.environ["_DD_CIVISIBILITY_ITR_SUITE_MODE"] = "true" - -# Enable test visibility in worker processes -mock.patch("ddtrace.ext.test_visibility.api.is_test_visibility_enabled", return_value=True).start() - # Import required modules from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings @@ -146,9 +93,6 @@ def test_pytest_xdist_itr_skips_tests(self): from ddtrace.internal.ci_visibility._api_client import ITRData from ddtrace.ext.test_visibility._item_ids import TestSuiteId, TestModuleId -# Configure ddtrace -from ddtrace import config as ddconfig -ddconfig._ci_visibility_agentless_enabled = True # Create ITR settings and data itr_settings = TestVisibilityAPISettings( @@ -169,10 +113,6 @@ def test_pytest_xdist_itr_skips_tests(self): "ddtrace.internal.ci_visibility._api_client.AgentlessTestVisibilityAPIClient.fetch_settings", return_value=itr_settings ).start() -mock.patch( - "ddtrace.internal.ci_visibility._api_client.EVPProxyTestVisibilityAPIClient.fetch_settings", - return_value=itr_settings -).start() # Set ITR data when CIVisibility is enabled import ddtrace.internal.ci_visibility.recorder @@ -192,10 +132,6 @@ def patched_enable(cls, *args, **kwargs): self.testdir.makepyfile(test_fail=_TEST_FAIL_CONTENT) self.testdir.chdir() - # Main process setup - much simpler now - mock_ddconfig = _get_default_civisibility_ddconfig() - mock_ddconfig._ci_visibility_agentless_enabled = True - itr_settings = TestVisibilityAPISettings( coverage_enabled=False, skipping_enabled=True, @@ -207,30 +143,14 @@ def patched_enable(cls, *args, **kwargs): test_management=TestManagementSettings(), ) - # Create the same ITR data for main process - skippable_suites = { - TestSuiteId(TestModuleId(""), "test_pass.py"), - TestSuiteId(TestModuleId(""), "test_fail.py"), - } - itr_data = ITRData(correlation_id="12345678-1234-1234-1234-123456789012", skippable_items=skippable_suites) - - def set_itr_data(self): - self._itr_data = itr_data - - with mock.patch("ddtrace.internal.ci_visibility.recorder.ddconfig", mock_ddconfig), mock.patch( - "ddtrace.internal.ci_visibility._api_client.AgentlessTestVisibilityAPIClient.fetch_settings", - return_value=itr_settings, - ), mock.patch( - "ddtrace.internal.ci_visibility._api_client.EVPProxyTestVisibilityAPIClient.fetch_settings", - return_value=itr_settings, - ), mock.patch( - "ddtrace.internal.ci_visibility.recorder.CIVisibility._fetch_tests_to_skip", side_effect=set_itr_data - ), mock.patch( + with mock.patch( "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", return_value=itr_settings - ), mock.patch( - "ddtrace.ext.test_visibility.api.is_test_visibility_enabled", return_value=True ): - rec = self.inline_run("--ddtrace") + rec = self.inline_run("--ddtrace", extra_env={ + "DD_CIVISIBILITY_AGENTLESS_ENABLED": "1", + "DD_API_KEY": "foobar.baz", + "DD_INSTRUMENTATION_TELEMETRY_ENABLED": "0", + }) assert rec.ret == 0 # All tests skipped, so exit code is 0 # Verify ITR worked From ec23911f1cb42f18c038c9d9788593fd77d938ea Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Wed, 11 Jun 2025 11:16:33 +0200 Subject: [PATCH 08/58] add unit tests --- tests/contrib/pytest/test_pytest_xdist_itr.py | 317 +++++++++++++++++- 1 file changed, 311 insertions(+), 6 deletions(-) diff --git a/tests/contrib/pytest/test_pytest_xdist_itr.py b/tests/contrib/pytest/test_pytest_xdist_itr.py index fd7c0b5d73f..1e1d919b7ef 100644 --- a/tests/contrib/pytest/test_pytest_xdist_itr.py +++ b/tests/contrib/pytest/test_pytest_xdist_itr.py @@ -7,11 +7,17 @@ import pytest +from ddtrace.contrib.internal.pytest._plugin_v2 import XdistHooks +from ddtrace.contrib.internal.pytest._plugin_v2 import _handle_itr_should_skip +from ddtrace.contrib.internal.pytest._plugin_v2 import _pytest_sessionfinish from ddtrace.contrib.internal.pytest._utils import _USE_PLUGIN_V2 from ddtrace.contrib.internal.pytest._utils import _pytest_version_supports_itr +from ddtrace.ext import test +from ddtrace.ext.test_visibility._item_ids import TestId +from ddtrace.ext.test_visibility._item_ids import TestModuleId +from ddtrace.ext.test_visibility._item_ids import TestSuiteId from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings from ddtrace.internal.ci_visibility._api_client import TestManagementSettings - from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings from tests.contrib.pytest.test_pytest import PytestTestCaseBase @@ -146,11 +152,14 @@ def patched_enable(cls, *args, **kwargs): with mock.patch( "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", return_value=itr_settings ): - rec = self.inline_run("--ddtrace", extra_env={ - "DD_CIVISIBILITY_AGENTLESS_ENABLED": "1", - "DD_API_KEY": "foobar.baz", - "DD_INSTRUMENTATION_TELEMETRY_ENABLED": "0", - }) + rec = self.inline_run( + "--ddtrace", + extra_env={ + "DD_CIVISIBILITY_AGENTLESS_ENABLED": "1", + "DD_API_KEY": "foobar.baz", + "DD_INSTRUMENTATION_TELEMETRY_ENABLED": "0", + }, + ) assert rec.ret == 0 # All tests skipped, so exit code is 0 # Verify ITR worked @@ -158,3 +167,299 @@ def patched_enable(cls, *args, **kwargs): session_span = [span for span in spans if span.get_tag("type") == "test_session_end"][0] assert session_span.get_tag("test.itr.tests_skipping.enabled") == "true" assert session_span.get_metric("test.itr.tests_skipping.count") == 4 # 4 tests skipped + + +class TestXdistHooksUnit: + """Unit tests for XdistHooks class functionality.""" + + def test_xdist_hooks_registers_span_id_with_valid_session_span(self): + """Test that XdistHooks properly extracts and passes span ID when session span exists.""" + mock_node = mock.MagicMock() + mock_node.workerinput = {} + + mock_span = mock.MagicMock() + mock_span.span_id = 12345 + + with mock.patch("ddtrace.internal.test_visibility.api.InternalTestSession.get_span", return_value=mock_span): + hooks = XdistHooks() + hooks.pytest_configure_node(mock_node) + + assert mock_node.workerinput["root_span"] == 12345 + + def test_xdist_hooks_registers_zero_when_no_session_span(self): + """Test that XdistHooks uses 0 for root span as fallback when no session span exists.""" + mock_node = mock.MagicMock() + mock_node.workerinput = {} + + with mock.patch("ddtrace.internal.test_visibility.api.InternalTestSession.get_span", return_value=None): + hooks = XdistHooks() + hooks.pytest_configure_node(mock_node) + + assert mock_node.workerinput["root_span"] == 0 + + def test_xdist_hooks_aggregates_itr_skipped_count_from_workers(self): + """Test that XdistHooks properly aggregates ITR skipped counts from worker nodes.""" + # Clean up any existing global state + if hasattr(pytest, "global_worker_itr_results"): + delattr(pytest, "global_worker_itr_results") + + hooks = XdistHooks() + + # First worker reports 3 skipped tests + mock_node1 = mock.MagicMock() + mock_node1.workeroutput = {"itr_skipped_count": 3} + hooks.pytest_testnodedown(mock_node1, None) + + assert hasattr(pytest, "global_worker_itr_results") + assert pytest.global_worker_itr_results == 3 + + # Second worker reports 5 skipped tests + mock_node2 = mock.MagicMock() + mock_node2.workeroutput = {"itr_skipped_count": 5} + hooks.pytest_testnodedown(mock_node2, None) + + assert pytest.global_worker_itr_results == 8 + + # Clean up + delattr(pytest, "global_worker_itr_results") + + def test_xdist_hooks_ignores_worker_without_itr_skipped_count(self): + """Test that XdistHooks ignores workers that don't have ITR skipped count.""" + # Clean up any existing global state + if hasattr(pytest, "global_worker_itr_results"): + delattr(pytest, "global_worker_itr_results") + + hooks = XdistHooks() + + # Worker without workeroutput + mock_node1 = mock.MagicMock() + del mock_node1.workeroutput + hooks.pytest_testnodedown(mock_node1, None) + + assert not hasattr(pytest, "global_worker_itr_results") + + # Worker with workeroutput but no itr_skipped_count + mock_node2 = mock.MagicMock() + mock_node2.workeroutput = {"other_data": "value"} + hooks.pytest_testnodedown(mock_node2, None) + + assert not hasattr(pytest, "global_worker_itr_results") + + def test_xdist_hooks_initializes_global_count_correctly(self): + """Test that the first worker initializes the global count to its value (regression test for += vs =).""" + # Clean up any existing global state + if hasattr(pytest, "global_worker_itr_results"): + delattr(pytest, "global_worker_itr_results") + + hooks = XdistHooks() + + # First worker should initialize the global count to its value + mock_node = mock.MagicMock() + mock_node.workeroutput = {"itr_skipped_count": 7} + hooks.pytest_testnodedown(mock_node, None) + + # Should be 7, not 0 + 7 = 7 (this would catch if initialization logic was wrong) + assert pytest.global_worker_itr_results == 7 + + # Clean up + delattr(pytest, "global_worker_itr_results") + + def test_handle_itr_should_skip_counts_skipped_tests_in_worker(self): + """Test that _handle_itr_should_skip properly counts skipped tests in worker processes.""" + # Create a mock item with worker config + mock_item = mock.MagicMock() + mock_item.config.workeroutput = {} + + test_id = TestId(TestSuiteId(TestModuleId("test_module"), "test_suite"), "test_name") + + with mock.patch( + "ddtrace.internal.test_visibility.api.InternalTestSession.is_test_skipping_enabled", return_value=True + ), mock.patch( + "ddtrace.internal.test_visibility.api.InternalTestSuite.is_itr_unskippable", return_value=False + ), mock.patch( + "ddtrace.internal.test_visibility.api.InternalTest.is_attempt_to_fix", return_value=False + ), mock.patch( + "ddtrace.internal.test_visibility.api.InternalTestSuite.is_itr_skippable", return_value=True + ), mock.patch( + "ddtrace.internal.test_visibility.api.InternalTest.mark_itr_skipped" + ): + result = _handle_itr_should_skip(mock_item, test_id) + + assert result is True + assert mock_item.config.workeroutput["itr_skipped_count"] == 1 + # Verify the skip marker was added + mock_item.add_marker.assert_called_once() + + def test_handle_itr_should_skip_increments_existing_worker_count(self): + """Test that _handle_itr_should_skip increments existing worker skipped count.""" + # Create a mock item with worker config that already has a count + mock_item = mock.MagicMock() + mock_item.config.workeroutput = {"itr_skipped_count": 5} + + test_id = TestId(TestSuiteId(TestModuleId("test_module"), "test_suite"), "test_name") + + with mock.patch( + "ddtrace.internal.test_visibility.api.InternalTestSession.is_test_skipping_enabled", return_value=True + ), mock.patch( + "ddtrace.internal.test_visibility.api.InternalTestSuite.is_itr_unskippable", return_value=False + ), mock.patch( + "ddtrace.internal.test_visibility.api.InternalTest.is_attempt_to_fix", return_value=False + ), mock.patch( + "ddtrace.internal.test_visibility.api.InternalTestSuite.is_itr_skippable", return_value=True + ), mock.patch( + "ddtrace.internal.test_visibility.api.InternalTest.mark_itr_skipped" + ): + result = _handle_itr_should_skip(mock_item, test_id) + + assert result is True + # This is a critical regression test: should be 6 (5+1) + assert mock_item.config.workeroutput["itr_skipped_count"] == 6 + + def test_handle_itr_should_skip_returns_false_when_not_skippable(self): + """Test that _handle_itr_should_skip returns False when test is not skippable.""" + mock_item = mock.MagicMock() + mock_item.config.workeroutput = {} + test_id = TestId(TestSuiteId(TestModuleId("test_module"), "test_suite"), "test_name") + + with mock.patch( + "ddtrace.internal.test_visibility.api.InternalTestSession.is_test_skipping_enabled", return_value=True + ), mock.patch( + "ddtrace.internal.test_visibility.api.InternalTestSuite.is_itr_unskippable", return_value=False + ), mock.patch( + "ddtrace.internal.test_visibility.api.InternalTest.is_attempt_to_fix", return_value=False + ), mock.patch( + "ddtrace.internal.test_visibility.api.InternalTestSuite.is_itr_skippable", return_value=False + ): # Not skippable + result = _handle_itr_should_skip(mock_item, test_id) + + assert result is False + # Should not have counted since test wasn't skipped + assert "itr_skipped_count" not in mock_item.config.workeroutput + mock_item.add_marker.assert_not_called() + + def test_handle_itr_should_skip_unskippable_test_gets_forced_run(self): + """Test that unskippable tests in skippable suites get marked as forced run.""" + mock_item = mock.MagicMock() + mock_item.config.workeroutput = {} + test_id = TestId(TestSuiteId(TestModuleId("test_module"), "test_suite"), "test_name") + + with mock.patch( + "ddtrace.internal.test_visibility.api.InternalTestSession.is_test_skipping_enabled", return_value=True + ), mock.patch( + "ddtrace.internal.test_visibility.api.InternalTestSuite.is_itr_unskippable", return_value=True + ), mock.patch( + "ddtrace.internal.test_visibility.api.InternalTest.is_attempt_to_fix", return_value=False + ), mock.patch( + "ddtrace.internal.test_visibility.api.InternalTestSuite.is_itr_skippable", return_value=True + ), mock.patch( + "ddtrace.internal.test_visibility.api.InternalTest.mark_itr_forced_run" + ) as mock_forced_run: + result = _handle_itr_should_skip(mock_item, test_id) + + assert result is False + mock_forced_run.assert_called_once_with(test_id) + # Should not have counted since test wasn't skipped + assert "itr_skipped_count" not in mock_item.config.workeroutput + + def test_pytest_sessionfinish_aggregates_worker_itr_results(self): + """Test that pytest_sessionfinish properly aggregates ITR results from workers.""" + # Set up global worker results + pytest.global_worker_itr_results = 10 + + mock_session = mock.MagicMock() + # Main process doesn't have workerinput + del mock_session.config.workerinput + mock_session.exitstatus = 0 + + mock_session_span = mock.MagicMock() + + # Test the ITR aggregation logic directly + # Simulate the conditions in _pytest_sessionfinish + + # Count ITR skipped tests from workers if we're in the main process + if hasattr(mock_session.config, "workerinput") is False and hasattr(pytest, "global_worker_itr_results"): + skipped_count = pytest.global_worker_itr_results + if skipped_count > 0: + session_span = mock_session_span # Use our mock directly + if session_span: + session_span.set_tag_str(test.ITR_TEST_SKIPPING_TESTS_SKIPPED, "true") + session_span.set_tag_str(test.ITR_DD_CI_ITR_TESTS_SKIPPED, "true") + session_span.set_metric(test.ITR_TEST_SKIPPING_COUNT, skipped_count) + + # Verify the session span was tagged with ITR results + mock_session_span.set_tag_str.assert_any_call(test.ITR_TEST_SKIPPING_TESTS_SKIPPED, "true") + mock_session_span.set_tag_str.assert_any_call(test.ITR_DD_CI_ITR_TESTS_SKIPPED, "true") + mock_session_span.set_metric.assert_called_with(test.ITR_TEST_SKIPPING_COUNT, 10) + + # Clean up + delattr(pytest, "global_worker_itr_results") + + def test_pytest_sessionfinish_no_aggregation_for_worker_process(self): + """Test that pytest_sessionfinish doesn't aggregate results when running in worker process.""" + # Set up global worker results + pytest.global_worker_itr_results = 10 + + mock_session = mock.MagicMock() + # Worker process has workerinput + mock_session.config.workerinput = {"root_span": "12345"} + mock_session.exitstatus = 0 + + mock_session_span = mock.MagicMock() + + with mock.patch("ddtrace.ext.test_visibility.api.is_test_visibility_enabled", return_value=True), mock.patch( + "ddtrace.internal.test_visibility.api.InternalTestSession.get_span", return_value=mock_session_span + ), mock.patch("ddtrace.internal.test_visibility.api.InternalTestSession.finish"): + _pytest_sessionfinish(mock_session, 0) + + # Verify no ITR tags were set (worker shouldn't aggregate) + mock_session_span.set_tag_str.assert_not_called() + mock_session_span.set_metric.assert_not_called() + + # Clean up + delattr(pytest, "global_worker_itr_results") + + def test_pytest_sessionfinish_no_aggregation_when_no_global_results(self): + """Test that pytest_sessionfinish doesn't aggregate when no global worker results exist.""" + # Ensure no global worker results exist + if hasattr(pytest, "global_worker_itr_results"): + delattr(pytest, "global_worker_itr_results") + + mock_session = mock.MagicMock() + # Main process doesn't have workerinput + del mock_session.config.workerinput + mock_session.exitstatus = 0 + + mock_session_span = mock.MagicMock() + + with mock.patch("ddtrace.ext.test_visibility.api.is_test_visibility_enabled", return_value=True), mock.patch( + "ddtrace.internal.test_visibility.api.InternalTestSession.get_span", return_value=mock_session_span + ), mock.patch("ddtrace.internal.test_visibility.api.InternalTestSession.finish"): + _pytest_sessionfinish(mock_session, 0) + + # Verify no ITR tags were set (no global results to aggregate) + mock_session_span.set_tag_str.assert_not_called() + mock_session_span.set_metric.assert_not_called() + + def test_pytest_sessionfinish_no_aggregation_when_zero_skipped(self): + """Test that pytest_sessionfinish doesn't aggregate when zero tests were skipped.""" + # Set up global worker results with zero skipped + pytest.global_worker_itr_results = 0 + + mock_session = mock.MagicMock() + # Main process doesn't have workerinput + del mock_session.config.workerinput + mock_session.exitstatus = 0 + + mock_session_span = mock.MagicMock() + + with mock.patch("ddtrace.ext.test_visibility.api.is_test_visibility_enabled", return_value=True), mock.patch( + "ddtrace.internal.test_visibility.api.InternalTestSession.get_span", return_value=mock_session_span + ), mock.patch("ddtrace.internal.test_visibility.api.InternalTestSession.finish"): + _pytest_sessionfinish(mock_session, 0) + + # Verify no ITR tags were set (zero tests skipped) + mock_session_span.set_tag_str.assert_not_called() + mock_session_span.set_metric.assert_not_called() + + # Clean up + delattr(pytest, "global_worker_itr_results") From 4e422dd2cfa842059a9c193db9c39370e572b6a8 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Wed, 11 Jun 2025 14:40:42 +0200 Subject: [PATCH 09/58] finish regardless of itr --- ddtrace/contrib/internal/pytest/_plugin_v2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddtrace/contrib/internal/pytest/_plugin_v2.py b/ddtrace/contrib/internal/pytest/_plugin_v2.py index 49801f73023..514db125d6b 100644 --- a/ddtrace/contrib/internal/pytest/_plugin_v2.py +++ b/ddtrace/contrib/internal/pytest/_plugin_v2.py @@ -470,7 +470,7 @@ def _pytest_runtest_protocol_post_yield(item, nextitem, coverage_collector): InternalTestSuite.mark_itr_skipped(suite_id) else: _handle_coverage_dependencies(suite_id) - InternalTestSuite.finish(suite_id) + InternalTestSuite.finish(suite_id) if nextitem is None or (next_test_id is not None and next_test_id.parent_id.parent_id != module_id): InternalTestModule.finish(module_id) From 6e6c2551055e64c1443d87dadcfeb2fc11921181 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Wed, 11 Jun 2025 16:40:17 +0200 Subject: [PATCH 10/58] change place to set itr tags --- ddtrace/contrib/internal/pytest/_plugin_v2.py | 21 +++++++++++++------ tests/contrib/pytest/test_pytest_xdist_itr.py | 16 ++++++++------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/ddtrace/contrib/internal/pytest/_plugin_v2.py b/ddtrace/contrib/internal/pytest/_plugin_v2.py index 514db125d6b..e7eadec14a5 100644 --- a/ddtrace/contrib/internal/pytest/_plugin_v2.py +++ b/ddtrace/contrib/internal/pytest/_plugin_v2.py @@ -785,14 +785,23 @@ def _pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: ModuleCodeCollector.uninstall() # Count ITR skipped tests from workers if we're in the main process - if hasattr(session.config, "workerinput") is False and hasattr(pytest, "global_worker_itr_results"): + if not hasattr(session.config, "workerinput") and hasattr(pytest, "global_worker_itr_results"): skipped_count = pytest.global_worker_itr_results if skipped_count > 0: - session_span = InternalTestSession.get_span() - if session_span: - session_span.set_tag_str(test.ITR_TEST_SKIPPING_TESTS_SKIPPED, "true") - session_span.set_tag_str(test.ITR_DD_CI_ITR_TESTS_SKIPPED, "true") - session_span.set_metric(test.ITR_TEST_SKIPPING_COUNT, skipped_count) + # Update the session's internal _itr_skipped_count so that when _set_itr_tags() is called + # during session finishing, it will use the correct worker-aggregated count + session_instance = InternalTestSession + if session_instance: + session_instance._itr_skipped_count = skipped_count + + # HACK: + # Also set the ITR tags directly on the session span since the normal _set_itr_tags + # flow may have already completed before worker results were aggregated + session_span = session_instance.get_span() + if session_span: + session_span.set_tag_str(test.ITR_TEST_SKIPPING_TESTS_SKIPPED, "true") + session_span.set_tag_str(test.ITR_DD_CI_ITR_TESTS_SKIPPED, "true") + session_span.set_metric(test.ITR_TEST_SKIPPING_COUNT, skipped_count) InternalTestSession.finish( force_finish_children=True, diff --git a/tests/contrib/pytest/test_pytest_xdist_itr.py b/tests/contrib/pytest/test_pytest_xdist_itr.py index 1e1d919b7ef..fc8491e1bfd 100644 --- a/tests/contrib/pytest/test_pytest_xdist_itr.py +++ b/tests/contrib/pytest/test_pytest_xdist_itr.py @@ -86,7 +86,7 @@ def inline_run(self, *args, **kwargs): return super().inline_run(*args, **kwargs) def test_pytest_xdist_itr_skips_tests(self): - """Test that ITR skips tests when enabled.""" + """Test that ITR tags are correctly aggregated from xdist workers.""" # Create a simplified sitecustomize with just the essential ITR setup itr_skipping_sitecustomize = """ # sitecustomize.py - Simplified ITR setup for xdist @@ -151,6 +151,9 @@ def patched_enable(cls, *args, **kwargs): with mock.patch( "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features", return_value=itr_settings + ), mock.patch( + "ddtrace.internal.ci_visibility.recorder.CIVisibility.test_skipping_enabled", + return_value=True, ): rec = self.inline_run( "--ddtrace", @@ -162,11 +165,12 @@ def patched_enable(cls, *args, **kwargs): ) assert rec.ret == 0 # All tests skipped, so exit code is 0 - # Verify ITR worked - spans = self.pop_spans() - session_span = [span for span in spans if span.get_tag("type") == "test_session_end"][0] - assert session_span.get_tag("test.itr.tests_skipping.enabled") == "true" - assert session_span.get_metric("test.itr.tests_skipping.count") == 4 # 4 tests skipped + spans = self.pop_spans() + session_span = [span for span in spans if span.get_tag("type") == "test_session_end"][0] + assert session_span.get_tag("test.itr.tests_skipping.enabled") == "true" + assert session_span.get_tag("test.itr.tests_skipping.type") == "suite" + # Verify number of skipped tests in session + assert session_span.get_metric("test.itr.tests_skipping.count") == 4 class TestXdistHooksUnit: From d41b7c8a077879af84955cc121efcd375aeb4ffe Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Thu, 12 Jun 2025 10:29:12 +0200 Subject: [PATCH 11/58] use proper channels to set distributed itr skip count --- ddtrace/contrib/internal/pytest/_plugin_v2.py | 18 +++++------------- ddtrace/ext/test_visibility/api.py | 4 ++-- ddtrace/internal/ci_visibility/api/_base.py | 6 +++++- ddtrace/internal/ci_visibility/api/_session.py | 4 ++++ ddtrace/internal/ci_visibility/recorder.py | 11 ++++++++++- ddtrace/internal/test_visibility/api.py | 6 ++++++ 6 files changed, 32 insertions(+), 17 deletions(-) diff --git a/ddtrace/contrib/internal/pytest/_plugin_v2.py b/ddtrace/contrib/internal/pytest/_plugin_v2.py index e7eadec14a5..67c354860bd 100644 --- a/ddtrace/contrib/internal/pytest/_plugin_v2.py +++ b/ddtrace/contrib/internal/pytest/_plugin_v2.py @@ -332,6 +332,7 @@ def pytest_sessionstart(session: pytest.Session) -> None: InternalTestSession.set_library_capabilities(library_capabilities) extracted_context = None + distributed_children = False if hasattr(session.config, "workerinput"): from ddtrace._trace.context import Context from ddtrace.constants import USER_KEEP @@ -349,8 +350,10 @@ def pytest_sessionstart(session: pytest.Session) -> None: "pytest_sessionstart: Could not convert root_span %s to int", received_root_span, ) + elif hasattr(pytest, "global_worker_itr_results"): + distributed_children = True - InternalTestSession.start(extracted_context) + InternalTestSession.start(distributed_children, extracted_context) if InternalTestSession.efd_enabled() and not _pytest_version_supports_efd(): log.warning("Early Flake Detection disabled: pytest version is not supported") @@ -790,18 +793,7 @@ def _pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: if skipped_count > 0: # Update the session's internal _itr_skipped_count so that when _set_itr_tags() is called # during session finishing, it will use the correct worker-aggregated count - session_instance = InternalTestSession - if session_instance: - session_instance._itr_skipped_count = skipped_count - - # HACK: - # Also set the ITR tags directly on the session span since the normal _set_itr_tags - # flow may have already completed before worker results were aggregated - session_span = session_instance.get_span() - if session_span: - session_span.set_tag_str(test.ITR_TEST_SKIPPING_TESTS_SKIPPED, "true") - session_span.set_tag_str(test.ITR_DD_CI_ITR_TESTS_SKIPPED, "true") - session_span.set_metric(test.ITR_TEST_SKIPPING_COUNT, skipped_count) + InternalTestSession.set_itr_tags(skipped_count) InternalTestSession.finish( force_finish_children=True, diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index b154aa344cc..32db2dfedfb 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -180,9 +180,9 @@ def discover( @staticmethod @_catch_and_log_exceptions - def start(context: Optional[Context] = None): + def start(distributed_children: bool = False, context: Optional[Context] = None): log.debug("Starting session") - core.dispatch("test_visibility.session.start", (context,)) + core.dispatch("test_visibility.session.start", (distributed_children, context)) class FinishArgs(NamedTuple): force_finish_children: bool diff --git a/ddtrace/internal/ci_visibility/api/_base.py b/ddtrace/internal/ci_visibility/api/_base.py index aa776b82e69..dad5cf56e0b 100644 --- a/ddtrace/internal/ci_visibility/api/_base.py +++ b/ddtrace/internal/ci_visibility/api/_base.py @@ -549,6 +549,7 @@ def __init__( ) -> None: super().__init__(name, session_settings, operation_name, initial_tags) self._children: Dict[CIDT, CITEMT] = {} + self._distributed_children = False def get_status(self) -> Union[TestStatus, SPECIAL_STATUS]: """Recursively computes status based on all children's status @@ -659,5 +660,8 @@ def _set_itr_tags(self, itr_enabled: bool) -> None: self.set_tag(test.ITR_TEST_SKIPPING_TESTS_SKIPPED, self._itr_skipped_count > 0) # Only parent items set skipped counts because tests would always be 1 or 0 - if self._children: + if self._children or self._distributed_children: self.set_tag(test.ITR_TEST_SKIPPING_COUNT, self._itr_skipped_count) + + def set_distributed_children(self) -> None: + self._distributed_children = True diff --git a/ddtrace/internal/ci_visibility/api/_session.py b/ddtrace/internal/ci_visibility/api/_session.py index 411c7369d76..d31724ea01e 100644 --- a/ddtrace/internal/ci_visibility/api/_session.py +++ b/ddtrace/internal/ci_visibility/api/_session.py @@ -107,6 +107,10 @@ def _telemetry_record_event_finished(self): def add_coverage_data(self, *args, **kwargs): raise NotImplementedError("Coverage data cannot be added to session.") + def set_skipped_count(self, skipped_count: int): + self._itr_skipped_count = skipped_count + self._set_itr_tags(True) + def set_covered_lines_pct(self, coverage_pct: float): self.set_tag(test.TEST_LINES_PCT, coverage_pct) diff --git a/ddtrace/internal/ci_visibility/recorder.py b/ddtrace/internal/ci_visibility/recorder.py index be69d1a423f..5e702e3d2f4 100644 --- a/ddtrace/internal/ci_visibility/recorder.py +++ b/ddtrace/internal/ci_visibility/recorder.py @@ -1103,10 +1103,12 @@ def _on_discover_session(discover_args: TestSession.DiscoverArgs) -> None: @_requires_civisibility_enabled -def _on_start_session(context: Optional[Context] = None) -> None: +def _on_start_session(distributed_children = False, context: Optional[Context] = None) -> None: log.debug("Handling start session") session = CIVisibility.get_session() session.start(context) + if distributed_children: + session.set_distributed_children() @_requires_civisibility_enabled @@ -1171,6 +1173,12 @@ def _on_session_set_library_capabilities(capabilities: LibraryCapabilities) -> N CIVisibility.set_library_capabilities(capabilities) +@_requires_civisibility_enabled +def _on_session_set_itr_skipped_count(skipped_count: int) -> None: + log.debug("Setting skipped count: %d", skipped_count) + CIVisibility.get_session().set_skipped_count(skipped_count) + + @_requires_civisibility_enabled def _on_session_get_path_codeowners(path: Path) -> Optional[List[str]]: log.debug("Getting codeowners for path %s", path) @@ -1203,6 +1211,7 @@ def _register_session_handlers() -> None: ) core.on("test_visibility.session.set_covered_lines_pct", _on_session_set_covered_lines_pct) core.on("test_visibility.session.set_library_capabilities", _on_session_set_library_capabilities) + core.on("test_visibility.session.set_itr_skipped_count", _on_session_set_itr_skipped_count) @_requires_civisibility_enabled diff --git a/ddtrace/internal/test_visibility/api.py b/ddtrace/internal/test_visibility/api.py index 856db4bba99..4b8efaada61 100644 --- a/ddtrace/internal/test_visibility/api.py +++ b/ddtrace/internal/test_visibility/api.py @@ -141,6 +141,12 @@ def get_path_codeowners(path: Path) -> t.Optional[t.List[str]]: def set_library_capabilities(capabilities: LibraryCapabilities) -> None: core.dispatch("test_visibility.session.set_library_capabilities", (capabilities,)) + @staticmethod + @_catch_and_log_exceptions + def set_itr_tags(skipped_count: int): + log.debug("Setting itr session tags: %d", skipped_count) + core.dispatch("test_visibility.session.set_itr_skipped_count", (skipped_count,)) + class InternalTestModule(ext_api.TestModule, InternalTestBase): pass From 22bc855feac8c195a133b79dc0a750707a6f00ac Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Thu, 12 Jun 2025 10:47:28 +0200 Subject: [PATCH 12/58] details --- ddtrace/contrib/internal/pytest/_plugin_v2.py | 2 +- ddtrace/internal/ci_visibility/recorder.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ddtrace/contrib/internal/pytest/_plugin_v2.py b/ddtrace/contrib/internal/pytest/_plugin_v2.py index 67c354860bd..821a89d69d4 100644 --- a/ddtrace/contrib/internal/pytest/_plugin_v2.py +++ b/ddtrace/contrib/internal/pytest/_plugin_v2.py @@ -788,7 +788,7 @@ def _pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: ModuleCodeCollector.uninstall() # Count ITR skipped tests from workers if we're in the main process - if not hasattr(session.config, "workerinput") and hasattr(pytest, "global_worker_itr_results"): + if hasattr(pytest, "global_worker_itr_results"): skipped_count = pytest.global_worker_itr_results if skipped_count > 0: # Update the session's internal _itr_skipped_count so that when _set_itr_tags() is called diff --git a/ddtrace/internal/ci_visibility/recorder.py b/ddtrace/internal/ci_visibility/recorder.py index 5e702e3d2f4..b2acd2fc250 100644 --- a/ddtrace/internal/ci_visibility/recorder.py +++ b/ddtrace/internal/ci_visibility/recorder.py @@ -1103,7 +1103,7 @@ def _on_discover_session(discover_args: TestSession.DiscoverArgs) -> None: @_requires_civisibility_enabled -def _on_start_session(distributed_children = False, context: Optional[Context] = None) -> None: +def _on_start_session(distributed_children: bool = False, context: Optional[Context] = None) -> None: log.debug("Handling start session") session = CIVisibility.get_session() session.start(context) From 6f7c2bc29fb1ea39983b6ff1e7b27c8997008792 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Thu, 12 Jun 2025 11:37:13 +0200 Subject: [PATCH 13/58] xdist context propagation tests fixed --- .../contrib/pytest/test_xdist_context_propagation.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/contrib/pytest/test_xdist_context_propagation.py b/tests/contrib/pytest/test_xdist_context_propagation.py index 9060f8bff4b..af6f5ab4591 100644 --- a/tests/contrib/pytest/test_xdist_context_propagation.py +++ b/tests/contrib/pytest/test_xdist_context_propagation.py @@ -96,7 +96,9 @@ def test_pytest_sessionstart_extracts_context_from_valid_workerinput(): # Verify that start was called with the expected context mock_start.assert_called_once() - context = mock_start.call_args[0][0] + distributed_children = mock_start.call_args[0][0] + assert distributed_children is False # No distributed children expected + context = mock_start.call_args[0][1] assert context is not None assert context.span_id == 54321 assert context.trace_id == 1 @@ -122,8 +124,8 @@ def test_pytest_sessionstart_handles_invalid_span_id_gracefully(): ): pytest_sessionstart(mock_session) - # Verify that start was called with None (no context extracted) - mock_start.assert_called_once_with(None) + # Verify that start was called with False, None (no distributed children, no context extracted) + mock_start.assert_called_once_with(False, None) def test_pytest_sessionstart_handles_missing_workerinput(): @@ -146,8 +148,8 @@ def test_pytest_sessionstart_handles_missing_workerinput(): ): pytest_sessionstart(mock_session) - # Verify that start was called with None (no context for main process) - mock_start.assert_called_once_with(None) + # Verify that start was called with False (no distributed children), None (no context for main process) + mock_start.assert_called_once_with(False, None) def test_xdist_hooks_class_has_required_hookimpl(): From 9785070f9642775ddd32ac698ec9e2254381dfb0 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Thu, 12 Jun 2025 11:55:16 +0200 Subject: [PATCH 14/58] pass proper argument --- ddtrace/internal/ci_visibility/api/_session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddtrace/internal/ci_visibility/api/_session.py b/ddtrace/internal/ci_visibility/api/_session.py index d31724ea01e..35ef85001a0 100644 --- a/ddtrace/internal/ci_visibility/api/_session.py +++ b/ddtrace/internal/ci_visibility/api/_session.py @@ -109,7 +109,7 @@ def add_coverage_data(self, *args, **kwargs): def set_skipped_count(self, skipped_count: int): self._itr_skipped_count = skipped_count - self._set_itr_tags(True) + self._set_itr_tags(self._session_settings.itr_test_skipping_enabled) def set_covered_lines_pct(self, coverage_pct: float): self.set_tag(test.TEST_LINES_PCT, coverage_pct) From 96ee0512bfdbfc6ba65bb612f6e2c30442049e72 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Thu, 12 Jun 2025 13:41:12 +0200 Subject: [PATCH 15/58] refactor 1 --- ddtrace/ext/test_visibility/_utils.py | 26 +- ddtrace/ext/test_visibility/api.py | 88 +++--- ddtrace/internal/ci_visibility/recorder.py | 298 +++++------------- .../ci_visibility/telemetry/test_session.py | 20 ++ .../internal/test_visibility/_atr_mixins.py | 104 +++--- .../test_visibility/_attempt_to_fix_mixins.py | 95 ++---- .../internal/test_visibility/_efd_mixins.py | 113 +++---- .../internal/test_visibility/_itr_mixins.py | 93 +++--- ddtrace/internal/test_visibility/_utils.py | 7 +- ddtrace/internal/test_visibility/api.py | 116 +++---- 10 files changed, 373 insertions(+), 587 deletions(-) create mode 100644 ddtrace/internal/ci_visibility/telemetry/test_session.py diff --git a/ddtrace/ext/test_visibility/_utils.py b/ddtrace/ext/test_visibility/_utils.py index e5ea3ac5fc8..e77b8ca4587 100644 --- a/ddtrace/ext/test_visibility/_utils.py +++ b/ddtrace/ext/test_visibility/_utils.py @@ -32,34 +32,44 @@ def wrapper(*args, **kwargs): def _get_item_tag(item_id: TestVisibilityItemId, tag_name: str) -> Any: log.debug("Getting tag for item %s: %s", item_id, tag_name) - tag_value = core.dispatch_with_results( - "test_visibility.item.get_tag", (_TestVisibilityAPIBase.GetTagArgs(item_id, tag_name),) - ).tag_value.value + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_get_tag + tag_value = on_get_tag(_TestVisibilityAPIBase.GetTagArgs(item_id, tag_name)) return tag_value def _set_item_tag(item_id: TestVisibilityItemId, tag_name: str, tag_value: Any, recurse: bool = False): log.debug("Setting tag for item %s: %s=%s", item_id, tag_name, tag_value) - core.dispatch("test_visibility.item.set_tag", (_TestVisibilityAPIBase.SetTagArgs(item_id, tag_name, tag_value),)) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_set_tag + on_set_tag(_TestVisibilityAPIBase.SetTagArgs(item_id, tag_name, tag_value)) def _set_item_tags(item_id: TestVisibilityItemId, tags: Dict[str, Any], recurse: bool = False): log.debug("Setting tags for item %s: %s", item_id, tags) - core.dispatch("test_visibility.item.set_tags", (_TestVisibilityAPIBase.SetTagsArgs(item_id, tags),)) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_set_tags + on_set_tags(_TestVisibilityAPIBase.SetTagsArgs(item_id, tags)) def _delete_item_tag(item_id: TestVisibilityItemId, tag_name: str, recurse: bool = False): log.debug("Deleting tag for item %s: %s", item_id, tag_name) - core.dispatch("test_visibility.item.delete_tag", (_TestVisibilityAPIBase.DeleteTagArgs(item_id, tag_name),)) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_delete_tag + on_delete_tag(_TestVisibilityAPIBase.DeleteTagArgs(item_id, tag_name)) def _delete_item_tags(item_id: TestVisibilityItemId, tag_names: List[str], recurse: bool = False): log.debug("Deleting tags for item %s: %s", item_id, tag_names) - core.dispatch("test_visibility.item.delete_tags", (_TestVisibilityAPIBase.DeleteTagsArgs(item_id, tag_names),)) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_delete_tags + on_delete_tags(_TestVisibilityAPIBase.DeleteTagsArgs(item_id, tag_names)) def _is_item_finished(item_id: TestVisibilityItemId) -> bool: log.debug("Checking if item %s is finished", item_id) - _is_finished = bool(core.dispatch_with_results("test_visibility.item.is_finished", (item_id,)).is_finished.value) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_item_is_finished + _is_finished = bool(on_item_is_finished(item_id)) log.debug("Item %s is finished: %s", item_id, _is_finished) return _is_finished diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 32db2dfedfb..1a3d4503bf5 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -39,7 +39,6 @@ from ddtrace.ext.test_visibility._utils import _is_item_finished from ddtrace.ext.test_visibility._utils import _set_item_tag from ddtrace.ext.test_visibility._utils import _set_item_tags -from ddtrace.internal import core from ddtrace.internal.logger import get_logger as _get_logger @@ -86,7 +85,8 @@ class TestExcInfo: @_catch_and_log_exceptions def enable_test_visibility(config: Optional[Any] = None): log.debug("Enabling Test Visibility with config: %s", config) - core.dispatch("test_visibility.enable", (config,)) + from ddtrace.internal.ci_visibility.recorder import CIVisibility + CIVisibility.enable(config=config) if not is_test_visibility_enabled(): log.warning("Failed to enable Test Visibility") @@ -94,13 +94,15 @@ def enable_test_visibility(config: Optional[Any] = None): @_catch_and_log_exceptions def is_test_visibility_enabled(): - return core.dispatch_with_results("test_visibility.is_enabled").is_enabled.value + from ddtrace.internal.ci_visibility.recorder import CIVisibility + return CIVisibility.enabled @_catch_and_log_exceptions def disable_test_visibility(): log.debug("Disabling Test Visibility") - core.dispatch("test_visibility.disable") + from ddtrace.internal.ci_visibility.recorder import CIVisibility + CIVisibility.disable() if is_test_visibility_enabled(): log.warning("Failed to disable Test Visibility") @@ -161,28 +163,27 @@ def discover( log.debug("Test Visibility is not enabled, session not registered.") return - core.dispatch( - "test_visibility.session.discover", - ( - TestSession.DiscoverArgs( - test_command, - reject_duplicates, - test_framework, - test_framework_version, - session_operation_name, - module_operation_name, - suite_operation_name, - test_operation_name, - root_dir, - ), - ), + from ddtrace.internal.ci_visibility.recorder import on_discover_session + on_discover_session( + TestSession.DiscoverArgs( + test_command, + reject_duplicates, + test_framework, + test_framework_version, + session_operation_name, + module_operation_name, + suite_operation_name, + test_operation_name, + root_dir, + ) ) @staticmethod @_catch_and_log_exceptions def start(distributed_children: bool = False, context: Optional[Context] = None): log.debug("Starting session") - core.dispatch("test_visibility.session.start", (distributed_children, context)) + from ddtrace.internal.ci_visibility.recorder import on_start_session + on_start_session(distributed_children, context) class FinishArgs(NamedTuple): force_finish_children: bool @@ -196,9 +197,8 @@ def finish( ): log.debug("Finishing session, force_finish_session_modules: %s", force_finish_children) - core.dispatch( - "test_visibility.session.finish", (TestSession.FinishArgs(force_finish_children, override_status),) - ) + from ddtrace.internal.ci_visibility.recorder import on_finish_session + on_finish_session(TestSession.FinishArgs(force_finish_children, override_status)) @staticmethod def get_tag(tag_name: str) -> Any: @@ -235,13 +235,15 @@ class FinishArgs(NamedTuple): @_catch_and_log_exceptions def discover(item_id: TestModuleId, module_path: Optional[Path] = None): log.debug("Registered module %s", item_id) - core.dispatch("test_visibility.module.discover", (TestModule.DiscoverArgs(item_id, module_path),)) + from ddtrace.internal.ci_visibility.recorder import on_discover_module + on_discover_module(TestModule.DiscoverArgs(item_id, module_path)) @staticmethod @_catch_and_log_exceptions def start(item_id: TestModuleId): log.debug("Starting module %s", item_id) - core.dispatch("test_visibility.module.start", (item_id,)) + from ddtrace.internal.ci_visibility.recorder import on_start_module + on_start_module(item_id) @staticmethod @_catch_and_log_exceptions @@ -256,9 +258,8 @@ def finish( override_status, force_finish_children, ) - core.dispatch( - "test_visibility.module.finish", (TestModule.FinishArgs(item_id, override_status, force_finish_children),) - ) + from ddtrace.internal.ci_visibility.recorder import on_finish_module + on_finish_module(TestModule.FinishArgs(item_id, override_status, force_finish_children)) class TestSuite(TestBase): @@ -276,15 +277,15 @@ def discover( ): """Registers a test suite with the Test Visibility service.""" log.debug("Registering suite %s, source: %s", item_id, source_file_info) - core.dispatch( - "test_visibility.suite.discover", (TestSuite.DiscoverArgs(item_id, codeowners, source_file_info),) - ) + from ddtrace.internal.ci_visibility.recorder import on_discover_suite + on_discover_suite(TestSuite.DiscoverArgs(item_id, codeowners, source_file_info)) @staticmethod @_catch_and_log_exceptions def start(item_id: TestSuiteId): log.debug("Starting suite %s", item_id) - core.dispatch("test_visibility.suite.start", (item_id,)) + from ddtrace.internal.ci_visibility.recorder import on_start_suite + on_start_suite(item_id) class FinishArgs(NamedTuple): suite_id: TestSuiteId @@ -304,10 +305,8 @@ def finish( force_finish_children, override_status, ) - core.dispatch( - "test_visibility.suite.finish", - (TestSuite.FinishArgs(item_id, force_finish_children, override_status),), - ) + from ddtrace.internal.ci_visibility.recorder import on_finish_suite + on_finish_suite(TestSuite.FinishArgs(item_id, force_finish_children, override_status)) class Test(TestBase): @@ -333,15 +332,15 @@ def discover( source_file_info, resource, ) - core.dispatch( - "test_visibility.test.discover", (Test.DiscoverArgs(item_id, codeowners, source_file_info, resource),) - ) + from ddtrace.internal.ci_visibility.recorder import on_discover_test + on_discover_test(Test.DiscoverArgs(item_id, codeowners, source_file_info, resource)) @staticmethod @_catch_and_log_exceptions def start(item_id: TestId): log.debug("Starting test %s", item_id) - core.dispatch("test_visibility.test.start", (item_id,)) + from ddtrace.internal.ci_visibility.recorder import on_start_test + on_start_test(item_id) class FinishArgs(NamedTuple): test_id: TestId @@ -364,16 +363,15 @@ def finish( skip_reason, exc_info, ) - core.dispatch( - "test_visibility.test.finish", - (Test.FinishArgs(item_id, status, skip_reason=skip_reason, exc_info=exc_info),), - ) + from ddtrace.internal.ci_visibility.recorder import on_finish_test + on_finish_test(Test.FinishArgs(item_id, status, skip_reason=skip_reason, exc_info=exc_info)) @staticmethod @_catch_and_log_exceptions def set_parameters(item_id: TestId, params: str): log.debug("Setting test %s parameters to %s", item_id, params) - core.dispatch("test_visibility.test.set_parameters", (item_id, params)) + from ddtrace.internal.ci_visibility.recorder import on_set_test_parameters + on_set_test_parameters(item_id, params) @staticmethod @_catch_and_log_exceptions diff --git a/ddtrace/internal/ci_visibility/recorder.py b/ddtrace/internal/ci_visibility/recorder.py index b2acd2fc250..a2394013602 100644 --- a/ddtrace/internal/ci_visibility/recorder.py +++ b/ddtrace/internal/ci_visibility/recorder.py @@ -32,7 +32,6 @@ from ddtrace.ext.test_visibility.api import TestSuiteId from ddtrace.internal import agent from ddtrace.internal import atexit -from ddtrace.internal import core from ddtrace.internal import telemetry from ddtrace.internal.ci_visibility._api_client import AgentlessTestVisibilityAPIClient from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings @@ -1036,7 +1035,7 @@ def wrapper(*args, **kwargs) -> Any: @_requires_civisibility_enabled -def _on_discover_session(discover_args: TestSession.DiscoverArgs) -> None: +def on_discover_session(discover_args: TestSession.DiscoverArgs) -> None: log.debug("Handling session discovery") # _requires_civisibility_enabled prevents us from getting here, but this makes type checkers happy @@ -1103,7 +1102,7 @@ def _on_discover_session(discover_args: TestSession.DiscoverArgs) -> None: @_requires_civisibility_enabled -def _on_start_session(distributed_children: bool = False, context: Optional[Context] = None) -> None: +def on_start_session(distributed_children: bool = False, context: Optional[Context] = None) -> None: log.debug("Handling start session") session = CIVisibility.get_session() session.start(context) @@ -1112,75 +1111,75 @@ def _on_start_session(distributed_children: bool = False, context: Optional[Cont @_requires_civisibility_enabled -def _on_finish_session(finish_args: TestSession.FinishArgs) -> None: +def on_finish_session(finish_args: TestSession.FinishArgs) -> None: log.debug("Handling finish session") session = CIVisibility.get_session() session.finish(finish_args.force_finish_children, finish_args.override_status) @_requires_civisibility_enabled -def _on_session_is_test_skipping_enabled() -> bool: +def on_session_is_test_skipping_enabled() -> bool: log.debug("Handling is test skipping enabled") return CIVisibility.test_skipping_enabled() @_requires_civisibility_enabled -def _on_session_get_workspace_path() -> Optional[Path]: +def on_session_get_workspace_path() -> Optional[Path]: log.debug("Handling finish for session id %s") path_str = CIVisibility.get_workspace_path() return Path(path_str) if path_str is not None else None @_requires_civisibility_enabled -def _on_session_should_collect_coverage() -> bool: +def on_session_should_collect_coverage() -> bool: log.debug("Handling should collect coverage") return CIVisibility.should_collect_coverage() @_requires_civisibility_enabled -def _on_session_get_codeowners() -> Optional[Codeowners]: +def on_session_get_codeowners() -> Optional[Codeowners]: log.debug("Getting codeowners") return CIVisibility.get_codeowners() @_requires_civisibility_enabled -def _on_session_get_tracer() -> Optional[Tracer]: +def on_session_get_tracer() -> Optional[Tracer]: log.debug("Getting tracer") return CIVisibility.get_tracer() @_requires_civisibility_enabled -def _on_session_is_atr_enabled() -> bool: +def on_session_is_atr_enabled() -> bool: log.debug("Getting Auto Test Retries enabled") return CIVisibility.is_atr_enabled() @_requires_civisibility_enabled -def _on_session_is_efd_enabled() -> bool: +def on_session_is_efd_enabled() -> bool: log.debug("Getting Early Flake Detection enabled") return CIVisibility.is_efd_enabled() @_requires_civisibility_enabled -def _on_session_set_covered_lines_pct(coverage_pct) -> None: +def on_session_set_covered_lines_pct(coverage_pct) -> None: log.debug("Setting coverage percentage for session to %s", coverage_pct) CIVisibility.get_session().set_covered_lines_pct(coverage_pct) @_requires_civisibility_enabled -def _on_session_set_library_capabilities(capabilities: LibraryCapabilities) -> None: +def on_session_set_library_capabilities(capabilities: LibraryCapabilities) -> None: log.debug("Setting library capabilities") CIVisibility.set_library_capabilities(capabilities) @_requires_civisibility_enabled -def _on_session_set_itr_skipped_count(skipped_count: int) -> None: +def on_session_set_itr_skipped_count(skipped_count: int) -> None: log.debug("Setting skipped count: %d", skipped_count) CIVisibility.get_session().set_skipped_count(skipped_count) @_requires_civisibility_enabled -def _on_session_get_path_codeowners(path: Path) -> Optional[List[str]]: +def on_session_get_path_codeowners(path: Path) -> Optional[List[str]]: log.debug("Getting codeowners for path %s", path) codeowners = CIVisibility.get_codeowners() if codeowners is None: @@ -1188,34 +1187,8 @@ def _on_session_get_path_codeowners(path: Path) -> Optional[List[str]]: return codeowners.of(str(path)) -def _register_session_handlers() -> None: - log.debug("Registering session handlers") - core.on("test_visibility.session.discover", _on_discover_session) - core.on("test_visibility.session.start", _on_start_session) - core.on("test_visibility.session.finish", _on_finish_session) - core.on("test_visibility.session.get_codeowners", _on_session_get_codeowners, "codeowners") - core.on("test_visibility.session.get_tracer", _on_session_get_tracer, "tracer") - core.on("test_visibility.session.get_path_codeowners", _on_session_get_path_codeowners, "path_codeowners") - core.on("test_visibility.session.get_workspace_path", _on_session_get_workspace_path, "workspace_path") - core.on("test_visibility.session.is_atr_enabled", _on_session_is_atr_enabled, "is_atr_enabled") - core.on("test_visibility.session.is_efd_enabled", _on_session_is_efd_enabled, "is_efd_enabled") - core.on( - "test_visibility.session.should_collect_coverage", - _on_session_should_collect_coverage, - "should_collect_coverage", - ) - core.on( - "test_visibility.session.is_test_skipping_enabled", - _on_session_is_test_skipping_enabled, - "is_test_skipping_enabled", - ) - core.on("test_visibility.session.set_covered_lines_pct", _on_session_set_covered_lines_pct) - core.on("test_visibility.session.set_library_capabilities", _on_session_set_library_capabilities) - core.on("test_visibility.session.set_itr_skipped_count", _on_session_set_itr_skipped_count) - - @_requires_civisibility_enabled -def _on_discover_module(discover_args: TestModule.DiscoverArgs) -> None: +def on_discover_module(discover_args: TestModule.DiscoverArgs) -> None: log.debug("Handling discovery for module %s", discover_args.module_id) session = CIVisibility.get_session() @@ -1230,26 +1203,19 @@ def _on_discover_module(discover_args: TestModule.DiscoverArgs) -> None: @_requires_civisibility_enabled -def _on_start_module(module_id: TestModuleId) -> None: +def on_start_module(module_id: TestModuleId) -> None: log.debug("Handling start for module id %s", module_id) CIVisibility.get_module_by_id(module_id).start() @_requires_civisibility_enabled -def _on_finish_module(finish_args: TestModule.FinishArgs) -> None: +def on_finish_module(finish_args: TestModule.FinishArgs) -> None: log.debug("Handling finish for module id %s", finish_args.module_id) CIVisibility.get_module_by_id(finish_args.module_id).finish() -def _register_module_handlers() -> None: - log.debug("Registering module handlers") - core.on("test_visibility.module.discover", _on_discover_module) - core.on("test_visibility.module.start", _on_start_module) - core.on("test_visibility.module.finish", _on_finish_module) - - @_requires_civisibility_enabled -def _on_discover_suite(discover_args: TestSuite.DiscoverArgs) -> None: +def on_discover_suite(discover_args: TestSuite.DiscoverArgs) -> None: log.debug("Handling discovery for suite args %s", discover_args) module = CIVisibility.get_module_by_id(discover_args.suite_id.parent_id) @@ -1265,28 +1231,21 @@ def _on_discover_suite(discover_args: TestSuite.DiscoverArgs) -> None: @_requires_civisibility_enabled -def _on_start_suite(suite_id: TestSuiteId) -> None: +def on_start_suite(suite_id: TestSuiteId) -> None: log.debug("Handling start for suite id %s", suite_id) CIVisibility.get_suite_by_id(suite_id).start() @_requires_civisibility_enabled -def _on_finish_suite(finish_args: TestSuite.FinishArgs) -> None: +def on_finish_suite(finish_args: TestSuite.FinishArgs) -> None: log.debug("Handling finish for suite id %s", finish_args.suite_id) CIVisibility.get_suite_by_id(finish_args.suite_id).finish( finish_args.force_finish_children, finish_args.override_status ) -def _register_suite_handlers() -> None: - log.debug("Registering suite handlers") - core.on("test_visibility.suite.discover", _on_discover_suite) - core.on("test_visibility.suite.start", _on_start_suite) - core.on("test_visibility.suite.finish", _on_finish_suite) - - @_requires_civisibility_enabled -def _on_discover_test(discover_args: Test.DiscoverArgs) -> None: +def on_discover_test(discover_args: Test.DiscoverArgs) -> None: log.debug("Handling discovery for test %s", discover_args.test_id) suite = CIVisibility.get_suite_by_id(discover_args.test_id.parent_id) @@ -1323,37 +1282,37 @@ def _on_discover_test(discover_args: Test.DiscoverArgs) -> None: @_requires_civisibility_enabled -def _on_is_new_test(test_id: Union[TestId, InternalTestId]) -> bool: +def on_is_new_test(test_id: Union[TestId, InternalTestId]) -> bool: log.debug("Handling is new test for test %s", test_id) return CIVisibility.get_test_by_id(test_id).is_new() @_requires_civisibility_enabled -def _on_is_quarantined_test(test_id: Union[TestId, InternalTestId]) -> bool: +def on_is_quarantined_test(test_id: Union[TestId, InternalTestId]) -> bool: log.debug("Handling is quarantined test for test %s", test_id) return CIVisibility.get_test_by_id(test_id).is_quarantined() @_requires_civisibility_enabled -def _on_is_disabled_test(test_id: Union[TestId, InternalTestId]) -> bool: +def on_is_disabled_test(test_id: Union[TestId, InternalTestId]) -> bool: log.debug("Handling is disabled test for test %s", test_id) return CIVisibility.get_test_by_id(test_id).is_disabled() @_requires_civisibility_enabled -def _on_is_attempt_to_fix(test_id: Union[TestId, InternalTestId]) -> bool: +def on_is_attempt_to_fix(test_id: Union[TestId, InternalTestId]) -> bool: log.debug("Handling is attempt to fix for test %s", test_id) return CIVisibility.get_test_by_id(test_id).is_attempt_to_fix() @_requires_civisibility_enabled -def _on_start_test(test_id: TestId) -> None: +def on_start_test(test_id: TestId) -> None: log.debug("Handling start for test id %s", test_id) CIVisibility.get_test_by_id(test_id).start() @_requires_civisibility_enabled -def _on_finish_test(finish_args: Test.FinishArgs) -> None: +def on_finish_test(finish_args: Test.FinishArgs) -> None: log.debug("Handling finish for test id %s, with status %s", finish_args.test_id, finish_args.status) CIVisibility.get_test_by_id(finish_args.test_id).finish_test( finish_args.status, finish_args.skip_reason, finish_args.exc_info @@ -1361,13 +1320,13 @@ def _on_finish_test(finish_args: Test.FinishArgs) -> None: @_requires_civisibility_enabled -def _on_set_test_parameters(item_id: TestId, parameters: str) -> None: +def on_set_test_parameters(item_id: TestId, parameters: str) -> None: log.debug("Handling set parameters for test id %s, parameters %s", item_id, parameters) CIVisibility.get_test_by_id(item_id).set_parameters(parameters) @_requires_civisibility_enabled -def _on_set_benchmark_data(set_benchmark_data_args: BenchmarkTestMixin.SetBenchmarkDataArgs) -> None: +def on_set_benchmark_data(set_benchmark_data_args: BenchmarkTestMixin.SetBenchmarkDataArgs) -> None: item_id = set_benchmark_data_args.test_id data = set_benchmark_data_args.benchmark_data is_benchmark = set_benchmark_data_args.is_benchmark @@ -1376,7 +1335,7 @@ def _on_set_benchmark_data(set_benchmark_data_args: BenchmarkTestMixin.SetBenchm @_requires_civisibility_enabled -def _on_test_overwrite_attributes(overwrite_attribute_args: InternalTest.OverwriteAttributesArgs) -> None: +def on_test_overwrite_attributes(overwrite_attribute_args: InternalTest.OverwriteAttributesArgs) -> None: item_id = overwrite_attribute_args.test_id name = overwrite_attribute_args.name suite_name = overwrite_attribute_args.suite_name @@ -1387,68 +1346,45 @@ def _on_test_overwrite_attributes(overwrite_attribute_args: InternalTest.Overwri CIVisibility.get_test_by_id(item_id).overwrite_attributes(name, suite_name, parameters, codeowners) -def _register_test_handlers(): - log.debug("Registering test handlers") - core.on("test_visibility.test.discover", _on_discover_test) - core.on("test_visibility.test.is_new", _on_is_new_test, "is_new") - core.on("test_visibility.test.is_quarantined", _on_is_quarantined_test, "is_quarantined") - core.on("test_visibility.test.is_disabled", _on_is_disabled_test, "is_disabled") - core.on("test_visibility.test.is_attempt_to_fix", _on_is_attempt_to_fix, "is_attempt_to_fix") - core.on("test_visibility.test.start", _on_start_test) - core.on("test_visibility.test.finish", _on_finish_test) - core.on("test_visibility.test.set_parameters", _on_set_test_parameters) - core.on("test_visibility.test.set_benchmark_data", _on_set_benchmark_data) - core.on("test_visibility.test.overwrite_attributes", _on_test_overwrite_attributes) - - @_requires_civisibility_enabled -def _on_item_get_span(item_id: TestVisibilityItemId) -> Optional[Span]: +def on_item_get_span(item_id: TestVisibilityItemId) -> Optional[Span]: log.debug("Handing get_span for item %s", item_id) item = CIVisibility.get_item_by_id(item_id) return item.get_span() @_requires_civisibility_enabled -def _on_item_is_finished(item_id: TestVisibilityItemId) -> bool: +def on_item_is_finished(item_id: TestVisibilityItemId) -> bool: log.debug("Handling is finished for item %s", item_id) return CIVisibility.get_item_by_id(item_id).is_finished() @_requires_civisibility_enabled -def _on_item_stash_set(item_id: TestVisibilityItemId, key: str, value: object) -> None: +def on_item_stash_set(item_id: TestVisibilityItemId, key: str, value: object) -> None: log.debug("Handling stash set for item %s, key %s, value %s", item_id, key, value) CIVisibility.get_item_by_id(item_id).stash_set(key, value) @_requires_civisibility_enabled -def _on_item_stash_get(item_id: TestVisibilityItemId, key: str) -> Optional[object]: +def on_item_stash_get(item_id: TestVisibilityItemId, key: str) -> Optional[object]: log.debug("Handling stash get for item %s, key %s", item_id, key) return CIVisibility.get_item_by_id(item_id).stash_get(key) @_requires_civisibility_enabled -def _on_item_stash_delete(item_id: TestVisibilityItemId, key: str) -> None: +def on_item_stash_delete(item_id: TestVisibilityItemId, key: str) -> None: log.debug("Handling stash delete for item %s, key %s", item_id, key) CIVisibility.get_item_by_id(item_id).stash_delete(key) -def _register_item_handlers() -> None: - log.debug("Registering item handlers") - core.on("test_visibility.item.get_span", _on_item_get_span, "span") - core.on("test_visibility.item.is_finished", _on_item_is_finished, "is_finished") - core.on("test_visibility.item.stash_set", _on_item_stash_set) - core.on("test_visibility.item.stash_get", _on_item_stash_get, "stash_value") - core.on("test_visibility.item.stash_delete", _on_item_stash_delete) - - @_requires_civisibility_enabled -def _on_get_coverage_data(item_id: Union[TestSuiteId, TestId]) -> Optional[Dict[Path, CoverageLines]]: +def on_get_coverage_data(item_id: Union[TestSuiteId, TestId]) -> Optional[Dict[Path, CoverageLines]]: log.debug("Handling get coverage data for item %s", item_id) return CIVisibility.get_item_by_id(item_id).get_coverage_data() @_requires_civisibility_enabled -def _on_add_coverage_data(add_coverage_args: ITRMixin.AddCoverageArgs) -> None: +def on_add_coverage_data(add_coverage_args: ITRMixin.AddCoverageArgs) -> None: """Adds coverage data to an item, merging with existing coverage data if necessary""" item_id = add_coverage_args.item_id coverage_data = add_coverage_args.coverage_data @@ -1462,14 +1398,8 @@ def _on_add_coverage_data(add_coverage_args: ITRMixin.AddCoverageArgs) -> None: CIVisibility.get_item_by_id(item_id).add_coverage_data(coverage_data) -def _register_coverage_handlers() -> None: - log.debug("Registering coverage handlers") - core.on("test_visibility.item.get_coverage_data", _on_get_coverage_data, "coverage_data") - core.on("test_visibility.item.add_coverage_data", _on_add_coverage_data) - - @_requires_civisibility_enabled -def _on_get_tag(get_tag_args: TestBase.GetTagArgs) -> Any: +def on_get_tag(get_tag_args: TestBase.GetTagArgs) -> Any: item_id = get_tag_args.item_id key = get_tag_args.name log.debug("Handling get tag for item id %s, key %s", item_id, key) @@ -1477,7 +1407,7 @@ def _on_get_tag(get_tag_args: TestBase.GetTagArgs) -> Any: @_requires_civisibility_enabled -def _on_set_tag(set_tag_args: TestBase.SetTagArgs) -> None: +def on_set_tag(set_tag_args: TestBase.SetTagArgs) -> None: item_id = set_tag_args.item_id key = set_tag_args.name value = set_tag_args.value @@ -1486,7 +1416,7 @@ def _on_set_tag(set_tag_args: TestBase.SetTagArgs) -> None: @_requires_civisibility_enabled -def _on_set_tags(set_tags_args: TestBase.SetTagsArgs) -> None: +def on_set_tags(set_tags_args: TestBase.SetTagsArgs) -> None: item_id = set_tags_args.item_id tags = set_tags_args.tags log.debug("Handling set tags for item id %s, tags %s", item_id, tags) @@ -1494,7 +1424,7 @@ def _on_set_tags(set_tags_args: TestBase.SetTagsArgs) -> None: @_requires_civisibility_enabled -def _on_delete_tag(delete_tag_args: TestBase.DeleteTagArgs) -> None: +def on_delete_tag(delete_tag_args: TestBase.DeleteTagArgs) -> None: item_id = delete_tag_args.item_id key = delete_tag_args.name log.debug("Handling delete tag for item id %s, key %s", item_id, key) @@ -1502,24 +1432,15 @@ def _on_delete_tag(delete_tag_args: TestBase.DeleteTagArgs) -> None: @_requires_civisibility_enabled -def _on_delete_tags(delete_tags_args: TestBase.DeleteTagsArgs) -> None: +def on_delete_tags(delete_tags_args: TestBase.DeleteTagsArgs) -> None: item_id = delete_tags_args.item_id keys = delete_tags_args.names log.debug("Handling delete tags for item id %s, keys %s", item_id, keys) CIVisibility.get_item_by_id(item_id).delete_tags(keys) -def _register_tag_handlers() -> None: - log.debug("Registering tag handlers") - core.on("test_visibility.item.get_tag", _on_get_tag, "tag_value") - core.on("test_visibility.item.set_tag", _on_set_tag) - core.on("test_visibility.item.set_tags", _on_set_tags) - core.on("test_visibility.item.delete_tag", _on_delete_tag) - core.on("test_visibility.item.delete_tags", _on_delete_tags) - - @_requires_civisibility_enabled -def _on_itr_finish_item_skipped(item_id: Union[TestSuiteId, TestId]) -> None: +def on_itr_finish_item_skipped(item_id: Union[TestSuiteId, TestId]) -> None: log.debug("Handling finish ITR skipped for item id %s", item_id) if not isinstance(item_id, (TestSuiteId, TestId)): log.warning("Only suites or tests can be skipped, not %s", type(item_id)) @@ -1528,25 +1449,25 @@ def _on_itr_finish_item_skipped(item_id: Union[TestSuiteId, TestId]) -> None: @_requires_civisibility_enabled -def _on_itr_mark_unskippable(item_id: Union[TestSuiteId, TestId]) -> None: +def on_itr_mark_unskippable(item_id: Union[TestSuiteId, TestId]) -> None: log.debug("Handling marking %s unskippable", item_id) CIVisibility.get_item_by_id(item_id).mark_itr_unskippable() @_requires_civisibility_enabled -def _on_itr_mark_forced_run(item_id: Union[TestSuiteId, TestId]) -> None: +def on_itr_mark_forced_run(item_id: Union[TestSuiteId, TestId]) -> None: log.debug("Handling marking %s as forced run", item_id) CIVisibility.get_item_by_id(item_id).mark_itr_forced_run() @_requires_civisibility_enabled -def _on_itr_was_forced_run(item_id: TestVisibilityItemId) -> bool: +def on_itr_was_forced_run(item_id: TestVisibilityItemId) -> bool: log.debug("Handling marking %s as forced run", item_id) return CIVisibility.get_item_by_id(item_id).was_itr_forced_run() @_requires_civisibility_enabled -def _on_itr_is_item_skippable(item_id: Union[TestSuiteId, TestId]) -> bool: +def on_itr_is_item_skippable(item_id: Union[TestSuiteId, TestId]) -> bool: """Skippable items are fetched as part CIVisibility.enable(), so they are assumed to be available.""" log.debug("Handling is item skippable for item id %s", item_id) @@ -1562,7 +1483,7 @@ def _on_itr_is_item_skippable(item_id: Union[TestSuiteId, TestId]) -> bool: @_requires_civisibility_enabled -def _on_itr_is_item_unskippable(item_id: Union[TestSuiteId, TestId]) -> bool: +def on_itr_is_item_unskippable(item_id: Union[TestSuiteId, TestId]) -> bool: log.debug("Handling is item unskippable for %s", item_id) if not isinstance(item_id, (TestSuiteId, TestId)): raise CIVisibilityError("Only suites or tests can be unskippable") @@ -1570,147 +1491,107 @@ def _on_itr_is_item_unskippable(item_id: Union[TestSuiteId, TestId]) -> bool: @_requires_civisibility_enabled -def _on_itr_was_item_skipped(item_id: Union[TestSuiteId, TestId]) -> bool: +def on_itr_was_item_skipped(item_id: Union[TestSuiteId, TestId]) -> bool: log.debug("Handling was item skipped for %s", item_id) return CIVisibility.get_item_by_id(item_id).is_itr_skipped() -def _register_itr_handlers() -> None: - log.debug("Registering ITR-related handlers") - core.on("test_visibility.itr.finish_skipped_by_itr", _on_itr_finish_item_skipped) - core.on("test_visibility.itr.is_item_skippable", _on_itr_is_item_skippable, "is_item_skippable") - core.on("test_visibility.itr.was_item_skipped", _on_itr_was_item_skipped, "was_item_skipped") - - core.on("test_visibility.itr.is_item_unskippable", _on_itr_is_item_unskippable, "is_item_unskippable") - core.on("test_visibility.itr.mark_forced_run", _on_itr_mark_forced_run) - core.on("test_visibility.itr.mark_unskippable", _on_itr_mark_unskippable) - core.on("test_visibility.itr.was_forced_run", _on_itr_was_forced_run, "was_forced_run") - - -# -# EFD handlers -# - - @_requires_civisibility_enabled -def _on_efd_is_enabled() -> bool: +def on_efd_is_enabled() -> bool: return CIVisibility.get_session().efd_is_enabled() @_requires_civisibility_enabled -def _on_efd_session_is_faulty() -> bool: +def on_efd_session_is_faulty() -> bool: return CIVisibility.get_session().efd_is_faulty_session() @_requires_civisibility_enabled -def _on_efd_session_has_efd_failed_tests() -> bool: +def on_efd_session_has_efd_failed_tests() -> bool: return CIVisibility.get_session().efd_has_failed_tests() @_requires_civisibility_enabled -def _on_efd_should_retry_test(test_id: InternalTestId) -> bool: +def on_efd_should_retry_test(test_id: InternalTestId) -> bool: return CIVisibility.get_test_by_id(test_id).efd_should_retry() @_requires_civisibility_enabled -def _on_efd_add_retry(test_id: InternalTestId, retry_number: int) -> Optional[int]: - return CIVisibility.get_test_by_id(test_id).efd_add_retry(retry_number) +def on_efd_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> Optional[int]: + return CIVisibility.get_test_by_id(test_id).efd_add_retry(start_immediately) @_requires_civisibility_enabled -def _on_efd_start_retry(test_id: InternalTestId, retry_number: int) -> None: +def on_efd_start_retry(test_id: InternalTestId, retry_number: int) -> None: CIVisibility.get_test_by_id(test_id).efd_start_retry(retry_number) @_requires_civisibility_enabled -def _on_efd_finish_retry(efd_finish_args: EFDTestMixin.EFDRetryFinishArgs) -> None: +def on_efd_finish_retry(efd_finish_args: EFDTestMixin.EFDRetryFinishArgs) -> None: CIVisibility.get_test_by_id(efd_finish_args.test_id).efd_finish_retry( efd_finish_args.retry_number, efd_finish_args.status, efd_finish_args.exc_info ) @_requires_civisibility_enabled -def _on_efd_get_final_status(test_id: InternalTestId) -> EFDTestStatus: +def on_efd_get_final_status(test_id: InternalTestId) -> EFDTestStatus: return CIVisibility.get_test_by_id(test_id).efd_get_final_status() -def _register_efd_handlers() -> None: - log.debug("Registering EFD handlers") - core.on("test_visibility.efd.is_enabled", _on_efd_is_enabled, "is_enabled") - core.on("test_visibility.efd.session_is_faulty", _on_efd_session_is_faulty, "is_faulty_session") - core.on("test_visibility.efd.session_has_failed_tests", _on_efd_session_has_efd_failed_tests, "has_failed_tests") - core.on("test_visibility.efd.should_retry_test", _on_efd_should_retry_test, "should_retry_test") - core.on("test_visibility.efd.add_retry", _on_efd_add_retry, "retry_number") - core.on("test_visibility.efd.start_retry", _on_efd_start_retry) - core.on("test_visibility.efd.finish_retry", _on_efd_finish_retry) - core.on("test_visibility.efd.get_final_status", _on_efd_get_final_status, "efd_final_status") - - @_requires_civisibility_enabled -def _on_atr_is_enabled() -> bool: +def on_atr_is_enabled() -> bool: return CIVisibility.is_atr_enabled() @_requires_civisibility_enabled -def _on_atr_session_has_failed_tests() -> bool: +def on_atr_session_has_failed_tests() -> bool: return CIVisibility.get_session().atr_has_failed_tests() @_requires_civisibility_enabled -def _on_atr_should_retry_test(item_id: InternalTestId) -> bool: +def on_atr_should_retry_test(item_id: InternalTestId) -> bool: return CIVisibility.get_test_by_id(item_id).atr_should_retry() @_requires_civisibility_enabled -def _on_atr_add_retry(item_id: InternalTestId, retry_number: int) -> Optional[int]: - return CIVisibility.get_test_by_id(item_id).atr_add_retry(retry_number) +def on_atr_add_retry(item_id: InternalTestId, start_immediately: bool = False) -> Optional[int]: + return CIVisibility.get_test_by_id(item_id).atr_add_retry(start_immediately) @_requires_civisibility_enabled -def _on_atr_start_retry(test_id: InternalTestId, retry_number: int) -> None: +def on_atr_start_retry(test_id: InternalTestId, retry_number: int) -> None: CIVisibility.get_test_by_id(test_id).atr_start_retry(retry_number) @_requires_civisibility_enabled -def _on_atr_finish_retry(atr_finish_args: ATRTestMixin.ATRRetryFinishArgs) -> None: +def on_atr_finish_retry(atr_finish_args: ATRTestMixin.ATRRetryFinishArgs) -> None: CIVisibility.get_test_by_id(atr_finish_args.test_id).atr_finish_retry( atr_finish_args.retry_number, atr_finish_args.status, atr_finish_args.exc_info ) @_requires_civisibility_enabled -def _on_atr_get_final_status(test_id: InternalTestId) -> TestStatus: +def on_atr_get_final_status(test_id: InternalTestId) -> TestStatus: return CIVisibility.get_test_by_id(test_id).atr_get_final_status() -def _register_atr_handlers() -> None: - log.debug("Registering ATR handlers") - core.on("test_visibility.atr.is_enabled", _on_atr_is_enabled, "is_enabled") - core.on("test_visibility.atr.session_has_failed_tests", _on_atr_session_has_failed_tests, "has_failed_tests") - core.on("test_visibility.atr.should_retry_test", _on_atr_should_retry_test, "should_retry_test") - core.on("test_visibility.atr.add_retry", _on_atr_add_retry, "retry_number") - core.on("test_visibility.atr.start_retry", _on_atr_start_retry) - core.on("test_visibility.atr.finish_retry", _on_atr_finish_retry) - core.on("test_visibility.atr.get_final_status", _on_atr_get_final_status, "atr_final_status") - - @_requires_civisibility_enabled -def _on_attempt_to_fix_should_retry_test(item_id: InternalTestId) -> bool: +def on_attempt_to_fix_should_retry_test(item_id: InternalTestId) -> bool: return CIVisibility.get_test_by_id(item_id).attempt_to_fix_should_retry() @_requires_civisibility_enabled -def _on_attempt_to_fix_add_retry(item_id: InternalTestId, retry_number: int) -> Optional[int]: - return CIVisibility.get_test_by_id(item_id).attempt_to_fix_add_retry(retry_number) +def on_attempt_to_fix_add_retry(item_id: InternalTestId, start_immediately: bool = False) -> Optional[int]: + return CIVisibility.get_test_by_id(item_id).attempt_to_fix_add_retry(start_immediately) @_requires_civisibility_enabled -def _on_attempt_to_fix_start_retry(test_id: InternalTestId, retry_number: int) -> None: +def on_attempt_to_fix_start_retry(test_id: InternalTestId, retry_number: int) -> None: CIVisibility.get_test_by_id(test_id).attempt_to_fix_start_retry(retry_number) @_requires_civisibility_enabled -def _on_attempt_to_fix_finish_retry( +def on_attempt_to_fix_finish_retry( attempt_to_fix_finish_args: AttemptToFixTestMixin.AttemptToFixRetryFinishArgs, ) -> None: CIVisibility.get_test_by_id(attempt_to_fix_finish_args.test_id).attempt_to_fix_finish_retry( @@ -1719,43 +1600,10 @@ def _on_attempt_to_fix_finish_retry( @_requires_civisibility_enabled -def _on_attempt_to_fix_get_final_status(test_id: InternalTestId) -> TestStatus: +def on_attempt_to_fix_get_final_status(test_id: InternalTestId) -> TestStatus: return CIVisibility.get_test_by_id(test_id).attempt_to_fix_get_final_status() @_requires_civisibility_enabled -def _on_attempt_to_fix_session_has_failed_tests() -> bool: +def on_attempt_to_fix_session_has_failed_tests() -> bool: return CIVisibility.get_session().attempt_to_fix_has_failed_tests() - - -def _register_attempt_to_fix_handlers() -> None: - log.debug("Registering AttemptToFix handlers") - core.on( - "test_visibility.attempt_to_fix.should_retry_test", _on_attempt_to_fix_should_retry_test, "should_retry_test" - ) - core.on("test_visibility.attempt_to_fix.add_retry", _on_attempt_to_fix_add_retry, "retry_number") - core.on("test_visibility.attempt_to_fix.start_retry", _on_attempt_to_fix_start_retry) - core.on("test_visibility.attempt_to_fix.finish_retry", _on_attempt_to_fix_finish_retry) - core.on( - "test_visibility.attempt_to_fix.session_has_failed_tests", - _on_attempt_to_fix_session_has_failed_tests, - "has_failed_tests", - ) - core.on( - "test_visibility.attempt_to_fix.get_final_status", - _on_attempt_to_fix_get_final_status, - "attempt_to_fix_final_status", - ) - - -_register_session_handlers() -_register_module_handlers() -_register_suite_handlers() -_register_test_handlers() -_register_item_handlers() -_register_tag_handlers() -_register_coverage_handlers() -_register_itr_handlers() -_register_efd_handlers() -_register_atr_handlers() -_register_attempt_to_fix_handlers() diff --git a/ddtrace/internal/ci_visibility/telemetry/test_session.py b/ddtrace/internal/ci_visibility/telemetry/test_session.py new file mode 100644 index 00000000000..051be2c7001 --- /dev/null +++ b/ddtrace/internal/ci_visibility/telemetry/test_session.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +from enum import Enum + +from ddtrace.internal.logger import get_logger +from ddtrace.internal.telemetry import telemetry_writer +from ddtrace.internal.telemetry.constants import TELEMETRY_NAMESPACE + + +log = get_logger(__name__) + + +class TEST_SESSION_TELEMETRY(str, Enum): + COLLECTED = "total_tests_collected" + + +def record_num_tests_discovered(count_tests: int): + log.debug("Recording session collected items telemetry: %s", count_tests) + telemetry_writer.add_distribution_metric( + TELEMETRY_NAMESPACE.CIVISIBILITY, TEST_SESSION_TELEMETRY.COLLECTED, count_tests + ) diff --git a/ddtrace/internal/test_visibility/_atr_mixins.py b/ddtrace/internal/test_visibility/_atr_mixins.py index 71737bf09e3..83d85c3cacc 100644 --- a/ddtrace/internal/test_visibility/_atr_mixins.py +++ b/ddtrace/internal/test_visibility/_atr_mixins.py @@ -7,7 +7,6 @@ from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId - log = get_logger(__name__) @@ -22,88 +21,65 @@ class ATRSessionMixin: @staticmethod @_catch_and_log_exceptions def atr_is_enabled() -> bool: - log.debug("Checking if Auto Test Retries is enabled for session") - is_enabled = core.dispatch_with_results("test_visibility.atr.is_enabled").is_enabled.value - log.debug("Auto Test Retries enabled: %s", is_enabled) - return is_enabled + log.debug("Checking if ATR is enabled") + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_atr_is_enabled + return on_atr_is_enabled() @staticmethod @_catch_and_log_exceptions def atr_has_failed_tests() -> bool: - log.debug("Checking if session has failed tests for Auto Test Retries") - has_failed_tests = core.dispatch_with_results( - "test_visibility.atr.session_has_failed_tests" - ).has_failed_tests.value - log.debug("Session has ATR failed tests: %s", has_failed_tests) - return has_failed_tests + log.debug("Checking if ATR session has failed tests") + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_atr_session_has_failed_tests + return on_atr_session_has_failed_tests() class ATRTestMixin: + @dataclasses.dataclass + class ATRRetryFinishArgs: + test_id: InternalTestId + retry_number: int + status: ext_api.TestStatus + exc_info: t.Optional[t.Tuple[t.Type[BaseException], BaseException, t.Any]] + @staticmethod @_catch_and_log_exceptions - def atr_should_retry(item_id: InternalTestId) -> bool: - log.debug("Checking if item %s should be retried for Auto Test Retries", item_id) - should_retry_test = core.dispatch_with_results( - "test_visibility.atr.should_retry_test", (item_id,) - ).should_retry_test.value - log.debug("Item %s should be retried: %s", item_id, should_retry_test) - return should_retry_test + def atr_should_retry(test_id: InternalTestId) -> bool: + log.debug("Checking if test %s should be retried by ATR", test_id) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_atr_should_retry_test + return on_atr_should_retry_test(test_id) @staticmethod @_catch_and_log_exceptions - def atr_add_retry(item_id: InternalTestId, start_immediately: bool = False) -> int: - log.debug("Adding Auto Test Retries retry for item %s", item_id) - retry_number = core.dispatch_with_results( - "test_visibility.atr.add_retry", (item_id, start_immediately) - ).retry_number.value - log.debug("Added Auto Test Retries retry %s for item %s", retry_number, item_id) + def atr_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_atr_add_retry + retry_number = on_atr_add_retry(test_id, start_immediately) + log.debug("Adding ATR retry %s for test %s", retry_number, test_id) return retry_number @staticmethod @_catch_and_log_exceptions - def atr_start_retry(item_id: InternalTestId) -> None: - log.debug("Starting retry for item %s", item_id) - core.dispatch("test_visibility.atr.start_retry", (item_id,)) - - class ATRRetryFinishArgs(t.NamedTuple): - test_id: InternalTestId - retry_number: int - status: ext_api.TestStatus - skip_reason: t.Optional[str] = None - exc_info: t.Optional[ext_api.TestExcInfo] = None + def atr_start_retry(test_id: InternalTestId, retry_number: int) -> None: + log.debug("Starting ATR retry %s for test %s", retry_number, test_id) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_atr_start_retry + on_atr_start_retry(test_id, retry_number) @staticmethod @_catch_and_log_exceptions - def atr_finish_retry( - item_id: InternalTestId, - retry_number: int, - status: ext_api.TestStatus, - skip_reason: t.Optional[str] = None, - exc_info: t.Optional[ext_api.TestExcInfo] = None, - ): - log.debug( - "Finishing ATR test retry %s for item %s, status: %s, skip_reason: %s, exc_info: %s", - retry_number, - item_id, - status, - skip_reason, - exc_info, - ) - core.dispatch( - "test_visibility.atr.finish_retry", - ( - ATRTestMixin.ATRRetryFinishArgs( - item_id, retry_number, status, skip_reason=skip_reason, exc_info=exc_info - ), - ), - ) + def atr_finish_retry(finish_args: "ATRTestMixin.ATRRetryFinishArgs") -> None: + log.debug("Finishing ATR retry %s for test %s", finish_args.retry_number, finish_args.test_id) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_atr_finish_retry + on_atr_finish_retry(finish_args) @staticmethod @_catch_and_log_exceptions - def atr_get_final_status(item_id: InternalTestId) -> ext_api.TestStatus: - log.debug("Getting final ATR status for item %s", item_id) - atr_final_status = core.dispatch_with_results( - "test_visibility.atr.get_final_status", (item_id,) - ).atr_final_status.value - log.debug("Final ATR status for item %s: %s", item_id, atr_final_status) - return atr_final_status + def atr_get_final_status(test_id: InternalTestId) -> ext_api.TestStatus: + log.debug("Getting ATR final status for test %s", test_id) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_atr_get_final_status + return on_atr_get_final_status(test_id) diff --git a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py index a2d0dfec9c5..83f7802ca90 100644 --- a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py +++ b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py @@ -6,7 +6,6 @@ from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId - log = get_logger(__name__) @@ -14,80 +13,56 @@ class AttemptToFixSessionMixin: @staticmethod @_catch_and_log_exceptions def attempt_to_fix_has_failed_tests() -> bool: - log.debug("Checking if session has failed tests for Attempt-to-Fix") - has_failed_tests = core.dispatch_with_results( - "test_visibility.attempt_to_fix.session_has_failed_tests" - ).has_failed_tests.value - log.debug("Session has Attempt-to-Fix failed tests: %s", has_failed_tests) - return has_failed_tests + log.debug("Checking if attempt to fix session has failed tests") + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_attempt_to_fix_session_has_failed_tests + return on_attempt_to_fix_session_has_failed_tests() class AttemptToFixTestMixin: + class AttemptToFixRetryFinishArgs(t.NamedTuple): + test_id: InternalTestId + retry_number: int + status: ext_api.TestStatus + exc_info: t.Optional[t.Tuple[t.Type[BaseException], BaseException, t.Any]] + @staticmethod @_catch_and_log_exceptions - def attempt_to_fix_should_retry(item_id: InternalTestId) -> bool: - log.debug("Checking if item %s should be retried for Attempt-to-Fix", item_id) - should_retry_test = core.dispatch_with_results( - "test_visibility.attempt_to_fix.should_retry_test", (item_id,) - ).should_retry_test.value - log.debug("Item %s should be retried: %s", item_id, should_retry_test) - return should_retry_test + def attempt_to_fix_should_retry(test_id: InternalTestId) -> bool: + log.debug("Checking if test %s should be retried by attempt to fix", test_id) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_attempt_to_fix_should_retry_test + return on_attempt_to_fix_should_retry_test(test_id) @staticmethod @_catch_and_log_exceptions - def attempt_to_fix_add_retry(item_id: InternalTestId, start_immediately: bool = False) -> int: - log.debug("Adding Attempt-to-Fix retry for item %s", item_id) - retry_number = core.dispatch_with_results( - "test_visibility.attempt_to_fix.add_retry", (item_id, start_immediately) - ).retry_number.value - log.debug("Added Auto Test Retries retry %s for item %s", retry_number, item_id) + def attempt_to_fix_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_attempt_to_fix_add_retry + retry_number = on_attempt_to_fix_add_retry(test_id, start_immediately) + log.debug("Adding attempt to fix retry %s for test %s", retry_number, test_id) return retry_number @staticmethod @_catch_and_log_exceptions - def attempt_to_fix_start_retry(item_id: InternalTestId) -> None: - log.debug("Starting retry for item %s", item_id) - core.dispatch("test_visibility.attempt_to_fix.start_retry", (item_id,)) - - class AttemptToFixRetryFinishArgs(t.NamedTuple): - test_id: InternalTestId - retry_number: int - status: ext_api.TestStatus - skip_reason: t.Optional[str] = None - exc_info: t.Optional[ext_api.TestExcInfo] = None + def attempt_to_fix_start_retry(test_id: InternalTestId, retry_number: int) -> None: + log.debug("Starting attempt to fix retry %s for test %s", retry_number, test_id) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_attempt_to_fix_start_retry + on_attempt_to_fix_start_retry(test_id, retry_number) @staticmethod @_catch_and_log_exceptions - def attempt_to_fix_finish_retry( - item_id: InternalTestId, - retry_number: int, - status: ext_api.TestStatus, - skip_reason: t.Optional[str] = None, - exc_info: t.Optional[ext_api.TestExcInfo] = None, - ): - log.debug( - "Finishing Attempt-to-Fix test retry %s for item %s, status: %s, skip_reason: %s, exc_info: %s", - retry_number, - item_id, - status, - skip_reason, - exc_info, - ) - core.dispatch( - "test_visibility.attempt_to_fix.finish_retry", - ( - AttemptToFixTestMixin.AttemptToFixRetryFinishArgs( - item_id, retry_number, status, skip_reason=skip_reason, exc_info=exc_info - ), - ), - ) + def attempt_to_fix_finish_retry(finish_args: "AttemptToFixTestMixin.AttemptToFixRetryFinishArgs") -> None: + log.debug("Finishing attempt to fix retry %s for test %s", finish_args.retry_number, finish_args.test_id) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_attempt_to_fix_finish_retry + on_attempt_to_fix_finish_retry(finish_args) @staticmethod @_catch_and_log_exceptions - def attempt_to_fix_get_final_status(item_id: InternalTestId) -> ext_api.TestStatus: - log.debug("Getting final Attempt-to-Fix status for item %s", item_id) - attempt_to_fix_final_status = core.dispatch_with_results( - "test_visibility.attempt_to_fix.get_final_status", (item_id,) - ).attempt_to_fix_final_status.value - log.debug("Final Attempt-to-Fix status for item %s: %s", item_id, attempt_to_fix_final_status) - return attempt_to_fix_final_status + def attempt_to_fix_get_final_status(test_id: InternalTestId) -> ext_api.TestStatus: + log.debug("Getting attempt to fix final status for test %s", test_id) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_attempt_to_fix_get_final_status + return on_attempt_to_fix_get_final_status(test_id) diff --git a/ddtrace/internal/test_visibility/_efd_mixins.py b/ddtrace/internal/test_visibility/_efd_mixins.py index ac49c5641c3..e0224f0e39f 100644 --- a/ddtrace/internal/test_visibility/_efd_mixins.py +++ b/ddtrace/internal/test_visibility/_efd_mixins.py @@ -22,99 +22,72 @@ class EFDSessionMixin: @staticmethod @_catch_and_log_exceptions def efd_enabled() -> bool: - log.debug("Checking if Early Flake Detection is enabled for the session") - is_enabled = core.dispatch_with_results("test_visibility.efd.is_enabled").is_enabled.value - log.debug("Early Flake Detection enabled: %s", is_enabled) - return is_enabled + log.debug("Checking if EFD is enabled") + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_efd_is_enabled + return on_efd_is_enabled() @staticmethod @_catch_and_log_exceptions def efd_is_faulty_session() -> bool: - log.debug("Checking if session is faulty for Early Flake Detection") - is_faulty_session = core.dispatch_with_results("test_visibility.efd.session_is_faulty").is_faulty_session.value - log.debug("Session faulty: %s", is_faulty_session) - return is_faulty_session + log.debug("Checking if EFD session is faulty") + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_efd_session_is_faulty + return on_efd_session_is_faulty() @staticmethod @_catch_and_log_exceptions def efd_has_failed_tests() -> bool: - log.debug("Checking if session has failed tests for Early Flake Detection") - has_failed_tests = core.dispatch_with_results( - "test_visibility.efd.session_has_failed_tests" - ).has_failed_tests.value - log.debug("Session has EFD failed tests: %s", has_failed_tests) - return has_failed_tests + log.debug("Checking if EFD session has failed tests") + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_efd_session_has_efd_failed_tests + return on_efd_session_has_efd_failed_tests() class EFDTestMixin: + class EFDRetryFinishArgs(t.NamedTuple): + test_id: InternalTestId + retry_number: int + status: ext_api.TestStatus + exc_info: t.Optional[t.Tuple[t.Type[BaseException], BaseException, t.Any]] + @staticmethod @_catch_and_log_exceptions - def efd_should_retry(item_id: InternalTestId) -> bool: - """Checks whether a test should be retried - - This does not differentiate between the feature being disabled, the test retries having completed, or the - session maximums having been reached. - """ - log.debug("Checking if item %s should be retried for Early Flake Detection", item_id) - should_retry_test = core.dispatch_with_results( - "test_visibility.efd.should_retry_test", (item_id,) - ).should_retry_test.value - return should_retry_test + def efd_should_retry(test_id: InternalTestId) -> bool: + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_efd_should_retry_test + log.debug("Checking if test %s should be retried by EFD", test_id) + return on_efd_should_retry_test(test_id) @staticmethod @_catch_and_log_exceptions - def efd_add_retry(item_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: - log.debug("Adding Early Flake Detection retry for item %s", item_id) - retry_number = core.dispatch_with_results( - "test_visibility.efd.add_retry", (item_id, start_immediately) - ).retry_number.value - log.debug("Added Early Flake Detection retry number %s for item %s", retry_number, item_id) + def efd_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_efd_add_retry + retry_number = on_efd_add_retry(test_id, start_immediately) + log.debug("Adding EFD retry %s for test %s", retry_number, test_id) return retry_number @staticmethod @_catch_and_log_exceptions - def efd_start_retry(item_id: InternalTestId, retry_number: int): - log.debug("Starting Early Flake Detection retry number %s for item %s", retry_number, item_id) - core.dispatch("test_visibility.efd.start_retry", (item_id, retry_number)) - - class EFDRetryFinishArgs(t.NamedTuple): - test_id: InternalTestId - retry_number: int - status: ext_api.TestStatus - skip_reason: t.Optional[str] = None - exc_info: t.Optional[ext_api.TestExcInfo] = None + def efd_start_retry(test_id: InternalTestId, retry_number: int) -> None: + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_efd_start_retry + log.debug("Starting EFD retry %s for test %s", retry_number, test_id) + on_efd_start_retry(test_id, retry_number) @staticmethod @_catch_and_log_exceptions - def efd_finish_retry( - item_id: InternalTestId, - retry_number: int, - status: ext_api.TestStatus, - skip_reason: t.Optional[str] = None, - exc_info: t.Optional[ext_api.TestExcInfo] = None, - ): - log.debug( - "Finishing EFD test retry %s for item %s, status: %s, skip_reason: %s, exc_info: %s", - retry_number, - item_id, - status, - skip_reason, - exc_info, - ) - core.dispatch( - "test_visibility.efd.finish_retry", - ( - EFDTestMixin.EFDRetryFinishArgs( - item_id, retry_number, status, skip_reason=skip_reason, exc_info=exc_info - ), - ), - ) + def efd_finish_retry(finish_args: "EFDTestMixin.EFDRetryFinishArgs") -> None: + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_efd_finish_retry + log.debug("Finishing EFD retry %s for test %s", finish_args.retry_number, finish_args.test_id) + on_efd_finish_retry(finish_args) @staticmethod @_catch_and_log_exceptions - def efd_get_final_status(item_id) -> EFDTestStatus: - log.debug("Getting final status for item %s in Early Flake Detection", item_id) - final_status = core.dispatch_with_results( - "test_visibility.efd.get_final_status", (item_id,) - ).efd_final_status.value - return final_status + def efd_get_final_status(test_id: InternalTestId) -> EFDTestStatus: + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_efd_get_final_status + log.debug("Getting EFD final status for test %s", test_id) + return on_efd_get_final_status(test_id) diff --git a/ddtrace/internal/test_visibility/_itr_mixins.py b/ddtrace/internal/test_visibility/_itr_mixins.py index 60572885a21..5e9764c46a2 100644 --- a/ddtrace/internal/test_visibility/_itr_mixins.py +++ b/ddtrace/internal/test_visibility/_itr_mixins.py @@ -8,97 +8,84 @@ from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId from ddtrace.internal.test_visibility.coverage_lines import CoverageLines - log = get_logger(__name__) class ITRMixin: """Mixin class for ITR-related functionality.""" + class AddCoverageArgs(t.NamedTuple): + item_id: t.Union[ext_api.TestSuiteId, InternalTestId] + coverage_data: t.Dict[Path, CoverageLines] + @staticmethod @_catch_and_log_exceptions def mark_itr_skipped(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): log.debug("Marking item %s as skipped by ITR", item_id) - core.dispatch("test_visibility.itr.finish_skipped_by_itr", (item_id,)) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_itr_finish_item_skipped + on_itr_finish_item_skipped(item_id) @staticmethod @_catch_and_log_exceptions def mark_itr_unskippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): log.debug("Marking item %s as unskippable by ITR", item_id) - core.dispatch("test_visibility.itr.mark_unskippable", (item_id,)) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_itr_mark_unskippable + on_itr_mark_unskippable(item_id) @staticmethod @_catch_and_log_exceptions def mark_itr_forced_run(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): - log.debug("Marking item %s as unskippable by ITR", item_id) - core.dispatch("test_visibility.itr.mark_forced_run", (item_id,)) + log.debug("Marking item %s as forced run by ITR", item_id) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_itr_mark_forced_run + on_itr_mark_forced_run(item_id) @staticmethod @_catch_and_log_exceptions - def was_forced_run(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: - """Skippable items are not currently tied to a test session, so no session ID is passed""" - log.debug("Checking if item %s was forced to run", item_id) - _was_forced_run = bool( - core.dispatch_with_results("test_visibility.itr.was_forced_run", (item_id,)).was_forced_run.value - ) - log.debug("Item %s was forced run: %s", item_id, _was_forced_run) - return _was_forced_run + def was_itr_forced_run(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: + log.debug("Checking if item %s was forced run by ITR", item_id) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_itr_was_forced_run + return on_itr_was_forced_run(item_id) @staticmethod @_catch_and_log_exceptions def is_itr_skippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: - """Skippable items are not currently tied to a test session, so no session ID is passed""" - log.debug("Checking if item %s is skippable", item_id) - is_item_skippable = bool( - core.dispatch_with_results("test_visibility.itr.is_item_skippable", (item_id,)).is_item_skippable.value - ) - log.debug("Item %s is skippable: %s", item_id, is_item_skippable) - - return is_item_skippable + log.debug("Checking if item %s is skippable by ITR", item_id) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_itr_is_item_skippable + return on_itr_is_item_skippable(item_id) @staticmethod @_catch_and_log_exceptions def is_itr_unskippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: - """Skippable items are not currently tied to a test session, so no session ID is passed""" - log.debug("Checking if item %s is unskippable", item_id) - is_item_unskippable = bool( - core.dispatch_with_results("test_visibility.itr.is_item_unskippable", (item_id,)).is_item_unskippable.value - ) - log.debug("Item %s is unskippable: %s", item_id, is_item_unskippable) - - return is_item_unskippable + log.debug("Checking if item %s is unskippable by ITR", item_id) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_itr_is_item_unskippable + return on_itr_is_item_unskippable(item_id) @staticmethod @_catch_and_log_exceptions - def was_skipped_by_itr(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: - """Skippable items are not currently tied to a test session, so no session ID is passed""" + def was_itr_skipped(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: log.debug("Checking if item %s was skipped by ITR", item_id) - was_item_skipped = bool( - core.dispatch_with_results("test_visibility.itr.was_item_skipped", (item_id,)).was_item_skipped.value - ) - log.debug("Item %s was skipped by ITR: %s", item_id, was_item_skipped) - return was_item_skipped - - class AddCoverageArgs(t.NamedTuple): - item_id: t.Union[ext_api.TestSuiteId, ext_api.TestId] - coverage_data: t.Dict[Path, CoverageLines] + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_itr_was_item_skipped + return on_itr_was_item_skipped(item_id) @staticmethod @_catch_and_log_exceptions - def add_coverage_data( - item_id: t.Union[ext_api.TestSuiteId, InternalTestId], coverage_data: t.Dict[Path, CoverageLines] - ): - log.debug("Adding coverage data for item %s: %s", item_id, coverage_data) - core.dispatch("test_visibility.item.add_coverage_data", (ITRMixin.AddCoverageArgs(item_id, coverage_data),)) + def add_coverage_data(add_coverage_args: "ITRMixin.AddCoverageArgs") -> None: + log.debug("Adding coverage data for item %s", add_coverage_args.item_id) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_add_coverage_data + on_add_coverage_data(add_coverage_args) @staticmethod @_catch_and_log_exceptions - def get_coverage_data( - item_id: t.Union[ext_api.TestSuiteId, InternalTestId] - ) -> t.Optional[t.Dict[Path, CoverageLines]]: + def get_coverage_data(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> t.Optional[t.Dict[Path, CoverageLines]]: log.debug("Getting coverage data for item %s", item_id) - coverage_data = core.dispatch_with_results( - "test_visibility.item.get_coverage_data", (item_id,) - ).coverage_data.value - log.debug("Coverage data for item %s: %s", item_id, coverage_data) - return coverage_data + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_get_coverage_data + return on_get_coverage_data(item_id) diff --git a/ddtrace/internal/test_visibility/_utils.py b/ddtrace/internal/test_visibility/_utils.py index 0e22a15dc94..db64b9646ce 100644 --- a/ddtrace/internal/test_visibility/_utils.py +++ b/ddtrace/internal/test_visibility/_utils.py @@ -1,13 +1,12 @@ from ddtrace.ext.test_visibility._test_visibility_base import TestVisibilityItemId -from ddtrace.internal import core from ddtrace.internal.logger import get_logger from ddtrace.trace import Span - log = get_logger(__name__) def _get_item_span(item_id: TestVisibilityItemId) -> Span: log.debug("Getting span for item %s", item_id) - span: Span = core.dispatch_with_results("test_visibility.item.get_span", (item_id,)).span.value - return span + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_item_get_span + return on_item_get_span(item_id) diff --git a/ddtrace/internal/test_visibility/api.py b/ddtrace/internal/test_visibility/api.py index 4b8efaada61..21fca81d097 100644 --- a/ddtrace/internal/test_visibility/api.py +++ b/ddtrace/internal/test_visibility/api.py @@ -5,10 +5,10 @@ from ddtrace.ext.test_visibility import api as ext_api from ddtrace.ext.test_visibility._test_visibility_base import TestSessionId from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions +from ddtrace.internal.test_visibility._utils import _get_item_span from ddtrace.ext.test_visibility._utils import _is_item_finished from ddtrace.ext.test_visibility.api import TestExcInfo from ddtrace.ext.test_visibility.api import TestStatus -from ddtrace.internal import core from ddtrace.internal.codeowners import Codeowners as _Codeowners from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._atr_mixins import ATRSessionMixin @@ -21,11 +21,9 @@ from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId from ddtrace.internal.test_visibility._itr_mixins import ITRMixin from ddtrace.internal.test_visibility._library_capabilities import LibraryCapabilities -from ddtrace.internal.test_visibility._utils import _get_item_span from ddtrace.trace import Span from ddtrace.trace import Tracer - log = get_logger(__name__) @@ -39,13 +37,17 @@ def get_span(item_id: t.Union[ext_api.TestVisibilityItemId, InternalTestId]) -> @_catch_and_log_exceptions def stash_set(item_id, key: str, value: object): log.debug("Stashing value %s for key %s in item %s", value, key, item_id) - core.dispatch("test_visibility.item.stash_set", (item_id, key, value)) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_item_stash_set + on_item_stash_set(item_id, key, value) @staticmethod @_catch_and_log_exceptions def stash_get(item_id: ext_api.TestVisibilityItemId, key: str): log.debug("Getting stashed value for key %s in item %s", key, item_id) - stash_value = core.dispatch_with_results("test_visibility.item.stash_get", (item_id, key)).stash_value.value + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_item_stash_get + stash_value = on_item_stash_get(item_id, key) log.debug("Got stashed value %s for key %s in item %s", stash_value, key, item_id) return stash_value @@ -53,7 +55,9 @@ def stash_get(item_id: ext_api.TestVisibilityItemId, key: str): @_catch_and_log_exceptions def stash_delete(item_id: ext_api.TestVisibilityItemId, key: str): log.debug("Deleting stashed value for key %s in item %s", key, item_id) - core.dispatch("test_visibility.item.stash_delete", (item_id, key)) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_item_stash_delete + on_item_stash_delete(item_id, key) class InternalTestSession(ext_api.TestSession, EFDSessionMixin, ATRSessionMixin, AttemptToFixSessionMixin): @@ -69,17 +73,18 @@ def is_finished() -> bool: @_catch_and_log_exceptions def get_codeowners() -> t.Optional[_Codeowners]: log.debug("Getting codeowners object") - - codeowners: t.Optional[_Codeowners] = core.dispatch_with_results( - "test_visibility.session.get_codeowners", - ).codeowners.value + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_session_get_codeowners + codeowners: t.Optional[_Codeowners] = on_session_get_codeowners() return codeowners @staticmethod @_catch_and_log_exceptions def get_tracer() -> t.Optional[Tracer]: log.debug("Getting test session tracer") - tracer: t.Optional[Tracer] = core.dispatch_with_results("test_visibility.session.get_tracer").tracer.value + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_session_get_tracer + tracer: t.Optional[Tracer] = on_session_get_tracer() log.debug("Got test session tracer: %s", tracer) return tracer @@ -87,65 +92,62 @@ def get_tracer() -> t.Optional[Tracer]: @_catch_and_log_exceptions def get_workspace_path() -> t.Optional[Path]: log.debug("Getting session workspace path") - - workspace_path: Path = core.dispatch_with_results( - "test_visibility.session.get_workspace_path" - ).workspace_path.value + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_session_get_workspace_path + workspace_path: Path = on_session_get_workspace_path() return workspace_path @staticmethod @_catch_and_log_exceptions def should_collect_coverage() -> bool: log.debug("Checking if coverage should be collected for session") - - _should_collect_coverage = bool( - core.dispatch_with_results("test_visibility.session.should_collect_coverage").should_collect_coverage.value - ) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_session_should_collect_coverage + _should_collect_coverage = bool(on_session_should_collect_coverage()) log.debug("Coverage should be collected: %s", _should_collect_coverage) - return _should_collect_coverage @staticmethod @_catch_and_log_exceptions def is_test_skipping_enabled() -> bool: log.debug("Checking if test skipping is enabled") - - _is_test_skipping_enabled = bool( - core.dispatch_with_results( - "test_visibility.session.is_test_skipping_enabled" - ).is_test_skipping_enabled.value - ) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_session_is_test_skipping_enabled + _is_test_skipping_enabled = bool(on_session_is_test_skipping_enabled()) log.debug("Test skipping is enabled: %s", _is_test_skipping_enabled) - return _is_test_skipping_enabled @staticmethod @_catch_and_log_exceptions def set_covered_lines_pct(coverage_pct: float): log.debug("Setting covered lines percentage for session to %s", coverage_pct) - - core.dispatch("test_visibility.session.set_covered_lines_pct", (coverage_pct,)) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_session_set_covered_lines_pct + on_session_set_covered_lines_pct(coverage_pct) @staticmethod @_catch_and_log_exceptions def get_path_codeowners(path: Path) -> t.Optional[t.List[str]]: log.debug("Getting codeowners object for path %s", path) - - path_codeowners: t.Optional[t.List[str]] = core.dispatch_with_results( - "test_visibility.session.get_path_codeowners", (path,) - ).path_codeowners.value + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_session_get_path_codeowners + path_codeowners: t.Optional[t.List[str]] = on_session_get_path_codeowners(path) return path_codeowners @staticmethod @_catch_and_log_exceptions def set_library_capabilities(capabilities: LibraryCapabilities) -> None: - core.dispatch("test_visibility.session.set_library_capabilities", (capabilities,)) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_session_set_library_capabilities + on_session_set_library_capabilities(capabilities) @staticmethod @_catch_and_log_exceptions def set_itr_tags(skipped_count: int): log.debug("Setting itr session tags: %d", skipped_count) - core.dispatch("test_visibility.session.set_itr_skipped_count", (skipped_count,)) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_session_set_itr_skipped_count + on_session_set_itr_skipped_count(skipped_count) class InternalTestModule(ext_api.TestModule, InternalTestBase): @@ -178,16 +180,18 @@ def finish( override_finish_time: t.Optional[float] = None, ): log.debug("Finishing test with status: %s, reason: %s", status, reason) - core.dispatch( - "test_visibility.test.finish", - (InternalTest.FinishArgs(item_id, status, reason, exc_info, override_finish_time),), - ) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_finish_test + from ddtrace.ext.test_visibility.api import Test + on_finish_test(Test.FinishArgs(item_id, status, reason, exc_info)) @staticmethod @_catch_and_log_exceptions def is_new_test(item_id: InternalTestId) -> bool: log.debug("Checking if test %s is new", item_id) - is_new = bool(core.dispatch_with_results("test_visibility.test.is_new", (item_id,)).is_new.value) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_is_new_test + is_new = bool(on_is_new_test(item_id)) log.debug("Test %s is new: %s", item_id, is_new) return is_new @@ -195,9 +199,9 @@ def is_new_test(item_id: InternalTestId) -> bool: @_catch_and_log_exceptions def is_quarantined_test(item_id: InternalTestId) -> bool: log.debug("Checking if test %s is quarantined", item_id) - is_quarantined = bool( - core.dispatch_with_results("test_visibility.test.is_quarantined", (item_id,)).is_quarantined.value - ) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_is_quarantined_test + is_quarantined = bool(on_is_quarantined_test(item_id)) log.debug("Test %s is quarantined: %s", item_id, is_quarantined) return is_quarantined @@ -205,7 +209,9 @@ def is_quarantined_test(item_id: InternalTestId) -> bool: @_catch_and_log_exceptions def is_disabled_test(item_id: InternalTestId) -> bool: log.debug("Checking if test %s is disabled", item_id) - is_disabled = bool(core.dispatch_with_results("test_visibility.test.is_disabled", (item_id,)).is_disabled.value) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_is_disabled_test + is_disabled = bool(on_is_disabled_test(item_id)) log.debug("Test %s is disabled: %s", item_id, is_disabled) return is_disabled @@ -213,9 +219,9 @@ def is_disabled_test(item_id: InternalTestId) -> bool: @_catch_and_log_exceptions def is_attempt_to_fix(item_id: InternalTestId) -> bool: log.debug("Checking if test %s is attempt to fix", item_id) - is_attempt_to_fix = bool( - core.dispatch_with_results("test_visibility.test.is_attempt_to_fix", (item_id,)).is_attempt_to_fix.value - ) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_is_attempt_to_fix + is_attempt_to_fix = bool(on_is_attempt_to_fix(item_id)) log.debug("Test %s is attempt to fix: %s", item_id, is_attempt_to_fix) return is_attempt_to_fix @@ -235,15 +241,9 @@ def overwrite_attributes( parameters: t.Optional[str] = None, codeowners: t.Optional[t.List[str]] = None, ): - log.debug( - "Overwriting attributes for test %s: name=%s" ", suite_name=%s" ", parameters=%s" ", codeowners=%s", - item_id, - name, - suite_name, - parameters, - codeowners, - ) - core.dispatch( - "test_visibility.test.overwrite_attributes", - (InternalTest.OverwriteAttributesArgs(item_id, name, suite_name, parameters, codeowners),), + log.debug("Overwriting attributes for test %s", item_id) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import on_test_overwrite_attributes + on_test_overwrite_attributes( + InternalTest.OverwriteAttributesArgs(item_id, name, suite_name, parameters, codeowners) ) From 97bbbd784a2f47b177b5a5a39f3498b24a992575 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Thu, 12 Jun 2025 13:53:00 +0200 Subject: [PATCH 16/58] style --- ddtrace/ext/test_visibility/_utils.py | 7 +++++- ddtrace/ext/test_visibility/api.py | 16 +++++++++++++ .../internal/test_visibility/_atr_mixins.py | 9 +++++++- .../test_visibility/_attempt_to_fix_mixins.py | 8 ++++++- .../internal/test_visibility/_efd_mixins.py | 9 +++++++- .../internal/test_visibility/_itr_mixins.py | 15 ++++++++++-- ddtrace/internal/test_visibility/_utils.py | 2 ++ ddtrace/internal/test_visibility/api.py | 23 +++++++++++++++++-- 8 files changed, 81 insertions(+), 8 deletions(-) diff --git a/ddtrace/ext/test_visibility/_utils.py b/ddtrace/ext/test_visibility/_utils.py index e77b8ca4587..f4ae14ccd5e 100644 --- a/ddtrace/ext/test_visibility/_utils.py +++ b/ddtrace/ext/test_visibility/_utils.py @@ -5,7 +5,6 @@ from ddtrace.ext.test_visibility._test_visibility_base import TestVisibilityItemId from ddtrace.ext.test_visibility._test_visibility_base import _TestVisibilityAPIBase -from ddtrace.internal import core from ddtrace.internal.logger import get_logger @@ -34,6 +33,7 @@ def _get_item_tag(item_id: TestVisibilityItemId, tag_name: str) -> Any: log.debug("Getting tag for item %s: %s", item_id, tag_name) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_get_tag + tag_value = on_get_tag(_TestVisibilityAPIBase.GetTagArgs(item_id, tag_name)) return tag_value @@ -42,6 +42,7 @@ def _set_item_tag(item_id: TestVisibilityItemId, tag_name: str, tag_value: Any, log.debug("Setting tag for item %s: %s=%s", item_id, tag_name, tag_value) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_set_tag + on_set_tag(_TestVisibilityAPIBase.SetTagArgs(item_id, tag_name, tag_value)) @@ -49,6 +50,7 @@ def _set_item_tags(item_id: TestVisibilityItemId, tags: Dict[str, Any], recurse: log.debug("Setting tags for item %s: %s", item_id, tags) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_set_tags + on_set_tags(_TestVisibilityAPIBase.SetTagsArgs(item_id, tags)) @@ -56,6 +58,7 @@ def _delete_item_tag(item_id: TestVisibilityItemId, tag_name: str, recurse: bool log.debug("Deleting tag for item %s: %s", item_id, tag_name) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_delete_tag + on_delete_tag(_TestVisibilityAPIBase.DeleteTagArgs(item_id, tag_name)) @@ -63,6 +66,7 @@ def _delete_item_tags(item_id: TestVisibilityItemId, tag_names: List[str], recur log.debug("Deleting tags for item %s: %s", item_id, tag_names) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_delete_tags + on_delete_tags(_TestVisibilityAPIBase.DeleteTagsArgs(item_id, tag_names)) @@ -70,6 +74,7 @@ def _is_item_finished(item_id: TestVisibilityItemId) -> bool: log.debug("Checking if item %s is finished", item_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_item_is_finished + _is_finished = bool(on_item_is_finished(item_id)) log.debug("Item %s is finished: %s", item_id, _is_finished) return _is_finished diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 1a3d4503bf5..1b3aeeaebc1 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -86,6 +86,7 @@ class TestExcInfo: def enable_test_visibility(config: Optional[Any] = None): log.debug("Enabling Test Visibility with config: %s", config) from ddtrace.internal.ci_visibility.recorder import CIVisibility + CIVisibility.enable(config=config) if not is_test_visibility_enabled(): @@ -95,6 +96,7 @@ def enable_test_visibility(config: Optional[Any] = None): @_catch_and_log_exceptions def is_test_visibility_enabled(): from ddtrace.internal.ci_visibility.recorder import CIVisibility + return CIVisibility.enabled @@ -102,6 +104,7 @@ def is_test_visibility_enabled(): def disable_test_visibility(): log.debug("Disabling Test Visibility") from ddtrace.internal.ci_visibility.recorder import CIVisibility + CIVisibility.disable() if is_test_visibility_enabled(): log.warning("Failed to disable Test Visibility") @@ -164,6 +167,7 @@ def discover( return from ddtrace.internal.ci_visibility.recorder import on_discover_session + on_discover_session( TestSession.DiscoverArgs( test_command, @@ -183,6 +187,7 @@ def discover( def start(distributed_children: bool = False, context: Optional[Context] = None): log.debug("Starting session") from ddtrace.internal.ci_visibility.recorder import on_start_session + on_start_session(distributed_children, context) class FinishArgs(NamedTuple): @@ -198,6 +203,7 @@ def finish( log.debug("Finishing session, force_finish_session_modules: %s", force_finish_children) from ddtrace.internal.ci_visibility.recorder import on_finish_session + on_finish_session(TestSession.FinishArgs(force_finish_children, override_status)) @staticmethod @@ -236,6 +242,7 @@ class FinishArgs(NamedTuple): def discover(item_id: TestModuleId, module_path: Optional[Path] = None): log.debug("Registered module %s", item_id) from ddtrace.internal.ci_visibility.recorder import on_discover_module + on_discover_module(TestModule.DiscoverArgs(item_id, module_path)) @staticmethod @@ -243,6 +250,7 @@ def discover(item_id: TestModuleId, module_path: Optional[Path] = None): def start(item_id: TestModuleId): log.debug("Starting module %s", item_id) from ddtrace.internal.ci_visibility.recorder import on_start_module + on_start_module(item_id) @staticmethod @@ -259,6 +267,7 @@ def finish( force_finish_children, ) from ddtrace.internal.ci_visibility.recorder import on_finish_module + on_finish_module(TestModule.FinishArgs(item_id, override_status, force_finish_children)) @@ -278,6 +287,7 @@ def discover( """Registers a test suite with the Test Visibility service.""" log.debug("Registering suite %s, source: %s", item_id, source_file_info) from ddtrace.internal.ci_visibility.recorder import on_discover_suite + on_discover_suite(TestSuite.DiscoverArgs(item_id, codeowners, source_file_info)) @staticmethod @@ -285,6 +295,7 @@ def discover( def start(item_id: TestSuiteId): log.debug("Starting suite %s", item_id) from ddtrace.internal.ci_visibility.recorder import on_start_suite + on_start_suite(item_id) class FinishArgs(NamedTuple): @@ -306,6 +317,7 @@ def finish( override_status, ) from ddtrace.internal.ci_visibility.recorder import on_finish_suite + on_finish_suite(TestSuite.FinishArgs(item_id, force_finish_children, override_status)) @@ -333,6 +345,7 @@ def discover( resource, ) from ddtrace.internal.ci_visibility.recorder import on_discover_test + on_discover_test(Test.DiscoverArgs(item_id, codeowners, source_file_info, resource)) @staticmethod @@ -340,6 +353,7 @@ def discover( def start(item_id: TestId): log.debug("Starting test %s", item_id) from ddtrace.internal.ci_visibility.recorder import on_start_test + on_start_test(item_id) class FinishArgs(NamedTuple): @@ -364,6 +378,7 @@ def finish( exc_info, ) from ddtrace.internal.ci_visibility.recorder import on_finish_test + on_finish_test(Test.FinishArgs(item_id, status, skip_reason=skip_reason, exc_info=exc_info)) @staticmethod @@ -371,6 +386,7 @@ def finish( def set_parameters(item_id: TestId, params: str): log.debug("Setting test %s parameters to %s", item_id, params) from ddtrace.internal.ci_visibility.recorder import on_set_test_parameters + on_set_test_parameters(item_id, params) @staticmethod diff --git a/ddtrace/internal/test_visibility/_atr_mixins.py b/ddtrace/internal/test_visibility/_atr_mixins.py index 83d85c3cacc..6b0f5cbfaf4 100644 --- a/ddtrace/internal/test_visibility/_atr_mixins.py +++ b/ddtrace/internal/test_visibility/_atr_mixins.py @@ -3,10 +3,10 @@ from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions import ddtrace.ext.test_visibility.api as ext_api -from ddtrace.internal import core from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId + log = get_logger(__name__) @@ -24,6 +24,7 @@ def atr_is_enabled() -> bool: log.debug("Checking if ATR is enabled") # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_atr_is_enabled + return on_atr_is_enabled() @staticmethod @@ -32,6 +33,7 @@ def atr_has_failed_tests() -> bool: log.debug("Checking if ATR session has failed tests") # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_atr_session_has_failed_tests + return on_atr_session_has_failed_tests() @@ -49,6 +51,7 @@ def atr_should_retry(test_id: InternalTestId) -> bool: log.debug("Checking if test %s should be retried by ATR", test_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_atr_should_retry_test + return on_atr_should_retry_test(test_id) @staticmethod @@ -56,6 +59,7 @@ def atr_should_retry(test_id: InternalTestId) -> bool: def atr_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_atr_add_retry + retry_number = on_atr_add_retry(test_id, start_immediately) log.debug("Adding ATR retry %s for test %s", retry_number, test_id) return retry_number @@ -66,6 +70,7 @@ def atr_start_retry(test_id: InternalTestId, retry_number: int) -> None: log.debug("Starting ATR retry %s for test %s", retry_number, test_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_atr_start_retry + on_atr_start_retry(test_id, retry_number) @staticmethod @@ -74,6 +79,7 @@ def atr_finish_retry(finish_args: "ATRTestMixin.ATRRetryFinishArgs") -> None: log.debug("Finishing ATR retry %s for test %s", finish_args.retry_number, finish_args.test_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_atr_finish_retry + on_atr_finish_retry(finish_args) @staticmethod @@ -82,4 +88,5 @@ def atr_get_final_status(test_id: InternalTestId) -> ext_api.TestStatus: log.debug("Getting ATR final status for test %s", test_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_atr_get_final_status + return on_atr_get_final_status(test_id) diff --git a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py index 83f7802ca90..c166793595a 100644 --- a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py +++ b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py @@ -2,10 +2,10 @@ from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions import ddtrace.ext.test_visibility.api as ext_api -from ddtrace.internal import core from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId + log = get_logger(__name__) @@ -16,6 +16,7 @@ def attempt_to_fix_has_failed_tests() -> bool: log.debug("Checking if attempt to fix session has failed tests") # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_attempt_to_fix_session_has_failed_tests + return on_attempt_to_fix_session_has_failed_tests() @@ -32,6 +33,7 @@ def attempt_to_fix_should_retry(test_id: InternalTestId) -> bool: log.debug("Checking if test %s should be retried by attempt to fix", test_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_attempt_to_fix_should_retry_test + return on_attempt_to_fix_should_retry_test(test_id) @staticmethod @@ -39,6 +41,7 @@ def attempt_to_fix_should_retry(test_id: InternalTestId) -> bool: def attempt_to_fix_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_attempt_to_fix_add_retry + retry_number = on_attempt_to_fix_add_retry(test_id, start_immediately) log.debug("Adding attempt to fix retry %s for test %s", retry_number, test_id) return retry_number @@ -49,6 +52,7 @@ def attempt_to_fix_start_retry(test_id: InternalTestId, retry_number: int) -> No log.debug("Starting attempt to fix retry %s for test %s", retry_number, test_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_attempt_to_fix_start_retry + on_attempt_to_fix_start_retry(test_id, retry_number) @staticmethod @@ -57,6 +61,7 @@ def attempt_to_fix_finish_retry(finish_args: "AttemptToFixTestMixin.AttemptToFix log.debug("Finishing attempt to fix retry %s for test %s", finish_args.retry_number, finish_args.test_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_attempt_to_fix_finish_retry + on_attempt_to_fix_finish_retry(finish_args) @staticmethod @@ -65,4 +70,5 @@ def attempt_to_fix_get_final_status(test_id: InternalTestId) -> ext_api.TestStat log.debug("Getting attempt to fix final status for test %s", test_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_attempt_to_fix_get_final_status + return on_attempt_to_fix_get_final_status(test_id) diff --git a/ddtrace/internal/test_visibility/_efd_mixins.py b/ddtrace/internal/test_visibility/_efd_mixins.py index e0224f0e39f..b6daf224aa5 100644 --- a/ddtrace/internal/test_visibility/_efd_mixins.py +++ b/ddtrace/internal/test_visibility/_efd_mixins.py @@ -3,7 +3,6 @@ from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions import ddtrace.ext.test_visibility.api as ext_api -from ddtrace.internal import core from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId @@ -25,6 +24,7 @@ def efd_enabled() -> bool: log.debug("Checking if EFD is enabled") # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_efd_is_enabled + return on_efd_is_enabled() @staticmethod @@ -33,6 +33,7 @@ def efd_is_faulty_session() -> bool: log.debug("Checking if EFD session is faulty") # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_efd_session_is_faulty + return on_efd_session_is_faulty() @staticmethod @@ -41,6 +42,7 @@ def efd_has_failed_tests() -> bool: log.debug("Checking if EFD session has failed tests") # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_efd_session_has_efd_failed_tests + return on_efd_session_has_efd_failed_tests() @@ -56,6 +58,7 @@ class EFDRetryFinishArgs(t.NamedTuple): def efd_should_retry(test_id: InternalTestId) -> bool: # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_efd_should_retry_test + log.debug("Checking if test %s should be retried by EFD", test_id) return on_efd_should_retry_test(test_id) @@ -64,6 +67,7 @@ def efd_should_retry(test_id: InternalTestId) -> bool: def efd_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_efd_add_retry + retry_number = on_efd_add_retry(test_id, start_immediately) log.debug("Adding EFD retry %s for test %s", retry_number, test_id) return retry_number @@ -73,6 +77,7 @@ def efd_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t def efd_start_retry(test_id: InternalTestId, retry_number: int) -> None: # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_efd_start_retry + log.debug("Starting EFD retry %s for test %s", retry_number, test_id) on_efd_start_retry(test_id, retry_number) @@ -81,6 +86,7 @@ def efd_start_retry(test_id: InternalTestId, retry_number: int) -> None: def efd_finish_retry(finish_args: "EFDTestMixin.EFDRetryFinishArgs") -> None: # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_efd_finish_retry + log.debug("Finishing EFD retry %s for test %s", finish_args.retry_number, finish_args.test_id) on_efd_finish_retry(finish_args) @@ -89,5 +95,6 @@ def efd_finish_retry(finish_args: "EFDTestMixin.EFDRetryFinishArgs") -> None: def efd_get_final_status(test_id: InternalTestId) -> EFDTestStatus: # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_efd_get_final_status + log.debug("Getting EFD final status for test %s", test_id) return on_efd_get_final_status(test_id) diff --git a/ddtrace/internal/test_visibility/_itr_mixins.py b/ddtrace/internal/test_visibility/_itr_mixins.py index 5e9764c46a2..5cde301f30f 100644 --- a/ddtrace/internal/test_visibility/_itr_mixins.py +++ b/ddtrace/internal/test_visibility/_itr_mixins.py @@ -3,11 +3,11 @@ from ddtrace.ext.test_visibility import api as ext_api from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions -from ddtrace.internal import core from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId from ddtrace.internal.test_visibility.coverage_lines import CoverageLines + log = get_logger(__name__) @@ -24,6 +24,7 @@ def mark_itr_skipped(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): log.debug("Marking item %s as skipped by ITR", item_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_itr_finish_item_skipped + on_itr_finish_item_skipped(item_id) @staticmethod @@ -32,6 +33,7 @@ def mark_itr_unskippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): log.debug("Marking item %s as unskippable by ITR", item_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_itr_mark_unskippable + on_itr_mark_unskippable(item_id) @staticmethod @@ -40,6 +42,7 @@ def mark_itr_forced_run(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): log.debug("Marking item %s as forced run by ITR", item_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_itr_mark_forced_run + on_itr_mark_forced_run(item_id) @staticmethod @@ -48,6 +51,7 @@ def was_itr_forced_run(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> log.debug("Checking if item %s was forced run by ITR", item_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_itr_was_forced_run + return on_itr_was_forced_run(item_id) @staticmethod @@ -56,6 +60,7 @@ def is_itr_skippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> b log.debug("Checking if item %s is skippable by ITR", item_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_itr_is_item_skippable + return on_itr_is_item_skippable(item_id) @staticmethod @@ -64,6 +69,7 @@ def is_itr_unskippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> log.debug("Checking if item %s is unskippable by ITR", item_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_itr_is_item_unskippable + return on_itr_is_item_unskippable(item_id) @staticmethod @@ -72,6 +78,7 @@ def was_itr_skipped(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bo log.debug("Checking if item %s was skipped by ITR", item_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_itr_was_item_skipped + return on_itr_was_item_skipped(item_id) @staticmethod @@ -80,12 +87,16 @@ def add_coverage_data(add_coverage_args: "ITRMixin.AddCoverageArgs") -> None: log.debug("Adding coverage data for item %s", add_coverage_args.item_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_add_coverage_data + on_add_coverage_data(add_coverage_args) @staticmethod @_catch_and_log_exceptions - def get_coverage_data(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> t.Optional[t.Dict[Path, CoverageLines]]: + def get_coverage_data( + item_id: t.Union[ext_api.TestSuiteId, InternalTestId] + ) -> t.Optional[t.Dict[Path, CoverageLines]]: log.debug("Getting coverage data for item %s", item_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_get_coverage_data + return on_get_coverage_data(item_id) diff --git a/ddtrace/internal/test_visibility/_utils.py b/ddtrace/internal/test_visibility/_utils.py index db64b9646ce..b56fd9a39dc 100644 --- a/ddtrace/internal/test_visibility/_utils.py +++ b/ddtrace/internal/test_visibility/_utils.py @@ -2,6 +2,7 @@ from ddtrace.internal.logger import get_logger from ddtrace.trace import Span + log = get_logger(__name__) @@ -9,4 +10,5 @@ def _get_item_span(item_id: TestVisibilityItemId) -> Span: log.debug("Getting span for item %s", item_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_item_get_span + return on_item_get_span(item_id) diff --git a/ddtrace/internal/test_visibility/api.py b/ddtrace/internal/test_visibility/api.py index 21fca81d097..61d27fd5d71 100644 --- a/ddtrace/internal/test_visibility/api.py +++ b/ddtrace/internal/test_visibility/api.py @@ -5,7 +5,6 @@ from ddtrace.ext.test_visibility import api as ext_api from ddtrace.ext.test_visibility._test_visibility_base import TestSessionId from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions -from ddtrace.internal.test_visibility._utils import _get_item_span from ddtrace.ext.test_visibility._utils import _is_item_finished from ddtrace.ext.test_visibility.api import TestExcInfo from ddtrace.ext.test_visibility.api import TestStatus @@ -21,9 +20,11 @@ from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId from ddtrace.internal.test_visibility._itr_mixins import ITRMixin from ddtrace.internal.test_visibility._library_capabilities import LibraryCapabilities +from ddtrace.internal.test_visibility._utils import _get_item_span from ddtrace.trace import Span from ddtrace.trace import Tracer + log = get_logger(__name__) @@ -39,6 +40,7 @@ def stash_set(item_id, key: str, value: object): log.debug("Stashing value %s for key %s in item %s", value, key, item_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_item_stash_set + on_item_stash_set(item_id, key, value) @staticmethod @@ -47,6 +49,7 @@ def stash_get(item_id: ext_api.TestVisibilityItemId, key: str): log.debug("Getting stashed value for key %s in item %s", key, item_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_item_stash_get + stash_value = on_item_stash_get(item_id, key) log.debug("Got stashed value %s for key %s in item %s", stash_value, key, item_id) return stash_value @@ -57,6 +60,7 @@ def stash_delete(item_id: ext_api.TestVisibilityItemId, key: str): log.debug("Deleting stashed value for key %s in item %s", key, item_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_item_stash_delete + on_item_stash_delete(item_id, key) @@ -75,6 +79,7 @@ def get_codeowners() -> t.Optional[_Codeowners]: log.debug("Getting codeowners object") # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_session_get_codeowners + codeowners: t.Optional[_Codeowners] = on_session_get_codeowners() return codeowners @@ -84,6 +89,7 @@ def get_tracer() -> t.Optional[Tracer]: log.debug("Getting test session tracer") # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_session_get_tracer + tracer: t.Optional[Tracer] = on_session_get_tracer() log.debug("Got test session tracer: %s", tracer) return tracer @@ -94,6 +100,7 @@ def get_workspace_path() -> t.Optional[Path]: log.debug("Getting session workspace path") # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_session_get_workspace_path + workspace_path: Path = on_session_get_workspace_path() return workspace_path @@ -103,6 +110,7 @@ def should_collect_coverage() -> bool: log.debug("Checking if coverage should be collected for session") # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_session_should_collect_coverage + _should_collect_coverage = bool(on_session_should_collect_coverage()) log.debug("Coverage should be collected: %s", _should_collect_coverage) return _should_collect_coverage @@ -113,6 +121,7 @@ def is_test_skipping_enabled() -> bool: log.debug("Checking if test skipping is enabled") # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_session_is_test_skipping_enabled + _is_test_skipping_enabled = bool(on_session_is_test_skipping_enabled()) log.debug("Test skipping is enabled: %s", _is_test_skipping_enabled) return _is_test_skipping_enabled @@ -123,6 +132,7 @@ def set_covered_lines_pct(coverage_pct: float): log.debug("Setting covered lines percentage for session to %s", coverage_pct) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_session_set_covered_lines_pct + on_session_set_covered_lines_pct(coverage_pct) @staticmethod @@ -131,6 +141,7 @@ def get_path_codeowners(path: Path) -> t.Optional[t.List[str]]: log.debug("Getting codeowners object for path %s", path) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_session_get_path_codeowners + path_codeowners: t.Optional[t.List[str]] = on_session_get_path_codeowners(path) return path_codeowners @@ -139,6 +150,7 @@ def get_path_codeowners(path: Path) -> t.Optional[t.List[str]]: def set_library_capabilities(capabilities: LibraryCapabilities) -> None: # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_session_set_library_capabilities + on_session_set_library_capabilities(capabilities) @staticmethod @@ -147,6 +159,7 @@ def set_itr_tags(skipped_count: int): log.debug("Setting itr session tags: %d", skipped_count) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_session_set_itr_skipped_count + on_session_set_itr_skipped_count(skipped_count) @@ -181,8 +194,9 @@ def finish( ): log.debug("Finishing test with status: %s, reason: %s", status, reason) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_finish_test from ddtrace.ext.test_visibility.api import Test + from ddtrace.internal.ci_visibility.recorder import on_finish_test + on_finish_test(Test.FinishArgs(item_id, status, reason, exc_info)) @staticmethod @@ -191,6 +205,7 @@ def is_new_test(item_id: InternalTestId) -> bool: log.debug("Checking if test %s is new", item_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_is_new_test + is_new = bool(on_is_new_test(item_id)) log.debug("Test %s is new: %s", item_id, is_new) return is_new @@ -201,6 +216,7 @@ def is_quarantined_test(item_id: InternalTestId) -> bool: log.debug("Checking if test %s is quarantined", item_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_is_quarantined_test + is_quarantined = bool(on_is_quarantined_test(item_id)) log.debug("Test %s is quarantined: %s", item_id, is_quarantined) return is_quarantined @@ -211,6 +227,7 @@ def is_disabled_test(item_id: InternalTestId) -> bool: log.debug("Checking if test %s is disabled", item_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_is_disabled_test + is_disabled = bool(on_is_disabled_test(item_id)) log.debug("Test %s is disabled: %s", item_id, is_disabled) return is_disabled @@ -221,6 +238,7 @@ def is_attempt_to_fix(item_id: InternalTestId) -> bool: log.debug("Checking if test %s is attempt to fix", item_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_is_attempt_to_fix + is_attempt_to_fix = bool(on_is_attempt_to_fix(item_id)) log.debug("Test %s is attempt to fix: %s", item_id, is_attempt_to_fix) return is_attempt_to_fix @@ -244,6 +262,7 @@ def overwrite_attributes( log.debug("Overwriting attributes for test %s", item_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import on_test_overwrite_attributes + on_test_overwrite_attributes( InternalTest.OverwriteAttributesArgs(item_id, name, suite_name, parameters, codeowners) ) From 36c48a54afb534c67e57a5370aea3d3fdec9b33a Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Thu, 12 Jun 2025 15:04:06 +0200 Subject: [PATCH 17/58] refactor 2 --- ddtrace/ext/test_visibility/_utils.py | 44 +-- ddtrace/internal/ci_visibility/recorder.py | 331 ------------------ .../ci_visibility/telemetry/test_session.py | 20 -- .../internal/test_visibility/_atr_mixins.py | 35 +- .../test_visibility/_attempt_to_fix_mixins.py | 28 +- .../internal/test_visibility/_efd_mixins.py | 44 +-- .../internal/test_visibility/_itr_mixins.py | 61 +++- ddtrace/internal/test_visibility/_utils.py | 12 +- ddtrace/internal/test_visibility/api.py | 159 +++++---- 9 files changed, 211 insertions(+), 523 deletions(-) delete mode 100644 ddtrace/internal/ci_visibility/telemetry/test_session.py diff --git a/ddtrace/ext/test_visibility/_utils.py b/ddtrace/ext/test_visibility/_utils.py index f4ae14ccd5e..45091792afe 100644 --- a/ddtrace/ext/test_visibility/_utils.py +++ b/ddtrace/ext/test_visibility/_utils.py @@ -4,7 +4,6 @@ from typing import List from ddtrace.ext.test_visibility._test_visibility_base import TestVisibilityItemId -from ddtrace.ext.test_visibility._test_visibility_base import _TestVisibilityAPIBase from ddtrace.internal.logger import get_logger @@ -29,52 +28,43 @@ def wrapper(*args, **kwargs): return wrapper -def _get_item_tag(item_id: TestVisibilityItemId, tag_name: str) -> Any: - log.debug("Getting tag for item %s: %s", item_id, tag_name) +def _get_item_tag(item_id: TestVisibilityItemId, name: str) -> Any: # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_get_tag + from ddtrace.internal.ci_visibility.recorder import CIVisibility - tag_value = on_get_tag(_TestVisibilityAPIBase.GetTagArgs(item_id, tag_name)) - return tag_value + return CIVisibility.get_item_by_id(item_id).get_tag(name) -def _set_item_tag(item_id: TestVisibilityItemId, tag_name: str, tag_value: Any, recurse: bool = False): - log.debug("Setting tag for item %s: %s=%s", item_id, tag_name, tag_value) +def _set_item_tag(item_id: TestVisibilityItemId, name: str, value: Any, recurse: bool = False) -> None: # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_set_tag + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_set_tag(_TestVisibilityAPIBase.SetTagArgs(item_id, tag_name, tag_value)) + CIVisibility.get_item_by_id(item_id).set_tag(name, value) -def _set_item_tags(item_id: TestVisibilityItemId, tags: Dict[str, Any], recurse: bool = False): - log.debug("Setting tags for item %s: %s", item_id, tags) +def _set_item_tags(item_id: TestVisibilityItemId, tags: Dict[str, Any], recurse: bool = False) -> None: # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_set_tags + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_set_tags(_TestVisibilityAPIBase.SetTagsArgs(item_id, tags)) + CIVisibility.get_item_by_id(item_id).set_tags(tags) -def _delete_item_tag(item_id: TestVisibilityItemId, tag_name: str, recurse: bool = False): - log.debug("Deleting tag for item %s: %s", item_id, tag_name) +def _delete_item_tag(item_id: TestVisibilityItemId, name: str, recurse: bool = False) -> None: # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_delete_tag + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_delete_tag(_TestVisibilityAPIBase.DeleteTagArgs(item_id, tag_name)) + CIVisibility.get_item_by_id(item_id).delete_tag(name) -def _delete_item_tags(item_id: TestVisibilityItemId, tag_names: List[str], recurse: bool = False): - log.debug("Deleting tags for item %s: %s", item_id, tag_names) +def _delete_item_tags(item_id: TestVisibilityItemId, names: List[str], recurse: bool = False) -> None: # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_delete_tags + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_delete_tags(_TestVisibilityAPIBase.DeleteTagsArgs(item_id, tag_names)) + CIVisibility.get_item_by_id(item_id).delete_tags(names) def _is_item_finished(item_id: TestVisibilityItemId) -> bool: - log.debug("Checking if item %s is finished", item_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_item_is_finished + from ddtrace.internal.ci_visibility.recorder import CIVisibility - _is_finished = bool(on_item_is_finished(item_id)) - log.debug("Item %s is finished: %s", item_id, _is_finished) - return _is_finished + return CIVisibility.get_item_by_id(item_id).is_finished() diff --git a/ddtrace/internal/ci_visibility/recorder.py b/ddtrace/internal/ci_visibility/recorder.py index a2394013602..520d76c786e 100644 --- a/ddtrace/internal/ci_visibility/recorder.py +++ b/ddtrace/internal/ci_visibility/recorder.py @@ -6,7 +6,6 @@ from typing import Any from typing import Callable from typing import Dict -from typing import List from typing import Optional from typing import Set from typing import Union @@ -22,7 +21,6 @@ from ddtrace.ext.test_visibility._test_visibility_base import TestSessionId from ddtrace.ext.test_visibility._test_visibility_base import TestVisibilityItemId from ddtrace.ext.test_visibility.api import Test -from ddtrace.ext.test_visibility.api import TestBase from ddtrace.ext.test_visibility.api import TestId from ddtrace.ext.test_visibility.api import TestModule from ddtrace.ext.test_visibility.api import TestModuleId @@ -73,21 +71,15 @@ from ddtrace.internal.codeowners import Codeowners from ddtrace.internal.logger import get_logger from ddtrace.internal.service import Service -from ddtrace.internal.test_visibility._atr_mixins import ATRTestMixin from ddtrace.internal.test_visibility._atr_mixins import AutoTestRetriesSettings from ddtrace.internal.test_visibility._attempt_to_fix_mixins import AttemptToFixTestMixin from ddtrace.internal.test_visibility._benchmark_mixin import BenchmarkTestMixin -from ddtrace.internal.test_visibility._efd_mixins import EFDTestMixin -from ddtrace.internal.test_visibility._efd_mixins import EFDTestStatus from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId -from ddtrace.internal.test_visibility._itr_mixins import ITRMixin from ddtrace.internal.test_visibility._library_capabilities import LibraryCapabilities from ddtrace.internal.test_visibility.api import InternalTest -from ddtrace.internal.test_visibility.coverage_lines import CoverageLines from ddtrace.internal.utils.formats import asbool from ddtrace.settings import IntegrationConfig from ddtrace.settings._agent import config as agent_config -from ddtrace.trace import Span from ddtrace.trace import Tracer @@ -1117,76 +1109,6 @@ def on_finish_session(finish_args: TestSession.FinishArgs) -> None: session.finish(finish_args.force_finish_children, finish_args.override_status) -@_requires_civisibility_enabled -def on_session_is_test_skipping_enabled() -> bool: - log.debug("Handling is test skipping enabled") - return CIVisibility.test_skipping_enabled() - - -@_requires_civisibility_enabled -def on_session_get_workspace_path() -> Optional[Path]: - log.debug("Handling finish for session id %s") - path_str = CIVisibility.get_workspace_path() - return Path(path_str) if path_str is not None else None - - -@_requires_civisibility_enabled -def on_session_should_collect_coverage() -> bool: - log.debug("Handling should collect coverage") - return CIVisibility.should_collect_coverage() - - -@_requires_civisibility_enabled -def on_session_get_codeowners() -> Optional[Codeowners]: - log.debug("Getting codeowners") - return CIVisibility.get_codeowners() - - -@_requires_civisibility_enabled -def on_session_get_tracer() -> Optional[Tracer]: - log.debug("Getting tracer") - return CIVisibility.get_tracer() - - -@_requires_civisibility_enabled -def on_session_is_atr_enabled() -> bool: - log.debug("Getting Auto Test Retries enabled") - return CIVisibility.is_atr_enabled() - - -@_requires_civisibility_enabled -def on_session_is_efd_enabled() -> bool: - log.debug("Getting Early Flake Detection enabled") - return CIVisibility.is_efd_enabled() - - -@_requires_civisibility_enabled -def on_session_set_covered_lines_pct(coverage_pct) -> None: - log.debug("Setting coverage percentage for session to %s", coverage_pct) - CIVisibility.get_session().set_covered_lines_pct(coverage_pct) - - -@_requires_civisibility_enabled -def on_session_set_library_capabilities(capabilities: LibraryCapabilities) -> None: - log.debug("Setting library capabilities") - CIVisibility.set_library_capabilities(capabilities) - - -@_requires_civisibility_enabled -def on_session_set_itr_skipped_count(skipped_count: int) -> None: - log.debug("Setting skipped count: %d", skipped_count) - CIVisibility.get_session().set_skipped_count(skipped_count) - - -@_requires_civisibility_enabled -def on_session_get_path_codeowners(path: Path) -> Optional[List[str]]: - log.debug("Getting codeowners for path %s", path) - codeowners = CIVisibility.get_codeowners() - if codeowners is None: - return None - return codeowners.of(str(path)) - - @_requires_civisibility_enabled def on_discover_module(discover_args: TestModule.DiscoverArgs) -> None: log.debug("Handling discovery for module %s", discover_args.module_id) @@ -1281,30 +1203,6 @@ def on_discover_test(discover_args: Test.DiscoverArgs) -> None: ) -@_requires_civisibility_enabled -def on_is_new_test(test_id: Union[TestId, InternalTestId]) -> bool: - log.debug("Handling is new test for test %s", test_id) - return CIVisibility.get_test_by_id(test_id).is_new() - - -@_requires_civisibility_enabled -def on_is_quarantined_test(test_id: Union[TestId, InternalTestId]) -> bool: - log.debug("Handling is quarantined test for test %s", test_id) - return CIVisibility.get_test_by_id(test_id).is_quarantined() - - -@_requires_civisibility_enabled -def on_is_disabled_test(test_id: Union[TestId, InternalTestId]) -> bool: - log.debug("Handling is disabled test for test %s", test_id) - return CIVisibility.get_test_by_id(test_id).is_disabled() - - -@_requires_civisibility_enabled -def on_is_attempt_to_fix(test_id: Union[TestId, InternalTestId]) -> bool: - log.debug("Handling is attempt to fix for test %s", test_id) - return CIVisibility.get_test_by_id(test_id).is_attempt_to_fix() - - @_requires_civisibility_enabled def on_start_test(test_id: TestId) -> None: log.debug("Handling start for test id %s", test_id) @@ -1346,235 +1244,6 @@ def on_test_overwrite_attributes(overwrite_attribute_args: InternalTest.Overwrit CIVisibility.get_test_by_id(item_id).overwrite_attributes(name, suite_name, parameters, codeowners) -@_requires_civisibility_enabled -def on_item_get_span(item_id: TestVisibilityItemId) -> Optional[Span]: - log.debug("Handing get_span for item %s", item_id) - item = CIVisibility.get_item_by_id(item_id) - return item.get_span() - - -@_requires_civisibility_enabled -def on_item_is_finished(item_id: TestVisibilityItemId) -> bool: - log.debug("Handling is finished for item %s", item_id) - return CIVisibility.get_item_by_id(item_id).is_finished() - - -@_requires_civisibility_enabled -def on_item_stash_set(item_id: TestVisibilityItemId, key: str, value: object) -> None: - log.debug("Handling stash set for item %s, key %s, value %s", item_id, key, value) - CIVisibility.get_item_by_id(item_id).stash_set(key, value) - - -@_requires_civisibility_enabled -def on_item_stash_get(item_id: TestVisibilityItemId, key: str) -> Optional[object]: - log.debug("Handling stash get for item %s, key %s", item_id, key) - return CIVisibility.get_item_by_id(item_id).stash_get(key) - - -@_requires_civisibility_enabled -def on_item_stash_delete(item_id: TestVisibilityItemId, key: str) -> None: - log.debug("Handling stash delete for item %s, key %s", item_id, key) - CIVisibility.get_item_by_id(item_id).stash_delete(key) - - -@_requires_civisibility_enabled -def on_get_coverage_data(item_id: Union[TestSuiteId, TestId]) -> Optional[Dict[Path, CoverageLines]]: - log.debug("Handling get coverage data for item %s", item_id) - return CIVisibility.get_item_by_id(item_id).get_coverage_data() - - -@_requires_civisibility_enabled -def on_add_coverage_data(add_coverage_args: ITRMixin.AddCoverageArgs) -> None: - """Adds coverage data to an item, merging with existing coverage data if necessary""" - item_id = add_coverage_args.item_id - coverage_data = add_coverage_args.coverage_data - - log.debug("Handling add coverage data for item id %s", item_id) - - if not isinstance(item_id, (TestSuiteId, TestId)): - log.warning("Coverage data can only be added to suites and tests, not %s", type(item_id)) - return - - CIVisibility.get_item_by_id(item_id).add_coverage_data(coverage_data) - - -@_requires_civisibility_enabled -def on_get_tag(get_tag_args: TestBase.GetTagArgs) -> Any: - item_id = get_tag_args.item_id - key = get_tag_args.name - log.debug("Handling get tag for item id %s, key %s", item_id, key) - return CIVisibility.get_item_by_id(item_id).get_tag(key) - - -@_requires_civisibility_enabled -def on_set_tag(set_tag_args: TestBase.SetTagArgs) -> None: - item_id = set_tag_args.item_id - key = set_tag_args.name - value = set_tag_args.value - log.debug("Handling set tag for item id %s, key %s, value %s", item_id, key, value) - CIVisibility.get_item_by_id(item_id).set_tag(key, value) - - -@_requires_civisibility_enabled -def on_set_tags(set_tags_args: TestBase.SetTagsArgs) -> None: - item_id = set_tags_args.item_id - tags = set_tags_args.tags - log.debug("Handling set tags for item id %s, tags %s", item_id, tags) - CIVisibility.get_item_by_id(item_id).set_tags(tags) - - -@_requires_civisibility_enabled -def on_delete_tag(delete_tag_args: TestBase.DeleteTagArgs) -> None: - item_id = delete_tag_args.item_id - key = delete_tag_args.name - log.debug("Handling delete tag for item id %s, key %s", item_id, key) - CIVisibility.get_item_by_id(item_id).delete_tag(key) - - -@_requires_civisibility_enabled -def on_delete_tags(delete_tags_args: TestBase.DeleteTagsArgs) -> None: - item_id = delete_tags_args.item_id - keys = delete_tags_args.names - log.debug("Handling delete tags for item id %s, keys %s", item_id, keys) - CIVisibility.get_item_by_id(item_id).delete_tags(keys) - - -@_requires_civisibility_enabled -def on_itr_finish_item_skipped(item_id: Union[TestSuiteId, TestId]) -> None: - log.debug("Handling finish ITR skipped for item id %s", item_id) - if not isinstance(item_id, (TestSuiteId, TestId)): - log.warning("Only suites or tests can be skipped, not %s", type(item_id)) - return - CIVisibility.get_item_by_id(item_id).finish_itr_skipped() - - -@_requires_civisibility_enabled -def on_itr_mark_unskippable(item_id: Union[TestSuiteId, TestId]) -> None: - log.debug("Handling marking %s unskippable", item_id) - CIVisibility.get_item_by_id(item_id).mark_itr_unskippable() - - -@_requires_civisibility_enabled -def on_itr_mark_forced_run(item_id: Union[TestSuiteId, TestId]) -> None: - log.debug("Handling marking %s as forced run", item_id) - CIVisibility.get_item_by_id(item_id).mark_itr_forced_run() - - -@_requires_civisibility_enabled -def on_itr_was_forced_run(item_id: TestVisibilityItemId) -> bool: - log.debug("Handling marking %s as forced run", item_id) - return CIVisibility.get_item_by_id(item_id).was_itr_forced_run() - - -@_requires_civisibility_enabled -def on_itr_is_item_skippable(item_id: Union[TestSuiteId, TestId]) -> bool: - """Skippable items are fetched as part CIVisibility.enable(), so they are assumed to be available.""" - log.debug("Handling is item skippable for item id %s", item_id) - - if not isinstance(item_id, (TestSuiteId, TestId)): - log.warning("Only suites or tests can be skippable, not %s", type(item_id)) - return False - - if not CIVisibility.test_skipping_enabled(): - log.debug("Test skipping is not enabled") - return False - - return CIVisibility.is_item_itr_skippable(item_id) - - -@_requires_civisibility_enabled -def on_itr_is_item_unskippable(item_id: Union[TestSuiteId, TestId]) -> bool: - log.debug("Handling is item unskippable for %s", item_id) - if not isinstance(item_id, (TestSuiteId, TestId)): - raise CIVisibilityError("Only suites or tests can be unskippable") - return CIVisibility.get_item_by_id(item_id).is_itr_unskippable() - - -@_requires_civisibility_enabled -def on_itr_was_item_skipped(item_id: Union[TestSuiteId, TestId]) -> bool: - log.debug("Handling was item skipped for %s", item_id) - return CIVisibility.get_item_by_id(item_id).is_itr_skipped() - - -@_requires_civisibility_enabled -def on_efd_is_enabled() -> bool: - return CIVisibility.get_session().efd_is_enabled() - - -@_requires_civisibility_enabled -def on_efd_session_is_faulty() -> bool: - return CIVisibility.get_session().efd_is_faulty_session() - - -@_requires_civisibility_enabled -def on_efd_session_has_efd_failed_tests() -> bool: - return CIVisibility.get_session().efd_has_failed_tests() - - -@_requires_civisibility_enabled -def on_efd_should_retry_test(test_id: InternalTestId) -> bool: - return CIVisibility.get_test_by_id(test_id).efd_should_retry() - - -@_requires_civisibility_enabled -def on_efd_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> Optional[int]: - return CIVisibility.get_test_by_id(test_id).efd_add_retry(start_immediately) - - -@_requires_civisibility_enabled -def on_efd_start_retry(test_id: InternalTestId, retry_number: int) -> None: - CIVisibility.get_test_by_id(test_id).efd_start_retry(retry_number) - - -@_requires_civisibility_enabled -def on_efd_finish_retry(efd_finish_args: EFDTestMixin.EFDRetryFinishArgs) -> None: - CIVisibility.get_test_by_id(efd_finish_args.test_id).efd_finish_retry( - efd_finish_args.retry_number, efd_finish_args.status, efd_finish_args.exc_info - ) - - -@_requires_civisibility_enabled -def on_efd_get_final_status(test_id: InternalTestId) -> EFDTestStatus: - return CIVisibility.get_test_by_id(test_id).efd_get_final_status() - - -@_requires_civisibility_enabled -def on_atr_is_enabled() -> bool: - return CIVisibility.is_atr_enabled() - - -@_requires_civisibility_enabled -def on_atr_session_has_failed_tests() -> bool: - return CIVisibility.get_session().atr_has_failed_tests() - - -@_requires_civisibility_enabled -def on_atr_should_retry_test(item_id: InternalTestId) -> bool: - return CIVisibility.get_test_by_id(item_id).atr_should_retry() - - -@_requires_civisibility_enabled -def on_atr_add_retry(item_id: InternalTestId, start_immediately: bool = False) -> Optional[int]: - return CIVisibility.get_test_by_id(item_id).atr_add_retry(start_immediately) - - -@_requires_civisibility_enabled -def on_atr_start_retry(test_id: InternalTestId, retry_number: int) -> None: - CIVisibility.get_test_by_id(test_id).atr_start_retry(retry_number) - - -@_requires_civisibility_enabled -def on_atr_finish_retry(atr_finish_args: ATRTestMixin.ATRRetryFinishArgs) -> None: - CIVisibility.get_test_by_id(atr_finish_args.test_id).atr_finish_retry( - atr_finish_args.retry_number, atr_finish_args.status, atr_finish_args.exc_info - ) - - -@_requires_civisibility_enabled -def on_atr_get_final_status(test_id: InternalTestId) -> TestStatus: - return CIVisibility.get_test_by_id(test_id).atr_get_final_status() - - @_requires_civisibility_enabled def on_attempt_to_fix_should_retry_test(item_id: InternalTestId) -> bool: return CIVisibility.get_test_by_id(item_id).attempt_to_fix_should_retry() diff --git a/ddtrace/internal/ci_visibility/telemetry/test_session.py b/ddtrace/internal/ci_visibility/telemetry/test_session.py deleted file mode 100644 index 051be2c7001..00000000000 --- a/ddtrace/internal/ci_visibility/telemetry/test_session.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 -from enum import Enum - -from ddtrace.internal.logger import get_logger -from ddtrace.internal.telemetry import telemetry_writer -from ddtrace.internal.telemetry.constants import TELEMETRY_NAMESPACE - - -log = get_logger(__name__) - - -class TEST_SESSION_TELEMETRY(str, Enum): - COLLECTED = "total_tests_collected" - - -def record_num_tests_discovered(count_tests: int): - log.debug("Recording session collected items telemetry: %s", count_tests) - telemetry_writer.add_distribution_metric( - TELEMETRY_NAMESPACE.CIVISIBILITY, TEST_SESSION_TELEMETRY.COLLECTED, count_tests - ) diff --git a/ddtrace/internal/test_visibility/_atr_mixins.py b/ddtrace/internal/test_visibility/_atr_mixins.py index 6b0f5cbfaf4..1034cfdd95f 100644 --- a/ddtrace/internal/test_visibility/_atr_mixins.py +++ b/ddtrace/internal/test_visibility/_atr_mixins.py @@ -23,44 +23,43 @@ class ATRSessionMixin: def atr_is_enabled() -> bool: log.debug("Checking if ATR is enabled") # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_atr_is_enabled + from ddtrace.internal.ci_visibility.recorder import CIVisibility - return on_atr_is_enabled() + return CIVisibility.is_atr_enabled() @staticmethod @_catch_and_log_exceptions def atr_has_failed_tests() -> bool: log.debug("Checking if ATR session has failed tests") # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_atr_session_has_failed_tests + from ddtrace.internal.ci_visibility.recorder import CIVisibility - return on_atr_session_has_failed_tests() + return CIVisibility.get_session().atr_has_failed_tests() class ATRTestMixin: - @dataclasses.dataclass - class ATRRetryFinishArgs: + class ATRRetryFinishArgs(t.NamedTuple): test_id: InternalTestId retry_number: int status: ext_api.TestStatus - exc_info: t.Optional[t.Tuple[t.Type[BaseException], BaseException, t.Any]] + exc_info: t.Optional[ext_api.TestExcInfo] @staticmethod @_catch_and_log_exceptions def atr_should_retry(test_id: InternalTestId) -> bool: log.debug("Checking if test %s should be retried by ATR", test_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_atr_should_retry_test + from ddtrace.internal.ci_visibility.recorder import CIVisibility - return on_atr_should_retry_test(test_id) + return CIVisibility.get_test_by_id(test_id).atr_should_retry() @staticmethod @_catch_and_log_exceptions def atr_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_atr_add_retry + from ddtrace.internal.ci_visibility.recorder import CIVisibility - retry_number = on_atr_add_retry(test_id, start_immediately) + retry_number = CIVisibility.get_test_by_id(test_id).atr_add_retry(start_immediately) log.debug("Adding ATR retry %s for test %s", retry_number, test_id) return retry_number @@ -69,24 +68,26 @@ def atr_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t def atr_start_retry(test_id: InternalTestId, retry_number: int) -> None: log.debug("Starting ATR retry %s for test %s", retry_number, test_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_atr_start_retry + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_atr_start_retry(test_id, retry_number) + CIVisibility.get_test_by_id(test_id).atr_start_retry(retry_number) @staticmethod @_catch_and_log_exceptions def atr_finish_retry(finish_args: "ATRTestMixin.ATRRetryFinishArgs") -> None: log.debug("Finishing ATR retry %s for test %s", finish_args.retry_number, finish_args.test_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_atr_finish_retry + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_atr_finish_retry(finish_args) + CIVisibility.get_test_by_id(finish_args.test_id).atr_finish_retry( + finish_args.retry_number, finish_args.status, finish_args.exc_info + ) @staticmethod @_catch_and_log_exceptions def atr_get_final_status(test_id: InternalTestId) -> ext_api.TestStatus: log.debug("Getting ATR final status for test %s", test_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_atr_get_final_status + from ddtrace.internal.ci_visibility.recorder import CIVisibility - return on_atr_get_final_status(test_id) + return CIVisibility.get_test_by_id(test_id).atr_get_final_status() diff --git a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py index c166793595a..e727436ac88 100644 --- a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py +++ b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py @@ -15,9 +15,9 @@ class AttemptToFixSessionMixin: def attempt_to_fix_has_failed_tests() -> bool: log.debug("Checking if attempt to fix session has failed tests") # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_attempt_to_fix_session_has_failed_tests + from ddtrace.internal.ci_visibility.recorder import CIVisibility - return on_attempt_to_fix_session_has_failed_tests() + return CIVisibility.get_session().attempt_to_fix_has_failed_tests() class AttemptToFixTestMixin: @@ -25,24 +25,24 @@ class AttemptToFixRetryFinishArgs(t.NamedTuple): test_id: InternalTestId retry_number: int status: ext_api.TestStatus - exc_info: t.Optional[t.Tuple[t.Type[BaseException], BaseException, t.Any]] + exc_info: t.Optional[ext_api.TestExcInfo] @staticmethod @_catch_and_log_exceptions def attempt_to_fix_should_retry(test_id: InternalTestId) -> bool: log.debug("Checking if test %s should be retried by attempt to fix", test_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_attempt_to_fix_should_retry_test + from ddtrace.internal.ci_visibility.recorder import CIVisibility - return on_attempt_to_fix_should_retry_test(test_id) + return CIVisibility.get_test_by_id(test_id).attempt_to_fix_should_retry() @staticmethod @_catch_and_log_exceptions def attempt_to_fix_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_attempt_to_fix_add_retry + from ddtrace.internal.ci_visibility.recorder import CIVisibility - retry_number = on_attempt_to_fix_add_retry(test_id, start_immediately) + retry_number = CIVisibility.get_test_by_id(test_id).attempt_to_fix_add_retry(start_immediately) log.debug("Adding attempt to fix retry %s for test %s", retry_number, test_id) return retry_number @@ -51,24 +51,26 @@ def attempt_to_fix_add_retry(test_id: InternalTestId, start_immediately: bool = def attempt_to_fix_start_retry(test_id: InternalTestId, retry_number: int) -> None: log.debug("Starting attempt to fix retry %s for test %s", retry_number, test_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_attempt_to_fix_start_retry + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_attempt_to_fix_start_retry(test_id, retry_number) + CIVisibility.get_test_by_id(test_id).attempt_to_fix_start_retry(retry_number) @staticmethod @_catch_and_log_exceptions def attempt_to_fix_finish_retry(finish_args: "AttemptToFixTestMixin.AttemptToFixRetryFinishArgs") -> None: log.debug("Finishing attempt to fix retry %s for test %s", finish_args.retry_number, finish_args.test_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_attempt_to_fix_finish_retry + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_attempt_to_fix_finish_retry(finish_args) + CIVisibility.get_test_by_id(finish_args.test_id).attempt_to_fix_finish_retry( + finish_args.retry_number, finish_args.status, finish_args.exc_info + ) @staticmethod @_catch_and_log_exceptions def attempt_to_fix_get_final_status(test_id: InternalTestId) -> ext_api.TestStatus: log.debug("Getting attempt to fix final status for test %s", test_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_attempt_to_fix_get_final_status + from ddtrace.internal.ci_visibility.recorder import CIVisibility - return on_attempt_to_fix_get_final_status(test_id) + return CIVisibility.get_test_by_id(test_id).attempt_to_fix_get_final_status() diff --git a/ddtrace/internal/test_visibility/_efd_mixins.py b/ddtrace/internal/test_visibility/_efd_mixins.py index b6daf224aa5..ab4fb641f7a 100644 --- a/ddtrace/internal/test_visibility/_efd_mixins.py +++ b/ddtrace/internal/test_visibility/_efd_mixins.py @@ -23,27 +23,27 @@ class EFDSessionMixin: def efd_enabled() -> bool: log.debug("Checking if EFD is enabled") # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_efd_is_enabled + from ddtrace.internal.ci_visibility.recorder import CIVisibility - return on_efd_is_enabled() + return CIVisibility.get_session().efd_is_enabled() @staticmethod @_catch_and_log_exceptions def efd_is_faulty_session() -> bool: log.debug("Checking if EFD session is faulty") # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_efd_session_is_faulty + from ddtrace.internal.ci_visibility.recorder import CIVisibility - return on_efd_session_is_faulty() + return CIVisibility.get_session().efd_is_faulty_session() @staticmethod @_catch_and_log_exceptions def efd_has_failed_tests() -> bool: log.debug("Checking if EFD session has failed tests") # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_efd_session_has_efd_failed_tests + from ddtrace.internal.ci_visibility.recorder import CIVisibility - return on_efd_session_has_efd_failed_tests() + return CIVisibility.get_session().efd_has_failed_tests() class EFDTestMixin: @@ -51,50 +51,52 @@ class EFDRetryFinishArgs(t.NamedTuple): test_id: InternalTestId retry_number: int status: ext_api.TestStatus - exc_info: t.Optional[t.Tuple[t.Type[BaseException], BaseException, t.Any]] + exc_info: t.Optional[ext_api.TestExcInfo] @staticmethod @_catch_and_log_exceptions def efd_should_retry(test_id: InternalTestId) -> bool: + log.debug("Checking if test %s should be retried by EFD", test_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_efd_should_retry_test + from ddtrace.internal.ci_visibility.recorder import CIVisibility - log.debug("Checking if test %s should be retried by EFD", test_id) - return on_efd_should_retry_test(test_id) + return CIVisibility.get_test_by_id(test_id).efd_should_retry() @staticmethod @_catch_and_log_exceptions def efd_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_efd_add_retry + from ddtrace.internal.ci_visibility.recorder import CIVisibility - retry_number = on_efd_add_retry(test_id, start_immediately) + retry_number = CIVisibility.get_test_by_id(test_id).efd_add_retry(start_immediately) log.debug("Adding EFD retry %s for test %s", retry_number, test_id) return retry_number @staticmethod @_catch_and_log_exceptions def efd_start_retry(test_id: InternalTestId, retry_number: int) -> None: + log.debug("Starting EFD retry %s for test %s", retry_number, test_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_efd_start_retry + from ddtrace.internal.ci_visibility.recorder import CIVisibility - log.debug("Starting EFD retry %s for test %s", retry_number, test_id) - on_efd_start_retry(test_id, retry_number) + CIVisibility.get_test_by_id(test_id).efd_start_retry(retry_number) @staticmethod @_catch_and_log_exceptions def efd_finish_retry(finish_args: "EFDTestMixin.EFDRetryFinishArgs") -> None: + log.debug("Finishing EFD retry %s for test %s", finish_args.retry_number, finish_args.test_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_efd_finish_retry + from ddtrace.internal.ci_visibility.recorder import CIVisibility - log.debug("Finishing EFD retry %s for test %s", finish_args.retry_number, finish_args.test_id) - on_efd_finish_retry(finish_args) + CIVisibility.get_test_by_id(finish_args.test_id).efd_finish_retry( + finish_args.retry_number, finish_args.status, finish_args.exc_info + ) @staticmethod @_catch_and_log_exceptions def efd_get_final_status(test_id: InternalTestId) -> EFDTestStatus: + log.debug("Getting EFD final status for test %s", test_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_efd_get_final_status + from ddtrace.internal.ci_visibility.recorder import CIVisibility - log.debug("Getting EFD final status for test %s", test_id) - return on_efd_get_final_status(test_id) + return CIVisibility.get_test_by_id(test_id).efd_get_final_status() diff --git a/ddtrace/internal/test_visibility/_itr_mixins.py b/ddtrace/internal/test_visibility/_itr_mixins.py index 5cde301f30f..9bd211f1d1e 100644 --- a/ddtrace/internal/test_visibility/_itr_mixins.py +++ b/ddtrace/internal/test_visibility/_itr_mixins.py @@ -3,6 +3,7 @@ from ddtrace.ext.test_visibility import api as ext_api from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions +from ddtrace.internal.ci_visibility.errors import CIVisibilityError from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId from ddtrace.internal.test_visibility.coverage_lines import CoverageLines @@ -23,72 +24,94 @@ class AddCoverageArgs(t.NamedTuple): def mark_itr_skipped(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): log.debug("Marking item %s as skipped by ITR", item_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_itr_finish_item_skipped + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_itr_finish_item_skipped(item_id) + if not isinstance(item_id, (ext_api.TestSuiteId, InternalTestId)): + log.warning("Only suites or tests can be skipped, not %s", type(item_id)) + return + CIVisibility.get_item_by_id(item_id).finish_itr_skipped() @staticmethod @_catch_and_log_exceptions def mark_itr_unskippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): log.debug("Marking item %s as unskippable by ITR", item_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_itr_mark_unskippable + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_itr_mark_unskippable(item_id) + CIVisibility.get_item_by_id(item_id).mark_itr_unskippable() @staticmethod @_catch_and_log_exceptions def mark_itr_forced_run(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): log.debug("Marking item %s as forced run by ITR", item_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_itr_mark_forced_run + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_itr_mark_forced_run(item_id) + CIVisibility.get_item_by_id(item_id).mark_itr_forced_run() @staticmethod @_catch_and_log_exceptions def was_itr_forced_run(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: log.debug("Checking if item %s was forced run by ITR", item_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_itr_was_forced_run + from ddtrace.internal.ci_visibility.recorder import CIVisibility - return on_itr_was_forced_run(item_id) + return CIVisibility.get_item_by_id(item_id).was_itr_forced_run() @staticmethod @_catch_and_log_exceptions def is_itr_skippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: log.debug("Checking if item %s is skippable by ITR", item_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_itr_is_item_skippable + from ddtrace.internal.ci_visibility.recorder import CIVisibility - return on_itr_is_item_skippable(item_id) + if not isinstance(item_id, (ext_api.TestSuiteId, InternalTestId)): + log.warning("Only suites or tests can be skippable, not %s", type(item_id)) + return False + + if not CIVisibility.test_skipping_enabled(): + log.debug("Test skipping is not enabled") + return False + + return CIVisibility.is_item_itr_skippable(item_id) @staticmethod @_catch_and_log_exceptions def is_itr_unskippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: log.debug("Checking if item %s is unskippable by ITR", item_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_itr_is_item_unskippable + from ddtrace.internal.ci_visibility.recorder import CIVisibility - return on_itr_is_item_unskippable(item_id) + if not isinstance(item_id, (ext_api.TestSuiteId, InternalTestId)): + raise CIVisibilityError("Only suites or tests can be unskippable") + return CIVisibility.get_item_by_id(item_id).is_itr_unskippable() @staticmethod @_catch_and_log_exceptions def was_itr_skipped(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: log.debug("Checking if item %s was skipped by ITR", item_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_itr_was_item_skipped + from ddtrace.internal.ci_visibility.recorder import CIVisibility - return on_itr_was_item_skipped(item_id) + return CIVisibility.get_item_by_id(item_id).is_itr_skipped() @staticmethod @_catch_and_log_exceptions def add_coverage_data(add_coverage_args: "ITRMixin.AddCoverageArgs") -> None: - log.debug("Adding coverage data for item %s", add_coverage_args.item_id) + """Adds coverage data to an item, merging with existing coverage data if necessary""" + item_id = add_coverage_args.item_id + coverage_data = add_coverage_args.coverage_data + + log.debug("Adding coverage data for item id %s", item_id) + + if not isinstance(item_id, (ext_api.TestSuiteId, InternalTestId)): + log.warning("Coverage data can only be added to suites and tests, not %s", type(item_id)) + return + # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_add_coverage_data + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_add_coverage_data(add_coverage_args) + CIVisibility.get_item_by_id(item_id).add_coverage_data(coverage_data) @staticmethod @_catch_and_log_exceptions @@ -97,6 +120,6 @@ def get_coverage_data( ) -> t.Optional[t.Dict[Path, CoverageLines]]: log.debug("Getting coverage data for item %s", item_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_get_coverage_data + from ddtrace.internal.ci_visibility.recorder import CIVisibility - return on_get_coverage_data(item_id) + return CIVisibility.get_item_by_id(item_id).get_coverage_data() diff --git a/ddtrace/internal/test_visibility/_utils.py b/ddtrace/internal/test_visibility/_utils.py index b56fd9a39dc..182957584ab 100644 --- a/ddtrace/internal/test_visibility/_utils.py +++ b/ddtrace/internal/test_visibility/_utils.py @@ -1,14 +1,16 @@ -from ddtrace.ext.test_visibility._test_visibility_base import TestVisibilityItemId +import typing as t + +from ddtrace.ext.test_visibility import api as ext_api from ddtrace.internal.logger import get_logger +from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId from ddtrace.trace import Span log = get_logger(__name__) -def _get_item_span(item_id: TestVisibilityItemId) -> Span: - log.debug("Getting span for item %s", item_id) +def _get_item_span(item_id: t.Union[ext_api.TestVisibilityItemId, InternalTestId]) -> Span: # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_item_get_span + from ddtrace.internal.ci_visibility.recorder import CIVisibility - return on_item_get_span(item_id) + return CIVisibility.get_item_by_id(item_id).get_span() diff --git a/ddtrace/internal/test_visibility/api.py b/ddtrace/internal/test_visibility/api.py index 61d27fd5d71..7e4e1aae997 100644 --- a/ddtrace/internal/test_visibility/api.py +++ b/ddtrace/internal/test_visibility/api.py @@ -39,29 +39,41 @@ def get_span(item_id: t.Union[ext_api.TestVisibilityItemId, InternalTestId]) -> def stash_set(item_id, key: str, value: object): log.debug("Stashing value %s for key %s in item %s", value, key, item_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_item_stash_set + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_item_stash_set(item_id, key, value) + CIVisibility.get_item_by_id(item_id).stash_set(key, value) @staticmethod @_catch_and_log_exceptions - def stash_get(item_id: ext_api.TestVisibilityItemId, key: str): + def stash_get(item_id: ext_api.TestVisibilityItemId, key: str) -> t.Optional[object]: log.debug("Getting stashed value for key %s in item %s", key, item_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_item_stash_get + from ddtrace.internal.ci_visibility.recorder import CIVisibility - stash_value = on_item_stash_get(item_id, key) - log.debug("Got stashed value %s for key %s in item %s", stash_value, key, item_id) - return stash_value + return CIVisibility.get_item_by_id(item_id).stash_get(key) @staticmethod @_catch_and_log_exceptions def stash_delete(item_id: ext_api.TestVisibilityItemId, key: str): log.debug("Deleting stashed value for key %s in item %s", key, item_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_item_stash_delete + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_item_stash_delete(item_id, key) + CIVisibility.get_item_by_id(item_id).stash_delete(key) + + @staticmethod + @_catch_and_log_exceptions + def overwrite_attributes(overwrite_attribute_args: "InternalTest.OverwriteAttributesArgs") -> None: + log.debug("Overwriting attributes: %s", overwrite_attribute_args) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import CIVisibility + + CIVisibility.get_test_by_id(overwrite_attribute_args.test_id).overwrite_attributes( + overwrite_attribute_args.name, + overwrite_attribute_args.suite_name, + overwrite_attribute_args.parameters, + overwrite_attribute_args.codeowners, + ) class InternalTestSession(ext_api.TestSession, EFDSessionMixin, ATRSessionMixin, AttemptToFixSessionMixin): @@ -76,91 +88,87 @@ def is_finished() -> bool: @staticmethod @_catch_and_log_exceptions def get_codeowners() -> t.Optional[_Codeowners]: - log.debug("Getting codeowners object") + log.debug("Getting codeowners") # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_session_get_codeowners + from ddtrace.internal.ci_visibility.recorder import CIVisibility - codeowners: t.Optional[_Codeowners] = on_session_get_codeowners() - return codeowners + return CIVisibility.get_codeowners() @staticmethod @_catch_and_log_exceptions def get_tracer() -> t.Optional[Tracer]: - log.debug("Getting test session tracer") + log.debug("Getting tracer") # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_session_get_tracer + from ddtrace.internal.ci_visibility.recorder import CIVisibility - tracer: t.Optional[Tracer] = on_session_get_tracer() - log.debug("Got test session tracer: %s", tracer) - return tracer + return CIVisibility.get_tracer() @staticmethod @_catch_and_log_exceptions def get_workspace_path() -> t.Optional[Path]: - log.debug("Getting session workspace path") + log.debug("Getting workspace path") # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_session_get_workspace_path + from ddtrace.internal.ci_visibility.recorder import CIVisibility - workspace_path: Path = on_session_get_workspace_path() - return workspace_path + path_str = CIVisibility.get_workspace_path() + return Path(path_str) if path_str is not None else None @staticmethod @_catch_and_log_exceptions def should_collect_coverage() -> bool: - log.debug("Checking if coverage should be collected for session") + log.debug("Checking if should collect coverage") # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_session_should_collect_coverage + from ddtrace.internal.ci_visibility.recorder import CIVisibility - _should_collect_coverage = bool(on_session_should_collect_coverage()) - log.debug("Coverage should be collected: %s", _should_collect_coverage) - return _should_collect_coverage + return CIVisibility.should_collect_coverage() @staticmethod @_catch_and_log_exceptions def is_test_skipping_enabled() -> bool: log.debug("Checking if test skipping is enabled") # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_session_is_test_skipping_enabled + from ddtrace.internal.ci_visibility.recorder import CIVisibility - _is_test_skipping_enabled = bool(on_session_is_test_skipping_enabled()) - log.debug("Test skipping is enabled: %s", _is_test_skipping_enabled) - return _is_test_skipping_enabled + return CIVisibility.test_skipping_enabled() @staticmethod @_catch_and_log_exceptions - def set_covered_lines_pct(coverage_pct: float): - log.debug("Setting covered lines percentage for session to %s", coverage_pct) + def set_covered_lines_pct(coverage_pct: float) -> None: + log.debug("Setting coverage percentage for session to %s", coverage_pct) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_session_set_covered_lines_pct + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_session_set_covered_lines_pct(coverage_pct) + CIVisibility.get_session().set_covered_lines_pct(coverage_pct) @staticmethod @_catch_and_log_exceptions def get_path_codeowners(path: Path) -> t.Optional[t.List[str]]: - log.debug("Getting codeowners object for path %s", path) + log.debug("Getting codeowners for path %s", path) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_session_get_path_codeowners + from ddtrace.internal.ci_visibility.recorder import CIVisibility - path_codeowners: t.Optional[t.List[str]] = on_session_get_path_codeowners(path) - return path_codeowners + codeowners = CIVisibility.get_codeowners() + if codeowners is None: + return None + return codeowners.of(str(path)) @staticmethod @_catch_and_log_exceptions def set_library_capabilities(capabilities: LibraryCapabilities) -> None: + log.debug("Setting library capabilities") # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_session_set_library_capabilities + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_session_set_library_capabilities(capabilities) + CIVisibility.set_library_capabilities(capabilities) @staticmethod @_catch_and_log_exceptions - def set_itr_tags(skipped_count: int): - log.debug("Setting itr session tags: %d", skipped_count) + def set_itr_skipped_count(skipped_count: int) -> None: + log.debug("Setting skipped count: %d", skipped_count) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_session_set_itr_skipped_count + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_session_set_itr_skipped_count(skipped_count) + CIVisibility.get_session().set_skipped_count(skipped_count) class InternalTestModule(ext_api.TestModule, InternalTestBase): @@ -197,51 +205,45 @@ def finish( from ddtrace.ext.test_visibility.api import Test from ddtrace.internal.ci_visibility.recorder import on_finish_test - on_finish_test(Test.FinishArgs(item_id, status, reason, exc_info)) + # Test.FinishArgs requires a non-optional status, so provide a default if None + final_status = status if status is not None else ext_api.TestStatus.PASS + on_finish_test(Test.FinishArgs(item_id, final_status, reason, exc_info)) @staticmethod @_catch_and_log_exceptions - def is_new_test(item_id: InternalTestId) -> bool: - log.debug("Checking if test %s is new", item_id) + def is_new(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: + log.debug("Checking if test %s is new", test_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_is_new_test + from ddtrace.internal.ci_visibility.recorder import CIVisibility - is_new = bool(on_is_new_test(item_id)) - log.debug("Test %s is new: %s", item_id, is_new) - return is_new + return CIVisibility.get_test_by_id(test_id).is_new() @staticmethod @_catch_and_log_exceptions - def is_quarantined_test(item_id: InternalTestId) -> bool: - log.debug("Checking if test %s is quarantined", item_id) + def is_quarantined(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: + log.debug("Checking if test %s is quarantined", test_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_is_quarantined_test + from ddtrace.internal.ci_visibility.recorder import CIVisibility - is_quarantined = bool(on_is_quarantined_test(item_id)) - log.debug("Test %s is quarantined: %s", item_id, is_quarantined) - return is_quarantined + return CIVisibility.get_test_by_id(test_id).is_quarantined() @staticmethod @_catch_and_log_exceptions - def is_disabled_test(item_id: InternalTestId) -> bool: - log.debug("Checking if test %s is disabled", item_id) + def is_disabled(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: + log.debug("Checking if test %s is disabled", test_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_is_disabled_test + from ddtrace.internal.ci_visibility.recorder import CIVisibility - is_disabled = bool(on_is_disabled_test(item_id)) - log.debug("Test %s is disabled: %s", item_id, is_disabled) - return is_disabled + return CIVisibility.get_test_by_id(test_id).is_disabled() @staticmethod @_catch_and_log_exceptions - def is_attempt_to_fix(item_id: InternalTestId) -> bool: - log.debug("Checking if test %s is attempt to fix", item_id) + def is_attempt_to_fix(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: + log.debug("Checking if test %s is attempt to fix", test_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_is_attempt_to_fix + from ddtrace.internal.ci_visibility.recorder import CIVisibility - is_attempt_to_fix = bool(on_is_attempt_to_fix(item_id)) - log.debug("Test %s is attempt to fix: %s", item_id, is_attempt_to_fix) - return is_attempt_to_fix + return CIVisibility.get_test_by_id(test_id).is_attempt_to_fix() class OverwriteAttributesArgs(NamedTuple): test_id: InternalTestId @@ -250,6 +252,12 @@ class OverwriteAttributesArgs(NamedTuple): parameters: t.Optional[str] = None codeowners: t.Optional[t.List[str]] = None + class FinishTestArgs(NamedTuple): + test_id: InternalTestId + status: t.Optional[TestStatus] = None + skip_reason: t.Optional[str] = None + exc_info: t.Optional[TestExcInfo] = None + @staticmethod @_catch_and_log_exceptions def overwrite_attributes( @@ -266,3 +274,14 @@ def overwrite_attributes( on_test_overwrite_attributes( InternalTest.OverwriteAttributesArgs(item_id, name, suite_name, parameters, codeowners) ) + + @staticmethod + @_catch_and_log_exceptions + def finish_test(finish_args: "InternalTest.FinishTestArgs") -> None: + log.debug("Finishing test %s with status %s", finish_args.test_id, finish_args.status) + # Lazy import to avoid circular dependency + from ddtrace.internal.ci_visibility.recorder import CIVisibility + + CIVisibility.get_test_by_id(finish_args.test_id).finish_test( + finish_args.status, finish_args.skip_reason, finish_args.exc_info + ) From 42135a68519d65267ca22d572c5247de5c866346 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Thu, 12 Jun 2025 17:59:48 +0200 Subject: [PATCH 18/58] more refactors --- ddtrace/ext/test_visibility/_utils.py | 6 ++--- ddtrace/ext/test_visibility/api.py | 6 ++--- ddtrace/internal/ci_visibility/api/_test.py | 5 ++++- .../internal/test_visibility/_efd_mixins.py | 22 ++++++++++++++----- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/ddtrace/ext/test_visibility/_utils.py b/ddtrace/ext/test_visibility/_utils.py index 45091792afe..d9ecf823d14 100644 --- a/ddtrace/ext/test_visibility/_utils.py +++ b/ddtrace/ext/test_visibility/_utils.py @@ -35,21 +35,21 @@ def _get_item_tag(item_id: TestVisibilityItemId, name: str) -> Any: return CIVisibility.get_item_by_id(item_id).get_tag(name) -def _set_item_tag(item_id: TestVisibilityItemId, name: str, value: Any, recurse: bool = False) -> None: +def _set_item_tag(item_id: TestVisibilityItemId, name: str, value: Any) -> None: # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import CIVisibility CIVisibility.get_item_by_id(item_id).set_tag(name, value) -def _set_item_tags(item_id: TestVisibilityItemId, tags: Dict[str, Any], recurse: bool = False) -> None: +def _set_item_tags(item_id: TestVisibilityItemId, tags: Dict[str, Any]) -> None: # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import CIVisibility CIVisibility.get_item_by_id(item_id).set_tags(tags) -def _delete_item_tag(item_id: TestVisibilityItemId, name: str, recurse: bool = False) -> None: +def _delete_item_tag(item_id: TestVisibilityItemId, name: str) -> None: # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import CIVisibility diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 1b3aeeaebc1..a3b7f1a759b 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -293,10 +293,10 @@ def discover( @staticmethod @_catch_and_log_exceptions def start(item_id: TestSuiteId): - log.debug("Starting suite %s", item_id) - from ddtrace.internal.ci_visibility.recorder import on_start_suite + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_start_suite(item_id) + log.debug("Starting suite %s", item_id) + CIVisibility.get_suite_by_id(item_id).start() class FinishArgs(NamedTuple): suite_id: TestSuiteId diff --git a/ddtrace/internal/ci_visibility/api/_test.py b/ddtrace/internal/ci_visibility/api/_test.py index a12966d39af..66addcfb807 100644 --- a/ddtrace/internal/ci_visibility/api/_test.py +++ b/ddtrace/internal/ci_visibility/api/_test.py @@ -372,7 +372,10 @@ def efd_add_retry(self, start_immediately=False) -> Optional[int]: def efd_start_retry(self, retry_number: int) -> None: self._efd_get_retry_test(retry_number).start() - def efd_finish_retry(self, retry_number: int, status: TestStatus, exc_info: Optional[TestExcInfo] = None) -> None: + def efd_finish_retry( + self, retry_number: int, status: TestStatus, exc_info: Optional[TestExcInfo] = None, skip_reason: str = None + ) -> None: + # TODO: use skip_reason for something retry_test = self._efd_get_retry_test(retry_number) if status is not None: diff --git a/ddtrace/internal/test_visibility/_efd_mixins.py b/ddtrace/internal/test_visibility/_efd_mixins.py index ab4fb641f7a..0b6900ff1e7 100644 --- a/ddtrace/internal/test_visibility/_efd_mixins.py +++ b/ddtrace/internal/test_visibility/_efd_mixins.py @@ -83,13 +83,25 @@ def efd_start_retry(test_id: InternalTestId, retry_number: int) -> None: @staticmethod @_catch_and_log_exceptions - def efd_finish_retry(finish_args: "EFDTestMixin.EFDRetryFinishArgs") -> None: - log.debug("Finishing EFD retry %s for test %s", finish_args.retry_number, finish_args.test_id) - # Lazy import to avoid circular dependency + def efd_finish_retry( + item_id: InternalTestId, + retry_number: int, + status: ext_api.TestStatus, + skip_reason: t.Optional[str] = None, + exc_info: t.Optional[ext_api.TestExcInfo] = None, + ): from ddtrace.internal.ci_visibility.recorder import CIVisibility - CIVisibility.get_test_by_id(finish_args.test_id).efd_finish_retry( - finish_args.retry_number, finish_args.status, finish_args.exc_info + log.debug( + "Finishing EFD test retry %s for item %s, status: %s, skip_reason: %s, exc_info: %s", + retry_number, + item_id, + status, + skip_reason, + exc_info, + ) + CIVisibility.get_test_by_id(item_id).efd_finish_retry( + retry_number=retry_number, status=status, skip_reason=skip_reason, exc_info=exc_info ) @staticmethod From 245e20441688d42e77a6b25e29d1ac22e2e76ead Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Thu, 12 Jun 2025 18:33:57 +0200 Subject: [PATCH 19/58] proper name functions --- ddtrace/contrib/internal/pytest/_plugin_v2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ddtrace/contrib/internal/pytest/_plugin_v2.py b/ddtrace/contrib/internal/pytest/_plugin_v2.py index 821a89d69d4..171499c05bf 100644 --- a/ddtrace/contrib/internal/pytest/_plugin_v2.py +++ b/ddtrace/contrib/internal/pytest/_plugin_v2.py @@ -520,8 +520,8 @@ def _pytest_run_one_test(item, nextitem): reports_dict = {report.when: report for report in reports} test_id = _get_test_id_from_item(item) - is_quarantined = InternalTest.is_quarantined_test(test_id) - is_disabled = InternalTest.is_disabled_test(test_id) + is_quarantined = InternalTest.is_quarantined(test_id) + is_disabled = InternalTest.is_disabled(test_id) is_attempt_to_fix = InternalTest.is_attempt_to_fix(test_id) setup_or_teardown_failed = False From 65f1ef67aa97be742686f79d50fa1a076e072589 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Thu, 12 Jun 2025 19:39:02 +0200 Subject: [PATCH 20/58] types --- ddtrace/ext/test_visibility/api.py | 16 ++++++++-------- ddtrace/internal/ci_visibility/api/_test.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index a3b7f1a759b..52841857d1a 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -116,20 +116,20 @@ def get_tag(item_id: TestVisibilityItemId, tag_name: str) -> Any: return _get_item_tag(item_id, tag_name) @staticmethod - def set_tag(item_id: TestVisibilityItemId, tag_name: str, tag_value: Any, recurse: bool = False): - _set_item_tag(item_id, tag_name, tag_value, recurse) + def set_tag(item_id: TestVisibilityItemId, tag_name: str, tag_value: Any): + _set_item_tag(item_id, tag_name, tag_value) @staticmethod - def set_tags(item_id: TestVisibilityItemId, tags: Dict[str, Any], recurse: bool = False): - _set_item_tags(item_id, tags, recurse) + def set_tags(item_id: TestVisibilityItemId, tags: Dict[str, Any]): + _set_item_tags(item_id, tags) @staticmethod - def delete_tag(item_id: TestVisibilityItemId, tag_name: str, recurse: bool = False): - _delete_item_tag(item_id, tag_name, recurse) + def delete_tag(item_id: TestVisibilityItemId, tag_name: str): + _delete_item_tag(item_id, tag_name) @staticmethod - def delete_tags(item_id: TestVisibilityItemId, tag_names: List[str], recurse: bool = False): - _delete_item_tags(item_id, tag_names, recurse) + def delete_tags(item_id: TestVisibilityItemId, tag_names: List[str]): + _delete_item_tags(item_id, tag_names) @staticmethod def is_finished(item_id: TestVisibilityItemId) -> bool: diff --git a/ddtrace/internal/ci_visibility/api/_test.py b/ddtrace/internal/ci_visibility/api/_test.py index 66addcfb807..760794e894e 100644 --- a/ddtrace/internal/ci_visibility/api/_test.py +++ b/ddtrace/internal/ci_visibility/api/_test.py @@ -373,7 +373,7 @@ def efd_start_retry(self, retry_number: int) -> None: self._efd_get_retry_test(retry_number).start() def efd_finish_retry( - self, retry_number: int, status: TestStatus, exc_info: Optional[TestExcInfo] = None, skip_reason: str = None + self, retry_number: int, status: TestStatus, exc_info: Optional[TestExcInfo] = None, skip_reason: Optional[str] = None ) -> None: # TODO: use skip_reason for something retry_test = self._efd_get_retry_test(retry_number) From 879eededdafac97c074f3df06a48e52a6e74cf8b Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Thu, 12 Jun 2025 19:42:59 +0200 Subject: [PATCH 21/58] fmt --- ddtrace/internal/ci_visibility/api/_test.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ddtrace/internal/ci_visibility/api/_test.py b/ddtrace/internal/ci_visibility/api/_test.py index 760794e894e..aa270c15055 100644 --- a/ddtrace/internal/ci_visibility/api/_test.py +++ b/ddtrace/internal/ci_visibility/api/_test.py @@ -373,7 +373,11 @@ def efd_start_retry(self, retry_number: int) -> None: self._efd_get_retry_test(retry_number).start() def efd_finish_retry( - self, retry_number: int, status: TestStatus, exc_info: Optional[TestExcInfo] = None, skip_reason: Optional[str] = None + self, + retry_number: int, + status: TestStatus, + exc_info: Optional[TestExcInfo] = None, + skip_reason: Optional[str] = None, ) -> None: # TODO: use skip_reason for something retry_test = self._efd_get_retry_test(retry_number) From 27df5209501ede68ad27149856fbc0ae7552bd85 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Thu, 12 Jun 2025 19:45:13 +0200 Subject: [PATCH 22/58] mypy --- ddtrace/ext/test_visibility/api.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 52841857d1a..5b2a25af4a4 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -211,20 +211,20 @@ def get_tag(tag_name: str) -> Any: return _get_item_tag(TestSessionId(), tag_name) @staticmethod - def set_tag(tag_name: str, tag_value: Any, recurse: bool = False): - _set_item_tag(TestSessionId(), tag_name, tag_value, recurse) + def set_tag(tag_name: str, tag_value: Any): + _set_item_tag(TestSessionId(), tag_name, tag_value) @staticmethod - def set_tags(tags: Dict[str, Any], recurse: bool = False): - _set_item_tags(TestSessionId(), tags, recurse) + def set_tags(tags: Dict[str, Any]): + _set_item_tags(TestSessionId(), tags) @staticmethod - def delete_tag(tag_name: str, recurse: bool = False): - _delete_item_tag(TestSessionId(), tag_name, recurse) + def delete_tag(tag_name: str): + _delete_item_tag(TestSessionId(), tag_name) @staticmethod - def delete_tags(tag_names: List[str], recurse: bool = False): - _delete_item_tags(TestSessionId(), tag_names, recurse) + def delete_tags(tag_names: List[str]): + _delete_item_tags(TestSessionId(), tag_names) class TestModule(TestBase): From 76b2ea500c15d5e4f695df5bb06cfcb6d3453033 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Thu, 12 Jun 2025 20:45:34 +0200 Subject: [PATCH 23/58] =?UTF-8?q?=F0=9F=94=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ddtrace/ext/test_visibility/api.py | 109 +++++++--- ddtrace/internal/ci_visibility/recorder.py | 193 ------------------ .../internal/test_visibility/_atr_mixins.py | 13 +- .../internal/test_visibility/_itr_mixins.py | 5 +- ddtrace/internal/test_visibility/api.py | 17 +- 5 files changed, 101 insertions(+), 236 deletions(-) diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 5b2a25af4a4..6ee6d24a71b 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -185,10 +185,13 @@ def discover( @staticmethod @_catch_and_log_exceptions def start(distributed_children: bool = False, context: Optional[Context] = None): - log.debug("Starting session") - from ddtrace.internal.ci_visibility.recorder import on_start_session + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_start_session(distributed_children, context) + log.debug("Starting session") + session = CIVisibility.get_session() + session.start(context) + if distributed_children: + session.set_distributed_children() class FinishArgs(NamedTuple): force_finish_children: bool @@ -200,11 +203,12 @@ def finish( force_finish_children: bool = False, override_status: Optional[TestStatus] = None, ): - log.debug("Finishing session, force_finish_session_modules: %s", force_finish_children) + from ddtrace.internal.ci_visibility.recorder import CIVisibility - from ddtrace.internal.ci_visibility.recorder import on_finish_session + log.debug("Finishing session, force_finish_session_modules: %s", force_finish_children) - on_finish_session(TestSession.FinishArgs(force_finish_children, override_status)) + session = CIVisibility.get_session() + session.finish(force_finish_children, override_status) @staticmethod def get_tag(tag_name: str) -> Any: @@ -240,18 +244,28 @@ class FinishArgs(NamedTuple): @staticmethod @_catch_and_log_exceptions def discover(item_id: TestModuleId, module_path: Optional[Path] = None): + from ddtrace.internal.ci_visibility.api._module import TestVisibilityModule + from ddtrace.internal.ci_visibility.recorder import CIVisibility + log.debug("Registered module %s", item_id) - from ddtrace.internal.ci_visibility.recorder import on_discover_module + session = CIVisibility.get_session() - on_discover_module(TestModule.DiscoverArgs(item_id, module_path)) + session.add_child( + item_id, + TestVisibilityModule( + item_id.name, + CIVisibility.get_session_settings(), + module_path, + ), + ) @staticmethod @_catch_and_log_exceptions def start(item_id: TestModuleId): - log.debug("Starting module %s", item_id) - from ddtrace.internal.ci_visibility.recorder import on_start_module + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_start_module(item_id) + log.debug("Starting module %s", item_id) + CIVisibility.get_module_by_id(item_id).start() @staticmethod @_catch_and_log_exceptions @@ -266,9 +280,9 @@ def finish( override_status, force_finish_children, ) - from ddtrace.internal.ci_visibility.recorder import on_finish_module + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_finish_module(TestModule.FinishArgs(item_id, override_status, force_finish_children)) + CIVisibility.get_module_by_id(item_id).finish() class TestSuite(TestBase): @@ -286,9 +300,20 @@ def discover( ): """Registers a test suite with the Test Visibility service.""" log.debug("Registering suite %s, source: %s", item_id, source_file_info) - from ddtrace.internal.ci_visibility.recorder import on_discover_suite + from ddtrace.internal.ci_visibility.api._suite import TestVisibilitySuite + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_discover_suite(TestSuite.DiscoverArgs(item_id, codeowners, source_file_info)) + module = CIVisibility.get_module_by_id(item_id.parent_id) + + module.add_child( + item_id, + TestVisibilitySuite( + item_id.name, + CIVisibility.get_session_settings(), + codeowners, + source_file_info, + ), + ) @staticmethod @_catch_and_log_exceptions @@ -316,9 +341,9 @@ def finish( force_finish_children, override_status, ) - from ddtrace.internal.ci_visibility.recorder import on_finish_suite + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_finish_suite(TestSuite.FinishArgs(item_id, force_finish_children, override_status)) + CIVisibility.get_suite_by_id(item_id).finish(force_finish_children, override_status) class Test(TestBase): @@ -344,17 +369,51 @@ def discover( source_file_info, resource, ) - from ddtrace.internal.ci_visibility.recorder import on_discover_test + from ddtrace.internal.ci_visibility._api_client import TestProperties + from ddtrace.internal.ci_visibility.api._test import TestVisibilityTest + from ddtrace.internal.ci_visibility.recorder import CIVisibility + + log.debug("Handling discovery for test %s", item_id) + suite = CIVisibility.get_suite_by_id(item_id.parent_id) + + # New tests are currently only considered for EFD: + # - if known tests were fetched properly (enforced by is_known_test) + # - if they have no parameters + if CIVisibility.is_known_tests_enabled() and item_id.parameters is None: + is_new = not CIVisibility.is_known_test(item_id) + else: + is_new = False - on_discover_test(Test.DiscoverArgs(item_id, codeowners, source_file_info, resource)) + test_properties = None + if CIVisibility.is_test_management_enabled(): + test_properties = CIVisibility.get_test_properties(item_id) + + if not test_properties: + test_properties = TestProperties() + + suite.add_child( + item_id, + TestVisibilityTest( + item_id.name, + CIVisibility.get_session_settings(), + parameters=item_id.parameters, + codeowners=codeowners, + source_file_info=source_file_info, + resource=resource, + is_new=is_new, + is_quarantined=test_properties.quarantined, + is_disabled=test_properties.disabled, + is_attempt_to_fix=test_properties.attempt_to_fix, + ), + ) @staticmethod @_catch_and_log_exceptions def start(item_id: TestId): log.debug("Starting test %s", item_id) - from ddtrace.internal.ci_visibility.recorder import on_start_test + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_start_test(item_id) + CIVisibility.get_test_by_id(item_id).start() class FinishArgs(NamedTuple): test_id: TestId @@ -377,17 +436,17 @@ def finish( skip_reason, exc_info, ) - from ddtrace.internal.ci_visibility.recorder import on_finish_test + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_finish_test(Test.FinishArgs(item_id, status, skip_reason=skip_reason, exc_info=exc_info)) + CIVisibility.get_test_by_id(item_id).finish_test(status, skip_reason, exc_info) @staticmethod @_catch_and_log_exceptions def set_parameters(item_id: TestId, params: str): log.debug("Setting test %s parameters to %s", item_id, params) - from ddtrace.internal.ci_visibility.recorder import on_set_test_parameters + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_set_test_parameters(item_id, params) + CIVisibility.get_test_by_id(item_id).set_parameters(params) @staticmethod @_catch_and_log_exceptions diff --git a/ddtrace/internal/ci_visibility/recorder.py b/ddtrace/internal/ci_visibility/recorder.py index 520d76c786e..fb917bf5457 100644 --- a/ddtrace/internal/ci_visibility/recorder.py +++ b/ddtrace/internal/ci_visibility/recorder.py @@ -13,20 +13,15 @@ import ddtrace from ddtrace import config as ddconfig -from ddtrace._trace.context import Context from ddtrace.contrib import trace_utils from ddtrace.ext import ci from ddtrace.ext import test from ddtrace.ext.test_visibility import ITR_SKIPPING_LEVEL from ddtrace.ext.test_visibility._test_visibility_base import TestSessionId from ddtrace.ext.test_visibility._test_visibility_base import TestVisibilityItemId -from ddtrace.ext.test_visibility.api import Test from ddtrace.ext.test_visibility.api import TestId -from ddtrace.ext.test_visibility.api import TestModule from ddtrace.ext.test_visibility.api import TestModuleId from ddtrace.ext.test_visibility.api import TestSession -from ddtrace.ext.test_visibility.api import TestStatus -from ddtrace.ext.test_visibility.api import TestSuite from ddtrace.ext.test_visibility.api import TestSuiteId from ddtrace.internal import agent from ddtrace.internal import atexit @@ -72,11 +67,8 @@ from ddtrace.internal.logger import get_logger from ddtrace.internal.service import Service from ddtrace.internal.test_visibility._atr_mixins import AutoTestRetriesSettings -from ddtrace.internal.test_visibility._attempt_to_fix_mixins import AttemptToFixTestMixin -from ddtrace.internal.test_visibility._benchmark_mixin import BenchmarkTestMixin from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId from ddtrace.internal.test_visibility._library_capabilities import LibraryCapabilities -from ddtrace.internal.test_visibility.api import InternalTest from ddtrace.internal.utils.formats import asbool from ddtrace.settings import IntegrationConfig from ddtrace.settings._agent import config as agent_config @@ -1091,188 +1083,3 @@ def on_discover_session(discover_args: TestSession.DiscoverArgs) -> None: CIVisibility.add_session(session) CIVisibility.set_test_session_name(test_command=discover_args.test_command) - - -@_requires_civisibility_enabled -def on_start_session(distributed_children: bool = False, context: Optional[Context] = None) -> None: - log.debug("Handling start session") - session = CIVisibility.get_session() - session.start(context) - if distributed_children: - session.set_distributed_children() - - -@_requires_civisibility_enabled -def on_finish_session(finish_args: TestSession.FinishArgs) -> None: - log.debug("Handling finish session") - session = CIVisibility.get_session() - session.finish(finish_args.force_finish_children, finish_args.override_status) - - -@_requires_civisibility_enabled -def on_discover_module(discover_args: TestModule.DiscoverArgs) -> None: - log.debug("Handling discovery for module %s", discover_args.module_id) - session = CIVisibility.get_session() - - session.add_child( - discover_args.module_id, - TestVisibilityModule( - discover_args.module_id.name, - CIVisibility.get_session_settings(), - discover_args.module_path, - ), - ) - - -@_requires_civisibility_enabled -def on_start_module(module_id: TestModuleId) -> None: - log.debug("Handling start for module id %s", module_id) - CIVisibility.get_module_by_id(module_id).start() - - -@_requires_civisibility_enabled -def on_finish_module(finish_args: TestModule.FinishArgs) -> None: - log.debug("Handling finish for module id %s", finish_args.module_id) - CIVisibility.get_module_by_id(finish_args.module_id).finish() - - -@_requires_civisibility_enabled -def on_discover_suite(discover_args: TestSuite.DiscoverArgs) -> None: - log.debug("Handling discovery for suite args %s", discover_args) - module = CIVisibility.get_module_by_id(discover_args.suite_id.parent_id) - - module.add_child( - discover_args.suite_id, - TestVisibilitySuite( - discover_args.suite_id.name, - CIVisibility.get_session_settings(), - discover_args.codeowners, - discover_args.source_file_info, - ), - ) - - -@_requires_civisibility_enabled -def on_start_suite(suite_id: TestSuiteId) -> None: - log.debug("Handling start for suite id %s", suite_id) - CIVisibility.get_suite_by_id(suite_id).start() - - -@_requires_civisibility_enabled -def on_finish_suite(finish_args: TestSuite.FinishArgs) -> None: - log.debug("Handling finish for suite id %s", finish_args.suite_id) - CIVisibility.get_suite_by_id(finish_args.suite_id).finish( - finish_args.force_finish_children, finish_args.override_status - ) - - -@_requires_civisibility_enabled -def on_discover_test(discover_args: Test.DiscoverArgs) -> None: - log.debug("Handling discovery for test %s", discover_args.test_id) - suite = CIVisibility.get_suite_by_id(discover_args.test_id.parent_id) - - # New tests are currently only considered for EFD: - # - if known tests were fetched properly (enforced by is_known_test) - # - if they have no parameters - if CIVisibility.is_known_tests_enabled() and discover_args.test_id.parameters is None: - is_new = not CIVisibility.is_known_test(discover_args.test_id) - else: - is_new = False - - test_properties = None - if CIVisibility.is_test_management_enabled(): - test_properties = CIVisibility.get_test_properties(discover_args.test_id) - - if not test_properties: - test_properties = TestProperties() - - suite.add_child( - discover_args.test_id, - TestVisibilityTest( - discover_args.test_id.name, - CIVisibility.get_session_settings(), - parameters=discover_args.test_id.parameters, - codeowners=discover_args.codeowners, - source_file_info=discover_args.source_file_info, - resource=discover_args.resource, - is_new=is_new, - is_quarantined=test_properties.quarantined, - is_disabled=test_properties.disabled, - is_attempt_to_fix=test_properties.attempt_to_fix, - ), - ) - - -@_requires_civisibility_enabled -def on_start_test(test_id: TestId) -> None: - log.debug("Handling start for test id %s", test_id) - CIVisibility.get_test_by_id(test_id).start() - - -@_requires_civisibility_enabled -def on_finish_test(finish_args: Test.FinishArgs) -> None: - log.debug("Handling finish for test id %s, with status %s", finish_args.test_id, finish_args.status) - CIVisibility.get_test_by_id(finish_args.test_id).finish_test( - finish_args.status, finish_args.skip_reason, finish_args.exc_info - ) - - -@_requires_civisibility_enabled -def on_set_test_parameters(item_id: TestId, parameters: str) -> None: - log.debug("Handling set parameters for test id %s, parameters %s", item_id, parameters) - CIVisibility.get_test_by_id(item_id).set_parameters(parameters) - - -@_requires_civisibility_enabled -def on_set_benchmark_data(set_benchmark_data_args: BenchmarkTestMixin.SetBenchmarkDataArgs) -> None: - item_id = set_benchmark_data_args.test_id - data = set_benchmark_data_args.benchmark_data - is_benchmark = set_benchmark_data_args.is_benchmark - log.debug("Handling set benchmark data for test id %s, data %s, is_benchmark %s", item_id, data, is_benchmark) - CIVisibility.get_test_by_id(item_id).set_benchmark_data(data, is_benchmark) - - -@_requires_civisibility_enabled -def on_test_overwrite_attributes(overwrite_attribute_args: InternalTest.OverwriteAttributesArgs) -> None: - item_id = overwrite_attribute_args.test_id - name = overwrite_attribute_args.name - suite_name = overwrite_attribute_args.suite_name - parameters = overwrite_attribute_args.parameters - codeowners = overwrite_attribute_args.codeowners - - log.debug("Handling overwrite attributes: %s", overwrite_attribute_args) - CIVisibility.get_test_by_id(item_id).overwrite_attributes(name, suite_name, parameters, codeowners) - - -@_requires_civisibility_enabled -def on_attempt_to_fix_should_retry_test(item_id: InternalTestId) -> bool: - return CIVisibility.get_test_by_id(item_id).attempt_to_fix_should_retry() - - -@_requires_civisibility_enabled -def on_attempt_to_fix_add_retry(item_id: InternalTestId, start_immediately: bool = False) -> Optional[int]: - return CIVisibility.get_test_by_id(item_id).attempt_to_fix_add_retry(start_immediately) - - -@_requires_civisibility_enabled -def on_attempt_to_fix_start_retry(test_id: InternalTestId, retry_number: int) -> None: - CIVisibility.get_test_by_id(test_id).attempt_to_fix_start_retry(retry_number) - - -@_requires_civisibility_enabled -def on_attempt_to_fix_finish_retry( - attempt_to_fix_finish_args: AttemptToFixTestMixin.AttemptToFixRetryFinishArgs, -) -> None: - CIVisibility.get_test_by_id(attempt_to_fix_finish_args.test_id).attempt_to_fix_finish_retry( - attempt_to_fix_finish_args.retry_number, attempt_to_fix_finish_args.status, attempt_to_fix_finish_args.exc_info - ) - - -@_requires_civisibility_enabled -def on_attempt_to_fix_get_final_status(test_id: InternalTestId) -> TestStatus: - return CIVisibility.get_test_by_id(test_id).attempt_to_fix_get_final_status() - - -@_requires_civisibility_enabled -def on_attempt_to_fix_session_has_failed_tests() -> bool: - return CIVisibility.get_session().attempt_to_fix_has_failed_tests() diff --git a/ddtrace/internal/test_visibility/_atr_mixins.py b/ddtrace/internal/test_visibility/_atr_mixins.py index 1034cfdd95f..c7b6c4bda04 100644 --- a/ddtrace/internal/test_visibility/_atr_mixins.py +++ b/ddtrace/internal/test_visibility/_atr_mixins.py @@ -74,13 +74,18 @@ def atr_start_retry(test_id: InternalTestId, retry_number: int) -> None: @staticmethod @_catch_and_log_exceptions - def atr_finish_retry(finish_args: "ATRTestMixin.ATRRetryFinishArgs") -> None: - log.debug("Finishing ATR retry %s for test %s", finish_args.retry_number, finish_args.test_id) + def atr_finish_retry( + test_id: InternalTestId, + retry_number: int, + status: ext_api.TestStatus, + exc_info: t.Optional[ext_api.TestExcInfo] = None, + ) -> None: + log.debug("Finishing ATR retry %s for test %s", retry_number, test_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import CIVisibility - CIVisibility.get_test_by_id(finish_args.test_id).atr_finish_retry( - finish_args.retry_number, finish_args.status, finish_args.exc_info + CIVisibility.get_test_by_id(test_id).atr_finish_retry( + retry_number=retry_number, status=status, exc_info=exc_info ) @staticmethod diff --git a/ddtrace/internal/test_visibility/_itr_mixins.py b/ddtrace/internal/test_visibility/_itr_mixins.py index 9bd211f1d1e..3abeffb412d 100644 --- a/ddtrace/internal/test_visibility/_itr_mixins.py +++ b/ddtrace/internal/test_visibility/_itr_mixins.py @@ -97,11 +97,8 @@ def was_itr_skipped(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bo @staticmethod @_catch_and_log_exceptions - def add_coverage_data(add_coverage_args: "ITRMixin.AddCoverageArgs") -> None: + def add_coverage_data(item_id, coverage_data) -> None: """Adds coverage data to an item, merging with existing coverage data if necessary""" - item_id = add_coverage_args.item_id - coverage_data = add_coverage_args.coverage_data - log.debug("Adding coverage data for item id %s", item_id) if not isinstance(item_id, (ext_api.TestSuiteId, InternalTestId)): diff --git a/ddtrace/internal/test_visibility/api.py b/ddtrace/internal/test_visibility/api.py index 7e4e1aae997..e394a619dbc 100644 --- a/ddtrace/internal/test_visibility/api.py +++ b/ddtrace/internal/test_visibility/api.py @@ -200,14 +200,12 @@ def finish( exc_info: t.Optional[ext_api.TestExcInfo] = None, override_finish_time: t.Optional[float] = None, ): - log.debug("Finishing test with status: %s, reason: %s", status, reason) # Lazy import to avoid circular dependency - from ddtrace.ext.test_visibility.api import Test - from ddtrace.internal.ci_visibility.recorder import on_finish_test + from ddtrace.internal.ci_visibility.recorder import CIVisibility - # Test.FinishArgs requires a non-optional status, so provide a default if None + log.debug("Finishing test with status: %s, reason: %s", status, reason) final_status = status if status is not None else ext_api.TestStatus.PASS - on_finish_test(Test.FinishArgs(item_id, final_status, reason, exc_info)) + CIVisibility.get_test_by_id(item_id).finish_test(final_status, reason, exc_info) @staticmethod @_catch_and_log_exceptions @@ -267,13 +265,12 @@ def overwrite_attributes( parameters: t.Optional[str] = None, codeowners: t.Optional[t.List[str]] = None, ): - log.debug("Overwriting attributes for test %s", item_id) # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import on_test_overwrite_attributes + from ddtrace.internal.ci_visibility.recorder import CIVisibility - on_test_overwrite_attributes( - InternalTest.OverwriteAttributesArgs(item_id, name, suite_name, parameters, codeowners) - ) + log.debug("Overwriting attributes for test %s", item_id) + + CIVisibility.get_test_by_id(item_id).overwrite_attributes(name, suite_name, parameters, codeowners) @staticmethod @_catch_and_log_exceptions From 9683ca224c54d0a53e3314aeec962b64f23b5565 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Thu, 12 Jun 2025 20:50:04 +0200 Subject: [PATCH 24/58] remove discoverargs --- ddtrace/ext/test_visibility/api.py | 46 +++++----------------- ddtrace/internal/ci_visibility/recorder.py | 35 ++++++++++------ 2 files changed, 31 insertions(+), 50 deletions(-) diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 6ee6d24a71b..cd52c346241 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -137,17 +137,6 @@ def is_finished(item_id: TestVisibilityItemId) -> bool: class TestSession(_TestVisibilityAPIBase): - class DiscoverArgs(NamedTuple): - test_command: str - reject_duplicates: bool - test_framework: str - test_framework_version: str - session_operation_name: str - module_operation_name: str - suite_operation_name: str - test_operation_name: str - root_dir: Optional[Path] = None - @staticmethod @_catch_and_log_exceptions def discover( @@ -169,17 +158,15 @@ def discover( from ddtrace.internal.ci_visibility.recorder import on_discover_session on_discover_session( - TestSession.DiscoverArgs( - test_command, - reject_duplicates, - test_framework, - test_framework_version, - session_operation_name, - module_operation_name, - suite_operation_name, - test_operation_name, - root_dir, - ) + test_command, + reject_duplicates, + test_framework, + test_framework_version, + session_operation_name, + module_operation_name, + suite_operation_name, + test_operation_name, + root_dir, ) @staticmethod @@ -232,10 +219,6 @@ def delete_tags(tag_names: List[str]): class TestModule(TestBase): - class DiscoverArgs(NamedTuple): - module_id: TestModuleId - module_path: Optional[Path] = None - class FinishArgs(NamedTuple): module_id: TestModuleId override_status: Optional[TestStatus] = None @@ -286,11 +269,6 @@ def finish( class TestSuite(TestBase): - class DiscoverArgs(NamedTuple): - suite_id: TestSuiteId - codeowners: Optional[List[str]] = None - source_file_info: Optional[TestSourceFileInfo] = None - @staticmethod @_catch_and_log_exceptions def discover( @@ -347,12 +325,6 @@ def finish( class Test(TestBase): - class DiscoverArgs(NamedTuple): - test_id: TestId - codeowners: Optional[List[str]] = None - source_file_info: Optional[TestSourceFileInfo] = None - resource: Optional[str] = None - @staticmethod @_catch_and_log_exceptions def discover( diff --git a/ddtrace/internal/ci_visibility/recorder.py b/ddtrace/internal/ci_visibility/recorder.py index fb917bf5457..057f548d497 100644 --- a/ddtrace/internal/ci_visibility/recorder.py +++ b/ddtrace/internal/ci_visibility/recorder.py @@ -21,7 +21,6 @@ from ddtrace.ext.test_visibility._test_visibility_base import TestVisibilityItemId from ddtrace.ext.test_visibility.api import TestId from ddtrace.ext.test_visibility.api import TestModuleId -from ddtrace.ext.test_visibility.api import TestSession from ddtrace.ext.test_visibility.api import TestSuiteId from ddtrace.internal import agent from ddtrace.internal import atexit @@ -1019,7 +1018,17 @@ def wrapper(*args, **kwargs) -> Any: @_requires_civisibility_enabled -def on_discover_session(discover_args: TestSession.DiscoverArgs) -> None: +def on_discover_session( + test_command, + reject_duplicates, + test_framework, + test_framework_version, + session_operation_name, + module_operation_name, + suite_operation_name, + test_operation_name, + root_dir, +) -> None: log.debug("Handling session discovery") # _requires_civisibility_enabled prevents us from getting here, but this makes type checkers happy @@ -1033,10 +1042,10 @@ def on_discover_session(discover_args: TestSession.DiscoverArgs) -> None: raise CIVisibilityError(error_msg) # If we're not provided a root directory, try and extract it from workspace, defaulting to CWD - workspace_path = discover_args.root_dir or Path(CIVisibility.get_workspace_path() or os.getcwd()) + workspace_path = root_dir or Path(CIVisibility.get_workspace_path() or os.getcwd()) # Prevent high cardinality of test framework telemetry tag by matching with known frameworks - test_framework_telemetry_name = _get_test_framework_telemetry_name(discover_args.test_framework) + test_framework_telemetry_name = _get_test_framework_telemetry_name(test_framework) efd_api_settings = CIVisibility.get_efd_api_settings() if efd_api_settings is None or not CIVisibility.is_efd_enabled(): @@ -1053,15 +1062,15 @@ def on_discover_session(discover_args: TestSession.DiscoverArgs) -> None: session_settings = TestVisibilitySessionSettings( tracer=tracer, test_service=test_service, - test_command=discover_args.test_command, - reject_duplicates=discover_args.reject_duplicates, - test_framework=discover_args.test_framework, + test_command=test_command, + reject_duplicates=reject_duplicates, + test_framework=test_framework, test_framework_metric_name=test_framework_telemetry_name, - test_framework_version=discover_args.test_framework_version, - session_operation_name=discover_args.session_operation_name, - module_operation_name=discover_args.module_operation_name, - suite_operation_name=discover_args.suite_operation_name, - test_operation_name=discover_args.test_operation_name, + test_framework_version=test_framework_version, + session_operation_name=session_operation_name, + module_operation_name=module_operation_name, + suite_operation_name=suite_operation_name, + test_operation_name=test_operation_name, workspace_path=workspace_path, is_unsupported_ci=CIVisibility.is_unknown_ci(), itr_enabled=CIVisibility.is_itr_enabled(), @@ -1082,4 +1091,4 @@ def on_discover_session(discover_args: TestSession.DiscoverArgs) -> None: ) CIVisibility.add_session(session) - CIVisibility.set_test_session_name(test_command=discover_args.test_command) + CIVisibility.set_test_session_name(test_command=test_command) From 3e062f4a031e423580f38dcdba0d93e817f51203 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Thu, 12 Jun 2025 21:02:46 +0200 Subject: [PATCH 25/58] =?UTF-8?q?=F0=9F=94=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ddtrace/contrib/internal/pytest/_plugin_v2.py | 4 ++-- ddtrace/internal/test_visibility/_efd_mixins.py | 6 ------ ddtrace/internal/test_visibility/api.py | 17 ----------------- 3 files changed, 2 insertions(+), 25 deletions(-) diff --git a/ddtrace/contrib/internal/pytest/_plugin_v2.py b/ddtrace/contrib/internal/pytest/_plugin_v2.py index 171499c05bf..03d431349fe 100644 --- a/ddtrace/contrib/internal/pytest/_plugin_v2.py +++ b/ddtrace/contrib/internal/pytest/_plugin_v2.py @@ -158,8 +158,8 @@ def _handle_test_management(item, test_id): """Add a user property to identify quarantined tests, and mark them for skipping if quarantine is enabled in skipping mode. """ - is_quarantined = InternalTest.is_quarantined_test(test_id) - is_disabled = InternalTest.is_disabled_test(test_id) + is_quarantined = InternalTest.is_quarantined(test_id) + is_disabled = InternalTest.is_disabled(test_id) is_attempt_to_fix = InternalTest.is_attempt_to_fix(test_id) if is_quarantined and asbool(os.getenv("_DD_TEST_SKIP_QUARANTINED_TESTS")): diff --git a/ddtrace/internal/test_visibility/_efd_mixins.py b/ddtrace/internal/test_visibility/_efd_mixins.py index 0b6900ff1e7..a75a7e177e9 100644 --- a/ddtrace/internal/test_visibility/_efd_mixins.py +++ b/ddtrace/internal/test_visibility/_efd_mixins.py @@ -47,12 +47,6 @@ def efd_has_failed_tests() -> bool: class EFDTestMixin: - class EFDRetryFinishArgs(t.NamedTuple): - test_id: InternalTestId - retry_number: int - status: ext_api.TestStatus - exc_info: t.Optional[ext_api.TestExcInfo] - @staticmethod @_catch_and_log_exceptions def efd_should_retry(test_id: InternalTestId) -> bool: diff --git a/ddtrace/internal/test_visibility/api.py b/ddtrace/internal/test_visibility/api.py index e394a619dbc..d6bcef49dba 100644 --- a/ddtrace/internal/test_visibility/api.py +++ b/ddtrace/internal/test_visibility/api.py @@ -250,12 +250,6 @@ class OverwriteAttributesArgs(NamedTuple): parameters: t.Optional[str] = None codeowners: t.Optional[t.List[str]] = None - class FinishTestArgs(NamedTuple): - test_id: InternalTestId - status: t.Optional[TestStatus] = None - skip_reason: t.Optional[str] = None - exc_info: t.Optional[TestExcInfo] = None - @staticmethod @_catch_and_log_exceptions def overwrite_attributes( @@ -271,14 +265,3 @@ def overwrite_attributes( log.debug("Overwriting attributes for test %s", item_id) CIVisibility.get_test_by_id(item_id).overwrite_attributes(name, suite_name, parameters, codeowners) - - @staticmethod - @_catch_and_log_exceptions - def finish_test(finish_args: "InternalTest.FinishTestArgs") -> None: - log.debug("Finishing test %s with status %s", finish_args.test_id, finish_args.status) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - - CIVisibility.get_test_by_id(finish_args.test_id).finish_test( - finish_args.status, finish_args.skip_reason, finish_args.exc_info - ) From 0d9d8d84edf268e68a743e5350a01e6ef107c5c9 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Fri, 13 Jun 2025 11:09:17 +0200 Subject: [PATCH 26/58] =?UTF-8?q?=F0=9F=94=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ddtrace/contrib/internal/pytest/_plugin_v2.py | 8 +- ddtrace/ext/test_visibility/_decorators.py | 24 +++++ ddtrace/ext/test_visibility/_utils.py | 51 ++-------- ddtrace/ext/test_visibility/api.py | 99 +++++++------------ ddtrace/ext/test_visibility/status.py | 33 +++++++ ddtrace/internal/ci_visibility/api/_base.py | 10 +- .../internal/ci_visibility/api/_session.py | 2 +- ddtrace/internal/ci_visibility/api/_suite.py | 4 +- ddtrace/internal/ci_visibility/api/_test.py | 18 ++-- ddtrace/internal/ci_visibility/recorder.py | 6 +- .../internal/test_visibility/_atr_mixins.py | 20 ++-- .../test_visibility/_attempt_to_fix_mixins.py | 30 +++--- .../test_visibility/_benchmark_mixin.py | 2 +- .../internal/test_visibility/_efd_mixins.py | 9 +- .../internal/test_visibility/_itr_mixins.py | 2 +- ddtrace/internal/test_visibility/api.py | 34 ++++--- 16 files changed, 177 insertions(+), 175 deletions(-) create mode 100644 ddtrace/ext/test_visibility/_decorators.py create mode 100644 ddtrace/ext/test_visibility/status.py diff --git a/ddtrace/contrib/internal/pytest/_plugin_v2.py b/ddtrace/contrib/internal/pytest/_plugin_v2.py index 03d431349fe..821a89d69d4 100644 --- a/ddtrace/contrib/internal/pytest/_plugin_v2.py +++ b/ddtrace/contrib/internal/pytest/_plugin_v2.py @@ -158,8 +158,8 @@ def _handle_test_management(item, test_id): """Add a user property to identify quarantined tests, and mark them for skipping if quarantine is enabled in skipping mode. """ - is_quarantined = InternalTest.is_quarantined(test_id) - is_disabled = InternalTest.is_disabled(test_id) + is_quarantined = InternalTest.is_quarantined_test(test_id) + is_disabled = InternalTest.is_disabled_test(test_id) is_attempt_to_fix = InternalTest.is_attempt_to_fix(test_id) if is_quarantined and asbool(os.getenv("_DD_TEST_SKIP_QUARANTINED_TESTS")): @@ -520,8 +520,8 @@ def _pytest_run_one_test(item, nextitem): reports_dict = {report.when: report for report in reports} test_id = _get_test_id_from_item(item) - is_quarantined = InternalTest.is_quarantined(test_id) - is_disabled = InternalTest.is_disabled(test_id) + is_quarantined = InternalTest.is_quarantined_test(test_id) + is_disabled = InternalTest.is_disabled_test(test_id) is_attempt_to_fix = InternalTest.is_attempt_to_fix(test_id) setup_or_teardown_failed = False diff --git a/ddtrace/ext/test_visibility/_decorators.py b/ddtrace/ext/test_visibility/_decorators.py new file mode 100644 index 00000000000..055bd5f9e29 --- /dev/null +++ b/ddtrace/ext/test_visibility/_decorators.py @@ -0,0 +1,24 @@ +from functools import wraps + +from ddtrace.internal.logger import get_logger + + +log = get_logger(__name__) + + +def _catch_and_log_exceptions(func): + """This decorator is meant to be used around all methods of the Test Visibility classes. + + It accepts an optional parameter to allow it to be used on functions and methods. + + No uncaught errors should ever reach the integration-side, and potentially cause crashes. + """ + + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception: # noqa: E722 + log.error("Uncaught exception occurred while calling %s", func.__name__, exc_info=True) + + return wrapper diff --git a/ddtrace/ext/test_visibility/_utils.py b/ddtrace/ext/test_visibility/_utils.py index d9ecf823d14..ec2659d29f4 100644 --- a/ddtrace/ext/test_visibility/_utils.py +++ b/ddtrace/ext/test_visibility/_utils.py @@ -1,70 +1,35 @@ -from functools import wraps from typing import Any from typing import Dict from typing import List from ddtrace.ext.test_visibility._test_visibility_base import TestVisibilityItemId -from ddtrace.internal.logger import get_logger -log = get_logger(__name__) - - -def _catch_and_log_exceptions(func): - """This decorator is meant to be used around all methods of the Test Visibility classes. - - It accepts an optional parameter to allow it to be used on functions and methods. - - No uncaught errors should ever reach the integration-side, and potentially cause crashes. - """ - - @wraps(func) - def wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except Exception: # noqa: E722 - log.error("Uncaught exception occurred while calling %s", func.__name__, exc_info=True) - - return wrapper - - -def _get_item_tag(item_id: TestVisibilityItemId, name: str) -> Any: - # Lazy import to avoid circular dependency +def _get_item_tag(item_id: TestVisibilityItemId, tag_name: str) -> Any: from ddtrace.internal.ci_visibility.recorder import CIVisibility - - return CIVisibility.get_item_by_id(item_id).get_tag(name) + return CIVisibility.get_item_by_id(item_id).get_tag(tag_name) -def _set_item_tag(item_id: TestVisibilityItemId, name: str, value: Any) -> None: - # Lazy import to avoid circular dependency +def _set_item_tag(item_id: TestVisibilityItemId, tag_name: str, tag_value: Any) -> None: from ddtrace.internal.ci_visibility.recorder import CIVisibility - - CIVisibility.get_item_by_id(item_id).set_tag(name, value) + CIVisibility.get_item_by_id(item_id).set_tag(tag_name, tag_value) def _set_item_tags(item_id: TestVisibilityItemId, tags: Dict[str, Any]) -> None: - # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import CIVisibility - CIVisibility.get_item_by_id(item_id).set_tags(tags) -def _delete_item_tag(item_id: TestVisibilityItemId, name: str) -> None: - # Lazy import to avoid circular dependency +def _delete_item_tag(item_id: TestVisibilityItemId, tag_name: str) -> None: from ddtrace.internal.ci_visibility.recorder import CIVisibility + CIVisibility.get_item_by_id(item_id).delete_tag(tag_name) - CIVisibility.get_item_by_id(item_id).delete_tag(name) - -def _delete_item_tags(item_id: TestVisibilityItemId, names: List[str], recurse: bool = False) -> None: - # Lazy import to avoid circular dependency +def _delete_item_tags(item_id: TestVisibilityItemId, tag_names: List[str]) -> None: from ddtrace.internal.ci_visibility.recorder import CIVisibility - - CIVisibility.get_item_by_id(item_id).delete_tags(names) + CIVisibility.get_item_by_id(item_id).delete_tags(tag_names) def _is_item_finished(item_id: TestVisibilityItemId) -> bool: - # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_item_by_id(item_id).is_finished() diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index cd52c346241..812aad57487 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -12,33 +12,30 @@ All types and methods for interacting with the API are provided and documented in this file. """ -import dataclasses from enum import Enum from pathlib import Path -from types import TracebackType from typing import Any from typing import Dict from typing import List -from typing import NamedTuple from typing import Optional -from typing import Type from ddtrace._trace.context import Context -from ddtrace.ext.test import Status as _TestStatus +from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions from ddtrace.ext.test_visibility._item_ids import TestId from ddtrace.ext.test_visibility._item_ids import TestModuleId from ddtrace.ext.test_visibility._item_ids import TestSuiteId from ddtrace.ext.test_visibility._test_visibility_base import TestSessionId -from ddtrace.ext.test_visibility._test_visibility_base import TestSourceFileInfoBase from ddtrace.ext.test_visibility._test_visibility_base import TestVisibilityItemId from ddtrace.ext.test_visibility._test_visibility_base import _TestVisibilityAPIBase -from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions from ddtrace.ext.test_visibility._utils import _delete_item_tag from ddtrace.ext.test_visibility._utils import _delete_item_tags from ddtrace.ext.test_visibility._utils import _get_item_tag from ddtrace.ext.test_visibility._utils import _is_item_finished from ddtrace.ext.test_visibility._utils import _set_item_tag from ddtrace.ext.test_visibility._utils import _set_item_tags +from ddtrace.ext.test_visibility.status import TestExcInfo +from ddtrace.ext.test_visibility.status import TestSourceFileInfo +from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.internal.logger import get_logger as _get_logger @@ -48,15 +45,6 @@ import ddtrace._trace.trace_handlers # noqa: F401, E402 -class TestStatus(Enum): - __test__ = False - PASS = _TestStatus.PASS.value - FAIL = _TestStatus.FAIL.value - SKIP = _TestStatus.SKIP.value - XFAIL = _TestStatus.XFAIL.value - XPASS = _TestStatus.XPASS.value - - DEFAULT_SESSION_NAME = "test_visibility_session" @@ -67,46 +55,30 @@ class DEFAULT_OPERATION_NAMES(Enum): TEST = "test_visibility.test" -@dataclasses.dataclass(frozen=True) -class TestSourceFileInfo(TestSourceFileInfoBase): - path: Path - start_line: Optional[int] = None - end_line: Optional[int] = None - - -@dataclasses.dataclass(frozen=True) -class TestExcInfo: - __test__ = False - exc_type: Type[BaseException] - exc_value: BaseException - exc_traceback: TracebackType - - @_catch_and_log_exceptions def enable_test_visibility(config: Optional[Any] = None): - log.debug("Enabling Test Visibility with config: %s", config) from ddtrace.internal.ci_visibility.recorder import CIVisibility + log.debug("Enabling Test Visibility with config: %s", config) CIVisibility.enable(config=config) - if not is_test_visibility_enabled(): + if not CIVisibility.enabled: log.warning("Failed to enable Test Visibility") @_catch_and_log_exceptions def is_test_visibility_enabled(): from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.enabled @_catch_and_log_exceptions def disable_test_visibility(): - log.debug("Disabling Test Visibility") from ddtrace.internal.ci_visibility.recorder import CIVisibility + log.debug("Disabling Test Visibility") CIVisibility.disable() - if is_test_visibility_enabled(): + if CIVisibility.enabled: log.warning("Failed to disable Test Visibility") @@ -173,16 +145,15 @@ def discover( @_catch_and_log_exceptions def start(distributed_children: bool = False, context: Optional[Context] = None): from ddtrace.internal.ci_visibility.recorder import CIVisibility - log.debug("Starting session") session = CIVisibility.get_session() session.start(context) if distributed_children: session.set_distributed_children() - class FinishArgs(NamedTuple): - force_finish_children: bool - override_status: Optional[TestStatus] + # class FinishArgs(NamedTuple): + # force_finish_children: bool + # override_status: Optional[TestStatus] @staticmethod @_catch_and_log_exceptions @@ -191,7 +162,6 @@ def finish( override_status: Optional[TestStatus] = None, ): from ddtrace.internal.ci_visibility.recorder import CIVisibility - log.debug("Finishing session, force_finish_session_modules: %s", force_finish_children) session = CIVisibility.get_session() @@ -219,10 +189,10 @@ def delete_tags(tag_names: List[str]): class TestModule(TestBase): - class FinishArgs(NamedTuple): - module_id: TestModuleId - override_status: Optional[TestStatus] = None - force_finish_children: bool = False + # class FinishArgs(NamedTuple): + # module_id: TestModuleId + # override_status: Optional[TestStatus] = None + # force_finish_children: bool = False @staticmethod @_catch_and_log_exceptions @@ -246,7 +216,6 @@ def discover(item_id: TestModuleId, module_path: Optional[Path] = None): @_catch_and_log_exceptions def start(item_id: TestModuleId): from ddtrace.internal.ci_visibility.recorder import CIVisibility - log.debug("Starting module %s", item_id) CIVisibility.get_module_by_id(item_id).start() @@ -257,13 +226,13 @@ def finish( override_status: Optional[TestStatus] = None, force_finish_children: bool = False, ): + from ddtrace.internal.ci_visibility.recorder import CIVisibility log.debug( "Finishing module %s, override_status: %s, force_finish_suites: %s", item_id, override_status, force_finish_children, ) - from ddtrace.internal.ci_visibility.recorder import CIVisibility CIVisibility.get_module_by_id(item_id).finish() @@ -276,10 +245,10 @@ def discover( codeowners: Optional[List[str]] = None, source_file_info: Optional[TestSourceFileInfo] = None, ): + from ddtrace.internal.ci_visibility.recorder import CIVisibility """Registers a test suite with the Test Visibility service.""" log.debug("Registering suite %s, source: %s", item_id, source_file_info) from ddtrace.internal.ci_visibility.api._suite import TestVisibilitySuite - from ddtrace.internal.ci_visibility.recorder import CIVisibility module = CIVisibility.get_module_by_id(item_id.parent_id) @@ -297,14 +266,13 @@ def discover( @_catch_and_log_exceptions def start(item_id: TestSuiteId): from ddtrace.internal.ci_visibility.recorder import CIVisibility - log.debug("Starting suite %s", item_id) CIVisibility.get_suite_by_id(item_id).start() - class FinishArgs(NamedTuple): - suite_id: TestSuiteId - force_finish_children: bool = False - override_status: Optional[TestStatus] = None + # class FinishArgs(NamedTuple): + # suite_id: TestSuiteId + # force_finish_children: bool = False + # override_status: Optional[TestStatus] = None @staticmethod @_catch_and_log_exceptions @@ -313,13 +281,13 @@ def finish( force_finish_children: bool = False, override_status: Optional[TestStatus] = None, ): + from ddtrace.internal.ci_visibility.recorder import CIVisibility log.debug( "Finishing suite %s, override_status: %s, force_finish_children: %s", item_id, force_finish_children, override_status, ) - from ddtrace.internal.ci_visibility.recorder import CIVisibility CIVisibility.get_suite_by_id(item_id).finish(force_finish_children, override_status) @@ -334,6 +302,10 @@ def discover( resource: Optional[str] = None, ): """Registers a test with the Test Visibility service.""" + from ddtrace.internal.ci_visibility._api_client import TestProperties + from ddtrace.internal.ci_visibility.api._test import TestVisibilityTest + from ddtrace.internal.ci_visibility.recorder import CIVisibility + log.debug( "Discovering test %s, codeowners: %s, source file: %s, resource: %s", item_id, @@ -341,9 +313,6 @@ def discover( source_file_info, resource, ) - from ddtrace.internal.ci_visibility._api_client import TestProperties - from ddtrace.internal.ci_visibility.api._test import TestVisibilityTest - from ddtrace.internal.ci_visibility.recorder import CIVisibility log.debug("Handling discovery for test %s", item_id) suite = CIVisibility.get_suite_by_id(item_id.parent_id) @@ -382,16 +351,16 @@ def discover( @staticmethod @_catch_and_log_exceptions def start(item_id: TestId): - log.debug("Starting test %s", item_id) from ddtrace.internal.ci_visibility.recorder import CIVisibility + log.debug("Starting test %s", item_id) CIVisibility.get_test_by_id(item_id).start() - class FinishArgs(NamedTuple): - test_id: TestId - status: TestStatus - skip_reason: Optional[str] = None - exc_info: Optional[TestExcInfo] = None + # class FinishArgs(NamedTuple): + # test_id: TestId + # status: TestStatus + # skip_reason: Optional[str] = None + # exc_info: Optional[TestExcInfo] = None @staticmethod @_catch_and_log_exceptions @@ -401,6 +370,7 @@ def finish( skip_reason: Optional[str] = None, exc_info: Optional[TestExcInfo] = None, ): + from ddtrace.internal.ci_visibility.recorder import CIVisibility log.debug( "Finishing test %s, status: %s, skip_reason: %s, exc_info: %s", item_id, @@ -408,15 +378,14 @@ def finish( skip_reason, exc_info, ) - from ddtrace.internal.ci_visibility.recorder import CIVisibility CIVisibility.get_test_by_id(item_id).finish_test(status, skip_reason, exc_info) @staticmethod @_catch_and_log_exceptions def set_parameters(item_id: TestId, params: str): - log.debug("Setting test %s parameters to %s", item_id, params) from ddtrace.internal.ci_visibility.recorder import CIVisibility + log.debug("Setting test %s parameters to %s", item_id, params) CIVisibility.get_test_by_id(item_id).set_parameters(params) diff --git a/ddtrace/ext/test_visibility/status.py b/ddtrace/ext/test_visibility/status.py new file mode 100644 index 00000000000..0b9ffaf4240 --- /dev/null +++ b/ddtrace/ext/test_visibility/status.py @@ -0,0 +1,33 @@ +import dataclasses +from enum import Enum +from pathlib import Path +from types import TracebackType +from typing import Optional +from typing import Type + +from ddtrace.ext.test import Status as _TestStatus +from ddtrace.ext.test_visibility._test_visibility_base import TestSourceFileInfoBase + + +class TestStatus(Enum): + __test__ = False + PASS = _TestStatus.PASS.value + FAIL = _TestStatus.FAIL.value + SKIP = _TestStatus.SKIP.value + XFAIL = _TestStatus.XFAIL.value + XPASS = _TestStatus.XPASS.value + + +@dataclasses.dataclass(frozen=True) +class TestExcInfo: + __test__ = False + exc_type: Type[BaseException] + exc_value: BaseException + exc_traceback: TracebackType + + +@dataclasses.dataclass(frozen=True) +class TestSourceFileInfo(TestSourceFileInfoBase): + path: Path + start_line: Optional[int] = None + end_line: Optional[int] = None diff --git a/ddtrace/internal/ci_visibility/api/_base.py b/ddtrace/internal/ci_visibility/api/_base.py index dad5cf56e0b..bfdeb8ba2c4 100644 --- a/ddtrace/internal/ci_visibility/api/_base.py +++ b/ddtrace/internal/ci_visibility/api/_base.py @@ -21,8 +21,8 @@ from ddtrace.ext.test_visibility._item_ids import TestId from ddtrace.ext.test_visibility._item_ids import TestModuleId from ddtrace.ext.test_visibility._item_ids import TestSuiteId -from ddtrace.ext.test_visibility.api import TestSourceFileInfo -from ddtrace.ext.test_visibility.api import TestStatus +from ddtrace.ext.test_visibility.status import TestSourceFileInfo +from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings from ddtrace.internal.ci_visibility._api_client import TestManagementSettings from ddtrace.internal.ci_visibility.api._coverage_data import TestVisibilityCoverageData @@ -156,7 +156,7 @@ def __init__( # General purpose attributes not used by all item types self._codeowners: Optional[List[str]] = [] - self._source_file_info: Optional[TestSourceFileInfo] = None + self._source_file_info: Optional["TestSourceFileInfo"] = None self._coverage_data: Optional[TestVisibilityCoverageData] = None def __repr__(self) -> str: @@ -312,11 +312,11 @@ def _set_span_tags(self): pass @property - def _source_file_info(self) -> Optional[TestSourceFileInfo]: + def _source_file_info(self) -> Optional["TestSourceFileInfo"]: return self.__source_file_info @_source_file_info.setter - def _source_file_info(self, source_file_info_value: Optional[TestSourceFileInfo] = None): + def _source_file_info(self, source_file_info_value: Optional["TestSourceFileInfo"] = None): """This checks that filepaths are absolute when setting source file info""" self.__source_file_info = None # Default value until source_file_info is validated diff --git a/ddtrace/internal/ci_visibility/api/_session.py b/ddtrace/internal/ci_visibility/api/_session.py index 35ef85001a0..7e368f0cf6d 100644 --- a/ddtrace/internal/ci_visibility/api/_session.py +++ b/ddtrace/internal/ci_visibility/api/_session.py @@ -5,7 +5,7 @@ from ddtrace.ext import test from ddtrace.ext.test_visibility import ITR_SKIPPING_LEVEL from ddtrace.ext.test_visibility._item_ids import TestModuleId -from ddtrace.ext.test_visibility.api import TestStatus +from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.internal.ci_visibility.api._base import TestVisibilityParentItem from ddtrace.internal.ci_visibility.api._base import TestVisibilitySessionSettings from ddtrace.internal.ci_visibility.api._module import TestVisibilityModule diff --git a/ddtrace/internal/ci_visibility/api/_suite.py b/ddtrace/internal/ci_visibility/api/_suite.py index ba24f82c05c..6e971a52b69 100644 --- a/ddtrace/internal/ci_visibility/api/_suite.py +++ b/ddtrace/internal/ci_visibility/api/_suite.py @@ -6,8 +6,8 @@ from ddtrace.ext import test from ddtrace.ext.test_visibility._item_ids import TestId from ddtrace.ext.test_visibility._item_ids import TestSuiteId -from ddtrace.ext.test_visibility.api import TestSourceFileInfo -from ddtrace.ext.test_visibility.api import TestStatus +from ddtrace.ext.test_visibility.status import TestSourceFileInfo +from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.internal.ci_visibility.api._base import TestVisibilityChildItem from ddtrace.internal.ci_visibility.api._base import TestVisibilityParentItem from ddtrace.internal.ci_visibility.api._base import TestVisibilitySessionSettings diff --git a/ddtrace/internal/ci_visibility/api/_test.py b/ddtrace/internal/ci_visibility/api/_test.py index aa270c15055..f37025922af 100644 --- a/ddtrace/internal/ci_visibility/api/_test.py +++ b/ddtrace/internal/ci_visibility/api/_test.py @@ -10,9 +10,9 @@ from ddtrace.ext import test from ddtrace.ext.test_visibility import ITR_SKIPPING_LEVEL from ddtrace.ext.test_visibility._item_ids import TestId -from ddtrace.ext.test_visibility.api import TestExcInfo -from ddtrace.ext.test_visibility.api import TestSourceFileInfo -from ddtrace.ext.test_visibility.api import TestStatus +from ddtrace.ext.test_visibility.status import TestExcInfo +from ddtrace.ext.test_visibility.status import TestSourceFileInfo +from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.internal.ci_visibility.api._base import SPECIAL_STATUS from ddtrace.internal.ci_visibility.api._base import TestVisibilityChildItem from ddtrace.internal.ci_visibility.api._base import TestVisibilityItemBase @@ -190,18 +190,18 @@ def _telemetry_record_event_finished(self): def finish_test( self, status: Optional[TestStatus] = None, - reason: Optional[str] = None, + skip_reason: Optional[str] = None, exc_info: Optional[TestExcInfo] = None, override_finish_time: Optional[float] = None, ) -> None: - log.debug("Test Visibility: finishing %s, with status: %s, reason: %s", self, status, reason) + log.debug("Test Visibility: finishing %s, with status: %s, skip_reason: %s", self, status, skip_reason) self.set_tag(test.TYPE, SpanTypes.TEST) if status is not None: self.set_status(status) - if reason is not None: - self.set_tag(test.SKIP_REASON, reason) + if skip_reason is not None: + self.set_tag(test.SKIP_REASON, skip_reason) if exc_info is not None: self._exc_info = exc_info @@ -376,8 +376,8 @@ def efd_finish_retry( self, retry_number: int, status: TestStatus, - exc_info: Optional[TestExcInfo] = None, skip_reason: Optional[str] = None, + exc_info: Optional[TestExcInfo] = None, ) -> None: # TODO: use skip_reason for something retry_test = self._efd_get_retry_test(retry_number) @@ -385,7 +385,7 @@ def efd_finish_retry( if status is not None: retry_test.set_status(status) - retry_test.finish_test(status, exc_info=exc_info) + retry_test.finish_test(status=status, skip_reason=skip_reason, exc_info=exc_info) def efd_get_final_status(self) -> EFDTestStatus: status_counts: Dict[TestStatus, int] = { diff --git a/ddtrace/internal/ci_visibility/recorder.py b/ddtrace/internal/ci_visibility/recorder.py index 057f548d497..4f207ce258a 100644 --- a/ddtrace/internal/ci_visibility/recorder.py +++ b/ddtrace/internal/ci_visibility/recorder.py @@ -17,11 +17,11 @@ from ddtrace.ext import ci from ddtrace.ext import test from ddtrace.ext.test_visibility import ITR_SKIPPING_LEVEL +from ddtrace.ext.test_visibility._item_ids import TestId +from ddtrace.ext.test_visibility._item_ids import TestModuleId +from ddtrace.ext.test_visibility._item_ids import TestSuiteId from ddtrace.ext.test_visibility._test_visibility_base import TestSessionId from ddtrace.ext.test_visibility._test_visibility_base import TestVisibilityItemId -from ddtrace.ext.test_visibility.api import TestId -from ddtrace.ext.test_visibility.api import TestModuleId -from ddtrace.ext.test_visibility.api import TestSuiteId from ddtrace.internal import agent from ddtrace.internal import atexit from ddtrace.internal import telemetry diff --git a/ddtrace/internal/test_visibility/_atr_mixins.py b/ddtrace/internal/test_visibility/_atr_mixins.py index c7b6c4bda04..6959a6eac27 100644 --- a/ddtrace/internal/test_visibility/_atr_mixins.py +++ b/ddtrace/internal/test_visibility/_atr_mixins.py @@ -1,8 +1,10 @@ import dataclasses import typing as t -from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions +from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions import ddtrace.ext.test_visibility.api as ext_api +from ddtrace.ext.test_visibility.status import TestStatus +from ddtrace.ext.test_visibility.status import TestExcInfo from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId @@ -38,11 +40,11 @@ def atr_has_failed_tests() -> bool: class ATRTestMixin: - class ATRRetryFinishArgs(t.NamedTuple): - test_id: InternalTestId - retry_number: int - status: ext_api.TestStatus - exc_info: t.Optional[ext_api.TestExcInfo] + # class ATRRetryFinishArgs(t.NamedTuple): + # test_id: InternalTestId + # retry_number: int + # status: ext_api.TestStatus + # exc_info: t.Optional[ext_api.TestExcInfo] @staticmethod @_catch_and_log_exceptions @@ -77,8 +79,8 @@ def atr_start_retry(test_id: InternalTestId, retry_number: int) -> None: def atr_finish_retry( test_id: InternalTestId, retry_number: int, - status: ext_api.TestStatus, - exc_info: t.Optional[ext_api.TestExcInfo] = None, + status: TestStatus, + exc_info: t.Optional[TestExcInfo] = None, ) -> None: log.debug("Finishing ATR retry %s for test %s", retry_number, test_id) # Lazy import to avoid circular dependency @@ -90,7 +92,7 @@ def atr_finish_retry( @staticmethod @_catch_and_log_exceptions - def atr_get_final_status(test_id: InternalTestId) -> ext_api.TestStatus: + def atr_get_final_status(test_id: InternalTestId) -> TestStatus: log.debug("Getting ATR final status for test %s", test_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import CIVisibility diff --git a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py index e727436ac88..8f18109ecc0 100644 --- a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py +++ b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py @@ -1,7 +1,8 @@ import typing as t -from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions -import ddtrace.ext.test_visibility.api as ext_api +from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions +from ddtrace.ext.test_visibility.status import TestStatus +from ddtrace.ext.test_visibility.status import TestExcInfo from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId @@ -21,11 +22,11 @@ def attempt_to_fix_has_failed_tests() -> bool: class AttemptToFixTestMixin: - class AttemptToFixRetryFinishArgs(t.NamedTuple): - test_id: InternalTestId - retry_number: int - status: ext_api.TestStatus - exc_info: t.Optional[ext_api.TestExcInfo] + # class AttemptToFixRetryFinishArgs(t.NamedTuple): + # test_id: InternalTestId + # retry_number: int + # status: ext_api.TestStatus + # exc_info: t.Optional[ext_api.TestExcInfo] @staticmethod @_catch_and_log_exceptions @@ -57,18 +58,23 @@ def attempt_to_fix_start_retry(test_id: InternalTestId, retry_number: int) -> No @staticmethod @_catch_and_log_exceptions - def attempt_to_fix_finish_retry(finish_args: "AttemptToFixTestMixin.AttemptToFixRetryFinishArgs") -> None: - log.debug("Finishing attempt to fix retry %s for test %s", finish_args.retry_number, finish_args.test_id) + def attempt_to_fix_finish_retry( + test_id: InternalTestId, + retry_number: int, + status: TestStatus, + exc_info: t.Optional[TestExcInfo], + ) -> None: + log.debug("Finishing attempt to fix retry %s for test %s", retry_number, test_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import CIVisibility - CIVisibility.get_test_by_id(finish_args.test_id).attempt_to_fix_finish_retry( - finish_args.retry_number, finish_args.status, finish_args.exc_info + CIVisibility.get_test_by_id(test_id).attempt_to_fix_finish_retry( + retry_number, status, exc_info ) @staticmethod @_catch_and_log_exceptions - def attempt_to_fix_get_final_status(test_id: InternalTestId) -> ext_api.TestStatus: + def attempt_to_fix_get_final_status(test_id: InternalTestId) -> TestStatus: log.debug("Getting attempt to fix final status for test %s", test_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import CIVisibility diff --git a/ddtrace/internal/test_visibility/_benchmark_mixin.py b/ddtrace/internal/test_visibility/_benchmark_mixin.py index c41d45b10a5..1f8f4d07c8c 100644 --- a/ddtrace/internal/test_visibility/_benchmark_mixin.py +++ b/ddtrace/internal/test_visibility/_benchmark_mixin.py @@ -1,6 +1,6 @@ import typing as t -from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions +from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions from ddtrace.internal import core from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId diff --git a/ddtrace/internal/test_visibility/_efd_mixins.py b/ddtrace/internal/test_visibility/_efd_mixins.py index a75a7e177e9..ab2d8f6bb53 100644 --- a/ddtrace/internal/test_visibility/_efd_mixins.py +++ b/ddtrace/internal/test_visibility/_efd_mixins.py @@ -1,8 +1,9 @@ from enum import Enum import typing as t -from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions -import ddtrace.ext.test_visibility.api as ext_api +from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions +from ddtrace.ext.test_visibility.status import TestStatus +from ddtrace.ext.test_visibility.status import TestExcInfo from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId @@ -80,9 +81,9 @@ def efd_start_retry(test_id: InternalTestId, retry_number: int) -> None: def efd_finish_retry( item_id: InternalTestId, retry_number: int, - status: ext_api.TestStatus, + status: TestStatus, skip_reason: t.Optional[str] = None, - exc_info: t.Optional[ext_api.TestExcInfo] = None, + exc_info: t.Optional[TestExcInfo] = None, ): from ddtrace.internal.ci_visibility.recorder import CIVisibility diff --git a/ddtrace/internal/test_visibility/_itr_mixins.py b/ddtrace/internal/test_visibility/_itr_mixins.py index 3abeffb412d..a165271f1cf 100644 --- a/ddtrace/internal/test_visibility/_itr_mixins.py +++ b/ddtrace/internal/test_visibility/_itr_mixins.py @@ -2,7 +2,7 @@ import typing as t from ddtrace.ext.test_visibility import api as ext_api -from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions +from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions from ddtrace.internal.ci_visibility.errors import CIVisibilityError from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId diff --git a/ddtrace/internal/test_visibility/api.py b/ddtrace/internal/test_visibility/api.py index d6bcef49dba..8cb25804a6b 100644 --- a/ddtrace/internal/test_visibility/api.py +++ b/ddtrace/internal/test_visibility/api.py @@ -3,11 +3,11 @@ from typing import NamedTuple from ddtrace.ext.test_visibility import api as ext_api +from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions from ddtrace.ext.test_visibility._test_visibility_base import TestSessionId -from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions from ddtrace.ext.test_visibility._utils import _is_item_finished -from ddtrace.ext.test_visibility.api import TestExcInfo -from ddtrace.ext.test_visibility.api import TestStatus +from ddtrace.ext.test_visibility.status import TestExcInfo +from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.internal.codeowners import Codeowners as _Codeowners from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._atr_mixins import ATRSessionMixin @@ -182,34 +182,36 @@ class InternalTestSuite(ext_api.TestSuite, InternalTestBase, ITRMixin): class InternalTest( ext_api.Test, InternalTestBase, ITRMixin, EFDTestMixin, ATRTestMixin, AttemptToFixTestMixin, BenchmarkTestMixin ): - class FinishArgs(NamedTuple): - """InternalTest allows finishing with an overridden finish time (for EFD and other retry purposes)""" + # class FinishArgs(NamedTuple): + # """InternalTest allows finishing with an overridden finish time (for EFD and other retry purposes)""" - test_id: InternalTestId - status: t.Optional[TestStatus] = None - skip_reason: t.Optional[str] = None - exc_info: t.Optional[TestExcInfo] = None - override_finish_time: t.Optional[float] = None + # test_id: InternalTestId + # status: t.Optional[TestStatus] = None + # skip_reason: t.Optional[str] = None + # exc_info: t.Optional[TestExcInfo] = None + # override_finish_time: t.Optional[float] = None @staticmethod @_catch_and_log_exceptions def finish( item_id: InternalTestId, status: t.Optional[ext_api.TestStatus] = None, - reason: t.Optional[str] = None, + skip_reason: t.Optional[str] = None, exc_info: t.Optional[ext_api.TestExcInfo] = None, override_finish_time: t.Optional[float] = None, ): # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import CIVisibility - log.debug("Finishing test with status: %s, reason: %s", status, reason) + log.debug("Finishing test with status: %s, skip_reason: %s", status, skip_reason) final_status = status if status is not None else ext_api.TestStatus.PASS - CIVisibility.get_test_by_id(item_id).finish_test(final_status, reason, exc_info) + CIVisibility.get_test_by_id(item_id).finish_test( + status=final_status, skip_reason=skip_reason, exc_info=exc_info + ) @staticmethod @_catch_and_log_exceptions - def is_new(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: + def is_new_test(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: log.debug("Checking if test %s is new", test_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import CIVisibility @@ -218,7 +220,7 @@ def is_new(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: @staticmethod @_catch_and_log_exceptions - def is_quarantined(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: + def is_quarantined_test(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: log.debug("Checking if test %s is quarantined", test_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import CIVisibility @@ -227,7 +229,7 @@ def is_quarantined(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: @staticmethod @_catch_and_log_exceptions - def is_disabled(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: + def is_disabled_test(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: log.debug("Checking if test %s is disabled", test_id) # Lazy import to avoid circular dependency from ddtrace.internal.ci_visibility.recorder import CIVisibility From eb366d9f2d68c32cf2c88042bad360196ff9b752 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Fri, 13 Jun 2025 11:39:20 +0200 Subject: [PATCH 27/58] registry --- ddtrace/ext/test_visibility/_utils.py | 19 ++--- ddtrace/ext/test_visibility/api.py | 66 ++++++--------- ddtrace/internal/ci_visibility/recorder.py | 10 +++ .../ci_visibility/service_registry.py | 48 +++++++++++ .../internal/test_visibility/_atr_mixins.py | 42 ++-------- .../test_visibility/_attempt_to_fix_mixins.py | 30 ++----- .../internal/test_visibility/_efd_mixins.py | 33 ++------ .../internal/test_visibility/_itr_mixins.py | 39 +++------ ddtrace/internal/test_visibility/_utils.py | 6 +- ddtrace/internal/test_visibility/api.py | 83 +++++-------------- 10 files changed, 149 insertions(+), 227 deletions(-) create mode 100644 ddtrace/internal/ci_visibility/service_registry.py diff --git a/ddtrace/ext/test_visibility/_utils.py b/ddtrace/ext/test_visibility/_utils.py index ec2659d29f4..b0bdf4f150d 100644 --- a/ddtrace/ext/test_visibility/_utils.py +++ b/ddtrace/ext/test_visibility/_utils.py @@ -3,33 +3,28 @@ from typing import List from ddtrace.ext.test_visibility._test_visibility_base import TestVisibilityItemId +from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service def _get_item_tag(item_id: TestVisibilityItemId, tag_name: str) -> Any: - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_item_by_id(item_id).get_tag(tag_name) + return require_ci_visibility_service().get_item_by_id(item_id).get_tag(tag_name) def _set_item_tag(item_id: TestVisibilityItemId, tag_name: str, tag_value: Any) -> None: - from ddtrace.internal.ci_visibility.recorder import CIVisibility - CIVisibility.get_item_by_id(item_id).set_tag(tag_name, tag_value) + require_ci_visibility_service().get_item_by_id(item_id).set_tag(tag_name, tag_value) def _set_item_tags(item_id: TestVisibilityItemId, tags: Dict[str, Any]) -> None: - from ddtrace.internal.ci_visibility.recorder import CIVisibility - CIVisibility.get_item_by_id(item_id).set_tags(tags) + require_ci_visibility_service().get_item_by_id(item_id).set_tags(tags) def _delete_item_tag(item_id: TestVisibilityItemId, tag_name: str) -> None: - from ddtrace.internal.ci_visibility.recorder import CIVisibility - CIVisibility.get_item_by_id(item_id).delete_tag(tag_name) + require_ci_visibility_service().get_item_by_id(item_id).delete_tag(tag_name) def _delete_item_tags(item_id: TestVisibilityItemId, tag_names: List[str]) -> None: - from ddtrace.internal.ci_visibility.recorder import CIVisibility - CIVisibility.get_item_by_id(item_id).delete_tags(tag_names) + require_ci_visibility_service().get_item_by_id(item_id).delete_tags(tag_names) def _is_item_finished(item_id: TestVisibilityItemId) -> bool: - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_item_by_id(item_id).is_finished() + return require_ci_visibility_service().get_item_by_id(item_id).is_finished() diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 812aad57487..bbf70e93ace 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -37,6 +37,7 @@ from ddtrace.ext.test_visibility.status import TestSourceFileInfo from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.internal.logger import get_logger as _get_logger +from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service log = _get_logger(__name__) @@ -57,28 +58,24 @@ class DEFAULT_OPERATION_NAMES(Enum): @_catch_and_log_exceptions def enable_test_visibility(config: Optional[Any] = None): - from ddtrace.internal.ci_visibility.recorder import CIVisibility log.debug("Enabling Test Visibility with config: %s", config) + require_ci_visibility_service().enable(config=config) - CIVisibility.enable(config=config) - - if not CIVisibility.enabled: + if not require_ci_visibility_service().enabled: log.warning("Failed to enable Test Visibility") @_catch_and_log_exceptions def is_test_visibility_enabled(): - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.enabled + return require_ci_visibility_service().enabled @_catch_and_log_exceptions def disable_test_visibility(): - from ddtrace.internal.ci_visibility.recorder import CIVisibility log.debug("Disabling Test Visibility") - CIVisibility.disable() - if CIVisibility.enabled: + require_ci_visibility_service().disable() + if require_ci_visibility_service().enabled: log.warning("Failed to disable Test Visibility") @@ -144,9 +141,8 @@ def discover( @staticmethod @_catch_and_log_exceptions def start(distributed_children: bool = False, context: Optional[Context] = None): - from ddtrace.internal.ci_visibility.recorder import CIVisibility log.debug("Starting session") - session = CIVisibility.get_session() + session = require_ci_visibility_service().get_session() session.start(context) if distributed_children: session.set_distributed_children() @@ -161,10 +157,9 @@ def finish( force_finish_children: bool = False, override_status: Optional[TestStatus] = None, ): - from ddtrace.internal.ci_visibility.recorder import CIVisibility log.debug("Finishing session, force_finish_session_modules: %s", force_finish_children) - session = CIVisibility.get_session() + session = require_ci_visibility_service().get_session() session.finish(force_finish_children, override_status) @staticmethod @@ -198,16 +193,14 @@ class TestModule(TestBase): @_catch_and_log_exceptions def discover(item_id: TestModuleId, module_path: Optional[Path] = None): from ddtrace.internal.ci_visibility.api._module import TestVisibilityModule - from ddtrace.internal.ci_visibility.recorder import CIVisibility - log.debug("Registered module %s", item_id) - session = CIVisibility.get_session() + session = require_ci_visibility_service().get_session() session.add_child( item_id, TestVisibilityModule( item_id.name, - CIVisibility.get_session_settings(), + require_ci_visibility_service().get_session_settings(), module_path, ), ) @@ -215,9 +208,8 @@ def discover(item_id: TestModuleId, module_path: Optional[Path] = None): @staticmethod @_catch_and_log_exceptions def start(item_id: TestModuleId): - from ddtrace.internal.ci_visibility.recorder import CIVisibility log.debug("Starting module %s", item_id) - CIVisibility.get_module_by_id(item_id).start() + require_ci_visibility_service().get_module_by_id(item_id).start() @staticmethod @_catch_and_log_exceptions @@ -226,7 +218,6 @@ def finish( override_status: Optional[TestStatus] = None, force_finish_children: bool = False, ): - from ddtrace.internal.ci_visibility.recorder import CIVisibility log.debug( "Finishing module %s, override_status: %s, force_finish_suites: %s", item_id, @@ -234,7 +225,7 @@ def finish( force_finish_children, ) - CIVisibility.get_module_by_id(item_id).finish() + require_ci_visibility_service().get_module_by_id(item_id).finish() class TestSuite(TestBase): @@ -245,18 +236,17 @@ def discover( codeowners: Optional[List[str]] = None, source_file_info: Optional[TestSourceFileInfo] = None, ): - from ddtrace.internal.ci_visibility.recorder import CIVisibility """Registers a test suite with the Test Visibility service.""" log.debug("Registering suite %s, source: %s", item_id, source_file_info) from ddtrace.internal.ci_visibility.api._suite import TestVisibilitySuite - module = CIVisibility.get_module_by_id(item_id.parent_id) + module = require_ci_visibility_service().get_module_by_id(item_id.parent_id) module.add_child( item_id, TestVisibilitySuite( item_id.name, - CIVisibility.get_session_settings(), + require_ci_visibility_service().get_session_settings(), codeowners, source_file_info, ), @@ -265,9 +255,8 @@ def discover( @staticmethod @_catch_and_log_exceptions def start(item_id: TestSuiteId): - from ddtrace.internal.ci_visibility.recorder import CIVisibility log.debug("Starting suite %s", item_id) - CIVisibility.get_suite_by_id(item_id).start() + require_ci_visibility_service().get_suite_by_id(item_id).start() # class FinishArgs(NamedTuple): # suite_id: TestSuiteId @@ -281,7 +270,6 @@ def finish( force_finish_children: bool = False, override_status: Optional[TestStatus] = None, ): - from ddtrace.internal.ci_visibility.recorder import CIVisibility log.debug( "Finishing suite %s, override_status: %s, force_finish_children: %s", item_id, @@ -289,7 +277,7 @@ def finish( override_status, ) - CIVisibility.get_suite_by_id(item_id).finish(force_finish_children, override_status) + require_ci_visibility_service().get_suite_by_id(item_id).finish(force_finish_children, override_status) class Test(TestBase): @@ -304,7 +292,6 @@ def discover( """Registers a test with the Test Visibility service.""" from ddtrace.internal.ci_visibility._api_client import TestProperties from ddtrace.internal.ci_visibility.api._test import TestVisibilityTest - from ddtrace.internal.ci_visibility.recorder import CIVisibility log.debug( "Discovering test %s, codeowners: %s, source file: %s, resource: %s", @@ -315,19 +302,19 @@ def discover( ) log.debug("Handling discovery for test %s", item_id) - suite = CIVisibility.get_suite_by_id(item_id.parent_id) + suite = require_ci_visibility_service().get_suite_by_id(item_id.parent_id) # New tests are currently only considered for EFD: # - if known tests were fetched properly (enforced by is_known_test) # - if they have no parameters - if CIVisibility.is_known_tests_enabled() and item_id.parameters is None: - is_new = not CIVisibility.is_known_test(item_id) + if require_ci_visibility_service().is_known_tests_enabled() and item_id.parameters is None: + is_new = not require_ci_visibility_service().is_known_test(item_id) else: is_new = False test_properties = None - if CIVisibility.is_test_management_enabled(): - test_properties = CIVisibility.get_test_properties(item_id) + if require_ci_visibility_service().is_test_management_enabled(): + test_properties = require_ci_visibility_service().get_test_properties(item_id) if not test_properties: test_properties = TestProperties() @@ -336,7 +323,7 @@ def discover( item_id, TestVisibilityTest( item_id.name, - CIVisibility.get_session_settings(), + require_ci_visibility_service().get_session_settings(), parameters=item_id.parameters, codeowners=codeowners, source_file_info=source_file_info, @@ -351,10 +338,9 @@ def discover( @staticmethod @_catch_and_log_exceptions def start(item_id: TestId): - from ddtrace.internal.ci_visibility.recorder import CIVisibility log.debug("Starting test %s", item_id) - CIVisibility.get_test_by_id(item_id).start() + require_ci_visibility_service().get_test_by_id(item_id).start() # class FinishArgs(NamedTuple): # test_id: TestId @@ -370,7 +356,6 @@ def finish( skip_reason: Optional[str] = None, exc_info: Optional[TestExcInfo] = None, ): - from ddtrace.internal.ci_visibility.recorder import CIVisibility log.debug( "Finishing test %s, status: %s, skip_reason: %s, exc_info: %s", item_id, @@ -379,15 +364,14 @@ def finish( exc_info, ) - CIVisibility.get_test_by_id(item_id).finish_test(status, skip_reason, exc_info) + require_ci_visibility_service().get_test_by_id(item_id).finish_test(status, skip_reason, exc_info) @staticmethod @_catch_and_log_exceptions def set_parameters(item_id: TestId, params: str): - from ddtrace.internal.ci_visibility.recorder import CIVisibility log.debug("Setting test %s parameters to %s", item_id, params) - CIVisibility.get_test_by_id(item_id).set_parameters(params) + require_ci_visibility_service().get_test_by_id(item_id).set_parameters(params) @staticmethod @_catch_and_log_exceptions diff --git a/ddtrace/internal/ci_visibility/recorder.py b/ddtrace/internal/ci_visibility/recorder.py index 4f207ce258a..5d015e062f2 100644 --- a/ddtrace/internal/ci_visibility/recorder.py +++ b/ddtrace/internal/ci_visibility/recorder.py @@ -553,6 +553,8 @@ def _should_skip_path(self, path: str, name: str, test_skipping_mode: Optional[s @classmethod def enable(cls, tracer=None, config=None, service=None) -> None: + from ddtrace.internal.ci_visibility.service_registry import CIVisibilityServiceRegistry + log.debug("Enabling %s", cls.__name__) if ddconfig._ci_visibility_agentless_enabled: if not os.getenv("_CI_DD_API_KEY", os.getenv("DD_API_KEY")): @@ -580,6 +582,9 @@ def enable(cls, tracer=None, config=None, service=None) -> None: cls._instance.start() atexit.register(cls.disable) + # Register with service registry for other modules to access + CIVisibilityServiceRegistry.register(cls._instance) + log.debug("%s enabled", cls.__name__) log.info( "Final settings: coverage collection: %s, " @@ -598,12 +603,17 @@ def enable(cls, tracer=None, config=None, service=None) -> None: @classmethod def disable(cls) -> None: + from ddtrace.internal.ci_visibility.service_registry import CIVisibilityServiceRegistry + if cls._instance is None: log.debug("%s not enabled", cls.__name__) return log.debug("Disabling %s", cls.__name__) atexit.unregister(cls.disable) + # Unregister from service registry first + CIVisibilityServiceRegistry.unregister() + cls._instance.stop() cls._instance = None cls.enabled = False diff --git a/ddtrace/internal/ci_visibility/service_registry.py b/ddtrace/internal/ci_visibility/service_registry.py new file mode 100644 index 00000000000..5a6faf15361 --- /dev/null +++ b/ddtrace/internal/ci_visibility/service_registry.py @@ -0,0 +1,48 @@ +"""Service registry to avoid circular imports in CI Visibility system.""" +import typing as t + +if t.TYPE_CHECKING: + from ddtrace.internal.ci_visibility.recorder import CIVisibility + + +class CIVisibilityServiceRegistry: + """Registry to access CIVisibility instance without circular imports. + + Since CIVisibility is a singleton, no locks are needed. + """ + + _instance: t.Optional["CIVisibility"] = None + + @classmethod + def register(cls, service: "CIVisibility") -> None: + """Register the CIVisibility service instance.""" + cls._instance = service + + @classmethod + def unregister(cls) -> None: + """Unregister the current service instance.""" + cls._instance = None + + @classmethod + def get_service(cls) -> t.Optional["CIVisibility"]: + """Get the registered CIVisibility service instance.""" + return cls._instance + + @classmethod + def require_service(cls) -> "CIVisibility": + """Get the registered service, raising if not available.""" + service = cls.get_service() + if service is None: + raise RuntimeError("CIVisibility service not registered") + return service + + +# Convenience functions +def get_ci_visibility_service() -> t.Optional["CIVisibility"]: + """Get the CIVisibility service if available.""" + return CIVisibilityServiceRegistry.get_service() + + +def require_ci_visibility_service() -> "CIVisibility": + """Get the CIVisibility service, raising if not available.""" + return CIVisibilityServiceRegistry.require_service() diff --git a/ddtrace/internal/test_visibility/_atr_mixins.py b/ddtrace/internal/test_visibility/_atr_mixins.py index 6959a6eac27..de6fa8cd573 100644 --- a/ddtrace/internal/test_visibility/_atr_mixins.py +++ b/ddtrace/internal/test_visibility/_atr_mixins.py @@ -2,11 +2,11 @@ import typing as t from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions -import ddtrace.ext.test_visibility.api as ext_api from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.ext.test_visibility.status import TestExcInfo from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId +from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service log = get_logger(__name__) @@ -24,44 +24,26 @@ class ATRSessionMixin: @_catch_and_log_exceptions def atr_is_enabled() -> bool: log.debug("Checking if ATR is enabled") - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - - return CIVisibility.is_atr_enabled() + return require_ci_visibility_service().is_atr_enabled() @staticmethod @_catch_and_log_exceptions def atr_has_failed_tests() -> bool: log.debug("Checking if ATR session has failed tests") - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - - return CIVisibility.get_session().atr_has_failed_tests() + return require_ci_visibility_service().get_session().atr_has_failed_tests() class ATRTestMixin: - # class ATRRetryFinishArgs(t.NamedTuple): - # test_id: InternalTestId - # retry_number: int - # status: ext_api.TestStatus - # exc_info: t.Optional[ext_api.TestExcInfo] - @staticmethod @_catch_and_log_exceptions def atr_should_retry(test_id: InternalTestId) -> bool: log.debug("Checking if test %s should be retried by ATR", test_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - - return CIVisibility.get_test_by_id(test_id).atr_should_retry() + return require_ci_visibility_service().get_test_by_id(test_id).atr_should_retry() @staticmethod @_catch_and_log_exceptions def atr_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - - retry_number = CIVisibility.get_test_by_id(test_id).atr_add_retry(start_immediately) + retry_number = require_ci_visibility_service().get_test_by_id(test_id).atr_add_retry(start_immediately) log.debug("Adding ATR retry %s for test %s", retry_number, test_id) return retry_number @@ -69,10 +51,7 @@ def atr_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t @_catch_and_log_exceptions def atr_start_retry(test_id: InternalTestId, retry_number: int) -> None: log.debug("Starting ATR retry %s for test %s", retry_number, test_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - - CIVisibility.get_test_by_id(test_id).atr_start_retry(retry_number) + require_ci_visibility_service().get_test_by_id(test_id).atr_start_retry(retry_number) @staticmethod @_catch_and_log_exceptions @@ -83,10 +62,7 @@ def atr_finish_retry( exc_info: t.Optional[TestExcInfo] = None, ) -> None: log.debug("Finishing ATR retry %s for test %s", retry_number, test_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - - CIVisibility.get_test_by_id(test_id).atr_finish_retry( + require_ci_visibility_service().get_test_by_id(test_id).atr_finish_retry( retry_number=retry_number, status=status, exc_info=exc_info ) @@ -94,7 +70,5 @@ def atr_finish_retry( @_catch_and_log_exceptions def atr_get_final_status(test_id: InternalTestId) -> TestStatus: log.debug("Getting ATR final status for test %s", test_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_test_by_id(test_id).atr_get_final_status() + return require_ci_visibility_service().get_test_by_id(test_id).atr_get_final_status() diff --git a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py index 8f18109ecc0..65e53a75784 100644 --- a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py +++ b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py @@ -5,6 +5,7 @@ from ddtrace.ext.test_visibility.status import TestExcInfo from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId +from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service log = get_logger(__name__) @@ -15,10 +16,8 @@ class AttemptToFixSessionMixin: @_catch_and_log_exceptions def attempt_to_fix_has_failed_tests() -> bool: log.debug("Checking if attempt to fix session has failed tests") - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_session().attempt_to_fix_has_failed_tests() + return require_ci_visibility_service().get_session().attempt_to_fix_has_failed_tests() class AttemptToFixTestMixin: @@ -32,18 +31,12 @@ class AttemptToFixTestMixin: @_catch_and_log_exceptions def attempt_to_fix_should_retry(test_id: InternalTestId) -> bool: log.debug("Checking if test %s should be retried by attempt to fix", test_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - - return CIVisibility.get_test_by_id(test_id).attempt_to_fix_should_retry() + return require_ci_visibility_service().get_test_by_id(test_id).attempt_to_fix_should_retry() @staticmethod @_catch_and_log_exceptions def attempt_to_fix_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - - retry_number = CIVisibility.get_test_by_id(test_id).attempt_to_fix_add_retry(start_immediately) + retry_number = require_ci_visibility_service().get_test_by_id(test_id).attempt_to_fix_add_retry(start_immediately) log.debug("Adding attempt to fix retry %s for test %s", retry_number, test_id) return retry_number @@ -51,10 +44,7 @@ def attempt_to_fix_add_retry(test_id: InternalTestId, start_immediately: bool = @_catch_and_log_exceptions def attempt_to_fix_start_retry(test_id: InternalTestId, retry_number: int) -> None: log.debug("Starting attempt to fix retry %s for test %s", retry_number, test_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - - CIVisibility.get_test_by_id(test_id).attempt_to_fix_start_retry(retry_number) + require_ci_visibility_service().get_test_by_id(test_id).attempt_to_fix_start_retry(retry_number) @staticmethod @_catch_and_log_exceptions @@ -65,10 +55,7 @@ def attempt_to_fix_finish_retry( exc_info: t.Optional[TestExcInfo], ) -> None: log.debug("Finishing attempt to fix retry %s for test %s", retry_number, test_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - - CIVisibility.get_test_by_id(test_id).attempt_to_fix_finish_retry( + require_ci_visibility_service().get_test_by_id(test_id).attempt_to_fix_finish_retry( retry_number, status, exc_info ) @@ -76,7 +63,4 @@ def attempt_to_fix_finish_retry( @_catch_and_log_exceptions def attempt_to_fix_get_final_status(test_id: InternalTestId) -> TestStatus: log.debug("Getting attempt to fix final status for test %s", test_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - - return CIVisibility.get_test_by_id(test_id).attempt_to_fix_get_final_status() + return require_ci_visibility_service().get_test_by_id(test_id).attempt_to_fix_get_final_status() diff --git a/ddtrace/internal/test_visibility/_efd_mixins.py b/ddtrace/internal/test_visibility/_efd_mixins.py index ab2d8f6bb53..6dd5f08fbf3 100644 --- a/ddtrace/internal/test_visibility/_efd_mixins.py +++ b/ddtrace/internal/test_visibility/_efd_mixins.py @@ -6,6 +6,7 @@ from ddtrace.ext.test_visibility.status import TestExcInfo from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId +from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service log = get_logger(__name__) @@ -23,28 +24,22 @@ class EFDSessionMixin: @_catch_and_log_exceptions def efd_enabled() -> bool: log.debug("Checking if EFD is enabled") - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_session().efd_is_enabled() + return require_ci_visibility_service().get_session().efd_is_enabled() @staticmethod @_catch_and_log_exceptions def efd_is_faulty_session() -> bool: log.debug("Checking if EFD session is faulty") - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_session().efd_is_faulty_session() + return require_ci_visibility_service().get_session().efd_is_faulty_session() @staticmethod @_catch_and_log_exceptions def efd_has_failed_tests() -> bool: log.debug("Checking if EFD session has failed tests") - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_session().efd_has_failed_tests() + return require_ci_visibility_service().get_session().efd_has_failed_tests() class EFDTestMixin: @@ -52,18 +47,14 @@ class EFDTestMixin: @_catch_and_log_exceptions def efd_should_retry(test_id: InternalTestId) -> bool: log.debug("Checking if test %s should be retried by EFD", test_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_test_by_id(test_id).efd_should_retry() + return require_ci_visibility_service().get_test_by_id(test_id).efd_should_retry() @staticmethod @_catch_and_log_exceptions def efd_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - retry_number = CIVisibility.get_test_by_id(test_id).efd_add_retry(start_immediately) + retry_number = require_ci_visibility_service().get_test_by_id(test_id).efd_add_retry(start_immediately) log.debug("Adding EFD retry %s for test %s", retry_number, test_id) return retry_number @@ -71,10 +62,8 @@ def efd_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t @_catch_and_log_exceptions def efd_start_retry(test_id: InternalTestId, retry_number: int) -> None: log.debug("Starting EFD retry %s for test %s", retry_number, test_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - CIVisibility.get_test_by_id(test_id).efd_start_retry(retry_number) + require_ci_visibility_service().get_test_by_id(test_id).efd_start_retry(retry_number) @staticmethod @_catch_and_log_exceptions @@ -85,8 +74,6 @@ def efd_finish_retry( skip_reason: t.Optional[str] = None, exc_info: t.Optional[TestExcInfo] = None, ): - from ddtrace.internal.ci_visibility.recorder import CIVisibility - log.debug( "Finishing EFD test retry %s for item %s, status: %s, skip_reason: %s, exc_info: %s", retry_number, @@ -95,7 +82,7 @@ def efd_finish_retry( skip_reason, exc_info, ) - CIVisibility.get_test_by_id(item_id).efd_finish_retry( + require_ci_visibility_service().get_test_by_id(item_id).efd_finish_retry( retry_number=retry_number, status=status, skip_reason=skip_reason, exc_info=exc_info ) @@ -103,7 +90,5 @@ def efd_finish_retry( @_catch_and_log_exceptions def efd_get_final_status(test_id: InternalTestId) -> EFDTestStatus: log.debug("Getting EFD final status for test %s", test_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_test_by_id(test_id).efd_get_final_status() + return require_ci_visibility_service().get_test_by_id(test_id).efd_get_final_status() diff --git a/ddtrace/internal/test_visibility/_itr_mixins.py b/ddtrace/internal/test_visibility/_itr_mixins.py index a165271f1cf..fc65b99492b 100644 --- a/ddtrace/internal/test_visibility/_itr_mixins.py +++ b/ddtrace/internal/test_visibility/_itr_mixins.py @@ -7,6 +7,7 @@ from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId from ddtrace.internal.test_visibility.coverage_lines import CoverageLines +from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service log = get_logger(__name__) @@ -23,77 +24,63 @@ class AddCoverageArgs(t.NamedTuple): @_catch_and_log_exceptions def mark_itr_skipped(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): log.debug("Marking item %s as skipped by ITR", item_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility if not isinstance(item_id, (ext_api.TestSuiteId, InternalTestId)): log.warning("Only suites or tests can be skipped, not %s", type(item_id)) return - CIVisibility.get_item_by_id(item_id).finish_itr_skipped() + require_ci_visibility_service().get_item_by_id(item_id).finish_itr_skipped() @staticmethod @_catch_and_log_exceptions def mark_itr_unskippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): log.debug("Marking item %s as unskippable by ITR", item_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - CIVisibility.get_item_by_id(item_id).mark_itr_unskippable() + require_ci_visibility_service().get_item_by_id(item_id).mark_itr_unskippable() @staticmethod @_catch_and_log_exceptions def mark_itr_forced_run(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): log.debug("Marking item %s as forced run by ITR", item_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - CIVisibility.get_item_by_id(item_id).mark_itr_forced_run() + require_ci_visibility_service().get_item_by_id(item_id).mark_itr_forced_run() @staticmethod @_catch_and_log_exceptions def was_itr_forced_run(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: log.debug("Checking if item %s was forced run by ITR", item_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_item_by_id(item_id).was_itr_forced_run() + return require_ci_visibility_service().get_item_by_id(item_id).was_itr_forced_run() @staticmethod @_catch_and_log_exceptions def is_itr_skippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: log.debug("Checking if item %s is skippable by ITR", item_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility if not isinstance(item_id, (ext_api.TestSuiteId, InternalTestId)): log.warning("Only suites or tests can be skippable, not %s", type(item_id)) return False - if not CIVisibility.test_skipping_enabled(): + if not require_ci_visibility_service().test_skipping_enabled(): log.debug("Test skipping is not enabled") return False - return CIVisibility.is_item_itr_skippable(item_id) + return require_ci_visibility_service().is_item_itr_skippable(item_id) @staticmethod @_catch_and_log_exceptions def is_itr_unskippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: log.debug("Checking if item %s is unskippable by ITR", item_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility if not isinstance(item_id, (ext_api.TestSuiteId, InternalTestId)): raise CIVisibilityError("Only suites or tests can be unskippable") - return CIVisibility.get_item_by_id(item_id).is_itr_unskippable() + return require_ci_visibility_service().get_item_by_id(item_id).is_itr_unskippable() @staticmethod @_catch_and_log_exceptions def was_itr_skipped(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: log.debug("Checking if item %s was skipped by ITR", item_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_item_by_id(item_id).is_itr_skipped() + return require_ci_visibility_service().get_item_by_id(item_id).is_itr_skipped() @staticmethod @_catch_and_log_exceptions @@ -105,10 +92,8 @@ def add_coverage_data(item_id, coverage_data) -> None: log.warning("Coverage data can only be added to suites and tests, not %s", type(item_id)) return - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - CIVisibility.get_item_by_id(item_id).add_coverage_data(coverage_data) + require_ci_visibility_service().get_item_by_id(item_id).add_coverage_data(coverage_data) @staticmethod @_catch_and_log_exceptions @@ -116,7 +101,5 @@ def get_coverage_data( item_id: t.Union[ext_api.TestSuiteId, InternalTestId] ) -> t.Optional[t.Dict[Path, CoverageLines]]: log.debug("Getting coverage data for item %s", item_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_item_by_id(item_id).get_coverage_data() + return require_ci_visibility_service().get_item_by_id(item_id).get_coverage_data() diff --git a/ddtrace/internal/test_visibility/_utils.py b/ddtrace/internal/test_visibility/_utils.py index 182957584ab..9ca078eec6f 100644 --- a/ddtrace/internal/test_visibility/_utils.py +++ b/ddtrace/internal/test_visibility/_utils.py @@ -3,6 +3,7 @@ from ddtrace.ext.test_visibility import api as ext_api from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId +from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service from ddtrace.trace import Span @@ -10,7 +11,4 @@ def _get_item_span(item_id: t.Union[ext_api.TestVisibilityItemId, InternalTestId]) -> Span: - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - - return CIVisibility.get_item_by_id(item_id).get_span() + return require_ci_visibility_service().get_item_by_id(item_id).get_span() diff --git a/ddtrace/internal/test_visibility/api.py b/ddtrace/internal/test_visibility/api.py index 8cb25804a6b..d383117bc1b 100644 --- a/ddtrace/internal/test_visibility/api.py +++ b/ddtrace/internal/test_visibility/api.py @@ -6,8 +6,6 @@ from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions from ddtrace.ext.test_visibility._test_visibility_base import TestSessionId from ddtrace.ext.test_visibility._utils import _is_item_finished -from ddtrace.ext.test_visibility.status import TestExcInfo -from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.internal.codeowners import Codeowners as _Codeowners from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._atr_mixins import ATRSessionMixin @@ -23,6 +21,7 @@ from ddtrace.internal.test_visibility._utils import _get_item_span from ddtrace.trace import Span from ddtrace.trace import Tracer +from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service log = get_logger(__name__) @@ -38,37 +37,29 @@ def get_span(item_id: t.Union[ext_api.TestVisibilityItemId, InternalTestId]) -> @_catch_and_log_exceptions def stash_set(item_id, key: str, value: object): log.debug("Stashing value %s for key %s in item %s", value, key, item_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - CIVisibility.get_item_by_id(item_id).stash_set(key, value) + require_ci_visibility_service().get_item_by_id(item_id).stash_set(key, value) @staticmethod @_catch_and_log_exceptions def stash_get(item_id: ext_api.TestVisibilityItemId, key: str) -> t.Optional[object]: log.debug("Getting stashed value for key %s in item %s", key, item_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_item_by_id(item_id).stash_get(key) + return require_ci_visibility_service().get_item_by_id(item_id).stash_get(key) @staticmethod @_catch_and_log_exceptions def stash_delete(item_id: ext_api.TestVisibilityItemId, key: str): log.debug("Deleting stashed value for key %s in item %s", key, item_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - CIVisibility.get_item_by_id(item_id).stash_delete(key) + require_ci_visibility_service().get_item_by_id(item_id).stash_delete(key) @staticmethod @_catch_and_log_exceptions def overwrite_attributes(overwrite_attribute_args: "InternalTest.OverwriteAttributesArgs") -> None: log.debug("Overwriting attributes: %s", overwrite_attribute_args) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - CIVisibility.get_test_by_id(overwrite_attribute_args.test_id).overwrite_attributes( + require_ci_visibility_service().get_test_by_id(overwrite_attribute_args.test_id).overwrite_attributes( overwrite_attribute_args.name, overwrite_attribute_args.suite_name, overwrite_attribute_args.parameters, @@ -89,65 +80,51 @@ def is_finished() -> bool: @_catch_and_log_exceptions def get_codeowners() -> t.Optional[_Codeowners]: log.debug("Getting codeowners") - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_codeowners() + return require_ci_visibility_service().get_codeowners() @staticmethod @_catch_and_log_exceptions def get_tracer() -> t.Optional[Tracer]: log.debug("Getting tracer") - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_tracer() + return require_ci_visibility_service().get_tracer() @staticmethod @_catch_and_log_exceptions def get_workspace_path() -> t.Optional[Path]: log.debug("Getting workspace path") - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - path_str = CIVisibility.get_workspace_path() + path_str = require_ci_visibility_service().get_workspace_path() return Path(path_str) if path_str is not None else None @staticmethod @_catch_and_log_exceptions def should_collect_coverage() -> bool: log.debug("Checking if should collect coverage") - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.should_collect_coverage() + return require_ci_visibility_service().should_collect_coverage() @staticmethod @_catch_and_log_exceptions def is_test_skipping_enabled() -> bool: log.debug("Checking if test skipping is enabled") - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.test_skipping_enabled() + return require_ci_visibility_service().test_skipping_enabled() @staticmethod @_catch_and_log_exceptions def set_covered_lines_pct(coverage_pct: float) -> None: log.debug("Setting coverage percentage for session to %s", coverage_pct) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - CIVisibility.get_session().set_covered_lines_pct(coverage_pct) + require_ci_visibility_service().get_session().set_covered_lines_pct(coverage_pct) @staticmethod @_catch_and_log_exceptions def get_path_codeowners(path: Path) -> t.Optional[t.List[str]]: log.debug("Getting codeowners for path %s", path) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - codeowners = CIVisibility.get_codeowners() + codeowners = require_ci_visibility_service().get_codeowners() if codeowners is None: return None return codeowners.of(str(path)) @@ -156,19 +133,15 @@ def get_path_codeowners(path: Path) -> t.Optional[t.List[str]]: @_catch_and_log_exceptions def set_library_capabilities(capabilities: LibraryCapabilities) -> None: log.debug("Setting library capabilities") - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - CIVisibility.set_library_capabilities(capabilities) + require_ci_visibility_service().set_library_capabilities(capabilities) @staticmethod @_catch_and_log_exceptions def set_itr_skipped_count(skipped_count: int) -> None: log.debug("Setting skipped count: %d", skipped_count) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - CIVisibility.get_session().set_skipped_count(skipped_count) + require_ci_visibility_service().get_session().set_skipped_count(skipped_count) class InternalTestModule(ext_api.TestModule, InternalTestBase): @@ -200,12 +173,9 @@ def finish( exc_info: t.Optional[ext_api.TestExcInfo] = None, override_finish_time: t.Optional[float] = None, ): - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - log.debug("Finishing test with status: %s, skip_reason: %s", status, skip_reason) final_status = status if status is not None else ext_api.TestStatus.PASS - CIVisibility.get_test_by_id(item_id).finish_test( + require_ci_visibility_service().get_test_by_id(item_id).finish_test( status=final_status, skip_reason=skip_reason, exc_info=exc_info ) @@ -213,37 +183,29 @@ def finish( @_catch_and_log_exceptions def is_new_test(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: log.debug("Checking if test %s is new", test_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_test_by_id(test_id).is_new() + return require_ci_visibility_service().get_test_by_id(test_id).is_new() @staticmethod @_catch_and_log_exceptions def is_quarantined_test(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: log.debug("Checking if test %s is quarantined", test_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_test_by_id(test_id).is_quarantined() + return require_ci_visibility_service().get_test_by_id(test_id).is_quarantined() @staticmethod @_catch_and_log_exceptions def is_disabled_test(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: log.debug("Checking if test %s is disabled", test_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_test_by_id(test_id).is_disabled() + return require_ci_visibility_service().get_test_by_id(test_id).is_disabled() @staticmethod @_catch_and_log_exceptions def is_attempt_to_fix(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: log.debug("Checking if test %s is attempt to fix", test_id) - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - return CIVisibility.get_test_by_id(test_id).is_attempt_to_fix() + return require_ci_visibility_service().get_test_by_id(test_id).is_attempt_to_fix() class OverwriteAttributesArgs(NamedTuple): test_id: InternalTestId @@ -261,9 +223,8 @@ def overwrite_attributes( parameters: t.Optional[str] = None, codeowners: t.Optional[t.List[str]] = None, ): - # Lazy import to avoid circular dependency - from ddtrace.internal.ci_visibility.recorder import CIVisibility - log.debug("Overwriting attributes for test %s", item_id) - CIVisibility.get_test_by_id(item_id).overwrite_attributes(name, suite_name, parameters, codeowners) + require_ci_visibility_service().get_test_by_id(item_id).overwrite_attributes( + name, suite_name, parameters, codeowners + ) From f5c8e484f9fe2ac346f04686a0d8518195010d18 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Fri, 13 Jun 2025 11:43:36 +0200 Subject: [PATCH 28/58] style --- ddtrace/ext/test_visibility/api.py | 3 ++- .../internal/ci_visibility/service_registry.py | 1 + ddtrace/internal/test_visibility/_atr_mixins.py | 4 ++-- .../test_visibility/_attempt_to_fix_mixins.py | 16 +++++++++------- ddtrace/internal/test_visibility/_efd_mixins.py | 5 ++--- ddtrace/internal/test_visibility/_itr_mixins.py | 3 +-- ddtrace/internal/test_visibility/_utils.py | 2 +- ddtrace/internal/test_visibility/api.py | 2 +- 8 files changed, 19 insertions(+), 17 deletions(-) diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index bbf70e93ace..41e267354ea 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -36,8 +36,8 @@ from ddtrace.ext.test_visibility.status import TestExcInfo from ddtrace.ext.test_visibility.status import TestSourceFileInfo from ddtrace.ext.test_visibility.status import TestStatus -from ddtrace.internal.logger import get_logger as _get_logger from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service +from ddtrace.internal.logger import get_logger as _get_logger log = _get_logger(__name__) @@ -193,6 +193,7 @@ class TestModule(TestBase): @_catch_and_log_exceptions def discover(item_id: TestModuleId, module_path: Optional[Path] = None): from ddtrace.internal.ci_visibility.api._module import TestVisibilityModule + log.debug("Registered module %s", item_id) session = require_ci_visibility_service().get_session() diff --git a/ddtrace/internal/ci_visibility/service_registry.py b/ddtrace/internal/ci_visibility/service_registry.py index 5a6faf15361..96d30e427c1 100644 --- a/ddtrace/internal/ci_visibility/service_registry.py +++ b/ddtrace/internal/ci_visibility/service_registry.py @@ -1,6 +1,7 @@ """Service registry to avoid circular imports in CI Visibility system.""" import typing as t + if t.TYPE_CHECKING: from ddtrace.internal.ci_visibility.recorder import CIVisibility diff --git a/ddtrace/internal/test_visibility/_atr_mixins.py b/ddtrace/internal/test_visibility/_atr_mixins.py index de6fa8cd573..ec62741347d 100644 --- a/ddtrace/internal/test_visibility/_atr_mixins.py +++ b/ddtrace/internal/test_visibility/_atr_mixins.py @@ -2,11 +2,11 @@ import typing as t from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions -from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.ext.test_visibility.status import TestExcInfo +from ddtrace.ext.test_visibility.status import TestStatus +from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId -from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service log = get_logger(__name__) diff --git a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py index 65e53a75784..41ec3432a6d 100644 --- a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py +++ b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py @@ -1,11 +1,11 @@ import typing as t from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions -from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.ext.test_visibility.status import TestExcInfo +from ddtrace.ext.test_visibility.status import TestStatus +from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId -from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service log = get_logger(__name__) @@ -36,7 +36,9 @@ def attempt_to_fix_should_retry(test_id: InternalTestId) -> bool: @staticmethod @_catch_and_log_exceptions def attempt_to_fix_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: - retry_number = require_ci_visibility_service().get_test_by_id(test_id).attempt_to_fix_add_retry(start_immediately) + retry_number = ( + require_ci_visibility_service().get_test_by_id(test_id).attempt_to_fix_add_retry(start_immediately) + ) log.debug("Adding attempt to fix retry %s for test %s", retry_number, test_id) return retry_number @@ -49,10 +51,10 @@ def attempt_to_fix_start_retry(test_id: InternalTestId, retry_number: int) -> No @staticmethod @_catch_and_log_exceptions def attempt_to_fix_finish_retry( - test_id: InternalTestId, - retry_number: int, - status: TestStatus, - exc_info: t.Optional[TestExcInfo], + test_id: InternalTestId, + retry_number: int, + status: TestStatus, + exc_info: t.Optional[TestExcInfo], ) -> None: log.debug("Finishing attempt to fix retry %s for test %s", retry_number, test_id) require_ci_visibility_service().get_test_by_id(test_id).attempt_to_fix_finish_retry( diff --git a/ddtrace/internal/test_visibility/_efd_mixins.py b/ddtrace/internal/test_visibility/_efd_mixins.py index 6dd5f08fbf3..3a5137b64c8 100644 --- a/ddtrace/internal/test_visibility/_efd_mixins.py +++ b/ddtrace/internal/test_visibility/_efd_mixins.py @@ -2,11 +2,11 @@ import typing as t from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions -from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.ext.test_visibility.status import TestExcInfo +from ddtrace.ext.test_visibility.status import TestStatus +from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId -from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service log = get_logger(__name__) @@ -53,7 +53,6 @@ def efd_should_retry(test_id: InternalTestId) -> bool: @staticmethod @_catch_and_log_exceptions def efd_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: - retry_number = require_ci_visibility_service().get_test_by_id(test_id).efd_add_retry(start_immediately) log.debug("Adding EFD retry %s for test %s", retry_number, test_id) return retry_number diff --git a/ddtrace/internal/test_visibility/_itr_mixins.py b/ddtrace/internal/test_visibility/_itr_mixins.py index fc65b99492b..69d8746f904 100644 --- a/ddtrace/internal/test_visibility/_itr_mixins.py +++ b/ddtrace/internal/test_visibility/_itr_mixins.py @@ -4,10 +4,10 @@ from ddtrace.ext.test_visibility import api as ext_api from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions from ddtrace.internal.ci_visibility.errors import CIVisibilityError +from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId from ddtrace.internal.test_visibility.coverage_lines import CoverageLines -from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service log = get_logger(__name__) @@ -92,7 +92,6 @@ def add_coverage_data(item_id, coverage_data) -> None: log.warning("Coverage data can only be added to suites and tests, not %s", type(item_id)) return - require_ci_visibility_service().get_item_by_id(item_id).add_coverage_data(coverage_data) @staticmethod diff --git a/ddtrace/internal/test_visibility/_utils.py b/ddtrace/internal/test_visibility/_utils.py index 9ca078eec6f..d828c377a4d 100644 --- a/ddtrace/internal/test_visibility/_utils.py +++ b/ddtrace/internal/test_visibility/_utils.py @@ -1,9 +1,9 @@ import typing as t from ddtrace.ext.test_visibility import api as ext_api +from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId -from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service from ddtrace.trace import Span diff --git a/ddtrace/internal/test_visibility/api.py b/ddtrace/internal/test_visibility/api.py index d383117bc1b..bf4c08c5ea3 100644 --- a/ddtrace/internal/test_visibility/api.py +++ b/ddtrace/internal/test_visibility/api.py @@ -6,6 +6,7 @@ from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions from ddtrace.ext.test_visibility._test_visibility_base import TestSessionId from ddtrace.ext.test_visibility._utils import _is_item_finished +from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service from ddtrace.internal.codeowners import Codeowners as _Codeowners from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._atr_mixins import ATRSessionMixin @@ -21,7 +22,6 @@ from ddtrace.internal.test_visibility._utils import _get_item_span from ddtrace.trace import Span from ddtrace.trace import Tracer -from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service log = get_logger(__name__) From 3618a2458cce21ab18689def2ca6d11bc7b5c62b Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Fri, 13 Jun 2025 12:30:31 +0200 Subject: [PATCH 29/58] fix --- ddtrace/ext/test_visibility/api.py | 58 ++--- ddtrace/internal/ci_visibility/recorder.py | 203 +++++------------- .../test_visibility/_attempt_to_fix_mixins.py | 6 - .../internal/test_visibility/_itr_mixins.py | 5 +- ddtrace/internal/test_visibility/api.py | 9 - 5 files changed, 84 insertions(+), 197 deletions(-) diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 41e267354ea..9c2bacd7588 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -59,7 +59,9 @@ class DEFAULT_OPERATION_NAMES(Enum): @_catch_and_log_exceptions def enable_test_visibility(config: Optional[Any] = None): log.debug("Enabling Test Visibility with config: %s", config) - require_ci_visibility_service().enable(config=config) + from ddtrace.internal.ci_visibility.recorder import CIVisibility + + CIVisibility.enable(config=config) if not require_ci_visibility_service().enabled: log.warning("Failed to enable Test Visibility") @@ -74,8 +76,9 @@ def is_test_visibility_enabled(): def disable_test_visibility(): log.debug("Disabling Test Visibility") - require_ci_visibility_service().disable() - if require_ci_visibility_service().enabled: + ci_visibility_instance = require_ci_visibility_service() + ci_visibility_instance.disable() + if ci_visibility_instance.enabled: log.warning("Failed to disable Test Visibility") @@ -147,10 +150,6 @@ def start(distributed_children: bool = False, context: Optional[Context] = None) if distributed_children: session.set_distributed_children() - # class FinishArgs(NamedTuple): - # force_finish_children: bool - # override_status: Optional[TestStatus] - @staticmethod @_catch_and_log_exceptions def finish( @@ -184,24 +183,20 @@ def delete_tags(tag_names: List[str]): class TestModule(TestBase): - # class FinishArgs(NamedTuple): - # module_id: TestModuleId - # override_status: Optional[TestStatus] = None - # force_finish_children: bool = False - @staticmethod @_catch_and_log_exceptions def discover(item_id: TestModuleId, module_path: Optional[Path] = None): from ddtrace.internal.ci_visibility.api._module import TestVisibilityModule log.debug("Registered module %s", item_id) - session = require_ci_visibility_service().get_session() + ci_visibility_instance = require_ci_visibility_service() + session = ci_visibility_instance.get_session() session.add_child( item_id, TestVisibilityModule( item_id.name, - require_ci_visibility_service().get_session_settings(), + ci_visibility_instance.get_session_settings(), module_path, ), ) @@ -226,7 +221,9 @@ def finish( force_finish_children, ) - require_ci_visibility_service().get_module_by_id(item_id).finish() + require_ci_visibility_service().get_module_by_id(item_id).finish( + override_status=override_status # , force_finish_children=force_finish_children + ) class TestSuite(TestBase): @@ -241,13 +238,14 @@ def discover( log.debug("Registering suite %s, source: %s", item_id, source_file_info) from ddtrace.internal.ci_visibility.api._suite import TestVisibilitySuite - module = require_ci_visibility_service().get_module_by_id(item_id.parent_id) + ci_visibility_instance = require_ci_visibility_service() + module = ci_visibility_instance.get_module_by_id(item_id.parent_id) module.add_child( item_id, TestVisibilitySuite( item_id.name, - require_ci_visibility_service().get_session_settings(), + ci_visibility_instance.get_session_settings(), codeowners, source_file_info, ), @@ -259,11 +257,6 @@ def start(item_id: TestSuiteId): log.debug("Starting suite %s", item_id) require_ci_visibility_service().get_suite_by_id(item_id).start() - # class FinishArgs(NamedTuple): - # suite_id: TestSuiteId - # force_finish_children: bool = False - # override_status: Optional[TestStatus] = None - @staticmethod @_catch_and_log_exceptions def finish( @@ -278,7 +271,7 @@ def finish( override_status, ) - require_ci_visibility_service().get_suite_by_id(item_id).finish(force_finish_children, override_status) + require_ci_visibility_service().get_suite_by_id(item_id).finish(override_status=override_status) class Test(TestBase): @@ -303,19 +296,20 @@ def discover( ) log.debug("Handling discovery for test %s", item_id) - suite = require_ci_visibility_service().get_suite_by_id(item_id.parent_id) + ci_visibility_instance = require_ci_visibility_service() + suite = ci_visibility_instance.get_suite_by_id(item_id.parent_id) # New tests are currently only considered for EFD: # - if known tests were fetched properly (enforced by is_known_test) # - if they have no parameters - if require_ci_visibility_service().is_known_tests_enabled() and item_id.parameters is None: - is_new = not require_ci_visibility_service().is_known_test(item_id) + if ci_visibility_instance.is_known_tests_enabled() and item_id.parameters is None: + is_new = not ci_visibility_instance.is_known_test(item_id) else: is_new = False test_properties = None - if require_ci_visibility_service().is_test_management_enabled(): - test_properties = require_ci_visibility_service().get_test_properties(item_id) + if ci_visibility_instance.is_test_management_enabled(): + test_properties = ci_visibility_instance.get_test_properties(item_id) if not test_properties: test_properties = TestProperties() @@ -324,7 +318,7 @@ def discover( item_id, TestVisibilityTest( item_id.name, - require_ci_visibility_service().get_session_settings(), + ci_visibility_instance.get_session_settings(), parameters=item_id.parameters, codeowners=codeowners, source_file_info=source_file_info, @@ -343,12 +337,6 @@ def start(item_id: TestId): require_ci_visibility_service().get_test_by_id(item_id).start() - # class FinishArgs(NamedTuple): - # test_id: TestId - # status: TestStatus - # skip_reason: Optional[str] = None - # exc_info: Optional[TestExcInfo] = None - @staticmethod @_catch_and_log_exceptions def finish( diff --git a/ddtrace/internal/ci_visibility/recorder.py b/ddtrace/internal/ci_visibility/recorder.py index 5d015e062f2..716def91f26 100644 --- a/ddtrace/internal/ci_visibility/recorder.py +++ b/ddtrace/internal/ci_visibility/recorder.py @@ -799,61 +799,25 @@ def get_instance(cls) -> "CIVisibility": raise CIVisibilityError(error_msg) return cls._instance - @classmethod - def get_tracer(cls) -> Optional[Tracer]: - if not cls.enabled: - error_msg = "CI Visibility is not enabled" - log.warning(error_msg) - raise CIVisibilityError(error_msg) - instance = cls.get_instance() - if instance is None: - return None - return instance.tracer + def get_tracer(self) -> Optional[Tracer]: + return self.tracer - @classmethod - def get_service(cls) -> Optional[str]: - if not cls.enabled: - error_msg = "CI Visibility is not enabled" - log.warning(error_msg) - raise CIVisibilityError(error_msg) - instance = cls.get_instance() - if instance is None: - return None - return instance._service + def get_service(self) -> Optional[str]: + return self._service - @classmethod - def get_codeowners(cls) -> Optional[Codeowners]: - if not cls.enabled: - error_msg = "CI Visibility is not enabled" - log.warning(error_msg) - raise CIVisibilityError(error_msg) - instance = cls.get_instance() - if instance is None: - return None - return instance._codeowners + def get_codeowners(self) -> Optional[Codeowners]: + return self._codeowners - @classmethod - def get_efd_api_settings(cls) -> Optional[EarlyFlakeDetectionSettings]: - if not cls.enabled: - error_msg = "CI Visibility is not enabled" - log.warning(error_msg) - raise CIVisibilityError(error_msg) - instance = cls.get_instance() - if instance is None or instance._api_settings is None: + def get_efd_api_settings(self) -> Optional[EarlyFlakeDetectionSettings]: + if self._api_settings is None: return None - return instance._api_settings.early_flake_detection + return self._api_settings.early_flake_detection - @classmethod - def get_atr_api_settings(cls) -> Optional[AutoTestRetriesSettings]: - if not cls.enabled: - error_msg = "CI Visibility is not enabled" - log.warning(error_msg) - raise CIVisibilityError(error_msg) - instance = cls.get_instance() - if instance is None or instance._api_settings is None: + def get_atr_api_settings(self) -> Optional[AutoTestRetriesSettings]: + if self._api_settings is None: return None - if instance._api_settings.flaky_test_retries_enabled: + if self._api_settings.flaky_test_retries_enabled: # NOTE: this is meant to come from integration settings but current plans to rewrite how integration # settings are defined make it better for this logic to be temporarily defined here. @@ -886,68 +850,35 @@ def get_atr_api_settings(cls) -> Optional[AutoTestRetriesSettings]: return None - @classmethod - def get_test_management_api_settings(cls) -> Optional[TestManagementSettings]: - if not cls.enabled: - error_msg = "CI Visibility is not enabled" - log.warning(error_msg) - raise CIVisibilityError(error_msg) - instance = cls.get_instance() - if instance is None or instance._api_settings is None: + def get_test_management_api_settings(self) -> Optional[TestManagementSettings]: + if self._api_settings is None: return None - return instance._api_settings.test_management + return self._api_settings.test_management - @classmethod - def get_workspace_path(cls) -> Optional[str]: - if not cls.enabled: - error_msg = "CI Visibility is not enabled" - log.warning(error_msg) - raise CIVisibilityError(error_msg) - instance = cls.get_instance() - if instance is None: - return None - return instance._tags.get(ci.WORKSPACE_PATH) + def get_workspace_path(self) -> Optional[str]: + return self._tags.get(ci.WORKSPACE_PATH) - @classmethod - def is_item_itr_skippable(cls, item_id: TestVisibilityItemId) -> bool: - if not cls.enabled: - error_msg = "CI Visibility is not enabled" - log.warning(error_msg) - raise CIVisibilityError(error_msg) - instance = cls.get_instance() - if instance is None or instance._itr_data is None: + def is_item_itr_skippable(self, item_id: TestVisibilityItemId) -> bool: + if self._itr_data is None: return False - if isinstance(item_id, TestSuiteId) and not instance._suite_skipping_mode: + if isinstance(item_id, TestSuiteId) and not self._suite_skipping_mode: log.debug("Skipping mode is suite, but item is not a suite: %s", item_id) return False - if isinstance(item_id, TestId) and instance._suite_skipping_mode: + if isinstance(item_id, TestId) and self._suite_skipping_mode: log.debug("Skipping mode is test, but item is not a test: %s", item_id) return False - return item_id in instance._itr_data.skippable_items - - @classmethod - def is_unknown_ci(cls) -> bool: - instance = cls.get_instance() - if instance is None: - return False + return item_id in self._itr_data.skippable_items - return instance._tags.get(ci.PROVIDER_NAME) is None + def is_unknown_ci(self) -> bool: + return self._tags.get(ci.PROVIDER_NAME) is None - @classmethod - def ci_provider_name_for_telemetry(cls) -> str: - instance = cls.get_instance() - if instance is None: - return UNSUPPORTED_PROVIDER - return TELEMETRY_BY_PROVIDER_NAME.get(instance._tags.get(ci.PROVIDER_NAME, UNSUPPORTED), UNSUPPORTED_PROVIDER) + def ci_provider_name_for_telemetry(self) -> str: + return TELEMETRY_BY_PROVIDER_NAME.get(self._tags.get(ci.PROVIDER_NAME, UNSUPPORTED), UNSUPPORTED_PROVIDER) - @classmethod - def is_auto_injected(cls) -> bool: - instance = cls.get_instance() - if instance is None: - return False - return instance._is_auto_injected + def is_auto_injected(self) -> bool: + return self._is_auto_injected def _get_ci_visibility_event_client(self) -> Optional[CIVisibilityEventClient]: writer = self.tracer._span_aggregator.writer @@ -958,10 +889,8 @@ def _get_ci_visibility_event_client(self) -> Optional[CIVisibilityEventClient]: return None - @classmethod - def set_test_session_name(cls, test_command: str) -> None: - instance = cls.get_instance() - client = instance._get_ci_visibility_event_client() + def set_test_session_name(self, test_command: str) -> None: + client = self._get_ci_visibility_event_client() if not client: log.debug("Not setting test session name because no CIVisibilityEventClient is active") return @@ -969,52 +898,36 @@ def set_test_session_name(cls, test_command: str) -> None: if ddconfig._test_session_name: test_session_name = ddconfig._test_session_name else: - job_name = instance._tags.get(ci.JOB_NAME) + job_name = self._tags.get(ci.JOB_NAME) test_session_name = f"{job_name}-{test_command}" if job_name else test_command log.debug("Setting test session name: %s", test_session_name) client.set_test_session_name(test_session_name) - @classmethod - def set_library_capabilities(cls, capabilities: LibraryCapabilities) -> None: - instance = cls.get_instance() - client = instance._get_ci_visibility_event_client() + def set_library_capabilities(self, capabilities: LibraryCapabilities) -> None: + client = self._get_ci_visibility_event_client() if not client: log.debug("Not setting library capabilities because no CIVisibilityEventClient is active") return client.set_metadata("test", capabilities.tags()) - @classmethod - def get_ci_tags(cls): - instance = cls.get_instance() - return instance._tags + def get_ci_tags(self): + return self._tags - @classmethod - def get_dd_env(cls): - instance = cls.get_instance() - return instance._dd_env - - @classmethod - def is_known_test(cls, test_id: Union[TestId, InternalTestId]) -> bool: - instance = cls.get_instance() - if instance is None: - return False + def get_dd_env(self): + return self._dd_env + def is_known_test(self, test_id: Union[TestId, InternalTestId]) -> bool: # The assumption that we were not able to fetch unique tests properly if the length is 0 is acceptable # because the current EFD usage would cause the session to be faulty even if the query was successful but # not unique tests exist. In this case, we assume all tests are unique. - if len(instance._known_test_ids) == 0: + if len(self._known_test_ids) == 0: return True - return test_id in instance._known_test_ids - - @classmethod - def get_test_properties(cls, test_id: Union[TestId, InternalTestId]) -> Optional[TestProperties]: - instance = cls.get_instance() - if instance is None: - return None + return test_id in self._known_test_ids - return instance._test_properties.get(test_id) + def get_test_properties(self, test_id: Union[TestId, InternalTestId]) -> Optional[TestProperties]: + return self._test_properties.get(test_id) def _requires_civisibility_enabled(func: Callable) -> Callable: @@ -1042,9 +955,9 @@ def on_discover_session( log.debug("Handling session discovery") # _requires_civisibility_enabled prevents us from getting here, but this makes type checkers happy - tracer = CIVisibility.get_tracer() - test_service = CIVisibility.get_service() instance = CIVisibility.get_instance() + test_service = instance.get_service() + tracer = instance.get_tracer() if tracer is None or test_service is None: error_msg = "Tracer or test service is None" @@ -1052,21 +965,21 @@ def on_discover_session( raise CIVisibilityError(error_msg) # If we're not provided a root directory, try and extract it from workspace, defaulting to CWD - workspace_path = root_dir or Path(CIVisibility.get_workspace_path() or os.getcwd()) + workspace_path = root_dir or Path(instance.get_workspace_path() or os.getcwd()) # Prevent high cardinality of test framework telemetry tag by matching with known frameworks test_framework_telemetry_name = _get_test_framework_telemetry_name(test_framework) - efd_api_settings = CIVisibility.get_efd_api_settings() - if efd_api_settings is None or not CIVisibility.is_efd_enabled(): + efd_api_settings = instance.get_efd_api_settings() + if efd_api_settings is None or not instance.is_efd_enabled(): efd_api_settings = EarlyFlakeDetectionSettings() - atr_api_settings = CIVisibility.get_atr_api_settings() + atr_api_settings = instance.get_atr_api_settings() if atr_api_settings is None or not CIVisibility.is_atr_enabled(): atr_api_settings = AutoTestRetriesSettings() - test_management_api_settings = CIVisibility.get_test_management_api_settings() - if test_management_api_settings is None or not CIVisibility.is_test_management_enabled(): + test_management_api_settings = instance.get_test_management_api_settings() + if test_management_api_settings is None or not instance.is_test_management_enabled(): test_management_api_settings = TestManagementSettings() session_settings = TestVisibilitySessionSettings( @@ -1082,18 +995,18 @@ def on_discover_session( suite_operation_name=suite_operation_name, test_operation_name=test_operation_name, workspace_path=workspace_path, - is_unsupported_ci=CIVisibility.is_unknown_ci(), - itr_enabled=CIVisibility.is_itr_enabled(), - itr_test_skipping_enabled=CIVisibility.test_skipping_enabled(), + is_unsupported_ci=instance.is_unknown_ci(), + itr_enabled=instance.is_itr_enabled(), + itr_test_skipping_enabled=instance.test_skipping_enabled(), itr_test_skipping_level=instance._itr_skipping_level, itr_correlation_id=instance._itr_meta.get(ITR_CORRELATION_ID_TAG_NAME, ""), - coverage_enabled=CIVisibility.should_collect_coverage(), - known_tests_enabled=CIVisibility.is_known_tests_enabled(), + coverage_enabled=instance.should_collect_coverage(), + known_tests_enabled=instance.is_known_tests_enabled(), efd_settings=efd_api_settings, atr_settings=atr_api_settings, test_management_settings=test_management_api_settings, - ci_provider_name=CIVisibility.ci_provider_name_for_telemetry(), - is_auto_injected=CIVisibility.is_auto_injected(), + ci_provider_name=instance.ci_provider_name_for_telemetry(), + is_auto_injected=instance.is_auto_injected(), ) session = TestVisibilitySession( @@ -1101,4 +1014,4 @@ def on_discover_session( ) CIVisibility.add_session(session) - CIVisibility.set_test_session_name(test_command=test_command) + instance.set_test_session_name(test_command=test_command) diff --git a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py index 41ec3432a6d..ff09a0e1fe3 100644 --- a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py +++ b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py @@ -21,12 +21,6 @@ def attempt_to_fix_has_failed_tests() -> bool: class AttemptToFixTestMixin: - # class AttemptToFixRetryFinishArgs(t.NamedTuple): - # test_id: InternalTestId - # retry_number: int - # status: ext_api.TestStatus - # exc_info: t.Optional[ext_api.TestExcInfo] - @staticmethod @_catch_and_log_exceptions def attempt_to_fix_should_retry(test_id: InternalTestId) -> bool: diff --git a/ddtrace/internal/test_visibility/_itr_mixins.py b/ddtrace/internal/test_visibility/_itr_mixins.py index 69d8746f904..d22c24401c5 100644 --- a/ddtrace/internal/test_visibility/_itr_mixins.py +++ b/ddtrace/internal/test_visibility/_itr_mixins.py @@ -55,16 +55,17 @@ def was_itr_forced_run(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> @_catch_and_log_exceptions def is_itr_skippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: log.debug("Checking if item %s is skippable by ITR", item_id) + ci_visibility_instance = require_ci_visibility_service() if not isinstance(item_id, (ext_api.TestSuiteId, InternalTestId)): log.warning("Only suites or tests can be skippable, not %s", type(item_id)) return False - if not require_ci_visibility_service().test_skipping_enabled(): + if not ci_visibility_instance.test_skipping_enabled(): log.debug("Test skipping is not enabled") return False - return require_ci_visibility_service().is_item_itr_skippable(item_id) + return ci_visibility_instance.is_item_itr_skippable(item_id) @staticmethod @_catch_and_log_exceptions diff --git a/ddtrace/internal/test_visibility/api.py b/ddtrace/internal/test_visibility/api.py index bf4c08c5ea3..05dec055ee9 100644 --- a/ddtrace/internal/test_visibility/api.py +++ b/ddtrace/internal/test_visibility/api.py @@ -155,15 +155,6 @@ class InternalTestSuite(ext_api.TestSuite, InternalTestBase, ITRMixin): class InternalTest( ext_api.Test, InternalTestBase, ITRMixin, EFDTestMixin, ATRTestMixin, AttemptToFixTestMixin, BenchmarkTestMixin ): - # class FinishArgs(NamedTuple): - # """InternalTest allows finishing with an overridden finish time (for EFD and other retry purposes)""" - - # test_id: InternalTestId - # status: t.Optional[TestStatus] = None - # skip_reason: t.Optional[str] = None - # exc_info: t.Optional[TestExcInfo] = None - # override_finish_time: t.Optional[float] = None - @staticmethod @_catch_and_log_exceptions def finish( From 3e72f8a53cd086c93fe38459b61cc9c21224d159 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Fri, 13 Jun 2025 12:53:20 +0200 Subject: [PATCH 30/58] avoid circular imports --- ddtrace/ext/test_visibility/_utils.py | 30 ---------------------- ddtrace/ext/test_visibility/api.py | 33 ++++++++++++++++++++----- ddtrace/internal/test_visibility/api.py | 6 ++--- 3 files changed, 29 insertions(+), 40 deletions(-) delete mode 100644 ddtrace/ext/test_visibility/_utils.py diff --git a/ddtrace/ext/test_visibility/_utils.py b/ddtrace/ext/test_visibility/_utils.py deleted file mode 100644 index b0bdf4f150d..00000000000 --- a/ddtrace/ext/test_visibility/_utils.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import Any -from typing import Dict -from typing import List - -from ddtrace.ext.test_visibility._test_visibility_base import TestVisibilityItemId -from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service - - -def _get_item_tag(item_id: TestVisibilityItemId, tag_name: str) -> Any: - return require_ci_visibility_service().get_item_by_id(item_id).get_tag(tag_name) - - -def _set_item_tag(item_id: TestVisibilityItemId, tag_name: str, tag_value: Any) -> None: - require_ci_visibility_service().get_item_by_id(item_id).set_tag(tag_name, tag_value) - - -def _set_item_tags(item_id: TestVisibilityItemId, tags: Dict[str, Any]) -> None: - require_ci_visibility_service().get_item_by_id(item_id).set_tags(tags) - - -def _delete_item_tag(item_id: TestVisibilityItemId, tag_name: str) -> None: - require_ci_visibility_service().get_item_by_id(item_id).delete_tag(tag_name) - - -def _delete_item_tags(item_id: TestVisibilityItemId, tag_names: List[str]) -> None: - require_ci_visibility_service().get_item_by_id(item_id).delete_tags(tag_names) - - -def _is_item_finished(item_id: TestVisibilityItemId) -> bool: - return require_ci_visibility_service().get_item_by_id(item_id).is_finished() diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 9c2bacd7588..7b2ef87f7b9 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -27,12 +27,6 @@ from ddtrace.ext.test_visibility._test_visibility_base import TestSessionId from ddtrace.ext.test_visibility._test_visibility_base import TestVisibilityItemId from ddtrace.ext.test_visibility._test_visibility_base import _TestVisibilityAPIBase -from ddtrace.ext.test_visibility._utils import _delete_item_tag -from ddtrace.ext.test_visibility._utils import _delete_item_tags -from ddtrace.ext.test_visibility._utils import _get_item_tag -from ddtrace.ext.test_visibility._utils import _is_item_finished -from ddtrace.ext.test_visibility._utils import _set_item_tag -from ddtrace.ext.test_visibility._utils import _set_item_tags from ddtrace.ext.test_visibility.status import TestExcInfo from ddtrace.ext.test_visibility.status import TestSourceFileInfo from ddtrace.ext.test_visibility.status import TestStatus @@ -40,6 +34,31 @@ from ddtrace.internal.logger import get_logger as _get_logger + +def _get_item_tag(item_id: TestVisibilityItemId, tag_name: str) -> Any: + return require_ci_visibility_service().get_item_by_id(item_id).get_tag(tag_name) + + +def _set_item_tag(item_id: TestVisibilityItemId, tag_name: str, tag_value: Any) -> None: + require_ci_visibility_service().get_item_by_id(item_id).set_tag(tag_name, tag_value) + + +def _set_item_tags(item_id: TestVisibilityItemId, tags: Dict[str, Any]) -> None: + require_ci_visibility_service().get_item_by_id(item_id).set_tags(tags) + + +def _delete_item_tag(item_id: TestVisibilityItemId, tag_name: str) -> None: + require_ci_visibility_service().get_item_by_id(item_id).delete_tag(tag_name) + + +def _delete_item_tags(item_id: TestVisibilityItemId, tag_names: List[str]) -> None: + require_ci_visibility_service().get_item_by_id(item_id).delete_tags(tag_names) + + +def _is_item_finished(item_id: TestVisibilityItemId) -> bool: + return require_ci_visibility_service().get_item_by_id(item_id).is_finished() + + log = _get_logger(__name__) # this triggers the registration of trace handlers after civis startup @@ -97,6 +116,7 @@ def set_tags(item_id: TestVisibilityItemId, tags: Dict[str, Any]): @staticmethod def delete_tag(item_id: TestVisibilityItemId, tag_name: str): + from ddtrace.ext.test_visibility._utils import _delete_item_tag _delete_item_tag(item_id, tag_name) @staticmethod @@ -175,6 +195,7 @@ def set_tags(tags: Dict[str, Any]): @staticmethod def delete_tag(tag_name: str): + from ddtrace.ext.test_visibility._utils import _delete_item_tag _delete_item_tag(TestSessionId(), tag_name) @staticmethod diff --git a/ddtrace/internal/test_visibility/api.py b/ddtrace/internal/test_visibility/api.py index 05dec055ee9..aa62fe99c40 100644 --- a/ddtrace/internal/test_visibility/api.py +++ b/ddtrace/internal/test_visibility/api.py @@ -5,7 +5,6 @@ from ddtrace.ext.test_visibility import api as ext_api from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions from ddtrace.ext.test_visibility._test_visibility_base import TestSessionId -from ddtrace.ext.test_visibility._utils import _is_item_finished from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service from ddtrace.internal.codeowners import Codeowners as _Codeowners from ddtrace.internal.logger import get_logger @@ -19,7 +18,6 @@ from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId from ddtrace.internal.test_visibility._itr_mixins import ITRMixin from ddtrace.internal.test_visibility._library_capabilities import LibraryCapabilities -from ddtrace.internal.test_visibility._utils import _get_item_span from ddtrace.trace import Span from ddtrace.trace import Tracer @@ -31,7 +29,7 @@ class InternalTestBase(ext_api.TestBase): @staticmethod @_catch_and_log_exceptions def get_span(item_id: t.Union[ext_api.TestVisibilityItemId, InternalTestId]) -> Span: - return _get_item_span(item_id) + return ext_api._get_item_span(item_id) @staticmethod @_catch_and_log_exceptions @@ -74,7 +72,7 @@ def get_span() -> Span: @staticmethod def is_finished() -> bool: - return _is_item_finished(TestSessionId()) + return ext_api._is_item_finished(TestSessionId()) @staticmethod @_catch_and_log_exceptions From dbae4787b59054966fef067382a9444fe7f8ec22 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Fri, 13 Jun 2025 12:57:37 +0200 Subject: [PATCH 31/58] remove more circular imports --- ddtrace/ext/test_visibility/api.py | 3 ++- ddtrace/internal/test_visibility/_utils.py | 14 -------------- ddtrace/internal/test_visibility/api.py | 6 +++++- 3 files changed, 7 insertions(+), 16 deletions(-) delete mode 100644 ddtrace/internal/test_visibility/_utils.py diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 7b2ef87f7b9..cdca64a1dba 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -34,7 +34,6 @@ from ddtrace.internal.logger import get_logger as _get_logger - def _get_item_tag(item_id: TestVisibilityItemId, tag_name: str) -> Any: return require_ci_visibility_service().get_item_by_id(item_id).get_tag(tag_name) @@ -117,6 +116,7 @@ def set_tags(item_id: TestVisibilityItemId, tags: Dict[str, Any]): @staticmethod def delete_tag(item_id: TestVisibilityItemId, tag_name: str): from ddtrace.ext.test_visibility._utils import _delete_item_tag + _delete_item_tag(item_id, tag_name) @staticmethod @@ -196,6 +196,7 @@ def set_tags(tags: Dict[str, Any]): @staticmethod def delete_tag(tag_name: str): from ddtrace.ext.test_visibility._utils import _delete_item_tag + _delete_item_tag(TestSessionId(), tag_name) @staticmethod diff --git a/ddtrace/internal/test_visibility/_utils.py b/ddtrace/internal/test_visibility/_utils.py deleted file mode 100644 index d828c377a4d..00000000000 --- a/ddtrace/internal/test_visibility/_utils.py +++ /dev/null @@ -1,14 +0,0 @@ -import typing as t - -from ddtrace.ext.test_visibility import api as ext_api -from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service -from ddtrace.internal.logger import get_logger -from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId -from ddtrace.trace import Span - - -log = get_logger(__name__) - - -def _get_item_span(item_id: t.Union[ext_api.TestVisibilityItemId, InternalTestId]) -> Span: - return require_ci_visibility_service().get_item_by_id(item_id).get_span() diff --git a/ddtrace/internal/test_visibility/api.py b/ddtrace/internal/test_visibility/api.py index aa62fe99c40..f7d539aeca5 100644 --- a/ddtrace/internal/test_visibility/api.py +++ b/ddtrace/internal/test_visibility/api.py @@ -25,11 +25,15 @@ log = get_logger(__name__) +def _get_item_span(item_id: t.Union[ext_api.TestVisibilityItemId, InternalTestId]) -> Span: + return require_ci_visibility_service().get_item_by_id(item_id).get_span() + + class InternalTestBase(ext_api.TestBase): @staticmethod @_catch_and_log_exceptions def get_span(item_id: t.Union[ext_api.TestVisibilityItemId, InternalTestId]) -> Span: - return ext_api._get_item_span(item_id) + return _get_item_span(item_id) @staticmethod @_catch_and_log_exceptions From ffdb58fb4d709bd2fa179c6ef9c01b80d939c34b Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Fri, 13 Jun 2025 13:06:31 +0200 Subject: [PATCH 32/58] typo --- ddtrace/ext/test_visibility/api.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index cdca64a1dba..97d537b55fc 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -115,8 +115,6 @@ def set_tags(item_id: TestVisibilityItemId, tags: Dict[str, Any]): @staticmethod def delete_tag(item_id: TestVisibilityItemId, tag_name: str): - from ddtrace.ext.test_visibility._utils import _delete_item_tag - _delete_item_tag(item_id, tag_name) @staticmethod @@ -195,8 +193,6 @@ def set_tags(tags: Dict[str, Any]): @staticmethod def delete_tag(tag_name: str): - from ddtrace.ext.test_visibility._utils import _delete_item_tag - _delete_item_tag(TestSessionId(), tag_name) @staticmethod From 94d21d7d1f5f67c6dafb1b1f7a363ad700d747d1 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Fri, 13 Jun 2025 15:29:57 +0200 Subject: [PATCH 33/58] remove catch and log exceptions --- ddtrace/ext/test_visibility/_decorators.py | 24 ---------- ddtrace/ext/test_visibility/api.py | 29 +++-------- ddtrace/internal/ci_visibility/recorder.py | 24 +++++----- .../internal/test_visibility/_atr_mixins.py | 8 ---- .../test_visibility/_attempt_to_fix_mixins.py | 7 --- .../test_visibility/_benchmark_mixin.py | 2 - .../internal/test_visibility/_efd_mixins.py | 9 ---- .../internal/test_visibility/_itr_mixins.py | 10 ---- ddtrace/internal/test_visibility/api.py | 48 +++++-------------- tests/ci_visibility/test_ci_visibility.py | 22 ++++----- 10 files changed, 43 insertions(+), 140 deletions(-) delete mode 100644 ddtrace/ext/test_visibility/_decorators.py diff --git a/ddtrace/ext/test_visibility/_decorators.py b/ddtrace/ext/test_visibility/_decorators.py deleted file mode 100644 index 055bd5f9e29..00000000000 --- a/ddtrace/ext/test_visibility/_decorators.py +++ /dev/null @@ -1,24 +0,0 @@ -from functools import wraps - -from ddtrace.internal.logger import get_logger - - -log = get_logger(__name__) - - -def _catch_and_log_exceptions(func): - """This decorator is meant to be used around all methods of the Test Visibility classes. - - It accepts an optional parameter to allow it to be used on functions and methods. - - No uncaught errors should ever reach the integration-side, and potentially cause crashes. - """ - - @wraps(func) - def wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except Exception: # noqa: E722 - log.error("Uncaught exception occurred while calling %s", func.__name__, exc_info=True) - - return wrapper diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 97d537b55fc..41a6421506a 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -20,7 +20,6 @@ from typing import Optional from ddtrace._trace.context import Context -from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions from ddtrace.ext.test_visibility._item_ids import TestId from ddtrace.ext.test_visibility._item_ids import TestModuleId from ddtrace.ext.test_visibility._item_ids import TestSuiteId @@ -74,7 +73,6 @@ class DEFAULT_OPERATION_NAMES(Enum): TEST = "test_visibility.test" -@_catch_and_log_exceptions def enable_test_visibility(config: Optional[Any] = None): log.debug("Enabling Test Visibility with config: %s", config) from ddtrace.internal.ci_visibility.recorder import CIVisibility @@ -85,12 +83,13 @@ def enable_test_visibility(config: Optional[Any] = None): log.warning("Failed to enable Test Visibility") -@_catch_and_log_exceptions def is_test_visibility_enabled(): - return require_ci_visibility_service().enabled + try: + return require_ci_visibility_service().enabled + finally: + return False -@_catch_and_log_exceptions def disable_test_visibility(): log.debug("Disabling Test Visibility") @@ -128,8 +127,8 @@ def is_finished(item_id: TestVisibilityItemId) -> bool: class TestSession(_TestVisibilityAPIBase): @staticmethod - @_catch_and_log_exceptions def discover( + item_id: TestVisibilityItemId, test_command: str, test_framework: str, test_framework_version: str, @@ -160,8 +159,7 @@ def discover( ) @staticmethod - @_catch_and_log_exceptions - def start(distributed_children: bool = False, context: Optional[Context] = None): + def start(item_id: TestVisibilityItemId, distributed_children: bool = False, context: Optional[Context] = None): log.debug("Starting session") session = require_ci_visibility_service().get_session() session.start(context) @@ -169,8 +167,8 @@ def start(distributed_children: bool = False, context: Optional[Context] = None) session.set_distributed_children() @staticmethod - @_catch_and_log_exceptions def finish( + item_id: TestVisibilityItemId, force_finish_children: bool = False, override_status: Optional[TestStatus] = None, ): @@ -202,7 +200,6 @@ def delete_tags(tag_names: List[str]): class TestModule(TestBase): @staticmethod - @_catch_and_log_exceptions def discover(item_id: TestModuleId, module_path: Optional[Path] = None): from ddtrace.internal.ci_visibility.api._module import TestVisibilityModule @@ -220,13 +217,11 @@ def discover(item_id: TestModuleId, module_path: Optional[Path] = None): ) @staticmethod - @_catch_and_log_exceptions def start(item_id: TestModuleId): log.debug("Starting module %s", item_id) require_ci_visibility_service().get_module_by_id(item_id).start() @staticmethod - @_catch_and_log_exceptions def finish( item_id: TestModuleId, override_status: Optional[TestStatus] = None, @@ -246,7 +241,6 @@ def finish( class TestSuite(TestBase): @staticmethod - @_catch_and_log_exceptions def discover( item_id: TestSuiteId, codeowners: Optional[List[str]] = None, @@ -270,13 +264,11 @@ def discover( ) @staticmethod - @_catch_and_log_exceptions def start(item_id: TestSuiteId): log.debug("Starting suite %s", item_id) require_ci_visibility_service().get_suite_by_id(item_id).start() @staticmethod - @_catch_and_log_exceptions def finish( item_id: TestSuiteId, force_finish_children: bool = False, @@ -294,7 +286,6 @@ def finish( class Test(TestBase): @staticmethod - @_catch_and_log_exceptions def discover( item_id: TestId, codeowners: Optional[List[str]] = None, @@ -349,14 +340,12 @@ def discover( ) @staticmethod - @_catch_and_log_exceptions def start(item_id: TestId): log.debug("Starting test %s", item_id) require_ci_visibility_service().get_test_by_id(item_id).start() @staticmethod - @_catch_and_log_exceptions def finish( item_id: TestId, status: TestStatus, @@ -374,26 +363,22 @@ def finish( require_ci_visibility_service().get_test_by_id(item_id).finish_test(status, skip_reason, exc_info) @staticmethod - @_catch_and_log_exceptions def set_parameters(item_id: TestId, params: str): log.debug("Setting test %s parameters to %s", item_id, params) require_ci_visibility_service().get_test_by_id(item_id).set_parameters(params) @staticmethod - @_catch_and_log_exceptions def mark_pass(item_id: TestId): log.debug("Marking test %s as passed", item_id) Test.finish(item_id, TestStatus.PASS) @staticmethod - @_catch_and_log_exceptions def mark_fail(item_id: TestId, exc_info: Optional[TestExcInfo] = None): log.debug("Marking test %s as failed, exc_info: %s", item_id, exc_info) Test.finish(item_id, TestStatus.FAIL, exc_info=exc_info) @staticmethod - @_catch_and_log_exceptions def mark_skip(item_id: TestId, skip_reason: Optional[str] = None): log.debug("Marking test %s as skipped, skip reason: %s", item_id, skip_reason) Test.finish(item_id, TestStatus.SKIP, skip_reason=skip_reason) diff --git a/ddtrace/internal/ci_visibility/recorder.py b/ddtrace/internal/ci_visibility/recorder.py index 716def91f26..82acc07bd47 100644 --- a/ddtrace/internal/ci_visibility/recorder.py +++ b/ddtrace/internal/ci_visibility/recorder.py @@ -556,6 +556,10 @@ def enable(cls, tracer=None, config=None, service=None) -> None: from ddtrace.internal.ci_visibility.service_registry import CIVisibilityServiceRegistry log.debug("Enabling %s", cls.__name__) + if cls._instance is not None: + log.debug("%s already enabled", cls.__name__) + return + if ddconfig._ci_visibility_agentless_enabled: if not os.getenv("_CI_DD_API_KEY", os.getenv("DD_API_KEY")): log.critical( @@ -566,12 +570,11 @@ def enable(cls, tracer=None, config=None, service=None) -> None: cls.enabled = False return - if cls._instance is not None: - log.debug("%s already enabled", cls.__name__) - return - try: cls._instance = cls(tracer=tracer, config=config, service=service) + # Register with service registry for other modules to access + CIVisibilityServiceRegistry.register(cls._instance) + except CIVisibilityAuthenticationException: log.warning("Authentication error, disabling CI Visibility, please check Datadog API key") cls.enabled = False @@ -582,9 +585,6 @@ def enable(cls, tracer=None, config=None, service=None) -> None: cls._instance.start() atexit.register(cls.disable) - # Register with service registry for other modules to access - CIVisibilityServiceRegistry.register(cls._instance) - log.debug("%s enabled", cls.__name__) log.info( "Final settings: coverage collection: %s, " @@ -594,11 +594,11 @@ def enable(cls, tracer=None, config=None, service=None) -> None: "Flaky Test Management: %s, " "Known Tests: %s", cls._instance._collect_coverage_enabled, - CIVisibility.test_skipping_enabled(), - CIVisibility.is_efd_enabled(), - CIVisibility.is_atr_enabled(), - CIVisibility.is_test_management_enabled(), - CIVisibility.is_known_tests_enabled(), + cls._instance.test_skipping_enabled(), + cls._instance.is_efd_enabled(), + cls._instance.is_atr_enabled(), + cls._instance.is_test_management_enabled(), + cls._instance.is_known_tests_enabled(), ) @classmethod diff --git a/ddtrace/internal/test_visibility/_atr_mixins.py b/ddtrace/internal/test_visibility/_atr_mixins.py index ec62741347d..c1b8d83740b 100644 --- a/ddtrace/internal/test_visibility/_atr_mixins.py +++ b/ddtrace/internal/test_visibility/_atr_mixins.py @@ -1,7 +1,6 @@ import dataclasses import typing as t -from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions from ddtrace.ext.test_visibility.status import TestExcInfo from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service @@ -21,13 +20,11 @@ class AutoTestRetriesSettings: class ATRSessionMixin: @staticmethod - @_catch_and_log_exceptions def atr_is_enabled() -> bool: log.debug("Checking if ATR is enabled") return require_ci_visibility_service().is_atr_enabled() @staticmethod - @_catch_and_log_exceptions def atr_has_failed_tests() -> bool: log.debug("Checking if ATR session has failed tests") return require_ci_visibility_service().get_session().atr_has_failed_tests() @@ -35,26 +32,22 @@ def atr_has_failed_tests() -> bool: class ATRTestMixin: @staticmethod - @_catch_and_log_exceptions def atr_should_retry(test_id: InternalTestId) -> bool: log.debug("Checking if test %s should be retried by ATR", test_id) return require_ci_visibility_service().get_test_by_id(test_id).atr_should_retry() @staticmethod - @_catch_and_log_exceptions def atr_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: retry_number = require_ci_visibility_service().get_test_by_id(test_id).atr_add_retry(start_immediately) log.debug("Adding ATR retry %s for test %s", retry_number, test_id) return retry_number @staticmethod - @_catch_and_log_exceptions def atr_start_retry(test_id: InternalTestId, retry_number: int) -> None: log.debug("Starting ATR retry %s for test %s", retry_number, test_id) require_ci_visibility_service().get_test_by_id(test_id).atr_start_retry(retry_number) @staticmethod - @_catch_and_log_exceptions def atr_finish_retry( test_id: InternalTestId, retry_number: int, @@ -67,7 +60,6 @@ def atr_finish_retry( ) @staticmethod - @_catch_and_log_exceptions def atr_get_final_status(test_id: InternalTestId) -> TestStatus: log.debug("Getting ATR final status for test %s", test_id) diff --git a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py index ff09a0e1fe3..36ec059b54e 100644 --- a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py +++ b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py @@ -1,6 +1,5 @@ import typing as t -from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions from ddtrace.ext.test_visibility.status import TestExcInfo from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service @@ -13,7 +12,6 @@ class AttemptToFixSessionMixin: @staticmethod - @_catch_and_log_exceptions def attempt_to_fix_has_failed_tests() -> bool: log.debug("Checking if attempt to fix session has failed tests") @@ -22,13 +20,11 @@ def attempt_to_fix_has_failed_tests() -> bool: class AttemptToFixTestMixin: @staticmethod - @_catch_and_log_exceptions def attempt_to_fix_should_retry(test_id: InternalTestId) -> bool: log.debug("Checking if test %s should be retried by attempt to fix", test_id) return require_ci_visibility_service().get_test_by_id(test_id).attempt_to_fix_should_retry() @staticmethod - @_catch_and_log_exceptions def attempt_to_fix_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: retry_number = ( require_ci_visibility_service().get_test_by_id(test_id).attempt_to_fix_add_retry(start_immediately) @@ -37,13 +33,11 @@ def attempt_to_fix_add_retry(test_id: InternalTestId, start_immediately: bool = return retry_number @staticmethod - @_catch_and_log_exceptions def attempt_to_fix_start_retry(test_id: InternalTestId, retry_number: int) -> None: log.debug("Starting attempt to fix retry %s for test %s", retry_number, test_id) require_ci_visibility_service().get_test_by_id(test_id).attempt_to_fix_start_retry(retry_number) @staticmethod - @_catch_and_log_exceptions def attempt_to_fix_finish_retry( test_id: InternalTestId, retry_number: int, @@ -56,7 +50,6 @@ def attempt_to_fix_finish_retry( ) @staticmethod - @_catch_and_log_exceptions def attempt_to_fix_get_final_status(test_id: InternalTestId) -> TestStatus: log.debug("Getting attempt to fix final status for test %s", test_id) return require_ci_visibility_service().get_test_by_id(test_id).attempt_to_fix_get_final_status() diff --git a/ddtrace/internal/test_visibility/_benchmark_mixin.py b/ddtrace/internal/test_visibility/_benchmark_mixin.py index 1f8f4d07c8c..d12e44cb053 100644 --- a/ddtrace/internal/test_visibility/_benchmark_mixin.py +++ b/ddtrace/internal/test_visibility/_benchmark_mixin.py @@ -1,6 +1,5 @@ import typing as t -from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions from ddtrace.internal import core from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId @@ -38,7 +37,6 @@ class SetBenchmarkDataArgs(t.NamedTuple): is_benchmark: bool = True @classmethod - @_catch_and_log_exceptions def set_benchmark_data( cls, item_id: InternalTestId, diff --git a/ddtrace/internal/test_visibility/_efd_mixins.py b/ddtrace/internal/test_visibility/_efd_mixins.py index 3a5137b64c8..6a9cc10da56 100644 --- a/ddtrace/internal/test_visibility/_efd_mixins.py +++ b/ddtrace/internal/test_visibility/_efd_mixins.py @@ -1,7 +1,6 @@ from enum import Enum import typing as t -from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions from ddtrace.ext.test_visibility.status import TestExcInfo from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service @@ -21,21 +20,18 @@ class EFDTestStatus(Enum): class EFDSessionMixin: @staticmethod - @_catch_and_log_exceptions def efd_enabled() -> bool: log.debug("Checking if EFD is enabled") return require_ci_visibility_service().get_session().efd_is_enabled() @staticmethod - @_catch_and_log_exceptions def efd_is_faulty_session() -> bool: log.debug("Checking if EFD session is faulty") return require_ci_visibility_service().get_session().efd_is_faulty_session() @staticmethod - @_catch_and_log_exceptions def efd_has_failed_tests() -> bool: log.debug("Checking if EFD session has failed tests") @@ -44,28 +40,24 @@ def efd_has_failed_tests() -> bool: class EFDTestMixin: @staticmethod - @_catch_and_log_exceptions def efd_should_retry(test_id: InternalTestId) -> bool: log.debug("Checking if test %s should be retried by EFD", test_id) return require_ci_visibility_service().get_test_by_id(test_id).efd_should_retry() @staticmethod - @_catch_and_log_exceptions def efd_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: retry_number = require_ci_visibility_service().get_test_by_id(test_id).efd_add_retry(start_immediately) log.debug("Adding EFD retry %s for test %s", retry_number, test_id) return retry_number @staticmethod - @_catch_and_log_exceptions def efd_start_retry(test_id: InternalTestId, retry_number: int) -> None: log.debug("Starting EFD retry %s for test %s", retry_number, test_id) require_ci_visibility_service().get_test_by_id(test_id).efd_start_retry(retry_number) @staticmethod - @_catch_and_log_exceptions def efd_finish_retry( item_id: InternalTestId, retry_number: int, @@ -86,7 +78,6 @@ def efd_finish_retry( ) @staticmethod - @_catch_and_log_exceptions def efd_get_final_status(test_id: InternalTestId) -> EFDTestStatus: log.debug("Getting EFD final status for test %s", test_id) diff --git a/ddtrace/internal/test_visibility/_itr_mixins.py b/ddtrace/internal/test_visibility/_itr_mixins.py index d22c24401c5..b4d23eda702 100644 --- a/ddtrace/internal/test_visibility/_itr_mixins.py +++ b/ddtrace/internal/test_visibility/_itr_mixins.py @@ -2,7 +2,6 @@ import typing as t from ddtrace.ext.test_visibility import api as ext_api -from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions from ddtrace.internal.ci_visibility.errors import CIVisibilityError from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service from ddtrace.internal.logger import get_logger @@ -21,7 +20,6 @@ class AddCoverageArgs(t.NamedTuple): coverage_data: t.Dict[Path, CoverageLines] @staticmethod - @_catch_and_log_exceptions def mark_itr_skipped(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): log.debug("Marking item %s as skipped by ITR", item_id) @@ -31,28 +29,24 @@ def mark_itr_skipped(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): require_ci_visibility_service().get_item_by_id(item_id).finish_itr_skipped() @staticmethod - @_catch_and_log_exceptions def mark_itr_unskippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): log.debug("Marking item %s as unskippable by ITR", item_id) require_ci_visibility_service().get_item_by_id(item_id).mark_itr_unskippable() @staticmethod - @_catch_and_log_exceptions def mark_itr_forced_run(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): log.debug("Marking item %s as forced run by ITR", item_id) require_ci_visibility_service().get_item_by_id(item_id).mark_itr_forced_run() @staticmethod - @_catch_and_log_exceptions def was_itr_forced_run(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: log.debug("Checking if item %s was forced run by ITR", item_id) return require_ci_visibility_service().get_item_by_id(item_id).was_itr_forced_run() @staticmethod - @_catch_and_log_exceptions def is_itr_skippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: log.debug("Checking if item %s is skippable by ITR", item_id) ci_visibility_instance = require_ci_visibility_service() @@ -68,7 +62,6 @@ def is_itr_skippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> b return ci_visibility_instance.is_item_itr_skippable(item_id) @staticmethod - @_catch_and_log_exceptions def is_itr_unskippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: log.debug("Checking if item %s is unskippable by ITR", item_id) @@ -77,14 +70,12 @@ def is_itr_unskippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> return require_ci_visibility_service().get_item_by_id(item_id).is_itr_unskippable() @staticmethod - @_catch_and_log_exceptions def was_itr_skipped(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: log.debug("Checking if item %s was skipped by ITR", item_id) return require_ci_visibility_service().get_item_by_id(item_id).is_itr_skipped() @staticmethod - @_catch_and_log_exceptions def add_coverage_data(item_id, coverage_data) -> None: """Adds coverage data to an item, merging with existing coverage data if necessary""" log.debug("Adding coverage data for item id %s", item_id) @@ -96,7 +87,6 @@ def add_coverage_data(item_id, coverage_data) -> None: require_ci_visibility_service().get_item_by_id(item_id).add_coverage_data(coverage_data) @staticmethod - @_catch_and_log_exceptions def get_coverage_data( item_id: t.Union[ext_api.TestSuiteId, InternalTestId] ) -> t.Optional[t.Dict[Path, CoverageLines]]: diff --git a/ddtrace/internal/test_visibility/api.py b/ddtrace/internal/test_visibility/api.py index f7d539aeca5..15ececc340a 100644 --- a/ddtrace/internal/test_visibility/api.py +++ b/ddtrace/internal/test_visibility/api.py @@ -3,7 +3,6 @@ from typing import NamedTuple from ddtrace.ext.test_visibility import api as ext_api -from ddtrace.ext.test_visibility._decorators import _catch_and_log_exceptions from ddtrace.ext.test_visibility._test_visibility_base import TestSessionId from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service from ddtrace.internal.codeowners import Codeowners as _Codeowners @@ -31,41 +30,42 @@ def _get_item_span(item_id: t.Union[ext_api.TestVisibilityItemId, InternalTestId class InternalTestBase(ext_api.TestBase): @staticmethod - @_catch_and_log_exceptions def get_span(item_id: t.Union[ext_api.TestVisibilityItemId, InternalTestId]) -> Span: return _get_item_span(item_id) @staticmethod - @_catch_and_log_exceptions def stash_set(item_id, key: str, value: object): log.debug("Stashing value %s for key %s in item %s", value, key, item_id) require_ci_visibility_service().get_item_by_id(item_id).stash_set(key, value) @staticmethod - @_catch_and_log_exceptions def stash_get(item_id: ext_api.TestVisibilityItemId, key: str) -> t.Optional[object]: log.debug("Getting stashed value for key %s in item %s", key, item_id) return require_ci_visibility_service().get_item_by_id(item_id).stash_get(key) @staticmethod - @_catch_and_log_exceptions def stash_delete(item_id: ext_api.TestVisibilityItemId, key: str): log.debug("Deleting stashed value for key %s in item %s", key, item_id) require_ci_visibility_service().get_item_by_id(item_id).stash_delete(key) @staticmethod - @_catch_and_log_exceptions - def overwrite_attributes(overwrite_attribute_args: "InternalTest.OverwriteAttributesArgs") -> None: - log.debug("Overwriting attributes: %s", overwrite_attribute_args) + def overwrite_attributes( + item_id: InternalTestId, + name: t.Optional[str] = None, + suite_name: t.Optional[str] = None, + parameters: t.Optional[str] = None, + codeowners: t.Optional[t.List[str]] = None, + ) -> None: + log.debug("Overwriting attributes for: %s", item_id) - require_ci_visibility_service().get_test_by_id(overwrite_attribute_args.test_id).overwrite_attributes( - overwrite_attribute_args.name, - overwrite_attribute_args.suite_name, - overwrite_attribute_args.parameters, - overwrite_attribute_args.codeowners, + require_ci_visibility_service().get_test_by_id(item_id).overwrite_attributes( + name, + suite_name, + parameters, + codeowners, ) @@ -79,21 +79,18 @@ def is_finished() -> bool: return ext_api._is_item_finished(TestSessionId()) @staticmethod - @_catch_and_log_exceptions def get_codeowners() -> t.Optional[_Codeowners]: log.debug("Getting codeowners") return require_ci_visibility_service().get_codeowners() @staticmethod - @_catch_and_log_exceptions def get_tracer() -> t.Optional[Tracer]: log.debug("Getting tracer") return require_ci_visibility_service().get_tracer() @staticmethod - @_catch_and_log_exceptions def get_workspace_path() -> t.Optional[Path]: log.debug("Getting workspace path") @@ -101,28 +98,24 @@ def get_workspace_path() -> t.Optional[Path]: return Path(path_str) if path_str is not None else None @staticmethod - @_catch_and_log_exceptions def should_collect_coverage() -> bool: log.debug("Checking if should collect coverage") return require_ci_visibility_service().should_collect_coverage() @staticmethod - @_catch_and_log_exceptions def is_test_skipping_enabled() -> bool: log.debug("Checking if test skipping is enabled") return require_ci_visibility_service().test_skipping_enabled() @staticmethod - @_catch_and_log_exceptions def set_covered_lines_pct(coverage_pct: float) -> None: log.debug("Setting coverage percentage for session to %s", coverage_pct) require_ci_visibility_service().get_session().set_covered_lines_pct(coverage_pct) @staticmethod - @_catch_and_log_exceptions def get_path_codeowners(path: Path) -> t.Optional[t.List[str]]: log.debug("Getting codeowners for path %s", path) @@ -132,14 +125,12 @@ def get_path_codeowners(path: Path) -> t.Optional[t.List[str]]: return codeowners.of(str(path)) @staticmethod - @_catch_and_log_exceptions def set_library_capabilities(capabilities: LibraryCapabilities) -> None: log.debug("Setting library capabilities") require_ci_visibility_service().set_library_capabilities(capabilities) @staticmethod - @_catch_and_log_exceptions def set_itr_skipped_count(skipped_count: int) -> None: log.debug("Setting skipped count: %d", skipped_count) @@ -158,7 +149,6 @@ class InternalTest( ext_api.Test, InternalTestBase, ITRMixin, EFDTestMixin, ATRTestMixin, AttemptToFixTestMixin, BenchmarkTestMixin ): @staticmethod - @_catch_and_log_exceptions def finish( item_id: InternalTestId, status: t.Optional[ext_api.TestStatus] = None, @@ -173,42 +163,30 @@ def finish( ) @staticmethod - @_catch_and_log_exceptions def is_new_test(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: log.debug("Checking if test %s is new", test_id) return require_ci_visibility_service().get_test_by_id(test_id).is_new() @staticmethod - @_catch_and_log_exceptions def is_quarantined_test(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: log.debug("Checking if test %s is quarantined", test_id) return require_ci_visibility_service().get_test_by_id(test_id).is_quarantined() @staticmethod - @_catch_and_log_exceptions def is_disabled_test(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: log.debug("Checking if test %s is disabled", test_id) return require_ci_visibility_service().get_test_by_id(test_id).is_disabled() @staticmethod - @_catch_and_log_exceptions def is_attempt_to_fix(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: log.debug("Checking if test %s is attempt to fix", test_id) return require_ci_visibility_service().get_test_by_id(test_id).is_attempt_to_fix() - class OverwriteAttributesArgs(NamedTuple): - test_id: InternalTestId - name: t.Optional[str] = None - suite_name: t.Optional[str] = None - parameters: t.Optional[str] = None - codeowners: t.Optional[t.List[str]] = None - @staticmethod - @_catch_and_log_exceptions def overwrite_attributes( item_id: InternalTestId, name: t.Optional[str] = None, diff --git a/tests/ci_visibility/test_ci_visibility.py b/tests/ci_visibility/test_ci_visibility.py index ccf48ad315b..431c0ee7f6b 100644 --- a/tests/ci_visibility/test_ci_visibility.py +++ b/tests/ci_visibility/test_ci_visibility.py @@ -1408,15 +1408,15 @@ def test_is_item_itr_skippable_test_level(self): # Check skippable tests are correct for test_id in expected_skippable_test_ids: - assert CIVisibility.is_item_itr_skippable(test_id) is True + assert CIVisibility._instance.is_item_itr_skippable(test_id) is True # Check non-skippable tests are correct for test_id in expected_non_skippable_test_ids: - assert CIVisibility.is_item_itr_skippable(test_id) is False + assert CIVisibility._instance.is_item_itr_skippable(test_id) is False # Check all suites are not skippable for suite_id in self._get_all_suite_ids(): - assert CIVisibility.is_item_itr_skippable(suite_id) is False + assert CIVisibility._instance.is_item_itr_skippable(suite_id) is False def test_is_item_itr_skippable_suite_level(self): with mock.patch.object(CIVisibility, "enabled", True), mock.patch.object( @@ -1432,15 +1432,15 @@ def test_is_item_itr_skippable_suite_level(self): # Check skippable suites are correct for suite_id in expected_skippable_suite_ids: - assert CIVisibility.is_item_itr_skippable(suite_id) is True + assert CIVisibility._instance.is_item_itr_skippable(suite_id) is True # Check non-skippable suites are correct for suite_id in expected_non_skippable_suite_ids: - assert CIVisibility.is_item_itr_skippable(suite_id) is False + assert CIVisibility._instance.is_item_itr_skippable(suite_id) is False # Check all tests are not skippable for test_id in self._get_all_test_ids(): - assert CIVisibility.is_item_itr_skippable(test_id) is False + assert CIVisibility._instance.is_item_itr_skippable(test_id) is False class TestCIVisibilitySetTestSessionName(TracerTestCase): @@ -1468,7 +1468,7 @@ def test_set_test_session_name_from_command(self): """ with _ci_override_env(dict()), set_up_mock_civisibility(), _patch_dummy_writer(): CIVisibility.enable() - CIVisibility.set_test_session_name(test_command="some_command") + CIVisibility._instance.set_test_session_name(test_command="some_command") self.assert_test_session_name("some_command") def test_set_test_session_name_from_dd_test_session_name_env_var(self): @@ -1479,7 +1479,7 @@ def test_set_test_session_name_from_dd_test_session_name_env_var(self): ) ), set_up_mock_civisibility(), _patch_dummy_writer(): CIVisibility.enable() - CIVisibility.set_test_session_name(test_command="some_command") + CIVisibility._instance.set_test_session_name(test_command="some_command") self.assert_test_session_name("the_name") def test_set_test_session_name_from_job_name_and_command(self): @@ -1493,7 +1493,7 @@ def test_set_test_session_name_from_job_name_and_command(self): ) ), set_up_mock_civisibility(), _patch_dummy_writer(): CIVisibility.enable() - CIVisibility.set_test_session_name(test_command="some_command") + CIVisibility._instance.set_test_session_name(test_command="some_command") self.assert_test_session_name("the_job-some_command") def test_set_test_session_name_from_dd_test_session_name_env_var_priority(self): @@ -1506,7 +1506,7 @@ def test_set_test_session_name_from_dd_test_session_name_env_var_priority(self): ) ), set_up_mock_civisibility(), _patch_dummy_writer(): CIVisibility.enable() - CIVisibility.set_test_session_name(test_command="some_command") + CIVisibility._instance.set_test_session_name(test_command="some_command") self.assert_test_session_name("the_name") @@ -1522,7 +1522,7 @@ def tearDown(self): def test_set_library_capabilities(self): with _ci_override_env(), set_up_mock_civisibility(), _patch_dummy_writer(): CIVisibility.enable() - CIVisibility.set_library_capabilities( + CIVisibility._instance.set_library_capabilities( LibraryCapabilities( early_flake_detection="1", auto_test_retries=None, From 80297ad5ba4af0c2df3a4f4926e78b45456df745 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Fri, 13 Jun 2025 17:15:52 +0200 Subject: [PATCH 34/58] =?UTF-8?q?=F0=9F=92=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ddtrace/contrib/internal/pytest/_plugin_v2.py | 8 ++-- .../contrib/internal/pytest/_report_links.py | 9 +++-- ddtrace/ext/test_visibility/_item_ids.py | 38 ------------------- .../test_visibility/_test_visibility_base.py | 34 ++++++++++++++++- ddtrace/ext/test_visibility/api.py | 24 ++++++------ ddtrace/internal/ci_visibility/_api_client.py | 4 +- ddtrace/internal/ci_visibility/api/_base.py | 12 +++--- ddtrace/internal/ci_visibility/api/_module.py | 4 +- .../internal/ci_visibility/api/_session.py | 2 +- ddtrace/internal/ci_visibility/api/_suite.py | 4 +- ddtrace/internal/ci_visibility/api/_test.py | 17 +++++++-- ddtrace/internal/ci_visibility/recorder.py | 6 +-- .../internal/test_visibility/_atr_mixins.py | 18 ++++----- .../test_visibility/_attempt_to_fix_mixins.py | 33 ++++++++-------- .../internal/test_visibility/_efd_mixins.py | 24 ++++++------ ddtrace/internal/test_visibility/api.py | 3 +- .../api/test_internal_ci_visibility_api.py | 6 +-- tests/ci_visibility/api_client/_util.py | 4 +- tests/ci_visibility/test_efd.py | 4 +- tests/contrib/pytest/test_pytest_xdist_itr.py | 8 ++-- 20 files changed, 136 insertions(+), 126 deletions(-) delete mode 100644 ddtrace/ext/test_visibility/_item_ids.py diff --git a/ddtrace/contrib/internal/pytest/_plugin_v2.py b/ddtrace/contrib/internal/pytest/_plugin_v2.py index 821a89d69d4..907914e283b 100644 --- a/ddtrace/contrib/internal/pytest/_plugin_v2.py +++ b/ddtrace/contrib/internal/pytest/_plugin_v2.py @@ -319,6 +319,7 @@ def pytest_sessionstart(session: pytest.Session) -> None: ) InternalTestSession.discover( + item_id=None, # unused arg test_command=command, test_framework=FRAMEWORK, test_framework_version=pytest.__version__, @@ -440,7 +441,7 @@ def _pytest_runtest_protocol_pre_yield(item) -> t.Optional[ModuleCodeCollector.C _handle_test_management(item, test_id) _handle_itr_should_skip(item, test_id) - item_will_skip = _pytest_marked_to_skip(item) or InternalTest.was_skipped_by_itr(test_id) + item_will_skip = _pytest_marked_to_skip(item) or InternalTest.was_itr_skipped(test_id) collect_test_coverage = InternalTestSession.should_collect_coverage() and not item_will_skip @@ -469,7 +470,7 @@ def _pytest_runtest_protocol_post_yield(item, nextitem, coverage_collector): # - we trust that the next item is in the same module if it is in the same suite next_test_id = _get_test_id_from_item(nextitem) if nextitem else None if next_test_id is None or next_test_id.parent_id != suite_id: - if InternalTestSuite.is_itr_skippable(suite_id) and not InternalTestSuite.was_forced_run(suite_id): + if InternalTestSuite.is_itr_skippable(suite_id) and not InternalTestSuite.was_itr_forced_run(suite_id): InternalTestSuite.mark_itr_skipped(suite_id) else: _handle_coverage_dependencies(suite_id) @@ -610,7 +611,7 @@ def _process_result(item, result) -> _TestOutcome: # If run with --runxfail flag, tests behave as if they were not marked with xfail, # that's why no XFAIL_REASON or test.RESULT tags will be added. if result.skipped: - if InternalTest.was_skipped_by_itr(test_id): + if InternalTest.was_itr_skipped(test_id): # Items that were skipped by ITR already have their status and reason set return _TestOutcome() @@ -796,6 +797,7 @@ def _pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: InternalTestSession.set_itr_tags(skipped_count) InternalTestSession.finish( + item_id=None, # unused argument force_finish_children=True, override_status=TestStatus.FAIL if session.exitstatus == pytest.ExitCode.TESTS_FAILED else None, ) diff --git a/ddtrace/contrib/internal/pytest/_report_links.py b/ddtrace/contrib/internal/pytest/_report_links.py index 3f5b76bc08e..d57b0526c54 100644 --- a/ddtrace/contrib/internal/pytest/_report_links.py +++ b/ddtrace/contrib/internal/pytest/_report_links.py @@ -3,7 +3,7 @@ from urllib.parse import quote from ddtrace.ext import ci -from ddtrace.internal.ci_visibility import CIVisibility +from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service DEFAULT_DATADOG_SITE = "datadoghq.com" @@ -16,10 +16,11 @@ def print_test_report_links(terminalreporter): base_url = _get_base_url( dd_site=os.getenv("DD_SITE", DEFAULT_DATADOG_SITE), dd_subdomain=os.getenv("DD_SUBDOMAIN", "") ) - ci_tags = CIVisibility.get_ci_tags() - settings = CIVisibility.get_session_settings() + ci_visibility_instance = require_ci_visibility_service() + ci_tags = ci_visibility_instance.get_ci_tags() + settings = ci_visibility_instance.get_session_settings() service = settings.test_service - env = CIVisibility.get_dd_env() + env = ci_visibility_instance.get_dd_env() redirect_test_commit_url = _build_test_commit_redirect_url(base_url, ci_tags, service, env) test_runs_url = _build_test_runs_url(base_url, ci_tags) diff --git a/ddtrace/ext/test_visibility/_item_ids.py b/ddtrace/ext/test_visibility/_item_ids.py deleted file mode 100644 index aec7bd21ab6..00000000000 --- a/ddtrace/ext/test_visibility/_item_ids.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Provides identifier classes for items used in the Test Visibility API - -NOTE: BETA - this API is currently in development and is subject to change. -""" -import dataclasses -from typing import Optional - -from ddtrace.ext.test_visibility._test_visibility_base import _TestVisibilityChildItemIdBase -from ddtrace.ext.test_visibility._test_visibility_base import _TestVisibilityRootItemIdBase - - -@dataclasses.dataclass(frozen=True) -class TestModuleId(_TestVisibilityRootItemIdBase): - name: str - - def __repr__(self): - return "TestModuleId(module={})".format( - self.name, - ) - - -@dataclasses.dataclass(frozen=True) -class TestSuiteId(_TestVisibilityChildItemIdBase[TestModuleId]): - def __repr__(self): - return "TestSuiteId(module={}, suite={})".format(self.parent_id.name, self.name) - - -@dataclasses.dataclass(frozen=True) -class TestId(_TestVisibilityChildItemIdBase[TestSuiteId]): - parameters: Optional[str] = None # For hashability, a JSON string of a dictionary of parameters - - def __repr__(self): - return "TestId(module={}, suite={}, test={}, parameters={})".format( - self.parent_id.parent_id.name, - self.parent_id.name, - self.name, - self.parameters, - ) diff --git a/ddtrace/ext/test_visibility/_test_visibility_base.py b/ddtrace/ext/test_visibility/_test_visibility_base.py index 883d7da410a..5c26ebcbb97 100644 --- a/ddtrace/ext/test_visibility/_test_visibility_base.py +++ b/ddtrace/ext/test_visibility/_test_visibility_base.py @@ -61,8 +61,40 @@ def get_parent_id(self) -> PT: return self.parent_id +@dataclasses.dataclass(frozen=True) +class TestModuleId(_TestVisibilityRootItemIdBase): + name: str + + def __repr__(self): + return "TestModuleId(module={})".format( + self.name, + ) + + +@dataclasses.dataclass(frozen=True) +class TestSuiteId(_TestVisibilityChildItemIdBase[TestModuleId]): + def __repr__(self): + return "TestSuiteId(module={}, suite={})".format(self.parent_id.name, self.name) + + +@dataclasses.dataclass(frozen=True) +class TestId(_TestVisibilityChildItemIdBase[TestSuiteId]): + parameters: Optional[str] = None # For hashability, a JSON string of a dictionary of parameters + + def __repr__(self): + return "TestId(module={}, suite={}, test={}, parameters={})".format( + self.parent_id.parent_id.name, + self.parent_id.name, + self.name, + self.parameters, + ) + + TestVisibilityItemId = TypeVar( - "TestVisibilityItemId", bound=Union[_TestVisibilityChildItemIdBase, _TestVisibilityRootItemIdBase, TestSessionId] + "TestVisibilityItemId", + bound=Union[ + _TestVisibilityChildItemIdBase, _TestVisibilityRootItemIdBase, TestSessionId, TestModuleId, TestSuiteId, TestId + ], ) diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 41a6421506a..dfd51e99cc0 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -20,10 +20,10 @@ from typing import Optional from ddtrace._trace.context import Context -from ddtrace.ext.test_visibility._item_ids import TestId -from ddtrace.ext.test_visibility._item_ids import TestModuleId -from ddtrace.ext.test_visibility._item_ids import TestSuiteId +from ddtrace.ext.test_visibility._test_visibility_base import TestId +from ddtrace.ext.test_visibility._test_visibility_base import TestModuleId from ddtrace.ext.test_visibility._test_visibility_base import TestSessionId +from ddtrace.ext.test_visibility._test_visibility_base import TestSuiteId from ddtrace.ext.test_visibility._test_visibility_base import TestVisibilityItemId from ddtrace.ext.test_visibility._test_visibility_base import _TestVisibilityAPIBase from ddtrace.ext.test_visibility.status import TestExcInfo @@ -86,13 +86,13 @@ def enable_test_visibility(config: Optional[Any] = None): def is_test_visibility_enabled(): try: return require_ci_visibility_service().enabled - finally: + except RuntimeError: return False + return False def disable_test_visibility(): log.debug("Disabling Test Visibility") - ci_visibility_instance = require_ci_visibility_service() ci_visibility_instance.disable() if ci_visibility_instance.enabled: @@ -169,8 +169,8 @@ def start(item_id: TestVisibilityItemId, distributed_children: bool = False, con @staticmethod def finish( item_id: TestVisibilityItemId, - force_finish_children: bool = False, override_status: Optional[TestStatus] = None, + force_finish_children: bool = False, ): log.debug("Finishing session, force_finish_session_modules: %s", force_finish_children) @@ -200,7 +200,7 @@ def delete_tags(tag_names: List[str]): class TestModule(TestBase): @staticmethod - def discover(item_id: TestModuleId, module_path: Optional[Path] = None): + def discover(item_id: TestModuleId, *args, **kwargs): from ddtrace.internal.ci_visibility.api._module import TestVisibilityModule log.debug("Registered module %s", item_id) @@ -212,12 +212,12 @@ def discover(item_id: TestModuleId, module_path: Optional[Path] = None): TestVisibilityModule( item_id.name, ci_visibility_instance.get_session_settings(), - module_path, + kwargs.get("module_path"), ), ) @staticmethod - def start(item_id: TestModuleId): + def start(item_id: TestModuleId, *args, **kwargs): log.debug("Starting module %s", item_id) require_ci_visibility_service().get_module_by_id(item_id).start() @@ -360,13 +360,15 @@ def finish( exc_info, ) - require_ci_visibility_service().get_test_by_id(item_id).finish_test(status, skip_reason, exc_info) + require_ci_visibility_service().get_test_by_id(item_id).finish_test( + status=status, skip_reason=skip_reason, exc_info=exc_info + ) @staticmethod def set_parameters(item_id: TestId, params: str): log.debug("Setting test %s parameters to %s", item_id, params) - require_ci_visibility_service().get_test_by_id(item_id).set_parameters(params) + require_ci_visibility_service().get_test_by_id(item_id).set_parameters(parameters=params) @staticmethod def mark_pass(item_id: TestId): diff --git a/ddtrace/internal/ci_visibility/_api_client.py b/ddtrace/internal/ci_visibility/_api_client.py index 86f6ab4029a..86e062e23fa 100644 --- a/ddtrace/internal/ci_visibility/_api_client.py +++ b/ddtrace/internal/ci_visibility/_api_client.py @@ -10,8 +10,8 @@ from uuid import uuid4 from ddtrace.ext.test_visibility import ITR_SKIPPING_LEVEL -from ddtrace.ext.test_visibility._item_ids import TestModuleId -from ddtrace.ext.test_visibility._item_ids import TestSuiteId +from ddtrace.ext.test_visibility._test_visibility_base import TestModuleId +from ddtrace.ext.test_visibility._test_visibility_base import TestSuiteId from ddtrace.internal.ci_visibility.constants import AGENTLESS_API_KEY_HEADER_NAME from ddtrace.internal.ci_visibility.constants import AGENTLESS_DEFAULT_SITE from ddtrace.internal.ci_visibility.constants import EVP_PROXY_AGENT_BASE_PATH diff --git a/ddtrace/internal/ci_visibility/api/_base.py b/ddtrace/internal/ci_visibility/api/_base.py index bfdeb8ba2c4..6eb5a95e35b 100644 --- a/ddtrace/internal/ci_visibility/api/_base.py +++ b/ddtrace/internal/ci_visibility/api/_base.py @@ -18,9 +18,9 @@ from ddtrace.ext import SpanTypes from ddtrace.ext import test from ddtrace.ext.test_visibility import ITR_SKIPPING_LEVEL -from ddtrace.ext.test_visibility._item_ids import TestId -from ddtrace.ext.test_visibility._item_ids import TestModuleId -from ddtrace.ext.test_visibility._item_ids import TestSuiteId +from ddtrace.ext.test_visibility._test_visibility_base import TestId +from ddtrace.ext.test_visibility._test_visibility_base import TestModuleId +from ddtrace.ext.test_visibility._test_visibility_base import TestSuiteId from ddtrace.ext.test_visibility.status import TestSourceFileInfo from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings @@ -156,7 +156,7 @@ def __init__( # General purpose attributes not used by all item types self._codeowners: Optional[List[str]] = [] - self._source_file_info: Optional["TestSourceFileInfo"] = None + self._source_file_info: Optional[TestSourceFileInfo] = None self._coverage_data: Optional[TestVisibilityCoverageData] = None def __repr__(self) -> str: @@ -312,11 +312,11 @@ def _set_span_tags(self): pass @property - def _source_file_info(self) -> Optional["TestSourceFileInfo"]: + def _source_file_info(self) -> Optional[TestSourceFileInfo]: return self.__source_file_info @_source_file_info.setter - def _source_file_info(self, source_file_info_value: Optional["TestSourceFileInfo"] = None): + def _source_file_info(self, source_file_info_value: Optional[TestSourceFileInfo] = None): """This checks that filepaths are absolute when setting source file info""" self.__source_file_info = None # Default value until source_file_info is validated diff --git a/ddtrace/internal/ci_visibility/api/_module.py b/ddtrace/internal/ci_visibility/api/_module.py index 306f391ec3a..b029954b043 100644 --- a/ddtrace/internal/ci_visibility/api/_module.py +++ b/ddtrace/internal/ci_visibility/api/_module.py @@ -3,8 +3,8 @@ from ddtrace.ext import test from ddtrace.ext.test_visibility import ITR_SKIPPING_LEVEL -from ddtrace.ext.test_visibility._item_ids import TestModuleId -from ddtrace.ext.test_visibility._item_ids import TestSuiteId +from ddtrace.ext.test_visibility._test_visibility_base import TestModuleId +from ddtrace.ext.test_visibility._test_visibility_base import TestSuiteId from ddtrace.internal.ci_visibility.api._base import TestVisibilityChildItem from ddtrace.internal.ci_visibility.api._base import TestVisibilityParentItem from ddtrace.internal.ci_visibility.api._base import TestVisibilitySessionSettings diff --git a/ddtrace/internal/ci_visibility/api/_session.py b/ddtrace/internal/ci_visibility/api/_session.py index 7e368f0cf6d..90f87b6aa83 100644 --- a/ddtrace/internal/ci_visibility/api/_session.py +++ b/ddtrace/internal/ci_visibility/api/_session.py @@ -4,7 +4,7 @@ from ddtrace.ext import test from ddtrace.ext.test_visibility import ITR_SKIPPING_LEVEL -from ddtrace.ext.test_visibility._item_ids import TestModuleId +from ddtrace.ext.test_visibility._test_visibility_base import TestModuleId from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.internal.ci_visibility.api._base import TestVisibilityParentItem from ddtrace.internal.ci_visibility.api._base import TestVisibilitySessionSettings diff --git a/ddtrace/internal/ci_visibility/api/_suite.py b/ddtrace/internal/ci_visibility/api/_suite.py index 6e971a52b69..6c8da13ac6e 100644 --- a/ddtrace/internal/ci_visibility/api/_suite.py +++ b/ddtrace/internal/ci_visibility/api/_suite.py @@ -4,8 +4,8 @@ from typing import Optional from ddtrace.ext import test -from ddtrace.ext.test_visibility._item_ids import TestId -from ddtrace.ext.test_visibility._item_ids import TestSuiteId +from ddtrace.ext.test_visibility._test_visibility_base import TestId +from ddtrace.ext.test_visibility._test_visibility_base import TestSuiteId from ddtrace.ext.test_visibility.status import TestSourceFileInfo from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.internal.ci_visibility.api._base import TestVisibilityChildItem diff --git a/ddtrace/internal/ci_visibility/api/_test.py b/ddtrace/internal/ci_visibility/api/_test.py index f37025922af..c0b4d7f6c85 100644 --- a/ddtrace/internal/ci_visibility/api/_test.py +++ b/ddtrace/internal/ci_visibility/api/_test.py @@ -9,7 +9,7 @@ from ddtrace.ext import SpanTypes from ddtrace.ext import test from ddtrace.ext.test_visibility import ITR_SKIPPING_LEVEL -from ddtrace.ext.test_visibility._item_ids import TestId +from ddtrace.ext.test_visibility._test_visibility_base import TestId from ddtrace.ext.test_visibility.status import TestExcInfo from ddtrace.ext.test_visibility.status import TestSourceFileInfo from ddtrace.ext.test_visibility.status import TestStatus @@ -472,7 +472,14 @@ def atr_add_retry(self, start_immediately=False) -> Optional[int]: def atr_start_retry(self, retry_number: int): self._atr_get_retry_test(retry_number).start() - def atr_finish_retry(self, retry_number: int, status: TestStatus, exc_info: Optional[TestExcInfo] = None): + def atr_finish_retry( + self, + retry_number: int, + status: TestStatus, + skip_reason: Optional[str] = None, + exc_info: Optional[TestExcInfo] = None, + ): + # TODO: Do something with skip reason retry_test = self._atr_get_retry_test(retry_number) if retry_number >= self._session_settings.atr_settings.max_retries: @@ -545,7 +552,11 @@ def attempt_to_fix_start_retry(self, retry_number: int): self._attempt_to_fix_get_retry_test(retry_number).start() def attempt_to_fix_finish_retry( - self, retry_number: int, status: TestStatus, exc_info: Optional[TestExcInfo] = None + self, + retry_number: int, + status: TestStatus, + skip_reason: Optional[str] = None, + exc_info: Optional[TestExcInfo] = None, ): retry_test = self._attempt_to_fix_get_retry_test(retry_number) diff --git a/ddtrace/internal/ci_visibility/recorder.py b/ddtrace/internal/ci_visibility/recorder.py index 82acc07bd47..4b023b34ac9 100644 --- a/ddtrace/internal/ci_visibility/recorder.py +++ b/ddtrace/internal/ci_visibility/recorder.py @@ -17,10 +17,10 @@ from ddtrace.ext import ci from ddtrace.ext import test from ddtrace.ext.test_visibility import ITR_SKIPPING_LEVEL -from ddtrace.ext.test_visibility._item_ids import TestId -from ddtrace.ext.test_visibility._item_ids import TestModuleId -from ddtrace.ext.test_visibility._item_ids import TestSuiteId +from ddtrace.ext.test_visibility._test_visibility_base import TestId +from ddtrace.ext.test_visibility._test_visibility_base import TestModuleId from ddtrace.ext.test_visibility._test_visibility_base import TestSessionId +from ddtrace.ext.test_visibility._test_visibility_base import TestSuiteId from ddtrace.ext.test_visibility._test_visibility_base import TestVisibilityItemId from ddtrace.internal import agent from ddtrace.internal import atexit diff --git a/ddtrace/internal/test_visibility/_atr_mixins.py b/ddtrace/internal/test_visibility/_atr_mixins.py index c1b8d83740b..f15a653d7be 100644 --- a/ddtrace/internal/test_visibility/_atr_mixins.py +++ b/ddtrace/internal/test_visibility/_atr_mixins.py @@ -32,20 +32,20 @@ def atr_has_failed_tests() -> bool: class ATRTestMixin: @staticmethod - def atr_should_retry(test_id: InternalTestId) -> bool: - log.debug("Checking if test %s should be retried by ATR", test_id) - return require_ci_visibility_service().get_test_by_id(test_id).atr_should_retry() + def atr_should_retry(item_id: InternalTestId) -> bool: + log.debug("Checking if test %s should be retried by ATR", item_id) + return require_ci_visibility_service().get_test_by_id(item_id).atr_should_retry() @staticmethod - def atr_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: - retry_number = require_ci_visibility_service().get_test_by_id(test_id).atr_add_retry(start_immediately) - log.debug("Adding ATR retry %s for test %s", retry_number, test_id) + def atr_add_retry(item_id: InternalTestId, start_immediately: bool = False) -> int: + retry_number = require_ci_visibility_service().get_test_by_id(item_id).atr_add_retry(start_immediately) + log.debug("Adding ATR retry %s for test %s", retry_number, item_id) return retry_number @staticmethod - def atr_start_retry(test_id: InternalTestId, retry_number: int) -> None: - log.debug("Starting ATR retry %s for test %s", retry_number, test_id) - require_ci_visibility_service().get_test_by_id(test_id).atr_start_retry(retry_number) + def atr_start_retry(item_id: InternalTestId, retry_number: int) -> None: + log.debug("Starting ATR retry %s for test %s", retry_number, item_id) + require_ci_visibility_service().get_test_by_id(item_id).atr_start_retry(retry_number) @staticmethod def atr_finish_retry( diff --git a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py index 36ec059b54e..0af871c7ca6 100644 --- a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py +++ b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py @@ -20,36 +20,37 @@ def attempt_to_fix_has_failed_tests() -> bool: class AttemptToFixTestMixin: @staticmethod - def attempt_to_fix_should_retry(test_id: InternalTestId) -> bool: - log.debug("Checking if test %s should be retried by attempt to fix", test_id) - return require_ci_visibility_service().get_test_by_id(test_id).attempt_to_fix_should_retry() + def attempt_to_fix_should_retry(item_id: InternalTestId) -> bool: + log.debug("Checking if test %s should be retried by attempt to fix", item_id) + return require_ci_visibility_service().get_test_by_id(item_id).attempt_to_fix_should_retry() @staticmethod - def attempt_to_fix_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: + def attempt_to_fix_add_retry(item_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: retry_number = ( - require_ci_visibility_service().get_test_by_id(test_id).attempt_to_fix_add_retry(start_immediately) + require_ci_visibility_service().get_test_by_id(item_id).attempt_to_fix_add_retry(start_immediately) ) - log.debug("Adding attempt to fix retry %s for test %s", retry_number, test_id) + log.debug("Adding attempt to fix retry %s for test %s", retry_number, item_id) return retry_number @staticmethod - def attempt_to_fix_start_retry(test_id: InternalTestId, retry_number: int) -> None: - log.debug("Starting attempt to fix retry %s for test %s", retry_number, test_id) - require_ci_visibility_service().get_test_by_id(test_id).attempt_to_fix_start_retry(retry_number) + def attempt_to_fix_start_retry(item_id: InternalTestId, retry_number: int) -> None: + log.debug("Starting attempt to fix retry %s for test %s", retry_number, item_id) + require_ci_visibility_service().get_test_by_id(item_id).attempt_to_fix_start_retry(retry_number) @staticmethod def attempt_to_fix_finish_retry( - test_id: InternalTestId, + item_id: InternalTestId, retry_number: int, status: TestStatus, - exc_info: t.Optional[TestExcInfo], + skip_reason: t.Optional[str] = None, + exc_info: t.Optional[TestExcInfo] = None, ) -> None: - log.debug("Finishing attempt to fix retry %s for test %s", retry_number, test_id) - require_ci_visibility_service().get_test_by_id(test_id).attempt_to_fix_finish_retry( + log.debug("Finishing attempt to fix retry %s for test %s", retry_number, item_id) + require_ci_visibility_service().get_test_by_id(item_id).attempt_to_fix_finish_retry( retry_number, status, exc_info ) @staticmethod - def attempt_to_fix_get_final_status(test_id: InternalTestId) -> TestStatus: - log.debug("Getting attempt to fix final status for test %s", test_id) - return require_ci_visibility_service().get_test_by_id(test_id).attempt_to_fix_get_final_status() + def attempt_to_fix_get_final_status(item_id: InternalTestId) -> TestStatus: + log.debug("Getting attempt to fix final status for test %s", item_id) + return require_ci_visibility_service().get_test_by_id(item_id).attempt_to_fix_get_final_status() diff --git a/ddtrace/internal/test_visibility/_efd_mixins.py b/ddtrace/internal/test_visibility/_efd_mixins.py index 6a9cc10da56..4ddd6b98c71 100644 --- a/ddtrace/internal/test_visibility/_efd_mixins.py +++ b/ddtrace/internal/test_visibility/_efd_mixins.py @@ -40,22 +40,22 @@ def efd_has_failed_tests() -> bool: class EFDTestMixin: @staticmethod - def efd_should_retry(test_id: InternalTestId) -> bool: - log.debug("Checking if test %s should be retried by EFD", test_id) + def efd_should_retry(item_id: InternalTestId) -> bool: + log.debug("Checking if test %s should be retried by EFD", item_id) - return require_ci_visibility_service().get_test_by_id(test_id).efd_should_retry() + return require_ci_visibility_service().get_test_by_id(item_id).efd_should_retry() @staticmethod - def efd_add_retry(test_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: - retry_number = require_ci_visibility_service().get_test_by_id(test_id).efd_add_retry(start_immediately) - log.debug("Adding EFD retry %s for test %s", retry_number, test_id) + def efd_add_retry(item_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: + retry_number = require_ci_visibility_service().get_test_by_id(item_id).efd_add_retry(start_immediately) + log.debug("Adding EFD retry %s for test %s", retry_number, item_id) return retry_number @staticmethod - def efd_start_retry(test_id: InternalTestId, retry_number: int) -> None: - log.debug("Starting EFD retry %s for test %s", retry_number, test_id) + def efd_start_retry(item_id: InternalTestId, retry_number: int) -> None: + log.debug("Starting EFD retry %s for test %s", retry_number, item_id) - require_ci_visibility_service().get_test_by_id(test_id).efd_start_retry(retry_number) + require_ci_visibility_service().get_test_by_id(item_id).efd_start_retry(retry_number) @staticmethod def efd_finish_retry( @@ -78,7 +78,7 @@ def efd_finish_retry( ) @staticmethod - def efd_get_final_status(test_id: InternalTestId) -> EFDTestStatus: - log.debug("Getting EFD final status for test %s", test_id) + def efd_get_final_status(item_id: InternalTestId) -> EFDTestStatus: + log.debug("Getting EFD final status for test %s", item_id) - return require_ci_visibility_service().get_test_by_id(test_id).efd_get_final_status() + return require_ci_visibility_service().get_test_by_id(item_id).efd_get_final_status() diff --git a/ddtrace/internal/test_visibility/api.py b/ddtrace/internal/test_visibility/api.py index 15ececc340a..8a28bf47e63 100644 --- a/ddtrace/internal/test_visibility/api.py +++ b/ddtrace/internal/test_visibility/api.py @@ -1,6 +1,5 @@ from pathlib import Path import typing as t -from typing import NamedTuple from ddtrace.ext.test_visibility import api as ext_api from ddtrace.ext.test_visibility._test_visibility_base import TestSessionId @@ -159,7 +158,7 @@ def finish( log.debug("Finishing test with status: %s, skip_reason: %s", status, skip_reason) final_status = status if status is not None else ext_api.TestStatus.PASS require_ci_visibility_service().get_test_by_id(item_id).finish_test( - status=final_status, skip_reason=skip_reason, exc_info=exc_info + status=final_status, skip_reason=skip_reason, exc_info=exc_info, override_finish_time=override_finish_time ) @staticmethod diff --git a/tests/ci_visibility/api/test_internal_ci_visibility_api.py b/tests/ci_visibility/api/test_internal_ci_visibility_api.py index e463f042241..37eea33989d 100644 --- a/tests/ci_visibility/api/test_internal_ci_visibility_api.py +++ b/tests/ci_visibility/api/test_internal_ci_visibility_api.py @@ -2,9 +2,9 @@ import pytest -from ddtrace.ext.test_visibility._item_ids import TestId -from ddtrace.ext.test_visibility._item_ids import TestModuleId -from ddtrace.ext.test_visibility._item_ids import TestSuiteId +from ddtrace.ext.test_visibility._test_visibility_base import TestId +from ddtrace.ext.test_visibility._test_visibility_base import TestModuleId +from ddtrace.ext.test_visibility._test_visibility_base import TestSuiteId from ddtrace.ext.test_visibility.api import TestSourceFileInfo from ddtrace.internal.ci_visibility.api._base import TestVisibilitySessionSettings from ddtrace.internal.ci_visibility.api._suite import TestVisibilitySuite diff --git a/tests/ci_visibility/api_client/_util.py b/tests/ci_visibility/api_client/_util.py index 35826186c08..949d3de97a2 100644 --- a/tests/ci_visibility/api_client/_util.py +++ b/tests/ci_visibility/api_client/_util.py @@ -7,8 +7,8 @@ import ddtrace from ddtrace.ext import ci from ddtrace.ext.test_visibility import ITR_SKIPPING_LEVEL -from ddtrace.ext.test_visibility._item_ids import TestModuleId -from ddtrace.ext.test_visibility._item_ids import TestSuiteId +from ddtrace.ext.test_visibility._test_visibility_base import TestModuleId +from ddtrace.ext.test_visibility._test_visibility_base import TestSuiteId from ddtrace.internal.ci_visibility import CIVisibility from ddtrace.internal.ci_visibility._api_client import AgentlessTestVisibilityAPIClient from ddtrace.internal.ci_visibility._api_client import EVPProxyTestVisibilityAPIClient diff --git a/tests/ci_visibility/test_efd.py b/tests/ci_visibility/test_efd.py index 0e2de603c6a..d47f824a0e2 100644 --- a/tests/ci_visibility/test_efd.py +++ b/tests/ci_visibility/test_efd.py @@ -4,8 +4,8 @@ import pytest -from ddtrace.ext.test_visibility._item_ids import TestModuleId -from ddtrace.ext.test_visibility._item_ids import TestSuiteId +from ddtrace.ext.test_visibility._test_visibility_base import TestModuleId +from ddtrace.ext.test_visibility._test_visibility_base import TestSuiteId from ddtrace.ext.test_visibility.api import TestStatus from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings from ddtrace.internal.ci_visibility.api._base import TestVisibilitySessionSettings diff --git a/tests/contrib/pytest/test_pytest_xdist_itr.py b/tests/contrib/pytest/test_pytest_xdist_itr.py index fc8491e1bfd..6efa7f41a2d 100644 --- a/tests/contrib/pytest/test_pytest_xdist_itr.py +++ b/tests/contrib/pytest/test_pytest_xdist_itr.py @@ -13,9 +13,9 @@ from ddtrace.contrib.internal.pytest._utils import _USE_PLUGIN_V2 from ddtrace.contrib.internal.pytest._utils import _pytest_version_supports_itr from ddtrace.ext import test -from ddtrace.ext.test_visibility._item_ids import TestId -from ddtrace.ext.test_visibility._item_ids import TestModuleId -from ddtrace.ext.test_visibility._item_ids import TestSuiteId +from ddtrace.ext.test_visibility._test_visibility_base import TestId +from ddtrace.ext.test_visibility._test_visibility_base import TestModuleId +from ddtrace.ext.test_visibility._test_visibility_base import TestSuiteId from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings from ddtrace.internal.ci_visibility._api_client import TestManagementSettings from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings @@ -97,7 +97,7 @@ def test_pytest_xdist_itr_skips_tests(self): from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings from ddtrace.internal.ci_visibility._api_client import TestManagementSettings from ddtrace.internal.ci_visibility._api_client import ITRData -from ddtrace.ext.test_visibility._item_ids import TestSuiteId, TestModuleId +from ddtrace.ext.test_visibility._test_visibility_base import TestSuiteId, TestModuleId # Create ITR settings and data From 939ff28bfde7177474ab8afbd962690b0d699df1 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Fri, 13 Jun 2025 17:33:46 +0200 Subject: [PATCH 35/58] mypy and other --- ddtrace/ext/test_visibility/api.py | 22 +++++++++---------- .../internal/test_visibility/_atr_mixins.py | 9 ++++---- .../test_visibility/_attempt_to_fix_mixins.py | 2 +- ddtrace/internal/test_visibility/api.py | 2 +- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index dfd51e99cc0..1d8bdc15da0 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -167,7 +167,7 @@ def start(item_id: TestVisibilityItemId, distributed_children: bool = False, con session.set_distributed_children() @staticmethod - def finish( + def finish( # type: ignore[override] item_id: TestVisibilityItemId, override_status: Optional[TestStatus] = None, force_finish_children: bool = False, @@ -200,7 +200,7 @@ def delete_tags(tag_names: List[str]): class TestModule(TestBase): @staticmethod - def discover(item_id: TestModuleId, *args, **kwargs): + def discover(item_id: TestModuleId, module_path: Optional[Path] = None): # type: ignore[override] from ddtrace.internal.ci_visibility.api._module import TestVisibilityModule log.debug("Registered module %s", item_id) @@ -212,17 +212,17 @@ def discover(item_id: TestModuleId, *args, **kwargs): TestVisibilityModule( item_id.name, ci_visibility_instance.get_session_settings(), - kwargs.get("module_path"), + module_path, ), ) @staticmethod - def start(item_id: TestModuleId, *args, **kwargs): + def start(item_id: TestModuleId, *args, **kwargs): # type: ignore[override] log.debug("Starting module %s", item_id) require_ci_visibility_service().get_module_by_id(item_id).start() @staticmethod - def finish( + def finish( # type: ignore[override] item_id: TestModuleId, override_status: Optional[TestStatus] = None, force_finish_children: bool = False, @@ -242,7 +242,7 @@ def finish( class TestSuite(TestBase): @staticmethod def discover( - item_id: TestSuiteId, + item_id: TestSuiteId, # type: ignore[override] codeowners: Optional[List[str]] = None, source_file_info: Optional[TestSourceFileInfo] = None, ): @@ -264,12 +264,12 @@ def discover( ) @staticmethod - def start(item_id: TestSuiteId): + def start(item_id: TestSuiteId): # type: ignore[override] log.debug("Starting suite %s", item_id) require_ci_visibility_service().get_suite_by_id(item_id).start() @staticmethod - def finish( + def finish( # type: ignore[override] item_id: TestSuiteId, force_finish_children: bool = False, override_status: Optional[TestStatus] = None, @@ -286,7 +286,7 @@ def finish( class Test(TestBase): @staticmethod - def discover( + def discover( # type: ignore[override] item_id: TestId, codeowners: Optional[List[str]] = None, source_file_info: Optional[TestSourceFileInfo] = None, @@ -340,13 +340,13 @@ def discover( ) @staticmethod - def start(item_id: TestId): + def start(item_id: TestId): # type: ignore[override] log.debug("Starting test %s", item_id) require_ci_visibility_service().get_test_by_id(item_id).start() @staticmethod - def finish( + def finish( # type: ignore[override] item_id: TestId, status: TestStatus, skip_reason: Optional[str] = None, diff --git a/ddtrace/internal/test_visibility/_atr_mixins.py b/ddtrace/internal/test_visibility/_atr_mixins.py index f15a653d7be..1c5bcc50c76 100644 --- a/ddtrace/internal/test_visibility/_atr_mixins.py +++ b/ddtrace/internal/test_visibility/_atr_mixins.py @@ -37,7 +37,7 @@ def atr_should_retry(item_id: InternalTestId) -> bool: return require_ci_visibility_service().get_test_by_id(item_id).atr_should_retry() @staticmethod - def atr_add_retry(item_id: InternalTestId, start_immediately: bool = False) -> int: + def atr_add_retry(item_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: retry_number = require_ci_visibility_service().get_test_by_id(item_id).atr_add_retry(start_immediately) log.debug("Adding ATR retry %s for test %s", retry_number, item_id) return retry_number @@ -49,13 +49,14 @@ def atr_start_retry(item_id: InternalTestId, retry_number: int) -> None: @staticmethod def atr_finish_retry( - test_id: InternalTestId, + item_id: InternalTestId, retry_number: int, status: TestStatus, + skip_reason: t.Optional[str] = None, exc_info: t.Optional[TestExcInfo] = None, ) -> None: - log.debug("Finishing ATR retry %s for test %s", retry_number, test_id) - require_ci_visibility_service().get_test_by_id(test_id).atr_finish_retry( + log.debug("Finishing ATR retry %s for test %s", retry_number, item_id) + require_ci_visibility_service().get_test_by_id(item_id).atr_finish_retry( retry_number=retry_number, status=status, exc_info=exc_info ) diff --git a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py index 0af871c7ca6..e642f949a39 100644 --- a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py +++ b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py @@ -47,7 +47,7 @@ def attempt_to_fix_finish_retry( ) -> None: log.debug("Finishing attempt to fix retry %s for test %s", retry_number, item_id) require_ci_visibility_service().get_test_by_id(item_id).attempt_to_fix_finish_retry( - retry_number, status, exc_info + retry_number=retry_number, status=status, skip_reason=skip_reason, exc_info=exc_info ) @staticmethod diff --git a/ddtrace/internal/test_visibility/api.py b/ddtrace/internal/test_visibility/api.py index 8a28bf47e63..8fcd563f5cd 100644 --- a/ddtrace/internal/test_visibility/api.py +++ b/ddtrace/internal/test_visibility/api.py @@ -148,7 +148,7 @@ class InternalTest( ext_api.Test, InternalTestBase, ITRMixin, EFDTestMixin, ATRTestMixin, AttemptToFixTestMixin, BenchmarkTestMixin ): @staticmethod - def finish( + def finish( # type: ignore[override] item_id: InternalTestId, status: t.Optional[ext_api.TestStatus] = None, skip_reason: t.Optional[str] = None, From 40626645f5fb01e0e2f09a419d09768e7ba38886 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Fri, 13 Jun 2025 17:56:47 +0200 Subject: [PATCH 36/58] undo --- ddtrace/contrib/internal/pytest/_plugin_v2.py | 2 -- ddtrace/ext/test_visibility/_test_visibility_base.py | 4 ++-- ddtrace/ext/test_visibility/api.py | 4 +--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/ddtrace/contrib/internal/pytest/_plugin_v2.py b/ddtrace/contrib/internal/pytest/_plugin_v2.py index 907914e283b..87cd3b00871 100644 --- a/ddtrace/contrib/internal/pytest/_plugin_v2.py +++ b/ddtrace/contrib/internal/pytest/_plugin_v2.py @@ -319,7 +319,6 @@ def pytest_sessionstart(session: pytest.Session) -> None: ) InternalTestSession.discover( - item_id=None, # unused arg test_command=command, test_framework=FRAMEWORK, test_framework_version=pytest.__version__, @@ -797,7 +796,6 @@ def _pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: InternalTestSession.set_itr_tags(skipped_count) InternalTestSession.finish( - item_id=None, # unused argument force_finish_children=True, override_status=TestStatus.FAIL if session.exitstatus == pytest.ExitCode.TESTS_FAILED else None, ) diff --git a/ddtrace/ext/test_visibility/_test_visibility_base.py b/ddtrace/ext/test_visibility/_test_visibility_base.py index 5c26ebcbb97..9c194d1dd3f 100644 --- a/ddtrace/ext/test_visibility/_test_visibility_base.py +++ b/ddtrace/ext/test_visibility/_test_visibility_base.py @@ -127,12 +127,12 @@ def __init__(self): @staticmethod @abc.abstractmethod - def discover(item_id: TestVisibilityItemId, *args, **kwargs): + def discover(*args, **kwargs): pass @staticmethod @abc.abstractmethod - def start(item_id: TestVisibilityItemId, *args, **kwargs): + def start(*args, **kwargs): pass @staticmethod diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 1d8bdc15da0..902c7995eb8 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -128,7 +128,6 @@ def is_finished(item_id: TestVisibilityItemId) -> bool: class TestSession(_TestVisibilityAPIBase): @staticmethod def discover( - item_id: TestVisibilityItemId, test_command: str, test_framework: str, test_framework_version: str, @@ -159,7 +158,7 @@ def discover( ) @staticmethod - def start(item_id: TestVisibilityItemId, distributed_children: bool = False, context: Optional[Context] = None): + def start(distributed_children: bool = False, context: Optional[Context] = None): log.debug("Starting session") session = require_ci_visibility_service().get_session() session.start(context) @@ -168,7 +167,6 @@ def start(item_id: TestVisibilityItemId, distributed_children: bool = False, con @staticmethod def finish( # type: ignore[override] - item_id: TestVisibilityItemId, override_status: Optional[TestStatus] = None, force_finish_children: bool = False, ): From 7483314de052275b3a0606a80582bfaf83efe618 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Fri, 13 Jun 2025 17:59:45 +0200 Subject: [PATCH 37/58] mypy --- ddtrace/ext/test_visibility/api.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 902c7995eb8..b9265f6b1e7 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -198,7 +198,7 @@ def delete_tags(tag_names: List[str]): class TestModule(TestBase): @staticmethod - def discover(item_id: TestModuleId, module_path: Optional[Path] = None): # type: ignore[override] + def discover(item_id: TestModuleId, module_path: Optional[Path] = None): from ddtrace.internal.ci_visibility.api._module import TestVisibilityModule log.debug("Registered module %s", item_id) @@ -215,7 +215,7 @@ def discover(item_id: TestModuleId, module_path: Optional[Path] = None): # type ) @staticmethod - def start(item_id: TestModuleId, *args, **kwargs): # type: ignore[override] + def start(item_id: TestModuleId, *args, **kwargs): log.debug("Starting module %s", item_id) require_ci_visibility_service().get_module_by_id(item_id).start() @@ -240,7 +240,7 @@ def finish( # type: ignore[override] class TestSuite(TestBase): @staticmethod def discover( - item_id: TestSuiteId, # type: ignore[override] + item_id: TestSuiteId, codeowners: Optional[List[str]] = None, source_file_info: Optional[TestSourceFileInfo] = None, ): @@ -262,7 +262,7 @@ def discover( ) @staticmethod - def start(item_id: TestSuiteId): # type: ignore[override] + def start(item_id: TestSuiteId): log.debug("Starting suite %s", item_id) require_ci_visibility_service().get_suite_by_id(item_id).start() @@ -284,7 +284,7 @@ def finish( # type: ignore[override] class Test(TestBase): @staticmethod - def discover( # type: ignore[override] + def discover( item_id: TestId, codeowners: Optional[List[str]] = None, source_file_info: Optional[TestSourceFileInfo] = None, @@ -338,7 +338,7 @@ def discover( # type: ignore[override] ) @staticmethod - def start(item_id: TestId): # type: ignore[override] + def start(item_id: TestId): log.debug("Starting test %s", item_id) require_ci_visibility_service().get_test_by_id(item_id).start() From 231c384c0bb98ab84e3b0a7dc1b1deb0d1aecb43 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Fri, 13 Jun 2025 18:25:20 +0200 Subject: [PATCH 38/58] refactor to fix test --- ddtrace/internal/ci_visibility/recorder.py | 26 ++++--- tests/ci_visibility/test_ci_visibility.py | 91 ++++++++++------------ 2 files changed, 55 insertions(+), 62 deletions(-) diff --git a/ddtrace/internal/ci_visibility/recorder.py b/ddtrace/internal/ci_visibility/recorder.py index 4b023b34ac9..6af50f2f513 100644 --- a/ddtrace/internal/ci_visibility/recorder.py +++ b/ddtrace/internal/ci_visibility/recorder.py @@ -127,6 +127,20 @@ def _get_custom_configurations() -> Dict[str, str]: return custom_configurations +def _is_item_itr_skippable(item_id: TestVisibilityItemId, suite_skipping_mode: bool, itr_data: Optional[ITRData]): + if itr_data is None: + return False + + if isinstance(item_id, TestSuiteId) and not suite_skipping_mode: + log.debug("Skipping mode is suite, but item is not a suite: %s", item_id) + return False + + if isinstance(item_id, TestId) and suite_skipping_mode: + log.debug("Skipping mode is test, but item is not a test: %s", item_id) + return False + return item_id in itr_data.skippable_items + + class CIVisibilityTracer(Tracer): def __init__(self, *args, **kwargs) -> None: # Allows for multiple instances of the civis tracer to be created without logging a warning @@ -859,17 +873,7 @@ def get_workspace_path(self) -> Optional[str]: return self._tags.get(ci.WORKSPACE_PATH) def is_item_itr_skippable(self, item_id: TestVisibilityItemId) -> bool: - if self._itr_data is None: - return False - - if isinstance(item_id, TestSuiteId) and not self._suite_skipping_mode: - log.debug("Skipping mode is suite, but item is not a suite: %s", item_id) - return False - - if isinstance(item_id, TestId) and self._suite_skipping_mode: - log.debug("Skipping mode is test, but item is not a test: %s", item_id) - return False - return item_id in self._itr_data.skippable_items + return _is_item_itr_skippable(item_id, self._suite_skipping_mode, self._itr_data) def is_unknown_ci(self) -> bool: return self._tags.get(ci.PROVIDER_NAME) is None diff --git a/tests/ci_visibility/test_ci_visibility.py b/tests/ci_visibility/test_ci_visibility.py index 431c0ee7f6b..c4c5566648a 100644 --- a/tests/ci_visibility/test_ci_visibility.py +++ b/tests/ci_visibility/test_ci_visibility.py @@ -7,7 +7,6 @@ import textwrap import time from typing import Set -from unittest.mock import Mock import mock import msgpack @@ -32,6 +31,7 @@ from ddtrace.internal.ci_visibility.git_client import CIVisibilityGitClientSerializerV1 from ddtrace.internal.ci_visibility.recorder import CIVisibilityTracer from ddtrace.internal.ci_visibility.recorder import _extract_repository_name_from_url +from ddtrace.internal.ci_visibility.recorder import _is_item_itr_skippable import ddtrace.internal.test_visibility._internal_item_ids from ddtrace.internal.test_visibility._library_capabilities import LibraryCapabilities from ddtrace.internal.utils.http import Response @@ -1382,65 +1382,54 @@ def _get_all_test_ids(self): return {getattr(self, test_id) for test_id in vars(self.__class__) if re.match(r"^m\d_s\d_t\d$", test_id)} def test_is_item_itr_skippable_test_level(self): - with mock.patch.object(CIVisibility, "enabled", True), mock.patch.object( - CIVisibility, "_instance", Mock() - ) as mock_instance: - mock_instance._itr_data = ITRData(skippable_items=self.test_level_tests_to_skip) - mock_instance._suite_skipping_mode = False - - expected_skippable_test_ids = { - self.m1_s1_t1, - self.m1_s1_t2, - self.m1_s1_t5, - self.m2_s1_t3, - self.m2_s2_t2, - self.m2_s2_t4, - self.m2_s2_t6, - self.m2_s2_t7, - self.m3_s1_t5, - self.m3_s2_t1, - self.m3_s2_t6, - self.m3_s2_t7, - } - expected_non_skippable_test_ids = self._get_all_test_ids() - expected_skippable_test_ids - - assert CIVisibility._instance is not None + expected_skippable_test_ids = { + self.m1_s1_t1, + self.m1_s1_t2, + self.m1_s1_t5, + self.m2_s1_t3, + self.m2_s2_t2, + self.m2_s2_t4, + self.m2_s2_t6, + self.m2_s2_t7, + self.m3_s1_t5, + self.m3_s2_t1, + self.m3_s2_t6, + self.m3_s2_t7, + } + expected_non_skippable_test_ids = self._get_all_test_ids() - expected_skippable_test_ids + itr_data = ITRData(skippable_items=self.test_level_tests_to_skip) + suite_skipping_mode = False - # Check skippable tests are correct - for test_id in expected_skippable_test_ids: - assert CIVisibility._instance.is_item_itr_skippable(test_id) is True + # Check skippable tests are correct + for test_id in expected_skippable_test_ids: + assert _is_item_itr_skippable(test_id, suite_skipping_mode, itr_data) is True - # Check non-skippable tests are correct - for test_id in expected_non_skippable_test_ids: - assert CIVisibility._instance.is_item_itr_skippable(test_id) is False + # Check non-skippable tests are correct + for test_id in expected_non_skippable_test_ids: + assert _is_item_itr_skippable(test_id, suite_skipping_mode, itr_data) is False - # Check all suites are not skippable - for suite_id in self._get_all_suite_ids(): - assert CIVisibility._instance.is_item_itr_skippable(suite_id) is False + # Check all suites are not skippable + for suite_id in self._get_all_suite_ids(): + assert _is_item_itr_skippable(suite_id, suite_skipping_mode, itr_data) is False def test_is_item_itr_skippable_suite_level(self): - with mock.patch.object(CIVisibility, "enabled", True), mock.patch.object( - CIVisibility, "_instance", Mock() - ) as mock_instance: - mock_instance._itr_data = ITRData(skippable_items=self.suite_level_test_suites_to_skip) - mock_instance._suite_skipping_mode = True - - expected_skippable_suite_ids = {self.m1_s1, self.m2_s1, self.m2_s2, self.m3_s1} - expected_non_skippable_suite_ids = self._get_all_suite_ids() - set(expected_skippable_suite_ids) + itr_data = ITRData(skippable_items=self.suite_level_test_suites_to_skip) + suite_skipping_mode = True - assert CIVisibility._instance is not None + expected_skippable_suite_ids = {self.m1_s1, self.m2_s1, self.m2_s2, self.m3_s1} + expected_non_skippable_suite_ids = self._get_all_suite_ids() - set(expected_skippable_suite_ids) - # Check skippable suites are correct - for suite_id in expected_skippable_suite_ids: - assert CIVisibility._instance.is_item_itr_skippable(suite_id) is True + # Check skippable suites are correct + for suite_id in expected_skippable_suite_ids: + assert _is_item_itr_skippable(suite_id, suite_skipping_mode, itr_data) is True - # Check non-skippable suites are correct - for suite_id in expected_non_skippable_suite_ids: - assert CIVisibility._instance.is_item_itr_skippable(suite_id) is False + # Check non-skippable suites are correct + for suite_id in expected_non_skippable_suite_ids: + assert _is_item_itr_skippable(suite_id, suite_skipping_mode, itr_data) is False - # Check all tests are not skippable - for test_id in self._get_all_test_ids(): - assert CIVisibility._instance.is_item_itr_skippable(test_id) is False + # Check all tests are not skippable + for test_id in self._get_all_test_ids(): + assert _is_item_itr_skippable(test_id, suite_skipping_mode, itr_data) is False class TestCIVisibilitySetTestSessionName(TracerTestCase): From 8a4b2df55db9dbd7916646ef9796d4e991096258 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Fri, 13 Jun 2025 18:43:40 +0200 Subject: [PATCH 39/58] fix some tests --- ddtrace/contrib/internal/pytest/_plugin_v1.py | 2 +- tests/contrib/pytest/test_report_links.py | 73 ++++++++++++++----- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/ddtrace/contrib/internal/pytest/_plugin_v1.py b/ddtrace/contrib/internal/pytest/_plugin_v1.py index ffc291afb80..5992f5066ce 100644 --- a/ddtrace/contrib/internal/pytest/_plugin_v1.py +++ b/ddtrace/contrib/internal/pytest/_plugin_v1.py @@ -472,7 +472,7 @@ def pytest_sessionstart(session): global _global_skipped_elements _global_skipped_elements = 0 - workspace_path = _CIVisibility.get_workspace_path() + workspace_path = _CIVisibility._instance.get_workspace_path() if workspace_path is None: workspace_path = session.config.rootdir diff --git a/tests/contrib/pytest/test_report_links.py b/tests/contrib/pytest/test_report_links.py index e41d5e22533..b67af07c790 100644 --- a/tests/contrib/pytest/test_report_links.py +++ b/tests/contrib/pytest/test_report_links.py @@ -88,9 +88,10 @@ def line(self, text): def test_print_report_links_full(mocker): terminalreporter = TerminalReporterMock() - + ci_visibility_instance = mocker.Mock(spec=CIVisibility) + mocker.patch("ddtrace.contrib.internal.pytest._report_links.require_ci_visibility_service", return_value=ci_visibility_instance) mocker.patch.object( - CIVisibility, + ci_visibility_instance, "get_ci_tags", lambda: { ci.git.REPOSITORY_URL: "https://github.com/some-org/some-repo", @@ -100,8 +101,12 @@ def test_print_report_links_full(mocker): ci.PIPELINE_ID: "123456", }, ) - mocker.patch.object(CIVisibility, "get_session_settings", lambda: _get_session_settings()) - mocker.patch.object(CIVisibility, "get_dd_env", lambda: None) + mocker.patch.object( + ci_visibility_instance, + "get_session_settings", lambda: _get_session_settings()) + mocker.patch.object( + ci_visibility_instance, + "get_dd_env", lambda: None) with override_env({}): _report_links.print_test_report_links(terminalreporter) @@ -122,8 +127,10 @@ def test_print_report_links_full(mocker): def test_print_report_links_only_commit_report(mocker): terminalreporter = TerminalReporterMock() + ci_visibility_instance = mocker.Mock(spec=CIVisibility) + mocker.patch("ddtrace.contrib.internal.pytest._report_links.require_ci_visibility_service", return_value=ci_visibility_instance) mocker.patch.object( - CIVisibility, + ci_visibility_instance, "get_ci_tags", lambda: { ci.git.REPOSITORY_URL: "https://github.com/some-org/some-repo", @@ -131,8 +138,12 @@ def test_print_report_links_only_commit_report(mocker): ci.git.COMMIT_SHA: "abcd0123", }, ) - mocker.patch.object(CIVisibility, "get_session_settings", lambda: _get_session_settings()) - mocker.patch.object(CIVisibility, "get_dd_env", lambda: None) + mocker.patch.object( + ci_visibility_instance, + "get_session_settings", lambda: _get_session_settings()) + mocker.patch.object( + ci_visibility_instance, + "get_dd_env", lambda: None) with override_env({}): _report_links.print_test_report_links(terminalreporter) @@ -149,16 +160,22 @@ def test_print_report_links_only_commit_report(mocker): def test_print_report_links_only_test_runs_report(mocker): terminalreporter = TerminalReporterMock() + ci_visibility_instance = mocker.Mock(spec=CIVisibility) + mocker.patch("ddtrace.contrib.internal.pytest._report_links.require_ci_visibility_service", return_value=ci_visibility_instance) mocker.patch.object( - CIVisibility, + ci_visibility_instance, "get_ci_tags", lambda: { ci.JOB_NAME: "the_job", ci.PIPELINE_ID: "123456", }, ) - mocker.patch.object(CIVisibility, "get_session_settings", lambda: _get_session_settings()) - mocker.patch.object(CIVisibility, "get_dd_env", lambda: None) + mocker.patch.object( + ci_visibility_instance, + "get_session_settings", lambda: _get_session_settings()) + mocker.patch.object( + ci_visibility_instance, + "get_dd_env", lambda: None) with override_env({}): _report_links.print_test_report_links(terminalreporter) @@ -176,13 +193,19 @@ def test_print_report_links_only_test_runs_report(mocker): def test_print_report_links_no_report(mocker): terminalreporter = TerminalReporterMock() + ci_visibility_instance = mocker.Mock(spec=CIVisibility) + mocker.patch("ddtrace.contrib.internal.pytest._report_links.require_ci_visibility_service", return_value=ci_visibility_instance) mocker.patch.object( - CIVisibility, + ci_visibility_instance, "get_ci_tags", lambda: {}, ) - mocker.patch.object(CIVisibility, "get_session_settings", lambda: _get_session_settings()) - mocker.patch.object(CIVisibility, "get_dd_env", lambda: None) + mocker.patch.object( + ci_visibility_instance, + "get_session_settings", lambda: _get_session_settings()) + mocker.patch.object( + ci_visibility_instance, + "get_dd_env", lambda: None) with override_env({}): _report_links.print_test_report_links(terminalreporter) @@ -193,8 +216,10 @@ def test_print_report_links_no_report(mocker): def test_print_report_links_escape_names(mocker): terminalreporter = TerminalReporterMock() + ci_visibility_instance = mocker.Mock(spec=CIVisibility) + mocker.patch("ddtrace.contrib.internal.pytest._report_links.require_ci_visibility_service", return_value=ci_visibility_instance) mocker.patch.object( - CIVisibility, + ci_visibility_instance, "get_ci_tags", lambda: { ci.git.REPOSITORY_URL: "https://github.com/some-org/some-repo", @@ -204,8 +229,12 @@ def test_print_report_links_escape_names(mocker): ci.PIPELINE_ID: 'a "strange" id', }, ) - mocker.patch.object(CIVisibility, "get_session_settings", lambda: _get_session_settings()) - mocker.patch.object(CIVisibility, "get_dd_env", lambda: None) + mocker.patch.object( + ci_visibility_instance, + "get_session_settings", lambda: _get_session_settings()) + mocker.patch.object( + ci_visibility_instance, + "get_dd_env", lambda: None) with override_env({}): _report_links.print_test_report_links(terminalreporter) @@ -225,8 +254,10 @@ def test_print_report_links_escape_names(mocker): def test_print_report_links_commit_report_with_env(mocker): terminalreporter = TerminalReporterMock() + ci_visibility_instance = mocker.Mock(spec=CIVisibility) + mocker.patch("ddtrace.contrib.internal.pytest._report_links.require_ci_visibility_service", return_value=ci_visibility_instance) mocker.patch.object( - CIVisibility, + ci_visibility_instance, "get_ci_tags", lambda: { ci.git.REPOSITORY_URL: "https://github.com/some-org/some-repo", @@ -234,8 +265,12 @@ def test_print_report_links_commit_report_with_env(mocker): ci.git.COMMIT_SHA: "abcd0123", }, ) - mocker.patch.object(CIVisibility, "get_session_settings", lambda: _get_session_settings()) - mocker.patch.object(CIVisibility, "get_dd_env", lambda: "the_env") + mocker.patch.object( + ci_visibility_instance, + "get_session_settings", lambda: _get_session_settings()) + mocker.patch.object( + ci_visibility_instance, + "get_dd_env", lambda: "the_env") with override_env({}): _report_links.print_test_report_links(terminalreporter) From 1d877ad1829b854b80b5051da86328784eaec5b0 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Fri, 13 Jun 2025 18:55:22 +0200 Subject: [PATCH 40/58] fmt --- tests/contrib/pytest/test_report_links.py | 78 +++++++++++------------ 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/tests/contrib/pytest/test_report_links.py b/tests/contrib/pytest/test_report_links.py index b67af07c790..11232e034e2 100644 --- a/tests/contrib/pytest/test_report_links.py +++ b/tests/contrib/pytest/test_report_links.py @@ -89,7 +89,10 @@ def line(self, text): def test_print_report_links_full(mocker): terminalreporter = TerminalReporterMock() ci_visibility_instance = mocker.Mock(spec=CIVisibility) - mocker.patch("ddtrace.contrib.internal.pytest._report_links.require_ci_visibility_service", return_value=ci_visibility_instance) + mocker.patch( + "ddtrace.contrib.internal.pytest._report_links.require_ci_visibility_service", + return_value=ci_visibility_instance, + ) mocker.patch.object( ci_visibility_instance, "get_ci_tags", @@ -101,12 +104,8 @@ def test_print_report_links_full(mocker): ci.PIPELINE_ID: "123456", }, ) - mocker.patch.object( - ci_visibility_instance, - "get_session_settings", lambda: _get_session_settings()) - mocker.patch.object( - ci_visibility_instance, - "get_dd_env", lambda: None) + mocker.patch.object(ci_visibility_instance, "get_session_settings", lambda: _get_session_settings()) + mocker.patch.object(ci_visibility_instance, "get_dd_env", lambda: None) with override_env({}): _report_links.print_test_report_links(terminalreporter) @@ -128,7 +127,10 @@ def test_print_report_links_only_commit_report(mocker): terminalreporter = TerminalReporterMock() ci_visibility_instance = mocker.Mock(spec=CIVisibility) - mocker.patch("ddtrace.contrib.internal.pytest._report_links.require_ci_visibility_service", return_value=ci_visibility_instance) + mocker.patch( + "ddtrace.contrib.internal.pytest._report_links.require_ci_visibility_service", + return_value=ci_visibility_instance, + ) mocker.patch.object( ci_visibility_instance, "get_ci_tags", @@ -138,12 +140,8 @@ def test_print_report_links_only_commit_report(mocker): ci.git.COMMIT_SHA: "abcd0123", }, ) - mocker.patch.object( - ci_visibility_instance, - "get_session_settings", lambda: _get_session_settings()) - mocker.patch.object( - ci_visibility_instance, - "get_dd_env", lambda: None) + mocker.patch.object(ci_visibility_instance, "get_session_settings", lambda: _get_session_settings()) + mocker.patch.object(ci_visibility_instance, "get_dd_env", lambda: None) with override_env({}): _report_links.print_test_report_links(terminalreporter) @@ -161,7 +159,10 @@ def test_print_report_links_only_test_runs_report(mocker): terminalreporter = TerminalReporterMock() ci_visibility_instance = mocker.Mock(spec=CIVisibility) - mocker.patch("ddtrace.contrib.internal.pytest._report_links.require_ci_visibility_service", return_value=ci_visibility_instance) + mocker.patch( + "ddtrace.contrib.internal.pytest._report_links.require_ci_visibility_service", + return_value=ci_visibility_instance, + ) mocker.patch.object( ci_visibility_instance, "get_ci_tags", @@ -170,12 +171,8 @@ def test_print_report_links_only_test_runs_report(mocker): ci.PIPELINE_ID: "123456", }, ) - mocker.patch.object( - ci_visibility_instance, - "get_session_settings", lambda: _get_session_settings()) - mocker.patch.object( - ci_visibility_instance, - "get_dd_env", lambda: None) + mocker.patch.object(ci_visibility_instance, "get_session_settings", lambda: _get_session_settings()) + mocker.patch.object(ci_visibility_instance, "get_dd_env", lambda: None) with override_env({}): _report_links.print_test_report_links(terminalreporter) @@ -194,18 +191,17 @@ def test_print_report_links_no_report(mocker): terminalreporter = TerminalReporterMock() ci_visibility_instance = mocker.Mock(spec=CIVisibility) - mocker.patch("ddtrace.contrib.internal.pytest._report_links.require_ci_visibility_service", return_value=ci_visibility_instance) + mocker.patch( + "ddtrace.contrib.internal.pytest._report_links.require_ci_visibility_service", + return_value=ci_visibility_instance, + ) mocker.patch.object( ci_visibility_instance, "get_ci_tags", lambda: {}, ) - mocker.patch.object( - ci_visibility_instance, - "get_session_settings", lambda: _get_session_settings()) - mocker.patch.object( - ci_visibility_instance, - "get_dd_env", lambda: None) + mocker.patch.object(ci_visibility_instance, "get_session_settings", lambda: _get_session_settings()) + mocker.patch.object(ci_visibility_instance, "get_dd_env", lambda: None) with override_env({}): _report_links.print_test_report_links(terminalreporter) @@ -217,7 +213,10 @@ def test_print_report_links_escape_names(mocker): terminalreporter = TerminalReporterMock() ci_visibility_instance = mocker.Mock(spec=CIVisibility) - mocker.patch("ddtrace.contrib.internal.pytest._report_links.require_ci_visibility_service", return_value=ci_visibility_instance) + mocker.patch( + "ddtrace.contrib.internal.pytest._report_links.require_ci_visibility_service", + return_value=ci_visibility_instance, + ) mocker.patch.object( ci_visibility_instance, "get_ci_tags", @@ -229,12 +228,8 @@ def test_print_report_links_escape_names(mocker): ci.PIPELINE_ID: 'a "strange" id', }, ) - mocker.patch.object( - ci_visibility_instance, - "get_session_settings", lambda: _get_session_settings()) - mocker.patch.object( - ci_visibility_instance, - "get_dd_env", lambda: None) + mocker.patch.object(ci_visibility_instance, "get_session_settings", lambda: _get_session_settings()) + mocker.patch.object(ci_visibility_instance, "get_dd_env", lambda: None) with override_env({}): _report_links.print_test_report_links(terminalreporter) @@ -255,7 +250,10 @@ def test_print_report_links_commit_report_with_env(mocker): terminalreporter = TerminalReporterMock() ci_visibility_instance = mocker.Mock(spec=CIVisibility) - mocker.patch("ddtrace.contrib.internal.pytest._report_links.require_ci_visibility_service", return_value=ci_visibility_instance) + mocker.patch( + "ddtrace.contrib.internal.pytest._report_links.require_ci_visibility_service", + return_value=ci_visibility_instance, + ) mocker.patch.object( ci_visibility_instance, "get_ci_tags", @@ -265,12 +263,8 @@ def test_print_report_links_commit_report_with_env(mocker): ci.git.COMMIT_SHA: "abcd0123", }, ) - mocker.patch.object( - ci_visibility_instance, - "get_session_settings", lambda: _get_session_settings()) - mocker.patch.object( - ci_visibility_instance, - "get_dd_env", lambda: "the_env") + mocker.patch.object(ci_visibility_instance, "get_session_settings", lambda: _get_session_settings()) + mocker.patch.object(ci_visibility_instance, "get_dd_env", lambda: "the_env") with override_env({}): _report_links.print_test_report_links(terminalreporter) From fd99097d79f0331aaa189b9ae17eb564ebe79340 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Mon, 16 Jun 2025 11:31:31 +0200 Subject: [PATCH 41/58] plugin v1 change --- ddtrace/contrib/internal/pytest/_plugin_v1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddtrace/contrib/internal/pytest/_plugin_v1.py b/ddtrace/contrib/internal/pytest/_plugin_v1.py index 5992f5066ce..e3865acc628 100644 --- a/ddtrace/contrib/internal/pytest/_plugin_v1.py +++ b/ddtrace/contrib/internal/pytest/_plugin_v1.py @@ -492,7 +492,7 @@ def pytest_sessionstart(session): test_session_span.set_tag_str(test.COMMAND, test_command) test_session_span.set_tag_str(_SESSION_ID, str(test_session_span.span_id)) - _CIVisibility.set_test_session_name(test_command=test_command) + _CIVisibility._instance.set_test_session_name(test_command=test_command) if _CIVisibility.test_skipping_enabled(): test_session_span.set_tag_str(test.ITR_TEST_SKIPPING_ENABLED, "true") From 9cc63599cb40c08e9a89e6ff36675fa861994427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADtor=20De=20Ara=C3=BAjo?= Date: Mon, 16 Jun 2025 13:38:13 +0000 Subject: [PATCH 42/58] default is not to pass --- ddtrace/internal/test_visibility/api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ddtrace/internal/test_visibility/api.py b/ddtrace/internal/test_visibility/api.py index 8fcd563f5cd..99eba84c24e 100644 --- a/ddtrace/internal/test_visibility/api.py +++ b/ddtrace/internal/test_visibility/api.py @@ -156,9 +156,8 @@ def finish( # type: ignore[override] override_finish_time: t.Optional[float] = None, ): log.debug("Finishing test with status: %s, skip_reason: %s", status, skip_reason) - final_status = status if status is not None else ext_api.TestStatus.PASS require_ci_visibility_service().get_test_by_id(item_id).finish_test( - status=final_status, skip_reason=skip_reason, exc_info=exc_info, override_finish_time=override_finish_time + status=status, skip_reason=skip_reason, exc_info=exc_info, override_finish_time=override_finish_time ) @staticmethod From 01c24f75fd4ad3efdfb2bdee5ac15ad732a5a8ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADtor=20De=20Ara=C3=BAjo?= Date: Mon, 16 Jun 2025 13:54:41 +0000 Subject: [PATCH 43/58] instance! --- ddtrace/contrib/internal/unittest/patch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddtrace/contrib/internal/unittest/patch.py b/ddtrace/contrib/internal/unittest/patch.py index 484c94fa552..bf80fadc985 100644 --- a/ddtrace/contrib/internal/unittest/patch.py +++ b/ddtrace/contrib/internal/unittest/patch.py @@ -659,7 +659,7 @@ def _start_test_session_span(instance) -> ddtrace.trace.Span: "true" if _CIVisibility._instance._collect_coverage_enabled else "false", ) - _CIVisibility.set_test_session_name(test_command=test_command) + _CIVisibility._instance.set_test_session_name(test_command=test_command) if _CIVisibility.test_skipping_enabled(): _set_test_skipping_tags_to_span(test_session_span) From dbbd469ccef0794f6fd1442db4825467f3383312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADtor=20De=20Ara=C3=BAjo?= Date: Mon, 16 Jun 2025 14:32:57 +0000 Subject: [PATCH 44/58] set_benchmark_data --- ddtrace/internal/test_visibility/_benchmark_mixin.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ddtrace/internal/test_visibility/_benchmark_mixin.py b/ddtrace/internal/test_visibility/_benchmark_mixin.py index d12e44cb053..e8b3b04464a 100644 --- a/ddtrace/internal/test_visibility/_benchmark_mixin.py +++ b/ddtrace/internal/test_visibility/_benchmark_mixin.py @@ -1,6 +1,6 @@ import typing as t -from ddtrace.internal import core +from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId @@ -44,10 +44,7 @@ def set_benchmark_data( is_benchmark: bool = True, ): log.debug("Setting benchmark data for test %s: %s", item_id, benchmark_data) - core.dispatch( - "test_visibility.test.set_benchmark_data", - (BenchmarkTestMixin.SetBenchmarkDataArgs(item_id, benchmark_data, is_benchmark),), - ) + require_ci_visibility_service().get_test_by_id(item_id).set_benchmark_data(benchmark_data, is_benchmark) BENCHMARK_TAG_MAP = { From f881219355e4b6527d49fe33df028b1a7ae89382 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Mon, 16 Jun 2025 18:17:33 +0200 Subject: [PATCH 45/58] catch and log exceptions, mypy --- ddtrace/ext/test_visibility/_utils.py | 22 +++++++++++++++ ddtrace/ext/test_visibility/api.py | 27 ++++++++++++++++--- .../internal/test_visibility/_atr_mixins.py | 8 ++++++ .../test_visibility/_attempt_to_fix_mixins.py | 7 +++++ .../test_visibility/_benchmark_mixin.py | 2 ++ .../internal/test_visibility/_efd_mixins.py | 9 +++++++ .../internal/test_visibility/_itr_mixins.py | 10 +++++++ ddtrace/internal/test_visibility/api.py | 23 +++++++++++++++- 8 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 ddtrace/ext/test_visibility/_utils.py diff --git a/ddtrace/ext/test_visibility/_utils.py b/ddtrace/ext/test_visibility/_utils.py new file mode 100644 index 00000000000..adfc13ed1ca --- /dev/null +++ b/ddtrace/ext/test_visibility/_utils.py @@ -0,0 +1,22 @@ +from functools import wraps + +from ddtrace.internal.logger import get_logger + + +log = get_logger(__name__) + + +def _catch_and_log_exceptions(func): + """This decorator is meant to be used around all methods of the Test Visibility classes. + It accepts an optional parameter to allow it to be used on functions and methods. + No uncaught errors should ever reach the integration-side, and potentially cause crashes. + """ + + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception: # noqa: E722 + log.error("Uncaught exception occurred while calling %s", func.__name__, exc_info=True) + + return wrapper diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index b9265f6b1e7..3a902d7dacd 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -26,6 +26,7 @@ from ddtrace.ext.test_visibility._test_visibility_base import TestSuiteId from ddtrace.ext.test_visibility._test_visibility_base import TestVisibilityItemId from ddtrace.ext.test_visibility._test_visibility_base import _TestVisibilityAPIBase +from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions from ddtrace.ext.test_visibility.status import TestExcInfo from ddtrace.ext.test_visibility.status import TestSourceFileInfo from ddtrace.ext.test_visibility.status import TestStatus @@ -73,6 +74,7 @@ class DEFAULT_OPERATION_NAMES(Enum): TEST = "test_visibility.test" +@_catch_and_log_exceptions def enable_test_visibility(config: Optional[Any] = None): log.debug("Enabling Test Visibility with config: %s", config) from ddtrace.internal.ci_visibility.recorder import CIVisibility @@ -83,6 +85,7 @@ def enable_test_visibility(config: Optional[Any] = None): log.warning("Failed to enable Test Visibility") +@_catch_and_log_exceptions def is_test_visibility_enabled(): try: return require_ci_visibility_service().enabled @@ -91,6 +94,7 @@ def is_test_visibility_enabled(): return False +@_catch_and_log_exceptions def disable_test_visibility(): log.debug("Disabling Test Visibility") ci_visibility_instance = require_ci_visibility_service() @@ -158,6 +162,7 @@ def discover( ) @staticmethod + @_catch_and_log_exceptions def start(distributed_children: bool = False, context: Optional[Context] = None): log.debug("Starting session") session = require_ci_visibility_service().get_session() @@ -166,7 +171,8 @@ def start(distributed_children: bool = False, context: Optional[Context] = None) session.set_distributed_children() @staticmethod - def finish( # type: ignore[override] + @_catch_and_log_exceptions + def finish( override_status: Optional[TestStatus] = None, force_finish_children: bool = False, ): @@ -198,6 +204,7 @@ def delete_tags(tag_names: List[str]): class TestModule(TestBase): @staticmethod + @_catch_and_log_exceptions def discover(item_id: TestModuleId, module_path: Optional[Path] = None): from ddtrace.internal.ci_visibility.api._module import TestVisibilityModule @@ -215,12 +222,14 @@ def discover(item_id: TestModuleId, module_path: Optional[Path] = None): ) @staticmethod + @_catch_and_log_exceptions def start(item_id: TestModuleId, *args, **kwargs): log.debug("Starting module %s", item_id) require_ci_visibility_service().get_module_by_id(item_id).start() @staticmethod - def finish( # type: ignore[override] + @_catch_and_log_exceptions + def finish( item_id: TestModuleId, override_status: Optional[TestStatus] = None, force_finish_children: bool = False, @@ -239,6 +248,7 @@ def finish( # type: ignore[override] class TestSuite(TestBase): @staticmethod + @_catch_and_log_exceptions def discover( item_id: TestSuiteId, codeowners: Optional[List[str]] = None, @@ -262,12 +272,14 @@ def discover( ) @staticmethod + @_catch_and_log_exceptions def start(item_id: TestSuiteId): log.debug("Starting suite %s", item_id) require_ci_visibility_service().get_suite_by_id(item_id).start() @staticmethod - def finish( # type: ignore[override] + @_catch_and_log_exceptions + def finish( item_id: TestSuiteId, force_finish_children: bool = False, override_status: Optional[TestStatus] = None, @@ -284,6 +296,7 @@ def finish( # type: ignore[override] class Test(TestBase): @staticmethod + @_catch_and_log_exceptions def discover( item_id: TestId, codeowners: Optional[List[str]] = None, @@ -338,13 +351,15 @@ def discover( ) @staticmethod + @_catch_and_log_exceptions def start(item_id: TestId): log.debug("Starting test %s", item_id) require_ci_visibility_service().get_test_by_id(item_id).start() @staticmethod - def finish( # type: ignore[override] + @_catch_and_log_exceptions + def finish( item_id: TestId, status: TestStatus, skip_reason: Optional[str] = None, @@ -363,22 +378,26 @@ def finish( # type: ignore[override] ) @staticmethod + @_catch_and_log_exceptions def set_parameters(item_id: TestId, params: str): log.debug("Setting test %s parameters to %s", item_id, params) require_ci_visibility_service().get_test_by_id(item_id).set_parameters(parameters=params) @staticmethod + @_catch_and_log_exceptions def mark_pass(item_id: TestId): log.debug("Marking test %s as passed", item_id) Test.finish(item_id, TestStatus.PASS) @staticmethod + @_catch_and_log_exceptions def mark_fail(item_id: TestId, exc_info: Optional[TestExcInfo] = None): log.debug("Marking test %s as failed, exc_info: %s", item_id, exc_info) Test.finish(item_id, TestStatus.FAIL, exc_info=exc_info) @staticmethod + @_catch_and_log_exceptions def mark_skip(item_id: TestId, skip_reason: Optional[str] = None): log.debug("Marking test %s as skipped, skip reason: %s", item_id, skip_reason) Test.finish(item_id, TestStatus.SKIP, skip_reason=skip_reason) diff --git a/ddtrace/internal/test_visibility/_atr_mixins.py b/ddtrace/internal/test_visibility/_atr_mixins.py index 1c5bcc50c76..437a743f86e 100644 --- a/ddtrace/internal/test_visibility/_atr_mixins.py +++ b/ddtrace/internal/test_visibility/_atr_mixins.py @@ -1,6 +1,7 @@ import dataclasses import typing as t +from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions from ddtrace.ext.test_visibility.status import TestExcInfo from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service @@ -20,11 +21,13 @@ class AutoTestRetriesSettings: class ATRSessionMixin: @staticmethod + @_catch_and_log_exceptions def atr_is_enabled() -> bool: log.debug("Checking if ATR is enabled") return require_ci_visibility_service().is_atr_enabled() @staticmethod + @_catch_and_log_exceptions def atr_has_failed_tests() -> bool: log.debug("Checking if ATR session has failed tests") return require_ci_visibility_service().get_session().atr_has_failed_tests() @@ -32,22 +35,26 @@ def atr_has_failed_tests() -> bool: class ATRTestMixin: @staticmethod + @_catch_and_log_exceptions def atr_should_retry(item_id: InternalTestId) -> bool: log.debug("Checking if test %s should be retried by ATR", item_id) return require_ci_visibility_service().get_test_by_id(item_id).atr_should_retry() @staticmethod + @_catch_and_log_exceptions def atr_add_retry(item_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: retry_number = require_ci_visibility_service().get_test_by_id(item_id).atr_add_retry(start_immediately) log.debug("Adding ATR retry %s for test %s", retry_number, item_id) return retry_number @staticmethod + @_catch_and_log_exceptions def atr_start_retry(item_id: InternalTestId, retry_number: int) -> None: log.debug("Starting ATR retry %s for test %s", retry_number, item_id) require_ci_visibility_service().get_test_by_id(item_id).atr_start_retry(retry_number) @staticmethod + @_catch_and_log_exceptions def atr_finish_retry( item_id: InternalTestId, retry_number: int, @@ -61,6 +68,7 @@ def atr_finish_retry( ) @staticmethod + @_catch_and_log_exceptions def atr_get_final_status(test_id: InternalTestId) -> TestStatus: log.debug("Getting ATR final status for test %s", test_id) diff --git a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py index e642f949a39..e2242a04dfb 100644 --- a/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py +++ b/ddtrace/internal/test_visibility/_attempt_to_fix_mixins.py @@ -1,5 +1,6 @@ import typing as t +from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions from ddtrace.ext.test_visibility.status import TestExcInfo from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service @@ -12,6 +13,7 @@ class AttemptToFixSessionMixin: @staticmethod + @_catch_and_log_exceptions def attempt_to_fix_has_failed_tests() -> bool: log.debug("Checking if attempt to fix session has failed tests") @@ -20,11 +22,13 @@ def attempt_to_fix_has_failed_tests() -> bool: class AttemptToFixTestMixin: @staticmethod + @_catch_and_log_exceptions def attempt_to_fix_should_retry(item_id: InternalTestId) -> bool: log.debug("Checking if test %s should be retried by attempt to fix", item_id) return require_ci_visibility_service().get_test_by_id(item_id).attempt_to_fix_should_retry() @staticmethod + @_catch_and_log_exceptions def attempt_to_fix_add_retry(item_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: retry_number = ( require_ci_visibility_service().get_test_by_id(item_id).attempt_to_fix_add_retry(start_immediately) @@ -33,11 +37,13 @@ def attempt_to_fix_add_retry(item_id: InternalTestId, start_immediately: bool = return retry_number @staticmethod + @_catch_and_log_exceptions def attempt_to_fix_start_retry(item_id: InternalTestId, retry_number: int) -> None: log.debug("Starting attempt to fix retry %s for test %s", retry_number, item_id) require_ci_visibility_service().get_test_by_id(item_id).attempt_to_fix_start_retry(retry_number) @staticmethod + @_catch_and_log_exceptions def attempt_to_fix_finish_retry( item_id: InternalTestId, retry_number: int, @@ -51,6 +57,7 @@ def attempt_to_fix_finish_retry( ) @staticmethod + @_catch_and_log_exceptions def attempt_to_fix_get_final_status(item_id: InternalTestId) -> TestStatus: log.debug("Getting attempt to fix final status for test %s", item_id) return require_ci_visibility_service().get_test_by_id(item_id).attempt_to_fix_get_final_status() diff --git a/ddtrace/internal/test_visibility/_benchmark_mixin.py b/ddtrace/internal/test_visibility/_benchmark_mixin.py index e8b3b04464a..39590b56cf9 100644 --- a/ddtrace/internal/test_visibility/_benchmark_mixin.py +++ b/ddtrace/internal/test_visibility/_benchmark_mixin.py @@ -1,5 +1,6 @@ import typing as t +from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId @@ -37,6 +38,7 @@ class SetBenchmarkDataArgs(t.NamedTuple): is_benchmark: bool = True @classmethod + @_catch_and_log_exceptions def set_benchmark_data( cls, item_id: InternalTestId, diff --git a/ddtrace/internal/test_visibility/_efd_mixins.py b/ddtrace/internal/test_visibility/_efd_mixins.py index 4ddd6b98c71..b98742e5f5d 100644 --- a/ddtrace/internal/test_visibility/_efd_mixins.py +++ b/ddtrace/internal/test_visibility/_efd_mixins.py @@ -1,6 +1,7 @@ from enum import Enum import typing as t +from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions from ddtrace.ext.test_visibility.status import TestExcInfo from ddtrace.ext.test_visibility.status import TestStatus from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service @@ -20,18 +21,21 @@ class EFDTestStatus(Enum): class EFDSessionMixin: @staticmethod + @_catch_and_log_exceptions def efd_enabled() -> bool: log.debug("Checking if EFD is enabled") return require_ci_visibility_service().get_session().efd_is_enabled() @staticmethod + @_catch_and_log_exceptions def efd_is_faulty_session() -> bool: log.debug("Checking if EFD session is faulty") return require_ci_visibility_service().get_session().efd_is_faulty_session() @staticmethod + @_catch_and_log_exceptions def efd_has_failed_tests() -> bool: log.debug("Checking if EFD session has failed tests") @@ -40,24 +44,28 @@ def efd_has_failed_tests() -> bool: class EFDTestMixin: @staticmethod + @_catch_and_log_exceptions def efd_should_retry(item_id: InternalTestId) -> bool: log.debug("Checking if test %s should be retried by EFD", item_id) return require_ci_visibility_service().get_test_by_id(item_id).efd_should_retry() @staticmethod + @_catch_and_log_exceptions def efd_add_retry(item_id: InternalTestId, start_immediately: bool = False) -> t.Optional[int]: retry_number = require_ci_visibility_service().get_test_by_id(item_id).efd_add_retry(start_immediately) log.debug("Adding EFD retry %s for test %s", retry_number, item_id) return retry_number @staticmethod + @_catch_and_log_exceptions def efd_start_retry(item_id: InternalTestId, retry_number: int) -> None: log.debug("Starting EFD retry %s for test %s", retry_number, item_id) require_ci_visibility_service().get_test_by_id(item_id).efd_start_retry(retry_number) @staticmethod + @_catch_and_log_exceptions def efd_finish_retry( item_id: InternalTestId, retry_number: int, @@ -78,6 +86,7 @@ def efd_finish_retry( ) @staticmethod + @_catch_and_log_exceptions def efd_get_final_status(item_id: InternalTestId) -> EFDTestStatus: log.debug("Getting EFD final status for test %s", item_id) diff --git a/ddtrace/internal/test_visibility/_itr_mixins.py b/ddtrace/internal/test_visibility/_itr_mixins.py index b4d23eda702..93f4b6a4689 100644 --- a/ddtrace/internal/test_visibility/_itr_mixins.py +++ b/ddtrace/internal/test_visibility/_itr_mixins.py @@ -2,6 +2,7 @@ import typing as t from ddtrace.ext.test_visibility import api as ext_api +from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions from ddtrace.internal.ci_visibility.errors import CIVisibilityError from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service from ddtrace.internal.logger import get_logger @@ -20,6 +21,7 @@ class AddCoverageArgs(t.NamedTuple): coverage_data: t.Dict[Path, CoverageLines] @staticmethod + @_catch_and_log_exceptions def mark_itr_skipped(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): log.debug("Marking item %s as skipped by ITR", item_id) @@ -29,24 +31,28 @@ def mark_itr_skipped(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): require_ci_visibility_service().get_item_by_id(item_id).finish_itr_skipped() @staticmethod + @_catch_and_log_exceptions def mark_itr_unskippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): log.debug("Marking item %s as unskippable by ITR", item_id) require_ci_visibility_service().get_item_by_id(item_id).mark_itr_unskippable() @staticmethod + @_catch_and_log_exceptions def mark_itr_forced_run(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): log.debug("Marking item %s as forced run by ITR", item_id) require_ci_visibility_service().get_item_by_id(item_id).mark_itr_forced_run() @staticmethod + @_catch_and_log_exceptions def was_itr_forced_run(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: log.debug("Checking if item %s was forced run by ITR", item_id) return require_ci_visibility_service().get_item_by_id(item_id).was_itr_forced_run() @staticmethod + @_catch_and_log_exceptions def is_itr_skippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: log.debug("Checking if item %s is skippable by ITR", item_id) ci_visibility_instance = require_ci_visibility_service() @@ -62,6 +68,7 @@ def is_itr_skippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> b return ci_visibility_instance.is_item_itr_skippable(item_id) @staticmethod + @_catch_and_log_exceptions def is_itr_unskippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: log.debug("Checking if item %s is unskippable by ITR", item_id) @@ -70,12 +77,14 @@ def is_itr_unskippable(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> return require_ci_visibility_service().get_item_by_id(item_id).is_itr_unskippable() @staticmethod + @_catch_and_log_exceptions def was_itr_skipped(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]) -> bool: log.debug("Checking if item %s was skipped by ITR", item_id) return require_ci_visibility_service().get_item_by_id(item_id).is_itr_skipped() @staticmethod + @_catch_and_log_exceptions def add_coverage_data(item_id, coverage_data) -> None: """Adds coverage data to an item, merging with existing coverage data if necessary""" log.debug("Adding coverage data for item id %s", item_id) @@ -87,6 +96,7 @@ def add_coverage_data(item_id, coverage_data) -> None: require_ci_visibility_service().get_item_by_id(item_id).add_coverage_data(coverage_data) @staticmethod + @_catch_and_log_exceptions def get_coverage_data( item_id: t.Union[ext_api.TestSuiteId, InternalTestId] ) -> t.Optional[t.Dict[Path, CoverageLines]]: diff --git a/ddtrace/internal/test_visibility/api.py b/ddtrace/internal/test_visibility/api.py index 99eba84c24e..0c74b66b4e3 100644 --- a/ddtrace/internal/test_visibility/api.py +++ b/ddtrace/internal/test_visibility/api.py @@ -3,6 +3,7 @@ from ddtrace.ext.test_visibility import api as ext_api from ddtrace.ext.test_visibility._test_visibility_base import TestSessionId +from ddtrace.ext.test_visibility._utils import _catch_and_log_exceptions from ddtrace.internal.ci_visibility.service_registry import require_ci_visibility_service from ddtrace.internal.codeowners import Codeowners as _Codeowners from ddtrace.internal.logger import get_logger @@ -29,28 +30,33 @@ def _get_item_span(item_id: t.Union[ext_api.TestVisibilityItemId, InternalTestId class InternalTestBase(ext_api.TestBase): @staticmethod + @_catch_and_log_exceptions def get_span(item_id: t.Union[ext_api.TestVisibilityItemId, InternalTestId]) -> Span: return _get_item_span(item_id) @staticmethod + @_catch_and_log_exceptions def stash_set(item_id, key: str, value: object): log.debug("Stashing value %s for key %s in item %s", value, key, item_id) require_ci_visibility_service().get_item_by_id(item_id).stash_set(key, value) @staticmethod + @_catch_and_log_exceptions def stash_get(item_id: ext_api.TestVisibilityItemId, key: str) -> t.Optional[object]: log.debug("Getting stashed value for key %s in item %s", key, item_id) return require_ci_visibility_service().get_item_by_id(item_id).stash_get(key) @staticmethod + @_catch_and_log_exceptions def stash_delete(item_id: ext_api.TestVisibilityItemId, key: str): log.debug("Deleting stashed value for key %s in item %s", key, item_id) require_ci_visibility_service().get_item_by_id(item_id).stash_delete(key) @staticmethod + @_catch_and_log_exceptions def overwrite_attributes( item_id: InternalTestId, name: t.Optional[str] = None, @@ -78,18 +84,21 @@ def is_finished() -> bool: return ext_api._is_item_finished(TestSessionId()) @staticmethod + @_catch_and_log_exceptions def get_codeowners() -> t.Optional[_Codeowners]: log.debug("Getting codeowners") return require_ci_visibility_service().get_codeowners() @staticmethod + @_catch_and_log_exceptions def get_tracer() -> t.Optional[Tracer]: log.debug("Getting tracer") return require_ci_visibility_service().get_tracer() @staticmethod + @_catch_and_log_exceptions def get_workspace_path() -> t.Optional[Path]: log.debug("Getting workspace path") @@ -97,24 +106,28 @@ def get_workspace_path() -> t.Optional[Path]: return Path(path_str) if path_str is not None else None @staticmethod + @_catch_and_log_exceptions def should_collect_coverage() -> bool: log.debug("Checking if should collect coverage") return require_ci_visibility_service().should_collect_coverage() @staticmethod + @_catch_and_log_exceptions def is_test_skipping_enabled() -> bool: log.debug("Checking if test skipping is enabled") return require_ci_visibility_service().test_skipping_enabled() @staticmethod + @_catch_and_log_exceptions def set_covered_lines_pct(coverage_pct: float) -> None: log.debug("Setting coverage percentage for session to %s", coverage_pct) require_ci_visibility_service().get_session().set_covered_lines_pct(coverage_pct) @staticmethod + @_catch_and_log_exceptions def get_path_codeowners(path: Path) -> t.Optional[t.List[str]]: log.debug("Getting codeowners for path %s", path) @@ -124,12 +137,14 @@ def get_path_codeowners(path: Path) -> t.Optional[t.List[str]]: return codeowners.of(str(path)) @staticmethod + @_catch_and_log_exceptions def set_library_capabilities(capabilities: LibraryCapabilities) -> None: log.debug("Setting library capabilities") require_ci_visibility_service().set_library_capabilities(capabilities) @staticmethod + @_catch_and_log_exceptions def set_itr_skipped_count(skipped_count: int) -> None: log.debug("Setting skipped count: %d", skipped_count) @@ -148,7 +163,8 @@ class InternalTest( ext_api.Test, InternalTestBase, ITRMixin, EFDTestMixin, ATRTestMixin, AttemptToFixTestMixin, BenchmarkTestMixin ): @staticmethod - def finish( # type: ignore[override] + @_catch_and_log_exceptions + def finish( item_id: InternalTestId, status: t.Optional[ext_api.TestStatus] = None, skip_reason: t.Optional[str] = None, @@ -161,30 +177,35 @@ def finish( # type: ignore[override] ) @staticmethod + @_catch_and_log_exceptions def is_new_test(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: log.debug("Checking if test %s is new", test_id) return require_ci_visibility_service().get_test_by_id(test_id).is_new() @staticmethod + @_catch_and_log_exceptions def is_quarantined_test(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: log.debug("Checking if test %s is quarantined", test_id) return require_ci_visibility_service().get_test_by_id(test_id).is_quarantined() @staticmethod + @_catch_and_log_exceptions def is_disabled_test(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: log.debug("Checking if test %s is disabled", test_id) return require_ci_visibility_service().get_test_by_id(test_id).is_disabled() @staticmethod + @_catch_and_log_exceptions def is_attempt_to_fix(test_id: t.Union[ext_api.TestId, InternalTestId]) -> bool: log.debug("Checking if test %s is attempt to fix", test_id) return require_ci_visibility_service().get_test_by_id(test_id).is_attempt_to_fix() @staticmethod + @_catch_and_log_exceptions def overwrite_attributes( item_id: InternalTestId, name: t.Optional[str] = None, From 6b5fa90ed0fe089e4652e7b1c99049f359404123 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Tue, 17 Jun 2025 09:53:56 +0200 Subject: [PATCH 46/58] dd_testing_raise;service registry simplification --- ddtrace/ext/test_visibility/_utils.py | 10 ++++ ddtrace/internal/ci_visibility/recorder.py | 10 ++-- .../ci_visibility/service_registry.py | 46 +++++-------------- 3 files changed, 26 insertions(+), 40 deletions(-) diff --git a/ddtrace/ext/test_visibility/_utils.py b/ddtrace/ext/test_visibility/_utils.py index adfc13ed1ca..8e2d159104e 100644 --- a/ddtrace/ext/test_visibility/_utils.py +++ b/ddtrace/ext/test_visibility/_utils.py @@ -1,11 +1,17 @@ from functools import wraps +import os from ddtrace.internal.logger import get_logger +TESTING_RAISE = os.getenv("DD_TESTING_RAISE", None) log = get_logger(__name__) +def _noop_decorator(func): + return func + + def _catch_and_log_exceptions(func): """This decorator is meant to be used around all methods of the Test Visibility classes. It accepts an optional parameter to allow it to be used on functions and methods. @@ -20,3 +26,7 @@ def wrapper(*args, **kwargs): log.error("Uncaught exception occurred while calling %s", func.__name__, exc_info=True) return wrapper + + +if TESTING_RAISE.lower() in ("1", "true"): + _catch_and_log_decorator = _noop_decorator diff --git a/ddtrace/internal/ci_visibility/recorder.py b/ddtrace/internal/ci_visibility/recorder.py index 6af50f2f513..d9b91606a47 100644 --- a/ddtrace/internal/ci_visibility/recorder.py +++ b/ddtrace/internal/ci_visibility/recorder.py @@ -59,6 +59,8 @@ from ddtrace.internal.ci_visibility.git_client import CIVisibilityGitClient from ddtrace.internal.ci_visibility.git_data import GitData from ddtrace.internal.ci_visibility.git_data import get_git_data_from_tags +from ddtrace.internal.ci_visibility.service_registry import register_ci_visibility_instance +from ddtrace.internal.ci_visibility.service_registry import unregister_ci_visibility_instance from ddtrace.internal.ci_visibility.utils import _get_test_framework_telemetry_name from ddtrace.internal.ci_visibility.writer import CIVisibilityEventClient from ddtrace.internal.ci_visibility.writer import CIVisibilityWriter @@ -567,8 +569,6 @@ def _should_skip_path(self, path: str, name: str, test_skipping_mode: Optional[s @classmethod def enable(cls, tracer=None, config=None, service=None) -> None: - from ddtrace.internal.ci_visibility.service_registry import CIVisibilityServiceRegistry - log.debug("Enabling %s", cls.__name__) if cls._instance is not None: log.debug("%s already enabled", cls.__name__) @@ -587,7 +587,7 @@ def enable(cls, tracer=None, config=None, service=None) -> None: try: cls._instance = cls(tracer=tracer, config=config, service=service) # Register with service registry for other modules to access - CIVisibilityServiceRegistry.register(cls._instance) + register_ci_visibility_instance(cls._instance) except CIVisibilityAuthenticationException: log.warning("Authentication error, disabling CI Visibility, please check Datadog API key") @@ -617,8 +617,6 @@ def enable(cls, tracer=None, config=None, service=None) -> None: @classmethod def disable(cls) -> None: - from ddtrace.internal.ci_visibility.service_registry import CIVisibilityServiceRegistry - if cls._instance is None: log.debug("%s not enabled", cls.__name__) return @@ -626,7 +624,7 @@ def disable(cls) -> None: atexit.unregister(cls.disable) # Unregister from service registry first - CIVisibilityServiceRegistry.unregister() + unregister_ci_visibility_instance() cls._instance.stop() cls._instance = None diff --git a/ddtrace/internal/ci_visibility/service_registry.py b/ddtrace/internal/ci_visibility/service_registry.py index 96d30e427c1..dcf64c5be42 100644 --- a/ddtrace/internal/ci_visibility/service_registry.py +++ b/ddtrace/internal/ci_visibility/service_registry.py @@ -5,45 +5,23 @@ if t.TYPE_CHECKING: from ddtrace.internal.ci_visibility.recorder import CIVisibility +CI_VISIBILITY_INSTANCE = None -class CIVisibilityServiceRegistry: - """Registry to access CIVisibility instance without circular imports. - Since CIVisibility is a singleton, no locks are needed. - """ +def register_ci_visibility_instance(service: "CIVisibility") -> None: + """Register the CIVisibility service instance.""" + global CI_VISIBILITY_INSTANCE + CI_VISIBILITY_INSTANCE = service - _instance: t.Optional["CIVisibility"] = None - @classmethod - def register(cls, service: "CIVisibility") -> None: - """Register the CIVisibility service instance.""" - cls._instance = service - - @classmethod - def unregister(cls) -> None: - """Unregister the current service instance.""" - cls._instance = None - - @classmethod - def get_service(cls) -> t.Optional["CIVisibility"]: - """Get the registered CIVisibility service instance.""" - return cls._instance - - @classmethod - def require_service(cls) -> "CIVisibility": - """Get the registered service, raising if not available.""" - service = cls.get_service() - if service is None: - raise RuntimeError("CIVisibility service not registered") - return service - - -# Convenience functions -def get_ci_visibility_service() -> t.Optional["CIVisibility"]: - """Get the CIVisibility service if available.""" - return CIVisibilityServiceRegistry.get_service() +def unregister_ci_visibility_instance() -> None: + """Unregister the current service instance.""" + global CI_VISIBILITY_INSTANCE + CI_VISIBILITY_INSTANCE = None def require_ci_visibility_service() -> "CIVisibility": """Get the CIVisibility service, raising if not available.""" - return CIVisibilityServiceRegistry.require_service() + if not CI_VISIBILITY_INSTANCE: + raise RuntimeError("CIVisibility service not registered") + return CI_VISIBILITY_INSTANCE From 0314e6f4f24dd2bedfa5c5991bc01f7fae508665 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Tue, 17 Jun 2025 10:28:12 +0200 Subject: [PATCH 47/58] typing --- ddtrace/ext/test_visibility/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddtrace/ext/test_visibility/_utils.py b/ddtrace/ext/test_visibility/_utils.py index 8e2d159104e..756324032e7 100644 --- a/ddtrace/ext/test_visibility/_utils.py +++ b/ddtrace/ext/test_visibility/_utils.py @@ -28,5 +28,5 @@ def wrapper(*args, **kwargs): return wrapper -if TESTING_RAISE.lower() in ("1", "true"): +if TESTING_RAISE and TESTING_RAISE.lower() in ("1", "true"): _catch_and_log_decorator = _noop_decorator From e214c276feb48f3ad2c22b0d18047872b84f969c Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Tue, 17 Jun 2025 10:45:04 +0200 Subject: [PATCH 48/58] fix sast complain --- ddtrace/ext/test_visibility/api.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 3a902d7dacd..30dd753c2f7 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -87,11 +87,12 @@ def enable_test_visibility(config: Optional[Any] = None): @_catch_and_log_exceptions def is_test_visibility_enabled(): + enabled = False try: - return require_ci_visibility_service().enabled + enabled = require_ci_visibility_service().enabled except RuntimeError: - return False - return False + pass + return enabled @_catch_and_log_exceptions From 319d6022eadf3f10f733affd20345d946d5a8d3a Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Tue, 17 Jun 2025 10:55:47 +0200 Subject: [PATCH 49/58] log warning instead of pass --- ddtrace/ext/test_visibility/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 30dd753c2f7..5475c5dbce5 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -91,7 +91,7 @@ def is_test_visibility_enabled(): try: enabled = require_ci_visibility_service().enabled except RuntimeError: - pass + log.warning("Failed to retrieve Test Visibility service", exc_info=True) return enabled From 6b974439870fdb89c3cdfdaa90a79b0616b761a6 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Tue, 17 Jun 2025 11:30:49 +0200 Subject: [PATCH 50/58] Remove unneeded *Args classes --- .../test_visibility/_test_visibility_base.py | 21 ------------------- .../test_visibility/_benchmark_mixin.py | 5 ----- .../internal/test_visibility/_itr_mixins.py | 4 ---- 3 files changed, 30 deletions(-) diff --git a/ddtrace/ext/test_visibility/_test_visibility_base.py b/ddtrace/ext/test_visibility/_test_visibility_base.py index 9c194d1dd3f..a8623e448f2 100644 --- a/ddtrace/ext/test_visibility/_test_visibility_base.py +++ b/ddtrace/ext/test_visibility/_test_visibility_base.py @@ -101,27 +101,6 @@ def __repr__(self): class _TestVisibilityAPIBase(abc.ABC): __test__ = False - class GetTagArgs(NamedTuple): - item_id: Union[_TestVisibilityChildItemIdBase, _TestVisibilityRootItemIdBase, TestSessionId] - name: str - - class SetTagArgs(NamedTuple): - item_id: Union[_TestVisibilityChildItemIdBase, _TestVisibilityRootItemIdBase, TestSessionId] - name: str - value: Any - - class DeleteTagArgs(NamedTuple): - item_id: Union[_TestVisibilityChildItemIdBase, _TestVisibilityRootItemIdBase, TestSessionId] - name: str - - class SetTagsArgs(NamedTuple): - item_id: Union[_TestVisibilityChildItemIdBase, _TestVisibilityRootItemIdBase, TestSessionId] - tags: Dict[str, Any] - - class DeleteTagsArgs(NamedTuple): - item_id: Union[_TestVisibilityChildItemIdBase, _TestVisibilityRootItemIdBase, TestSessionId] - names: List[str] - def __init__(self): raise NotImplementedError("This class is not meant to be instantiated") diff --git a/ddtrace/internal/test_visibility/_benchmark_mixin.py b/ddtrace/internal/test_visibility/_benchmark_mixin.py index 39590b56cf9..74a341dee25 100644 --- a/ddtrace/internal/test_visibility/_benchmark_mixin.py +++ b/ddtrace/internal/test_visibility/_benchmark_mixin.py @@ -32,11 +32,6 @@ class BenchmarkDurationData(t.NamedTuple): class BenchmarkTestMixin: - class SetBenchmarkDataArgs(t.NamedTuple): - test_id: InternalTestId - benchmark_data: t.Optional[BenchmarkDurationData] - is_benchmark: bool = True - @classmethod @_catch_and_log_exceptions def set_benchmark_data( diff --git a/ddtrace/internal/test_visibility/_itr_mixins.py b/ddtrace/internal/test_visibility/_itr_mixins.py index 93f4b6a4689..ce66272af4a 100644 --- a/ddtrace/internal/test_visibility/_itr_mixins.py +++ b/ddtrace/internal/test_visibility/_itr_mixins.py @@ -16,10 +16,6 @@ class ITRMixin: """Mixin class for ITR-related functionality.""" - class AddCoverageArgs(t.NamedTuple): - item_id: t.Union[ext_api.TestSuiteId, InternalTestId] - coverage_data: t.Dict[Path, CoverageLines] - @staticmethod @_catch_and_log_exceptions def mark_itr_skipped(item_id: t.Union[ext_api.TestSuiteId, InternalTestId]): From b1823ff26bec71dbb505e4868697e1363b6f5e93 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Tue, 17 Jun 2025 11:36:14 +0200 Subject: [PATCH 51/58] use dd_testing_raise from ddconfig --- ddtrace/ext/test_visibility/_utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ddtrace/ext/test_visibility/_utils.py b/ddtrace/ext/test_visibility/_utils.py index 756324032e7..f7f9a70be9c 100644 --- a/ddtrace/ext/test_visibility/_utils.py +++ b/ddtrace/ext/test_visibility/_utils.py @@ -1,10 +1,9 @@ from functools import wraps -import os +from ddtrace import config as ddconfig from ddtrace.internal.logger import get_logger -TESTING_RAISE = os.getenv("DD_TESTING_RAISE", None) log = get_logger(__name__) @@ -28,5 +27,5 @@ def wrapper(*args, **kwargs): return wrapper -if TESTING_RAISE and TESTING_RAISE.lower() in ("1", "true"): +if ddconfig._raise: _catch_and_log_decorator = _noop_decorator From dbfadbb1e821b3af2949d46b26944d97011f8fb3 Mon Sep 17 00:00:00 2001 From: Federico Mon Date: Tue, 17 Jun 2025 12:15:32 +0200 Subject: [PATCH 52/58] Update ddtrace/internal/ci_visibility/api/_test.py --- ddtrace/internal/ci_visibility/api/_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ddtrace/internal/ci_visibility/api/_test.py b/ddtrace/internal/ci_visibility/api/_test.py index c0b4d7f6c85..510a2423e67 100644 --- a/ddtrace/internal/ci_visibility/api/_test.py +++ b/ddtrace/internal/ci_visibility/api/_test.py @@ -379,7 +379,6 @@ def efd_finish_retry( skip_reason: Optional[str] = None, exc_info: Optional[TestExcInfo] = None, ) -> None: - # TODO: use skip_reason for something retry_test = self._efd_get_retry_test(retry_number) if status is not None: From 8d8a18bfc96fe553567096c58b0b7390acbf6009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADtor=20De=20Ara=C3=BAjo?= Date: Tue, 17 Jun 2025 10:34:45 +0000 Subject: [PATCH 53/58] some fixes --- .../test_visibility/_test_visibility_base.py | 4 ---- ddtrace/ext/test_visibility/api.py | 20 +++++++++---------- ddtrace/internal/ci_visibility/api/_test.py | 5 ++--- ddtrace/internal/ci_visibility/recorder.py | 4 ++-- .../internal/test_visibility/_atr_mixins.py | 2 +- 5 files changed, 15 insertions(+), 20 deletions(-) diff --git a/ddtrace/ext/test_visibility/_test_visibility_base.py b/ddtrace/ext/test_visibility/_test_visibility_base.py index a8623e448f2..04517bbfabe 100644 --- a/ddtrace/ext/test_visibility/_test_visibility_base.py +++ b/ddtrace/ext/test_visibility/_test_visibility_base.py @@ -2,11 +2,7 @@ import dataclasses from enum import Enum from pathlib import Path -from typing import Any -from typing import Dict from typing import Generic -from typing import List -from typing import NamedTuple from typing import Optional from typing import TypeVar from typing import Union diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 5475c5dbce5..5eb28caf327 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -151,15 +151,15 @@ def discover( from ddtrace.internal.ci_visibility.recorder import on_discover_session on_discover_session( - test_command, - reject_duplicates, - test_framework, - test_framework_version, - session_operation_name, - module_operation_name, - suite_operation_name, - test_operation_name, - root_dir, + test_command=test_command, + reject_duplicates=reject_duplicates, + test_framework=test_framework, + test_framework_version=test_framework_version, + session_operation_name=session_operation_name, + module_operation_name=module_operation_name, + suite_operation_name=suite_operation_name, + test_operation_name=test_operation_name, + root_dir=root_dir, ) @staticmethod @@ -174,8 +174,8 @@ def start(distributed_children: bool = False, context: Optional[Context] = None) @staticmethod @_catch_and_log_exceptions def finish( - override_status: Optional[TestStatus] = None, force_finish_children: bool = False, + override_status: Optional[TestStatus] = None, ): log.debug("Finishing session, force_finish_session_modules: %s", force_finish_children) diff --git a/ddtrace/internal/ci_visibility/api/_test.py b/ddtrace/internal/ci_visibility/api/_test.py index 510a2423e67..55b91cb3d5d 100644 --- a/ddtrace/internal/ci_visibility/api/_test.py +++ b/ddtrace/internal/ci_visibility/api/_test.py @@ -478,7 +478,6 @@ def atr_finish_retry( skip_reason: Optional[str] = None, exc_info: Optional[TestExcInfo] = None, ): - # TODO: Do something with skip reason retry_test = self._atr_get_retry_test(retry_number) if retry_number >= self._session_settings.atr_settings.max_retries: @@ -488,7 +487,7 @@ def atr_finish_retry( if self.atr_get_final_status() == TestStatus.FAIL: retry_test.set_tag(TEST_HAS_FAILED_ALL_RETRIES, True) - retry_test.finish_test(status, exc_info=exc_info) + retry_test.finish_test(status=status, skip_reason=skip_reason, exc_info=exc_info) def atr_get_final_status(self) -> TestStatus: if self._status in [TestStatus.PASS, TestStatus.SKIP]: @@ -574,7 +573,7 @@ def attempt_to_fix_finish_retry( retry_test.set_tag(TEST_ATTEMPT_TO_FIX_PASSED, all_passed) - retry_test.finish_test(status, exc_info=exc_info) + retry_test.finish_test(status, skip_reason=skip_reason, exc_info=exc_info) def attempt_to_fix_get_final_status(self) -> TestStatus: if all(retry._status == TestStatus.PASS for retry in self._attempt_to_fix_retries): diff --git a/ddtrace/internal/ci_visibility/recorder.py b/ddtrace/internal/ci_visibility/recorder.py index d9b91606a47..538f2b09d17 100644 --- a/ddtrace/internal/ci_visibility/recorder.py +++ b/ddtrace/internal/ci_visibility/recorder.py @@ -134,11 +134,11 @@ def _is_item_itr_skippable(item_id: TestVisibilityItemId, suite_skipping_mode: b return False if isinstance(item_id, TestSuiteId) and not suite_skipping_mode: - log.debug("Skipping mode is suite, but item is not a suite: %s", item_id) + log.debug("Skipping mode is at test level, but item is a suite: %s", item_id) return False if isinstance(item_id, TestId) and suite_skipping_mode: - log.debug("Skipping mode is test, but item is not a test: %s", item_id) + log.debug("Skipping mode is at suite level, but item is a test: %s", item_id) return False return item_id in itr_data.skippable_items diff --git a/ddtrace/internal/test_visibility/_atr_mixins.py b/ddtrace/internal/test_visibility/_atr_mixins.py index 437a743f86e..1892fc3eee4 100644 --- a/ddtrace/internal/test_visibility/_atr_mixins.py +++ b/ddtrace/internal/test_visibility/_atr_mixins.py @@ -64,7 +64,7 @@ def atr_finish_retry( ) -> None: log.debug("Finishing ATR retry %s for test %s", retry_number, item_id) require_ci_visibility_service().get_test_by_id(item_id).atr_finish_retry( - retry_number=retry_number, status=status, exc_info=exc_info + retry_number=retry_number, status=status, skip_reason=skip_reason, exc_info=exc_info ) @staticmethod From 1f764eb3fe96ea83b7df15850d7bdf92df94a857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADtor=20De=20Ara=C3=BAjo?= Date: Tue, 17 Jun 2025 10:48:02 +0000 Subject: [PATCH 54/58] finish him! --- ddtrace/ext/test_visibility/api.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 5eb28caf327..915af5628a6 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -180,7 +180,7 @@ def finish( log.debug("Finishing session, force_finish_session_modules: %s", force_finish_children) session = require_ci_visibility_service().get_session() - session.finish(force_finish_children, override_status) + session.finish(force=force_finish_children, override_status=override_status) @staticmethod def get_tag(tag_name: str) -> Any: @@ -243,7 +243,7 @@ def finish( ) require_ci_visibility_service().get_module_by_id(item_id).finish( - override_status=override_status # , force_finish_children=force_finish_children + force=force_finish_children, override_status=override_status ) @@ -292,7 +292,9 @@ def finish( override_status, ) - require_ci_visibility_service().get_suite_by_id(item_id).finish(override_status=override_status) + require_ci_visibility_service().get_suite_by_id(item_id).finish( + force=force_finish_children, override_status=override_status + ) class Test(TestBase): From cf4c043d205eacfc143aecef82f4cb3ccbb7378a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADtor=20De=20Ara=C3=BAjo?= Date: Tue, 17 Jun 2025 11:14:48 +0000 Subject: [PATCH 55/58] itr:noskip From a348899398b3cf688e48911f388742c666847755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADtor=20De=20Ara=C3=BAjo?= Date: Tue, 17 Jun 2025 12:49:21 +0000 Subject: [PATCH 56/58] it is not an error for CI Visibility to not be available when it's not enabled :bigbrain: --- ddtrace/ext/test_visibility/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 915af5628a6..9432d63cf93 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -81,7 +81,7 @@ def enable_test_visibility(config: Optional[Any] = None): CIVisibility.enable(config=config) - if not require_ci_visibility_service().enabled: + if not is_test_visibility_enabled(): log.warning("Failed to enable Test Visibility") @@ -91,7 +91,7 @@ def is_test_visibility_enabled(): try: enabled = require_ci_visibility_service().enabled except RuntimeError: - log.warning("Failed to retrieve Test Visibility service", exc_info=True) + pass return enabled From ad551e20792af2a874434500f5c5878874e90809 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADtor=20De=20Ara=C3=BAjo?= Date: Tue, 17 Jun 2025 12:55:43 +0000 Subject: [PATCH 57/58] cleanup, itr:noskip --- ddtrace/ext/test_visibility/api.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ddtrace/ext/test_visibility/api.py b/ddtrace/ext/test_visibility/api.py index 9432d63cf93..93fbfbe19d3 100644 --- a/ddtrace/ext/test_visibility/api.py +++ b/ddtrace/ext/test_visibility/api.py @@ -87,12 +87,10 @@ def enable_test_visibility(config: Optional[Any] = None): @_catch_and_log_exceptions def is_test_visibility_enabled(): - enabled = False try: - enabled = require_ci_visibility_service().enabled + return require_ci_visibility_service().enabled except RuntimeError: - pass - return enabled + return False @_catch_and_log_exceptions From b5b6ec55668148edbab58b3af7de3c69c991067e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADtor=20De=20Ara=C3=BAjo?= Date: Tue, 17 Jun 2025 13:25:53 +0000 Subject: [PATCH 58/58] itr:noskip