Skip to content

Commit 0460a1f

Browse files
fix(ci_visibility): fix corner case in Python 3.9 code coverage instrumentation (#15697)
## Description In Python 3.9, `dis.findlinestarts()` (used to determine where to insert instrumentation calls) can return imprecise line number values in some corner cases. This PR ensures that we do not explode in such cases (even though the line number information may not be 100% precise). ## Testing Unit tests. ## Risks None. ## Additional Notes Perhaps a better solution would be not to rely on `dis.findlinestarts()` and implement our own routine for identifying line starts in Python 3.9. But I don't think it's worth doing given that Python 3.9 is itself past its end of life. (cherry picked from commit fbc2b78)
1 parent c4ddcba commit 0460a1f

File tree

5 files changed

+78
-1
lines changed

5 files changed

+78
-1
lines changed

ddtrace/internal/coverage/instrumentation_py3_9.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def update_location_data(
145145
new_data.append(line_delta)
146146

147147
# Also add the current trap size to the accumulated offset
148-
accumulated_new_offset = trap_map[current_orig_offset] << 1
148+
accumulated_new_offset = trap_map.get(current_orig_offset, 0) << 1
149149
current_new_offset += accumulated_new_offset
150150

151151
return bytes(new_data)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ exclude = [
145145
"tests/contrib/grpc/hello_pb2.py",
146146
"tests/contrib/django_celery/app/*",
147147
"tests/contrib/protobuf/schemas/**/*.py",
148+
"tests/coverage/included_path/py39_line_numbers.py",
148149
"tests/appsec/iast/fixtures/ast/str/non_utf8_content.py",
149150
"tests/appsec/iast/fixtures/aspects/str/non_utf8_content.py",
150151
"tests/lib-injection/dd-lib-python-init-test-protobuf-old/addressbook_pb2.py"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
fixes:
3+
- |
4+
CI Visibility: This fix resolves an issue where code coverage instrumentation in Python 3.9 would raise an exception
5+
while handling line numbers in some corner cases.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
x = {}
2+
3+
def g(*args, **kwargs):
4+
pass
5+
6+
7+
def precise_line_numbers():
8+
g(None,
9+
**x)
10+
11+
12+
def imprecise_line_numbers():
13+
g(
14+
None, **x)
15+
16+
17+
def call_all_functions():
18+
precise_line_numbers()
19+
imprecise_line_numbers()
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import pytest
2+
3+
4+
@pytest.mark.subprocess
5+
def test_coverage_py39_line_numbers():
6+
"""
7+
In Python 3.9, `dis.findlinestarts()` (used to determine where to insert instrumentation calls) can return
8+
imprecise line number values in some corner cases. This test ensures that we do not explode in such cases (even
9+
though the line number information may not be 100% precise).
10+
"""
11+
import os
12+
from pathlib import Path
13+
14+
from ddtrace.internal.coverage.code import ModuleCodeCollector
15+
from ddtrace.internal.coverage.installer import install
16+
from tests.coverage.utils import _get_relpath_dict
17+
18+
cwd_path = os.getcwd()
19+
include_path = Path(cwd_path + "/tests/coverage/included_path/")
20+
21+
install(include_paths=[include_path], collect_import_time_coverage=True)
22+
23+
from tests.coverage.included_path.py39_line_numbers import call_all_functions
24+
25+
ModuleCodeCollector.start_coverage()
26+
call_all_functions()
27+
ModuleCodeCollector.stop_coverage()
28+
29+
executable = _get_relpath_dict(cwd_path, ModuleCodeCollector._instance.lines)
30+
covered = _get_relpath_dict(cwd_path, ModuleCodeCollector._instance._get_covered_lines(include_imported=False))
31+
covered_with_imports = _get_relpath_dict(
32+
cwd_path, ModuleCodeCollector._instance._get_covered_lines(include_imported=True)
33+
)
34+
35+
expected_executable = {
36+
"tests/coverage/included_path/py39_line_numbers.py": {1, 3, 4, 7, 8, 9, 12, 13, 14, 17, 18, 19},
37+
}
38+
expected_covered = {
39+
"tests/coverage/included_path/py39_line_numbers.py": {4, 8, 9, 13, 14, 18, 19},
40+
}
41+
expected_covered_with_imports = {
42+
"tests/coverage/included_path/py39_line_numbers.py": {1, 3, 4, 7, 8, 9, 12, 13, 14, 17, 18, 19},
43+
}
44+
45+
assert executable == expected_executable, (
46+
f"Executable lines mismatch: expected={expected_executable} vs actual={executable}"
47+
)
48+
assert covered == expected_covered, f"Covered lines mismatch: expected={expected_covered} vs actual={covered}"
49+
assert covered_with_imports == expected_covered_with_imports, (
50+
f"Covered lines with imports mismatch: expected={expected_covered_with_imports} "
51+
f"vs actual={covered_with_imports}"
52+
)

0 commit comments

Comments
 (0)