Skip to content

Commit dee79d2

Browse files
Yuval Peressnashif
authored andcommitted
ztest: Add register functionality
Add new functionality to ztest to improve test modularity. The two primary new entry points are: * ztest_register_test_suite * ztest_run_registered_test_suites When registering a new test suite, users provide the name as well as an optional predicate used to filter the tests for each run. Using NULL as the predicate ensures that the test is run exactly once (after which it is automatically filtered from future runs). Calls to ztest_run_registered_test_suites take a state pointer as an argument. This allows the the pragma functions to decide whether the test should be run. The biggest benefit of this system (other than the ability to filter tests and maintain a larger test state) is the ability to better modularize the test source code. Instead of all the various tests having to coordinate and the main function having to know which tests to run, each source file manages registering its own test suite and handling the conditions for running the suite. Signed-off-by: Yuval Peress <[email protected]>
1 parent 87c1f9a commit dee79d2

File tree

19 files changed

+625
-42
lines changed

19 files changed

+625
-42
lines changed

include/linker/common-ram.ld

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,7 @@
140140
#ifdef CONFIG_USERSPACE
141141
_static_kernel_objects_end = .;
142142
#endif
143+
144+
#if defined(CONFIG_ZTEST)
145+
ITERABLE_SECTION_RAM(ztest_suite_node, 4)
146+
#endif /* CONFIG_ZTEST */

scripts/pylib/twister/twisterlib.py

Lines changed: 116 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
#
44
# Copyright (c) 2018 Intel Corporation
55
# SPDX-License-Identifier: Apache-2.0
6-
76
import os
87
import contextlib
98
import string
@@ -34,6 +33,7 @@
3433
import yaml
3534
import json
3635
from multiprocessing import Lock, Process, Value
36+
from typing import List
3737

3838
try:
3939
# Use the C LibYAML parser if available, rather than the Python parser.
@@ -1576,6 +1576,41 @@ class DisablePyTestCollectionMixin(object):
15761576
__test__ = False
15771577

15781578

1579+
class ScanPathResult:
1580+
"""Result of the TestCase.scan_path function call.
1581+
1582+
Attributes:
1583+
matches A list of test cases
1584+
warnings A string containing one or more
1585+
warnings to display
1586+
has_registered_test_suites Whether or not the path contained any
1587+
calls to the ztest_register_test_suite
1588+
macro.
1589+
has_run_registered_test_suites Whether or not the path contained at
1590+
least one call to
1591+
ztest_run_registered_test_suites.
1592+
"""
1593+
def __init__(self,
1594+
matches: List[str] = None,
1595+
warnings: str = None,
1596+
has_registered_test_suites: bool = False,
1597+
has_run_registered_test_suites: bool = False):
1598+
self.matches = matches
1599+
self.warnings = warnings
1600+
self.has_registered_test_suites = has_registered_test_suites
1601+
self.has_run_registered_test_suites = has_run_registered_test_suites
1602+
1603+
def __eq__(self, other):
1604+
if not isinstance(other, ScanPathResult):
1605+
return False
1606+
return (sorted(self.matches) == sorted(other.matches) and
1607+
self.warnings == other.warnings and
1608+
(self.has_registered_test_suites ==
1609+
other.has_registered_test_suites) and
1610+
(self.has_run_registered_test_suites ==
1611+
other.has_run_registered_test_suites))
1612+
1613+
15791614
class TestCase(DisablePyTestCollectionMixin):
15801615
"""Class representing a test application
15811616
"""
@@ -1662,29 +1697,42 @@ def scan_file(inf_name):
16621697
# line--as we only search starting the end of this match
16631698
br"^\s*ztest_test_suite\(\s*(?P<suite_name>[a-zA-Z0-9_]+)\s*,",
16641699
re.MULTILINE)
1700+
registered_suite_regex = re.compile(
1701+
br"^\s*ztest_register_test_suite"
1702+
br"\(\s*(?P<suite_name>[a-zA-Z0-9_]+)\s*,",
1703+
re.MULTILINE)
16651704
stc_regex = re.compile(
1666-
br"^\s*" # empy space at the beginning is ok
1705+
br"""^\s* # empy space at the beginning is ok
16671706
# catch the case where it is declared in the same sentence, e.g:
16681707
#
16691708
# ztest_test_suite(mutex_complex, ztest_user_unit_test(TESTNAME));
1670-
br"(?:ztest_test_suite\([a-zA-Z0-9_]+,\s*)?"
1709+
# ztest_register_test_suite(n, p, ztest_user_unit_test(TESTNAME),
1710+
(?:ztest_
1711+
(?:test_suite\(|register_test_suite\([a-zA-Z0-9_]+\s*,\s*)
1712+
[a-zA-Z0-9_]+\s*,\s*
1713+
)?
16711714
# Catch ztest[_user]_unit_test-[_setup_teardown](TESTNAME)
1672-
br"ztest_(?:1cpu_)?(?:user_)?unit_test(?:_setup_teardown)?"
1715+
ztest_(?:1cpu_)?(?:user_)?unit_test(?:_setup_teardown)?
16731716
# Consume the argument that becomes the extra testcse
1674-
br"\(\s*"
1675-
br"(?P<stc_name>[a-zA-Z0-9_]+)"
1717+
\(\s*(?P<stc_name>[a-zA-Z0-9_]+)
16761718
# _setup_teardown() variant has two extra arguments that we ignore
1677-
br"(?:\s*,\s*[a-zA-Z0-9_]+\s*,\s*[a-zA-Z0-9_]+)?"
1678-
br"\s*\)",
1719+
(?:\s*,\s*[a-zA-Z0-9_]+\s*,\s*[a-zA-Z0-9_]+)?
1720+
\s*\)""",
16791721
# We don't check how it finishes; we don't care
1680-
re.MULTILINE)
1722+
re.MULTILINE | re.VERBOSE)
16811723
suite_run_regex = re.compile(
16821724
br"^\s*ztest_run_test_suite\((?P<suite_name>[a-zA-Z0-9_]+)\)",
16831725
re.MULTILINE)
1726+
registered_suite_run_regex = re.compile(
1727+
br"^\s*ztest_run_registered_test_suites\("
1728+
br"(\*+|&)?(?P<state_identifier>[a-zA-Z0-9_]+)\)",
1729+
re.MULTILINE)
16841730
achtung_regex = re.compile(
16851731
br"(#ifdef|#endif)",
16861732
re.MULTILINE)
16871733
warnings = None
1734+
has_registered_test_suites = False
1735+
has_run_registered_test_suites = False
16881736

