@@ -118,6 +118,12 @@ def test_pytest_xdist_itr_skips_tests_at_test_level_by_pytest_addopts_env_var(se
118118 return_value=itr_settings
119119).start()
120120
121+ # Mock fetch_skippable_items to return our test data
122+ mock.patch(
123+ "ddtrace.internal.ci_visibility._api_client._TestVisibilityAPIClientBase.fetch_skippable_items",
124+ return_value=itr_data
125+ ).start()
126+
121127# Set ITR data when CIVisibility is enabled
122128import ddtrace.internal.ci_visibility.recorder
123129CIVisibility = ddtrace.internal.ci_visibility.recorder.CIVisibility
@@ -173,6 +179,129 @@ def patched_enable(cls, *args, **kwargs):
173179 # Verify number of skipped tests in session
174180 assert session_span .get_metric ("test.itr.tests_skipping.count" ) == 2
175181
182+ def test_xdist_suite_mode_skipped_suites (self ):
183+ """Test that suite-level ITR skipping works correctly in xdist and counts suites, not individual tests."""
184+
185+ itr_skipping_sitecustomize = """
186+ # sitecustomize.py - ITR setup for xdist worker nodes
187+ from unittest import mock
188+
189+ # Import required modules
190+ from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings
191+ from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings
192+ from ddtrace.internal.ci_visibility._api_client import TestManagementSettings
193+ from ddtrace.internal.ci_visibility._api_client import ITRData
194+ from ddtrace.ext.test_visibility._test_visibility_base import TestSuiteId, TestModuleId, TestId
195+
196+ # Create ITR settings and data
197+ itr_settings = TestVisibilityAPISettings(
198+ coverage_enabled=False, skipping_enabled=True, require_git=False, itr_enabled=True,
199+ flaky_test_retries_enabled=False, known_tests_enabled=False,
200+ early_flake_detection=EarlyFlakeDetectionSettings(), test_management=TestManagementSettings()
201+ )
202+
203+ # Create skippable suites for suite-level skipping
204+ skippable_suites = {
205+ TestSuiteId(TestModuleId(""), "test_scope1.py"),
206+ TestSuiteId(TestModuleId(""), "test_scope2.py")
207+ }
208+ itr_data = ITRData(correlation_id="12345678-1234-1234-1234-123456789012", skippable_items=skippable_suites)
209+
210+ # Mock API calls to return our settings
211+ mock.patch(
212+ "ddtrace.internal.ci_visibility._api_client._TestVisibilityAPIClientBase.fetch_settings",
213+ return_value=itr_settings
214+ ).start()
215+
216+ mock.patch(
217+ "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features",
218+ return_value=itr_settings
219+ ).start()
220+
221+ # Mock fetch_skippable_items to return our test data
222+ mock.patch(
223+ "ddtrace.internal.ci_visibility._api_client._TestVisibilityAPIClientBase.fetch_skippable_items",
224+ return_value=itr_data
225+ ).start()
226+
227+ # Set ITR data when CIVisibility is enabled
228+ import ddtrace.internal.ci_visibility.recorder
229+ CIVisibility = ddtrace.internal.ci_visibility.recorder.CIVisibility
230+ original_enable = CIVisibility.enable
231+
232+ def patched_enable(cls, *args, **kwargs):
233+ result = original_enable(*args, **kwargs)
234+ if cls._instance:
235+ cls._instance._itr_data = itr_data
236+ return result
237+
238+ CIVisibility.enable = classmethod(patched_enable)
239+ """
240+
241+ # Create test files
242+ self .testdir .makepyfile (sitecustomize = itr_skipping_sitecustomize )
243+ self .testdir .makepyfile (
244+ test_scope1 = """
245+ import pytest
246+
247+ class TestScope1:
248+ def test_scope1_method1(self):
249+ assert True
250+
251+ def test_scope1_method2(self):
252+ assert True
253+ """ ,
254+ test_scope2 = """
255+ import pytest
256+
257+ class TestScope2:
258+ def test_scope2_method1(self):
259+ assert True
260+ """ ,
261+ )
262+ self .testdir .chdir ()
263+
264+ itr_settings = TestVisibilityAPISettings (
265+ coverage_enabled = False ,
266+ skipping_enabled = True ,
267+ require_git = False ,
268+ itr_enabled = True ,
269+ flaky_test_retries_enabled = False ,
270+ known_tests_enabled = False ,
271+ early_flake_detection = EarlyFlakeDetectionSettings (),
272+ test_management = TestManagementSettings (),
273+ )
274+
275+ with mock .patch (
276+ "ddtrace.internal.ci_visibility.recorder.CIVisibility._check_enabled_features" , return_value = itr_settings
277+ ), mock .patch (
278+ "ddtrace.internal.ci_visibility.recorder.CIVisibility.test_skipping_enabled" ,
279+ return_value = True ,
280+ ):
281+ # Run with xdist using loadscope mode (suite-level skipping)
282+ rec = self .inline_run (
283+ "--ddtrace" ,
284+ "-n" ,
285+ "2" ,
286+ "--dist=loadscope" ,
287+ "-s" ,
288+ "-vvv" ,
289+ extra_env = {
290+ "DD_CIVISIBILITY_AGENTLESS_ENABLED" : "1" ,
291+ "DD_API_KEY" : "foobar.baz" ,
292+ "DD_INSTRUMENTATION_TELEMETRY_ENABLED" : "0" ,
293+ },
294+ )
295+ assert rec .ret == 0 # All tests skipped, so exit code is 0
296+
297+ # Assert on session span metrics - key assertion for suite-level skipping
298+ spans = self .pop_spans ()
299+ session_span = [span for span in spans if span .get_tag ("type" ) == "test_session_end" ][0 ]
300+ assert session_span .get_tag ("test.itr.tests_skipping.enabled" ) == "true"
301+ assert session_span .get_tag ("test.itr.tests_skipping.type" ) == "suite" # loadscope uses suite-level skipping
302+ # Verify number of skipped SUITES in session (should be 2 suites, not 3 tests)
303+ assert session_span .get_metric ("test.itr.tests_skipping.count" ) == 2
304+
176305 def test_pytest_xdist_itr_skips_tests_at_test_level_without_loadscope (self ):
177306 """Test that ITR tags are correctly aggregated from xdist workers."""
178307 # Create a simplified sitecustomize with just the essential ITR setup
@@ -210,6 +339,12 @@ def test_pytest_xdist_itr_skips_tests_at_test_level_without_loadscope(self):
210339 return_value=itr_settings
211340).start()
212341
342+ # Mock fetch_skippable_items to return our test data
343+ mock.patch(
344+ "ddtrace.internal.ci_visibility._api_client._TestVisibilityAPIClientBase.fetch_skippable_items",
345+ return_value=itr_data
346+ ).start()
347+
213348# Set ITR data when CIVisibility is enabled
214349import ddtrace.internal.ci_visibility.recorder
215350CIVisibility = ddtrace.internal.ci_visibility.recorder.CIVisibility
@@ -300,6 +435,12 @@ def test_pytest_xdist_itr_skips_tests_at_suite_level_with_loadscope(self):
300435 return_value=itr_settings
301436).start()
302437
438+ # Mock fetch_skippable_items to return our test data
439+ mock.patch(
440+ "ddtrace.internal.ci_visibility._api_client._TestVisibilityAPIClientBase.fetch_skippable_items",
441+ return_value=itr_data
442+ ).start()
443+
303444# Set ITR data when CIVisibility is enabled
304445import ddtrace.internal.ci_visibility.recorder
305446CIVisibility = ddtrace.internal.ci_visibility.recorder.CIVisibility
@@ -460,16 +601,16 @@ def test_handle_itr_should_skip_counts_skipped_tests_in_worker(self):
460601 test_id = TestId (TestSuiteId (TestModuleId ("test_module" ), "test_suite" ), "test_name" )
461602
462603 mock_service = mock .MagicMock ()
463- mock_service ._suite_skipping_mode = True
604+ mock_service ._suite_skipping_mode = False # Use test-level skipping for worker count tests
464605
465606 with mock .patch (
466607 "ddtrace.internal.test_visibility.api.InternalTestSession.is_test_skipping_enabled" , return_value = True
467608 ), mock .patch (
468- "ddtrace.internal.test_visibility.api.InternalTestSuite .is_itr_unskippable" , return_value = False
609+ "ddtrace.internal.test_visibility.api.InternalTest .is_itr_unskippable" , return_value = False
469610 ), mock .patch (
470611 "ddtrace.internal.test_visibility.api.InternalTest.is_attempt_to_fix" , return_value = False
471612 ), mock .patch (
472- "ddtrace.internal.test_visibility.api.InternalTestSuite .is_itr_skippable" , return_value = True
613+ "ddtrace.internal.test_visibility.api.InternalTest .is_itr_skippable" , return_value = True
473614 ), mock .patch (
474615 "ddtrace.internal.test_visibility.api.InternalTest.mark_itr_skipped"
475616 ), mock .patch (
@@ -491,16 +632,16 @@ def test_handle_itr_should_skip_increments_existing_worker_count(self):
491632 test_id = TestId (TestSuiteId (TestModuleId ("test_module" ), "test_suite" ), "test_name" )
492633
493634 mock_service = mock .MagicMock ()
494- mock_service ._suite_skipping_mode = True
635+ mock_service ._suite_skipping_mode = False # Use test-level skipping for worker count tests
495636
496637 with mock .patch (
497638 "ddtrace.internal.test_visibility.api.InternalTestSession.is_test_skipping_enabled" , return_value = True
498639 ), mock .patch (
499- "ddtrace.internal.test_visibility.api.InternalTestSuite .is_itr_unskippable" , return_value = False
640+ "ddtrace.internal.test_visibility.api.InternalTest .is_itr_unskippable" , return_value = False
500641 ), mock .patch (
501642 "ddtrace.internal.test_visibility.api.InternalTest.is_attempt_to_fix" , return_value = False
502643 ), mock .patch (
503- "ddtrace.internal.test_visibility.api.InternalTestSuite .is_itr_skippable" , return_value = True
644+ "ddtrace.internal.test_visibility.api.InternalTest .is_itr_skippable" , return_value = True
504645 ), mock .patch (
505646 "ddtrace.internal.test_visibility.api.InternalTest.mark_itr_skipped"
506647 ), mock .patch (
@@ -1690,6 +1831,11 @@ def test_func2():
16901831
16911832 # The ITR skipping type should be suite due to explicit env var override
16921833 assert session_span .get_tag ("test.itr.tests_skipping.type" ) == "suite"
1834+ expected_suite_count = 0 # No suites skipped
1835+ actual_count = session_span .get_metric ("test.itr.tests_skipping.count" )
1836+ assert (
1837+ actual_count == expected_suite_count
1838+ ), f"Expected { expected_suite_count } suites skipped but got { actual_count } "
16931839
16941840 def test_explicit_env_var_overrides_xdist_test_mode (self ):
16951841 """Test that explicit _DD_CIVISIBILITY_ITR_SUITE_MODE=False overrides xdist suite-level detection."""
@@ -1742,6 +1888,12 @@ def patched_enable(cls, *args, **kwargs):
17421888 return_value=itr_settings
17431889).start()
17441890
1891+ # Mock fetch_skippable_items to return our test data
1892+ mock.patch(
1893+ "ddtrace.internal.ci_visibility._api_client._TestVisibilityAPIClientBase.fetch_skippable_items",
1894+ return_value=itr_data
1895+ ).start()
1896+
17451897CIVisibility.enable = classmethod(patched_enable)
17461898
17471899"""
0 commit comments