From 1116c8d15fd4ce3b206bb5bf9058cad1fa328f71 Mon Sep 17 00:00:00 2001 From: Alberto Vara Date: Mon, 20 Nov 2023 11:35:05 +0100 Subject: [PATCH] chore(iast): update test to find leaks (#7629) Update IAST test with memray to find possible leaksm. Add regression tests for https://github.com/DataDog/dd-trace-py/pull/7630 Those tests implement memray and pytest-memray, those are inspired in this PR https://github.com/DataDog/dd-trace-py/pull/7112 thanks @pablogsal More Info: https://github.com/bloomberg/memray https://github.com/bloomberg/pytest-memray ## Checklist - [x] Change(s) are motivated and described in the PR description. - [x] Testing strategy is described if automated tests are not included in the PR. - [x] Risk is outlined (performance impact, potential for breakage, maintainability, etc). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] [Library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) are followed. If no release note is required, add label `changelog/no-changelog`. - [x] Documentation is included (in-code, generated user docs, [public corp docs](https://github.com/DataDog/documentation/)). - [x] Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Title is accurate. - [x] No unnecessary changes are introduced. - [x] Description motivates each change. - [x] Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [x] Testing strategy adequately addresses listed risk(s). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] Release note makes sense to a user of the library. - [x] Reviewer has explicitly acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment. - [x] Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting) - [x] If this PR touches code that signs or publishes builds or packages, or handles credentials of any kind, I've requested a review from `@DataDog/security-design-and-guidance`. - [x] This PR doesn't touch any of that. --------- Co-authored-by: Gabriele N. Tornetta --- .circleci/config.templ.yml | 9 +- .riot/requirements/1b5d605.txt | 37 ++++ .riot/requirements/7121e51.txt | 35 ++++ .riot/requirements/861bff5.txt | 37 ++++ .riot/requirements/f43b103.txt | 38 ++++ riotfile.py | 15 ++ tests/.suitespec.json | 9 + tests/appsec/iast/test_iast_mem_check.py | 84 -------- tests/appsec/iast_memcheck/__init__.py | 0 tests/appsec/iast_memcheck/_stacktrace_py.py | 38 ++++ tests/appsec/iast_memcheck/conftest.py | 9 + .../appsec/iast_memcheck/fixtures/__init__.py | 0 .../iast_memcheck/fixtures/stacktrace.py | 99 ++++++++++ .../iast_memcheck/test_iast_mem_check.py | 184 ++++++++++++++++++ 14 files changed, 509 insertions(+), 85 deletions(-) create mode 100644 .riot/requirements/1b5d605.txt create mode 100644 .riot/requirements/7121e51.txt create mode 100644 .riot/requirements/861bff5.txt create mode 100644 .riot/requirements/f43b103.txt delete mode 100644 tests/appsec/iast/test_iast_mem_check.py create mode 100644 tests/appsec/iast_memcheck/__init__.py create mode 100644 tests/appsec/iast_memcheck/_stacktrace_py.py create mode 100644 tests/appsec/iast_memcheck/conftest.py create mode 100644 tests/appsec/iast_memcheck/fixtures/__init__.py create mode 100644 tests/appsec/iast_memcheck/fixtures/stacktrace.py create mode 100644 tests/appsec/iast_memcheck/test_iast_mem_check.py diff --git a/.circleci/config.templ.yml b/.circleci/config.templ.yml index f693bdf31b6..275637bb7c6 100644 --- a/.circleci/config.templ.yml +++ b/.circleci/config.templ.yml @@ -432,7 +432,14 @@ jobs: <<: *machine_executor steps: - run_test: - pattern: 'appsec_iast' + pattern: 'appsec_iast$' + snapshot: true + + appsec_iast_memcheck: + <<: *machine_executor + steps: + - run_test: + pattern: 'appsec_iast_memcheck' snapshot: true appsec_integrations: diff --git a/.riot/requirements/1b5d605.txt b/.riot/requirements/1b5d605.txt new file mode 100644 index 00000000000..856fbaf7ae5 --- /dev/null +++ b/.riot/requirements/1b5d605.txt @@ -0,0 +1,37 @@ +# +# This file is autogenerated by pip-compile with python 3.9 +# To update, run: +# +# pip-compile --no-annotate --resolver=backtracking .riot/requirements/1b5d605.in +# +attrs==23.1.0 +certifi==2023.7.22 +cffi==1.16.0 +charset-normalizer==3.3.2 +coverage[toml]==7.3.2 +cryptography==41.0.5 +exceptiongroup==1.1.3 +hypothesis==6.45.0 +idna==3.4 +iniconfig==2.0.0 +jinja2==3.1.2 +markdown-it-py==3.0.0 +markupsafe==2.1.3 +mdurl==0.1.2 +memray==1.10.0 +mock==5.1.0 +opentracing==2.4.0 +packaging==23.2 +pluggy==1.3.0 +pycparser==2.21 +pycryptodome==3.19.0 +pygments==2.16.1 +pytest==7.4.3 +pytest-cov==4.1.0 +pytest-memray==1.5.0 +pytest-mock==3.12.0 +requests==2.31.0 +rich==13.7.0 +sortedcontainers==2.4.0 +tomli==2.0.1 +urllib3==2.1.0 diff --git a/.riot/requirements/7121e51.txt b/.riot/requirements/7121e51.txt new file mode 100644 index 00000000000..367e9ee8fc7 --- /dev/null +++ b/.riot/requirements/7121e51.txt @@ -0,0 +1,35 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --no-annotate .riot/requirements/7121e51.in +# +attrs==23.1.0 +certifi==2023.7.22 +cffi==1.16.0 +charset-normalizer==3.3.2 +coverage[toml]==7.3.2 +cryptography==41.0.5 +hypothesis==6.45.0 +idna==3.4 +iniconfig==2.0.0 +jinja2==3.1.2 +markdown-it-py==3.0.0 +markupsafe==2.1.3 +mdurl==0.1.2 +memray==1.10.0 +mock==5.1.0 +opentracing==2.4.0 +packaging==23.2 +pluggy==1.3.0 +pycparser==2.21 +pycryptodome==3.19.0 +pygments==2.16.1 +pytest==7.4.3 +pytest-cov==4.1.0 +pytest-memray==1.5.0 +pytest-mock==3.12.0 +requests==2.31.0 +rich==13.7.0 +sortedcontainers==2.4.0 +urllib3==2.1.0 diff --git a/.riot/requirements/861bff5.txt b/.riot/requirements/861bff5.txt new file mode 100644 index 00000000000..d2348f73a03 --- /dev/null +++ b/.riot/requirements/861bff5.txt @@ -0,0 +1,37 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile --no-annotate --resolver=backtracking .riot/requirements/861bff5.in +# +attrs==23.1.0 +certifi==2023.7.22 +cffi==1.16.0 +charset-normalizer==3.3.2 +coverage[toml]==7.3.2 +cryptography==41.0.5 +exceptiongroup==1.1.3 +hypothesis==6.45.0 +idna==3.4 +iniconfig==2.0.0 +jinja2==3.1.2 +markdown-it-py==3.0.0 +markupsafe==2.1.3 +mdurl==0.1.2 +memray==1.10.0 +mock==5.1.0 +opentracing==2.4.0 +packaging==23.2 +pluggy==1.3.0 +pycparser==2.21 +pycryptodome==3.19.0 +pygments==2.16.1 +pytest==7.4.3 +pytest-cov==4.1.0 +pytest-memray==1.5.0 +pytest-mock==3.12.0 +requests==2.31.0 +rich==13.7.0 +sortedcontainers==2.4.0 +tomli==2.0.1 +urllib3==2.1.0 diff --git a/.riot/requirements/f43b103.txt b/.riot/requirements/f43b103.txt new file mode 100644 index 00000000000..3f155d23e04 --- /dev/null +++ b/.riot/requirements/f43b103.txt @@ -0,0 +1,38 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# pip-compile --no-annotate .riot/requirements/f43b103.in +# +attrs==23.1.0 +certifi==2023.7.22 +cffi==1.16.0 +charset-normalizer==3.3.2 +coverage[toml]==7.3.2 +cryptography==41.0.5 +exceptiongroup==1.1.3 +hypothesis==6.45.0 +idna==3.4 +iniconfig==2.0.0 +jinja2==3.1.2 +markdown-it-py==3.0.0 +markupsafe==2.1.3 +mdurl==0.1.2 +memray==1.10.0 +mock==5.1.0 +opentracing==2.4.0 +packaging==23.2 +pluggy==1.3.0 +pycparser==2.21 +pycryptodome==3.19.0 +pygments==2.16.1 +pytest==7.4.3 +pytest-cov==4.1.0 +pytest-memray==1.5.0 +pytest-mock==3.12.0 +requests==2.31.0 +rich==13.7.0 +sortedcontainers==2.4.0 +tomli==2.0.1 +typing-extensions==4.8.0 +urllib3==2.1.0 diff --git a/riotfile.py b/riotfile.py index 9ea2022450e..15c7c2893d1 100644 --- a/riotfile.py +++ b/riotfile.py @@ -150,6 +150,21 @@ def select_pys(min_version=MIN_PYTHON_VERSION, max_version=MAX_PYTHON_VERSION): "_DD_APPSEC_DEDUPLICATION_ENABLED": "false", }, ), + Venv( + name="appsec_iast_memcheck", + pys=select_pys(min_version="3.8", max_version="3.11"), + command="pytest {cmdargs} --memray --stacks=35 tests/appsec/iast_memcheck/", + pkgs={ + "requests": latest, + "pycryptodome": latest, + "cryptography": latest, + "pytest-memray": latest, + }, + env={ + "DD_IAST_REQUEST_SAMPLING": "100", # Override default 30% to analyze all IAST requests + "_DD_APPSEC_DEDUPLICATION_ENABLED": "false", + }, + ), Venv( name="appsec_integrations", command="pytest {cmdargs} tests/appsec/integrations/", diff --git a/tests/.suitespec.json b/tests/.suitespec.json index 8ce5c663d92..0c36d3a2566 100644 --- a/tests/.suitespec.json +++ b/tests/.suitespec.json @@ -407,6 +407,15 @@ "@appsec_iast", "tests/appsec/iast/*" ], + "appsec_iast_memcheck": [ + "@bootstrap", + "@core", + "@tracing", + "@appsec", + "@appsec_iast", + "tests/appsec/iast/*", + "tests/appsec/iast_memcheck/*" + ], "appsec_integrations": [ "@bootstrap", "@core", diff --git a/tests/appsec/iast/test_iast_mem_check.py b/tests/appsec/iast/test_iast_mem_check.py deleted file mode 100644 index f111e8ad553..00000000000 --- a/tests/appsec/iast/test_iast_mem_check.py +++ /dev/null @@ -1,84 +0,0 @@ -import os - -import pytest - -from ddtrace.appsec._constants import IAST -from ddtrace.appsec._iast._taint_tracking import get_tainted_ranges -from ddtrace.appsec._iast._utils import _is_python_version_supported as python_supported_by_iast -from ddtrace.internal import core -from tests.appsec.iast.aspects.conftest import _iast_patched_module - - -FIXTURES_PATH = "tests/appsec/iast/fixtures/propagation_path.py" - - -@pytest.mark.skipif(not python_supported_by_iast(), reason="Python version not supported by IAST") -@pytest.mark.parametrize( - "origin1, origin2", - [ - ("taintsource1", "taintsource2"), - ("taintsource", "taintsource"), - (b"taintsource1", "taintsource2"), - (b"taintsource1", b"taintsource2"), - ("taintsource1", b"taintsource2"), - (bytearray(b"taintsource1"), "taintsource2"), - (bytearray(b"taintsource1"), bytearray(b"taintsource2")), - ("taintsource1", bytearray(b"taintsource2")), - (bytearray(b"taintsource1"), b"taintsource2"), - (bytearray(b"taintsource1"), bytearray(b"taintsource2")), - (b"taintsource1", bytearray(b"taintsource2")), - ], -) -def test_propagation_memory_check(origin1, origin2, iast_span_defaults): - from ddtrace.appsec._iast._taint_tracking import OriginType - from ddtrace.appsec._iast._taint_tracking import active_map_addreses_size - from ddtrace.appsec._iast._taint_tracking import create_context - from ddtrace.appsec._iast._taint_tracking import initializer_size - from ddtrace.appsec._iast._taint_tracking import num_objects_tainted - from ddtrace.appsec._iast._taint_tracking import reset_context - from ddtrace.appsec._iast._taint_tracking import taint_pyobject - from ddtrace.vendor import psutil - from tests.appsec.iast.fixtures.propagation_path import propagation_memory_check - - expected_result = propagation_memory_check(origin1, origin2) - - start_memory = psutil.Process(os.getpid()).memory_info().rss - - mod = _iast_patched_module("tests.appsec.iast.fixtures.propagation_path") - - _num_objects_tainted = 0 - _active_map_addreses_size = 0 - _initializer_size = 0 - for _ in range(500): - create_context() - tainted_string_1 = taint_pyobject( - origin1, source_name="path1", source_value=origin1, source_origin=OriginType.PATH - ) - tainted_string_2 = taint_pyobject( - origin2, source_name="path2", source_value=origin2, source_origin=OriginType.PARAMETER - ) - result = mod.propagation_memory_check(tainted_string_1, tainted_string_2) - - assert result == expected_result - - span_report = core.get_item(IAST.CONTEXT_KEY, span=iast_span_defaults) - assert len(span_report.sources) > 0 - assert len(span_report.vulnerabilities) > 0 - assert len(get_tainted_ranges(result)) == 6 - - if _num_objects_tainted == 0: - _num_objects_tainted = num_objects_tainted() - assert _num_objects_tainted > 0 - if _active_map_addreses_size == 0: - _active_map_addreses_size = active_map_addreses_size() - assert _active_map_addreses_size > 0 - if _initializer_size == 0: - _initializer_size = initializer_size() - assert _initializer_size > 0 - - assert _num_objects_tainted == num_objects_tainted() - assert _active_map_addreses_size == active_map_addreses_size() - assert _initializer_size == initializer_size() - reset_context() - end_memory = psutil.Process(os.getpid()).memory_info().rss - assert end_memory < start_memory * 1.05, "Memory increment to {} from {}".format(end_memory, start_memory) diff --git a/tests/appsec/iast_memcheck/__init__.py b/tests/appsec/iast_memcheck/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/appsec/iast_memcheck/_stacktrace_py.py b/tests/appsec/iast_memcheck/_stacktrace_py.py new file mode 100644 index 00000000000..8366fe1d270 --- /dev/null +++ b/tests/appsec/iast_memcheck/_stacktrace_py.py @@ -0,0 +1,38 @@ +import inspect +import os +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: # pragma: no cover + from typing import Optional + from typing import Text + from typing import Tuple + + +FIRST_FRAME_NO_DDTRACE = 1 + +DD_TRACE_INSTALLED_PREFIX = os.sep + "ddtrace" + os.sep +SITE_PACKAGES_PREFIX = os.sep + "site-packages" + os.sep +TESTS_PREFIX = os.sep + "tests" + os.sep + + +def get_info_frame(cwd): + # type: (Text) -> Optional[Tuple[Text, int]] + """Get the filename (path + filename) and line number of the original wrapped function to report it. + + CAVEAT: We should migrate this function to native code to improve the performance. + """ + stack = inspect.stack() + for frame in stack[FIRST_FRAME_NO_DDTRACE:]: + filename = frame.filename + lineno = frame.lineno + if ( + (DD_TRACE_INSTALLED_PREFIX in filename and TESTS_PREFIX not in filename) + or (cwd not in filename) + or (SITE_PACKAGES_PREFIX in filename) + ): + continue + + return filename, lineno + + return None diff --git a/tests/appsec/iast_memcheck/conftest.py b/tests/appsec/iast_memcheck/conftest.py new file mode 100644 index 00000000000..cc5a7e42ffa --- /dev/null +++ b/tests/appsec/iast_memcheck/conftest.py @@ -0,0 +1,9 @@ +import pytest + +from tests.appsec.iast.conftest import iast_span + + +@pytest.fixture +def iast_span_defaults(tracer): + for t in iast_span(tracer, dict(DD_IAST_ENABLED="true")): + yield t diff --git a/tests/appsec/iast_memcheck/fixtures/__init__.py b/tests/appsec/iast_memcheck/fixtures/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/appsec/iast_memcheck/fixtures/stacktrace.py b/tests/appsec/iast_memcheck/fixtures/stacktrace.py new file mode 100644 index 00000000000..fa08bcc334e --- /dev/null +++ b/tests/appsec/iast_memcheck/fixtures/stacktrace.py @@ -0,0 +1,99 @@ +import os + +from ddtrace.appsec._iast._stacktrace import get_info_frame +from tests.appsec.iast_memcheck._stacktrace_py import get_info_frame as get_info_frame_py + + +CWD = os.path.abspath(os.getcwd()) + + +def func_1(a, b, c): + return func_2(a, b, c) + + +def func_2(a, b, c): + return func_3(a, b, c) + + +def func_3(a, b, c): + return func_4(a, b, c) + + +def func_4(a, b, c): + return func_5(a, b, c) + + +def func_5(a, b, c): + return func_6(a, b, c) + + +def func_6(a, b, c): + return func_7(a, b, c) + + +def func_7(a, b, c): + return func_8(a, b, c) + + +def func_8(a, b, c): + return func_9(a, b, c) + + +def func_9(a, b, c): + return func_10(a, b, c) + + +def func_10(a, b, c): + return func_11(a, b, c) + + +def func_11(a, b, c): + return func_12(a, b, c) + + +def func_12(a, b, c): + return func_13(a, b, c) + + +def func_13(a, b, c): + return func_14(a, b, c) + + +def func_14(a, b, c): + return func_15(a, b, c) + + +def func_15(a, b, c): + return func_16(a, b, c) + + +def func_16(a, b, c): + return func_17(a, b, c) + + +def func_17(a, b, c): + return func_18(a, b, c) + + +def func_18(a, b, c): + return func_19(a, b, c) + + +def func_19(a, b, c): + return func_20(a, b, c) + + +def func_20(a, b, c): + if b == "py": + func = get_info_frame_py + else: + func = get_info_frame + if a == "empty_byte": + frame_info = func(b"") + elif a == "empty_string": + frame_info = func("") + elif a == "random_string": + frame_info = func("aaaa") + else: + frame_info = func(CWD) + return frame_info diff --git a/tests/appsec/iast_memcheck/test_iast_mem_check.py b/tests/appsec/iast_memcheck/test_iast_mem_check.py new file mode 100644 index 00000000000..c6aa9bd6f92 --- /dev/null +++ b/tests/appsec/iast_memcheck/test_iast_mem_check.py @@ -0,0 +1,184 @@ +import os + +import pytest +from pytest_memray import LeaksFilterFunction +from pytest_memray import Stack + +from ddtrace.appsec._constants import IAST +from ddtrace.appsec._iast._stacktrace import get_info_frame +from ddtrace.appsec._iast._taint_tracking import OriginType +from ddtrace.appsec._iast._taint_tracking import active_map_addreses_size +from ddtrace.appsec._iast._taint_tracking import create_context +from ddtrace.appsec._iast._taint_tracking import get_tainted_ranges +from ddtrace.appsec._iast._taint_tracking import initializer_size +from ddtrace.appsec._iast._taint_tracking import num_objects_tainted +from ddtrace.appsec._iast._taint_tracking import reset_context +from ddtrace.appsec._iast._taint_tracking import taint_pyobject +from ddtrace.appsec._iast._utils import _is_python_version_supported as python_supported_by_iast +from ddtrace.internal import core +from tests.appsec.iast.aspects.conftest import _iast_patched_module +from tests.appsec.iast.fixtures.propagation_path import propagation_memory_check +from tests.appsec.iast_memcheck._stacktrace_py import get_info_frame as get_info_frame_py +from tests.appsec.iast_memcheck.fixtures.stacktrace import func_1 + + +FIXTURES_PATH = "tests/appsec/iast/fixtures/propagation_path.py" + +LOOPS = 5 +CWD = os.path.abspath(os.getcwd()) +ALLOW_LIST = ["iast_memcheck/test_iast_mem_check.py", "fixtures/stacktrace.py"] +DISALLOW_LIST = ["_iast/_ast/visitor", "_pytest/assertion/rewrite", "coverage/", "internal/ci_visibility/"] + +mod = _iast_patched_module("tests.appsec.iast.fixtures.propagation_path") + + +class IASTFilter(LeaksFilterFunction): + def __call__(self, stack: Stack) -> bool: + for frame in stack.frames: + for disallowed_element in DISALLOW_LIST: + if disallowed_element in frame.filename: + return False + + for allowed_element in ALLOW_LIST: + if allowed_element in frame.filename: + return True + + return False + + +@pytest.mark.skipif(not python_supported_by_iast(), reason="Python version not supported by IAST") +@pytest.mark.limit_leaks("19 KB", filter_fn=IASTFilter()) +@pytest.mark.parametrize( + "origin1, origin2", + [ + ("taintsource1", "taintsource2"), + ("taintsource", "taintsource"), + (b"taintsource1", "taintsource2"), + (b"taintsource1", b"taintsource2"), + ("taintsource1", b"taintsource2"), + (bytearray(b"taintsource1"), "taintsource2"), + (bytearray(b"taintsource1"), bytearray(b"taintsource2")), + ("taintsource1", bytearray(b"taintsource2")), + (bytearray(b"taintsource1"), b"taintsource2"), + (bytearray(b"taintsource1"), bytearray(b"taintsource2")), + (b"taintsource1", bytearray(b"taintsource2")), + ], +) +def test_propagation_memory_check(origin1, origin2, iast_span_defaults): + _num_objects_tainted = 0 + _active_map_addreses_size = 0 + _initializer_size = 0 + for _ in range(LOOPS): + create_context() + tainted_string_1 = taint_pyobject( + origin1, source_name="path1", source_value=origin1, source_origin=OriginType.PATH + ) + tainted_string_2 = taint_pyobject( + origin2, source_name="path2", source_value=origin2, source_origin=OriginType.PARAMETER + ) + result = mod.propagation_memory_check(tainted_string_1, tainted_string_2) + + span_report = core.get_item(IAST.CONTEXT_KEY, span=iast_span_defaults) + assert len(span_report.sources) > 0 + assert len(span_report.vulnerabilities) > 0 + assert len(get_tainted_ranges(result)) == 6 + + if _num_objects_tainted == 0: + _num_objects_tainted = num_objects_tainted() + assert _num_objects_tainted > 0 + if _active_map_addreses_size == 0: + _active_map_addreses_size = active_map_addreses_size() + assert _active_map_addreses_size > 0 + if _initializer_size == 0: + _initializer_size = initializer_size() + assert _initializer_size > 0 + + assert _num_objects_tainted == num_objects_tainted() + assert _active_map_addreses_size == active_map_addreses_size() + assert _initializer_size == initializer_size() + reset_context() + + +@pytest.mark.limit_leaks("450 B", filter_fn=IASTFilter()) +def test_stacktrace_memory_check(): + for _ in range(LOOPS): + frame_info = func_1("", "2", "3") + if not frame_info: + pytest.fail("No stacktrace") + + file_name, line_number = frame_info + assert file_name + assert line_number > 0 + + +@pytest.mark.limit_leaks("301 B", filter_fn=IASTFilter()) +def test_stacktrace_memory_check_direct_call(): + for _ in range(LOOPS): + frame_info = get_info_frame(CWD) + if not frame_info: + pytest.fail("No stacktrace") + + file_name, line_number = frame_info + assert file_name + assert line_number > 0 + + +@pytest.mark.limit_leaks("460 KB", filter_fn=IASTFilter()) +def test_stacktrace_memory_check_no_native(): + for _ in range(LOOPS): + frame_info = func_1("", "py", "3") + if not frame_info: + pytest.fail("No stacktrace") + + file_name, line_number = frame_info + assert file_name + assert line_number > 0 + + +@pytest.mark.limit_leaks("24 KB", filter_fn=IASTFilter()) +def test_stacktrace_memory_check_no_native_direct_call(): + for _ in range(2): + frame_info = get_info_frame_py(CWD) + if not frame_info: + pytest.fail("No stacktrace") + + file_name, line_number = frame_info + assert file_name + assert line_number > 0 + + +@pytest.mark.limit_leaks("370 B", filter_fn=IASTFilter()) +def test_stacktrace_memory_empty_byte_check(): + for _ in range(LOOPS): + frame_info = func_1("empty_byte", "2", "3") + if not frame_info: + pytest.fail("No stacktrace") + + file_name, line_number = frame_info + assert file_name + assert line_number > 0 + + +@pytest.mark.limit_leaks("370 B", filter_fn=IASTFilter()) +def test_stacktrace_memory_empty_string_check(): + for _ in range(LOOPS): + frame_info = func_1("empty_string", "2", "3") + if not frame_info: + pytest.fail("No stacktrace") + + file_name, line_number = frame_info + assert file_name + assert line_number > 0 + + +@pytest.mark.limit_leaks("2.5 KB", filter_fn=IASTFilter()) +def test_stacktrace_memory_random_string_check(): + """2.1 KB is enough but CI allocates 1.0 MB bytes""" + for _ in range(LOOPS): + frame_info = func_1("random_string", "2", "3") + if not frame_info: + pytest.fail("No stacktrace") + + file_name, line_number = frame_info + assert file_name == "" + assert line_number == 0