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): introduce experimental internal coverage collector #8727

Merged
merged 51 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
5655116
wip: coverage
P403n1x87 Mar 11, 2024
b69f439
getting serious now
P403n1x87 Mar 12, 2024
cd2d32e
optimise transformation
P403n1x87 Mar 12, 2024
b956569
ad-hoc instrumentation logic
P403n1x87 Mar 12, 2024
312fdfa
clean-up
P403n1x87 Mar 12, 2024
60a91d1
fix: get code objects by topological order
P403n1x87 Mar 13, 2024
e16818e
skip None line numbers
P403n1x87 Mar 13, 2024
3a18d1f
make ModuleCodeCollector work with pytest plugin
romainkomorndatadog Mar 13, 2024
09f6914
add whole session
romainkomorndatadog Mar 13, 2024
bfb9ada
use both context and instance counters
romainkomorndatadog Mar 14, 2024
de50073
counters and reports stuff
romainkomorndatadog Mar 14, 2024
1a066eb
accept all paths from cwd, don't reinstall the module if already inst…
romainkomorndatadog Mar 15, 2024
a05b91e
drop counters
romainkomorndatadog Mar 15, 2024
61345e7
add comments warning of experimental use
romainkomorndatadog Mar 18, 2024
81d7ad9
Merge branch 'main' into romainkomorn/add_clear
romainkomorndatadog Mar 20, 2024
ce78dd7
handle 'pragma: no cover' style comments
romainkomorndatadog Mar 21, 2024
ea75f39
fix regex and check if command ends with ':'
romainkomorndatadog Mar 21, 2024
54d3f70
AST-based nocover attempt
romainkomorndatadog Mar 21, 2024
64289a7
don't delete every single line...
romainkomorndatadog Mar 21, 2024
a51a03d
cache ast
romainkomorndatadog Mar 21, 2024
83689d5
Update riotfile.py
romainkomorndatadog Mar 21, 2024
94dbdeb
Merge branch 'main' into romainkomorn/add_clear
romainkomorndatadog Mar 22, 2024
3211f0b
Apply suggestions from code review
romainkomorndatadog Mar 25, 2024
7d769a9
move reporting functions to own module
romainkomorndatadog Mar 25, 2024
988f6cc
use _get_covered_lines for seen lines
romainkomorndatadog Mar 25, 2024
05b2686
remove unused (un)register_run_module_*
romainkomorndatadog Mar 28, 2024
e0185d1
Merge branch 'main' into romainkomorn/add_clear
romainkomorndatadog Apr 2, 2024
08cc22c
Merge branch 'main' into romainkomorn/add_clear
romainkomorndatadog Apr 5, 2024
5556f16
Merge branch 'main' into romainkomorn/add_clear
romainkomorndatadog Apr 5, 2024
40b3d24
Merge branch 'main' into romainkomorn/add_clear
romainkomorndatadog Apr 15, 2024
83cfd22
update pytest 3.x deps
romainkomorndatadog Apr 15, 2024
e2afae8
Merge branch 'main' into romainkomorn/add_clear
romainkomorndatadog Apr 17, 2024
de1a57f
Merge branch 'main' into romainkomorn/add_clear
romainkomorndatadog Apr 17, 2024
eff0cf4
Merge branch 'main' into romainkomorn/add_clear
romainkomorndatadog Apr 18, 2024
94e39be
Merge branch 'main' into romainkomorn/add_clear
romainkomorndatadog Apr 18, 2024
47e3732
prep for merge
romainkomorndatadog May 15, 2024
145bde4
Merge branch 'main' into romainkomorn/add_clear
romainkomorndatadog May 15, 2024
055fd27
update for versions plugin layout
romainkomorndatadog May 15, 2024
47373ca
undo utils init change
romainkomorndatadog May 15, 2024
ddf7c22
Add coverage report writer and comparer
romainkomorndatadog May 16, 2024
d8e59b7
small report tweaks
romainkomorndatadog May 16, 2024
d2a4abb
Merge branch 'main' into romainkomorn/add_clear
romainkomorndatadog May 20, 2024
0a99c82
Merge branch 'main' into romainkomorn/add_clear
romainkomorndatadog May 20, 2024
ecaa52f
Merge branch 'main' into romainkomorn/add_clear
romainkomorndatadog May 20, 2024
ec98f43
Merge branch 'main' into romainkomorn/add_clear
romainkomorndatadog May 21, 2024
87c6758
Merge branch 'main' into romainkomorn/add_clear
romainkomorndatadog May 21, 2024
43e7b02
Merge branch 'romainkomorn/add_clear' of github.com:DataDog/dd-trace-…
romainkomorndatadog May 21, 2024
cb00126
Merge branch 'main' into romainkomorn/add_clear
romainkomorndatadog May 22, 2024
e7686e9
scripts/compile-and-prune-test-requirements
romainkomorndatadog May 22, 2024
d5fcbec
Merge branch 'romainkomorn/add_clear' of github.com:DataDog/dd-trace-…
romainkomorndatadog May 22, 2024
8a68963
Merge branch 'main' into romainkomorn/add_clear
romainkomorndatadog May 22, 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
29 changes: 29 additions & 0 deletions ddtrace/contrib/pytest/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"""
from doctest import DocTest
import json
import os
import re
from typing import Dict # noqa:F401

Expand Down Expand Up @@ -49,6 +50,7 @@
from ddtrace.internal.ci_visibility.constants import SUITE_ID as _SUITE_ID
from ddtrace.internal.ci_visibility.constants import SUITE_TYPE as _SUITE_TYPE
from ddtrace.internal.ci_visibility.constants import TEST
from ddtrace.internal.ci_visibility.coverage import USE_DD_COVERAGE
from ddtrace.internal.ci_visibility.coverage import _module_has_dd_coverage_enabled
from ddtrace.internal.ci_visibility.coverage import _report_coverage_to_span
from ddtrace.internal.ci_visibility.coverage import _start_coverage
Expand All @@ -61,7 +63,9 @@
from ddtrace.internal.ci_visibility.utils import get_relative_or_absolute_path_for_path
from ddtrace.internal.ci_visibility.utils import take_over_logger_stream_handler
from ddtrace.internal.constants import COMPONENT
from ddtrace.internal.coverage.code import ModuleCodeCollector
from ddtrace.internal.logger import get_logger
from ddtrace.internal.utils.formats import asbool


PATCH_ALL_HELP_MSG = "Call ddtrace.patch_all before running tests."
Expand All @@ -70,6 +74,10 @@

_global_skipped_elements = 0

# COVER_SESSION is an experimental feature flag that provides full coverage (similar to coverage run), and is an
# experimental feature. It currently significantly increases test import time and should not be used.
COVER_SESSION = asbool(os.environ.get("_DD_COVER_SESSION", "false"))


def _is_pytest_8_or_later():
if hasattr(pytest, "version_tuple"):
Expand Down Expand Up @@ -821,6 +829,20 @@ def pytest_runtest_protocol(item, nextitem):
_stop_coverage(pytest)


def pytest_load_initial_conftests(early_config, parser, args):
# Enables experimental use of ModuleCodeCollector for coverage collection.
if USE_DD_COVERAGE:
if not ModuleCodeCollector.is_installed():
ModuleCodeCollector.install()
if COVER_SESSION:
ModuleCodeCollector.start_coverage()
else:
if COVER_SESSION:
log.warning(
"_DD_COVER_SESSION must be used with _DD_USE_INTERNAL_COVERAGE but not DD_CIVISIBILITY_ITR_ENABLED"
)


@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""Store outcome for tracing."""
Expand Down Expand Up @@ -936,3 +958,10 @@ def pytest_ddtrace_get_item_test_name(item):
if item.config.getoption("ddtrace-include-class-name") or item.config.getini("ddtrace-include-class-name"):
return "%s.%s" % (item.cls.__name__, item.name)
return item.name


