diff --git a/.riot/requirements/15235b0.txt b/.riot/requirements/12974a3.txt similarity index 50% rename from .riot/requirements/15235b0.txt rename to .riot/requirements/12974a3.txt index 7380eee24f9..db272e2f553 100644 --- a/.riot/requirements/15235b0.txt +++ b/.riot/requirements/12974a3.txt @@ -2,35 +2,38 @@ # This file is autogenerated by pip-compile with Python 3.8 # by the following command: # -# pip-compile --no-annotate --resolver=backtracking .riot/requirements/15235b0.in +# pip-compile --no-annotate .riot/requirements/12974a3.in # attrs==23.2.0 -certifi==2023.11.17 +certifi==2024.7.4 charset-normalizer==3.3.2 click==7.1.2 -coverage[toml]==7.4.0 -exceptiongroup==1.2.0 +coverage[toml]==7.6.0 +deprecated==1.2.14 +exceptiongroup==1.2.2 flask==1.1.4 -gunicorn==21.2.0 +gunicorn==22.0.0 httpretty==1.0.5 hypothesis==6.45.0 -idna==3.6 -importlib-metadata==7.0.1 +idna==3.7 +importlib-metadata==7.0.0 iniconfig==2.0.0 itsdangerous==1.1.0 jinja2==2.11.3 markupsafe==1.1.1 mock==5.1.0 +opentelemetry-api==1.24.0 opentracing==2.4.0 -packaging==23.2 -pluggy==1.3.0 -pytest==7.4.4 -pytest-cov==4.1.0 -pytest-mock==3.12.0 +packaging==24.1 +pluggy==1.5.0 +pytest==8.3.2 +pytest-cov==5.0.0 +pytest-mock==3.14.0 pytest-randomly==3.15.0 -requests==2.31.0 +requests==2.32.3 sortedcontainers==2.4.0 tomli==2.0.1 -urllib3==2.1.0 +urllib3==2.2.2 werkzeug==1.0.1 -zipp==3.17.0 +wrapt==1.16.0 +zipp==3.19.2 diff --git a/.riot/requirements/1153ad9.txt b/.riot/requirements/1677649.txt similarity index 51% rename from .riot/requirements/1153ad9.txt rename to .riot/requirements/1677649.txt index 3816405de28..e4862279695 100644 --- a/.riot/requirements/1153ad9.txt +++ b/.riot/requirements/1677649.txt @@ -2,31 +2,36 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --no-annotate --resolver=backtracking .riot/requirements/1153ad9.in +# pip-compile --no-annotate .riot/requirements/1677649.in # attrs==23.2.0 -certifi==2023.11.17 +certifi==2024.7.4 charset-normalizer==3.3.2 click==7.1.2 -coverage[toml]==7.4.0 +coverage[toml]==7.6.0 +deprecated==1.2.14 flask==1.1.4 -gunicorn==21.2.0 +gunicorn==22.0.0 httpretty==1.0.5 hypothesis==6.45.0 -idna==3.6 +idna==3.7 +importlib-metadata==7.0.0 iniconfig==2.0.0 itsdangerous==1.1.0 jinja2==2.11.3 markupsafe==1.1.1 mock==5.1.0 +opentelemetry-api==1.24.0 opentracing==2.4.0 -packaging==23.2 -pluggy==1.3.0 -pytest==7.4.4 -pytest-cov==4.1.0 -pytest-mock==3.12.0 +packaging==24.1 +pluggy==1.5.0 +pytest==8.3.2 +pytest-cov==5.0.0 +pytest-mock==3.14.0 pytest-randomly==3.15.0 -requests==2.31.0 +requests==2.32.3 sortedcontainers==2.4.0 -urllib3==2.1.0 +urllib3==2.2.2 werkzeug==1.0.1 +wrapt==1.16.0 +zipp==3.19.2 diff --git a/.riot/requirements/427c22a.txt b/.riot/requirements/17c4377.txt similarity index 68% rename from .riot/requirements/427c22a.txt rename to .riot/requirements/17c4377.txt index 10859c1323e..7ad55b82d7b 100644 --- a/.riot/requirements/427c22a.txt +++ b/.riot/requirements/17c4377.txt @@ -2,27 +2,29 @@ # This file is autogenerated by pip-compile with Python 3.7 # by the following command: # -# pip-compile --no-annotate --resolver=backtracking .riot/requirements/427c22a.in +# pip-compile --config=pyproject.toml --no-annotate --resolver=backtracking .riot/requirements/17c4377.in # attrs==23.2.0 -certifi==2023.11.17 +certifi==2024.7.4 charset-normalizer==3.3.2 click==7.1.2 coverage[toml]==7.2.7 -exceptiongroup==1.2.0 +deprecated==1.2.14 +exceptiongroup==1.2.2 flask==1.1.4 -gunicorn==21.2.0 +gunicorn==22.0.0 httpretty==1.0.5 hypothesis==6.45.0 -idna==3.6 +idna==3.7 importlib-metadata==6.7.0 iniconfig==2.0.0 itsdangerous==1.1.0 jinja2==2.11.3 markupsafe==1.1.1 mock==5.1.0 +opentelemetry-api==1.22.0 opentracing==2.4.0 -packaging==23.2 +packaging==24.0 pluggy==1.2.0 pytest==7.4.4 pytest-cov==4.1.0 @@ -34,4 +36,5 @@ tomli==2.0.1 typing-extensions==4.7.1 urllib3==2.0.7 werkzeug==1.0.1 +wrapt==1.16.0 zipp==3.15.0 diff --git a/.riot/requirements/135aac0.txt b/.riot/requirements/18589ec.txt similarity index 50% rename from .riot/requirements/135aac0.txt rename to .riot/requirements/18589ec.txt index 34447792a37..b57924b80f4 100644 --- a/.riot/requirements/135aac0.txt +++ b/.riot/requirements/18589ec.txt @@ -2,35 +2,38 @@ # This file is autogenerated by pip-compile with Python 3.9 # by the following command: # -# pip-compile --no-annotate --resolver=backtracking .riot/requirements/135aac0.in +# pip-compile --no-annotate .riot/requirements/18589ec.in # attrs==23.2.0 -certifi==2023.11.17 +certifi==2024.7.4 charset-normalizer==3.3.2 click==7.1.2 -coverage[toml]==7.4.0 -exceptiongroup==1.2.0 +coverage[toml]==7.6.0 +deprecated==1.2.14 +exceptiongroup==1.2.2 flask==1.1.4 -gunicorn==21.2.0 +gunicorn==22.0.0 httpretty==1.0.5 hypothesis==6.45.0 -idna==3.6 -importlib-metadata==7.0.1 +idna==3.7 +importlib-metadata==7.0.0 iniconfig==2.0.0 itsdangerous==1.1.0 jinja2==2.11.3 markupsafe==1.1.1 mock==5.1.0 +opentelemetry-api==1.24.0 opentracing==2.4.0 -packaging==23.2 -pluggy==1.3.0 -pytest==7.4.4 -pytest-cov==4.1.0 -pytest-mock==3.12.0 +packaging==24.1 +pluggy==1.5.0 +pytest==8.3.2 +pytest-cov==5.0.0 +pytest-mock==3.14.0 pytest-randomly==3.15.0 -requests==2.31.0 +requests==2.32.3 sortedcontainers==2.4.0 tomli==2.0.1 -urllib3==2.1.0 +urllib3==2.2.2 werkzeug==1.0.1 -zipp==3.17.0 +wrapt==1.16.0 +zipp==3.19.2 diff --git a/.riot/requirements/118cb50.txt b/.riot/requirements/1a14242.txt similarity index 50% rename from .riot/requirements/118cb50.txt rename to .riot/requirements/1a14242.txt index ecb387eea6c..024e32a631f 100644 --- a/.riot/requirements/118cb50.txt +++ b/.riot/requirements/1a14242.txt @@ -2,33 +2,38 @@ # This file is autogenerated by pip-compile with Python 3.10 # by the following command: # -# pip-compile --no-annotate .riot/requirements/118cb50.in +# pip-compile --no-annotate .riot/requirements/1a14242.in # attrs==23.2.0 -certifi==2023.11.17 +certifi==2024.7.4 charset-normalizer==3.3.2 click==7.1.2 -coverage[toml]==7.4.0 -exceptiongroup==1.2.0 +coverage[toml]==7.6.0 +deprecated==1.2.14 +exceptiongroup==1.2.2 flask==1.1.4 -gunicorn==21.2.0 +gunicorn==22.0.0 httpretty==1.0.5 hypothesis==6.45.0 -idna==3.6 +idna==3.7 +importlib-metadata==7.0.0 iniconfig==2.0.0 itsdangerous==1.1.0 jinja2==2.11.3 markupsafe==1.1.1 mock==5.1.0 +opentelemetry-api==1.24.0 opentracing==2.4.0 -packaging==23.2 -pluggy==1.3.0 -pytest==7.4.4 -pytest-cov==4.1.0 -pytest-mock==3.12.0 +packaging==24.1 +pluggy==1.5.0 +pytest==8.3.2 +pytest-cov==5.0.0 +pytest-mock==3.14.0 pytest-randomly==3.15.0 -requests==2.31.0 +requests==2.32.3 sortedcontainers==2.4.0 tomli==2.0.1 -urllib3==2.1.0 +urllib3==2.2.2 werkzeug==1.0.1 +wrapt==1.16.0 +zipp==3.19.2 diff --git a/.riot/requirements/17a929f.txt b/.riot/requirements/5c0475c.txt similarity index 51% rename from .riot/requirements/17a929f.txt rename to .riot/requirements/5c0475c.txt index d37acc78410..e57d30b0c6c 100644 --- a/.riot/requirements/17a929f.txt +++ b/.riot/requirements/5c0475c.txt @@ -2,31 +2,36 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --no-annotate .riot/requirements/17a929f.in +# pip-compile --no-annotate .riot/requirements/5c0475c.in # attrs==23.2.0 -certifi==2023.11.17 +certifi==2024.7.4 charset-normalizer==3.3.2 click==7.1.2 -coverage[toml]==7.4.0 +coverage[toml]==7.6.0 +deprecated==1.2.14 flask==1.1.4 -gunicorn==21.2.0 +gunicorn==22.0.0 httpretty==1.0.5 hypothesis==6.45.0 -idna==3.6 +idna==3.7 +importlib-metadata==7.0.0 iniconfig==2.0.0 itsdangerous==1.1.0 jinja2==2.11.3 markupsafe==1.1.1 mock==5.1.0 +opentelemetry-api==1.24.0 opentracing==2.4.0 -packaging==23.2 -pluggy==1.3.0 -pytest==7.4.4 -pytest-cov==4.1.0 -pytest-mock==3.12.0 +packaging==24.1 +pluggy==1.5.0 +pytest==8.3.2 +pytest-cov==5.0.0 +pytest-mock==3.14.0 pytest-randomly==3.15.0 -requests==2.31.0 +requests==2.32.3 sortedcontainers==2.4.0 -urllib3==2.1.0 +urllib3==2.2.2 werkzeug==1.0.1 +wrapt==1.16.0 +zipp==3.19.2 diff --git a/ddtrace/contrib/pytest/plugin.py b/ddtrace/contrib/pytest/plugin.py index 48b710ce6bd..820c2d94965 100644 --- a/ddtrace/contrib/pytest/plugin.py +++ b/ddtrace/contrib/pytest/plugin.py @@ -37,7 +37,6 @@ from ddtrace.contrib.unittest import unpatch as unpatch_unittest from ddtrace.ext import SpanTypes from ddtrace.ext import test -from ddtrace.ext.git import extract_workspace_path from ddtrace.internal.ci_visibility import CIVisibility as _CIVisibility from ddtrace.internal.ci_visibility.constants import EVENT_TYPE as _EVENT_TYPE from ddtrace.internal.ci_visibility.constants import ITR_CORRELATION_ID_TAG_NAME @@ -462,11 +461,8 @@ def pytest_sessionstart(session): log.debug("CI Visibility enabled - starting test session") global _global_skipped_elements _global_skipped_elements = 0 - try: - workspace_path = extract_workspace_path() - except ValueError: - log.debug("Couldn't extract workspace path from git, reverting to config rootdir") - workspace_path = session.config.rootdir + + workspace_path = _CIVisibility.get_workspace_path() or session.config.rootdir session._dd_workspace_path = workspace_path diff --git a/ddtrace/ext/git.py b/ddtrace/ext/git.py index 277d510e7a7..bf1a1323ab6 100644 --- a/ddtrace/ext/git.py +++ b/ddtrace/ext/git.py @@ -6,6 +6,7 @@ import os import random import re +from shutil import which import subprocess from typing import Dict # noqa:F401 from typing import Generator # noqa:F401 @@ -18,6 +19,7 @@ from ddtrace.internal import compat from ddtrace.internal.logger import get_logger +from ddtrace.internal.utils.cache import cached from ddtrace.internal.utils.time import StopWatch @@ -80,6 +82,16 @@ def is_ref_a_tag(ref): return "tags/" in ref if ref else False +@cached() +def _get_executable_path(executable_name: str) -> Optional[str]: + """Return the path to an executable. + + NOTE: cached() requires an argument which is why executable_name is passed in, even though it's really only ever + used to find the git executable at this point. + """ + return which(executable_name, mode=os.X_OK) + + def _git_subprocess_cmd_with_details(*cmd, cwd=None, std_in=None): # type: (str, Optional[str], Optional[bytes]) -> _GitSubprocessDetails """Helper for invoking the git CLI binary @@ -90,7 +102,10 @@ def _git_subprocess_cmd_with_details(*cmd, cwd=None, std_in=None): - the time it took to execute the command, in milliseconds - the exit code """ - git_cmd = ["git"] + git_cmd = _get_executable_path("git") + if git_cmd is None: + raise FileNotFoundError("Git executable not found") + git_cmd = [git_cmd] git_cmd.extend(cmd) log.debug("Executing git command: %s", git_cmd) diff --git a/ddtrace/internal/ci_visibility/recorder.py b/ddtrace/internal/ci_visibility/recorder.py index 53e6a5a98f2..75411ee092a 100644 --- a/ddtrace/internal/ci_visibility/recorder.py +++ b/ddtrace/internal/ci_visibility/recorder.py @@ -750,6 +750,17 @@ def get_service(cls) -> Optional[str]: return None return instance._service + @classmethod + def get_workspace_path(cls) -> Optional[str]: + if not cls.enabled: + error_msg = "CI Visibility is not enabled" + log.warning(error_msg) + raise CIVisibilityError(error_msg) + instance = cls.get_instance() + if instance is None: + return None + return instance._tags.get(ci.WORKSPACE_PATH) + def _requires_civisibility_enabled(func): def wrapper(*args, **kwargs): diff --git a/releasenotes/notes/ci_visibility-fix-dont_make_pytest_contrib_depend_on_git-d922e9326c981f38.yaml b/releasenotes/notes/ci_visibility-fix-dont_make_pytest_contrib_depend_on_git-d922e9326c981f38.yaml new file mode 100644 index 00000000000..3594a995298 --- /dev/null +++ b/releasenotes/notes/ci_visibility-fix-dont_make_pytest_contrib_depend_on_git-d922e9326c981f38.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + CI Visibility: Fixes an issue where the pytest plugin would crash if the git binary was absent diff --git a/releasenotes/notes/profiling-add-lock-with-f75908e35a70ab71.yaml b/releasenotes/notes/profiling-add-lock-with-f75908e35a70ab71.yaml index 422be5a041c..3969cd6f88c 100644 --- a/releasenotes/notes/profiling-add-lock-with-f75908e35a70ab71.yaml +++ b/releasenotes/notes/profiling-add-lock-with-f75908e35a70ab71.yaml @@ -1,3 +1,3 @@ -features: +fixes: - | profiling: captures lock usages with ``with`` context managers, e.g. ``with lock:`` diff --git a/riotfile.py b/riotfile.py index ce8bef35caa..dc355573e20 100644 --- a/riotfile.py +++ b/riotfile.py @@ -320,6 +320,8 @@ def select_pys(min_version=MIN_PYTHON_VERSION, max_version=MAX_PYTHON_VERSION): "flask": "<=2.2.3", "httpretty": "<1.1", "werkzeug": "<2.0", + # FIXME: ddtrace does not support the latest versions of opentelemetry-api + "opentelemetry-api": "<1.25.0", "pytest-randomly": latest, "markupsafe": "<2.0", }, diff --git a/tests/contrib/pytest/test_pytest.py b/tests/contrib/pytest/test_pytest.py index c653beececb..6c56b622377 100644 --- a/tests/contrib/pytest/test_pytest.py +++ b/tests/contrib/pytest/test_pytest.py @@ -3774,3 +3774,59 @@ def test_grand_slam(): } assert expected_source_info == test_names_to_source_info + + def test_pytest_without_git_does_not_crash(self): + with open("tools.py", "w+") as fd: + fd.write( + textwrap.dedent( + ( + """ + def add_two_number_list(list_1, list_2): + output_list = [] + for number_a, number_b in zip(list_1, list_2): + output_list.append(number_a + number_b) + return output_list + + def multiply_two_number_list(list_1, list_2): + output_list = [] + for number_a, number_b in zip(list_1, list_2): + output_list.append(number_a * number_b) + return output_list + """ + ) + ) + ) + + with open("test_tools.py", "w+") as fd: + fd.write( + textwrap.dedent( + ( + """ + from tools import add_two_number_list + + def test_add_two_number_list(): + a_list = [1,2,3,4,5,6,7,8] + b_list = [2,3,4,5,6,7,8,9] + actual_output = add_two_number_list(a_list, b_list) + + assert actual_output == [3,5,7,9,11,13,15,17] + """ + ) + ) + ) + + self.testdir.chdir() + with mock.patch("ddtrace.ext.git._get_executable_path", return_value=None): + self.inline_run("--ddtrace") + + spans = self.pop_spans() + assert len(spans) == 4 + test_span = spans[0] + test_session_span = spans[1] + test_module_span = spans[2] + test_suite_span = spans[3] + + assert test_session_span.get_metric("test.code_coverage.lines_pct") is None + assert test_module_span.get_metric("test.code_coverage.lines_pct") is None + assert test_suite_span.get_metric("test.code_coverage.lines_pct") is None + assert test_span.get_metric("test.code_coverage.lines_pct") is None diff --git a/tests/internal/test_packages.py b/tests/internal/test_packages.py index 4f689d69c06..bd00c70f333 100644 --- a/tests/internal/test_packages.py +++ b/tests/internal/test_packages.py @@ -55,6 +55,10 @@ def test_get_distributions(): importlib_pkgs.add("importlib-metadata") elif pkg.name == "importlib-metadata" and "importlib_metadata" in pkg_resources_ws: importlib_pkgs.add("importlib_metadata") + elif pkg.name == "importlib-resources" and "importlib_resources" in pkg_resources_ws: + importlib_pkgs.add("importlib_resources") + elif pkg.name == "importlib_resources" and "importlib-resources" in pkg_resources_ws: + importlib_pkgs.add("importlib-resources") else: importlib_pkgs.add(pkg.name)