Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(ci_visibility): add support for bitmap-based coverage data #10270

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
d4307ea
WIP v2 of CI Visibility plugin
romainkomorndatadog Aug 2, 2024
a0c606e
Merge branch 'refs/heads/main' into romain.komorn/SDTEST-225/introduc…
romainkomorndatadog Aug 2, 2024
748eb8c
properly deal with adding children
romainkomorndatadog Aug 2, 2024
7dc408b
dont restart items that have already started
romainkomorndatadog Aug 2, 2024
36991ba
optimize not finishing suites/modules unnecessarily
romainkomorndatadog Aug 2, 2024
72447ab
don't log warning if item already exists
romainkomorndatadog Aug 5, 2024
ee7e202
Merge branch 'refs/heads/main' into romain.komorn/SDTEST-225/pytest_p…
romainkomorndatadog Aug 6, 2024
d78bbfd
Add inital ITR support
romainkomorndatadog Aug 6, 2024
811ddc2
make children private
romainkomorndatadog Aug 7, 2024
5e379fc
fix should collect coverage
romainkomorndatadog Aug 7, 2024
817b235
add telemetry and imported lines dependency
romainkomorndatadog Aug 7, 2024
51b23cc
Merge branch 'main' into romain.komorn/SDTEST-225/pytest_plugin_v2_it…
romainkomorndatadog Aug 7, 2024
3c99d36
comment
romainkomorndatadog Aug 7, 2024
9e5c6c9
Merge branch 'romain.komorn/SDTEST-225/pytest_plugin_v2_itr_support' …
romainkomorndatadog Aug 7, 2024
84f5cdf
factor out source file info to util, move utils to _utils
romainkomorndatadog Aug 8, 2024
626c427
Merge branch 'main' into romain.komorn/SDTEST-225/pytest_plugin_v2_it…
romainkomorndatadog Aug 8, 2024
a2811df
update tests
romainkomorndatadog Aug 8, 2024
6ba4880
fix parent relationship, add bunch of return type hints
romainkomorndatadog Aug 9, 2024
361c0b9
stash
romainkomorndatadog Aug 11, 2024
44de0d8
fix tags
romainkomorndatadog Aug 11, 2024
f4eca82
Merge branch 'refs/heads/main' into romain.komorn/SDTEST-225/add_test…
romainkomorndatadog Aug 12, 2024
e33d972
fix more tests
romainkomorndatadog Aug 12, 2024
c099289
stash
romainkomorndatadog Aug 12, 2024
817b852
more test fixes
romainkomorndatadog Aug 12, 2024
0e10d27
add suite and v2 snapshots
romainkomorndatadog Aug 12, 2024
8806ed2
fix hatch env name
romainkomorndatadog Aug 12, 2024
e8f39dd
quote hatch env
romainkomorndatadog Aug 12, 2024
6b8514a
single quote inside
romainkomorndatadog Aug 12, 2024
d4282dd
Only v2
romainkomorndatadog Aug 12, 2024
7749bae
3.7 fixes
romainkomorndatadog Aug 12, 2024
9012b28
remove unnecessary test
romainkomorndatadog Aug 13, 2024
cf90292
telemetry tweaks and log fix
romainkomorndatadog Aug 13, 2024
f394ac8
fix telemetry (again)
romainkomorndatadog Aug 13, 2024
f2a14f5
Merge branch 'refs/heads/main' into romain.komorn/SDTEST-225/add_test…
romainkomorndatadog Aug 13, 2024
5c93c19
don't print stack
romainkomorndatadog Aug 13, 2024
cc27394
another comment fix
romainkomorndatadog Aug 13, 2024
69578db
move v2 functions to try-except blocks, revert sub-plugin strategy
romainkomorndatadog Aug 13, 2024
9cc3811
remove test hook
romainkomorndatadog Aug 13, 2024
392e5d7
remove docstring
romainkomorndatadog Aug 13, 2024
a1ac5a1
Update _plugin_v2.py
romainkomorndatadog Aug 13, 2024
1d71b25
Merge branch 'main' into romain.komorn/SDTEST-225/remove_sub_plugin_a…
romainkomorndatadog Aug 14, 2024
d927635
Merge branch 'main' into romain.komorn/SDTEST-225/remove_sub_plugin_a…
romainkomorndatadog Aug 14, 2024
4b27df4
fmt
romainkomorndatadog Aug 14, 2024
32c568f
fix missed merge, apparently, and add return statements
romainkomorndatadog Aug 16, 2024
002292a
stash
romainkomorndatadog Aug 14, 2024
8fd6ce2
use special session ID type
romainkomorndatadog Aug 16, 2024
3f31b98
fixes for snapshot tests
romainkomorndatadog Aug 16, 2024
00620a8
bah fmt
romainkomorndatadog Aug 16, 2024
e168398
Merge branch 'refs/heads/main' into romain.komorn/SDTEST-225/simplify…
romainkomorndatadog Aug 17, 2024
10a076f
uncomment abstractmethod decorators
romainkomorndatadog Aug 17, 2024
d7fc885
install ModuleCodeCollector in v2
romainkomorndatadog Aug 17, 2024
1197806
add support for bitmaps
romainkomorndatadog Aug 17, 2024
6952a48
update hatch env to latest and use int.bin_count if available
romainkomorndatadog Aug 19, 2024
4bbc155
Merge branch 'refs/heads/main' into romain.komorn/SDTEST-225/use_byte…
romainkomorndatadog Aug 20, 2024
6fdc3c3
deal with little endianness
romainkomorndatadog Aug 20, 2024
d24edd2
fix dd_coverage tests
romainkomorndatadog Aug 21, 2024
4954a5e
fix pickling security complaints
romainkomorndatadog Aug 21, 2024
042ec4f
fix fake runner coverage tests
romainkomorndatadog Aug 22, 2024
59a4d18
remove types
romainkomorndatadog Aug 22, 2024
780429a
account for pytest 3.7 coverage difference
romainkomorndatadog Aug 22, 2024
db63ba4
Merge branch 'main' into romain.komorn/SDTEST-225/use_bytearrays_for_…
romainkomorndatadog Aug 22, 2024
92a52e5
Update ddtrace/contrib/pytest/_plugin_v2.py
romainkomorndatadog Aug 22, 2024
fc7799f
Update ddtrace/internal/ci_visibility/api/ci_base.py
romainkomorndatadog Aug 22, 2024
149caa0
Merge branch 'main' into romain.komorn/SDTEST-225/use_bytearrays_for_…
romainkomorndatadog Aug 22, 2024
bfa0678
Merge branch 'main' into romain.komorn/SDTEST-225/use_bytearrays_for_…
romainkomorndatadog Aug 26, 2024
9501af1
Merge branch 'main' into romain.komorn/SDTEST-225/use_bytearrays_for_…
romainkomorndatadog Aug 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 5 additions & 16 deletions ddtrace/contrib/pytest/_plugin_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,13 @@
from ddtrace.ext.ci_visibility.api import is_ci_visibility_enabled
from ddtrace.internal.ci_visibility.constants import SKIPPED_BY_ITR_REASON
from ddtrace.internal.ci_visibility.telemetry.coverage import COVERAGE_LIBRARY
from ddtrace.internal.ci_visibility.telemetry.coverage import record_code_coverage_empty
from ddtrace.internal.ci_visibility.telemetry.coverage import record_code_coverage_finished
from ddtrace.internal.ci_visibility.telemetry.coverage import record_code_coverage_started
from ddtrace.internal.ci_visibility.utils import take_over_logger_stream_handler
from ddtrace.internal.coverage.code import ModuleCodeCollector
from ddtrace.internal.coverage.installer import install as install_coverage
from ddtrace.internal.coverage.util import collapse_ranges
from ddtrace.internal.coverage.lines import CoverageLines
from ddtrace.internal.logger import get_logger


