Skip to content

Commit b4f8203

Browse files
fix(ci_visibility): add flag to show class in test name [backport 1.20] (#7278)
Backport 70dda63 from #7225 to 1.20. Provides a workaround to #4550 by adding a `--ddtrace-include-class-name` option that will, for class-based tests, add the class name to the test as `ClassName.test_name` . ## Checklist - [x] Change(s) are motivated and described in the PR description. - [x] Testing strategy is described if automated tests are not included in the PR. - [x] Risk is outlined (performance impact, potential for breakage, maintainability, etc). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] [Library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) are followed. If no release note is required, add label `changelog/no-changelog`. - [x] Documentation is included (in-code, generated user docs, [public corp docs](https://github.com/DataDog/documentation/)). - [x] Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Title is accurate. - [x] No unnecessary changes are introduced. - [x] Description motivates each change. - [x] Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [x] Testing strategy adequately addresses listed risk(s). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] Release note makes sense to a user of the library. - [x] Reviewer has explicitly acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment. - [x] Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting) - [x] If this PR touches code that signs or publishes builds or packages, or handles credentials of any kind, I've requested a review from `@DataDog/security-design-and-guidance`. - [x] This PR doesn't touch any of that. --------- Co-authored-by: Romain Komorn <[email protected]> Co-authored-by: Romain Komorn <[email protected]>
1 parent 5ec0177 commit b4f8203

File tree

5 files changed

+149
-9
lines changed

5 files changed

+149
-9
lines changed
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
FRAMEWORK = "pytest"
22
KIND = "test"
33

4-
HELP_MSG = "Enable tracing of pytest functions."
4+
DDTRACE_HELP_MSG = "Enable tracing of pytest functions."
5+
NO_DDTRACE_HELP_MSG = "Disable tracing of pytest functions."
6+
DDTRACE_INCLUDE_CLASS_HELP_MSG = "Prepend 'ClassName.' to names of class-based tests."
57

68
# XFail Reason
79
XFAIL_REASON = "pytest.xfail.reason"

ddtrace/contrib/pytest/plugin.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222

2323
import ddtrace
2424
from ddtrace.constants import SPAN_KIND
25+
from ddtrace.contrib.pytest.constants import DDTRACE_HELP_MSG
26+
from ddtrace.contrib.pytest.constants import DDTRACE_INCLUDE_CLASS_HELP_MSG
2527
from ddtrace.contrib.pytest.constants import FRAMEWORK
26-
from ddtrace.contrib.pytest.constants import HELP_MSG
2728
from ddtrace.contrib.pytest.constants import KIND
29+
from ddtrace.contrib.pytest.constants import NO_DDTRACE_HELP_MSG
2830
from ddtrace.contrib.pytest.constants import XFAIL_REASON
2931
from ddtrace.contrib.unittest import unpatch as unpatch_unittest
3032
from ddtrace.ext import SpanTypes
@@ -229,6 +231,14 @@ def _get_suite_name(item, test_module_path=None):
229231
return item.nodeid
230232

231233

234+
def _get_item_name(item):
235+
"""Extract name from item, prepending class if desired"""
236+
if hasattr(item, "cls") and item.cls:
237+
if item.config.getoption("ddtrace-include-class-name") or item.config.getini("ddtrace-include-class-name"):
238+
return "%s.%s" % (item.cls.__name__, item.name)
239+
return item.name
240+
241+
232242
def _is_test_unskippable(item):
233243
return any(
234244
[
@@ -348,15 +358,15 @@ def pytest_addoption(parser):
348358
action="store_true",
349359
dest="ddtrace",
350360
default=False,
351-
help=HELP_MSG,
361+
help=DDTRACE_HELP_MSG,
352362
)
353363

354364
group._addoption(
355365
"--no-ddtrace",
356366
action="store_true",
357367
dest="no-ddtrace",
358368
default=False,
359-
help=HELP_MSG,
369+
help=NO_DDTRACE_HELP_MSG,
360370
)
361371

362372
group._addoption(
@@ -367,9 +377,18 @@ def pytest_addoption(parser):
367377
help=PATCH_ALL_HELP_MSG,
368378
)
369379

370-
parser.addini("ddtrace", HELP_MSG, type="bool")
371-
parser.addini("no-ddtrace", HELP_MSG, type="bool")
380+
group._addoption(
381+
"--ddtrace-include-class-name",
382+
action="store_true",
383+
dest="ddtrace-include-class-name",
384+
default=False,
385+
help=DDTRACE_INCLUDE_CLASS_HELP_MSG,
386+
)
387+
388+
parser.addini("ddtrace", DDTRACE_HELP_MSG, type="bool")
389+
parser.addini("no-ddtrace", DDTRACE_HELP_MSG, type="bool")
372390
parser.addini("ddtrace-patch-all", PATCH_ALL_HELP_MSG, type="bool")
391+
parser.addini("ddtrace-include-class-name", DDTRACE_INCLUDE_CLASS_HELP_MSG, type="bool")
373392

374393

375394
def pytest_configure(config):
@@ -494,8 +513,10 @@ def pytest_collection_modifyitems(session, config, items):
494513
for item in items:
495514
test_is_unskippable = _is_test_unskippable(item)
496515

516+
item_name = _get_item_name(item)
517+
497518
if test_is_unskippable:
498-
log.debug("Test %s in module %s is marked as unskippable", (item.name, item.module))
519+
log.debug("Test %s in module %s is marked as unskippable", (item_name, item.module))
499520
item._dd_itr_test_unskippable = True
500521

501522
# Due to suite skipping mode, defer adding ITR skip marker until unskippable status of the suite has been
@@ -512,7 +533,7 @@ def pytest_collection_modifyitems(session, config, items):
512533
item_to_skip._dd_itr_forced = True
513534
items_to_skip_by_module[item.module] = []
514535

515-
if _CIVisibility._instance._should_skip_path(str(get_fslocation_from_item(item)[0]), item.name):
536+
if _CIVisibility._instance._should_skip_path(str(get_fslocation_from_item(item)[0]), item_name):
516537
if test_is_unskippable or (
517538
_CIVisibility._instance._suite_skipping_mode and current_suite_has_unskippable_test
518539
):
@@ -547,6 +568,8 @@ def pytest_runtest_protocol(item, nextitem):
547568
)
548569
)
549570