@pytest.hookimpl(trylast=True)
def pytest_terminal_summary(terminalreporter, exitstatus, config):
# Reports coverage if experimental session-level coverage is enabled.
if USE_DD_COVERAGE and COVER_SESSION:
ModuleCodeCollector.report()
48 changes: 46 additions & 2 deletions ddtrace/internal/ci_visibility/coverage.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
from itertools import groupby
import json
import os
from typing import Dict # noqa:F401
from typing import Iterable # noqa:F401
from typing import List # noqa:F401
from typing import Optional # noqa:F401
from typing import Tuple # noqa:F401
from typing import Union # noqa:F401

import ddtrace
from ddtrace.internal.ci_visibility.constants import COVERAGE_TAG_NAME
from ddtrace.internal.ci_visibility.utils import get_relative_or_absolute_path_for_path
from ddtrace.internal.coverage.code import ModuleCodeCollector
from ddtrace.internal.logger import get_logger
from ddtrace.internal.utils.formats import asbool


log = get_logger(__name__)
_global_relative_file_paths_for_cov: Dict[str, Dict[str, str]] = {}

# This feature-flags experimental collection of code coverage via our internal ModuleCodeCollector.
# It is disabled by default because it is not production-ready.
USE_DD_COVERAGE = asbool(os.environ.get("_DD_USE_INTERNAL_COVERAGE", "false"))
P403n1x87 marked this conversation as resolved.
Show resolved Hide resolved

try:
from coverage import Coverage
from coverage import version_info as coverage_version
Expand Down Expand Up @@ -45,19 +53,30 @@ def _initialize_coverage(root_dir):