Expand Down Expand Up @@ -96,25 +97,13 @@ def _handle_collected_coverage(test_id, coverage_collector) -> None:

if not test_covered_lines:
log.debug("No covered lines found for test %s", test_id)
record_code_coverage_empty()
return

# TODO: switch representation to bytearrays as part of new ITR coverage strategy
# This code is temporary / PoC

coverage_data: t.Dict[Path, t.List[t.Tuple[int, int]]] = {}
coverage_data: t.Dict[Path, CoverageLines] = {}

for path_str, covered_lines in test_covered_lines.items():
file_path = Path(path_str)
if not file_path.is_absolute():
file_path = file_path.resolve()

sorted_lines = sorted(covered_lines)

collapsed_ranges = collapse_ranges(sorted_lines)
file_segments = []
for file_segment in collapsed_ranges:
file_segments.append((file_segment[0], file_segment[1]))
coverage_data[file_path] = file_segments
coverage_data[Path(path_str).absolute()] = covered_lines

CISuite.add_coverage_data(test_id.parent_id, coverage_data)

Expand Down
6 changes: 3 additions & 3 deletions ddtrace/ext/ci_visibility/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from typing import List
from typing import NamedTuple
from typing import Optional
from typing import Tuple
from typing import Type
from typing import Union

Expand All @@ -43,6 +42,7 @@
from ddtrace.ext.test import Status as TestStatus
from ddtrace.internal import core
from ddtrace.internal.codeowners import Codeowners
from ddtrace.internal.coverage.lines import CoverageLines
from ddtrace.internal.logger import get_logger