571+
item_name = _get_item_name(item)
572+
550573
test_session_span = _extract_span(item.session)
551574

552575
pytest_module_item = _find_pytest_item(item, pytest.Module)
@@ -605,7 +628,7 @@ def pytest_runtest_protocol(item, nextitem):
605628
span.set_tag_str(SPAN_KIND, KIND)
606629
span.set_tag_str(test.FRAMEWORK, FRAMEWORK)
607630
span.set_tag_str(_EVENT_TYPE, SpanTypes.TEST)
608-
span.set_tag_str(test.NAME, item.name)
631+
span.set_tag_str(test.NAME, item_name)
609632
span.set_tag_str(test.COMMAND, _get_pytest_command(item.config))
610633
if test_session_span:
611634
span.set_tag_str(_SESSION_ID, str(test_session_span.span_id))

docs/spelling_wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ plugin
164164
posix
165165
postgres
166166
pre
167+
prepend
167168
prepended
168169
profiler
169170
protobuf
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
fixes:
3+
- |
4+
CI Visibility: fixes an issue where class-based test methods with the same
5+
name across classes would be considered duplicates, and cause one (or more)
6+
tests to be dropped from results, by adding ``--ddtrace-include-class-name``
7+
as an optional flag (defaulting to false) to prepend the class name to the
8+
test name.