16891737
with open(inf_name) as inf:
16901738
if os.name == 'nt':
@@ -1695,52 +1743,94 @@ def scan_file(inf_name):
16951743

16961744
with contextlib.closing(mmap.mmap(**mmap_args)) as main_c:
16971745
suite_regex_match = suite_regex.search(main_c)
1698-
if not suite_regex_match:
1746+
registered_suite_regex_match = registered_suite_regex.search(
1747+
main_c)
1748+
1749+
if registered_suite_regex_match:
1750+
has_registered_test_suites = True
1751+
if registered_suite_run_regex.search(main_c):
1752+
has_run_registered_test_suites = True
1753+
1754+
if not suite_regex_match and not has_registered_test_suites:
16991755
# can't find ztest_test_suite, maybe a client, because
17001756
# it includes ztest.h
1701-
return None, None
1757+
return ScanPathResult(
1758+
matches=None,
1759+
warnings=None,
1760+
has_registered_test_suites=has_registered_test_suites,
1761+
has_run_registered_test_suites=has_run_registered_test_suites)
17021762

17031763
suite_run_match = suite_run_regex.search(main_c)
1704-
if not suite_run_match:
1764+
if suite_regex_match and not suite_run_match:
17051765
raise ValueError("can't find ztest_run_test_suite")
17061766

1767+
if suite_regex_match:
1768+
search_start = suite_regex_match.end()
1769+
else:
1770+
search_start = registered_suite_regex_match.end()
1771+
1772+
if suite_run_match:
1773+
search_end = suite_run_match.start()
1774+
else:
1775+
search_end = re.compile(br"\);", re.MULTILINE) \
1776+
.search(main_c, search_start) \
1777+
.end()
17071778
achtung_matches = re.findall(
17081779
achtung_regex,
1709-
main_c[suite_regex_match.end():suite_run_match.start()])
1780+
main_c[search_start:search_end])
17101781
if achtung_matches:
17111782
warnings = "found invalid %s in ztest_test_suite()" \
17121783
% ", ".join(sorted({match.decode() for match in achtung_matches},reverse = True))
17131784
_matches = re.findall(
17141785
stc_regex,
1715-
main_c[suite_regex_match.end():suite_run_match.start()])
1786+
main_c[search_start:search_end])
17161787
for match in _matches:
17171788
if not match.decode().startswith("test_"):
17181789
warnings = "Found a test that does not start with test_"
17191790
matches = [match.decode().replace("test_", "", 1) for match in _matches]
1720-
return matches, warnings
1791+
return ScanPathResult(
1792+
matches=matches,
1793+
warnings=warnings,
1794+
has_registered_test_suites=has_registered_test_suites,
1795+
has_run_registered_test_suites=has_run_registered_test_suites)
17211796

