twister: support filtering tests using regex

Use --test-pattern to filter scenarios using regular expressions, for
example:

./scripts/twister --test-pattern '^kernel.semaphore.*' -v

will run all those test using this identifier pattern, and nothing else.
Multiple patterns are possible.

Signed-off-by: Anas Nashif <anas.nashif@intel.com>
This commit is contained in:
Anas Nashif 2025-07-12 11:55:02 -04:00
parent 5e4688fc15
commit ac4aabc10d
3 changed files with 45 additions and 16 deletions

View File

@ -253,6 +253,12 @@ Artificially long but functional example:
timeout would be multiplication of test timeout value, board-level timeout multiplier
and global timeout multiplier (this parameter)""")
parser.add_argument(
"--test-pattern", action="append",
help="""Run only the tests matching the specified pattern. The pattern
can include regular expressions.
""")
test_xor_subtest.add_argument(
"-s", "--test", "--scenario", action="append", type = norm_path,
help="""Run only the specified test suite scenario. These are named by

View File

@ -202,13 +202,12 @@ class TestPlan:
def discover(self):
self.handle_modules()
if self.options.test:
self.run_individual_testsuite = self.options.test
self.test_config = TestConfiguration(self.env.test_config)
self.add_configurations()
num = self.add_testsuites(testsuite_filter=self.run_individual_testsuite)
num = self.add_testsuites(testsuite_filter=self.options.test,
testsuite_pattern=self.options.test_pattern)
if num == 0:
raise TwisterRuntimeError("No testsuites found at the specified location...")
if self.load_errors:
@ -507,9 +506,35 @@ class TestPlan:
testcases.remove(case.detailed_name)
return testcases
def add_testsuites(self, testsuite_filter=None):
def _is_testsuite_selected(self, suite: TestSuite, testsuite_filter, testsuite_patterns_r):
"""Check if the testsuite is selected by the user."""
if not testsuite_filter and not testsuite_patterns_r:
# no matching requested, include all testsuites
return True
if testsuite_filter:
scenario = os.path.basename(suite.name)
if (
suite.name
and (suite.name in testsuite_filter or scenario in testsuite_filter)
):
return True
if testsuite_patterns_r:
for r in testsuite_patterns_r:
if r.search(suite.id):
return True
return False
def add_testsuites(self, testsuite_filter=None, testsuite_pattern=None):
if testsuite_filter is None:
testsuite_filter = []
testsuite_patterns_r = []
if testsuite_pattern is None:
testsuite_pattern = []
else:
for pattern in testsuite_pattern:
testsuite_patterns_r.append(re.compile(pattern))
for root in self.env.test_roots:
root = os.path.abspath(root)
@ -574,14 +599,11 @@ class TestPlan:
else:
suite.add_subcases(suite_dict)
if testsuite_filter:
scenario = os.path.basename(suite.name)
if (
suite.name
and (suite.name in testsuite_filter or scenario in testsuite_filter)
):
self.testsuites[suite.name] = suite
elif suite.name in self.testsuites:
if not self._is_testsuite_selected(suite, testsuite_filter,
testsuite_patterns_r):
# skip testsuite if they were not selected directly by the user
continue
if suite.name in self.testsuites:
msg = (
f"test suite '{suite.name}' in '{suite.yamlfile}' is already added"
)

View File

@ -570,6 +570,7 @@ def test_testplan_discover(
env.test_config = tmp_tc
testplan = TestPlan(env=env)
testplan.options = mock.Mock(
test_pattern=[],
test='ts1',
quarantine_list=[tmp_path / qf for qf in ql],
quarantine_verify=qv
@ -589,7 +590,7 @@ def test_testplan_discover(
with pytest.raises(exception) if exception else nullcontext():
testplan.discover()
testplan.add_testsuites.assert_called_once_with(testsuite_filter='ts1')
testplan.add_testsuites.assert_called_once_with(testsuite_filter='ts1', testsuite_pattern=[])
assert all([log in caplog.text for log in expected_logs])
@ -1186,7 +1187,7 @@ TESTDATA_9 = [
(['good_test/dummy.common.1', 'good_test/dummy.common.2', 'good_test/dummy.common.3'], False, True, 3, 1),
(['good_test/dummy.common.1', 'good_test/dummy.common.2',
'duplicate_test/dummy.common.1', 'duplicate_test/dummy.common.2'], False, True, 4, 1),
(['dummy.common.1', 'dummy.common.2'], False, False, 2, 1),
(['dummy.common.1', 'dummy.common.2'], False, False, 2, 2),
(['good_test/dummy.common.1', 'good_test/dummy.common.2', 'good_test/dummy.common.3'], True, True, 0, 1),
]
@ -1314,7 +1315,7 @@ tests:
testplan = TestPlan(env=env)
res = testplan.add_testsuites(testsuite_filter)
res = testplan.add_testsuites(testsuite_filter, testsuite_pattern=[])
assert res == expected_suite_count
assert testplan.load_errors == expected_errors