tests/contrib/pytest/test_pytest.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2992,3 +2992,109 @@ def test_inner_wasnot_going_to_skip_skipif():
29922992
assert inner_module_span.get_tag("test.itr.tests_skipping.tests_skipped") == "false"
29932993
assert inner_module_span.get_tag("_dd.ci.itr.tests_skipped") == "false"
29942994
assert inner_module_span.get_tag("test.itr.forced_run") == "true"
2995+
2996+
def test_pytest_ddtrace_test_names(self):
2997+
package_outer_dir = self.testdir.mkpydir("test_package")
2998+
os.chdir(str(package_outer_dir))
2999+
with open("test_names.py", "w+") as fd:
3000+
fd.write(
3001+
textwrap.dedent(
3002+
(
3003+
"""
3004+
def test_ok():
3005+
assert True
3006+
3007+
class TestClassOne():
3008+
def test_ok(self):
3009+
assert True
3010+
3011+
class TestClassTwo():
3012+
def test_ok(self):
3013+
assert True
3014+
"""
3015+
)
3016+
)
3017+
)
3018+
3019+
self.testdir.chdir()
3020+
self.inline_run("--ddtrace")
3021+
3022+
spans = self.pop_spans()
3023+
assert len(spans) == 6
3024+
3025+
session_span = [span for span in spans if span.get_tag("type") == "test_session_end"][0]
3026+
assert session_span.get_tag("test.status") == "pass"
3027+
3028+
module_span = [span for span in spans if span.get_tag("type") == "test_module_end"][0]
3029+
assert module_span.get_tag("test.module") == "test_package"
3030+
3031+
suite_span = [span for span in spans if span.get_tag("type") == "test_suite_end"][0]
3032+
assert suite_span.get_tag("test.module") == "test_package"
3033+
assert suite_span.get_tag("test.suite") == "test_names.py"
3034+
3035+
test_spans = [span for span in spans if span.get_tag("type") == "test"]
3036+
assert len(test_spans) == 3
3037+
assert test_spans[0].get_tag("test.module") == "test_package"
3038+
assert test_spans[0].get_tag("test.suite") == "test_names.py"
3039+
assert test_spans[0].get_tag("test.name") == "test_ok"
3040+
3041+
assert test_spans[1].get_tag("test.module") == "test_package"
3042+
assert test_spans[1].get_tag("test.suite") == "test_names.py"
3043+
assert test_spans[1].get_tag("test.name") == "test_ok"
3044+
3045+
assert test_spans[2].get_tag("test.module") == "test_package"
3046+
assert test_spans[2].get_tag("test.suite") == "test_names.py"
3047+
assert test_spans[2].get_tag("test.name") == "test_ok"
3048+
3049+
def test_pytest_ddtrace_test_names_include_class_opt(self):
3050+
package_outer_dir = self.testdir.mkpydir("test_package")
3051+
os.chdir(str(package_outer_dir))
3052+
with open("test_names.py", "w+") as fd:
3053+
fd.write(
3054+
textwrap.dedent(
3055+
(
3056+
"""
3057+
def test_ok():
3058+
assert True
3059+
3060+
class TestClassOne():
3061+
def test_ok(self):
3062+
assert True
3063+
3064+
class TestClassTwo():
3065+
def test_ok(self):
3066+
assert True
3067+
"""
3068+
)
3069+
)
3070+
)
3071+
3072+
self.testdir.chdir()
3073+
self.inline_run("--ddtrace", "--ddtrace-include-class-name")
3074+
3075+
spans = self.pop_spans()
3076+
assert len(spans) == 6
3077+
3078+
session_span = [span for span in spans if span.get_tag("type") == "test_session_end"][0]
3079+
assert session_span.get_tag("test.status") == "pass"
3080+
3081+
module_span = [span for span in spans if span.get_tag("type") == "test_module_end"][0]
3082+
assert module_span.get_tag("test.module") == "test_package"
3083+
3084+
suite_span = [span for span in spans if span.get_tag("type") == "test_suite_end"][0]
3085+
assert suite_span.get_tag("test.module") == "test_package"
3086+
assert suite_span.get_tag("test.suite") == "test_names.py"
3087+
3088+
test_spans = [span for span in spans if span.get_tag("type") == "test"]
3089+
assert len(test_spans) == 3
3090+
assert test_spans[0].get_tag("test.module") == "test_package"
3091+
assert test_spans[0].get_tag("test.suite") == "test_names.py"
3092+
assert test_spans[0].get_tag("test.name") == "test_ok"
3093+
3094+
assert test_spans[1].get_tag("test.module") == "test_package"
3095+
assert test_spans[1].get_tag("test.suite") == "test_names.py"
3096+
assert test_spans[1].get_tag("test.name") == "TestClassOne.test_ok"
3097+
3098+
assert test_spans[2].get_tag("test.module") == "test_package"
3099+
assert test_spans[2].get_tag("test.suite") == "test_names.py"
3100+
assert test_spans[2].get_tag("test.name") == "TestClassTwo.test_ok"

0 commit comments

Comments
 (0)