@@ -35,6 +35,110 @@ def _is_coverage_invoked_by_coverage_run() -> bool:
3535 return _command_invokes_coverage_run (_original_sys_argv_command )
3636
3737
38+ def _find_pytest_cov_instance (session ):
39+ """Find and return pytest-cov coverage instance if available."""
40+ for plugin in session .config .pluginmanager .list_name_plugin ():
41+ _ , plugin_instance = plugin
42+ if hasattr (plugin_instance , "cov_controller" ) and plugin_instance .cov_controller :
43+ if hasattr (plugin_instance .cov_controller , "cov" ) and plugin_instance .cov_controller .cov :
44+ return plugin_instance .cov_controller .cov
45+ return None
46+
47+
48+ def _register_pytest_cov_instance (session ):
49+ """Register pytest-cov instance with ddtrace if available."""
50+ from ddtrace .contrib .internal .coverage .patch import set_coverage_instance
51+
52+ cov_instance = _find_pytest_cov_instance (session )
53+ if cov_instance :
54+ set_coverage_instance (cov_instance )
55+ log .debug ("Registered pytest-cov coverage instance with ddtrace: %s" , type (cov_instance ))
56+ return True
57+
58+ log .debug ("No pytest-cov controller found in plugin manager" )
59+ return False
60+
61+
62+ def _save_pytest_cov_data (session ):
63+ """Save pytest-cov data before report generation."""
64+ cov_instance = _find_pytest_cov_instance (session )
65+ if not cov_instance :
66+ return
67+
68+ try :
69+ # Save coverage data
70+ if hasattr (cov_instance , "save" ):
71+ cov_instance .save ()
72+ log .debug ("Saved pytest-cov coverage data before report generation" )
73+
74+ # Stop collection if still running
75+ if hasattr (cov_instance , "_started" ) and cov_instance ._started :
76+ if hasattr (cov_instance , "stop" ):
77+ cov_instance .stop ()
78+ log .debug ("Stopped pytest-cov coverage collection" )
79+
80+ except Exception as save_error :
81+ log .debug ("Could not save pytest-cov data: %s" , save_error )
82+
83+
84+ def _generate_lcov_report (session , tmp_path , is_pytest_cov_enabled_func ):
85+ """Generate LCOV report using either pytest-cov or ddtrace."""
86+ from ddtrace .contrib .internal .coverage .patch import generate_lcov_report
87+
88+ log .debug ("Generating LCOV report to file: %s" , tmp_path )
89+
90+ if is_pytest_cov_enabled_func (session .config ):
91+ # Try pytest-cov instance directly first
92+ pytest_cov_instance = _find_pytest_cov_instance (session )
93+ if pytest_cov_instance :
94+ log .debug ("Using pytest-cov instance directly to generate LCOV report" )
95+ try :
96+ pct_covered = pytest_cov_instance .lcov_report (outfile = str (tmp_path ))
97+ log .debug ("Generated LCOV report directly from pytest-cov instance" )
98+ return pct_covered
99+ except Exception as direct_error :
100+ log .debug ("Direct pytest-cov report generation failed: %s" , direct_error )
101+ # Fall back to ddtrace method
102+
103+ # Use ddtrace method
104+ return generate_lcov_report (outfile = str (tmp_path ))
105+
106+
107+ def _validate_and_read_report (tmp_path ):
108+ """Validate report file exists and read its contents."""
109+ if not tmp_path .exists ():
110+ log .warning ("LCOV report file was not created: %s" , tmp_path )
111+ return None
112+
113+ coverage_report_bytes = tmp_path .read_bytes ()
114+ if not coverage_report_bytes :
115+ log .warning ("LCOV report file is empty: %s" , tmp_path )
116+ # Clean up empty file
117+ try :
118+ tmp_path .unlink ()
119+ except Exception :
120+ pass # Ignore cleanup errors
121+ return None
122+
123+ log .debug ("Read coverage report: %d bytes" , len (coverage_report_bytes ))
124+ return coverage_report_bytes
125+
126+
127+ def _cleanup_temp_file (tmp_path ):
128+ """Clean up temporary coverage report file."""
129+ try :
130+ tmp_path .unlink ()
131+ except Exception as e :
132+ log .debug ("Failed to clean up temporary coverage report file: %s" , e )
133+
134+
135+ def _stop_coverage_if_needed (stop_coverage_func , session , is_pytest_cov_enabled_func ):
136+ """Stop coverage collection if we started it ourselves (not pytest-cov)."""
137+ if stop_coverage_func and not is_pytest_cov_enabled_func (session .config ):
138+ log .debug ("Stopping coverage.py collection" )
139+ stop_coverage_func (save = True )
140+
141+
38142def handle_coverage_report (
39143 session ,
40144 upload_func : Callable [[bytes , str ], bool ],
@@ -51,68 +155,73 @@ def handle_coverage_report(
51155 stop_coverage_func: Optional function to stop coverage collection
52156 """
53157 try :
54- from ddtrace .contrib .internal .coverage .patch import generate_lcov_report
55158 from ddtrace .contrib .internal .coverage .patch import is_coverage_running
56- from ddtrace .contrib .internal .coverage .patch import set_coverage_instance
57159
58160 log .debug ("Coverage report upload is enabled, checking for coverage data" )
59161
60- # If pytest-cov is enabled but coverage detection fails, register the pytest-cov instance
162+ # Register pytest-cov instance if needed
61163 if is_pytest_cov_enabled_func (session .config ) and not is_coverage_running ():
62- # Try to get coverage from pytest-cov plugin and register it
63- for plugin in session .config .pluginmanager .list_name_plugin ():
64- _ , plugin_instance = plugin
65- if hasattr (plugin_instance , "cov_controller" ) and plugin_instance .cov_controller :
66- set_coverage_instance (plugin_instance .cov_controller .cov )
67- log .debug ("Registered pytest-cov coverage instance with ddtrace" )
68- break
69-
70- # Now check if coverage is available (either ddtrace started or pytest-cov registered)
71- if is_coverage_running ():
72- try :
73- coverage_format = "lcov" # Default to LCOV
74- # Generate the report in a temporary file
75- with tempfile .NamedTemporaryFile (mode = "wb" , suffix = ".lcov" , delete = False ) as tmp_file :
76- tmp_path = Path (tmp_file .name )
77-
78- # Generate LCOV report. This returns the percentage and also stores it
79- # in _coverage_data, so we don't need to generate a second report just for the percentage.
80- pct_covered = generate_lcov_report (outfile = str (tmp_path ))
81- log .debug ("Generated LCOV coverage report: %s (%.1f%% coverage)" , tmp_path , pct_covered )
164+ log .debug ("pytest-cov is enabled but coverage not running, trying to register pytest-cov instance" )
165+ _register_pytest_cov_instance (session )
82166
83- # Read the report file
84- coverage_report_bytes = tmp_path .read_bytes ()
85- log .debug ("Read coverage report: %d bytes" , len (coverage_report_bytes ))
86-
87- # Upload the report using provided upload function
88- upload_success = upload_func (coverage_report_bytes , coverage_format )
89-
90- if upload_success :
91- log .info ("Successfully uploaded coverage report" )
92- else :
93- log .warning ("Failed to upload coverage report" )
94-
95- # Clean up temporary file
96- try :
97- tmp_path .unlink ()
98- except Exception as e :
99- log .debug ("Failed to clean up temporary coverage report file: %s" , e )
100-
101- # Stop coverage AFTER generating and uploading the report (if we started it ourselves)
102- if stop_coverage_func and not is_pytest_cov_enabled_func (session .config ):
103- log .debug ("Stopping coverage.py collection" )
104- stop_coverage_func (save = True )
105-
106- except Exception as e :
107- log .exception ("Error generating or uploading coverage report: %s" , e )
108- # Still try to stop coverage even if report generation failed
109- if stop_coverage_func and not is_pytest_cov_enabled_func (session .config ):
110- try :
111- stop_coverage_func (save = True )
112- except Exception :
113- log .debug ("Could not stop coverage after error" , exc_info = True )
114- else :
167+ # Check if coverage is available
168+ if not is_coverage_running ():
115169 log .debug ("Coverage is not running, skipping coverage report upload" )
170+ return
171+
172+ log .debug ("Coverage is running, attempting to generate coverage report" )
173+
174+ # Save pytest-cov data if using pytest-cov
175+ if is_pytest_cov_enabled_func (session .config ):
176+ _save_pytest_cov_data (session )
177+
178+ # Generate and upload report
179+ coverage_format = "lcov"
180+ with tempfile .NamedTemporaryFile (mode = "wb" , suffix = ".lcov" , delete = False ) as tmp_file :
181+ tmp_path = Path (tmp_file .name )
182+
183+ try :
184+ # Generate LCOV report
185+ pct_covered = _generate_lcov_report (session , tmp_path , is_pytest_cov_enabled_func )
186+ if pct_covered is not None :
187+ log .debug ("Generated LCOV coverage report: %s (%.1f%% coverage)" , tmp_path , pct_covered )
188+ else :
189+ log .debug ("Generated LCOV coverage report: %s (coverage percentage unavailable)" , tmp_path )
190+
191+ except Exception as report_error :
192+ # Handle "No data to report" and other coverage errors
193+ if "No data to report" in str (report_error ):
194+ log .debug ("No coverage data available to generate LCOV report: %s" , report_error )
195+ _cleanup_temp_file (tmp_path )
196+ return
197+ else :
198+ # Re-raise unexpected errors
199+ raise
200+
201+ try :
202+ # Read and validate report
203+ coverage_report_bytes = _validate_and_read_report (tmp_path )
204+ if not coverage_report_bytes :
205+ return
206+
207+ # Upload the report
208+ upload_success = upload_func (coverage_report_bytes , coverage_format )
209+ if upload_success :
210+ log .info ("Successfully uploaded coverage report" )
211+ else :
212+ log .warning ("Failed to upload coverage report" )
213+
214+ finally :
215+ # Always clean up temp file
216+ _cleanup_temp_file (tmp_path )
217+ # Stop coverage after upload (if we started it)
218+ _stop_coverage_if_needed (stop_coverage_func , session , is_pytest_cov_enabled_func )
116219
117220 except Exception as e :
118221 log .exception ("Error in coverage report upload handling: %s" , e )
222+ # Still try to stop coverage even if report generation failed
223+ if stop_coverage_func and not is_pytest_cov_enabled_func (session .config ):
224+ try :
225+ stop_coverage_func (save = True )
226+ except Exception :
227+ log .debug ("Could not stop coverage after error" , exc_info = True )
0 commit comments