Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into brettlangdon/improve.…
Browse files Browse the repository at this point in the history
…assertion
  • Loading branch information
brettlangdon committed Dec 17, 2024
2 parents 70d8f59 + a4f0e38 commit ca9ee58
Show file tree
Hide file tree
Showing 17 changed files with 280 additions and 84 deletions.
11 changes: 7 additions & 4 deletions .gitlab/tests.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
stages:
- tests
- precheck
- hatch
- riot

variables:
RIOT_RUN_CMD: riot -P -v run --exitfirst --pass-env -s
Expand All @@ -22,7 +24,7 @@ variables:

.test_base_hatch:
extends: .testrunner
stage: tests
stage: hatch
# Hatch doesn't use pre-built wheels or venvs so we can start them right away
needs: []
parallel: 4
Expand Down Expand Up @@ -57,7 +59,7 @@ variables:

build_base_venvs:
extends: .testrunner
stage: tests
stage: riot
parallel:
matrix:
- PYTHON_VERSION: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
Expand All @@ -76,9 +78,10 @@ build_base_venvs:
- ddtrace/internal/datadog/profiling/crashtracker/crashtracker_exe*
- ddtrace/internal/datadog/profiling/test/test_*

# Do not define a `needs:` in order to depend on the whole `precheck` stage
.test_base_riot:
extends: .testrunner
stage: tests
stage: riot
needs: [ build_base_venvs ]
parallel: 4
services:
Expand Down
16 changes: 12 additions & 4 deletions ddtrace/appsec/_common_module_patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@ def wrapped_read_F3E51D71B4EC16EF(original_read_callable, instance, args, kwargs
"""
wrapper for _io.BytesIO and _io.StringIO read function
"""
from ddtrace.appsec._iast._iast_request_context import is_iast_request_enabled

result = original_read_callable(*args, **kwargs)
if asm_config._iast_enabled:
if asm_config._iast_enabled and is_iast_request_enabled():
from ddtrace.appsec._iast._taint_tracking import OriginType
from ddtrace.appsec._iast._taint_tracking import Source
from ddtrace.appsec._iast._taint_tracking import get_tainted_ranges
Expand All @@ -87,7 +89,9 @@ def wrapped_open_CFDDB7ABBA9081B6(original_open_callable, instance, args, kwargs
"""
wrapper for open file function
"""
if asm_config._iast_enabled:
from ddtrace.appsec._iast._iast_request_context import is_iast_request_enabled

if asm_config._iast_enabled and is_iast_request_enabled():
try:
from ddtrace.appsec._iast.taint_sinks.path_traversal import check_and_report_path_traversal

Expand Down Expand Up @@ -176,7 +180,9 @@ def wrapped_request_D8CB81E472AF98A2(original_request_callable, instance, args,
wrapper for third party requests.request function
https://requests.readthedocs.io
"""
if asm_config._iast_enabled:
from ddtrace.appsec._iast._iast_request_context import is_iast_request_enabled

if asm_config._iast_enabled and is_iast_request_enabled():
from ddtrace.appsec._iast.taint_sinks.ssrf import _iast_report_ssrf

_iast_report_ssrf(original_request_callable, *args, **kwargs)
Expand Down Expand Up @@ -216,7 +222,9 @@ def wrapped_system_5542593D237084A7(original_command_callable, instance, args, k
"""
command = args[0] if args else kwargs.get("command", None)
if command is not None:
if asm_config._iast_enabled:
from ddtrace.appsec._iast._iast_request_context import is_iast_request_enabled

if asm_config._iast_enabled and is_iast_request_enabled():
from ddtrace.appsec._iast.taint_sinks.command_injection import _iast_report_cmdi

_iast_report_cmdi(command)
Expand Down
6 changes: 6 additions & 0 deletions ddtrace/contrib/pytest/_plugin_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ def pytest_configure(config: pytest_Config) -> None:
enable_test_visibility(config=dd_config.pytest)
if _is_pytest_cov_enabled(config):
patch_coverage()

# pytest-bdd plugin support
if config.pluginmanager.hasplugin("pytest-bdd"):
from ddtrace.contrib.pytest._pytest_bdd_subplugin import _PytestBddSubPlugin

config.pluginmanager.register(_PytestBddSubPlugin(), "_datadog-pytest-bdd")
else:
# If the pytest ddtrace plugin is not enabled, we should disable CI Visibility, as it was enabled during
# pytest_load_initial_conftests
Expand Down
110 changes: 110 additions & 0 deletions ddtrace/contrib/pytest/_pytest_bdd_subplugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""Provides functionality to support the pytest-bdd plugin as part of the ddtrace integration
NOTE: This replaces the previous ddtrace.pytest_bdd plugin.
This plugin mainly modifies the names of the test, its suite, and parameters. It does not, however modify the tests'
suite from the perspective of Test Visibility data.
The plugin is only instantiated and added if the pytest-bdd plugin itself is installed and enabled, because the hook
implementations will cause errors unless the hookspecs are added by the original plugin.
"""
from pathlib import Path
import sys

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.ext import test
from ddtrace.internal.logger import get_logger
from ddtrace.internal.test_visibility.api import InternalTest
from ddtrace.internal.test_visibility.api import InternalTestSession


log = get_logger(__name__)


def _get_workspace_relative_path(feature_path_str: str) -> Path:
feature_path = Path(feature_path_str).resolve()
workspace_path = InternalTestSession.get_workspace_path()
if workspace_path:
try:
return feature_path.relative_to(workspace_path)
except ValueError: # noqa: E722
log.debug("Feature path %s is not relative to workspace path %s", feature_path, workspace_path)
return feature_path


class _PytestBddSubPlugin:
def __init__(self):
self.framework_version = get_version()

@staticmethod
@pytest.hookimpl(tryfirst=True)
def pytest_bdd_before_scenario(request, feature, scenario):
test_id = _get_test_id_from_item(request.node)
feature_path = _get_workspace_relative_path(scenario.feature.filename)
codeowners = InternalTestSession.get_path_codeowners(feature_path)

InternalTest.overwrite_attributes(
test_id, name=scenario.name, suite_name=str(feature_path), codeowners=codeowners
)

@pytest.hookimpl(tryfirst=True)
def pytest_bdd_before_step(self, request, feature, scenario, step, step_func):
feature_test_id = _get_test_id_from_item(request.node)

feature_span = InternalTest.get_span(feature_test_id)

tracer = InternalTestSession.get_tracer()
if tracer is None:
return

span = tracer.start_span(
step.type,
resource=step.name,
span_type=STEP_KIND,
child_of=feature_span,
activate=True,
)
span.set_tag_str("component", "pytest_bdd")

span.set_tag(test.FRAMEWORK, FRAMEWORK)
span.set_tag(test.FRAMEWORK_VERSION, self.framework_version)

feature_path = _get_workspace_relative_path(scenario.feature.filename)

span.set_tag(test.FILE, str(feature_path))
span.set_tag(test.CODEOWNERS, InternalTestSession.get_path_codeowners(feature_path))

_store_span(step_func, span)

@staticmethod
@pytest.hookimpl(trylast=True)
def pytest_bdd_after_step(request, feature, scenario, step, step_func, step_func_args):
span = _extract_span(step_func)
if span is not None:
step_func_args_json = _get_step_func_args_json(step, step_func, step_func_args)
if step_func_args:
span.set_tag(test.PARAMETERS, step_func_args_json)
span.finish()

@staticmethod
def pytest_bdd_step_error(request, feature, scenario, step, step_func, step_func_args, exception):
span = _extract_span(step_func)
if span is not None:
if hasattr(exception, "__traceback__"):
tb = exception.__traceback__
else:
# PY2 compatibility workaround
_, _, tb = sys.exc_info()
step_func_args_json = _get_step_func_args_json(step, step_func, step_func_args)
if step_func_args:
span.set_tag(test.PARAMETERS, step_func_args_json)
span.set_exc_info(type(exception), exception, tb)
span.finish()
17 changes: 14 additions & 3 deletions ddtrace/contrib/pytest_bdd/plugin.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
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.vendor.debtcollector import deprecate


def pytest_configure(config):
if config.pluginmanager.hasplugin("pytest-bdd") and config.pluginmanager.hasplugin("ddtrace"):
if is_ddtrace_enabled(config):
from ._plugin import _PytestBddPlugin
if not _USE_PLUGIN_V2:
if is_ddtrace_enabled(config):
from ._plugin import _PytestBddPlugin

config.pluginmanager.register(_PytestBddPlugin(), "_datadog-pytest-bdd")
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")
22 changes: 22 additions & 0 deletions ddtrace/internal/ci_visibility/api/_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ def __init__(
self._is_benchmark = False
self._benchmark_duration_data: Optional[BenchmarkDurationData] = None

# Some parameters can be overwritten:
self._overwritten_suite_name: Optional[str] = None

def __repr__(self) -> str:
suite_name = self.parent.name if self.parent is not None else "none"
module_name = self.parent.parent.name if self.parent is not None and self.parent.parent is not None else "none"
Expand All @@ -102,6 +105,9 @@ def _set_item_tags(self) -> None:
if self._is_benchmark:
self.set_tag(test.TYPE, BENCHMARK)

if self._overwritten_suite_name is not None:
self.set_tag(test.SUITE, self._overwritten_suite_name)

def _set_efd_tags(self) -> None:
if self._efd_is_retry:
self.set_tag(TEST_IS_RETRY, self._efd_is_retry)
Expand Down Expand Up @@ -202,6 +208,22 @@ def finish_itr_skipped(self) -> None:
self.mark_itr_skipped()
self.finish_test(TestStatus.SKIP)

def overwrite_attributes(
self,
name: Optional[str] = None,
suite_name: Optional[str] = None,
parameters: Optional[str] = None,
codeowners: Optional[List[str]] = None,
) -> None:
if name is not None:
self.name = name
if suite_name is not None:
self._overwritten_suite_name = suite_name
if parameters is not None:
self.set_parameters(parameters)
if codeowners is not None:
self._codeowners = codeowners

def add_coverage_data(self, coverage_data: Dict[Path, CoverageLines]) -> None:
self._coverage_data.add_covered_files(coverage_data)

Expand Down
21 changes: 21 additions & 0 deletions ddtrace/internal/ci_visibility/recorder.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
from ddtrace.internal.test_visibility._efd_mixins import EFDTestStatus
from ddtrace.internal.test_visibility._internal_item_ids import InternalTestId
from ddtrace.internal.test_visibility._itr_mixins import ITRMixin
from ddtrace.internal.test_visibility.api import InternalTest
from ddtrace.internal.test_visibility.coverage_lines import CoverageLines
from ddtrace.internal.utils.formats import asbool
from ddtrace.internal.utils.http import verify_url
Expand Down Expand Up @@ -1008,6 +1009,12 @@ def _on_session_get_codeowners() -> Optional[Codeowners]:
return CIVisibility.get_codeowners()


@_requires_civisibility_enabled
def _on_session_get_tracer() -> Optional[Tracer]:
log.debug("Getting tracer")
return CIVisibility.get_tracer()


@_requires_civisibility_enabled
def _on_session_is_atr_enabled() -> bool:
log.debug("Getting Auto Test Retries enabled")
Expand Down Expand Up @@ -1041,6 +1048,7 @@ def _register_session_handlers():
core.on("test_visibility.session.start", _on_start_session)
core.on("test_visibility.session.finish", _on_finish_session)
core.on("test_visibility.session.get_codeowners", _on_session_get_codeowners, "codeowners")
core.on("test_visibility.session.get_tracer", _on_session_get_tracer, "tracer")
core.on("test_visibility.session.get_path_codeowners", _on_session_get_path_codeowners, "path_codeowners")
core.on("test_visibility.session.get_workspace_path", _on_session_get_workspace_path, "workspace_path")
core.on("test_visibility.session.is_atr_enabled", _on_session_is_atr_enabled, "is_atr_enabled")
Expand Down Expand Up @@ -1191,6 +1199,18 @@ def _on_set_benchmark_data(set_benchmark_data_args: BenchmarkTestMixin.SetBenchm
CIVisibility.get_test_by_id(item_id).set_benchmark_data(data, is_benchmark)


@_requires_civisibility_enabled
def _on_test_overwrite_attributes(overwrite_attribute_args: InternalTest.OverwriteAttributesArgs):
item_id = overwrite_attribute_args.test_id
name = overwrite_attribute_args.name
suite_name = overwrite_attribute_args.suite_name
parameters = overwrite_attribute_args.parameters
codeowners = overwrite_attribute_args.codeowners

log.debug("Handling overwrite attributes: %s", overwrite_attribute_args)
CIVisibility.get_test_by_id(item_id).overwrite_attributes(name, suite_name, parameters, codeowners)


def _register_test_handlers():
log.debug("Registering test handlers")
core.on("test_visibility.test.discover", _on_discover_test)
Expand All @@ -1199,6 +1219,7 @@ def _register_test_handlers():
core.on("test_visibility.test.finish", _on_finish_test)
core.on("test_visibility.test.set_parameters", _on_set_test_parameters)
core.on("test_visibility.test.set_benchmark_data", _on_set_benchmark_data)
core.on("test_visibility.test.overwrite_attributes", _on_test_overwrite_attributes)


@_requires_civisibility_enabled
Expand Down
Loading

0 comments on commit ca9ee58

Please sign in to comment.