77
88from ddtrace import DDTraceDeprecationWarning
99from ddtrace import config as dd_config
10+ from ddtrace .contrib .internal .coverage .patch import _is_coverage_available
1011from ddtrace .contrib .internal .coverage .patch import get_coverage_percentage
1112from ddtrace .contrib .internal .coverage .patch import patch as patch_coverage
1213from ddtrace .contrib .internal .coverage .patch import run_coverage_report
14+ from ddtrace .contrib .internal .coverage .patch import start_coverage
1315from ddtrace .contrib .internal .coverage .utils import _is_coverage_invoked_by_coverage_run
1416from ddtrace .contrib .internal .coverage .utils import _is_coverage_patched
1517from ddtrace .contrib .internal .pytest ._benchmark_utils import _set_benchmark_data_from_item
@@ -171,6 +173,30 @@ def _handle_itr_xdist_skipped_suite(item, suite_id) -> bool:
171173 return True
172174
173175
176+ def _is_coverage_report_upload_enabled () -> bool :
177+ """
178+ Check if coverage report upload is enabled, following V3 pattern:
179+ 1. First, get setting from API (via CI visibility service)
180+ 2. Then, allow environment variable to override
181+
182+ This should only be called after pytest_sessionstart when settings are available.
183+ """
184+ # Get API setting (should be available after InternalTestSession.discover())
185+ coverage_report_upload_enabled = False
186+ try :
187+ ci_visibility_service = require_ci_visibility_service ()
188+ if hasattr (ci_visibility_service , "_api_settings" ) and ci_visibility_service ._api_settings :
189+ coverage_report_upload_enabled = ci_visibility_service ._api_settings .coverage_report_upload_enabled
190+ except Exception :
191+ log .debug ("Unable to check if coverage report upload is enabled from settings" , exc_info = True )
192+
193+ # Allow environment variable to override (same pattern as V3)
194+ if asbool (os .getenv ("DD_CIVISIBILITY_CODE_COVERAGE_REPORT_UPLOAD_ENABLED" , "false" )):
195+ coverage_report_upload_enabled = True
196+
197+ return coverage_report_upload_enabled
198+
199+
174200def _handle_test_management (item , test_id ):
175201 """Add a user property to identify quarantined tests, and mark them for skipping if quarantine is enabled in
176202 skipping mode.
@@ -381,6 +407,32 @@ def _is_pytest_cov_enabled(config) -> bool:
381407 return cov_option
382408
383409
410+ def _handle_coverage_patch_early (config ):
411+ """
412+ Handle coverage patching in pytest_configure (must be early for pytest-cov).
413+ Coverage start decision is deferred to pytest_sessionstart when API settings are available.
414+ """
415+ pytest_cov_enabled = _is_pytest_cov_enabled (config )
416+
417+ # Check environment variable (API settings not available yet)
418+ env_coverage_upload = asbool (os .getenv ("DD_CIVISIBILITY_CODE_COVERAGE_REPORT_UPLOAD_ENABLED" , "false" ))
419+
420+ # Patch if pytest-cov is enabled OR env var suggests we might need coverage
421+ if not (pytest_cov_enabled or env_coverage_upload ):
422+ return
423+
424+ # Patch coverage.py early (needed for pytest-cov to work)
425+ patch_coverage ()
426+
427+ # Verify patching worked
428+ if not _is_coverage_available ():
429+ log .warning ("Coverage requested but coverage.py not available - install with 'pip install coverage'" )
430+ return
431+
432+ if pytest_cov_enabled :
433+ log .debug ("pytest-cov detected, coverage.py patched successfully" )
434+
435+
384436def pytest_configure (config : pytest_Config ) -> None :
385437 global skip_pytest_runtest_protocol
386438
@@ -399,8 +451,8 @@ def pytest_configure(config: pytest_Config) -> None:
399451 if is_enabled (config ):
400452 unpatch_unittest ()
401453 enable_test_visibility (config = dd_config .pytest )
402- if _is_pytest_cov_enabled ( config ):
403- patch_coverage ( )
454+
455+ _handle_coverage_patch_early ( config )
404456
405457 skip_pytest_runtest_protocol = False
406458
@@ -475,6 +527,19 @@ def pytest_sessionstart(session: pytest.Session) -> None:
475527
476528 InternalTestSession .set_library_capabilities (library_capabilities )
477529
530+ # Start coverage if needed (API settings are now available after discover())
531+ if (
532+ not _is_pytest_cov_enabled (session .config )
533+ and _is_coverage_available ()
534+ and _is_coverage_report_upload_enabled ()
535+ ):
536+ workspace_path = InternalTestSession .get_workspace_path ()
537+ if workspace_path :
538+ start_coverage (source = [str (workspace_path )])
539+ log .debug ("Started coverage.py for report upload" )
540+ else :
541+ log .warning ("Coverage report upload enabled but workspace path not available" )
542+
478543 extracted_context = None
479544 distributed_children = False
480545 if hasattr (session .config , "workerinput" ):
@@ -755,6 +820,33 @@ def _pytest_run_one_test(item, nextitem):
755820 item .ihook .pytest_runtest_logfinish (nodeid = item .nodeid , location = item .location )
756821
757822
823+ def _handle_coverage_report_upload (session : pytest .Session ) -> None :
824+ """
825+ Handle coverage report upload if enabled using shared implementation.
826+ """
827+ from ddtrace .contrib .internal .coverage .utils import handle_coverage_report
828+
829+ def upload_func (coverage_report_bytes : bytes , coverage_format : str ) -> bool :
830+ """Upload coverage report using native V2 writer/recorder infrastructure."""
831+ try :
832+ # Get the CI visibility service (recorder)
833+ ci_visibility_service = require_ci_visibility_service ()
834+
835+ # Use the recorder's native upload method which integrates with writer infrastructure
836+ return ci_visibility_service .upload_coverage_report (coverage_report_bytes , coverage_format )
837+
838+ except Exception as e :
839+ log .exception ("Error in native coverage upload: %s" , e )
840+ return False
841+
842+ handle_coverage_report (
843+ session = session ,
844+ upload_func = upload_func ,
845+ is_pytest_cov_enabled_func = _is_pytest_cov_enabled ,
846+ stop_coverage_func = None , # V2 plugin doesn't need to stop coverage manually
847+ )
848+
849+
758850def _process_reports_dict (item , reports ) -> _TestOutcome :
759851 final_outcome = None
760852
@@ -963,6 +1055,8 @@ def _pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
9631055
9641056 invoked_by_coverage_run_status = _is_coverage_invoked_by_coverage_run ()
9651057 pytest_cov_status = _is_pytest_cov_enabled (session .config )
1058+ coverage_report_upload_enabled = _is_coverage_report_upload_enabled ()
1059+
9661060 if _is_coverage_patched () and (pytest_cov_status or invoked_by_coverage_run_status ):
9671061 if invoked_by_coverage_run_status and not pytest_cov_status :
9681062 run_coverage_report ()
@@ -981,6 +1075,10 @@ def _pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
9811075 else :
9821076 InternalTestSession .set_covered_lines_pct (lines_pct_value )
9831077
1078+ # Handle coverage report upload if enabled
1079+ if coverage_report_upload_enabled :
1080+ _handle_coverage_report_upload (session )
1081+
9841082 if ModuleCodeCollector .is_installed ():
9851083 ModuleCodeCollector .uninstall ()
9861084
0 commit comments