17221797
def scan_path(self, path):
17231798
subcases = []
1799+
has_registered_test_suites = False
1800+
has_run_registered_test_suites = False
17241801
for filename in glob.glob(os.path.join(path, "src", "*.c*")):
17251802
try:
1726-
_subcases, warnings = self.scan_file(filename)
1727-
if warnings:
1728-
logger.error("%s: %s" % (filename, warnings))
1729-
raise TwisterRuntimeError("%s: %s" % (filename, warnings))
1730-
if _subcases:
1731-
subcases += _subcases
1803+
result: ScanPathResult = self.scan_file(filename)
1804+
if result.warnings:
1805+
logger.error("%s: %s" % (filename, result.warnings))
1806+
raise TwisterRuntimeError(
1807+
"%s: %s" % (filename, result.warnings))
1808+
if result.matches:
1809+
subcases += result.matches
1810+
if result.has_registered_test_suites:
1811+
has_registered_test_suites = True
1812+
if result.has_run_registered_test_suites:
1813+
has_run_registered_test_suites = True
17321814
except ValueError as e:
17331815
logger.error("%s: can't find: %s" % (filename, e))
17341816

17351817
for filename in glob.glob(os.path.join(path, "*.c")):
17361818
try:
1737-
_subcases, warnings = self.scan_file(filename)
1738-
if warnings:
1739-
logger.error("%s: %s" % (filename, warnings))
1740-
if _subcases:
1741-
subcases += _subcases
1819+
result: ScanPathResult = self.scan_file(filename)
1820+
if result.warnings:
1821+
logger.error("%s: %s" % (filename, result.warnings))
1822+
if result.matches:
1823+
subcases += result.matches
17421824
except ValueError as e:
17431825
logger.error("%s: can't find: %s" % (filename, e))
1826+
1827+
if has_registered_test_suites and not has_run_registered_test_suites:
1828+
warning = \
1829+
"Found call to 'ztest_register_test_suite()' but no "\
1830+
"call to 'ztest_run_registered_test_suites()'"
1831+
logger.error(warning)
1832+
raise TwisterRuntimeError(warning)
1833+
17441834
return subcases
17451835

17461836
def parse_subcases(self, test_path):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
tests:
2+
test_d.check_1:
3+
tags: test_d
4+
build_only: True
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* Copyright 2021 Google LLC
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
ztest_register_test_suite(feature4, NULL,
8+
ztest_unit_test(test_unit_1a),
9+
ztest_unit_test(test_unit_1b));

scripts/tests/twister/test_testinstance.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313

1414
ZEPHYR_BASE = os.getenv("ZEPHYR_BASE")
1515
sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts/pylib/twister"))
16-
from twisterlib import TestInstance, BuildError, TestCase, TwisterException
16+
from twisterlib import (TestInstance, BuildError, TestCase, TwisterException,
17+
ScanPathResult)
1718

1819

