Skip to content

Commit 4fd3010

Browse files
fix(pytest+unittest): add safety check for Windows paths [backport 2.2] (#7680)
1 parent 9384499 commit 4fd3010

File tree

5 files changed

+65
-14
lines changed

5 files changed

+65
-14
lines changed

ddtrace/contrib/pytest/plugin.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from ddtrace.internal.ci_visibility.coverage import _initialize_coverage
4848
from ddtrace.internal.ci_visibility.coverage import build_payload as build_coverage_payload
4949
from ddtrace.internal.ci_visibility.utils import _add_start_end_source_file_path_data_to_span
50+
from ddtrace.internal.ci_visibility.utils import get_relative_or_absolute_path_for_path
5051
from ddtrace.internal.constants import COMPONENT
5152
from ddtrace.internal.logger import get_logger
5253

@@ -642,7 +643,7 @@ def pytest_runtest_protocol(item, nextitem):
642643
_CIVisibility.set_codeowners_of(item.location[0], span=span)
643644
if hasattr(item, "_obj"):
644645
test_method_object = item._obj
645-
_add_start_end_source_file_path_data_to_span(span, test_method_object, test_name)
646+
_add_start_end_source_file_path_data_to_span(span, test_method_object, test_name, item.config.rootdir)
646647

647648
# We preemptively set FAIL as a status, because if pytest_runtest_makereport is not called
648649
# (where the actual test status is set), it means there was a pytest error
@@ -806,8 +807,7 @@ def pytest_ddtrace_get_item_suite_name(item):
806807
if test_module_path:
807808
if not pytest_module_item.nodeid.startswith(test_module_path):
808809
log.warning("Suite path is not under module path: '%s' '%s'", pytest_module_item.nodeid, test_module_path)
809-
suite_path = os.path.relpath(pytest_module_item.nodeid, start=test_module_path)
810-
return suite_path
810+
return get_relative_or_absolute_path_for_path(pytest_module_item.nodeid, test_module_path)
811811
return pytest_module_item.nodeid
812812

813813

ddtrace/contrib/unittest/patch.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from ddtrace.internal.ci_visibility.coverage import _initialize_coverage
3232
from ddtrace.internal.ci_visibility.coverage import build_payload as build_coverage_payload
3333
from ddtrace.internal.ci_visibility.utils import _add_start_end_source_file_path_data_to_span
34+
from ddtrace.internal.ci_visibility.utils import get_relative_or_absolute_path_for_path
3435
from ddtrace.internal.constants import COMPONENT
3536
from ddtrace.internal.logger import get_logger
3637
from ddtrace.internal.utils.formats import asbool
@@ -258,7 +259,14 @@ def _extract_test_file_name(item) -> str:
258259

259260
def _extract_module_file_path(item) -> str:
260261
if _is_test(item):
261-
return os.path.relpath(inspect.getfile(item.__class__))
262+
try:
263+
test_module_object = inspect.getfile(item.__class__)
264+
except TypeError:
265+
log.debug(
266+
"Tried to collect module file path but it is a built-in Python function",
267+
)
268+
return ""
269+
return get_relative_or_absolute_path_for_path(test_module_object, os.getcwd())
262270

263271
return ""
264272

@@ -537,14 +545,18 @@ def add_xpass_test_wrapper(func, instance, args: tuple, kwargs: dict):
537545
def _mark_test_as_unskippable(obj):
538546
test_name = obj.__name__
539547
test_suite_name = str(obj).split(".")[0].split()[1]
540-
test_module_path = os.path.relpath(obj.__code__.co_filename)
548+
test_module_path = get_relative_or_absolute_path_for_path(obj.__code__.co_filename, os.getcwd())
541549
test_module_suite_name = _generate_module_suite_test_path(test_module_path, test_suite_name, test_name)
542550
_CIVisibility._unittest_data["unskippable_tests"].add(test_module_suite_name)
543551
return obj
544552

545553

554+
def _using_unskippable_decorator(args, kwargs):
555+
return args[0] is False and _extract_skip_if_reason(args, kwargs) == ITR_UNSKIPPABLE_REASON
556+
557+
546558
def skip_if_decorator(func, instance, args: tuple, kwargs: dict):
547-
if args[0] is False and _extract_skip_if_reason(args, kwargs) == ITR_UNSKIPPABLE_REASON:
559+
if _using_unskippable_decorator(args, kwargs):
548560
return _mark_test_as_unskippable
549561
return func(*args, **kwargs)
550562

@@ -789,7 +801,7 @@ def _start_test_span(instance, test_suite_span: ddtrace.Span) -> ddtrace.Span:
789801

790802
_CIVisibility.set_codeowners_of(_extract_test_file_name(instance), span=span)
791803

792-
_add_start_end_source_file_path_data_to_span(span, test_method_object, test_name)
804+
_add_start_end_source_file_path_data_to_span(span, test_method_object, test_name, os.getcwd())
793805

794806
_store_test_span(instance, span)
795807
return span

ddtrace/internal/ci_visibility/utils.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,25 @@
1010
log = get_logger(__name__)
1111

1212

13-
def get_source_file_path_for_test_method(test_method_object) -> typing.Union[str, None]:
13+
def get_relative_or_absolute_path_for_path(path: str, start_directory: str):
1414
try:
15-
source_file_path = os.path.relpath(inspect.getfile(test_method_object))
15+
relative_path = os.path.relpath(path, start=start_directory)
16+
except ValueError:
17+
log.debug(
18+
"Tried to collect relative path but it is using different drive paths on Windows, "
19+
"using absolute path instead",
20+
)
21+
return os.path.abspath(path)
22+
return relative_path
23+
24+
25+
def get_source_file_path_for_test_method(test_method_object, repo_directory: str) -> typing.Union[str, None]:
26+
try:
27+
file_object = inspect.getfile(test_method_object)
1628
except TypeError:
17-
return None
18-
return source_file_path
29+
return ""
30+
31+
return get_relative_or_absolute_path_for_path(file_object, repo_directory)
1932

2033

2134
def get_source_lines_for_test_method(
@@ -30,16 +43,21 @@ def get_source_lines_for_test_method(
3043
return start_line, end_line
3144

3245

33-
def _add_start_end_source_file_path_data_to_span(span: ddtrace.Span, test_method_object, test_name: str):
46+
def _add_start_end_source_file_path_data_to_span(
47+
span: ddtrace.Span, test_method_object, test_name: str, repo_directory: str
48+
):
3449
if not test_method_object:
3550
log.debug(
3651
"Tried to collect source start/end lines for test method %s but test method object could not be found",
3752
test_name,
3853
)
3954
return
40-
source_file_path = get_source_file_path_for_test_method(test_method_object)
55+
source_file_path = get_source_file_path_for_test_method(test_method_object, repo_directory)
4156
if not source_file_path:
42-
log.debug("Tried to collect file path for test %s but it is a built-in Python function", test_name)
57+
log.debug(
58+
"Tried to collect file path for test %s but it is a built-in Python function",
59+
test_name,
60+
)
4361
return
4462
start_line, end_line = get_source_lines_for_test_method(test_method_object)
4563
if not start_line or not end_line:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
CI Visibility: Fixes an issue where a ``ValueError`` was raised when using different path drives on Windows

tests/ci_visibility/test_utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import os.path
2+
from unittest import mock
3+
4+
from ddtrace.internal.ci_visibility.utils import get_relative_or_absolute_path_for_path
5+
6+
7+
def test_gets_relative_path():
8+
actual_output = get_relative_or_absolute_path_for_path("ddtrace/contrib", start_directory=os.getcwd())
9+
10+
assert not os.path.isabs(actual_output)
11+
12+
13+
def test_gets_absolute_path_with_exception():
14+
with mock.patch("os.path.relpath", side_effect=ValueError()):
15+
actual_output = get_relative_or_absolute_path_for_path("ddtrace/contrib", start_directory=os.getcwd())
16+
17+
assert os.path.isabs(actual_output)

0 commit comments

Comments
 (0)