Skip to content

Commit 471eaa8

Browse files
committed
fix tests
1 parent 1d55081 commit 471eaa8

File tree

8 files changed

+483
-75
lines changed

8 files changed

+483
-75
lines changed

ddtrace/contrib/internal/coverage/patch.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,29 @@ def _is_coverage_available():
5858

5959
def coverage_report_wrapper(func: Any, instance: Any, args: tuple, kwargs: dict) -> Any:
6060
"""Wrapper to cache percentage when report() is called."""
61-
global _cached_coverage_percentage
61+
global _cached_coverage_percentage, _coverage_upload_callback
62+
63+
# Check if we're already in a callback (recursion guard)
64+
if hasattr(instance, "_dd_in_callback"):
65+
# Just call the original function without triggering callback
66+
return func(*args, **kwargs)
67+
6268
pct_covered = func(*args, **kwargs)
6369
_cached_coverage_percentage = pct_covered
70+
71+
# Trigger coverage upload if callback is set
72+
if _coverage_upload_callback:
73+
try:
74+
# Set flag to prevent recursion
75+
instance._dd_in_callback = True
76+
try:
77+
_coverage_upload_callback(instance, "text", pct_covered)
78+
finally:
79+
# Always clear the flag
80+
delattr(instance, "_dd_in_callback")
81+
except Exception as e:
82+
log.debug("Coverage upload callback failed: %s", e)
83+
6484
return pct_covered
6585

6686

@@ -243,9 +263,10 @@ def reset_coverage_state() -> None:
243263
"""
244264
Reset all coverage state.
245265
"""
246-
global _coverage_instance, _cached_coverage_percentage
266+
global _coverage_instance, _cached_coverage_percentage, _coverage_upload_callback
247267
_coverage_instance = None
248268
_cached_coverage_percentage = None
269+
_coverage_upload_callback = None
249270
log.debug("Reset coverage state")
250271

251272

@@ -291,3 +312,21 @@ def erase_coverage() -> None:
291312

292313
# Clear cache since data is gone
293314
reset_coverage_state()
315+
316+
317+
def set_coverage_upload_callback(callback: Any) -> None:
318+
"""
319+
Set a callback to be triggered when coverage reports are generated.
320+
321+
The callback will be called with (coverage_instance, format, percentage).
322+
"""
323+
global _coverage_upload_callback
324+
_coverage_upload_callback = callback
325+
log.debug("Set coverage upload callback")
326+
327+
328+
def clear_coverage_upload_callback() -> None:
329+
"""Clear the coverage upload callback."""
330+
global _coverage_upload_callback
331+
_coverage_upload_callback = None
332+
log.debug("Cleared coverage upload callback")

ddtrace/contrib/internal/coverage/utils.py

Lines changed: 164 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
38142
def 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

Comments
 (0)