Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Speed up testplan startup by avoiding repeated source-file tokenization in ``LocationMetadata.from_object``: functions now resolve their location directly from ``__code__``, and parametrized testcases reuse a single template lookup across all generated variants.
11 changes: 5 additions & 6 deletions testplan/testing/multitest/suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,10 @@ def wrapper(function: Any) -> Any:
raise err

# Register generated functions as testcases
template_location = TestCaseStaticMetadata(
LocationMetadata.from_object(function)
)

for func in functions:
# this has to be called before wrappers otherwise wrappers can
# fail if they rely on ``__testcase__``
Expand All @@ -710,12 +714,7 @@ def wrapper(function: Any) -> Any:

__GENERATED_TESTCASES__.append(func)

add_testcase_metadata(
func,
TestCaseStaticMetadata(
LocationMetadata.from_object(function)
),
)
add_testcase_metadata(func, template_location)

return function

Expand Down
15 changes: 11 additions & 4 deletions testplan/testing/multitest/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,19 @@ def from_object(cls, obj: object) -> Optional["LocationMetadata"]:
return getattr(obj, LOCATION_METADATA_ATTRIBUTE) # type: ignore[no-any-return]
try:
object_name = obj.__name__ # type: ignore[attr-defined]
file = getsourcefile(obj) # type: ignore[arg-type]
_, line_no = getsourcelines(obj) # type: ignore[arg-type]
code = getattr(obj, "__code__", None)
if code is not None:
# Functions expose the defining file and first line directly
# on their code object — no file I/O or tokenization needed.
file = code.co_filename or ""
line_no = code.co_firstlineno
else:
file = getsourcefile(obj) # type: ignore[arg-type]
_, line_no = getsourcelines(obj) # type: ignore[arg-type]
except Exception:
return None # we do best effort here
else:
return cls(object_name, file or "", line_no)

return cls(object_name, file, line_no)


@dataclass
Expand Down
48 changes: 47 additions & 1 deletion tests/unit/testplan/testing/multitest/test_suite.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""Unit tests for the testplan.testing.multitest.suite module."""

import re
from unittest import mock

import pytest

from testplan.common.utils.exceptions import should_raise
from testplan.common.utils.interface import MethodSignatureMismatch
from testplan.common.utils.strings import format_description
from testplan.testing.multitest import suite
from testplan.testing.multitest import suite, test_metadata
from testplan.testing.multitest.test_metadata import LocationMetadata
from testplan.testing import tagging


Expand Down Expand Up @@ -248,3 +250,47 @@ def test_skip_if_signature():
)
def test_format_description(text, expected):
format_description(text) == expected


def _location_sample_function(self, env, result):
pass


def test_from_object_function_skips_getsourcelines():
with (
mock.patch.object(
test_metadata, "getsourcelines"
) as mocked_getsourcelines,
mock.patch.object(
test_metadata, "getsourcefile"
) as mocked_getsourcefile,
):
LocationMetadata.from_object(_location_sample_function)

mocked_getsourcelines.assert_not_called()
mocked_getsourcefile.assert_not_called()


def test_parametrization_template_inspected_once():
real_from_object = LocationMetadata.from_object
template_calls = []

def tracking_from_object(obj):
if getattr(obj, "__name__", None) == "case":
template_calls.append(obj)
return real_from_object(obj)

with mock.patch.object(
LocationMetadata, "from_object", side_effect=tracking_from_object
):

@suite.testsuite
class ParamSuite:
@suite.testcase(parameters=[range(3)])
def case(self, env, result, value):
pass

assert len(template_calls) == 1, (
"Parametrization template should be inspected once across all "
f"variants; got {len(template_calls)} calls"
)
Loading