Expand Down Expand Up @@ -421,11 +421,11 @@ def was_item_skipped_by_itr(item_id: Union[CISuiteId, CITestId]) -> bool:

class AddCoverageArgs(NamedTuple):
item_id: Union[_CIVisibilityChildItemIdBase, _CIVisibilityRootItemIdBase]
coverage_data: Dict[Path, List[Tuple[int, int]]]
coverage_data: Dict[Path, CoverageLines]

@staticmethod
@_catch_and_log_exceptions
def add_coverage_data(item_id: Union[CISuiteId, CITestId], coverage_data: Dict[Path, List[Tuple[int, int]]]):
def add_coverage_data(item_id: Union[CISuiteId, CITestId], coverage_data: Dict[Path, CoverageLines]):
log.debug("Adding coverage data for item %s: %s", item_id, coverage_data)
core.dispatch("ci_visibility.item.add_coverage_data", (CIITRMixin.AddCoverageArgs(item_id, coverage_data),))

Expand Down
6 changes: 3 additions & 3 deletions ddtrace/internal/ci_visibility/api/ci_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from typing import Generic
from typing import List
from typing import Optional
from typing import Tuple
from typing import TypeVar
from typing import Union

Expand All @@ -34,6 +33,7 @@
from ddtrace.internal.ci_visibility.telemetry.itr import record_itr_skipped
from ddtrace.internal.ci_visibility.telemetry.itr import record_itr_unskippable
from ddtrace.internal.constants import COMPONENT
from ddtrace.internal.coverage.lines import CoverageLines
from ddtrace.internal.logger import get_logger


Expand Down Expand Up @@ -431,15 +431,15 @@ def get_parent_span(self) -> Optional[Span]:
return None

@abc.abstractmethod
def add_coverage_data(self, coverage_data: Dict[Path, List[Tuple[int, int]]]) -> None:
def add_coverage_data(self, coverage_data: Dict[Path, CoverageLines]) -> None:
pass

@_require_span
def _add_coverage_data_tag(self) -> None:
if self._span is None:
return
if self._coverage_data:
self._span.set_tag_str(
self._span.set_struct_tag(
COVERAGE_TAG_NAME, self._coverage_data.build_payload(self._session_settings.workspace_path)
)

Expand Down
43 changes: 12 additions & 31 deletions ddtrace/internal/ci_visibility/api/ci_coverage_data.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from collections import defaultdict
import json
from pathlib import Path
from typing import Dict
from typing import List
from typing import Tuple

from ddtrace.internal.coverage.lines import CoverageLines


try:
Expand All @@ -15,60 +15,41 @@

class CoverageFilePayload(TypedDict):
filename: str
segments: List[Tuple[int, int, int, int, int]]


class CICoverageSegment:
"""Container for coverage segment data

Columns and counts are currently unused in the current data model in ddtrace.internal.ci_visibility.coverage and
.encoder , so they are not included in this class.
"""

def __init__(self, start_line: int, end_line: int):
self.start_line: int = start_line
self.end_line: int = end_line
bitmap: bytes


class CICoverageData:
"""Container for coverage data for an item (suite or test)"""

def __init__(self) -> None:
self._coverage_data: Dict[Path, List[CICoverageSegment]] = defaultdict(list)
self._coverage_data: Dict[Path, CoverageLines] = defaultdict(CoverageLines)

def __bool__(self):
return bool(self._coverage_data)

def add_coverage_segment(self, file_path: Path, start_line, end_line):
"""Add a coverage segment to the coverage data"""
self._coverage_data[file_path.absolute()].append(CICoverageSegment(start_line, end_line))

def add_coverage_segments(self, segments: Dict[Path, List[Tuple[int, int]]]):
def add_covered_files(self, covered_files: Dict[Path, CoverageLines]):
"""Add coverage segments to the coverage data"""
for file_path, segment_data in segments.items():
for segment in segment_data:
self.add_coverage_segment(file_path, segment[0], segment[1])
for file_path, covered_lines in covered_files.items():
self._coverage_data[file_path.absolute()].update(covered_lines)

def _build_payload(self, root_dir: Path) -> List[CoverageFilePayload]:
"""Generate a CI Visibility coverage payload

Tuples are used here since JSON serializes tuples as lists.
"""
coverage_data = []
for file_path, segments in self._coverage_data.items():
for file_path, covered_lines in self._coverage_data.items():
try:
# Report relative path unless the file path is not relative to root_dir
# Paths are assumed to be absolute based on having been converted at instantiation / add time.
relative_path = file_path.relative_to(root_dir)
except ValueError:
relative_path = file_path
segments_data = []
for segment in segments:
segments_data.append((segment.start_line, 0, segment.end_line, 0, -1))
file_payload: CoverageFilePayload = {"filename": str(relative_path), "segments": segments_data}
path_str = f"/{str(relative_path)}"
file_payload: CoverageFilePayload = {"filename": path_str, "bitmap": covered_lines.to_bytes()}
coverage_data.append(file_payload)
return coverage_data

def build_payload(self, root_dir: Path) -> str:
def build_payload(self, root_dir: Path) -> Dict[str, List[CoverageFilePayload]]:
"""Generate a CI Visibility coverage payload in JSON format"""
return json.dumps({"files": self._build_payload(root_dir)})
return {"files": self._build_payload(root_dir)}
4 changes: 1 addition & 3 deletions ddtrace/internal/ci_visibility/api/ci_module.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple

from ddtrace.ext import test
from ddtrace.ext.ci_visibility.api import CIModuleId
Expand Down Expand Up @@ -83,5 +81,5 @@ def _telemetry_record_event_finished(self):
test_framework=self._session_settings.test_framework_metric_name,
)

