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/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/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; 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/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. 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 348272c7d23..e62440325fe 100644 --- a/tests/internal/test_module.py +++ b/tests/internal/test_module.py @@ -572,23 +572,11 @@ def __getattr__(name): assert missing_deprecations == set( [ - # Note: The following ddtrace.contrib modules are expected to be part of the public API - # TODO: Revisit whether integration utils should be part of the public API + "ddtrace.contrib.trace_utils", + # Note: The modules below are deprecated but they do not follow the template above "ddtrace.contrib.redis_utils", - "ddtrace.contrib.internal.trace_utils", - "ddtrace.contrib.internal.trace_utils_async", - "ddtrace.contrib.internal.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", + "ddtrace.contrib.trace_utils_async", + "ddtrace.contrib.trace_utils_redis", ] )