3131_WINDOWS = platform .system () == 'Windows'
3232
3333
34- result_re = re .compile (r".*(PASS|FAIL|SKIP) - (test_)?(\S*) in (\d*[.,]?\d*) seconds" )
3534class 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
711717class 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 = ""
0 commit comments