def add_coverage_data(self, coverage_data: Dict[Path, List[Tuple[int, int]]]):
def add_coverage_data(self, *args, **kwargs):
raise NotImplementedError("Coverage data cannot be added to modules.")
7 changes: 2 additions & 5 deletions ddtrace/internal/ci_visibility/api/ci_session.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
from pathlib import Path
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple

from ddtrace.ext import test
from ddtrace.ext.ci_visibility.api import CIModuleId
Expand Down Expand Up @@ -77,5 +74,5 @@ def _telemetry_record_event_finished(self):
is_unsupported_ci=self._session_settings.is_unsupported_ci,
)

def add_coverage_data(self, coverage_data: Dict[Path, List[Tuple[int, int]]]) -> None:
raise NotImplementedError("Coverage data cannot be added to sessions.")
def add_coverage_data(self, *args, **kwargs):
raise NotImplementedError("Coverage data cannot be added to modules.")
10 changes: 7 additions & 3 deletions ddtrace/internal/ci_visibility/api/ci_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple

from ddtrace.ext import test
from ddtrace.ext.ci_visibility.api import CISourceFileInfo
Expand All @@ -20,6 +19,7 @@
from ddtrace.internal.ci_visibility.telemetry.constants import EVENT_TYPES
from ddtrace.internal.ci_visibility.telemetry.events import record_event_created
from ddtrace.internal.ci_visibility.telemetry.events import record_event_finished
from ddtrace.internal.coverage.lines import CoverageLines
from ddtrace.internal.logger import get_logger


Expand All @@ -44,6 +44,10 @@ def __init__(

self._coverage_data: CICoverageData = CICoverageData()

def __repr__(self) -> str:
module_name = self.parent.name if self.parent is not None else "none"
return f"{self.__class__.__name__}(name={self.name}, module={module_name})"

def finish(self, force: bool = False, override_status: Optional[CITestStatus] = None) -> None:
super().finish(force=force, override_status=override_status)

Expand Down Expand Up @@ -88,5 +92,5 @@ def _telemetry_record_event_finished(self):
test_framework=self._session_settings.test_framework_metric_name,
)

def add_coverage_data(self, coverage_data: Dict[Path, List[Tuple[int, int]]]) -> None:
self._coverage_data.add_coverage_segments(coverage_data)
def add_coverage_data(self, coverage_data: Dict[Path, CoverageLines]) -> None:
self._coverage_data.add_covered_files(coverage_data)
12 changes: 8 additions & 4 deletions ddtrace/internal/ci_visibility/api/ci_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple

