From 0e0848f3e6c21055e612d68acd30a0c755c45b43 Mon Sep 17 00:00:00 2001 From: Munir Abdinur Date: Fri, 17 Jan 2025 13:19:00 -0500 Subject: [PATCH 1/3] chore(civis): move unittest and pytest integrations to internal [3.0] (#11978) - Moves unittest and pytest integrations from ddtrace.contrib to ddtrace.contrib.internal - Ensures a deprecation warning is logged if `ddtrace.contrib.` is used ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting) --- ddtrace/contrib/internal/pytest/__init__.py | 0 .../{ => internal}/pytest/_atr_utils.py | 18 +- .../{ => internal}/pytest/_benchmark_utils.py | 4 +- .../{ => internal}/pytest/_efd_utils.py | 18 +- .../{ => internal}/pytest/_plugin_v1.py | 18 +- .../{ => internal}/pytest/_plugin_v2.py | 76 +- .../pytest/_pytest_bdd_subplugin.py | 14 +- .../{ => internal}/pytest/_retry_utils.py | 4 +- .../contrib/{ => internal}/pytest/_types.py | 2 +- .../contrib/{ => internal}/pytest/_utils.py | 8 +- ddtrace/contrib/internal/pytest/constants.py | 11 + ddtrace/contrib/internal/pytest/newhooks.py | 26 + ddtrace/contrib/internal/pytest/patch.py | 6 + ddtrace/contrib/internal/pytest/plugin.py | 178 ++++ .../contrib/internal/pytest_bdd/__init__.py | 0 .../{ => internal}/pytest_bdd/_plugin.py | 8 +- .../contrib/internal/pytest_bdd/constants.py | 2 + ddtrace/contrib/internal/pytest_bdd/patch.py | 9 + ddtrace/contrib/internal/pytest_bdd/plugin.py | 30 + .../internal/pytest_benchmark/__init__.py | 0 .../pytest_benchmark/_plugin.py | 8 +- .../internal/pytest_benchmark/constants.py | 79 ++ .../internal/pytest_benchmark/plugin.py | 19 + ddtrace/contrib/internal/unittest/__init__.py | 0 .../contrib/internal/unittest/constants.py | 8 + ddtrace/contrib/internal/unittest/patch.py | 868 +++++++++++++++++ ddtrace/contrib/pytest/__init__.py | 30 +- ddtrace/contrib/pytest/constants.py | 19 +- ddtrace/contrib/pytest/newhooks.py | 34 +- ddtrace/contrib/pytest/plugin.py | 172 +--- ddtrace/contrib/pytest_bdd/__init__.py | 27 +- ddtrace/contrib/pytest_bdd/constants.py | 16 +- ddtrace/contrib/pytest_bdd/plugin.py | 26 +- ddtrace/contrib/pytest_benchmark/__init__.py | 11 + ddtrace/contrib/pytest_benchmark/constants.py | 87 +- ddtrace/contrib/pytest_benchmark/plugin.py | 25 +- ddtrace/contrib/unittest/__init__.py | 17 +- ddtrace/contrib/unittest/constants.py | 20 +- ddtrace/contrib/unittest/patch.py | 874 +----------------- ddtrace/internal/ci_visibility/api/_test.py | 2 +- pyproject.toml | 6 +- ...ci-vis-ints-internal-532bc22d19bb62ab.yaml | 5 + tests/contrib/asynctest/test_asynctest.py | 2 +- .../contrib/pytest/test_coverage_per_suite.py | 4 +- tests/contrib/pytest/test_pytest.py | 8 +- tests/contrib/pytest/test_pytest_atr.py | 4 +- tests/contrib/pytest/test_pytest_efd.py | 4 +- .../contrib/pytest/test_pytest_quarantine.py | 4 +- tests/contrib/pytest/test_pytest_snapshot.py | 2 +- .../contrib/pytest/test_pytest_snapshot_v2.py | 2 +- tests/contrib/pytest_bdd/test_pytest_bdd.py | 13 +- .../pytest_benchmark/test_pytest_benchmark.py | 38 +- tests/contrib/unittest/test_unittest.py | 18 +- tests/contrib/unittest/test_unittest_patch.py | 6 +- tests/internal/test_module.py | 11 - 55 files changed, 1529 insertions(+), 1372 deletions(-) create mode 100644 ddtrace/contrib/internal/pytest/__init__.py rename ddtrace/contrib/{ => internal}/pytest/_atr_utils.py (94%) rename ddtrace/contrib/{ => internal}/pytest/_benchmark_utils.py (87%) rename ddtrace/contrib/{ => internal}/pytest/_efd_utils.py (94%) rename ddtrace/contrib/{ => internal}/pytest/_plugin_v1.py (98%) rename ddtrace/contrib/{ => internal}/pytest/_plugin_v2.py (90%) rename ddtrace/contrib/{ => internal}/pytest/_pytest_bdd_subplugin.py (88%) rename ddtrace/contrib/{ => internal}/pytest/_retry_utils.py (97%) rename ddtrace/contrib/{ => internal}/pytest/_types.py (90%) rename ddtrace/contrib/{ => internal}/pytest/_utils.py (95%) create mode 100644 ddtrace/contrib/internal/pytest/constants.py create mode 100644 ddtrace/contrib/internal/pytest/newhooks.py create mode 100644 ddtrace/contrib/internal/pytest/patch.py create mode 100644 ddtrace/contrib/internal/pytest/plugin.py create mode 100644 ddtrace/contrib/internal/pytest_bdd/__init__.py rename ddtrace/contrib/{ => internal}/pytest_bdd/_plugin.py (94%) create mode 100644 ddtrace/contrib/internal/pytest_bdd/constants.py create mode 100644 ddtrace/contrib/internal/pytest_bdd/patch.py create mode 100644 ddtrace/contrib/internal/pytest_bdd/plugin.py create mode 100644 ddtrace/contrib/internal/pytest_benchmark/__init__.py rename ddtrace/contrib/{ => internal}/pytest_benchmark/_plugin.py (73%) create mode 100644 ddtrace/contrib/internal/pytest_benchmark/constants.py create mode 100644 ddtrace/contrib/internal/pytest_benchmark/plugin.py create mode 100644 ddtrace/contrib/internal/unittest/__init__.py create mode 100644 ddtrace/contrib/internal/unittest/constants.py create mode 100644 ddtrace/contrib/internal/unittest/patch.py create mode 100644 releasenotes/notes/make-ci-vis-ints-internal-532bc22d19bb62ab.yaml diff --git a/ddtrace/contrib/internal/pytest/__init__.py b/ddtrace/contrib/internal/pytest/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ddtrace/contrib/pytest/_atr_utils.py b/ddtrace/contrib/internal/pytest/_atr_utils.py similarity index 94% rename from ddtrace/contrib/pytest/_atr_utils.py rename to ddtrace/contrib/internal/pytest/_atr_utils.py index 0d684486602..82d65c284b0 100644 --- a/ddtrace/contrib/pytest/_atr_utils.py +++ b/ddtrace/contrib/internal/pytest/_atr_utils.py @@ -3,15 +3,15 @@ import _pytest import pytest -from ddtrace.contrib.pytest._retry_utils import RetryOutcomes -from ddtrace.contrib.pytest._retry_utils import _get_outcome_from_retry -from ddtrace.contrib.pytest._retry_utils import _get_retry_attempt_string -from ddtrace.contrib.pytest._retry_utils import set_retry_num -from ddtrace.contrib.pytest._types import _pytest_report_teststatus_return_type -from ddtrace.contrib.pytest._types import pytest_TestReport -from ddtrace.contrib.pytest._utils import PYTEST_STATUS -from ddtrace.contrib.pytest._utils import _get_test_id_from_item -from ddtrace.contrib.pytest._utils import _TestOutcome +from ddtrace.contrib.internal.pytest._retry_utils import RetryOutcomes +from ddtrace.contrib.internal.pytest._retry_utils import _get_outcome_from_retry +from ddtrace.contrib.internal.pytest._retry_utils import _get_retry_attempt_string +from ddtrace.contrib.internal.pytest._retry_utils import set_retry_num +from ddtrace.contrib.internal.pytest._types import _pytest_report_teststatus_return_type +from ddtrace.contrib.internal.pytest._types import pytest_TestReport +from ddtrace.contrib.internal.pytest._utils import PYTEST_STATUS +from ddtrace.contrib.internal.pytest._utils import _get_test_id_from_item +from ddtrace.contrib.internal.pytest._utils import _TestOutcome from ddtrace.ext.test_visibility.api import TestStatus from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId diff --git a/ddtrace/contrib/pytest/_benchmark_utils.py b/ddtrace/contrib/internal/pytest/_benchmark_utils.py similarity index 87% rename from ddtrace/contrib/pytest/_benchmark_utils.py rename to ddtrace/contrib/internal/pytest/_benchmark_utils.py index 77dd6061b13..70a79c60700 100644 --- a/ddtrace/contrib/pytest/_benchmark_utils.py +++ b/ddtrace/contrib/internal/pytest/_benchmark_utils.py @@ -1,7 +1,7 @@ import pytest -from ddtrace.contrib.pytest._utils import _get_test_id_from_item -from ddtrace.contrib.pytest_benchmark.constants import PLUGIN_METRICS_V2 +from ddtrace.contrib.internal.pytest._utils import _get_test_id_from_item +from ddtrace.contrib.internal.pytest_benchmark.constants import PLUGIN_METRICS_V2 from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._benchmark_mixin import BenchmarkDurationData from ddtrace.internal.test_visibility.api import InternalTest diff --git a/ddtrace/contrib/pytest/_efd_utils.py b/ddtrace/contrib/internal/pytest/_efd_utils.py similarity index 94% rename from ddtrace/contrib/pytest/_efd_utils.py rename to ddtrace/contrib/internal/pytest/_efd_utils.py index 1e16934bb11..a64148cd574 100644 --- a/ddtrace/contrib/pytest/_efd_utils.py +++ b/ddtrace/contrib/internal/pytest/_efd_utils.py @@ -3,15 +3,15 @@ import _pytest import pytest -from ddtrace.contrib.pytest._retry_utils import RetryOutcomes -from ddtrace.contrib.pytest._retry_utils import _get_outcome_from_retry -from ddtrace.contrib.pytest._retry_utils import _get_retry_attempt_string -from ddtrace.contrib.pytest._retry_utils import set_retry_num -from ddtrace.contrib.pytest._types import _pytest_report_teststatus_return_type -from ddtrace.contrib.pytest._types import pytest_TestReport -from ddtrace.contrib.pytest._utils import PYTEST_STATUS -from ddtrace.contrib.pytest._utils import _get_test_id_from_item -from ddtrace.contrib.pytest._utils import _TestOutcome +from ddtrace.contrib.internal.pytest._retry_utils import RetryOutcomes +from ddtrace.contrib.internal.pytest._retry_utils import _get_outcome_from_retry +from ddtrace.contrib.internal.pytest._retry_utils import _get_retry_attempt_string +from ddtrace.contrib.internal.pytest._retry_utils import set_retry_num +from ddtrace.contrib.internal.pytest._types import _pytest_report_teststatus_return_type +from ddtrace.contrib.internal.pytest._types import pytest_TestReport +from ddtrace.contrib.internal.pytest._utils import PYTEST_STATUS +from ddtrace.contrib.internal.pytest._utils import _get_test_id_from_item +from ddtrace.contrib.internal.pytest._utils import _TestOutcome from ddtrace.ext.test_visibility.api import TestStatus from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility._efd_mixins import EFDTestStatus diff --git a/ddtrace/contrib/pytest/_plugin_v1.py b/ddtrace/contrib/internal/pytest/_plugin_v1.py similarity index 98% rename from ddtrace/contrib/pytest/_plugin_v1.py rename to ddtrace/contrib/internal/pytest/_plugin_v1.py index e7b7b2caac9..fc4982bdc67 100644 --- a/ddtrace/contrib/pytest/_plugin_v1.py +++ b/ddtrace/contrib/internal/pytest/_plugin_v1.py @@ -30,15 +30,15 @@ from ddtrace.contrib.internal.coverage.patch import unpatch as unpatch_coverage from ddtrace.contrib.internal.coverage.utils import _is_coverage_invoked_by_coverage_run from ddtrace.contrib.internal.coverage.utils import _is_coverage_patched -from ddtrace.contrib.pytest._utils import _extract_span -from ddtrace.contrib.pytest._utils import _is_enabled_early -from ddtrace.contrib.pytest._utils import _is_pytest_8_or_later -from ddtrace.contrib.pytest._utils import _is_test_unskippable -from ddtrace.contrib.pytest.constants import FRAMEWORK -from ddtrace.contrib.pytest.constants import KIND -from ddtrace.contrib.pytest.constants import XFAIL_REASON -from ddtrace.contrib.pytest.plugin import is_enabled -from ddtrace.contrib.unittest import unpatch as unpatch_unittest +from ddtrace.contrib.internal.pytest._utils import _extract_span +from ddtrace.contrib.internal.pytest._utils import _is_enabled_early +from ddtrace.contrib.internal.pytest._utils import _is_pytest_8_or_later +from ddtrace.contrib.internal.pytest._utils import _is_test_unskippable +from ddtrace.contrib.internal.pytest.constants import FRAMEWORK +from ddtrace.contrib.internal.pytest.constants import KIND +from ddtrace.contrib.internal.pytest.constants import XFAIL_REASON +from ddtrace.contrib.internal.pytest.plugin import is_enabled +from ddtrace.contrib.internal.unittest.patch import unpatch as unpatch_unittest from ddtrace.ext import SpanTypes from ddtrace.ext import test from ddtrace.internal.ci_visibility import CIVisibility as _CIVisibility diff --git a/ddtrace/contrib/pytest/_plugin_v2.py b/ddtrace/contrib/internal/pytest/_plugin_v2.py similarity index 90% rename from ddtrace/contrib/pytest/_plugin_v2.py rename to ddtrace/contrib/internal/pytest/_plugin_v2.py index f15373a776a..ece2098e05e 100644 --- a/ddtrace/contrib/pytest/_plugin_v2.py +++ b/ddtrace/contrib/internal/pytest/_plugin_v2.py @@ -6,38 +6,38 @@ from ddtrace import DDTraceDeprecationWarning from ddtrace import config as dd_config -from ddtrace import patch +from ddtrace._monkey import patch from ddtrace.contrib.coverage import patch as patch_coverage from ddtrace.contrib.internal.coverage.constants import PCT_COVERED_KEY from ddtrace.contrib.internal.coverage.data import _coverage_data from ddtrace.contrib.internal.coverage.patch import run_coverage_report from ddtrace.contrib.internal.coverage.utils import _is_coverage_invoked_by_coverage_run from ddtrace.contrib.internal.coverage.utils import _is_coverage_patched -from ddtrace.contrib.pytest._benchmark_utils import _set_benchmark_data_from_item -from ddtrace.contrib.pytest._plugin_v1 import _extract_reason -from ddtrace.contrib.pytest._plugin_v1 import _is_pytest_cov_enabled -from ddtrace.contrib.pytest._types import _pytest_report_teststatus_return_type -from ddtrace.contrib.pytest._types import pytest_CallInfo -from ddtrace.contrib.pytest._types import pytest_Config -from ddtrace.contrib.pytest._types import pytest_TestReport -from ddtrace.contrib.pytest._utils import PYTEST_STATUS -from ddtrace.contrib.pytest._utils import _get_module_path_from_item -from ddtrace.contrib.pytest._utils import _get_names_from_item -from ddtrace.contrib.pytest._utils import _get_session_command -from ddtrace.contrib.pytest._utils import _get_source_file_info -from ddtrace.contrib.pytest._utils import _get_test_id_from_item -from ddtrace.contrib.pytest._utils import _get_test_parameters_json -from ddtrace.contrib.pytest._utils import _is_enabled_early -from ddtrace.contrib.pytest._utils import _is_test_unskippable -from ddtrace.contrib.pytest._utils import _pytest_marked_to_skip -from ddtrace.contrib.pytest._utils import _pytest_version_supports_atr -from ddtrace.contrib.pytest._utils import _pytest_version_supports_efd -from ddtrace.contrib.pytest._utils import _pytest_version_supports_retries -from ddtrace.contrib.pytest._utils import _TestOutcome -from ddtrace.contrib.pytest.constants import FRAMEWORK -from ddtrace.contrib.pytest.constants import XFAIL_REASON -from ddtrace.contrib.pytest.plugin import is_enabled -from ddtrace.contrib.unittest import unpatch as unpatch_unittest +from ddtrace.contrib.internal.pytest._benchmark_utils import _set_benchmark_data_from_item +from ddtrace.contrib.internal.pytest._plugin_v1 import _extract_reason +from ddtrace.contrib.internal.pytest._plugin_v1 import _is_pytest_cov_enabled +from ddtrace.contrib.internal.pytest._types import _pytest_report_teststatus_return_type +from ddtrace.contrib.internal.pytest._types import pytest_CallInfo +from ddtrace.contrib.internal.pytest._types import pytest_Config +from ddtrace.contrib.internal.pytest._types import pytest_TestReport +from ddtrace.contrib.internal.pytest._utils import PYTEST_STATUS +from ddtrace.contrib.internal.pytest._utils import _get_module_path_from_item +from ddtrace.contrib.internal.pytest._utils import _get_names_from_item +from ddtrace.contrib.internal.pytest._utils import _get_session_command +from ddtrace.contrib.internal.pytest._utils import _get_source_file_info +from ddtrace.contrib.internal.pytest._utils import _get_test_id_from_item +from ddtrace.contrib.internal.pytest._utils import _get_test_parameters_json +from ddtrace.contrib.internal.pytest._utils import _is_enabled_early +from ddtrace.contrib.internal.pytest._utils import _is_test_unskippable +from ddtrace.contrib.internal.pytest._utils import _pytest_marked_to_skip +from ddtrace.contrib.internal.pytest._utils import _pytest_version_supports_atr +from ddtrace.contrib.internal.pytest._utils import _pytest_version_supports_efd +from ddtrace.contrib.internal.pytest._utils import _pytest_version_supports_retries +from ddtrace.contrib.internal.pytest._utils import _TestOutcome +from ddtrace.contrib.internal.pytest.constants import FRAMEWORK +from ddtrace.contrib.internal.pytest.constants import XFAIL_REASON +from ddtrace.contrib.internal.pytest.plugin import is_enabled +from ddtrace.contrib.internal.unittest.patch import unpatch as unpatch_unittest from ddtrace.ext import test from ddtrace.ext.test_visibility import ITR_SKIPPING_LEVEL from ddtrace.ext.test_visibility.api import TestExcInfo @@ -63,21 +63,21 @@ if _pytest_version_supports_retries(): - from ddtrace.contrib.pytest._retry_utils import get_retry_num + from ddtrace.contrib.internal.pytest._retry_utils import get_retry_num if _pytest_version_supports_efd(): - from ddtrace.contrib.pytest._efd_utils import efd_get_failed_reports - from ddtrace.contrib.pytest._efd_utils import efd_get_teststatus - from ddtrace.contrib.pytest._efd_utils import efd_handle_retries - from ddtrace.contrib.pytest._efd_utils import efd_pytest_terminal_summary_post_yield + from ddtrace.contrib.internal.pytest._efd_utils import efd_get_failed_reports + from ddtrace.contrib.internal.pytest._efd_utils import efd_get_teststatus + from ddtrace.contrib.internal.pytest._efd_utils import efd_handle_retries + from ddtrace.contrib.internal.pytest._efd_utils import efd_pytest_terminal_summary_post_yield if _pytest_version_supports_atr(): - from ddtrace.contrib.pytest._atr_utils import atr_get_failed_reports - from ddtrace.contrib.pytest._atr_utils import atr_get_teststatus - from ddtrace.contrib.pytest._atr_utils import atr_handle_retries - from ddtrace.contrib.pytest._atr_utils import atr_pytest_terminal_summary_post_yield - from ddtrace.contrib.pytest._atr_utils import quarantine_atr_get_teststatus - from ddtrace.contrib.pytest._atr_utils import quarantine_pytest_terminal_summary_post_yield + from ddtrace.contrib.internal.pytest._atr_utils import atr_get_failed_reports + from ddtrace.contrib.internal.pytest._atr_utils import atr_get_teststatus + from ddtrace.contrib.internal.pytest._atr_utils import atr_handle_retries + from ddtrace.contrib.internal.pytest._atr_utils import atr_pytest_terminal_summary_post_yield + from ddtrace.contrib.internal.pytest._atr_utils import quarantine_atr_get_teststatus + from ddtrace.contrib.internal.pytest._atr_utils import quarantine_pytest_terminal_summary_post_yield log = get_logger(__name__) @@ -217,7 +217,7 @@ def pytest_configure(config: pytest_Config) -> None: # pytest-bdd plugin support if config.pluginmanager.hasplugin("pytest-bdd"): - from ddtrace.contrib.pytest._pytest_bdd_subplugin import _PytestBddSubPlugin + from ddtrace.contrib.internal.pytest._pytest_bdd_subplugin import _PytestBddSubPlugin config.pluginmanager.register(_PytestBddSubPlugin(), "_datadog-pytest-bdd") else: diff --git a/ddtrace/contrib/pytest/_pytest_bdd_subplugin.py b/ddtrace/contrib/internal/pytest/_pytest_bdd_subplugin.py similarity index 88% rename from ddtrace/contrib/pytest/_pytest_bdd_subplugin.py rename to ddtrace/contrib/internal/pytest/_pytest_bdd_subplugin.py index 7c964af3d59..4349f10654e 100644 --- a/ddtrace/contrib/pytest/_pytest_bdd_subplugin.py +++ b/ddtrace/contrib/internal/pytest/_pytest_bdd_subplugin.py @@ -13,13 +13,13 @@ import pytest -from ddtrace.contrib.pytest._utils import _get_test_id_from_item -from ddtrace.contrib.pytest_bdd import get_version -from ddtrace.contrib.pytest_bdd._plugin import _extract_span -from ddtrace.contrib.pytest_bdd._plugin import _get_step_func_args_json -from ddtrace.contrib.pytest_bdd._plugin import _store_span -from ddtrace.contrib.pytest_bdd.constants import FRAMEWORK -from ddtrace.contrib.pytest_bdd.constants import STEP_KIND +from ddtrace.contrib.internal.pytest._utils import _get_test_id_from_item +from ddtrace.contrib.internal.pytest_bdd._plugin import _extract_span +from ddtrace.contrib.internal.pytest_bdd._plugin import _get_step_func_args_json +from ddtrace.contrib.internal.pytest_bdd._plugin import _store_span +from ddtrace.contrib.internal.pytest_bdd.constants import FRAMEWORK +from ddtrace.contrib.internal.pytest_bdd.constants import STEP_KIND +from ddtrace.contrib.internal.pytest_bdd.patch import get_version from ddtrace.ext import test from ddtrace.internal.logger import get_logger from ddtrace.internal.test_visibility.api import InternalTest diff --git a/ddtrace/contrib/pytest/_retry_utils.py b/ddtrace/contrib/internal/pytest/_retry_utils.py similarity index 97% rename from ddtrace/contrib/pytest/_retry_utils.py rename to ddtrace/contrib/internal/pytest/_retry_utils.py index 6e38a2974c8..eab45f049be 100644 --- a/ddtrace/contrib/pytest/_retry_utils.py +++ b/ddtrace/contrib/internal/pytest/_retry_utils.py @@ -8,8 +8,8 @@ from _pytest.runner import CallInfo import pytest -from ddtrace.contrib.pytest._types import tmppath_result_key -from ddtrace.contrib.pytest._utils import _TestOutcome +from ddtrace.contrib.internal.pytest._types import tmppath_result_key +from ddtrace.contrib.internal.pytest._utils import _TestOutcome from ddtrace.ext.test_visibility.api import TestExcInfo from ddtrace.ext.test_visibility.api import TestStatus from ddtrace.internal import core diff --git a/ddtrace/contrib/pytest/_types.py b/ddtrace/contrib/internal/pytest/_types.py similarity index 90% rename from ddtrace/contrib/pytest/_types.py rename to ddtrace/contrib/internal/pytest/_types.py index ff1d07feb4d..8222bc7bc54 100644 --- a/ddtrace/contrib/pytest/_types.py +++ b/ddtrace/contrib/internal/pytest/_types.py @@ -1,6 +1,6 @@ import typing as t -from ddtrace.contrib.pytest._utils import _get_pytest_version_tuple +from ddtrace.contrib.internal.pytest._utils import _get_pytest_version_tuple if _get_pytest_version_tuple() >= (7, 0, 0): diff --git a/ddtrace/contrib/pytest/_utils.py b/ddtrace/contrib/internal/pytest/_utils.py similarity index 95% rename from ddtrace/contrib/pytest/_utils.py rename to ddtrace/contrib/internal/pytest/_utils.py index 8dc53ab0228..7e8b2bc2714 100644 --- a/ddtrace/contrib/pytest/_utils.py +++ b/ddtrace/contrib/internal/pytest/_utils.py @@ -7,10 +7,10 @@ import pytest -from ddtrace.contrib.pytest.constants import ATR_MIN_SUPPORTED_VERSION -from ddtrace.contrib.pytest.constants import EFD_MIN_SUPPORTED_VERSION -from ddtrace.contrib.pytest.constants import ITR_MIN_SUPPORTED_VERSION -from ddtrace.contrib.pytest.constants import RETRIES_MIN_SUPPORTED_VERSION +from ddtrace.contrib.internal.pytest.constants import ATR_MIN_SUPPORTED_VERSION +from ddtrace.contrib.internal.pytest.constants import EFD_MIN_SUPPORTED_VERSION +from ddtrace.contrib.internal.pytest.constants import ITR_MIN_SUPPORTED_VERSION +from ddtrace.contrib.internal.pytest.constants import RETRIES_MIN_SUPPORTED_VERSION from ddtrace.ext.test_visibility.api import TestExcInfo from ddtrace.ext.test_visibility.api import TestModuleId from ddtrace.ext.test_visibility.api import TestSourceFileInfo diff --git a/ddtrace/contrib/internal/pytest/constants.py b/ddtrace/contrib/internal/pytest/constants.py new file mode 100644 index 00000000000..cc5d768fc38 --- /dev/null +++ b/ddtrace/contrib/internal/pytest/constants.py @@ -0,0 +1,11 @@ +FRAMEWORK = "pytest" +KIND = "test" + + +# XFail Reason +XFAIL_REASON = "pytest.xfail.reason" + +ITR_MIN_SUPPORTED_VERSION = (7, 2, 0) +RETRIES_MIN_SUPPORTED_VERSION = (7, 0, 0) +EFD_MIN_SUPPORTED_VERSION = RETRIES_MIN_SUPPORTED_VERSION +ATR_MIN_SUPPORTED_VERSION = RETRIES_MIN_SUPPORTED_VERSION diff --git a/ddtrace/contrib/internal/pytest/newhooks.py b/ddtrace/contrib/internal/pytest/newhooks.py new file mode 100644 index 00000000000..c44fd0a1535 --- /dev/null +++ b/ddtrace/contrib/internal/pytest/newhooks.py @@ -0,0 +1,26 @@ +"""pytest-ddtrace hooks. + +These hooks are used to provide extra data used by the Datadog CI Visibility plugin. + +For example: module, suite, and test names for a given item. + +Note that these names will affect th display and reporting of tests in the Datadog UI, as well as information stored +the Intelligent Test Runner. Differing hook implementations may impact the behavior of Datadog CI Visibility products. +""" + +import pytest + + +@pytest.hookspec(firstresult=True) +def pytest_ddtrace_get_item_module_name(item: pytest.Item) -> str: + """Returns the module name to use when reporting CI Visibility results, should be unique""" + + +@pytest.hookspec(firstresult=True) +def pytest_ddtrace_get_item_suite_name(item: pytest.Item) -> str: + """Returns the suite name to use when reporting CI Visibility result, should be unique""" + + +@pytest.hookspec(firstresult=True) +def pytest_ddtrace_get_item_test_name(item: pytest.Item) -> str: + """Returns the test name to use when reporting CI Visibility result, should be unique""" diff --git a/ddtrace/contrib/internal/pytest/patch.py b/ddtrace/contrib/internal/pytest/patch.py new file mode 100644 index 00000000000..0299b665268 --- /dev/null +++ b/ddtrace/contrib/internal/pytest/patch.py @@ -0,0 +1,6 @@ +# Get version is imported from patch.py in _monkey.py +def get_version(): + # type: () -> str + import pytest + + return pytest.__version__ diff --git a/ddtrace/contrib/internal/pytest/plugin.py b/ddtrace/contrib/internal/pytest/plugin.py new file mode 100644 index 00000000000..52cf54a6f9c --- /dev/null +++ b/ddtrace/contrib/internal/pytest/plugin.py @@ -0,0 +1,178 @@ +""" +This custom pytest plugin implements tracing for pytest by using pytest hooks. The plugin registers tracing code +to be run at specific points during pytest execution. The most important hooks used are: + + * pytest_sessionstart: during pytest session startup, a custom trace filter is configured to the global tracer to + only send test spans, which are generated by the plugin. + * pytest_runtest_protocol: this wraps around the execution of a pytest test function, which we trace. Most span + tags are generated and added in this function. We also store the span on the underlying pytest test item to + retrieve later when we need to report test status/result. + * pytest_runtest_makereport: this hook is used to set the test status/result tag, including skipped tests and + expected failures. + +""" +import os +from typing import Dict # noqa:F401 + +import pytest + +from ddtrace import config +from ddtrace.appsec._iast._pytest_plugin import ddtrace_iast # noqa:F401 +from ddtrace.appsec._iast._utils import _is_iast_enabled +from ddtrace.contrib.internal.pytest._utils import _USE_PLUGIN_V2 +from ddtrace.contrib.internal.pytest._utils import _extract_span +from ddtrace.contrib.internal.pytest._utils import _pytest_version_supports_itr + + +# pytest default settings +config._add( + "pytest", + dict( + _default_service="pytest", + operation_name=os.getenv("DD_PYTEST_OPERATION_NAME", default="pytest.test"), + ), +) + + +DDTRACE_HELP_MSG = "Enable tracing of pytest functions." +NO_DDTRACE_HELP_MSG = "Disable tracing of pytest functions." +DDTRACE_INCLUDE_CLASS_HELP_MSG = "Prepend 'ClassName.' to names of class-based tests." +PATCH_ALL_HELP_MSG = "Call ddtrace.patch_all before running tests." + + +def is_enabled(config): + """Check if the ddtrace plugin is enabled.""" + return (config.getoption("ddtrace") or config.getini("ddtrace")) and not config.getoption("no-ddtrace") + + +def pytest_addoption(parser): + """Add ddtrace options.""" + group = parser.getgroup("ddtrace") + + group._addoption( + "--ddtrace", + action="store_true", + dest="ddtrace", + default=False, + help=DDTRACE_HELP_MSG, + ) + + group._addoption( + "--no-ddtrace", + action="store_true", + dest="no-ddtrace", + default=False, + help=NO_DDTRACE_HELP_MSG, + ) + + group._addoption( + "--ddtrace-patch-all", + action="store_true", + dest="ddtrace-patch-all", + default=False, + help=PATCH_ALL_HELP_MSG, + ) + + group._addoption( + "--ddtrace-include-class-name", + action="store_true", + dest="ddtrace-include-class-name", + default=False, + help=DDTRACE_INCLUDE_CLASS_HELP_MSG, + ) + + group._addoption( + "--ddtrace-iast-fail-tests", + action="store_true", + dest="ddtrace-iast-fail-tests", + default=False, + help=DDTRACE_INCLUDE_CLASS_HELP_MSG, + ) + + parser.addini("ddtrace", DDTRACE_HELP_MSG, type="bool") + parser.addini("no-ddtrace", DDTRACE_HELP_MSG, type="bool") + parser.addini("ddtrace-patch-all", PATCH_ALL_HELP_MSG, type="bool") + parser.addini("ddtrace-include-class-name", DDTRACE_INCLUDE_CLASS_HELP_MSG, type="bool") + if _is_iast_enabled(): + from ddtrace.appsec._iast import _iast_pytest_activation + + _iast_pytest_activation() + + +# Version-specific pytest hooks +if _USE_PLUGIN_V2: + from ddtrace.contrib.internal.pytest._plugin_v2 import pytest_collection_finish # noqa: F401 + from ddtrace.contrib.internal.pytest._plugin_v2 import pytest_configure as _versioned_pytest_configure + from ddtrace.contrib.internal.pytest._plugin_v2 import pytest_ddtrace_get_item_module_name # noqa: F401 + from ddtrace.contrib.internal.pytest._plugin_v2 import pytest_ddtrace_get_item_suite_name # noqa: F401 + from ddtrace.contrib.internal.pytest._plugin_v2 import pytest_ddtrace_get_item_test_name # noqa: F401 + from ddtrace.contrib.internal.pytest._plugin_v2 import pytest_load_initial_conftests # noqa: F401 + from ddtrace.contrib.internal.pytest._plugin_v2 import pytest_report_teststatus # noqa: F401 + from ddtrace.contrib.internal.pytest._plugin_v2 import pytest_runtest_makereport # noqa: F401 + from ddtrace.contrib.internal.pytest._plugin_v2 import pytest_runtest_protocol # noqa: F401 + from ddtrace.contrib.internal.pytest._plugin_v2 import pytest_sessionfinish # noqa: F401 + from ddtrace.contrib.internal.pytest._plugin_v2 import pytest_sessionstart # noqa: F401 + from ddtrace.contrib.internal.pytest._plugin_v2 import pytest_terminal_summary # noqa: F401 +else: + from ddtrace.contrib.internal.pytest._plugin_v1 import pytest_collection_modifyitems # noqa: F401 + from ddtrace.contrib.internal.pytest._plugin_v1 import pytest_configure as _versioned_pytest_configure + from ddtrace.contrib.internal.pytest._plugin_v1 import pytest_ddtrace_get_item_module_name # noqa: F401 + from ddtrace.contrib.internal.pytest._plugin_v1 import pytest_ddtrace_get_item_suite_name # noqa: F401 + from ddtrace.contrib.internal.pytest._plugin_v1 import pytest_ddtrace_get_item_test_name # noqa: F401 + from ddtrace.contrib.internal.pytest._plugin_v1 import pytest_load_initial_conftests # noqa: F401 + from ddtrace.contrib.internal.pytest._plugin_v1 import pytest_runtest_makereport # noqa: F401 + from ddtrace.contrib.internal.pytest._plugin_v1 import pytest_runtest_protocol # noqa: F401 + from ddtrace.contrib.internal.pytest._plugin_v1 import pytest_sessionfinish # noqa: F401 + from ddtrace.contrib.internal.pytest._plugin_v1 import pytest_sessionstart # noqa: F401 + + # Internal coverage is only used for ITR at the moment, so the hook is only added if the pytest version supports it + if _pytest_version_supports_itr(): + from ddtrace.contrib.internal.pytest._plugin_v1 import pytest_terminal_summary # noqa: F401 + + +def pytest_configure(config): + config.addinivalue_line("markers", "dd_tags(**kwargs): add tags to current span") + if is_enabled(config): + _versioned_pytest_configure(config) + + +@pytest.hookimpl +def pytest_addhooks(pluginmanager): + from ddtrace.contrib.internal.pytest import newhooks + + pluginmanager.add_hookspecs(newhooks) + + +@pytest.fixture(scope="function") +def ddspan(request): + """Return the :class:`ddtrace._trace.span.Span` instance associated with the + current test when Datadog CI Visibility is enabled. + """ + from ddtrace.internal.ci_visibility import CIVisibility as _CIVisibility + + if _CIVisibility.enabled: + return _extract_span(request.node) + + +@pytest.fixture(scope="session") +def ddtracer(): + """Return the :class:`ddtrace.tracer.Tracer` instance for Datadog CI + visibility if it is enabled, otherwise return the default Datadog tracer. + """ + import ddtrace + from ddtrace.internal.ci_visibility import CIVisibility as _CIVisibility + + if _CIVisibility.enabled: + return _CIVisibility._instance.tracer + return ddtrace.tracer + + +@pytest.fixture(scope="session", autouse=True) +def patch_all(request): + """Patch all available modules for Datadog tracing when ddtrace-patch-all + is specified in command or .ini. + """ + import ddtrace + + if request.config.getoption("ddtrace-patch-all") or request.config.getini("ddtrace-patch-all"): + ddtrace.patch_all() diff --git a/ddtrace/contrib/internal/pytest_bdd/__init__.py b/ddtrace/contrib/internal/pytest_bdd/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ddtrace/contrib/pytest_bdd/_plugin.py b/ddtrace/contrib/internal/pytest_bdd/_plugin.py similarity index 94% rename from ddtrace/contrib/pytest_bdd/_plugin.py rename to ddtrace/contrib/internal/pytest_bdd/_plugin.py index 1ed8b8099e5..eb7bcc1028a 100644 --- a/ddtrace/contrib/pytest_bdd/_plugin.py +++ b/ddtrace/contrib/internal/pytest_bdd/_plugin.py @@ -4,10 +4,10 @@ import pytest -from ddtrace.contrib.pytest._utils import _extract_span as _extract_feature_span -from ddtrace.contrib.pytest_bdd import get_version -from ddtrace.contrib.pytest_bdd.constants import FRAMEWORK -from ddtrace.contrib.pytest_bdd.constants import STEP_KIND +from ddtrace.contrib.internal.pytest._utils import _extract_span as _extract_feature_span +from ddtrace.contrib.internal.pytest_bdd.constants import FRAMEWORK +from ddtrace.contrib.internal.pytest_bdd.constants import STEP_KIND +from ddtrace.contrib.internal.pytest_bdd.patch import get_version from ddtrace.ext import test from ddtrace.internal.ci_visibility import CIVisibility as _CIVisibility from ddtrace.internal.logger import get_logger diff --git a/ddtrace/contrib/internal/pytest_bdd/constants.py b/ddtrace/contrib/internal/pytest_bdd/constants.py new file mode 100644 index 00000000000..2dd377f7619 --- /dev/null +++ b/ddtrace/contrib/internal/pytest_bdd/constants.py @@ -0,0 +1,2 @@ +FRAMEWORK = "pytest_bdd" +STEP_KIND = "pytest_bdd.step" diff --git a/ddtrace/contrib/internal/pytest_bdd/patch.py b/ddtrace/contrib/internal/pytest_bdd/patch.py new file mode 100644 index 00000000000..efab83aee4b --- /dev/null +++ b/ddtrace/contrib/internal/pytest_bdd/patch.py @@ -0,0 +1,9 @@ +# ddtrace/_monkey.py expects all integrations to define get_version in /patch.py file +def get_version(): + # type: () -> str + try: + import importlib.metadata as importlib_metadata + except ImportError: + import importlib_metadata # type: ignore[no-redef] + + return str(importlib_metadata.version("pytest-bdd")) diff --git a/ddtrace/contrib/internal/pytest_bdd/plugin.py b/ddtrace/contrib/internal/pytest_bdd/plugin.py new file mode 100644 index 00000000000..22856056162 --- /dev/null +++ b/ddtrace/contrib/internal/pytest_bdd/plugin.py @@ -0,0 +1,30 @@ +from ddtrace import DDTraceDeprecationWarning +from ddtrace import config +from ddtrace.contrib.internal.pytest._utils import _USE_PLUGIN_V2 +from ddtrace.contrib.internal.pytest.plugin import is_enabled as is_ddtrace_enabled +from ddtrace.vendor.debtcollector import deprecate + + +# pytest-bdd default settings +config._add( + "pytest_bdd", + dict( + _default_service="pytest_bdd", + ), +) + + +def pytest_configure(config): + if config.pluginmanager.hasplugin("pytest-bdd") and config.pluginmanager.hasplugin("ddtrace"): + if not _USE_PLUGIN_V2: + if is_ddtrace_enabled(config): + from ._plugin import _PytestBddPlugin + + deprecate( + "the ddtrace.pytest_bdd plugin is deprecated", + message="it will be integrated with the main pytest ddtrace plugin", + removal_version="3.0.0", + category=DDTraceDeprecationWarning, + ) + + config.pluginmanager.register(_PytestBddPlugin(), "_datadog-pytest-bdd") diff --git a/ddtrace/contrib/internal/pytest_benchmark/__init__.py b/ddtrace/contrib/internal/pytest_benchmark/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ddtrace/contrib/pytest_benchmark/_plugin.py b/ddtrace/contrib/internal/pytest_benchmark/_plugin.py similarity index 73% rename from ddtrace/contrib/pytest_benchmark/_plugin.py rename to ddtrace/contrib/internal/pytest_benchmark/_plugin.py index ac6afa350d6..ee54660afc8 100644 --- a/ddtrace/contrib/pytest_benchmark/_plugin.py +++ b/ddtrace/contrib/internal/pytest_benchmark/_plugin.py @@ -1,9 +1,9 @@ import pytest -from ddtrace.contrib.pytest._utils import _extract_span -from ddtrace.contrib.pytest_benchmark.constants import BENCHMARK_INFO -from ddtrace.contrib.pytest_benchmark.constants import PLUGIN_METRICS -from ddtrace.contrib.pytest_benchmark.constants import PLUGIN_OUTLIERS +from ddtrace.contrib.internal.pytest._utils import _extract_span +from ddtrace.contrib.internal.pytest_benchmark.constants import BENCHMARK_INFO +from ddtrace.contrib.internal.pytest_benchmark.constants import PLUGIN_METRICS +from ddtrace.contrib.internal.pytest_benchmark.constants import PLUGIN_OUTLIERS from ddtrace.ext.test import TEST_TYPE diff --git a/ddtrace/contrib/internal/pytest_benchmark/constants.py b/ddtrace/contrib/internal/pytest_benchmark/constants.py new file mode 100644 index 00000000000..b4c4f7f5b27 --- /dev/null +++ b/ddtrace/contrib/internal/pytest_benchmark/constants.py @@ -0,0 +1,79 @@ +BENCHMARK_INFO = "benchmark.duration.info" +BENCHMARK_MEAN = "benchmark.duration.mean" +BENCHMARK_RUN = "benchmark.duration.runs" + +STATISTICS_HD15IQR = "benchmark.duration.statistics.hd15iqr" +STATISTICS_IQR = "benchmark.duration.statistics.iqr" +STATISTICS_IQR_OUTLIERS = "benchmark.duration.statistics.iqr_outliers" +STATISTICS_LD15IQR = "benchmark.duration.statistics.ld15iqr" +STATISTICS_MAX = "benchmark.duration.statistics.max" +STATISTICS_MEAN = "benchmark.duration.statistics.mean" +STATISTICS_MEDIAN = "benchmark.duration.statistics.median" +STATISTICS_MIN = "benchmark.duration.statistics.min" +STATISTICS_N = "benchmark.duration.statistics.n" +STATISTICS_OPS = "benchmark.duration.statistics.ops" +STATISTICS_OUTLIERS = "benchmark.duration.statistics.outliers" +STATISTICS_Q1 = "benchmark.duration.statistics.q1" +STATISTICS_Q3 = "benchmark.duration.statistics.q3" +STATISTICS_STDDEV = "benchmark.duration.statistics.std_dev" +STATISTICS_STDDEV_OUTLIERS = "benchmark.duration.statistics.std_dev_outliers" +STATISTICS_TOTAL = "benchmark.duration.statistics.total" + +PLUGIN_HD15IQR = "hd15iqr" +PLUGIN_IQR = "iqr" +PLUGIN_IQR_OUTLIERS = "iqr_outliers" +PLUGIN_LD15IQR = "ld15iqr" +PLUGIN_MAX = "max" +PLUGIN_MEAN = "mean" +PLUGIN_MEDIAN = "median" +PLUGIN_MIN = "min" +PLUGIN_OPS = "ops" +PLUGIN_OUTLIERS = "outliers" +PLUGIN_Q1 = "q1" +PLUGIN_Q3 = "q3" +PLUGIN_ROUNDS = "rounds" +PLUGIN_STDDEV = "stddev" +PLUGIN_STDDEV_OUTLIERS = "stddev_outliers" +PLUGIN_TOTAL = "total" + +PLUGIN_METRICS = { + BENCHMARK_MEAN: PLUGIN_MEAN, + BENCHMARK_RUN: PLUGIN_ROUNDS, + STATISTICS_HD15IQR: PLUGIN_HD15IQR, + STATISTICS_IQR: PLUGIN_IQR, + STATISTICS_IQR_OUTLIERS: PLUGIN_IQR_OUTLIERS, + STATISTICS_LD15IQR: PLUGIN_LD15IQR, + STATISTICS_MAX: PLUGIN_MAX, + STATISTICS_MEAN: PLUGIN_MEAN, + STATISTICS_MEDIAN: PLUGIN_MEDIAN, + STATISTICS_MIN: PLUGIN_MIN, + STATISTICS_OPS: PLUGIN_OPS, + STATISTICS_OUTLIERS: PLUGIN_OUTLIERS, + STATISTICS_Q1: PLUGIN_Q1, + STATISTICS_Q3: PLUGIN_Q3, + STATISTICS_N: PLUGIN_ROUNDS, + STATISTICS_STDDEV: PLUGIN_STDDEV, + STATISTICS_STDDEV_OUTLIERS: PLUGIN_STDDEV_OUTLIERS, + STATISTICS_TOTAL: PLUGIN_TOTAL, +} + +PLUGIN_METRICS_V2 = { + "duration_mean": PLUGIN_MEAN, + "duration_runs": PLUGIN_ROUNDS, + "statistics_hd15iqr": PLUGIN_HD15IQR, + "statistics_iqr": PLUGIN_IQR, + "statistics_iqr_outliers": PLUGIN_IQR_OUTLIERS, + "statistics_ld15iqr": PLUGIN_LD15IQR, + "statistics_max": PLUGIN_MAX, + "statistics_mean": PLUGIN_MEAN, + "statistics_median": PLUGIN_MEDIAN, + "statistics_min": PLUGIN_MIN, + "statistics_n": PLUGIN_ROUNDS, + "statistics_ops": PLUGIN_OPS, + "statistics_outliers": PLUGIN_OUTLIERS, + "statistics_q1": PLUGIN_Q1, + "statistics_q3": PLUGIN_Q3, + "statistics_std_dev": PLUGIN_STDDEV, + "statistics_std_dev_outliers": PLUGIN_STDDEV_OUTLIERS, + "statistics_total": PLUGIN_TOTAL, +} diff --git a/ddtrace/contrib/internal/pytest_benchmark/plugin.py b/ddtrace/contrib/internal/pytest_benchmark/plugin.py new file mode 100644 index 00000000000..04728f764a3 --- /dev/null +++ b/ddtrace/contrib/internal/pytest_benchmark/plugin.py @@ -0,0 +1,19 @@ +from ddtrace import DDTraceDeprecationWarning +from ddtrace.contrib.internal.pytest._utils import _USE_PLUGIN_V2 +from ddtrace.contrib.internal.pytest.plugin import is_enabled as is_ddtrace_enabled +from ddtrace.vendor.debtcollector import deprecate + + +def pytest_configure(config): + if config.pluginmanager.hasplugin("benchmark") and config.pluginmanager.hasplugin("ddtrace"): + if is_ddtrace_enabled(config): + deprecate( + "this version of the ddtrace.pytest_benchmark plugin is deprecated", + message="it will be integrated with the main pytest ddtrace plugin", + removal_version="3.0.0", + category=DDTraceDeprecationWarning, + ) + if not _USE_PLUGIN_V2: + from ._plugin import _PytestBenchmarkPlugin + + config.pluginmanager.register(_PytestBenchmarkPlugin(), "_datadog-pytest-benchmark") diff --git a/ddtrace/contrib/internal/unittest/__init__.py b/ddtrace/contrib/internal/unittest/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ddtrace/contrib/internal/unittest/constants.py b/ddtrace/contrib/internal/unittest/constants.py new file mode 100644 index 00000000000..dc58863a2a5 --- /dev/null +++ b/ddtrace/contrib/internal/unittest/constants.py @@ -0,0 +1,8 @@ +COMPONENT_VALUE = "unittest" +FRAMEWORK = "unittest" +KIND = "test" + +TEST_OPERATION_NAME = "unittest.test" +SUITE_OPERATION_NAME = "unittest.test_suite" +SESSION_OPERATION_NAME = "unittest.test_session" +MODULE_OPERATION_NAME = "unittest.test_module" diff --git a/ddtrace/contrib/internal/unittest/patch.py b/ddtrace/contrib/internal/unittest/patch.py new file mode 100644 index 00000000000..74ce8d1d6a0 --- /dev/null +++ b/ddtrace/contrib/internal/unittest/patch.py @@ -0,0 +1,868 @@ +import inspect +import os +from typing import Union +import unittest + +import wrapt + +import ddtrace +from ddtrace import config +from ddtrace.constants import SPAN_KIND +from ddtrace.contrib.internal.coverage.data import _coverage_data +from ddtrace.contrib.internal.coverage.patch import patch as patch_coverage +from ddtrace.contrib.internal.coverage.patch import run_coverage_report +from ddtrace.contrib.internal.coverage.patch import unpatch as unpatch_coverage +from ddtrace.contrib.internal.coverage.utils import _is_coverage_invoked_by_coverage_run +from ddtrace.contrib.internal.coverage.utils import _is_coverage_patched +from ddtrace.contrib.internal.unittest.constants import COMPONENT_VALUE +from ddtrace.contrib.internal.unittest.constants import FRAMEWORK +from ddtrace.contrib.internal.unittest.constants import KIND +from ddtrace.contrib.internal.unittest.constants import MODULE_OPERATION_NAME +from ddtrace.contrib.internal.unittest.constants import SESSION_OPERATION_NAME +from ddtrace.contrib.internal.unittest.constants import SUITE_OPERATION_NAME +from ddtrace.ext import SpanTypes +from ddtrace.ext import test +from ddtrace.ext.ci import RUNTIME_VERSION +from ddtrace.ext.ci import _get_runtime_and_os_metadata +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 +from ddtrace.internal.ci_visibility.constants import ITR_UNSKIPPABLE_REASON +from ddtrace.internal.ci_visibility.constants import MODULE_ID as _MODULE_ID +from ddtrace.internal.ci_visibility.constants import MODULE_TYPE as _MODULE_TYPE +from ddtrace.internal.ci_visibility.constants import SESSION_ID as _SESSION_ID +from ddtrace.internal.ci_visibility.constants import SESSION_TYPE as _SESSION_TYPE +from ddtrace.internal.ci_visibility.constants import SKIPPED_BY_ITR_REASON +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 _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 +from ddtrace.internal.ci_visibility.coverage import _stop_coverage +from ddtrace.internal.ci_visibility.coverage import _switch_coverage_context +from ddtrace.internal.ci_visibility.utils import _add_pct_covered_to_span +from ddtrace.internal.ci_visibility.utils import _add_start_end_source_file_path_data_to_span +from ddtrace.internal.ci_visibility.utils import _generate_fully_qualified_test_name +from ddtrace.internal.ci_visibility.utils import get_relative_or_absolute_path_for_path +from ddtrace.internal.constants import COMPONENT +from ddtrace.internal.logger import get_logger +from ddtrace.internal.utils.formats import asbool +from ddtrace.internal.utils.wrappers import unwrap as _u + + +log = get_logger(__name__) +_global_skipped_elements = 0 + +# unittest default settings +config._add( + "unittest", + dict( + _default_service="unittest", + operation_name=os.getenv("DD_UNITTEST_OPERATION_NAME", default="unittest.test"), + strict_naming=asbool(os.getenv("DD_CIVISIBILITY_UNITTEST_STRICT_NAMING", default=True)), + ), +) + + +def get_version(): + # type: () -> str + return "" + + +def _enable_unittest_if_not_started(): + _initialize_unittest_data() + if _CIVisibility.enabled: + return + _CIVisibility.enable(config=ddtrace.config.unittest) + + +def _initialize_unittest_data(): + if not hasattr(_CIVisibility, "_unittest_data"): + _CIVisibility._unittest_data = {} + if "suites" not in _CIVisibility._unittest_data: + _CIVisibility._unittest_data["suites"] = {} + if "modules" not in _CIVisibility._unittest_data: + _CIVisibility._unittest_data["modules"] = {} + if "unskippable_tests" not in _CIVisibility._unittest_data: + _CIVisibility._unittest_data["unskippable_tests"] = set() + + +def _set_tracer(tracer: ddtrace.tracer): + """Manually sets the tracer instance to `unittest.`""" + unittest._datadog_tracer = tracer + + +def _is_test_coverage_enabled(test_object) -> bool: + return _CIVisibility._instance._collect_coverage_enabled and not _is_skipped_test(test_object) + + +def _is_skipped_test(test_object) -> bool: + testMethod = getattr(test_object, test_object._testMethodName, "") + return ( + (hasattr(test_object.__class__, "__unittest_skip__") and test_object.__class__.__unittest_skip__) + or (hasattr(testMethod, "__unittest_skip__") and testMethod.__unittest_skip__) + or _is_skipped_by_itr(test_object) + ) + + +def _is_skipped_by_itr(test_object) -> bool: + return hasattr(test_object, "_dd_itr_skip") and test_object._dd_itr_skip + + +def _should_be_skipped_by_itr(args: tuple, test_module_suite_path: str, test_name: str, test_object) -> bool: + return ( + len(args) + and _CIVisibility._instance._should_skip_path(test_module_suite_path, test_name) + and not _is_skipped_test(test_object) + ) + + +def _is_marked_as_unskippable(test_object) -> bool: + test_suite_name = _extract_suite_name_from_test_method(test_object) + test_name = _extract_test_method_name(test_object) + test_module_path = _extract_module_file_path(test_object) + test_module_suite_name = _generate_fully_qualified_test_name(test_module_path, test_suite_name, test_name) + return ( + hasattr(_CIVisibility, "_unittest_data") + and test_module_suite_name in _CIVisibility._unittest_data["unskippable_tests"] + ) + + +def _update_skipped_elements_and_set_tags(test_module_span: ddtrace.Span, test_session_span: ddtrace.Span): + global _global_skipped_elements + _global_skipped_elements += 1 + + test_module_span._metrics[test.ITR_TEST_SKIPPING_COUNT] += 1 + test_module_span.set_tag_str(test.ITR_TEST_SKIPPING_TESTS_SKIPPED, "true") + test_module_span.set_tag_str(test.ITR_DD_CI_ITR_TESTS_SKIPPED, "true") + + test_session_span.set_tag_str(test.ITR_TEST_SKIPPING_TESTS_SKIPPED, "true") + test_session_span.set_tag_str(test.ITR_DD_CI_ITR_TESTS_SKIPPED, "true") + + +def _store_test_span(item, span: ddtrace.Span): + """Store datadog span at `unittest` test instance.""" + item._datadog_span = span + + +def _store_module_identifier(test_object: unittest.TextTestRunner): + """Store module identifier at `unittest` module instance, this is useful to classify event types.""" + if hasattr(test_object, "test") and hasattr(test_object.test, "_tests"): + for module in test_object.test._tests: + if len(module._tests) and _extract_module_name_from_module(module): + _set_identifier(module, "module") + + +def _store_suite_identifier(module): + """Store suite identifier at `unittest` suite instance, this is useful to classify event types.""" + if hasattr(module, "_tests"): + for suite in module._tests: + if len(suite._tests) and _extract_module_name_from_module(suite): + _set_identifier(suite, "suite") + + +def _is_test(item) -> bool: + if ( + type(item) == unittest.TestSuite + or not hasattr(item, "_testMethodName") + or (ddtrace.config.unittest.strict_naming and not item._testMethodName.startswith("test")) + ): + return False + return True + + +def _extract_span(item) -> Union[ddtrace.Span, None]: + return getattr(item, "_datadog_span", None) + + +def _extract_command_name_from_session(session: unittest.TextTestRunner) -> str: + if not hasattr(session, "progName"): + return "python -m unittest" + return getattr(session, "progName", "") + + +def _extract_test_method_name(test_object) -> str: + """Extract test method name from `unittest` instance.""" + return getattr(test_object, "_testMethodName", "") + + +def _extract_session_span() -> Union[ddtrace.Span, None]: + return getattr(_CIVisibility, "_datadog_session_span", None) + + +def _extract_module_span(module_identifier: str) -> Union[ddtrace.Span, None]: + if hasattr(_CIVisibility, "_unittest_data") and module_identifier in _CIVisibility._unittest_data["modules"]: + return _CIVisibility._unittest_data["modules"][module_identifier].get("module_span") + return None + + +def _extract_suite_span(suite_identifier: str) -> Union[ddtrace.Span, None]: + if hasattr(_CIVisibility, "_unittest_data") and suite_identifier in _CIVisibility._unittest_data["suites"]: + return _CIVisibility._unittest_data["suites"][suite_identifier].get("suite_span") + return None + + +def _update_status_item(item: ddtrace.Span, status: str): + """ + Sets the status for each Span implementing the test FAIL logic override. + """ + existing_status = item.get_tag(test.STATUS) + if existing_status and (status == test.Status.SKIP.value or existing_status == test.Status.FAIL.value): + return None + item.set_tag_str(test.STATUS, status) + return None + + +def _extract_suite_name_from_test_method(item) -> str: + item_type = type(item) + return getattr(item_type, "__name__", "") + + +def _extract_module_name_from_module(item) -> str: + if _is_test(item): + return type(item).__module__ + return "" + + +def _extract_test_reason(item: tuple) -> str: + """ + Given a tuple of type [test_class, str], it returns the test failure/skip reason + """ + return item[1] + + +def _extract_test_file_name(item) -> str: + return os.path.basename(inspect.getfile(item.__class__)) + + +def _extract_module_file_path(item) -> str: + if _is_test(item): + try: + test_module_object = inspect.getfile(item.__class__) + except TypeError: + log.debug( + "Tried to collect module file path but it is a built-in Python function", + ) + return "" + return get_relative_or_absolute_path_for_path(test_module_object, os.getcwd()) + + return "" + + +def _generate_test_resource(suite_name: str, test_name: str) -> str: + return "{}.{}".format(suite_name, test_name) + + +def _generate_suite_resource(test_suite: str) -> str: + return "{}".format(test_suite) + + +def _generate_module_resource(test_module: str) -> str: + return "{}".format(test_module) + + +def _generate_session_resource(test_command: str) -> str: + return "{}".format(test_command) + + +def _set_test_skipping_tags_to_span(span: ddtrace.Span): + span.set_tag_str(test.ITR_TEST_SKIPPING_ENABLED, "true") + span.set_tag_str(test.ITR_TEST_SKIPPING_TYPE, TEST) + span.set_tag_str(test.ITR_TEST_SKIPPING_TESTS_SKIPPED, "false") + span.set_tag_str(test.ITR_DD_CI_ITR_TESTS_SKIPPED, "false") + span.set_tag_str(test.ITR_FORCED_RUN, "false") + span.set_tag_str(test.ITR_UNSKIPPABLE, "false") + + +def _set_identifier(item, name: str): + """ + Adds an event type classification to a `unittest` test. + """ + item._datadog_object = name + + +def _is_valid_result(instance: unittest.TextTestRunner, args: tuple) -> bool: + return instance and isinstance(instance, unittest.runner.TextTestResult) and args + + +def _is_valid_test_call(kwargs: dict) -> bool: + """ + Validates that kwargs is empty to ensure that `unittest` is running a test + """ + return not len(kwargs) + + +def _is_valid_module_suite_call(func) -> bool: + """ + Validates that the mocked function is an actual function from `unittest` + """ + return type(func).__name__ == "method" or type(func).__name__ == "instancemethod" + + +def _is_invoked_by_cli(instance: unittest.TextTestRunner) -> bool: + return ( + hasattr(instance, "progName") + or hasattr(_CIVisibility, "_datadog_entry") + and _CIVisibility._datadog_entry == "cli" + ) + + +def _extract_test_method_object(test_object): + if hasattr(test_object, "_testMethodName"): + return getattr(test_object, test_object._testMethodName, None) + return None + + +def _is_invoked_by_text_test_runner() -> bool: + return hasattr(_CIVisibility, "_datadog_entry") and _CIVisibility._datadog_entry == "TextTestRunner" + + +def _generate_module_suite_path(test_module_path: str, test_suite_name: str) -> str: + return "{}.{}".format(test_module_path, test_suite_name) + + +def _populate_suites_and_modules(test_objects: list, seen_suites: dict, seen_modules: dict): + """ + Discovers suites and modules and initializes the seen_suites and seen_modules dictionaries. + """ + if not hasattr(test_objects, "__iter__"): + return + for test_object in test_objects: + if not _is_test(test_object): + _populate_suites_and_modules(test_object, seen_suites, seen_modules) + continue + test_module_path = _extract_module_file_path(test_object) + test_suite_name = _extract_suite_name_from_test_method(test_object) + test_module_suite_path = _generate_module_suite_path(test_module_path, test_suite_name) + if test_module_path not in seen_modules: + seen_modules[test_module_path] = { + "module_span": None, + "remaining_suites": 0, + } + if test_module_suite_path not in seen_suites: + seen_suites[test_module_suite_path] = { + "suite_span": None, + "remaining_tests": 0, + } + + seen_modules[test_module_path]["remaining_suites"] += 1 + + seen_suites[test_module_suite_path]["remaining_tests"] += 1 + + +def _finish_remaining_suites_and_modules(seen_suites: dict, seen_modules: dict): + """ + Forces all suite and module spans to finish and updates their statuses. + """ + for suite in seen_suites.values(): + test_suite_span = suite["suite_span"] + if test_suite_span and not test_suite_span.finished: + _finish_span(test_suite_span) + + for module in seen_modules.values(): + test_module_span = module["module_span"] + if test_module_span and not test_module_span.finished: + _finish_span(test_module_span) + del _CIVisibility._unittest_data + + +def _update_remaining_suites_and_modules( + test_module_suite_path: str, test_module_path: str, test_module_span: ddtrace.Span, test_suite_span: ddtrace.Span +): + """ + Updates the remaining test suite and test counter and finishes spans when these have finished their execution. + """ + suite_dict = _CIVisibility._unittest_data["suites"][test_module_suite_path] + modules_dict = _CIVisibility._unittest_data["modules"][test_module_path] + + suite_dict["remaining_tests"] -= 1 + if suite_dict["remaining_tests"] == 0: + modules_dict["remaining_suites"] -= 1 + _finish_span(test_suite_span) + if modules_dict["remaining_suites"] == 0: + _finish_span(test_module_span) + + +def _update_test_skipping_count_span(span: ddtrace.Span): + if _CIVisibility.test_skipping_enabled(): + span.set_metric(test.ITR_TEST_SKIPPING_COUNT, _global_skipped_elements) + + +def _extract_skip_if_reason(args, kwargs): + if len(args) >= 2: + return _extract_test_reason(args) + elif kwargs and "reason" in kwargs: + return kwargs["reason"] + return "" + + +def patch(): + """ + Patch the instrumented methods from unittest + """ + if getattr(unittest, "_datadog_patch", False) or _CIVisibility.enabled: + return + _initialize_unittest_data() + + unittest._datadog_patch = True + + _w = wrapt.wrap_function_wrapper + + _w(unittest, "TextTestResult.addSuccess", add_success_test_wrapper) + _w(unittest, "TextTestResult.addFailure", add_failure_test_wrapper) + _w(unittest, "TextTestResult.addError", add_failure_test_wrapper) + _w(unittest, "TextTestResult.addSkip", add_skip_test_wrapper) + _w(unittest, "TextTestResult.addExpectedFailure", add_xfail_test_wrapper) + _w(unittest, "TextTestResult.addUnexpectedSuccess", add_xpass_test_wrapper) + _w(unittest, "skipIf", skip_if_decorator) + _w(unittest, "TestCase.run", handle_test_wrapper) + _w(unittest, "TestSuite.run", collect_text_test_runner_session) + _w(unittest, "TextTestRunner.run", handle_text_test_runner_wrapper) + _w(unittest, "TestProgram.runTests", handle_cli_run) + + +def unpatch(): + """ + Undo patched instrumented methods from unittest + """ + if not getattr(unittest, "_datadog_patch", False): + return + + _u(unittest.TextTestResult, "addSuccess") + _u(unittest.TextTestResult, "addFailure") + _u(unittest.TextTestResult, "addError") + _u(unittest.TextTestResult, "addSkip") + _u(unittest.TextTestResult, "addExpectedFailure") + _u(unittest.TextTestResult, "addUnexpectedSuccess") + _u(unittest, "skipIf") + _u(unittest.TestSuite, "run") + _u(unittest.TestCase, "run") + _u(unittest.TextTestRunner, "run") + _u(unittest.TestProgram, "runTests") + + unittest._datadog_patch = False + _CIVisibility.disable() + + +def _set_test_span_status(test_item, status: str, exc_info: str = None, skip_reason: str = None): + span = _extract_span(test_item) + if not span: + log.debug("Tried setting test result for test but could not find span for %s", test_item) + return None + span.set_tag_str(test.STATUS, status) + if exc_info: + span.set_exc_info(exc_info[0], exc_info[1], exc_info[2]) + if status == test.Status.SKIP.value: + span.set_tag_str(test.SKIP_REASON, skip_reason) + + +def _set_test_xpass_xfail_result(test_item, result: str): + """ + Sets `test.result` and `test.status` to a XFAIL or XPASS test. + """ + span = _extract_span(test_item) + if not span: + log.debug("Tried setting test result for an xpass or xfail test but could not find span for %s", test_item) + return None + span.set_tag_str(test.RESULT, result) + status = span.get_tag(test.STATUS) + if result == test.Status.XFAIL.value: + if status == test.Status.PASS.value: + span.set_tag_str(test.STATUS, test.Status.FAIL.value) + elif status == test.Status.FAIL.value: + span.set_tag_str(test.STATUS, test.Status.PASS.value) + + +def add_success_test_wrapper(func, instance: unittest.TextTestRunner, args: tuple, kwargs: dict): + if _is_valid_result(instance, args): + _set_test_span_status(test_item=args[0], status=test.Status.PASS.value) + + return func(*args, **kwargs) + + +def add_failure_test_wrapper(func, instance: unittest.TextTestRunner, args: tuple, kwargs: dict): + if _is_valid_result(instance, args): + _set_test_span_status(test_item=args[0], exc_info=_extract_test_reason(args), status=test.Status.FAIL.value) + + return func(*args, **kwargs) + + +def add_xfail_test_wrapper(func, instance: unittest.TextTestRunner, args: tuple, kwargs: dict): + if _is_valid_result(instance, args): + _set_test_xpass_xfail_result(test_item=args[0], result=test.Status.XFAIL.value) + + return func(*args, **kwargs) + + +def add_skip_test_wrapper(func, instance: unittest.TextTestRunner, args: tuple, kwargs: dict): + if _is_valid_result(instance, args): + _set_test_span_status(test_item=args[0], skip_reason=_extract_test_reason(args), status=test.Status.SKIP.value) + + return func(*args, **kwargs) + + +def add_xpass_test_wrapper(func, instance, args: tuple, kwargs: dict): + if _is_valid_result(instance, args): + _set_test_xpass_xfail_result(test_item=args[0], result=test.Status.XPASS.value) + + return func(*args, **kwargs) + + +def _mark_test_as_unskippable(obj): + test_name = obj.__name__ + test_suite_name = str(obj).split(".")[0].split()[1] + test_module_path = get_relative_or_absolute_path_for_path(obj.__code__.co_filename, os.getcwd()) + test_module_suite_name = _generate_fully_qualified_test_name(test_module_path, test_suite_name, test_name) + _CIVisibility._unittest_data["unskippable_tests"].add(test_module_suite_name) + return obj + + +def _using_unskippable_decorator(args, kwargs): + return args[0] is False and _extract_skip_if_reason(args, kwargs) == ITR_UNSKIPPABLE_REASON + + +def skip_if_decorator(func, instance, args: tuple, kwargs: dict): + if _using_unskippable_decorator(args, kwargs): + return _mark_test_as_unskippable + return func(*args, **kwargs) + + +def handle_test_wrapper(func, instance, args: tuple, kwargs: dict): + """ + Creates module and suite spans for `unittest` test executions. + """ + if _is_valid_test_call(kwargs) and _is_test(instance) and hasattr(_CIVisibility, "_unittest_data"): + test_name = _extract_test_method_name(instance) + test_suite_name = _extract_suite_name_from_test_method(instance) + test_module_path = _extract_module_file_path(instance) + test_module_suite_path = _generate_module_suite_path(test_module_path, test_suite_name) + test_suite_span = _extract_suite_span(test_module_suite_path) + test_module_span = _extract_module_span(test_module_path) + if test_module_span is None and test_module_path in _CIVisibility._unittest_data["modules"]: + test_module_span = _start_test_module_span(instance) + _CIVisibility._unittest_data["modules"][test_module_path]["module_span"] = test_module_span + if test_suite_span is None and test_module_suite_path in _CIVisibility._unittest_data["suites"]: + test_suite_span = _start_test_suite_span(instance) + suite_dict = _CIVisibility._unittest_data["suites"][test_module_suite_path] + suite_dict["suite_span"] = test_suite_span + if not test_module_span or not test_suite_span: + log.debug("Suite and/or module span not found for test: %s", test_name) + return func(*args, **kwargs) + with _start_test_span(instance, test_suite_span) as span: + test_session_span = _CIVisibility._datadog_session_span + root_directory = os.getcwd() + fqn_test = _generate_fully_qualified_test_name(test_module_path, test_suite_name, test_name) + + if _CIVisibility.test_skipping_enabled(): + if ITR_CORRELATION_ID_TAG_NAME in _CIVisibility._instance._itr_meta: + span.set_tag_str( + ITR_CORRELATION_ID_TAG_NAME, _CIVisibility._instance._itr_meta[ITR_CORRELATION_ID_TAG_NAME] + ) + + if _is_marked_as_unskippable(instance): + span.set_tag_str(test.ITR_UNSKIPPABLE, "true") + test_module_span.set_tag_str(test.ITR_UNSKIPPABLE, "true") + test_session_span.set_tag_str(test.ITR_UNSKIPPABLE, "true") + test_module_suite_path_without_extension = "{}/{}".format( + os.path.splitext(test_module_path)[0], test_suite_name + ) + if _should_be_skipped_by_itr(args, test_module_suite_path_without_extension, test_name, instance): + if _is_marked_as_unskippable(instance): + span.set_tag_str(test.ITR_FORCED_RUN, "true") + test_module_span.set_tag_str(test.ITR_FORCED_RUN, "true") + test_session_span.set_tag_str(test.ITR_FORCED_RUN, "true") + else: + _update_skipped_elements_and_set_tags(test_module_span, test_session_span) + instance._dd_itr_skip = True + span.set_tag_str(test.ITR_SKIPPED, "true") + span.set_tag_str(test.SKIP_REASON, SKIPPED_BY_ITR_REASON) + + if _is_skipped_by_itr(instance): + result = args[0] + result.startTest(test=instance) + result.addSkip(test=instance, reason=SKIPPED_BY_ITR_REASON) + _set_test_span_status( + test_item=instance, skip_reason=SKIPPED_BY_ITR_REASON, status=test.Status.SKIP.value + ) + result.stopTest(test=instance) + else: + if _is_test_coverage_enabled(instance): + if not _module_has_dd_coverage_enabled(unittest, silent_mode=True): + unittest._dd_coverage = _start_coverage(root_directory) + _switch_coverage_context(unittest._dd_coverage, fqn_test) + result = func(*args, **kwargs) + _update_status_item(test_suite_span, span.get_tag(test.STATUS)) + if _is_test_coverage_enabled(instance): + _report_coverage_to_span(unittest._dd_coverage, span, root_directory) + + _update_remaining_suites_and_modules( + test_module_suite_path, test_module_path, test_module_span, test_suite_span + ) + return result + return func(*args, **kwargs) + + +def collect_text_test_runner_session(func, instance: unittest.TestSuite, args: tuple, kwargs: dict): + """ + Discovers test suites and tests for the current `unittest` `TextTestRunner` execution + """ + if not _is_valid_module_suite_call(func): + return func(*args, **kwargs) + _initialize_unittest_data() + if _is_invoked_by_text_test_runner(): + seen_suites = _CIVisibility._unittest_data["suites"] + seen_modules = _CIVisibility._unittest_data["modules"] + _populate_suites_and_modules(instance._tests, seen_suites, seen_modules) + + result = func(*args, **kwargs) + + return result + result = func(*args, **kwargs) + return result + + +def _start_test_session_span(instance) -> ddtrace.Span: + """ + Starts a test session span and sets the required tags for a `unittest` session instance. + """ + tracer = getattr(unittest, "_datadog_tracer", _CIVisibility._instance.tracer) + test_command = _extract_command_name_from_session(instance) + resource_name = _generate_session_resource(test_command) + test_session_span = tracer.trace( + SESSION_OPERATION_NAME, + service=_CIVisibility._instance._service, + span_type=SpanTypes.TEST, + resource=resource_name, + ) + test_session_span.set_tag_str(_EVENT_TYPE, _SESSION_TYPE) + test_session_span.set_tag_str(_SESSION_ID, str(test_session_span.span_id)) + + test_session_span.set_tag_str(COMPONENT, COMPONENT_VALUE) + test_session_span.set_tag_str(SPAN_KIND, KIND) + + test_session_span.set_tag_str(test.COMMAND, test_command) + test_session_span.set_tag_str(test.FRAMEWORK, FRAMEWORK) + test_session_span.set_tag_str(test.FRAMEWORK_VERSION, _get_runtime_and_os_metadata()[RUNTIME_VERSION]) + + test_session_span.set_tag_str(test.TEST_TYPE, SpanTypes.TEST) + test_session_span.set_tag_str( + test.ITR_TEST_CODE_COVERAGE_ENABLED, + "true" if _CIVisibility._instance._collect_coverage_enabled else "false", + ) + + _CIVisibility.set_test_session_name(test_command=test_command) + + if _CIVisibility.test_skipping_enabled(): + _set_test_skipping_tags_to_span(test_session_span) + else: + test_session_span.set_tag_str(test.ITR_TEST_SKIPPING_ENABLED, "false") + _store_module_identifier(instance) + if _is_coverage_invoked_by_coverage_run(): + patch_coverage() + return test_session_span + + +def _start_test_module_span(instance) -> ddtrace.Span: + """ + Starts a test module span and sets the required tags for a `unittest` module instance. + """ + tracer = getattr(unittest, "_datadog_tracer", _CIVisibility._instance.tracer) + test_session_span = _extract_session_span() + test_module_name = _extract_module_name_from_module(instance) + resource_name = _generate_module_resource(test_module_name) + test_module_span = tracer._start_span( + MODULE_OPERATION_NAME, + service=_CIVisibility._instance._service, + span_type=SpanTypes.TEST, + activate=True, + child_of=test_session_span, + resource=resource_name, + ) + test_module_span.set_tag_str(_EVENT_TYPE, _MODULE_TYPE) + test_module_span.set_tag_str(_SESSION_ID, str(test_session_span.span_id)) + test_module_span.set_tag_str(_MODULE_ID, str(test_module_span.span_id)) + + test_module_span.set_tag_str(COMPONENT, COMPONENT_VALUE) + test_module_span.set_tag_str(SPAN_KIND, KIND) + + test_module_span.set_tag_str(test.COMMAND, test_session_span.get_tag(test.COMMAND)) + test_module_span.set_tag_str(test.FRAMEWORK, FRAMEWORK) + test_module_span.set_tag_str(test.FRAMEWORK_VERSION, _get_runtime_and_os_metadata()[RUNTIME_VERSION]) + + test_module_span.set_tag_str(test.TEST_TYPE, SpanTypes.TEST) + test_module_span.set_tag_str(test.MODULE, test_module_name) + test_module_span.set_tag_str(test.MODULE_PATH, _extract_module_file_path(instance)) + test_module_span.set_tag_str( + test.ITR_TEST_CODE_COVERAGE_ENABLED, + "true" if _CIVisibility._instance._collect_coverage_enabled else "false", + ) + if _CIVisibility.test_skipping_enabled(): + _set_test_skipping_tags_to_span(test_module_span) + test_module_span.set_metric(test.ITR_TEST_SKIPPING_COUNT, 0) + else: + test_module_span.set_tag_str(test.ITR_TEST_SKIPPING_ENABLED, "false") + _store_suite_identifier(instance) + return test_module_span + + +def _start_test_suite_span(instance) -> ddtrace.Span: + """ + Starts a test suite span and sets the required tags for a `unittest` suite instance. + """ + tracer = getattr(unittest, "_datadog_tracer", _CIVisibility._instance.tracer) + test_module_path = _extract_module_file_path(instance) + test_module_span = _extract_module_span(test_module_path) + test_suite_name = _extract_suite_name_from_test_method(instance) + resource_name = _generate_suite_resource(test_suite_name) + test_suite_span = tracer._start_span( + SUITE_OPERATION_NAME, + service=_CIVisibility._instance._service, + span_type=SpanTypes.TEST, + child_of=test_module_span, + activate=True, + resource=resource_name, + ) + test_suite_span.set_tag_str(_EVENT_TYPE, _SUITE_TYPE) + test_suite_span.set_tag_str(_SESSION_ID, test_module_span.get_tag(_SESSION_ID)) + test_suite_span.set_tag_str(_SUITE_ID, str(test_suite_span.span_id)) + test_suite_span.set_tag_str(_MODULE_ID, str(test_module_span.span_id)) + + test_suite_span.set_tag_str(COMPONENT, COMPONENT_VALUE) + test_suite_span.set_tag_str(SPAN_KIND, KIND) + + test_suite_span.set_tag_str(test.COMMAND, test_module_span.get_tag(test.COMMAND)) + test_suite_span.set_tag_str(test.FRAMEWORK, FRAMEWORK) + test_suite_span.set_tag_str(test.FRAMEWORK_VERSION, _get_runtime_and_os_metadata()[RUNTIME_VERSION]) + + test_suite_span.set_tag_str(test.TEST_TYPE, SpanTypes.TEST) + test_suite_span.set_tag_str(test.SUITE, test_suite_name) + test_suite_span.set_tag_str(test.MODULE, test_module_span.get_tag(test.MODULE)) + test_suite_span.set_tag_str(test.MODULE_PATH, test_module_path) + return test_suite_span + + +def _start_test_span(instance, test_suite_span: ddtrace.Span) -> ddtrace.Span: + """ + Starts a test span and sets the required tags for a `unittest` test instance. + """ + tracer = getattr(unittest, "_datadog_tracer", _CIVisibility._instance.tracer) + test_name = _extract_test_method_name(instance) + test_method_object = _extract_test_method_object(instance) + test_suite_name = _extract_suite_name_from_test_method(instance) + resource_name = _generate_test_resource(test_suite_name, test_name) + span = tracer._start_span( + ddtrace.config.unittest.operation_name, + service=_CIVisibility._instance._service, + resource=resource_name, + span_type=SpanTypes.TEST, + child_of=test_suite_span, + activate=True, + ) + span.set_tag_str(_EVENT_TYPE, SpanTypes.TEST) + span.set_tag_str(_SESSION_ID, test_suite_span.get_tag(_SESSION_ID)) + span.set_tag_str(_MODULE_ID, test_suite_span.get_tag(_MODULE_ID)) + span.set_tag_str(_SUITE_ID, test_suite_span.get_tag(_SUITE_ID)) + + span.set_tag_str(COMPONENT, COMPONENT_VALUE) + span.set_tag_str(SPAN_KIND, KIND) + + span.set_tag_str(test.COMMAND, test_suite_span.get_tag(test.COMMAND)) + span.set_tag_str(test.FRAMEWORK, FRAMEWORK) + span.set_tag_str(test.FRAMEWORK_VERSION, _get_runtime_and_os_metadata()[RUNTIME_VERSION]) + + span.set_tag_str(test.TYPE, SpanTypes.TEST) + span.set_tag_str(test.NAME, test_name) + span.set_tag_str(test.SUITE, test_suite_name) + span.set_tag_str(test.MODULE, test_suite_span.get_tag(test.MODULE)) + span.set_tag_str(test.MODULE_PATH, test_suite_span.get_tag(test.MODULE_PATH)) + span.set_tag_str(test.STATUS, test.Status.FAIL.value) + span.set_tag_str(test.CLASS_HIERARCHY, test_suite_name) + + _CIVisibility.set_codeowners_of(_extract_test_file_name(instance), span=span) + + _add_start_end_source_file_path_data_to_span(span, test_method_object, test_name, os.getcwd()) + + _store_test_span(instance, span) + return span + + +def _finish_span(current_span: ddtrace.Span): + """ + Finishes active span and populates span status upwards + """ + current_status = current_span.get_tag(test.STATUS) + parent_span = current_span._parent + if current_status and parent_span: + _update_status_item(parent_span, current_status) + elif not current_status: + current_span.set_tag_str(test.SUITE, test.Status.FAIL.value) + current_span.finish() + + +def _finish_test_session_span(): + _finish_remaining_suites_and_modules( + _CIVisibility._unittest_data["suites"], _CIVisibility._unittest_data["modules"] + ) + _update_test_skipping_count_span(_CIVisibility._datadog_session_span) + if _CIVisibility._instance._collect_coverage_enabled and _module_has_dd_coverage_enabled(unittest): + _stop_coverage(unittest) + if _is_coverage_patched() and _is_coverage_invoked_by_coverage_run(): + run_coverage_report() + _add_pct_covered_to_span(_coverage_data, _CIVisibility._datadog_session_span) + unpatch_coverage() + _finish_span(_CIVisibility._datadog_session_span) + + +def handle_cli_run(func, instance: unittest.TestProgram, args: tuple, kwargs: dict): + """ + Creates session span and discovers test suites and tests for the current `unittest` CLI execution + """ + if _is_invoked_by_cli(instance): + _enable_unittest_if_not_started() + for parent_module in instance.test._tests: + for module in parent_module._tests: + _populate_suites_and_modules( + module, _CIVisibility._unittest_data["suites"], _CIVisibility._unittest_data["modules"] + ) + + test_session_span = _start_test_session_span(instance) + _CIVisibility._datadog_entry = "cli" + _CIVisibility._datadog_session_span = test_session_span + + try: + result = func(*args, **kwargs) + except SystemExit as e: + if _CIVisibility.enabled and _CIVisibility._datadog_session_span and hasattr(_CIVisibility, "_unittest_data"): + _finish_test_session_span() + + raise e + return result + + +def handle_text_test_runner_wrapper(func, instance: unittest.TextTestRunner, args: tuple, kwargs: dict): + """ + Creates session span if unittest is called through the `TextTestRunner` method + """ + if _is_invoked_by_cli(instance): + return func(*args, **kwargs) + _enable_unittest_if_not_started() + _CIVisibility._datadog_entry = "TextTestRunner" + if not hasattr(_CIVisibility, "_datadog_session_span"): + _CIVisibility._datadog_session_span = _start_test_session_span(instance) + _CIVisibility._datadog_expected_sessions = 0 + _CIVisibility._datadog_finished_sessions = 0 + _CIVisibility._datadog_expected_sessions += 1 + try: + result = func(*args, **kwargs) + except SystemExit as e: + _CIVisibility._datadog_finished_sessions += 1 + if _CIVisibility._datadog_finished_sessions == _CIVisibility._datadog_expected_sessions: + _finish_test_session_span() + del _CIVisibility._datadog_session_span + raise e + _CIVisibility._datadog_finished_sessions += 1 + if _CIVisibility._datadog_finished_sessions == _CIVisibility._datadog_expected_sessions: + _finish_test_session_span() + del _CIVisibility._datadog_session_span + return result diff --git a/ddtrace/contrib/pytest/__init__.py b/ddtrace/contrib/pytest/__init__.py index 30be6789602..0037949af50 100644 --- a/ddtrace/contrib/pytest/__init__.py +++ b/ddtrace/contrib/pytest/__init__.py @@ -60,27 +60,15 @@ Default: ``"pytest.test"`` """ +from ddtrace.contrib.internal.pytest.patch import get_version # noqa: F401 +from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning +from ddtrace.vendor.debtcollector import deprecate -import os -from ddtrace import config - - -# pytest default settings -config._add( - "pytest", - dict( - _default_service="pytest", - operation_name=os.getenv("DD_PYTEST_OPERATION_NAME", default="pytest.test"), - ), +deprecate( + ("%s is deprecated" % (__name__)), + message="Avoid using this package directly. " + "Use ``ddtrace.auto`` or the ``ddtrace-run`` command to enable and configure this integration.", + category=DDTraceDeprecationWarning, + removal_version="3.0.0", ) - - -def get_version(): - # type: () -> str - import pytest - - return pytest.__version__ - - -__all__ = ["get_version"] diff --git a/ddtrace/contrib/pytest/constants.py b/ddtrace/contrib/pytest/constants.py index cc5d768fc38..695c48e5b95 100644 --- a/ddtrace/contrib/pytest/constants.py +++ b/ddtrace/contrib/pytest/constants.py @@ -1,11 +1,14 @@ -FRAMEWORK = "pytest" -KIND = "test" +from ddtrace.contrib.internal.pytest.constants import * # noqa: F403 +from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning +from ddtrace.vendor.debtcollector import deprecate -# XFail Reason -XFAIL_REASON = "pytest.xfail.reason" +def __getattr__(name): + deprecate( + ("%s.%s is deprecated" % (__name__, name)), + category=DDTraceDeprecationWarning, + ) -ITR_MIN_SUPPORTED_VERSION = (7, 2, 0) -RETRIES_MIN_SUPPORTED_VERSION = (7, 0, 0) -EFD_MIN_SUPPORTED_VERSION = RETRIES_MIN_SUPPORTED_VERSION -ATR_MIN_SUPPORTED_VERSION = RETRIES_MIN_SUPPORTED_VERSION + if name in globals(): + return globals()[name] + raise AttributeError("%s has no attribute %s", __name__, name) diff --git a/ddtrace/contrib/pytest/newhooks.py b/ddtrace/contrib/pytest/newhooks.py index c44fd0a1535..b54e146fde9 100644 --- a/ddtrace/contrib/pytest/newhooks.py +++ b/ddtrace/contrib/pytest/newhooks.py @@ -1,26 +1,14 @@ -"""pytest-ddtrace hooks. +from ddtrace.contrib.internal.pytest.newhooks import * # noqa: F403 +from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning +from ddtrace.vendor.debtcollector import deprecate -These hooks are used to provide extra data used by the Datadog CI Visibility plugin. -For example: module, suite, and test names for a given item. +def __getattr__(name): + deprecate( + ("%s.%s is deprecated" % (__name__, name)), + category=DDTraceDeprecationWarning, + ) -Note that these names will affect th display and reporting of tests in the Datadog UI, as well as information stored -the Intelligent Test Runner. Differing hook implementations may impact the behavior of Datadog CI Visibility products. -""" - -import pytest - - -@pytest.hookspec(firstresult=True) -def pytest_ddtrace_get_item_module_name(item: pytest.Item) -> str: - """Returns the module name to use when reporting CI Visibility results, should be unique""" - - -@pytest.hookspec(firstresult=True) -def pytest_ddtrace_get_item_suite_name(item: pytest.Item) -> str: - """Returns the suite name to use when reporting CI Visibility result, should be unique""" - - -@pytest.hookspec(firstresult=True) -def pytest_ddtrace_get_item_test_name(item: pytest.Item) -> str: - """Returns the test name to use when reporting CI Visibility result, should be unique""" + if name in globals(): + return globals()[name] + raise AttributeError("%s has no attribute %s", __name__, name) diff --git a/ddtrace/contrib/pytest/plugin.py b/ddtrace/contrib/pytest/plugin.py index a09a81be49a..05002fc74d4 100644 --- a/ddtrace/contrib/pytest/plugin.py +++ b/ddtrace/contrib/pytest/plugin.py @@ -1,166 +1,14 @@ -""" -This custom pytest plugin implements tracing for pytest by using pytest hooks. The plugin registers tracing code -to be run at specific points during pytest execution. The most important hooks used are: +from ddtrace.contrib.internal.pytest.plugin import * # noqa: F403 +from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning +from ddtrace.vendor.debtcollector import deprecate - * pytest_sessionstart: during pytest session startup, a custom trace filter is configured to the global tracer to - only send test spans, which are generated by the plugin. - * pytest_runtest_protocol: this wraps around the execution of a pytest test function, which we trace. Most span - tags are generated and added in this function. We also store the span on the underlying pytest test item to - retrieve later when we need to report test status/result. - * pytest_runtest_makereport: this hook is used to set the test status/result tag, including skipped tests and - expected failures. -""" -from typing import Dict # noqa:F401 - -import pytest - -from ddtrace.appsec._iast._pytest_plugin import ddtrace_iast # noqa:F401 -from ddtrace.appsec._iast._utils import _is_iast_enabled -from ddtrace.contrib.pytest._utils import _USE_PLUGIN_V2 -from ddtrace.contrib.pytest._utils import _extract_span -from ddtrace.contrib.pytest._utils import _pytest_version_supports_itr - - -DDTRACE_HELP_MSG = "Enable tracing of pytest functions." -NO_DDTRACE_HELP_MSG = "Disable tracing of pytest functions." -DDTRACE_INCLUDE_CLASS_HELP_MSG = "Prepend 'ClassName.' to names of class-based tests." -PATCH_ALL_HELP_MSG = "Call ddtrace.patch_all before running tests." - - -def is_enabled(config): - """Check if the ddtrace plugin is enabled.""" - return (config.getoption("ddtrace") or config.getini("ddtrace")) and not config.getoption("no-ddtrace") - - -def pytest_addoption(parser): - """Add ddtrace options.""" - group = parser.getgroup("ddtrace") - - group._addoption( - "--ddtrace", - action="store_true", - dest="ddtrace", - default=False, - help=DDTRACE_HELP_MSG, +def __getattr__(name): + deprecate( + ("%s.%s is deprecated" % (__name__, name)), + category=DDTraceDeprecationWarning, ) - group._addoption( - "--no-ddtrace", - action="store_true", - dest="no-ddtrace", - default=False, - help=NO_DDTRACE_HELP_MSG, - ) - - group._addoption( - "--ddtrace-patch-all", - action="store_true", - dest="ddtrace-patch-all", - default=False, - help=PATCH_ALL_HELP_MSG, - ) - - group._addoption( - "--ddtrace-include-class-name", - action="store_true", - dest="ddtrace-include-class-name", - default=False, - help=DDTRACE_INCLUDE_CLASS_HELP_MSG, - ) - - group._addoption( - "--ddtrace-iast-fail-tests", - action="store_true", - dest="ddtrace-iast-fail-tests", - default=False, - help=DDTRACE_INCLUDE_CLASS_HELP_MSG, - ) - - parser.addini("ddtrace", DDTRACE_HELP_MSG, type="bool") - parser.addini("no-ddtrace", DDTRACE_HELP_MSG, type="bool") - parser.addini("ddtrace-patch-all", PATCH_ALL_HELP_MSG, type="bool") - parser.addini("ddtrace-include-class-name", DDTRACE_INCLUDE_CLASS_HELP_MSG, type="bool") - if _is_iast_enabled(): - from ddtrace.appsec._iast import _iast_pytest_activation - - _iast_pytest_activation() - - -# Version-specific pytest hooks -if _USE_PLUGIN_V2: - from ddtrace.contrib.pytest._plugin_v2 import pytest_collection_finish # noqa: F401 - from ddtrace.contrib.pytest._plugin_v2 import pytest_configure as _versioned_pytest_configure - from ddtrace.contrib.pytest._plugin_v2 import pytest_ddtrace_get_item_module_name # noqa: F401 - from ddtrace.contrib.pytest._plugin_v2 import pytest_ddtrace_get_item_suite_name # noqa: F401 - from ddtrace.contrib.pytest._plugin_v2 import pytest_ddtrace_get_item_test_name # noqa: F401 - from ddtrace.contrib.pytest._plugin_v2 import pytest_load_initial_conftests # noqa: F401 - from ddtrace.contrib.pytest._plugin_v2 import pytest_report_teststatus # noqa: F401 - from ddtrace.contrib.pytest._plugin_v2 import pytest_runtest_makereport # noqa: F401 - from ddtrace.contrib.pytest._plugin_v2 import pytest_runtest_protocol # noqa: F401 - from ddtrace.contrib.pytest._plugin_v2 import pytest_sessionfinish # noqa: F401 - from ddtrace.contrib.pytest._plugin_v2 import pytest_sessionstart # noqa: F401 - from ddtrace.contrib.pytest._plugin_v2 import pytest_terminal_summary # noqa: F401 -else: - from ddtrace.contrib.pytest._plugin_v1 import pytest_collection_modifyitems # noqa: F401 - from ddtrace.contrib.pytest._plugin_v1 import pytest_configure as _versioned_pytest_configure - from ddtrace.contrib.pytest._plugin_v1 import pytest_ddtrace_get_item_module_name # noqa: F401 - from ddtrace.contrib.pytest._plugin_v1 import pytest_ddtrace_get_item_suite_name # noqa: F401 - from ddtrace.contrib.pytest._plugin_v1 import pytest_ddtrace_get_item_test_name # noqa: F401 - from ddtrace.contrib.pytest._plugin_v1 import pytest_load_initial_conftests # noqa: F401 - from ddtrace.contrib.pytest._plugin_v1 import pytest_runtest_makereport # noqa: F401 - from ddtrace.contrib.pytest._plugin_v1 import pytest_runtest_protocol # noqa: F401 - from ddtrace.contrib.pytest._plugin_v1 import pytest_sessionfinish # noqa: F401 - from ddtrace.contrib.pytest._plugin_v1 import pytest_sessionstart # noqa: F401 - - # Internal coverage is only used for ITR at the moment, so the hook is only added if the pytest version supports it - if _pytest_version_supports_itr(): - from ddtrace.contrib.pytest._plugin_v1 import pytest_terminal_summary # noqa: F401 - - -def pytest_configure(config): - config.addinivalue_line("markers", "dd_tags(**kwargs): add tags to current span") - if is_enabled(config): - _versioned_pytest_configure(config) - - -@pytest.hookimpl -def pytest_addhooks(pluginmanager): - from ddtrace.contrib.pytest import newhooks - - pluginmanager.add_hookspecs(newhooks) - - -@pytest.fixture(scope="function") -def ddspan(request): - """Return the :class:`ddtrace._trace.span.Span` instance associated with the - current test when Datadog CI Visibility is enabled. - """ - from ddtrace.internal.ci_visibility import CIVisibility as _CIVisibility - - if _CIVisibility.enabled: - return _extract_span(request.node) - - -@pytest.fixture(scope="session") -def ddtracer(): - """Return the :class:`ddtrace.tracer.Tracer` instance for Datadog CI - visibility if it is enabled, otherwise return the default Datadog tracer. - """ - import ddtrace - from ddtrace.internal.ci_visibility import CIVisibility as _CIVisibility - - if _CIVisibility.enabled: - return _CIVisibility._instance.tracer - return ddtrace.tracer - - -@pytest.fixture(scope="session", autouse=True) -def patch_all(request): - """Patch all available modules for Datadog tracing when ddtrace-patch-all - is specified in command or .ini. - """ - import ddtrace - - if request.config.getoption("ddtrace-patch-all") or request.config.getini("ddtrace-patch-all"): - ddtrace.patch_all() + if name in globals(): + return globals()[name] + raise AttributeError("%s has no attribute %s", __name__, name) diff --git a/ddtrace/contrib/pytest_bdd/__init__.py b/ddtrace/contrib/pytest_bdd/__init__.py index b1cc6701fda..2e91392914d 100644 --- a/ddtrace/contrib/pytest_bdd/__init__.py +++ b/ddtrace/contrib/pytest_bdd/__init__.py @@ -21,27 +21,18 @@ for more details. """ +from ddtrace.contrib.internal.pytest_bdd.patch import get_version +from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning +from ddtrace.vendor.debtcollector import deprecate -from ddtrace import config - -# pytest-bdd default settings -config._add( - "pytest_bdd", - dict( - _default_service="pytest_bdd", - ), +deprecate( + ("%s is deprecated" % (__name__)), + message="Avoid using this package directly. " + "Use ``ddtrace.auto`` or the ``ddtrace-run`` command to enable and configure this integration.", + category=DDTraceDeprecationWarning, + removal_version="3.0.0", ) -def get_version(): - # type: () -> str - try: - import importlib.metadata as importlib_metadata - except ImportError: - import importlib_metadata # type: ignore[no-redef] - - return str(importlib_metadata.version("pytest-bdd")) - - __all__ = ["get_version"] diff --git a/ddtrace/contrib/pytest_bdd/constants.py b/ddtrace/contrib/pytest_bdd/constants.py index 2dd377f7619..9c2e907debd 100644 --- a/ddtrace/contrib/pytest_bdd/constants.py +++ b/ddtrace/contrib/pytest_bdd/constants.py @@ -1,2 +1,14 @@ -FRAMEWORK = "pytest_bdd" -STEP_KIND = "pytest_bdd.step" +from ddtrace.contrib.internal.pytest_bdd.constants import * # noqa: F403 +from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning +from ddtrace.vendor.debtcollector import deprecate + + +def __getattr__(name): + deprecate( + ("%s.%s is deprecated" % (__name__, name)), + category=DDTraceDeprecationWarning, + ) + + if name in globals(): + return globals()[name] + raise AttributeError("%s has no attribute %s", __name__, name) diff --git a/ddtrace/contrib/pytest_bdd/plugin.py b/ddtrace/contrib/pytest_bdd/plugin.py index 1dc714c89c5..88645368d38 100644 --- a/ddtrace/contrib/pytest_bdd/plugin.py +++ b/ddtrace/contrib/pytest_bdd/plugin.py @@ -1,20 +1,14 @@ -from ddtrace import DDTraceDeprecationWarning -from ddtrace.contrib.pytest._utils import _USE_PLUGIN_V2 -from ddtrace.contrib.pytest.plugin import is_enabled as is_ddtrace_enabled +from ddtrace.contrib.internal.pytest_bdd.plugin import * # noqa: F403 +from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning from ddtrace.vendor.debtcollector import deprecate -def pytest_configure(config): - if config.pluginmanager.hasplugin("pytest-bdd") and config.pluginmanager.hasplugin("ddtrace"): - if not _USE_PLUGIN_V2: - if is_ddtrace_enabled(config): - from ._plugin import _PytestBddPlugin +def __getattr__(name): + deprecate( + ("%s.%s is deprecated" % (__name__, name)), + category=DDTraceDeprecationWarning, + ) - deprecate( - "the ddtrace.pytest_bdd plugin is deprecated", - message="it will be integrated with the main pytest ddtrace plugin", - removal_version="3.0.0", - category=DDTraceDeprecationWarning, - ) - - config.pluginmanager.register(_PytestBddPlugin(), "_datadog-pytest-bdd") + if name in globals(): + return globals()[name] + raise AttributeError("%s has no attribute %s", __name__, name) diff --git a/ddtrace/contrib/pytest_benchmark/__init__.py b/ddtrace/contrib/pytest_benchmark/__init__.py index e69de29bb2d..3829deeb38a 100644 --- a/ddtrace/contrib/pytest_benchmark/__init__.py +++ b/ddtrace/contrib/pytest_benchmark/__init__.py @@ -0,0 +1,11 @@ +from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning +from ddtrace.vendor.debtcollector import deprecate + + +deprecate( + ("%s is deprecated" % (__name__)), + message="Avoid using this package directly. " + "Use ``ddtrace.auto`` or the ``ddtrace-run`` command to enable and configure this integration.", + category=DDTraceDeprecationWarning, + removal_version="3.0.0", +) diff --git a/ddtrace/contrib/pytest_benchmark/constants.py b/ddtrace/contrib/pytest_benchmark/constants.py index b4c4f7f5b27..522f664d4b8 100644 --- a/ddtrace/contrib/pytest_benchmark/constants.py +++ b/ddtrace/contrib/pytest_benchmark/constants.py @@ -1,79 +1,14 @@ -BENCHMARK_INFO = "benchmark.duration.info" -BENCHMARK_MEAN = "benchmark.duration.mean" -BENCHMARK_RUN = "benchmark.duration.runs" +from ddtrace.contrib.internal.pytest_benchmark.constants import * # noqa: F403 +from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning +from ddtrace.vendor.debtcollector import deprecate -STATISTICS_HD15IQR = "benchmark.duration.statistics.hd15iqr" -STATISTICS_IQR = "benchmark.duration.statistics.iqr" -STATISTICS_IQR_OUTLIERS = "benchmark.duration.statistics.iqr_outliers" -STATISTICS_LD15IQR = "benchmark.duration.statistics.ld15iqr" -STATISTICS_MAX = "benchmark.duration.statistics.max" -STATISTICS_MEAN = "benchmark.duration.statistics.mean" -STATISTICS_MEDIAN = "benchmark.duration.statistics.median" -STATISTICS_MIN = "benchmark.duration.statistics.min" -STATISTICS_N = "benchmark.duration.statistics.n" -STATISTICS_OPS = "benchmark.duration.statistics.ops" -STATISTICS_OUTLIERS = "benchmark.duration.statistics.outliers" -STATISTICS_Q1 = "benchmark.duration.statistics.q1" -STATISTICS_Q3 = "benchmark.duration.statistics.q3" -STATISTICS_STDDEV = "benchmark.duration.statistics.std_dev" -STATISTICS_STDDEV_OUTLIERS = "benchmark.duration.statistics.std_dev_outliers" -STATISTICS_TOTAL = "benchmark.duration.statistics.total" -PLUGIN_HD15IQR = "hd15iqr" -PLUGIN_IQR = "iqr" -PLUGIN_IQR_OUTLIERS = "iqr_outliers" -PLUGIN_LD15IQR = "ld15iqr" -PLUGIN_MAX = "max" -PLUGIN_MEAN = "mean" -PLUGIN_MEDIAN = "median" -PLUGIN_MIN = "min" -PLUGIN_OPS = "ops" -PLUGIN_OUTLIERS = "outliers" -PLUGIN_Q1 = "q1" -PLUGIN_Q3 = "q3" -PLUGIN_ROUNDS = "rounds" -PLUGIN_STDDEV = "stddev" -PLUGIN_STDDEV_OUTLIERS = "stddev_outliers" -PLUGIN_TOTAL = "total" +def __getattr__(name): + deprecate( + ("%s.%s is deprecated" % (__name__, name)), + category=DDTraceDeprecationWarning, + ) -PLUGIN_METRICS = { - BENCHMARK_MEAN: PLUGIN_MEAN, - BENCHMARK_RUN: PLUGIN_ROUNDS, - STATISTICS_HD15IQR: PLUGIN_HD15IQR, - STATISTICS_IQR: PLUGIN_IQR, - STATISTICS_IQR_OUTLIERS: PLUGIN_IQR_OUTLIERS, - STATISTICS_LD15IQR: PLUGIN_LD15IQR, - STATISTICS_MAX: PLUGIN_MAX, - STATISTICS_MEAN: PLUGIN_MEAN, - STATISTICS_MEDIAN: PLUGIN_MEDIAN, - STATISTICS_MIN: PLUGIN_MIN, - STATISTICS_OPS: PLUGIN_OPS, - STATISTICS_OUTLIERS: PLUGIN_OUTLIERS, - STATISTICS_Q1: PLUGIN_Q1, - STATISTICS_Q3: PLUGIN_Q3, - STATISTICS_N: PLUGIN_ROUNDS, - STATISTICS_STDDEV: PLUGIN_STDDEV, - STATISTICS_STDDEV_OUTLIERS: PLUGIN_STDDEV_OUTLIERS, - STATISTICS_TOTAL: PLUGIN_TOTAL, -} - -PLUGIN_METRICS_V2 = { - "duration_mean": PLUGIN_MEAN, - "duration_runs": PLUGIN_ROUNDS, - "statistics_hd15iqr": PLUGIN_HD15IQR, - "statistics_iqr": PLUGIN_IQR, - "statistics_iqr_outliers": PLUGIN_IQR_OUTLIERS, - "statistics_ld15iqr": PLUGIN_LD15IQR, - "statistics_max": PLUGIN_MAX, - "statistics_mean": PLUGIN_MEAN, - "statistics_median": PLUGIN_MEDIAN, - "statistics_min": PLUGIN_MIN, - "statistics_n": PLUGIN_ROUNDS, - "statistics_ops": PLUGIN_OPS, - "statistics_outliers": PLUGIN_OUTLIERS, - "statistics_q1": PLUGIN_Q1, - "statistics_q3": PLUGIN_Q3, - "statistics_std_dev": PLUGIN_STDDEV, - "statistics_std_dev_outliers": PLUGIN_STDDEV_OUTLIERS, - "statistics_total": PLUGIN_TOTAL, -} + if name in globals(): + return globals()[name] + raise AttributeError("%s has no attribute %s", __name__, name) diff --git a/ddtrace/contrib/pytest_benchmark/plugin.py b/ddtrace/contrib/pytest_benchmark/plugin.py index 4cb76148dbc..7a33bbf838d 100644 --- a/ddtrace/contrib/pytest_benchmark/plugin.py +++ b/ddtrace/contrib/pytest_benchmark/plugin.py @@ -1,19 +1,14 @@ -from ddtrace import DDTraceDeprecationWarning -from ddtrace.contrib.pytest._utils import _USE_PLUGIN_V2 -from ddtrace.contrib.pytest.plugin import is_enabled as is_ddtrace_enabled +from ddtrace.contrib.internal.pytest_benchmark.plugin import * # noqa: F403 +from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning from ddtrace.vendor.debtcollector import deprecate -def pytest_configure(config): - if config.pluginmanager.hasplugin("benchmark") and config.pluginmanager.hasplugin("ddtrace"): - if is_ddtrace_enabled(config): - deprecate( - "this version of the ddtrace.pytest_benchmark plugin is deprecated", - message="it will be integrated with the main pytest ddtrace plugin", - removal_version="3.0.0", - category=DDTraceDeprecationWarning, - ) - if not _USE_PLUGIN_V2: - from ._plugin import _PytestBenchmarkPlugin +def __getattr__(name): + deprecate( + ("%s.%s is deprecated" % (__name__, name)), + category=DDTraceDeprecationWarning, + ) - config.pluginmanager.register(_PytestBenchmarkPlugin(), "_datadog-pytest-benchmark") + if name in globals(): + return globals()[name] + raise AttributeError("%s has no attribute %s", __name__, name) diff --git a/ddtrace/contrib/unittest/__init__.py b/ddtrace/contrib/unittest/__init__.py index 5180b59c959..43a1e8a740c 100644 --- a/ddtrace/contrib/unittest/__init__.py +++ b/ddtrace/contrib/unittest/__init__.py @@ -34,11 +34,18 @@ Default: ``True`` """ +from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning +from ddtrace.vendor.debtcollector import deprecate +from ..internal.unittest.patch import get_version # noqa: F401 +from ..internal.unittest.patch import patch # noqa: F401 +from ..internal.unittest.patch import unpatch # noqa: F401 -from .patch import get_version -from .patch import patch -from .patch import unpatch - -__all__ = ["patch", "unpatch", "get_version"] +deprecate( + ("%s is deprecated" % (__name__)), + message="Avoid using this package directly. " + "Use ``ddtrace.auto`` or the ``ddtrace-run`` command to enable and configure this integration.", + category=DDTraceDeprecationWarning, + removal_version="3.0.0", +) diff --git a/ddtrace/contrib/unittest/constants.py b/ddtrace/contrib/unittest/constants.py index dc58863a2a5..fc8643d5e06 100644 --- a/ddtrace/contrib/unittest/constants.py +++ b/ddtrace/contrib/unittest/constants.py @@ -1,8 +1,14 @@ -COMPONENT_VALUE = "unittest" -FRAMEWORK = "unittest" -KIND = "test" +from ddtrace.contrib.internal.unittest.constants import * # noqa: F403 +from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning +from ddtrace.vendor.debtcollector import deprecate -TEST_OPERATION_NAME = "unittest.test" -SUITE_OPERATION_NAME = "unittest.test_suite" -SESSION_OPERATION_NAME = "unittest.test_session" -MODULE_OPERATION_NAME = "unittest.test_module" + +def __getattr__(name): + deprecate( + ("%s.%s is deprecated" % (__name__, name)), + category=DDTraceDeprecationWarning, + ) + + if name in globals(): + return globals()[name] + raise AttributeError("%s has no attribute %s", __name__, name) diff --git a/ddtrace/contrib/unittest/patch.py b/ddtrace/contrib/unittest/patch.py index 2c8bdd299a6..277b3b421c6 100644 --- a/ddtrace/contrib/unittest/patch.py +++ b/ddtrace/contrib/unittest/patch.py @@ -1,868 +1,14 @@ -import inspect -import os -from typing import Union -import unittest +from ddtrace.contrib.internal.unittest.patch import * # noqa: F403 +from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning +from ddtrace.vendor.debtcollector import deprecate -import wrapt -import ddtrace -from ddtrace import config -from ddtrace.constants import SPAN_KIND -from ddtrace.contrib.internal.coverage.data import _coverage_data -from ddtrace.contrib.internal.coverage.patch import patch as patch_coverage -from ddtrace.contrib.internal.coverage.patch import run_coverage_report -from ddtrace.contrib.internal.coverage.patch import unpatch as unpatch_coverage -from ddtrace.contrib.internal.coverage.utils import _is_coverage_invoked_by_coverage_run -from ddtrace.contrib.internal.coverage.utils import _is_coverage_patched -from ddtrace.contrib.unittest.constants import COMPONENT_VALUE -from ddtrace.contrib.unittest.constants import FRAMEWORK -from ddtrace.contrib.unittest.constants import KIND -from ddtrace.contrib.unittest.constants import MODULE_OPERATION_NAME -from ddtrace.contrib.unittest.constants import SESSION_OPERATION_NAME -from ddtrace.contrib.unittest.constants import SUITE_OPERATION_NAME -from ddtrace.ext import SpanTypes -from ddtrace.ext import test -from ddtrace.ext.ci import RUNTIME_VERSION -from ddtrace.ext.ci import _get_runtime_and_os_metadata -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 -from ddtrace.internal.ci_visibility.constants import ITR_UNSKIPPABLE_REASON -from ddtrace.internal.ci_visibility.constants import MODULE_ID as _MODULE_ID -from ddtrace.internal.ci_visibility.constants import MODULE_TYPE as _MODULE_TYPE -from ddtrace.internal.ci_visibility.constants import SESSION_ID as _SESSION_ID -from ddtrace.internal.ci_visibility.constants import SESSION_TYPE as _SESSION_TYPE -from ddtrace.internal.ci_visibility.constants import SKIPPED_BY_ITR_REASON -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 _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 -from ddtrace.internal.ci_visibility.coverage import _stop_coverage -from ddtrace.internal.ci_visibility.coverage import _switch_coverage_context -from ddtrace.internal.ci_visibility.utils import _add_pct_covered_to_span -from ddtrace.internal.ci_visibility.utils import _add_start_end_source_file_path_data_to_span -from ddtrace.internal.ci_visibility.utils import _generate_fully_qualified_test_name -from ddtrace.internal.ci_visibility.utils import get_relative_or_absolute_path_for_path -from ddtrace.internal.constants import COMPONENT -from ddtrace.internal.logger import get_logger -from ddtrace.internal.utils.formats import asbool -from ddtrace.internal.utils.wrappers import unwrap as _u - - -log = get_logger(__name__) -_global_skipped_elements = 0 - -# unittest default settings -config._add( - "unittest", - dict( - _default_service="unittest", - operation_name=os.getenv("DD_UNITTEST_OPERATION_NAME", default="unittest.test"), - strict_naming=asbool(os.getenv("DD_CIVISIBILITY_UNITTEST_STRICT_NAMING", default=True)), - ), -) - - -def get_version(): - # type: () -> str - return "" - - -def _enable_unittest_if_not_started(): - _initialize_unittest_data() - if _CIVisibility.enabled: - return - _CIVisibility.enable(config=ddtrace.config.unittest) - - -def _initialize_unittest_data(): - if not hasattr(_CIVisibility, "_unittest_data"): - _CIVisibility._unittest_data = {} - if "suites" not in _CIVisibility._unittest_data: - _CIVisibility._unittest_data["suites"] = {} - if "modules" not in _CIVisibility._unittest_data: - _CIVisibility._unittest_data["modules"] = {} - if "unskippable_tests" not in _CIVisibility._unittest_data: - _CIVisibility._unittest_data["unskippable_tests"] = set() - - -def _set_tracer(tracer: ddtrace.tracer): - """Manually sets the tracer instance to `unittest.`""" - unittest._datadog_tracer = tracer - - -def _is_test_coverage_enabled(test_object) -> bool: - return _CIVisibility._instance._collect_coverage_enabled and not _is_skipped_test(test_object) - - -def _is_skipped_test(test_object) -> bool: - testMethod = getattr(test_object, test_object._testMethodName, "") - return ( - (hasattr(test_object.__class__, "__unittest_skip__") and test_object.__class__.__unittest_skip__) - or (hasattr(testMethod, "__unittest_skip__") and testMethod.__unittest_skip__) - or _is_skipped_by_itr(test_object) - ) - - -def _is_skipped_by_itr(test_object) -> bool: - return hasattr(test_object, "_dd_itr_skip") and test_object._dd_itr_skip - - -def _should_be_skipped_by_itr(args: tuple, test_module_suite_path: str, test_name: str, test_object) -> bool: - return ( - len(args) - and _CIVisibility._instance._should_skip_path(test_module_suite_path, test_name) - and not _is_skipped_test(test_object) - ) - - -def _is_marked_as_unskippable(test_object) -> bool: - test_suite_name = _extract_suite_name_from_test_method(test_object) - test_name = _extract_test_method_name(test_object) - test_module_path = _extract_module_file_path(test_object) - test_module_suite_name = _generate_fully_qualified_test_name(test_module_path, test_suite_name, test_name) - return ( - hasattr(_CIVisibility, "_unittest_data") - and test_module_suite_name in _CIVisibility._unittest_data["unskippable_tests"] - ) - - -def _update_skipped_elements_and_set_tags(test_module_span: ddtrace.Span, test_session_span: ddtrace.Span): - global _global_skipped_elements - _global_skipped_elements += 1 - - test_module_span._metrics[test.ITR_TEST_SKIPPING_COUNT] += 1 - test_module_span.set_tag_str(test.ITR_TEST_SKIPPING_TESTS_SKIPPED, "true") - test_module_span.set_tag_str(test.ITR_DD_CI_ITR_TESTS_SKIPPED, "true") - - test_session_span.set_tag_str(test.ITR_TEST_SKIPPING_TESTS_SKIPPED, "true") - test_session_span.set_tag_str(test.ITR_DD_CI_ITR_TESTS_SKIPPED, "true") - - -def _store_test_span(item, span: ddtrace.Span): - """Store datadog span at `unittest` test instance.""" - item._datadog_span = span - - -def _store_module_identifier(test_object: unittest.TextTestRunner): - """Store module identifier at `unittest` module instance, this is useful to classify event types.""" - if hasattr(test_object, "test") and hasattr(test_object.test, "_tests"): - for module in test_object.test._tests: - if len(module._tests) and _extract_module_name_from_module(module): - _set_identifier(module, "module") - - -def _store_suite_identifier(module): - """Store suite identifier at `unittest` suite instance, this is useful to classify event types.""" - if hasattr(module, "_tests"): - for suite in module._tests: - if len(suite._tests) and _extract_module_name_from_module(suite): - _set_identifier(suite, "suite") - - -def _is_test(item) -> bool: - if ( - type(item) == unittest.TestSuite - or not hasattr(item, "_testMethodName") - or (ddtrace.config.unittest.strict_naming and not item._testMethodName.startswith("test")) - ): - return False - return True - - -def _extract_span(item) -> Union[ddtrace.Span, None]: - return getattr(item, "_datadog_span", None) - - -def _extract_command_name_from_session(session: unittest.TextTestRunner) -> str: - if not hasattr(session, "progName"): - return "python -m unittest" - return getattr(session, "progName", "") - - -def _extract_test_method_name(test_object) -> str: - """Extract test method name from `unittest` instance.""" - return getattr(test_object, "_testMethodName", "") - - -def _extract_session_span() -> Union[ddtrace.Span, None]: - return getattr(_CIVisibility, "_datadog_session_span", None) - - -def _extract_module_span(module_identifier: str) -> Union[ddtrace.Span, None]: - if hasattr(_CIVisibility, "_unittest_data") and module_identifier in _CIVisibility._unittest_data["modules"]: - return _CIVisibility._unittest_data["modules"][module_identifier].get("module_span") - return None - - -def _extract_suite_span(suite_identifier: str) -> Union[ddtrace.Span, None]: - if hasattr(_CIVisibility, "_unittest_data") and suite_identifier in _CIVisibility._unittest_data["suites"]: - return _CIVisibility._unittest_data["suites"][suite_identifier].get("suite_span") - return None - - -def _update_status_item(item: ddtrace.Span, status: str): - """ - Sets the status for each Span implementing the test FAIL logic override. - """ - existing_status = item.get_tag(test.STATUS) - if existing_status and (status == test.Status.SKIP.value or existing_status == test.Status.FAIL.value): - return None - item.set_tag_str(test.STATUS, status) - return None - - -def _extract_suite_name_from_test_method(item) -> str: - item_type = type(item) - return getattr(item_type, "__name__", "") - - -def _extract_module_name_from_module(item) -> str: - if _is_test(item): - return type(item).__module__ - return "" - - -def _extract_test_reason(item: tuple) -> str: - """ - Given a tuple of type [test_class, str], it returns the test failure/skip reason - """ - return item[1] - - -def _extract_test_file_name(item) -> str: - return os.path.basename(inspect.getfile(item.__class__)) - - -def _extract_module_file_path(item) -> str: - if _is_test(item): - try: - test_module_object = inspect.getfile(item.__class__) - except TypeError: - log.debug( - "Tried to collect module file path but it is a built-in Python function", - ) - return "" - return get_relative_or_absolute_path_for_path(test_module_object, os.getcwd()) - - return "" - - -def _generate_test_resource(suite_name: str, test_name: str) -> str: - return "{}.{}".format(suite_name, test_name) - - -def _generate_suite_resource(test_suite: str) -> str: - return "{}".format(test_suite) - - -def _generate_module_resource(test_module: str) -> str: - return "{}".format(test_module) - - -def _generate_session_resource(test_command: str) -> str: - return "{}".format(test_command) - - -def _set_test_skipping_tags_to_span(span: ddtrace.Span): - span.set_tag_str(test.ITR_TEST_SKIPPING_ENABLED, "true") - span.set_tag_str(test.ITR_TEST_SKIPPING_TYPE, TEST) - span.set_tag_str(test.ITR_TEST_SKIPPING_TESTS_SKIPPED, "false") - span.set_tag_str(test.ITR_DD_CI_ITR_TESTS_SKIPPED, "false") - span.set_tag_str(test.ITR_FORCED_RUN, "false") - span.set_tag_str(test.ITR_UNSKIPPABLE, "false") - - -def _set_identifier(item, name: str): - """ - Adds an event type classification to a `unittest` test. - """ - item._datadog_object = name - - -def _is_valid_result(instance: unittest.TextTestRunner, args: tuple) -> bool: - return instance and isinstance(instance, unittest.runner.TextTestResult) and args - - -def _is_valid_test_call(kwargs: dict) -> bool: - """ - Validates that kwargs is empty to ensure that `unittest` is running a test - """ - return not len(kwargs) - - -def _is_valid_module_suite_call(func) -> bool: - """ - Validates that the mocked function is an actual function from `unittest` - """ - return type(func).__name__ == "method" or type(func).__name__ == "instancemethod" - - -def _is_invoked_by_cli(instance: unittest.TextTestRunner) -> bool: - return ( - hasattr(instance, "progName") - or hasattr(_CIVisibility, "_datadog_entry") - and _CIVisibility._datadog_entry == "cli" +def __getattr__(name): + deprecate( + ("%s.%s is deprecated" % (__name__, name)), + category=DDTraceDeprecationWarning, ) - -def _extract_test_method_object(test_object): - if hasattr(test_object, "_testMethodName"): - return getattr(test_object, test_object._testMethodName, None) - return None - - -def _is_invoked_by_text_test_runner() -> bool: - return hasattr(_CIVisibility, "_datadog_entry") and _CIVisibility._datadog_entry == "TextTestRunner" - - -def _generate_module_suite_path(test_module_path: str, test_suite_name: str) -> str: - return "{}.{}".format(test_module_path, test_suite_name) - - -def _populate_suites_and_modules(test_objects: list, seen_suites: dict, seen_modules: dict): - """ - Discovers suites and modules and initializes the seen_suites and seen_modules dictionaries. - """ - if not hasattr(test_objects, "__iter__"): - return - for test_object in test_objects: - if not _is_test(test_object): - _populate_suites_and_modules(test_object, seen_suites, seen_modules) - continue - test_module_path = _extract_module_file_path(test_object) - test_suite_name = _extract_suite_name_from_test_method(test_object) - test_module_suite_path = _generate_module_suite_path(test_module_path, test_suite_name) - if test_module_path not in seen_modules: - seen_modules[test_module_path] = { - "module_span": None, - "remaining_suites": 0, - } - if test_module_suite_path not in seen_suites: - seen_suites[test_module_suite_path] = { - "suite_span": None, - "remaining_tests": 0, - } - - seen_modules[test_module_path]["remaining_suites"] += 1 - - seen_suites[test_module_suite_path]["remaining_tests"] += 1 - - -def _finish_remaining_suites_and_modules(seen_suites: dict, seen_modules: dict): - """ - Forces all suite and module spans to finish and updates their statuses. - """ - for suite in seen_suites.values(): - test_suite_span = suite["suite_span"] - if test_suite_span and not test_suite_span.finished: - _finish_span(test_suite_span) - - for module in seen_modules.values(): - test_module_span = module["module_span"] - if test_module_span and not test_module_span.finished: - _finish_span(test_module_span) - del _CIVisibility._unittest_data - - -def _update_remaining_suites_and_modules( - test_module_suite_path: str, test_module_path: str, test_module_span: ddtrace.Span, test_suite_span: ddtrace.Span -): - """ - Updates the remaining test suite and test counter and finishes spans when these have finished their execution. - """ - suite_dict = _CIVisibility._unittest_data["suites"][test_module_suite_path] - modules_dict = _CIVisibility._unittest_data["modules"][test_module_path] - - suite_dict["remaining_tests"] -= 1 - if suite_dict["remaining_tests"] == 0: - modules_dict["remaining_suites"] -= 1 - _finish_span(test_suite_span) - if modules_dict["remaining_suites"] == 0: - _finish_span(test_module_span) - - -def _update_test_skipping_count_span(span: ddtrace.Span): - if _CIVisibility.test_skipping_enabled(): - span.set_metric(test.ITR_TEST_SKIPPING_COUNT, _global_skipped_elements) - - -def _extract_skip_if_reason(args, kwargs): - if len(args) >= 2: - return _extract_test_reason(args) - elif kwargs and "reason" in kwargs: - return kwargs["reason"] - return "" - - -def patch(): - """ - Patch the instrumented methods from unittest - """ - if getattr(unittest, "_datadog_patch", False) or _CIVisibility.enabled: - return - _initialize_unittest_data() - - unittest._datadog_patch = True - - _w = wrapt.wrap_function_wrapper - - _w(unittest, "TextTestResult.addSuccess", add_success_test_wrapper) - _w(unittest, "TextTestResult.addFailure", add_failure_test_wrapper) - _w(unittest, "TextTestResult.addError", add_failure_test_wrapper) - _w(unittest, "TextTestResult.addSkip", add_skip_test_wrapper) - _w(unittest, "TextTestResult.addExpectedFailure", add_xfail_test_wrapper) - _w(unittest, "TextTestResult.addUnexpectedSuccess", add_xpass_test_wrapper) - _w(unittest, "skipIf", skip_if_decorator) - _w(unittest, "TestCase.run", handle_test_wrapper) - _w(unittest, "TestSuite.run", collect_text_test_runner_session) - _w(unittest, "TextTestRunner.run", handle_text_test_runner_wrapper) - _w(unittest, "TestProgram.runTests", handle_cli_run) - - -def unpatch(): - """ - Undo patched instrumented methods from unittest - """ - if not getattr(unittest, "_datadog_patch", False): - return - - _u(unittest.TextTestResult, "addSuccess") - _u(unittest.TextTestResult, "addFailure") - _u(unittest.TextTestResult, "addError") - _u(unittest.TextTestResult, "addSkip") - _u(unittest.TextTestResult, "addExpectedFailure") - _u(unittest.TextTestResult, "addUnexpectedSuccess") - _u(unittest, "skipIf") - _u(unittest.TestSuite, "run") - _u(unittest.TestCase, "run") - _u(unittest.TextTestRunner, "run") - _u(unittest.TestProgram, "runTests") - - unittest._datadog_patch = False - _CIVisibility.disable() - - -def _set_test_span_status(test_item, status: str, exc_info: str = None, skip_reason: str = None): - span = _extract_span(test_item) - if not span: - log.debug("Tried setting test result for test but could not find span for %s", test_item) - return None - span.set_tag_str(test.STATUS, status) - if exc_info: - span.set_exc_info(exc_info[0], exc_info[1], exc_info[2]) - if status == test.Status.SKIP.value: - span.set_tag_str(test.SKIP_REASON, skip_reason) - - -def _set_test_xpass_xfail_result(test_item, result: str): - """ - Sets `test.result` and `test.status` to a XFAIL or XPASS test. - """ - span = _extract_span(test_item) - if not span: - log.debug("Tried setting test result for an xpass or xfail test but could not find span for %s", test_item) - return None - span.set_tag_str(test.RESULT, result) - status = span.get_tag(test.STATUS) - if result == test.Status.XFAIL.value: - if status == test.Status.PASS.value: - span.set_tag_str(test.STATUS, test.Status.FAIL.value) - elif status == test.Status.FAIL.value: - span.set_tag_str(test.STATUS, test.Status.PASS.value) - - -def add_success_test_wrapper(func, instance: unittest.TextTestRunner, args: tuple, kwargs: dict): - if _is_valid_result(instance, args): - _set_test_span_status(test_item=args[0], status=test.Status.PASS.value) - - return func(*args, **kwargs) - - -def add_failure_test_wrapper(func, instance: unittest.TextTestRunner, args: tuple, kwargs: dict): - if _is_valid_result(instance, args): - _set_test_span_status(test_item=args[0], exc_info=_extract_test_reason(args), status=test.Status.FAIL.value) - - return func(*args, **kwargs) - - -def add_xfail_test_wrapper(func, instance: unittest.TextTestRunner, args: tuple, kwargs: dict): - if _is_valid_result(instance, args): - _set_test_xpass_xfail_result(test_item=args[0], result=test.Status.XFAIL.value) - - return func(*args, **kwargs) - - -def add_skip_test_wrapper(func, instance: unittest.TextTestRunner, args: tuple, kwargs: dict): - if _is_valid_result(instance, args): - _set_test_span_status(test_item=args[0], skip_reason=_extract_test_reason(args), status=test.Status.SKIP.value) - - return func(*args, **kwargs) - - -def add_xpass_test_wrapper(func, instance, args: tuple, kwargs: dict): - if _is_valid_result(instance, args): - _set_test_xpass_xfail_result(test_item=args[0], result=test.Status.XPASS.value) - - return func(*args, **kwargs) - - -def _mark_test_as_unskippable(obj): - test_name = obj.__name__ - test_suite_name = str(obj).split(".")[0].split()[1] - test_module_path = get_relative_or_absolute_path_for_path(obj.__code__.co_filename, os.getcwd()) - test_module_suite_name = _generate_fully_qualified_test_name(test_module_path, test_suite_name, test_name) - _CIVisibility._unittest_data["unskippable_tests"].add(test_module_suite_name) - return obj - - -def _using_unskippable_decorator(args, kwargs): - return args[0] is False and _extract_skip_if_reason(args, kwargs) == ITR_UNSKIPPABLE_REASON - - -def skip_if_decorator(func, instance, args: tuple, kwargs: dict): - if _using_unskippable_decorator(args, kwargs): - return _mark_test_as_unskippable - return func(*args, **kwargs) - - -def handle_test_wrapper(func, instance, args: tuple, kwargs: dict): - """ - Creates module and suite spans for `unittest` test executions. - """ - if _is_valid_test_call(kwargs) and _is_test(instance) and hasattr(_CIVisibility, "_unittest_data"): - test_name = _extract_test_method_name(instance) - test_suite_name = _extract_suite_name_from_test_method(instance) - test_module_path = _extract_module_file_path(instance) - test_module_suite_path = _generate_module_suite_path(test_module_path, test_suite_name) - test_suite_span = _extract_suite_span(test_module_suite_path) - test_module_span = _extract_module_span(test_module_path) - if test_module_span is None and test_module_path in _CIVisibility._unittest_data["modules"]: - test_module_span = _start_test_module_span(instance) - _CIVisibility._unittest_data["modules"][test_module_path]["module_span"] = test_module_span - if test_suite_span is None and test_module_suite_path in _CIVisibility._unittest_data["suites"]: - test_suite_span = _start_test_suite_span(instance) - suite_dict = _CIVisibility._unittest_data["suites"][test_module_suite_path] - suite_dict["suite_span"] = test_suite_span - if not test_module_span or not test_suite_span: - log.debug("Suite and/or module span not found for test: %s", test_name) - return func(*args, **kwargs) - with _start_test_span(instance, test_suite_span) as span: - test_session_span = _CIVisibility._datadog_session_span - root_directory = os.getcwd() - fqn_test = _generate_fully_qualified_test_name(test_module_path, test_suite_name, test_name) - - if _CIVisibility.test_skipping_enabled(): - if ITR_CORRELATION_ID_TAG_NAME in _CIVisibility._instance._itr_meta: - span.set_tag_str( - ITR_CORRELATION_ID_TAG_NAME, _CIVisibility._instance._itr_meta[ITR_CORRELATION_ID_TAG_NAME] - ) - - if _is_marked_as_unskippable(instance): - span.set_tag_str(test.ITR_UNSKIPPABLE, "true") - test_module_span.set_tag_str(test.ITR_UNSKIPPABLE, "true") - test_session_span.set_tag_str(test.ITR_UNSKIPPABLE, "true") - test_module_suite_path_without_extension = "{}/{}".format( - os.path.splitext(test_module_path)[0], test_suite_name - ) - if _should_be_skipped_by_itr(args, test_module_suite_path_without_extension, test_name, instance): - if _is_marked_as_unskippable(instance): - span.set_tag_str(test.ITR_FORCED_RUN, "true") - test_module_span.set_tag_str(test.ITR_FORCED_RUN, "true") - test_session_span.set_tag_str(test.ITR_FORCED_RUN, "true") - else: - _update_skipped_elements_and_set_tags(test_module_span, test_session_span) - instance._dd_itr_skip = True - span.set_tag_str(test.ITR_SKIPPED, "true") - span.set_tag_str(test.SKIP_REASON, SKIPPED_BY_ITR_REASON) - - if _is_skipped_by_itr(instance): - result = args[0] - result.startTest(test=instance) - result.addSkip(test=instance, reason=SKIPPED_BY_ITR_REASON) - _set_test_span_status( - test_item=instance, skip_reason=SKIPPED_BY_ITR_REASON, status=test.Status.SKIP.value - ) - result.stopTest(test=instance) - else: - if _is_test_coverage_enabled(instance): - if not _module_has_dd_coverage_enabled(unittest, silent_mode=True): - unittest._dd_coverage = _start_coverage(root_directory) - _switch_coverage_context(unittest._dd_coverage, fqn_test) - result = func(*args, **kwargs) - _update_status_item(test_suite_span, span.get_tag(test.STATUS)) - if _is_test_coverage_enabled(instance): - _report_coverage_to_span(unittest._dd_coverage, span, root_directory) - - _update_remaining_suites_and_modules( - test_module_suite_path, test_module_path, test_module_span, test_suite_span - ) - return result - return func(*args, **kwargs) - - -def collect_text_test_runner_session(func, instance: unittest.TestSuite, args: tuple, kwargs: dict): - """ - Discovers test suites and tests for the current `unittest` `TextTestRunner` execution - """ - if not _is_valid_module_suite_call(func): - return func(*args, **kwargs) - _initialize_unittest_data() - if _is_invoked_by_text_test_runner(): - seen_suites = _CIVisibility._unittest_data["suites"] - seen_modules = _CIVisibility._unittest_data["modules"] - _populate_suites_and_modules(instance._tests, seen_suites, seen_modules) - - result = func(*args, **kwargs) - - return result - result = func(*args, **kwargs) - return result - - -def _start_test_session_span(instance) -> ddtrace.Span: - """ - Starts a test session span and sets the required tags for a `unittest` session instance. - """ - tracer = getattr(unittest, "_datadog_tracer", _CIVisibility._instance.tracer) - test_command = _extract_command_name_from_session(instance) - resource_name = _generate_session_resource(test_command) - test_session_span = tracer.trace( - SESSION_OPERATION_NAME, - service=_CIVisibility._instance._service, - span_type=SpanTypes.TEST, - resource=resource_name, - ) - test_session_span.set_tag_str(_EVENT_TYPE, _SESSION_TYPE) - test_session_span.set_tag_str(_SESSION_ID, str(test_session_span.span_id)) - - test_session_span.set_tag_str(COMPONENT, COMPONENT_VALUE) - test_session_span.set_tag_str(SPAN_KIND, KIND) - - test_session_span.set_tag_str(test.COMMAND, test_command) - test_session_span.set_tag_str(test.FRAMEWORK, FRAMEWORK) - test_session_span.set_tag_str(test.FRAMEWORK_VERSION, _get_runtime_and_os_metadata()[RUNTIME_VERSION]) - - test_session_span.set_tag_str(test.TEST_TYPE, SpanTypes.TEST) - test_session_span.set_tag_str( - test.ITR_TEST_CODE_COVERAGE_ENABLED, - "true" if _CIVisibility._instance._collect_coverage_enabled else "false", - ) - - _CIVisibility.set_test_session_name(test_command=test_command) - - if _CIVisibility.test_skipping_enabled(): - _set_test_skipping_tags_to_span(test_session_span) - else: - test_session_span.set_tag_str(test.ITR_TEST_SKIPPING_ENABLED, "false") - _store_module_identifier(instance) - if _is_coverage_invoked_by_coverage_run(): - patch_coverage() - return test_session_span - - -def _start_test_module_span(instance) -> ddtrace.Span: - """ - Starts a test module span and sets the required tags for a `unittest` module instance. - """ - tracer = getattr(unittest, "_datadog_tracer", _CIVisibility._instance.tracer) - test_session_span = _extract_session_span() - test_module_name = _extract_module_name_from_module(instance) - resource_name = _generate_module_resource(test_module_name) - test_module_span = tracer._start_span( - MODULE_OPERATION_NAME, - service=_CIVisibility._instance._service, - span_type=SpanTypes.TEST, - activate=True, - child_of=test_session_span, - resource=resource_name, - ) - test_module_span.set_tag_str(_EVENT_TYPE, _MODULE_TYPE) - test_module_span.set_tag_str(_SESSION_ID, str(test_session_span.span_id)) - test_module_span.set_tag_str(_MODULE_ID, str(test_module_span.span_id)) - - test_module_span.set_tag_str(COMPONENT, COMPONENT_VALUE) - test_module_span.set_tag_str(SPAN_KIND, KIND) - - test_module_span.set_tag_str(test.COMMAND, test_session_span.get_tag(test.COMMAND)) - test_module_span.set_tag_str(test.FRAMEWORK, FRAMEWORK) - test_module_span.set_tag_str(test.FRAMEWORK_VERSION, _get_runtime_and_os_metadata()[RUNTIME_VERSION]) - - test_module_span.set_tag_str(test.TEST_TYPE, SpanTypes.TEST) - test_module_span.set_tag_str(test.MODULE, test_module_name) - test_module_span.set_tag_str(test.MODULE_PATH, _extract_module_file_path(instance)) - test_module_span.set_tag_str( - test.ITR_TEST_CODE_COVERAGE_ENABLED, - "true" if _CIVisibility._instance._collect_coverage_enabled else "false", - ) - if _CIVisibility.test_skipping_enabled(): - _set_test_skipping_tags_to_span(test_module_span) - test_module_span.set_metric(test.ITR_TEST_SKIPPING_COUNT, 0) - else: - test_module_span.set_tag_str(test.ITR_TEST_SKIPPING_ENABLED, "false") - _store_suite_identifier(instance) - return test_module_span - - -def _start_test_suite_span(instance) -> ddtrace.Span: - """ - Starts a test suite span and sets the required tags for a `unittest` suite instance. - """ - tracer = getattr(unittest, "_datadog_tracer", _CIVisibility._instance.tracer) - test_module_path = _extract_module_file_path(instance) - test_module_span = _extract_module_span(test_module_path) - test_suite_name = _extract_suite_name_from_test_method(instance) - resource_name = _generate_suite_resource(test_suite_name) - test_suite_span = tracer._start_span( - SUITE_OPERATION_NAME, - service=_CIVisibility._instance._service, - span_type=SpanTypes.TEST, - child_of=test_module_span, - activate=True, - resource=resource_name, - ) - test_suite_span.set_tag_str(_EVENT_TYPE, _SUITE_TYPE) - test_suite_span.set_tag_str(_SESSION_ID, test_module_span.get_tag(_SESSION_ID)) - test_suite_span.set_tag_str(_SUITE_ID, str(test_suite_span.span_id)) - test_suite_span.set_tag_str(_MODULE_ID, str(test_module_span.span_id)) - - test_suite_span.set_tag_str(COMPONENT, COMPONENT_VALUE) - test_suite_span.set_tag_str(SPAN_KIND, KIND) - - test_suite_span.set_tag_str(test.COMMAND, test_module_span.get_tag(test.COMMAND)) - test_suite_span.set_tag_str(test.FRAMEWORK, FRAMEWORK) - test_suite_span.set_tag_str(test.FRAMEWORK_VERSION, _get_runtime_and_os_metadata()[RUNTIME_VERSION]) - - test_suite_span.set_tag_str(test.TEST_TYPE, SpanTypes.TEST) - test_suite_span.set_tag_str(test.SUITE, test_suite_name) - test_suite_span.set_tag_str(test.MODULE, test_module_span.get_tag(test.MODULE)) - test_suite_span.set_tag_str(test.MODULE_PATH, test_module_path) - return test_suite_span - - -def _start_test_span(instance, test_suite_span: ddtrace.Span) -> ddtrace.Span: - """ - Starts a test span and sets the required tags for a `unittest` test instance. - """ - tracer = getattr(unittest, "_datadog_tracer", _CIVisibility._instance.tracer) - test_name = _extract_test_method_name(instance) - test_method_object = _extract_test_method_object(instance) - test_suite_name = _extract_suite_name_from_test_method(instance) - resource_name = _generate_test_resource(test_suite_name, test_name) - span = tracer._start_span( - ddtrace.config.unittest.operation_name, - service=_CIVisibility._instance._service, - resource=resource_name, - span_type=SpanTypes.TEST, - child_of=test_suite_span, - activate=True, - ) - span.set_tag_str(_EVENT_TYPE, SpanTypes.TEST) - span.set_tag_str(_SESSION_ID, test_suite_span.get_tag(_SESSION_ID)) - span.set_tag_str(_MODULE_ID, test_suite_span.get_tag(_MODULE_ID)) - span.set_tag_str(_SUITE_ID, test_suite_span.get_tag(_SUITE_ID)) - - span.set_tag_str(COMPONENT, COMPONENT_VALUE) - span.set_tag_str(SPAN_KIND, KIND) - - span.set_tag_str(test.COMMAND, test_suite_span.get_tag(test.COMMAND)) - span.set_tag_str(test.FRAMEWORK, FRAMEWORK) - span.set_tag_str(test.FRAMEWORK_VERSION, _get_runtime_and_os_metadata()[RUNTIME_VERSION]) - - span.set_tag_str(test.TYPE, SpanTypes.TEST) - span.set_tag_str(test.NAME, test_name) - span.set_tag_str(test.SUITE, test_suite_name) - span.set_tag_str(test.MODULE, test_suite_span.get_tag(test.MODULE)) - span.set_tag_str(test.MODULE_PATH, test_suite_span.get_tag(test.MODULE_PATH)) - span.set_tag_str(test.STATUS, test.Status.FAIL.value) - span.set_tag_str(test.CLASS_HIERARCHY, test_suite_name) - - _CIVisibility.set_codeowners_of(_extract_test_file_name(instance), span=span) - - _add_start_end_source_file_path_data_to_span(span, test_method_object, test_name, os.getcwd()) - - _store_test_span(instance, span) - return span - - -def _finish_span(current_span: ddtrace.Span): - """ - Finishes active span and populates span status upwards - """ - current_status = current_span.get_tag(test.STATUS) - parent_span = current_span._parent - if current_status and parent_span: - _update_status_item(parent_span, current_status) - elif not current_status: - current_span.set_tag_str(test.SUITE, test.Status.FAIL.value) - current_span.finish() - - -def _finish_test_session_span(): - _finish_remaining_suites_and_modules( - _CIVisibility._unittest_data["suites"], _CIVisibility._unittest_data["modules"] - ) - _update_test_skipping_count_span(_CIVisibility._datadog_session_span) - if _CIVisibility._instance._collect_coverage_enabled and _module_has_dd_coverage_enabled(unittest): - _stop_coverage(unittest) - if _is_coverage_patched() and _is_coverage_invoked_by_coverage_run(): - run_coverage_report() - _add_pct_covered_to_span(_coverage_data, _CIVisibility._datadog_session_span) - unpatch_coverage() - _finish_span(_CIVisibility._datadog_session_span) - - -def handle_cli_run(func, instance: unittest.TestProgram, args: tuple, kwargs: dict): - """ - Creates session span and discovers test suites and tests for the current `unittest` CLI execution - """ - if _is_invoked_by_cli(instance): - _enable_unittest_if_not_started() - for parent_module in instance.test._tests: - for module in parent_module._tests: - _populate_suites_and_modules( - module, _CIVisibility._unittest_data["suites"], _CIVisibility._unittest_data["modules"] - ) - - test_session_span = _start_test_session_span(instance) - _CIVisibility._datadog_entry = "cli" - _CIVisibility._datadog_session_span = test_session_span - - try: - result = func(*args, **kwargs) - except SystemExit as e: - if _CIVisibility.enabled and _CIVisibility._datadog_session_span and hasattr(_CIVisibility, "_unittest_data"): - _finish_test_session_span() - - raise e - return result - - -def handle_text_test_runner_wrapper(func, instance: unittest.TextTestRunner, args: tuple, kwargs: dict): - """ - Creates session span if unittest is called through the `TextTestRunner` method - """ - if _is_invoked_by_cli(instance): - return func(*args, **kwargs) - _enable_unittest_if_not_started() - _CIVisibility._datadog_entry = "TextTestRunner" - if not hasattr(_CIVisibility, "_datadog_session_span"): - _CIVisibility._datadog_session_span = _start_test_session_span(instance) - _CIVisibility._datadog_expected_sessions = 0 - _CIVisibility._datadog_finished_sessions = 0 - _CIVisibility._datadog_expected_sessions += 1 - try: - result = func(*args, **kwargs) - except SystemExit as e: - _CIVisibility._datadog_finished_sessions += 1 - if _CIVisibility._datadog_finished_sessions == _CIVisibility._datadog_expected_sessions: - _finish_test_session_span() - del _CIVisibility._datadog_session_span - raise e - _CIVisibility._datadog_finished_sessions += 1 - if _CIVisibility._datadog_finished_sessions == _CIVisibility._datadog_expected_sessions: - _finish_test_session_span() - del _CIVisibility._datadog_session_span - return result + if name in globals(): + return globals()[name] + raise AttributeError("%s has no attribute %s", __name__, name) diff --git a/ddtrace/internal/ci_visibility/api/_test.py b/ddtrace/internal/ci_visibility/api/_test.py index 0f8a2efd41d..a7ab21dd459 100644 --- a/ddtrace/internal/ci_visibility/api/_test.py +++ b/ddtrace/internal/ci_visibility/api/_test.py @@ -5,7 +5,7 @@ from typing import Optional from typing import Union -from ddtrace.contrib.pytest_benchmark.constants import BENCHMARK_INFO +from ddtrace.contrib.internal.pytest_benchmark.constants import BENCHMARK_INFO from ddtrace.ext import SpanTypes from ddtrace.ext import test from ddtrace.ext.test_visibility import ITR_SKIPPING_LEVEL diff --git a/pyproject.toml b/pyproject.toml index df5fbdcdbb2..03560d1b171 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,9 +55,9 @@ ddtrace-run = "ddtrace.commands.ddtrace_run:main" ddcontextvars_context = "ddtrace.internal.opentelemetry.context:DDRuntimeContext" [project.entry-points.pytest11] -ddtrace = "ddtrace.contrib.pytest.plugin" -"ddtrace.pytest_bdd" = "ddtrace.contrib.pytest_bdd.plugin" -"ddtrace.pytest_benchmark" = "ddtrace.contrib.pytest_benchmark.plugin" +ddtrace = "ddtrace.contrib.internal.pytest.plugin" +"ddtrace.pytest_bdd" = "ddtrace.contrib.internal.pytest_bdd.plugin" +"ddtrace.pytest_benchmark" = "ddtrace.contrib.internal.pytest_benchmark.plugin" [project.entry-points.'ddtrace.products'] "code-origin-for-spans" = "ddtrace.debugging._products.code_origin.span" diff --git a/releasenotes/notes/make-ci-vis-ints-internal-532bc22d19bb62ab.yaml b/releasenotes/notes/make-ci-vis-ints-internal-532bc22d19bb62ab.yaml new file mode 100644 index 00000000000..334a65b1cfb --- /dev/null +++ b/releasenotes/notes/make-ci-vis-ints-internal-532bc22d19bb62ab.yaml @@ -0,0 +1,5 @@ +--- +deprecations: + - | + ci vis: Moves the implementational details of the pytest, pytest_benchmark, pytest_bdd, and unittest integrations + from ``ddtrace.contrib.`` to ``ddtrace.contrib.internal.``. diff --git a/tests/contrib/asynctest/test_asynctest.py b/tests/contrib/asynctest/test_asynctest.py index 44e0c0c2387..cd325e0ec8b 100644 --- a/tests/contrib/asynctest/test_asynctest.py +++ b/tests/contrib/asynctest/test_asynctest.py @@ -5,7 +5,7 @@ import pytest import ddtrace -from ddtrace.contrib.pytest.plugin import is_enabled +from ddtrace.contrib.internal.pytest.plugin import is_enabled from ddtrace.ext import test from ddtrace.internal.ci_visibility import CIVisibility from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings diff --git a/tests/contrib/pytest/test_coverage_per_suite.py b/tests/contrib/pytest/test_coverage_per_suite.py index a9c985fa7b4..adb2a710c76 100644 --- a/tests/contrib/pytest/test_coverage_per_suite.py +++ b/tests/contrib/pytest/test_coverage_per_suite.py @@ -4,8 +4,8 @@ import pytest -from ddtrace.contrib.pytest._utils import _USE_PLUGIN_V2 -from ddtrace.contrib.pytest._utils import _pytest_version_supports_itr +from ddtrace.contrib.internal.pytest._utils import _USE_PLUGIN_V2 +from ddtrace.contrib.internal.pytest._utils import _pytest_version_supports_itr from ddtrace.ext.test_visibility import ITR_SKIPPING_LEVEL from ddtrace.internal.ci_visibility._api_client import ITRData from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings diff --git a/tests/contrib/pytest/test_pytest.py b/tests/contrib/pytest/test_pytest.py index 8dcf9c50bd4..267a9d97eac 100644 --- a/tests/contrib/pytest/test_pytest.py +++ b/tests/contrib/pytest/test_pytest.py @@ -9,10 +9,10 @@ import ddtrace from ddtrace.constants import ERROR_MSG from ddtrace.constants import SAMPLING_PRIORITY_KEY -from ddtrace.contrib.pytest import get_version -from ddtrace.contrib.pytest._utils import _USE_PLUGIN_V2 -from ddtrace.contrib.pytest.constants import XFAIL_REASON -from ddtrace.contrib.pytest.plugin import is_enabled +from ddtrace.contrib.internal.pytest._utils import _USE_PLUGIN_V2 +from ddtrace.contrib.internal.pytest.constants import XFAIL_REASON +from ddtrace.contrib.internal.pytest.patch import get_version +from ddtrace.contrib.internal.pytest.plugin import is_enabled from ddtrace.ext import ci from ddtrace.ext import git from ddtrace.ext import test diff --git a/tests/contrib/pytest/test_pytest_atr.py b/tests/contrib/pytest/test_pytest_atr.py index 3e526e8cdf7..ebb4f8421d8 100644 --- a/tests/contrib/pytest/test_pytest_atr.py +++ b/tests/contrib/pytest/test_pytest_atr.py @@ -10,8 +10,8 @@ import pytest -from ddtrace.contrib.pytest._utils import _USE_PLUGIN_V2 -from ddtrace.contrib.pytest._utils import _pytest_version_supports_atr +from ddtrace.contrib.internal.pytest._utils import _USE_PLUGIN_V2 +from ddtrace.contrib.internal.pytest._utils import _pytest_version_supports_atr from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings from tests.ci_visibility.util import _get_default_civisibility_ddconfig from tests.contrib.pytest.test_pytest import PytestTestCaseBase diff --git a/tests/contrib/pytest/test_pytest_efd.py b/tests/contrib/pytest/test_pytest_efd.py index 2affcec3585..e2a2fa08cab 100644 --- a/tests/contrib/pytest/test_pytest_efd.py +++ b/tests/contrib/pytest/test_pytest_efd.py @@ -10,8 +10,8 @@ import pytest -from ddtrace.contrib.pytest._utils import _USE_PLUGIN_V2 -from ddtrace.contrib.pytest._utils import _pytest_version_supports_efd +from ddtrace.contrib.internal.pytest._utils import _USE_PLUGIN_V2 +from ddtrace.contrib.internal.pytest._utils import _pytest_version_supports_efd from ddtrace.internal.ci_visibility._api_client import EarlyFlakeDetectionSettings from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings from tests.ci_visibility.api_client._util import _make_fqdn_test_ids diff --git a/tests/contrib/pytest/test_pytest_quarantine.py b/tests/contrib/pytest/test_pytest_quarantine.py index 93b0b07eade..52e5c5a393c 100644 --- a/tests/contrib/pytest/test_pytest_quarantine.py +++ b/tests/contrib/pytest/test_pytest_quarantine.py @@ -10,8 +10,8 @@ import pytest -from ddtrace.contrib.pytest._utils import _USE_PLUGIN_V2 -from ddtrace.contrib.pytest._utils import _pytest_version_supports_efd +from ddtrace.contrib.internal.pytest._utils import _USE_PLUGIN_V2 +from ddtrace.contrib.internal.pytest._utils import _pytest_version_supports_efd from ddtrace.internal.ci_visibility._api_client import QuarantineSettings from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings from tests.contrib.pytest.test_pytest import PytestTestCaseBase diff --git a/tests/contrib/pytest/test_pytest_snapshot.py b/tests/contrib/pytest/test_pytest_snapshot.py index f8c4090c33d..8b298c28c95 100644 --- a/tests/contrib/pytest/test_pytest_snapshot.py +++ b/tests/contrib/pytest/test_pytest_snapshot.py @@ -3,7 +3,7 @@ import pytest -from ddtrace.contrib.pytest._utils import _USE_PLUGIN_V2 +from ddtrace.contrib.internal.pytest._utils import _USE_PLUGIN_V2 from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings from tests.ci_visibility.util import _get_default_ci_env_vars from tests.utils import TracerTestCase diff --git a/tests/contrib/pytest/test_pytest_snapshot_v2.py b/tests/contrib/pytest/test_pytest_snapshot_v2.py index 85d70d4c38e..dad546df23b 100644 --- a/tests/contrib/pytest/test_pytest_snapshot_v2.py +++ b/tests/contrib/pytest/test_pytest_snapshot_v2.py @@ -3,7 +3,7 @@ import pytest -from ddtrace.contrib.pytest._utils import _USE_PLUGIN_V2 +from ddtrace.contrib.internal.pytest._utils import _USE_PLUGIN_V2 from ddtrace.internal.ci_visibility._api_client import TestVisibilityAPISettings from tests.ci_visibility.util import _get_default_ci_env_vars from tests.utils import TracerTestCase diff --git a/tests/contrib/pytest_bdd/test_pytest_bdd.py b/tests/contrib/pytest_bdd/test_pytest_bdd.py index edf3ab90454..25dff3c8bee 100644 --- a/tests/contrib/pytest_bdd/test_pytest_bdd.py +++ b/tests/contrib/pytest_bdd/test_pytest_bdd.py @@ -2,8 +2,8 @@ import os from ddtrace.constants import ERROR_MSG -from ddtrace.contrib.pytest_bdd._plugin import _get_step_func_args_json -from ddtrace.contrib.pytest_bdd._plugin import get_version +from ddtrace.contrib.internal.pytest_bdd._plugin import _get_step_func_args_json +from ddtrace.contrib.internal.pytest_bdd._plugin import get_version from ddtrace.ext import test from tests.contrib.patch import emit_integration_and_version_to_test_agent from tests.contrib.pytest.test_pytest import PytestTestCaseBase @@ -197,20 +197,23 @@ def test_simple(): assert spans[0].get_tag(ERROR_MSG) def test_get_step_func_args_json_empty(self): - self.monkeypatch.setattr("ddtrace.contrib.pytest_bdd._plugin._extract_step_func_args", lambda *args: None) + self.monkeypatch.setattr( + "ddtrace.contrib.internal.pytest_bdd._plugin._extract_step_func_args", lambda *args: None + ) assert _get_step_func_args_json(None, lambda: None, None) is None def test_get_step_func_args_json_valid(self): self.monkeypatch.setattr( - "ddtrace.contrib.pytest_bdd._plugin._extract_step_func_args", lambda *args: {"func_arg": "test string"} + "ddtrace.contrib.internal.pytest_bdd._plugin._extract_step_func_args", + lambda *args: {"func_arg": "test string"}, ) assert _get_step_func_args_json(None, lambda: None, None) == '{"func_arg": "test string"}' def test_get_step_func_args_json_invalid(self): self.monkeypatch.setattr( - "ddtrace.contrib.pytest_bdd._plugin._extract_step_func_args", lambda *args: {"func_arg": set()} + "ddtrace.contrib.internal.pytest_bdd._plugin._extract_step_func_args", lambda *args: {"func_arg": set()} ) expected = '{"error_serializing_args": "Object of type set is not JSON serializable"}' diff --git a/tests/contrib/pytest_benchmark/test_pytest_benchmark.py b/tests/contrib/pytest_benchmark/test_pytest_benchmark.py index ba55659b8f8..233a389855c 100644 --- a/tests/contrib/pytest_benchmark/test_pytest_benchmark.py +++ b/tests/contrib/pytest_benchmark/test_pytest_benchmark.py @@ -1,24 +1,24 @@ import os -from ddtrace.contrib.pytest_benchmark.constants import BENCHMARK_INFO -from ddtrace.contrib.pytest_benchmark.constants import BENCHMARK_MEAN -from ddtrace.contrib.pytest_benchmark.constants import BENCHMARK_RUN -from ddtrace.contrib.pytest_benchmark.constants import STATISTICS_HD15IQR -from ddtrace.contrib.pytest_benchmark.constants import STATISTICS_IQR -from ddtrace.contrib.pytest_benchmark.constants import STATISTICS_IQR_OUTLIERS -from ddtrace.contrib.pytest_benchmark.constants import STATISTICS_LD15IQR -from ddtrace.contrib.pytest_benchmark.constants import STATISTICS_MAX -from ddtrace.contrib.pytest_benchmark.constants import STATISTICS_MEAN -from ddtrace.contrib.pytest_benchmark.constants import STATISTICS_MEDIAN -from ddtrace.contrib.pytest_benchmark.constants import STATISTICS_MIN -from ddtrace.contrib.pytest_benchmark.constants import STATISTICS_N -from ddtrace.contrib.pytest_benchmark.constants import STATISTICS_OPS -from ddtrace.contrib.pytest_benchmark.constants import STATISTICS_OUTLIERS -from ddtrace.contrib.pytest_benchmark.constants import STATISTICS_Q1 -from ddtrace.contrib.pytest_benchmark.constants import STATISTICS_Q3 -from ddtrace.contrib.pytest_benchmark.constants import STATISTICS_STDDEV -from ddtrace.contrib.pytest_benchmark.constants import STATISTICS_STDDEV_OUTLIERS -from ddtrace.contrib.pytest_benchmark.constants import STATISTICS_TOTAL +from ddtrace.contrib.internal.pytest_benchmark.constants import BENCHMARK_INFO +from ddtrace.contrib.internal.pytest_benchmark.constants import BENCHMARK_MEAN +from ddtrace.contrib.internal.pytest_benchmark.constants import BENCHMARK_RUN +from ddtrace.contrib.internal.pytest_benchmark.constants import STATISTICS_HD15IQR +from ddtrace.contrib.internal.pytest_benchmark.constants import STATISTICS_IQR +from ddtrace.contrib.internal.pytest_benchmark.constants import STATISTICS_IQR_OUTLIERS +from ddtrace.contrib.internal.pytest_benchmark.constants import STATISTICS_LD15IQR +from ddtrace.contrib.internal.pytest_benchmark.constants import STATISTICS_MAX +from ddtrace.contrib.internal.pytest_benchmark.constants import STATISTICS_MEAN +from ddtrace.contrib.internal.pytest_benchmark.constants import STATISTICS_MEDIAN +from ddtrace.contrib.internal.pytest_benchmark.constants import STATISTICS_MIN +from ddtrace.contrib.internal.pytest_benchmark.constants import STATISTICS_N +from ddtrace.contrib.internal.pytest_benchmark.constants import STATISTICS_OPS +from ddtrace.contrib.internal.pytest_benchmark.constants import STATISTICS_OUTLIERS +from ddtrace.contrib.internal.pytest_benchmark.constants import STATISTICS_Q1 +from ddtrace.contrib.internal.pytest_benchmark.constants import STATISTICS_Q3 +from ddtrace.contrib.internal.pytest_benchmark.constants import STATISTICS_STDDEV +from ddtrace.contrib.internal.pytest_benchmark.constants import STATISTICS_STDDEV_OUTLIERS +from ddtrace.contrib.internal.pytest_benchmark.constants import STATISTICS_TOTAL from ddtrace.ext.test import TEST_TYPE from tests.contrib.pytest.test_pytest import PytestTestCaseBase diff --git a/tests/contrib/unittest/test_unittest.py b/tests/contrib/unittest/test_unittest.py index b2af61b47d8..cd24c26f3c0 100644 --- a/tests/contrib/unittest/test_unittest.py +++ b/tests/contrib/unittest/test_unittest.py @@ -7,15 +7,15 @@ from ddtrace.constants import ERROR_MSG from ddtrace.constants import ERROR_TYPE from ddtrace.constants import SPAN_KIND -from ddtrace.contrib.unittest.constants import COMPONENT_VALUE -from ddtrace.contrib.unittest.constants import FRAMEWORK -from ddtrace.contrib.unittest.constants import KIND -from ddtrace.contrib.unittest.constants import MODULE_OPERATION_NAME -from ddtrace.contrib.unittest.constants import SESSION_OPERATION_NAME -from ddtrace.contrib.unittest.constants import SUITE_OPERATION_NAME -from ddtrace.contrib.unittest.constants import TEST_OPERATION_NAME -from ddtrace.contrib.unittest.patch import _set_tracer -from ddtrace.contrib.unittest.patch import patch +from ddtrace.contrib.internal.unittest.constants import COMPONENT_VALUE +from ddtrace.contrib.internal.unittest.constants import FRAMEWORK +from ddtrace.contrib.internal.unittest.constants import KIND +from ddtrace.contrib.internal.unittest.constants import MODULE_OPERATION_NAME +from ddtrace.contrib.internal.unittest.constants import SESSION_OPERATION_NAME +from ddtrace.contrib.internal.unittest.constants import SUITE_OPERATION_NAME +from ddtrace.contrib.internal.unittest.constants import TEST_OPERATION_NAME +from ddtrace.contrib.internal.unittest.patch import _set_tracer +from ddtrace.contrib.internal.unittest.patch import patch from ddtrace.ext import SpanTypes from ddtrace.ext import test from ddtrace.ext.ci import RUNTIME_VERSION diff --git a/tests/contrib/unittest/test_unittest_patch.py b/tests/contrib/unittest/test_unittest_patch.py index e8996fc573a..ccb8668fa12 100644 --- a/tests/contrib/unittest/test_unittest_patch.py +++ b/tests/contrib/unittest/test_unittest_patch.py @@ -3,12 +3,12 @@ # removed the ``_generated`` suffix from the file name, to prevent the content # from being overwritten by future re-generations. -from ddtrace.contrib.unittest.patch import get_version -from ddtrace.contrib.unittest.patch import patch +from ddtrace.contrib.internal.unittest.patch import get_version +from ddtrace.contrib.internal.unittest.patch import patch try: - from ddtrace.contrib.unittest.patch import unpatch + from ddtrace.contrib.internal.unittest.patch import unpatch except ImportError: unpatch = None from tests.contrib.patch import PatchTestCase diff --git a/tests/internal/test_module.py b/tests/internal/test_module.py index 885f796af81..c84c2c740d6 100644 --- a/tests/internal/test_module.py +++ b/tests/internal/test_module.py @@ -578,17 +578,6 @@ def __getattr__(name): "ddtrace.contrib.trace_utils", "ddtrace.contrib.trace_utils_async", "ddtrace.contrib.trace_utils_redis", - # TODO: The following contrib modules are part of the public API (unlike most integrations). - # We should consider privatizing the internals of these integrations. - "ddtrace.contrib.unittest.patch", - "ddtrace.contrib.unittest.constants", - "ddtrace.contrib.pytest.constants", - "ddtrace.contrib.pytest.newhooks", - "ddtrace.contrib.pytest.plugin", - "ddtrace.contrib.pytest_benchmark.constants", - "ddtrace.contrib.pytest_benchmark.plugin", - "ddtrace.contrib.pytest_bdd.constants", - "ddtrace.contrib.pytest_bdd.plugin", ] ) From ef4c99759f8c1d87642718db2292ae98e073b3c6 Mon Sep 17 00:00:00 2001 From: Nick Ripley Date: Fri, 17 Jan 2025 13:58:09 -0500 Subject: [PATCH 2/3] refactor(profiling): add more GIL assertions to memalloc (#12000) The first GIL assertion I added, to memalloc_add_event, has not tripped yet on a test application. One the one hand, it's reassuring that we always see the GIL in that part of the code. On the other hand, there are other parts of the memory profiler that could in theory be called concurrently where I didn't add the assertion: when stopping the profiler, and when iterating over the events to aggregate them. Add GIL assertions to those points. The goal is ultimately to understand why we needed to add locks to the profiler to prevent it from crashing, given that the GIL exists. --- ddtrace/profiling/collector/_memalloc.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ddtrace/profiling/collector/_memalloc.c b/ddtrace/profiling/collector/_memalloc.c index b55e9ebcfab..1f2b87e0433 100644 --- a/ddtrace/profiling/collector/_memalloc.c +++ b/ddtrace/profiling/collector/_memalloc.c @@ -109,13 +109,19 @@ memalloc_init() } static void -memalloc_add_event(memalloc_context_t* ctx, void* ptr, size_t size) +memalloc_assert_gil() { if (g_crash_on_no_gil && !PyGILState_Check()) { int* p = NULL; *p = 0; abort(); // should never reach here } +} + +static void +memalloc_add_event(memalloc_context_t* ctx, void* ptr, size_t size) +{ + memalloc_assert_gil(); uint64_t alloc_count = atomic_add_clamped(&global_alloc_tracker->alloc_count, 1, ALLOC_TRACKER_MAX_COUNT); @@ -332,6 +338,8 @@ memalloc_stop(PyObject* Py_UNUSED(module), PyObject* Py_UNUSED(args)) return NULL; } + memalloc_assert_gil(); + PyMem_SetAllocator(PYMEM_DOMAIN_OBJ, &global_memalloc_ctx.pymem_allocator_obj); memalloc_tb_deinit(); if (memlock_trylock(&g_memalloc_lock)) { @@ -389,6 +397,8 @@ iterevents_new(PyTypeObject* type, PyObject* Py_UNUSED(args), PyObject* Py_UNUSE if (!iestate) return NULL; + memalloc_assert_gil(); + /* reset the current traceback list */ if (memlock_trylock(&g_memalloc_lock)) { iestate->alloc_tracker = global_alloc_tracker; From 4084e8ab0e0920a0bb3cb13d8b4456cf675a4a17 Mon Sep 17 00:00:00 2001 From: Brett Langdon Date: Fri, 17 Jan 2025 15:40:14 -0500 Subject: [PATCH 3/3] chore(lib-injection): dedupe files in OCI image (#11960) --- .gitlab/prepare-oci-package.sh | 16 ++++++++++++++++ .../chore-reduce-oci-image-ce45f1868ee14415.yaml | 4 ++++ 2 files changed, 20 insertions(+) create mode 100644 releasenotes/notes/chore-reduce-oci-image-ce45f1868ee14415.yaml diff --git a/.gitlab/prepare-oci-package.sh b/.gitlab/prepare-oci-package.sh index 27d5354219e..5958c31e731 100755 --- a/.gitlab/prepare-oci-package.sh +++ b/.gitlab/prepare-oci-package.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -eo pipefail if [ -n "$CI_COMMIT_TAG" ] && [ -z "$PYTHON_PACKAGE_VERSION" ]; then PYTHON_PACKAGE_VERSION=${CI_COMMIT_TAG##v} @@ -38,3 +39,18 @@ fi cp -r ../pywheels-dep/site-packages* sources/ddtrace_pkgs cp ../lib-injection/sources/* sources/ + +if ! type rdfind &> /dev/null; then + clean-apt install rdfind +fi +echo "Deduplicating package files" +cd ./sources +rdfind -makesymlinks true -makeresultsfile true -checksum sha256 -deterministic true -outputname deduped.txt . +echo "Converting symlinks to relative symlinks" +find . -type l | while read -r l; do + target="$(realpath "$l")" + rel_target="$(realpath --relative-to="$(dirname "$(realpath -s "$l")")" "$target")" + dest_base="$(basename "$l")" + dest_dir="$(dirname "$l")" + (cd "${dest_dir}" && ln -sf "${rel_target}" "${dest_base}") +done diff --git a/releasenotes/notes/chore-reduce-oci-image-ce45f1868ee14415.yaml b/releasenotes/notes/chore-reduce-oci-image-ce45f1868ee14415.yaml new file mode 100644 index 00000000000..42d4e6d348b --- /dev/null +++ b/releasenotes/notes/chore-reduce-oci-image-ce45f1868ee14415.yaml @@ -0,0 +1,4 @@ +--- +other: + - | + lib-injection: Reduce size of OCI image size to improve k8s lib-injection pull and startup times.