2929from ddtrace .ext import SpanTypes
3030from ddtrace .ext import test
3131from ddtrace .internal .ci_visibility import CIVisibility as _CIVisibility
32+ from ddtrace .internal .ci_visibility .constants import COVERAGE_TAG_NAME
3233from ddtrace .internal .ci_visibility .constants import EVENT_TYPE as _EVENT_TYPE
3334from ddtrace .internal .ci_visibility .constants import MODULE_ID as _MODULE_ID
3435from ddtrace .internal .ci_visibility .constants import MODULE_TYPE as _MODULE_TYPE
3536from ddtrace .internal .ci_visibility .constants import SESSION_ID as _SESSION_ID
3637from ddtrace .internal .ci_visibility .constants import SESSION_TYPE as _SESSION_TYPE
38+ from ddtrace .internal .ci_visibility .constants import SUITE
3739from ddtrace .internal .ci_visibility .constants import SUITE_ID as _SUITE_ID
3840from ddtrace .internal .ci_visibility .constants import SUITE_TYPE as _SUITE_TYPE
39- from ddtrace .internal .ci_visibility .coverage import _coverage_end
40- from ddtrace .internal .ci_visibility .coverage import _coverage_start
41- from ddtrace .internal .ci_visibility .coverage import _initialize
41+ from ddtrace .internal .ci_visibility .constants import TEST
42+ from ddtrace .internal .ci_visibility .coverage import _initialize_coverage
43+ from ddtrace .internal .ci_visibility .coverage import build_payload as build_coverage_payload
44+ from ddtrace .internal .ci_visibility .recorder import _get_test_skipping_level
4245from ddtrace .internal .constants import COMPONENT
4346from ddtrace .internal .logger import get_logger
4447
4548
4649SKIPPED_BY_ITR = "Skipped by Datadog Intelligent Test Runner"
4750PATCH_ALL_HELP_MSG = "Call ddtrace.patch_all before running tests."
51+
4852log = get_logger (__name__ )
4953
5054
@@ -70,6 +74,22 @@ def _store_span(item, span):
7074 setattr (item , "_datadog_span" , span )
7175
7276
77+ def _attach_coverage (item ):
78+ coverage = _initialize_coverage (str (item .config .rootdir ))
79+ setattr (item , "_coverage" , coverage )
80+ coverage .start ()
81+
82+
83+ def _detach_coverage (item , span ):
84+ if not hasattr (item , "_coverage" ):
85+ return
86+ span_id = str (span .trace_id )
87+ item ._coverage .stop ()
88+ span .set_tag (COVERAGE_TAG_NAME , build_coverage_payload (item ._coverage , test_id = span_id ))
89+ item ._coverage .erase ()
90+ del item ._coverage
91+
92+
7393def _extract_module_span (item ):
7494 """Extract span from `pytest.Item` instance."""
7595 return getattr (item , "_datadog_span_module" , None )
@@ -201,15 +221,14 @@ def _start_test_module_span(pytest_package_item=None, pytest_module_item=None):
201221 return test_module_span , is_package
202222
203223
204- def _start_test_suite_span (item , test_module_span ):
224+ def _start_test_suite_span (item , test_module_span , should_enable_coverage = False ):
205225 """
206226 Starts a test suite span at the start of a new pytest test module.
207227 Note that ``item`` is a ``pytest.Module`` object referencing the test file being run.
208228 """
209229 test_session_span = _extract_span (item .session )
210230 if test_module_span is None and isinstance (item .parent , pytest .Package ):
211231 test_module_span = _extract_span (item .parent )
212-
213232 parent_span = test_module_span
214233 if parent_span is None :
215234 parent_span = test_session_span
@@ -237,6 +256,9 @@ def _start_test_suite_span(item, test_module_span):
237256 test_suite_span .set_tag_str (test .MODULE_PATH , test_module_path )
238257 test_suite_span .set_tag_str (test .SUITE , _get_suite_name (item , test_module_path ))
239258 _store_span (item , test_suite_span )
259+
260+ if should_enable_coverage :
261+ _attach_coverage (item )
240262 return test_suite_span
241263
242264
@@ -350,7 +372,7 @@ def pytest_collection_modifyitems(session, config, items):
350372 if _CIVisibility .test_skipping_enabled ():
351373 skip = pytest .mark .skip (reason = SKIPPED_BY_ITR )
352374 for item in items :
353- if _CIVisibility ._instance ._should_skip_path (str (get_fslocation_from_item (item )[0 ])):
375+ if _CIVisibility ._instance ._should_skip_path (str (get_fslocation_from_item (item )[0 ]), item . name ):
354376 item .add_marker (skip )
355377
356378
@@ -366,97 +388,113 @@ def pytest_runtest_protocol(item, nextitem):
366388 if "reason" in marker .kwargs and marker .kwargs ["reason" ] == SKIPPED_BY_ITR
367389 ]
368390
369- if is_skipped_by_itr :
370- yield
371- else :
372- test_session_span = _extract_span (item .session )
391+ test_session_span = _extract_span (item .session )
373392
374- pytest_module_item = _find_pytest_item (item , pytest .Module )
375- pytest_package_item = _find_pytest_item (pytest_module_item , pytest .Package )
393+ pytest_module_item = _find_pytest_item (item , pytest .Module )
394+ pytest_package_item = _find_pytest_item (pytest_module_item , pytest .Package )
395+
396+ module_is_package = True
397+
398+ test_module_span = _extract_span (pytest_package_item )
399+ if not test_module_span :
400+ test_module_span = _extract_module_span (pytest_module_item )
401+ if test_module_span :
402+ module_is_package = False
403+
404+ if test_module_span is None :
405+ test_module_span , module_is_package = _start_test_module_span (pytest_package_item , pytest_module_item )
406+
407+ test_suite_span = _extract_span (pytest_module_item )
408+ if pytest_module_item is not None and test_suite_span is None :
409+ # Start coverage for the test suite if coverage is enabled
410+ test_suite_span = _start_test_suite_span (
411+ pytest_module_item ,
412+ test_module_span ,
413+ should_enable_coverage = (
414+ _get_test_skipping_level () == SUITE
415+ and _CIVisibility ._instance ._collect_coverage_enabled
416+ and not is_skipped_by_itr
417+ ),
418+ )
376419
377- module_is_package = True
420+ with _CIVisibility ._instance .tracer ._start_span (
421+ ddtrace .config .pytest .operation_name ,
422+ service = _CIVisibility ._instance ._service ,
423+ resource = item .nodeid ,
424+ span_type = SpanTypes .TEST ,
425+ activate = True ,
426+ ) as span :
427+ span .set_tag_str (COMPONENT , "pytest" )
428+ span .set_tag_str (SPAN_KIND , KIND )
429+ span .set_tag_str (test .FRAMEWORK , FRAMEWORK )
430+ span .set_tag_str (_EVENT_TYPE , SpanTypes .TEST )
431+ span .set_tag_str (test .NAME , item .name )
432+ span .set_tag_str (test .COMMAND , _get_pytest_command (item .config ))
433+ span .set_tag_str (_SESSION_ID , str (test_session_span .span_id ))
434+
435+ span .set_tag_str (_MODULE_ID , str (test_module_span .span_id ))
436+ span .set_tag_str (test .MODULE , test_module_span .get_tag (test .MODULE ))
437+ span .set_tag_str (test .MODULE_PATH , test_module_span .get_tag (test .MODULE_PATH ))
438+
439+ span .set_tag_str (_SUITE_ID , str (test_suite_span .span_id ))
440+ test_class_hierarchy = _get_test_class_hierarchy (item )
441+ if test_class_hierarchy :
442+ span .set_tag_str (test .CLASS_HIERARCHY , test_class_hierarchy )
443+ if hasattr (item , "dtest" ) and isinstance (item .dtest , DocTest ):
444+ span .set_tag_str (test .SUITE , "{}.py" .format (item .dtest .globs ["__name__" ]))
445+ else :
446+ span .set_tag_str (test .SUITE , test_suite_span .get_tag (test .SUITE ))
378447
379- test_module_span = _extract_span (pytest_package_item )
380- if not test_module_span :
381- test_module_span = _extract_module_span (pytest_module_item )
382- if test_module_span :
383- module_is_package = False
448+ span .set_tag_str (test .TYPE , SpanTypes .TEST )
449+ span .set_tag_str (test .FRAMEWORK_VERSION , pytest .__version__ )
384450
385- if test_module_span is None :
386- test_module_span , module_is_package = _start_test_module_span ( pytest_package_item , pytest_module_item )
451+ if item . location and item . location [ 0 ] :
452+ _CIVisibility . set_codeowners_of ( item . location [ 0 ], span = span )
387453
388- test_suite_span = _extract_span (pytest_module_item )
389- if pytest_module_item is not None and test_suite_span is None :
390- test_suite_span = _start_test_suite_span (pytest_module_item , test_module_span )
391- # Start coverage for the test suite if coverage is enabled
392- if _CIVisibility ._instance ._collect_coverage_enabled :
393- _initialize (str (item .config .rootdir ))
394- _coverage_start ()
454+ # We preemptively set FAIL as a status, because if pytest_runtest_makereport is not called
455+ # (where the actual test status is set), it means there was a pytest error
456+ span .set_tag_str (test .STATUS , test .Status .FAIL .value )
395457
396- with _CIVisibility ._instance .tracer ._start_span (
397- ddtrace .config .pytest .operation_name ,
398- service = _CIVisibility ._instance ._service ,
399- resource = item .nodeid ,
400- span_type = SpanTypes .TEST ,
401- activate = True ,
402- ) as span :
403- span .set_tag_str (COMPONENT , "pytest" )
404- span .set_tag_str (SPAN_KIND , KIND )
405- span .set_tag_str (test .FRAMEWORK , FRAMEWORK )
406- span .set_tag_str (_EVENT_TYPE , SpanTypes .TEST )
407- span .set_tag_str (test .NAME , item .name )
408- span .set_tag_str (test .COMMAND , _get_pytest_command (item .config ))
409- span .set_tag_str (_SESSION_ID , str (test_session_span .span_id ))
410-
411- span .set_tag_str (_MODULE_ID , str (test_module_span .span_id ))
412- span .set_tag_str (test .MODULE , test_module_span .get_tag (test .MODULE ))
413- span .set_tag_str (test .MODULE_PATH , test_module_span .get_tag (test .MODULE_PATH ))
414-
415- span .set_tag_str (_SUITE_ID , str (test_suite_span .span_id ))
416- test_class_hierarchy = _get_test_class_hierarchy (item )
417- if test_class_hierarchy :
418- span .set_tag_str (test .CLASS_HIERARCHY , test_class_hierarchy )
419- if hasattr (item , "dtest" ) and isinstance (item .dtest , DocTest ):
420- span .set_tag_str (test .SUITE , "{}.py" .format (item .dtest .globs ["__name__" ]))
421- else :
422- span .set_tag_str (test .SUITE , test_suite_span .get_tag (test .SUITE ))
423-
424- span .set_tag_str (test .TYPE , SpanTypes .TEST )
425- span .set_tag_str (test .FRAMEWORK_VERSION , pytest .__version__ )
426-
427- if item .location and item .location [0 ]:
428- _CIVisibility .set_codeowners_of (item .location [0 ], span = span )
429-
430- # We preemptively set FAIL as a status, because if pytest_runtest_makereport is not called
431- # (where the actual test status is set), it means there was a pytest error
432- span .set_tag_str (test .STATUS , test .Status .FAIL .value )
433-
434- # Parameterized test cases will have a `callspec` attribute attached to the pytest Item object.
435- # Pytest docs: https://docs.pytest.org/en/6.2.x/reference.html#pytest.Function
436- if getattr (item , "callspec" , None ):
437- parameters = {"arguments" : {}, "metadata" : {}} # type: Dict[str, Dict[str, str]]
438- for param_name , param_val in item .callspec .params .items ():
439- try :
440- parameters ["arguments" ][param_name ] = encode_test_parameter (param_val )
441- except Exception :
442- parameters ["arguments" ][param_name ] = "Could not encode"
443- log .warning ("Failed to encode %r" , param_name , exc_info = True )
444- span .set_tag_str (test .PARAMETERS , json .dumps (parameters ))
445-
446- markers = [marker .kwargs for marker in item .iter_markers (name = "dd_tags" )]
447- for tags in markers :
448- span .set_tags (tags )
449- _store_span (item , span )
450-
451- # Run the actual test
452- yield
458+ # Parameterized test cases will have a `callspec` attribute attached to the pytest Item object.
459+ # Pytest docs: https://docs.pytest.org/en/6.2.x/reference.html#pytest.Function
460+ if getattr (item , "callspec" , None ):
461+ parameters = {"arguments" : {}, "metadata" : {}} # type: Dict[str, Dict[str, str]]
462+ for param_name , param_val in item .callspec .params .items ():
463+ try :
464+ parameters ["arguments" ][param_name ] = encode_test_parameter (param_val )
465+ except Exception :
466+ parameters ["arguments" ][param_name ] = "Could not encode"
467+ log .warning ("Failed to encode %r" , param_name , exc_info = True )
468+ span .set_tag_str (test .PARAMETERS , json .dumps (parameters ))
469+
470+ markers = [marker .kwargs for marker in item .iter_markers (name = "dd_tags" )]
471+ for tags in markers :
472+ span .set_tags (tags )
473+ _store_span (item , span )
474+
475+ coverage_per_test = (
476+ _get_test_skipping_level () == TEST
477+ and _CIVisibility ._instance ._collect_coverage_enabled
478+ and not is_skipped_by_itr
479+ )
480+ if coverage_per_test :
481+ _attach_coverage (item )
482+ # Run the actual test
483+ yield
484+ # Finish coverage for the test suite if coverage is enabled
485+ if coverage_per_test :
486+ _detach_coverage (item , span )
453487
454488 nextitem_pytest_module_item = _find_pytest_item (nextitem , pytest .Module )
455489 if nextitem is None or nextitem_pytest_module_item != pytest_module_item and not test_suite_span .finished :
456490 _mark_test_status (pytest_module_item , test_suite_span )
457491 # Finish coverage for the test suite if coverage is enabled
458- if _CIVisibility ._instance ._collect_coverage_enabled :
459- _coverage_end (test_suite_span )
492+ if (
493+ _get_test_skipping_level () == SUITE
494+ and _CIVisibility ._instance ._collect_coverage_enabled
495+ and not is_skipped_by_itr
496+ ):
497+ _detach_coverage (pytest_module_item , test_suite_span )
460498 test_suite_span .finish ()
461499
462500 if not module_is_package :
0 commit comments