Skip to content

Commit a72244f

Browse files
golowanownashif
authored andcommitted
twister: ztest: harness: Fix missed TestCase statuses
Fix a problem of Ztest suite names not taken into account by Twister to identify a TestCase, so in some situations a Ztest test's status was not assigned to the proper TestCase and it remains 'None' whereas the actual status value lost, eventually the resulting total execution counters not correct. The issue was observed in these situations: * Ztest application with multiple test suites having same test names. * Ztest suite is 'skipped' entirely on execution with all its tests. The proposed solution extends Twister test case name for Ztest to include Ztest suite name, so the resulting identifier looks like: `<test_scenario_name>.<ztest_suite_name>.<ztest_name>` The above naming scheme now requires ztest_suite_name part to be provided for `--sub-test` command line option. Testcase identifiers in twister.json and testplan.json will also include ztest_suite_name component. The Twister Ztest(Test) Harness is improved to track all state changes known from the test application's log for Ztest suites and test cases, so now it parses log output from a Ztest application more scurpulously. Regular expressions to match log records are extended and optimized to compile them only once and, in some cases, fixed (suite summary). Signed-off-by: Dmitrii Golovanov <[email protected]>
1 parent 74d1f60 commit a72244f

File tree

18 files changed

+254
-110
lines changed

18 files changed

+254
-110
lines changed

scripts/pylib/twister/twisterlib/environment.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env python3
22
# vim: set syntax=python ts=4 :
33
#
4-
# Copyright (c) 2018 Intel Corporation
4+
# Copyright (c) 2018-2024 Intel Corporation
55
# Copyright 2022 NXP
66
# Copyright (c) 2024 Arm Limited (or its affiliates). All rights reserved.
77
#
@@ -149,7 +149,8 @@ def add_parse_arguments(parser = None):
149149
test_plan_report_xor.add_argument("--list-tests", action="store_true",
150150
help="""List of all sub-test functions recursively found in
151151
all --testsuite-root arguments. Note different sub-tests can share
152-
the same section name and come from different directories.
152+
the same test scenario identifier (section.subsection)
153+
and come from different directories.
153154
The output is flattened and reports --sub-test names only,
154155
not their directories. For instance net.socket.getaddrinfo_ok
155156
and net.socket.fd_set belong to different directories.
@@ -239,17 +240,22 @@ def add_parse_arguments(parser = None):
239240

240241
test_xor_subtest.add_argument(
241242
"-s", "--test", "--scenario", action="append", type = norm_path,
242-
help="Run only the specified testsuite scenario. These are named by "
243-
"<path/relative/to/Zephyr/base/section.name.in.testcase.yaml>")
243+
help="""Run only the specified test suite scenario. These are named by
244+
'path/relative/to/Zephyr/base/section.subsection_in_testcase_yaml',
245+
or just 'section.subsection' identifier. With '--testsuite-root' option
246+
the scenario will be found faster.
247+
""")
244248

245249
test_xor_subtest.add_argument(
246250
"--sub-test", action="append",
247-
help="""Recursively find sub-test functions and run the entire
248-
test section where they were found, including all sibling test
251+
help="""Recursively find sub-test functions (test cases) and run the entire
252+
test scenario (section.subsection) where they were found, including all sibling test
249253
functions. Sub-tests are named by:
250-
section.name.in.testcase.yaml.function_name_without_test_prefix
251-
Example: In kernel.fifo.fifo_loop: 'kernel.fifo' is a section name
252-
and 'fifo_loop' is a name of a function found in main.c without test prefix.
254+
'section.subsection_in_testcase_yaml.ztest_suite.ztest_without_test_prefix'.
255+
Example_1: 'kernel.fifo.fifo_api_1cpu.fifo_loop' where 'kernel.fifo' is a test scenario
256+
name (section.subsection) and 'fifo_api_1cpu.fifo_loop' is
257+
a Ztest suite_name.test_name identificator.
258+
Example_2: 'debug.coredump.logging_backend' is a standalone test scenario name.
253259
""")
254260