from ddtrace.ext import test
from ddtrace.ext.ci_visibility.api import CIExcInfo
Expand All @@ -17,6 +16,7 @@
from ddtrace.internal.ci_visibility.telemetry.constants import EVENT_TYPES
from ddtrace.internal.ci_visibility.telemetry.events import record_event_created
from ddtrace.internal.ci_visibility.telemetry.events import record_event_finished
from ddtrace.internal.coverage.lines import CoverageLines
from ddtrace.internal.logger import get_logger


Expand Down Expand Up @@ -60,7 +60,11 @@ def __init__(
self._is_benchmark = None

def __repr__(self) -> str:
return f"{self.__class__.__name__}(name={self.name}, parameters={self._parameters})"
suite_name = self.parent.name if self.parent is not None else "none"
module_name = self.parent.parent.name if self.parent is not None and self.parent.parent is not None else "none"
return "{}(name={}, suite={}, module={}, parameters={})".format(
self.__class__.__name__, self.name, suite_name, module_name, self._parameters
)

def _get_hierarchy_tags(self) -> Dict[str, str]:
return {
Expand Down Expand Up @@ -139,5 +143,5 @@ def make_early_flake_retry_from_test(self, original_test_id: CITestId, retry_num
),
)

def add_coverage_data(self, coverage_data: Dict[Path, List[Tuple[int, int]]]) -> None:
self._coverage_data.add_coverage_segments(coverage_data)
def add_coverage_data(self, coverage_data: Dict[Path, CoverageLines]) -> None:
self._coverage_data.add_covered_files(coverage_data)
26 changes: 23 additions & 3 deletions ddtrace/internal/ci_visibility/encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@
from ddtrace.internal.ci_visibility.telemetry.payload import record_endpoint_payload_events_count
from ddtrace.internal.ci_visibility.telemetry.payload import record_endpoint_payload_events_serialization_time
from ddtrace.internal.encoding import JSONEncoderV2
from ddtrace.internal.logger import get_logger
from ddtrace.internal.utils.time import StopWatch
from ddtrace.internal.writer.writer import NoEncodableSpansError


log = get_logger(__name__)

if TYPE_CHECKING: # pragma: no cover
from typing import Any # noqa:F401
from typing import Dict # noqa:F401
Expand Down Expand Up @@ -158,7 +161,11 @@ def _set_itr_suite_skipping_mode(self, new_value):
self.itr_suite_skipping_mode = new_value

def put(self, spans):
spans_with_coverage = [span for span in spans if COVERAGE_TAG_NAME in span.get_tags()]
spans_with_coverage = [
span
for span in spans
if COVERAGE_TAG_NAME in span.get_tags() or span.get_struct_tag(COVERAGE_TAG_NAME) is not None
]
if not spans_with_coverage:
raise NoEncodableSpansError()
return super(CIVisibilityCoverageEncoderV02, self).put(spans_with_coverage)
Expand Down Expand Up @@ -194,7 +201,10 @@ def _build_body(self, data):
def _build_data(self, traces):
# type: (List[List[Span]]) -> Optional[bytes]
normalized_covs = [
self._convert_span(span, "") for trace in traces for span in trace if COVERAGE_TAG_NAME in span.get_tags()
self._convert_span(span, "")
for trace in traces
for span in trace
if (COVERAGE_TAG_NAME in span.get_tags() or span.get_struct_tag(COVERAGE_TAG_NAME) is not None)
]
if not normalized_covs:
return None
Expand All @@ -211,13 +221,23 @@ def _build_payload(self, traces):

def _convert_span(self, span, dd_origin):
# type: (Span, str) -> Dict[str, Any]
files: Dict[str, Any] = {}

files_struct_tag_value = span.get_struct_tag(COVERAGE_TAG_NAME)
if files_struct_tag_value is not None and "files" in files_struct_tag_value:
files = files_struct_tag_value["files"]
elif COVERAGE_TAG_NAME in span.get_tags():
files = json.loads(str(span.get_tag(COVERAGE_TAG_NAME)))["files"]

converted_span = {
"test_session_id": int(span.get_tag(SESSION_ID) or "1"),
"test_suite_id": int(span.get_tag(SUITE_ID) or "1"),
"files": json.loads(str(span.get_tag(COVERAGE_TAG_NAME)))["files"],
"files": files,
}

if not self.itr_suite_skipping_mode:
converted_span["span_id"] = span.span_id

log.debug("Span converted to coverage event: %s", converted_span)

return converted_span
Loading
Loading