Skip to content

Commit

Permalink
Merge branch 'main' into brettlangdon/benchmark.improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
brettlangdon authored May 28, 2024
2 parents c7c8a1b + 0d695e6 commit 8156e02
Show file tree
Hide file tree
Showing 24 changed files with 403 additions and 90 deletions.
1 change: 1 addition & 0 deletions .circleci/config.templ.yml
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ jobs:

appsec_iast_packages:
<<: *machine_executor
parallelism: 10
steps:
- run_test:
pattern: 'appsec_iast_packages'
Expand Down
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ tests/contrib/django/django_app/appsec_urls.py @DataDog/asm-python
tests/contrib/django/test_django_appsec.py @DataDog/asm-python
tests/snapshots/tests*appsec*.json @DataDog/asm-python
tests/contrib/*/test*appsec*.py @DataDog/asm-python
scripts/iast/* @DataDog/asm-python

# Profiling
ddtrace/profiling @DataDog/profiling-python @DataDog/apm-core-python
Expand Down
141 changes: 141 additions & 0 deletions benchmarks/bm/iast_utils/ast_patching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#!/usr/bin/env python3

import os
import shutil


PROJECT_NAME = "my_project"
NUM_MODULES = 300 # Number of modules to create


# Template for a Python module generator
def module_template(module_number, import_modules):
imports_block = ""
if import_modules:
imports_block = "\n".join([f"from {import_module} import *" for import_module in import_modules])

body_block = f"""
import os
def func_{module_number}():
print('This is function func_{module_number} from module_{module_number}.py')
class Class{module_number}:
def __init__(self):
print('This is Class{module_number} from module_{module_number}.py')
def slice_test(self, arg):
return arg[1:3]
def index_access(self, arg):
return arg[1]
def os_path_join(self, arg1, arg2):
return os.path.join(arg1, arg2)
def string_concat(self, arg1, arg2):
return arg1 + arg2
def string_fstring(self, arg1, arg2):
return f'{{arg1}} {{arg2}}'
def string_format(self, arg1, arg2):
return '{{0}} {{1}}'.format(arg1, arg2)
def string_format_modulo(self, arg1, arg2):
return '%s %s' % (arg1, arg2)
def string_join(self, arg1, arg2):
return ''.join([arg1, arg2])
def string_decode(self, arg):
return arg.decode()
def string_encode(self, arg):
return arg.encode("utf-8")
def string_replace(self, arg, old, new):
return arg.replace(old, new)
def bytearray_extend(self, arg1, arg2):
return arg1.extend(arg2)
def string_upper(self, arg):
return arg.upper()
def string_lower(self, arg):
return arg.lower()
def string_swapcase(self, arg):
return arg.swapcase()
def string_title(self, arg):
return arg.title()
def string_capitalize(self, arg):
return arg.capitalize()
def string_casefold(self, arg):
return arg.casefold()
def string_translate(self, arg, table):
return arg.translate(table)
def string_zfill(self, arg, width):
return arg.zfill(width)
def string_ljust(self, arg, width):
return arg.ljust(width)
def str_call(self, arg):
return str(arg)
def bytes_call(self, arg):
return bytes(arg)
def bytearray_call(self, arg):
return bytearray(arg)
if __name__ == "__main__":
print('This is module_{module_number}.py')
"""
return f"{imports_block}\n{body_block}"


def create_project_structure():
project_name = PROJECT_NAME
num_modules = NUM_MODULES

# Create the project directory
os.makedirs(project_name, exist_ok=True)

# Create the __init__.py file to make the directory a package
with open(os.path.join(project_name, "__init__.py"), "w") as f:
f.write(f"# This is the __init__.py file for the {project_name} package\n")

# last file path
module_path = ""
# Create the modules
for i in range(1, num_modules + 1):
module_name = f"module_{i}.py"
module_path = os.path.join(project_name, module_name)

# Import all the previous modules in the last module only
if i == num_modules:
import_modules = [f"module_{j}" for j in range(1, i)]
else:
import_modules = None

# Render the template with context
rendered_content = module_template(i, import_modules)

with open(module_path, "w") as f:
f.write(rendered_content)

return module_path


def destroy_project_structure():
project_name = PROJECT_NAME
# Remove the project directory
shutil.rmtree(project_name)
5 changes: 5 additions & 0 deletions benchmarks/iast_ast_patching/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
no_iast: &base_variant
iast_enabled: 0

iast_enabled: &iast_enabled
iast_enabled: 1
29 changes: 29 additions & 0 deletions benchmarks/iast_ast_patching/scenario.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import os
import subprocess
import sys

import bm
from bm.iast_utils.ast_patching import create_project_structure
from bm.iast_utils.ast_patching import destroy_project_structure


class IAST_AST_Patching(bm.Scenario):
iast_enabled = bm.var_bool()

def run(self):
try:
python_file_path = create_project_structure()

env = os.environ.copy()
env["DD_IAST_ENABLED"] = str(self.iast_enabled)

subp_cmd = ["ddtrace-run", sys.executable, python_file_path]

def _(loops):
for _ in range(loops):
subprocess.check_output(subp_cmd, env=env)

yield _

finally:
destroy_project_structure()
1 change: 1 addition & 0 deletions ddtrace/appsec/_remoteconfiguration.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ def _appsec_rules_data(features: Mapping[str, Any], test_tracer: Optional[Tracer
_add_rules_to_list(features, "rules_override", "rules override", ruleset)
_add_rules_to_list(features, "scanners", "scanners", ruleset)
_add_rules_to_list(features, "processors", "processors", ruleset)
_add_rules_to_list(features, "actions", "actions", ruleset)
if ruleset:
return tracer._appsec_processor._update_rules({k: v for k, v in ruleset.items() if v is not None})

Expand Down
36 changes: 20 additions & 16 deletions ddtrace/contrib/pytest/_plugin_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from doctest import DocTest
import json
import os
from pathlib import Path
import re
from typing import Dict # noqa:F401

Expand All @@ -31,6 +32,8 @@
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.utils import _is_pytest_8_or_later
from ddtrace.contrib.pytest.utils import _pytest_version_supports_itr
from ddtrace.contrib.unittest import unpatch as unpatch_unittest
from ddtrace.ext import SpanTypes
from ddtrace.ext import test
Expand Down Expand Up @@ -75,12 +78,6 @@
COVER_SESSION = asbool(os.environ.get("_DD_COVER_SESSION", "false"))


def _is_pytest_8_or_later():
if hasattr(pytest, "version_tuple"):
return pytest.version_tuple >= (8, 0, 0)
return False


def encode_test_parameter(parameter):
param_repr = repr(parameter)
# if the representation includes an id() we'll remove it
Expand Down Expand Up @@ -868,13 +865,20 @@ def pytest_ddtrace_get_item_test_name(item):
return "%s.%s" % (item.cls.__name__, item.name)
return item.name

@staticmethod
@pytest.hookimpl(trylast=True)
def pytest_terminal_summary(terminalreporter, exitstatus, config):
# Reports coverage if experimental session-level coverage is enabled.
if USE_DD_COVERAGE and COVER_SESSION:
ModuleCodeCollector.report()
try:
ModuleCodeCollector.write_json_report_to_file("dd_coverage.json")
except Exception:
log.debug("Failed to write coverage report to file", exc_info=True)
# 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():

@staticmethod
@pytest.hookimpl(trylast=True)
def pytest_terminal_summary(terminalreporter, exitstatus, config):
# Reports coverage if experimental session-level coverage is enabled.
if USE_DD_COVERAGE and COVER_SESSION:
from ddtrace.ext.git import extract_workspace_path

workspace_path = Path(extract_workspace_path())

ModuleCodeCollector.report(workspace_path)
try:
ModuleCodeCollector.write_json_report_to_file("dd_coverage.json")
except Exception:
log.debug("Failed to write coverage report to file", exc_info=True)
2 changes: 2 additions & 0 deletions ddtrace/contrib/pytest/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@

# XFail Reason
XFAIL_REASON = "pytest.xfail.reason"

ITR_MIN_SUPPORTED_VERSION = (6, 8, 0)
23 changes: 19 additions & 4 deletions ddtrace/contrib/pytest/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@
"""
import os
from pathlib import Path
from typing import Dict # noqa:F401

import pytest

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."
Expand All @@ -24,14 +27,21 @@


def _is_enabled_early(early_config):
"""Hackily checks if the ddtrace plugin is enabled before the config is fully populated.
"""Checks if the ddtrace plugin is enabled before the config is fully populated.
This is necessary because the module watchdog for coverage collection needs to be enabled as early as possible.
This is necessary because the module watchdog for coverage collectio needs to be enabled as early as possible.
Note: since coverage is used for ITR purposes, we only check if the plugin is enabled if the pytest version supports
ITR
"""
if not _pytest_version_supports_itr():
return False

if (
"--no-ddtrace" in early_config.invocation_params.args
or early_config.getini("ddtrace") is False
or early_config.getini("no-ddtrace")
or "ddtrace" in early_config.inicfg
and early_config.getini("ddtrace") is False
):
return False

Expand Down Expand Up @@ -97,10 +107,15 @@ def pytest_load_initial_conftests(early_config, parser, args):
COVER_SESSION = asbool(os.environ.get("_DD_COVER_SESSION", "false"))

if USE_DD_COVERAGE:
from ddtrace.ext.git import extract_workspace_path
from ddtrace.internal.coverage.code import ModuleCodeCollector

workspace_path = Path(extract_workspace_path())

log.debug("Installing ModuleCodeCollector with include_paths=%s", [workspace_path])

if not ModuleCodeCollector.is_installed():
ModuleCodeCollector.install()
ModuleCodeCollector.install(include_paths=[workspace_path])
if COVER_SESSION:
ModuleCodeCollector.start_coverage()
else:
Expand Down
17 changes: 17 additions & 0 deletions ddtrace/contrib/pytest/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import pytest

from ddtrace.contrib.pytest.constants import ITR_MIN_SUPPORTED_VERSION


def _get_pytest_version_tuple():
if hasattr(pytest, "version_tuple"):
return pytest.version_tuple
return tuple(map(int, pytest.__version__.split(".")))


def _is_pytest_8_or_later():
return _get_pytest_version_tuple() >= (8, 0, 0)


def _pytest_version_supports_itr():
return _get_pytest_version_tuple() >= ITR_MIN_SUPPORTED_VERSION
Loading

0 comments on commit 8156e02

Please sign in to comment.