255261
parser.add_argument(

scripts/pylib/twister/twisterlib/harness.py

Lines changed: 127 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
_WINDOWS = platform.system() == 'Windows'
3232

3333

34-
result_re = re.compile(r".*(PASS|FAIL|SKIP) - (test_)?(\S*) in (\d*[.,]?\d*) seconds")
3534
class Harness:
3635
GCOV_START = "GCOV_COVERAGE_DUMP_START"
3736
GCOV_END = "GCOV_COVERAGE_DUMP_END"
@@ -59,12 +58,19 @@ def __init__(self):
5958
self.ztest = False
6059
self.detected_suite_names = []
6160
self.run_id = None
61+
self.started_suites = {}
62+
self.started_cases = {}
6263
self.matched_run_id = False
6364
self.run_id_exists = False
6465
self.instance: TestInstance | None = None
6566
self.testcase_output = ""
6667
self._match = False
6768

69+
70+
@property
71+
def trace(self) -> bool:
72+
return self.instance.handler.options.verbose > 2
73+
6874
@property
6975
def status(self) -> TwisterStatus:
7076
return self._status
@@ -710,42 +716,124 @@ def _check_result(self, line):
710716

711717
class Test(Harness):
712718
__test__ = False # for pytest to skip this class when collects tests
713-
RUN_PASSED = "PROJECT EXECUTION SUCCESSFUL"
714-
RUN_FAILED = "PROJECT EXECUTION FAILED"
715-
test_suite_start_pattern = r"Running TESTSUITE (?P<suite_name>.*)"
716-
ZTEST_START_PATTERN = r"START - (test_)?([a-zA-Z0-9_-]+)"
717719

718-
def handle(self, line):
719-
test_suite_match = re.search(self.test_suite_start_pattern, line)
720-
if test_suite_match:
721-
suite_name = test_suite_match.group("suite_name")
720+
test_suite_start_pattern = re.compile(r"Running TESTSUITE (?P<suite_name>\S*)")
721+
test_suite_end_pattern = re.compile(r"TESTSUITE (?P<suite_name>\S*)\s+(?P<suite_status>succeeded|failed)")
722+
test_case_start_pattern = re.compile(r"START - (test_)?([a-zA-Z0-9_-]+)")
723+
test_case_end_pattern = re.compile(r".*(PASS|FAIL|SKIP) - (test_)?(\S*) in (\d*[.,]?\d*) seconds")
724+
test_suite_summary_pattern = re.compile(r"SUITE (?P<suite_status>\S*) - .* \[(?P<suite_name>\S*)\]: .* duration = (\d*[.,]?\d*) seconds")
725+
test_case_summary_pattern = re.compile(r" - (PASS|FAIL|SKIP) - \[([^\.]*).(test_)?(\S*)\] duration = (\d*[.,]?\d*) seconds")
726+
727+
728+
def get_testcase(self, tc_name, phase, ts_name=None):
729+
""" Search a Ztest case among detected in the test image binary
730+
expecting the same test names as already known from the ELF.
731+
Track suites and cases unexpectedly found in the log.
732+
"""
733+
ts_names = self.started_suites.keys()
734+
if ts_name:
735+
if ts_name not in self.instance.testsuite.ztest_suite_names:
736+
logger.warning(f"On {phase}: unexpected Ztest suite '{ts_name}' "
737+
f"not present among: {self.instance.testsuite.ztest_suite_names}")
738+
if ts_name not in self.detected_suite_names:
739+
if self.trace:
740+
logger.debug(f"On {phase}: detected new Ztest suite '{ts_name}'")
741+
self.detected_suite_names.append(ts_name)
742+
ts_names = [ ts_name ] if ts_name in ts_names else []
743+
744+
# Firstly try to match the test case ID to the first running Ztest suite with this test name.
745+
for ts_name_ in ts_names:
746+
if self.started_suites[ts_name_]['count'] < (0 if phase == 'TS_SUM' else 1):
747+
continue
748+
tc_fq_id = "{}.{}.{}".format(self.id, ts_name_, tc_name)
749+
if tc := self.instance.get_case_by_name(tc_fq_id):
750+
if self.trace:
751+
logger.debug(f"On {phase}: Ztest case '{tc_name}' matched to '{tc_fq_id}")
752+
return tc
753+
logger.debug(f"On {phase}: Ztest case '{tc_name}' is not known in {self.started_suites} running suite(s).")
754+
tc_id = "{}.{}".format(self.id, tc_name)
755+
return self.instance.get_case_or_create(tc_id)
756+
757+
def start_suite(self, suite_name):
758+
if suite_name not in self.detected_suite_names:
722759
self.detected_suite_names.append(suite_name)
760+
if suite_name not in self.instance.testsuite.ztest_suite_names:
761+
logger.warning(f"Unexpected Ztest suite '{suite_name}'")
762+
if suite_name in self.started_suites:
763+
if self.started_suites[suite_name]['count'] > 0:
764+
logger.warning(f"Already STARTED '{suite_name}':{self.started_suites[suite_name]}")
765+
elif self.trace:
766+
logger.debug(f"START suite '{suite_name}'")
767+
self.started_suites[suite_name]['count'] += 1
768+
self.started_suites[suite_name]['repeat'] += 1
769+
else:
770+
self.started_suites[suite_name] = { 'count': 1, 'repeat': 0 }
771+
772+
def end_suite(self, suite_name, phase='', suite_status=None):
773+
if suite_name in self.started_suites:
774+
if phase == 'TS_SUM' and self.started_suites[suite_name]['count'] == 0:
775+
return
776+
if self.started_suites[suite_name]['count'] < 1:
777+
logger.error(f"Already ENDED {phase} suite '{suite_name}':{self.started_suites[suite_name]}")
778+
elif self.trace:
779+
logger.debug(f"END {phase} suite '{suite_name}':{self.started_suites[suite_name]}")
780+
self.started_suites[suite_name]['count'] -= 1
781+
elif suite_status == 'SKIP':
782+
self.start_suite(suite_name) # register skipped suites at their summary end
783+
self.started_suites[suite_name]['count'] -= 1
784+
else:
785+
logger.warning(f"END {phase} suite '{suite_name}' without START detected")
723786

724-
testcase_match = re.search(self.ZTEST_START_PATTERN, line)
725-
if testcase_match:
726-
name = "{}.{}".format(self.id, testcase_match.group(2))
727-
tc = self.instance.get_case_or_create(name)
787+
def start_case(self, tc_name):
788+
if tc_name in self.started_cases:
789+
if self.started_cases[tc_name]['count'] > 0:
790+
logger.warning(f"Already STARTED '{tc_name}':{self.started_cases[tc_name]}")
791+
self.started_cases[tc_name]['count'] += 1
792+
else:
793+
self.started_cases[tc_name] = { 'count': 1 }
794+
795+
def end_case(self, tc_name, phase=''):
796+
if tc_name in self.started_cases:
797+
if phase == 'TS_SUM' and self.started_cases[tc_name]['count'] == 0:
798+
return
799+
if self.started_cases[tc_name]['count'] < 1:
800+
logger.error(f"Already ENDED {phase} case '{tc_name}':{self.started_cases[tc_name]}")
801+
elif self.trace:
802+
logger.debug(f"END {phase} case '{tc_name}':{self.started_cases[tc_name]}")
803+
self.started_cases[tc_name]['count'] -= 1
804+
elif phase != 'TS_SUM':
805+
logger.warning(f"END {phase} case '{tc_name}' without START detected")
806+
807+
808+
def handle(self, line):
809+
testcase_match = None
810+
if self._match:
811+
self.testcase_output += line + "\n"
812+
813+
if test_suite_start_match := re.search(self.test_suite_start_pattern, line):
814+
self.start_suite(test_suite_start_match.group("suite_name"))
815+
elif test_suite_end_match := re.search(self.test_suite_end_pattern, line):
816+
suite_name=test_suite_end_match.group("suite_name")
817+
self.end_suite(suite_name, 'TS_END')
818+
elif testcase_match := re.search(self.test_case_start_pattern, line):
819+
tc_name = testcase_match.group(2)
820+
tc = self.get_testcase(tc_name, 'TC_START')
821+
self.start_case(tc.name)
728822
# Mark the test as started, if something happens here, it is mostly
729823
# due to this tests, for example timeout. This should in this case
730824
# be marked as failed and not blocked (not run).
731825
tc.status = TwisterStatus.STARTED
732-
733-
if testcase_match or self._match:
734-
self.testcase_output += line + "\n"
735-
self._match = True
736-
737-
result_match = result_re.match(line)
826+
if not self._match:
827+
self.testcase_output += line + "\n"
828+
self._match = True
738829
# some testcases are skipped based on predicates and do not show up
739830
# during test execution, however they are listed in the summary. Parse
740831
# the summary for status and use that status instead.
741-
742-
summary_re = re.compile(r"- (PASS|FAIL|SKIP) - \[([^\.]*).(test_)?(\S*)\] duration = (\d*[.,]?\d*) seconds")
743-
summary_match = summary_re.match(line)
744-
745-
if result_match:
832+
elif result_match := self.test_case_end_pattern.match(line):
746833
matched_status = result_match.group(1)
747-
name = "{}.{}".format(self.id, result_match.group(3))
748-
tc = self.instance.get_case_or_create(name)
834+
tc_name = result_match.group(3)
835+
tc = self.get_testcase(tc_name, 'TC_END')
836+
self.end_case(tc.name)
749837
tc.status = TwisterStatus[matched_status]
750838
if tc.status == TwisterStatus.SKIP:
751839
tc.reason = "ztest skip"
@@ -755,15 +843,22 @@ def handle(self, line):
755843
self.testcase_output = ""
756844
self._match = False
757845
self.ztest = True
758-
elif summary_match:
759-
matched_status = summary_match.group(1)
760-
self.detected_suite_names.append(summary_match.group(2))
761-
name = "{}.{}".format(self.id, summary_match.group(4))
762-
tc = self.instance.get_case_or_create(name)
846+
elif test_suite_summary_match := self.test_suite_summary_pattern.match(line):
847+
suite_name=test_suite_summary_match.group("suite_name")
848+
suite_status=test_suite_summary_match.group("suite_status")
849+
self._match = False
850+
self.ztest = True
851+
self.end_suite(suite_name, 'TS_SUM', suite_status=suite_status)
852+
elif test_case_summary_match := self.test_case_summary_pattern.match(line):
853+
matched_status = test_case_summary_match.group(1)
854+
suite_name = test_case_summary_match.group(2)
855+
tc_name = test_case_summary_match.group(4)
856+
tc = self.get_testcase(tc_name, 'TS_SUM', suite_name)
857+
self.end_case(tc.name, 'TS_SUM')
763858
tc.status = TwisterStatus[matched_status]
764859
if tc.status == TwisterStatus.SKIP:
765860
tc.reason = "ztest skip"
766-
tc.duration = float(summary_match.group(5))
861+
tc.duration = float(test_case_summary_match.group(5))
767862
if tc.status == TwisterStatus.FAIL:
768863
tc.output = self.testcase_output
769864
self.testcase_output = ""

scripts/pylib/twister/twisterlib/runner.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# vim: set syntax=python ts=4 :
22
#
3-
# Copyright (c) 20180-2022 Intel Corporation
3+
# Copyright (c) 2018-2024 Intel Corporation
44
# Copyright 2022 NXP
55
# SPDX-License-Identifier: Apache-2.0
66

@@ -1108,13 +1108,16 @@ def determine_testcases(self, results):
11081108
matches = new_ztest_unit_test_regex.findall(sym.name)
11091109
if matches:
11101110
for m in matches:
1111-
# new_ztest_suite = m[0] # not used for now
1111+
new_ztest_suite = m[0]
1112+
if new_ztest_suite not in self.instance.testsuite.ztest_suite_names:
1113+
logger.warning(f"Unexpected Ztest suite '{new_ztest_suite}' "
1114+
f"not present in: {self.instance.testsuite.ztest_suite_names}")
11121115
test_func_name = m[1].replace("test_", "", 1)
1113-
testcase_id = f"{yaml_testsuite_name}.{test_func_name}"
1116+
testcase_id = f"{yaml_testsuite_name}.{new_ztest_suite}.{test_func_name}"
11141117
detected_cases.append(testcase_id)
11151118

11161119
if detected_cases:
1117-
logger.debug(f"{', '.join(detected_cases)} in {elf_file}")
1120+
logger.debug(f"Detected Ztest cases: [{', '.join(detected_cases)}] in {elf_file}")
11181121
tc_keeper = {tc.name: {'status': tc.status, 'reason': tc.reason} for tc in self.instance.testcases}
11191122
self.instance.testcases.clear()
11201123
self.instance.testsuite.testcases.clear()

scripts/pylib/twister/twisterlib/testsuite.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# vim: set syntax=python ts=4 :
22
#
3-
# Copyright (c) 2018-2022 Intel Corporation
3+
# Copyright (c) 2018-2024 Intel Corporation
44
# SPDX-License-Identifier: Apache-2.0
55

66
from enum import Enum
@@ -248,14 +248,16 @@ def _find_ztest_testcases(search_area, testcase_regex):
248248
testcase_regex_matches = \
249249
[m for m in testcase_regex.finditer(search_area)]
250250
testcase_names = \
251-
[m.group("testcase_name") for m in testcase_regex_matches]
252-
testcase_names = [name.decode("UTF-8") for name in testcase_names]
251+
[(m.group("suite_name") if m.groupdict().get("suite_name") else b'', m.group("testcase_name")) \
252+
for m in testcase_regex_matches]
253+
testcase_names = [(ts_name.decode("UTF-8"), tc_name.decode("UTF-8")) for ts_name, tc_name in testcase_names]
253254
warnings = None
254255
for testcase_name in testcase_names:
255-
if not testcase_name.startswith("test_"):
256+
if not testcase_name[1].startswith("test_"):
256257
warnings = "Found a test that does not start with test_"
257258
testcase_names = \
258-
[tc_name.replace("test_", "", 1) for tc_name in testcase_names]
259+
[(ts_name + '.' if ts_name else '') + f"{tc_name.replace('test_', '', 1)}" \
260+
for (ts_name, tc_name) in testcase_names]
259261

260262
return testcase_names, warnings
261263

0 commit comments

Comments
 (0)