def _start_coverage(root_dir: str):
# Experimental feature to use internal coverage collection
if USE_DD_COVERAGE:
ctx = ModuleCodeCollector.CollectInContext()
return ctx
coverage = _initialize_coverage(root_dir)
coverage.start()
return coverage


def _stop_coverage(module):
# Experimental feature to use internal coverage collection
if USE_DD_COVERAGE:
module._dd_coverage.__exit__()
return
if _module_has_dd_coverage_enabled(module):
module._dd_coverage.stop()
module._dd_coverage.erase()
del module._dd_coverage


def _module_has_dd_coverage_enabled(module, silent_mode: bool = False) -> bool:
# Experimental feature to use internal coverage collection
if USE_DD_COVERAGE:
return hasattr(module, "_dd_coverage")
if not hasattr(module, "_dd_coverage"):
if not silent_mode:
log.warning("Datadog Coverage has not been initiated")
Expand All @@ -73,7 +92,15 @@ def _coverage_has_valid_data(coverage_data: Coverage, silent_mode: bool = False)
return True


def _switch_coverage_context(coverage_data: Coverage, unique_test_name: str):
def _switch_coverage_context(
coverage_data: Union[Coverage, ModuleCodeCollector.CollectInContext], unique_test_name: str
):
# Experimental feature to use internal coverage collection
if isinstance(coverage_data, ModuleCodeCollector.CollectInContext):
if USE_DD_COVERAGE:
# In this case, coverage_data is the context manager supplied by ModuleCodeCollector.CollectInContext
coverage_data.__enter__()
return
if not _coverage_has_valid_data(coverage_data, silent_mode=True):
return
coverage_data._collector.data.clear() # type: ignore[union-attr]
Expand All @@ -83,7 +110,24 @@ def _switch_coverage_context(coverage_data: Coverage, unique_test_name: str):
log.warning(err)


def _report_coverage_to_span(coverage_data: Coverage, span: ddtrace.Span, root_dir: str):
def _report_coverage_to_span(
coverage_data: Union[Coverage, ModuleCodeCollector.CollectInContext], span: ddtrace.Span, root_dir: str
):
# Experimental feature to use internal coverage collection
if isinstance(coverage_data, ModuleCodeCollector.CollectInContext):
if USE_DD_COVERAGE:
# In this case, coverage_data is the context manager supplied by ModuleCodeCollector.CollectInContext
files = ModuleCodeCollector.report_seen_lines()
if not files:
return
span.set_tag_str(
COVERAGE_TAG_NAME,
json.dumps({"files": files}),
)
coverage_data.__exit__(None, None, None)

return

span_id = str(span.trace_id)
if not _coverage_has_valid_data(coverage_data):
return
Expand Down
Empty file.
61 changes: 61 additions & 0 deletions ddtrace/internal/coverage/_native.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>

#if PY_VERSION_HEX < 0x030c0000
#if defined __GNUC__ && defined HAVE_STD_ATOMIC
#undef HAVE_STD_ATOMIC
#endif
#endif

// ----------------------------------------------------------------------------
static PyObject*
replace_in_tuple(PyObject* m, PyObject* args)
{
PyObject* tuple = NULL;
PyObject* item = NULL;
PyObject* replacement = NULL;

if (!PyArg_ParseTuple(args, "O!OO", &PyTuple_Type, &tuple, &item, &replacement))
return NULL;

for (Py_ssize_t i = 0; i < PyTuple_Size(tuple); i++) {
PyObject* current = PyTuple_GetItem(tuple, i);
if (current == item) {
Py_DECREF(current);
// !!! DANGER !!!
PyTuple_SET_ITEM(tuple, i, replacement);
Py_INCREF(replacement);
}
}

Py_RETURN_NONE;
}

// ----------------------------------------------------------------------------
static PyMethodDef native_methods[] = {
{ "replace_in_tuple", replace_in_tuple, METH_VARARGS, "Replace an item in a tuple." },
{ NULL, NULL, 0, NULL } /* Sentinel */
};

// ----------------------------------------------------------------------------
static struct PyModuleDef nativemodule = {
PyModuleDef_HEAD_INIT,
"_native", /* name of module */
NULL, /* module documentation, may be NULL */
-1, /* size of per-interpreter state of the module,
or -1 if the module keeps state in global variables. */
native_methods,
};

// ----------------------------------------------------------------------------
PyMODINIT_FUNC
PyInit__native(void)
{
PyObject* m;

m = PyModule_Create(&nativemodule);
if (m == NULL)
return NULL;

return m;
}
3 changes: 3 additions & 0 deletions ddtrace/internal/coverage/_native.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import typing as t

def replace_in_tuple(tup: tuple, item: t.Any, replacement: t.Any) -> None: ...
Loading
Loading