1920
TESTDATA_1 = [
@@ -112,21 +113,45 @@ def test_get_unique_exception(testcase_root, workdir, name, exception):
112113
unique = TestCase(testcase_root, workdir, name)
113114
assert unique == exception
114115

116+
115117
TESTDATA_5 = [
116-
("testcases/tests/test_ztest.c", None, ['a', 'c', 'unit_a', 'newline', 'test_test_aa', 'user', 'last']),
117-
("testcases/tests/test_a/test_ztest_error.c", "Found a test that does not start with test_", ['1a', '1c', '2a', '2b']),
118-
("testcases/tests/test_a/test_ztest_error_1.c", "found invalid #ifdef, #endif in ztest_test_suite()", ['unit_1a', 'unit_1b', 'Unit_1c']),
118+
("testcases/tests/test_ztest.c",
119+
ScanPathResult(
120+
warnings=None,
121+
matches=['a', 'c', 'unit_a',
122+
'newline',
123+
'test_test_aa',
124+
'user', 'last'],
125+
has_registered_test_suites=False,
126+
has_run_registered_test_suites=False)),
127+
("testcases/tests/test_a/test_ztest_error.c",
128+
ScanPathResult(
129+
warnings="Found a test that does not start with test_",
130+
matches=['1a', '1c', '2a', '2b'],
131+
has_registered_test_suites=False,
132+
has_run_registered_test_suites=False)),
133+
("testcases/tests/test_a/test_ztest_error_1.c",
134+
ScanPathResult(
135+
warnings="found invalid #ifdef, #endif in ztest_test_suite()",
136+
matches=['unit_1a', 'unit_1b', 'Unit_1c'],
137+
has_registered_test_suites=False,
138+
has_run_registered_test_suites=False)),
139+
("testcases/tests/test_d/test_ztest_error_register_test_suite.c",
140+
ScanPathResult(
141+
warnings=None, matches=['unit_1a', 'unit_1b'],
142+
has_registered_test_suites=True,
143+
has_run_registered_test_suites=False)),
119144
]
120145

121-
@pytest.mark.parametrize("test_file, expected_warnings, expected_subcases", TESTDATA_5)
122-
def test_scan_file(test_data, test_file, expected_warnings, expected_subcases):
146+
@pytest.mark.parametrize("test_file, expected", TESTDATA_5)
147+
def test_scan_file(test_data, test_file, expected: ScanPathResult):
123148
'''Testing scan_file method with different ztest files for warnings and results'''
124149

125-
testcase = TestCase("/scripts/tests/twister/test_data/testcases/tests", ".", "test_a.check_1")
150+
testcase = TestCase("/scripts/tests/twister/test_data/testcases/tests", ".",
151+
"test_a.check_1")
126152

127-
results, warnings = testcase.scan_file(os.path.join(test_data, test_file))
128-
assert sorted(results) == sorted(expected_subcases)
129-
assert warnings == expected_warnings
153+
result: ScanPathResult = testcase.scan_file(os.path.join(test_data, test_file))
154+
assert result == expected
130155

131156

132157
TESTDATA_6 = [

scripts/tests/twister/test_testsuite_class.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def test_testsuite_add_testcases(class_testsuite):
3030
'test_c.check_2',
3131
'test_a.check_1',
3232
'test_a.check_2',
33+
'test_d.check_1',
3334
'sample_test.app']
3435
testcase_list = []
3536
for key in sorted(class_testsuite.testcases.keys()):
@@ -58,9 +59,18 @@ def test_add_configurations(test_data, class_testsuite, board_root_dir):
5859
def test_get_all_testcases(class_testsuite, all_testcases_dict):
5960
""" Testing get_all_testcases function of TestSuite class in Twister """
6061
class_testsuite.testcases = all_testcases_dict
61-
expected_tests = ['sample_test.app', 'test_a.check_1.1a', 'test_a.check_1.1c',
62-
'test_a.check_1.2a', 'test_a.check_1.2b', 'test_a.check_1.Unit_1c', 'test_a.check_1.unit_1a', 'test_a.check_1.unit_1b', 'test_a.check_2.1a', 'test_a.check_2.1c', 'test_a.check_2.2a', 'test_a.check_2.2b', 'test_a.check_2.Unit_1c', 'test_a.check_2.unit_1a', 'test_a.check_2.unit_1b', 'test_b.check_1', 'test_b.check_2', 'test_c.check_1', 'test_c.check_2']
63-
assert len(class_testsuite.get_all_tests()) == 19
62+
expected_tests = ['sample_test.app', 'test_a.check_1.1a',
63+
'test_a.check_1.1c',
64+
'test_a.check_1.2a', 'test_a.check_1.2b',
65+
'test_a.check_1.Unit_1c', 'test_a.check_1.unit_1a',
66+
'test_a.check_1.unit_1b', 'test_a.check_2.1a',
67+
'test_a.check_2.1c', 'test_a.check_2.2a',
68+
'test_a.check_2.2b', 'test_a.check_2.Unit_1c',
69+
'test_a.check_2.unit_1a', 'test_a.check_2.unit_1b',
70+
'test_b.check_1', 'test_b.check_2', 'test_c.check_1',
71+
'test_c.check_2', 'test_d.check_1.unit_1a',
72+
'test_d.check_1.unit_1b']
73+
assert len(class_testsuite.get_all_tests()) == len(expected_tests)
6474
assert sorted(class_testsuite.get_all_tests()) == sorted(expected_tests)
6575

6676
def test_get_platforms(class_testsuite, platforms_list):

subsys/testsuite/include/ztest.ld

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright 2021 Google LLC
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
SECTIONS
8+
{
9+
.data.ztest_suite_node_area : ALIGN(4)
10+
{
11+
_ztest_suite_node_list_start = .;
12+
KEEP(*(SORT_BY_NAME(._ztest_suite_node.static.*)))
13+
_ztest_suite_node_list_end = .;
14+
}
15+
}
16+
INSERT AFTER .data;

subsys/testsuite/unittest.cmake

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ target_compile_options(testbinary PRIVATE
6262
$<$<COMPILE_LANGUAGE:ASM>:${EXTRA_AFLAGS_AS_LIST}>
6363
)
6464

65+
target_link_options(testbinary PRIVATE
66+
-T "${ZEPHYR_BASE}/subsys/testsuite/include/ztest.ld"
67+
)
68+
6569
target_link_libraries(testbinary PRIVATE
6670
${EXTRA_LDFLAGS_AS_LIST}
6771
)

0 commit comments

Comments
 (0)