|
16 | 16 | from collections import OrderedDict
|
17 | 17 | from enum import Enum
|
18 | 18 |
|
| 19 | +import junitparser.junitparser as junit |
19 | 20 | from pytest import ExitCode
|
20 | 21 | from twisterlib.constants import SUPPORTED_SIMS_IN_PYTEST
|
21 | 22 | from twisterlib.environment import PYTEST_PLUGIN_INSTALLED, ZEPHYR_BASE
|
@@ -955,6 +956,142 @@ def build(self):
|
955 | 956 | logger.debug(f'Copying executable from {original_exe_path} to {new_exe_path}')
|
956 | 957 | shutil.copy(original_exe_path, new_exe_path)
|
957 | 958 |
|
| 959 | +class Ctest(Harness): |
| 960 | + def configure(self, instance: TestInstance): |
| 961 | + super().configure(instance) |
| 962 | + self.running_dir = instance.build_dir |
| 963 | + self.report_file = os.path.join(self.running_dir, 'report.xml') |
| 964 | + self.ctest_log_file_path = os.path.join(self.running_dir, 'twister_harness.log') |
| 965 | + self._output = [] |
| 966 | + |
| 967 | + def ctest_run(self, timeout): |
| 968 | + assert self.instance is not None |
| 969 | + try: |
| 970 | + cmd = self.generate_command() |
| 971 | + self.run_command(cmd, timeout) |
| 972 | + except Exception as err: |
| 973 | + logger.error(str(err)) |
| 974 | + self.status = TwisterStatus.FAIL |
| 975 | + self.instance.reason = str(err) |
| 976 | + finally: |
| 977 | + self.instance.record(self.recording) |
| 978 | + self._update_test_status() |
| 979 | + |
| 980 | + def generate_command(self): |
| 981 | + config = self.instance.testsuite.harness_config |
| 982 | + handler: Handler = self.instance.handler |
| 983 | + ctest_args_yaml = config.get('ctest_args', []) if config else [] |
| 984 | + command = [ |
| 985 | + 'ctest', |
| 986 | + '--build-nocmake', |
| 987 | + '--test-dir', |
| 988 | + self.running_dir, |
| 989 | + '--output-junit', |
| 990 | + self.report_file, |
| 991 | + '--output-log', |
| 992 | + self.ctest_log_file_path, |
| 993 | + '--output-on-failure', |
| 994 | + ] |
| 995 | + base_timeout = handler.get_test_timeout() |
| 996 | + command.extend(['--timeout', str(base_timeout)]) |
| 997 | + command.extend(ctest_args_yaml) |
| 998 | + |
| 999 | + if handler.options.ctest_args: |
| 1000 | + command.extend(handler.options.ctest_args) |
| 1001 | + |
| 1002 | + return command |
| 1003 | + |
| 1004 | + def run_command(self, cmd, timeout): |
| 1005 | + with subprocess.Popen( |
| 1006 | + cmd, |
| 1007 | + stdout=subprocess.PIPE, |
| 1008 | + stderr=subprocess.STDOUT, |
| 1009 | + ) as proc: |
| 1010 | + try: |
| 1011 | + reader_t = threading.Thread(target=self._output_reader, args=(proc,), daemon=True) |
| 1012 | + reader_t.start() |
| 1013 | + reader_t.join(timeout) |
| 1014 | + if reader_t.is_alive(): |
| 1015 | + terminate_process(proc) |
| 1016 | + logger.warning('Timeout has occurred. Can be extended in testspec file. ' |
| 1017 | + f'Currently set to {timeout} seconds.') |
| 1018 | + self.instance.reason = 'Ctest timeout' |
| 1019 | + self.status = TwisterStatus.FAIL |
| 1020 | + proc.wait(timeout) |
| 1021 | + except subprocess.TimeoutExpired: |
| 1022 | + self.status = TwisterStatus.FAIL |
| 1023 | + proc.kill() |
| 1024 | + |
| 1025 | + if proc.returncode in (ExitCode.INTERRUPTED, ExitCode.USAGE_ERROR, ExitCode.INTERNAL_ERROR): |
| 1026 | + self.status = TwisterStatus.ERROR |
| 1027 | + self.instance.reason = f'Ctest error - return code {proc.returncode}' |
| 1028 | + with open(self.ctest_log_file_path, 'w') as log_file: |
| 1029 | + log_file.write(shlex.join(cmd) + '\n\n') |
| 1030 | + log_file.write('\n'.join(self._output)) |
| 1031 | + |
| 1032 | + def _output_reader(self, proc): |
| 1033 | + self._output = [] |
| 1034 | + while proc.stdout.readable() and proc.poll() is None: |
| 1035 | + line = proc.stdout.readline().decode().strip() |
| 1036 | + if not line: |
| 1037 | + continue |
| 1038 | + self._output.append(line) |
| 1039 | + logger.debug(f'CTEST: {line}') |
| 1040 | + self.parse_record(line) |
| 1041 | + proc.communicate() |
| 1042 | + |
| 1043 | + def _update_test_status(self): |
| 1044 | + if self.status == TwisterStatus.NONE: |
| 1045 | + self.instance.testcases = [] |
| 1046 | + try: |
| 1047 | + self._parse_report_file(self.report_file) |
| 1048 | + except Exception as e: |
| 1049 | + logger.error(f'Error when parsing file {self.report_file}: {e}') |
| 1050 | + self.status = TwisterStatus.FAIL |
| 1051 | + finally: |
| 1052 | + if not self.instance.testcases: |
| 1053 | + self.instance.init_cases() |
| 1054 | + |
| 1055 | + self.instance.status = self.status if self.status != TwisterStatus.NONE else \ |
| 1056 | + TwisterStatus.FAIL |
| 1057 | + if self.instance.status in [TwisterStatus.ERROR, TwisterStatus.FAIL]: |
| 1058 | + self.instance.reason = self.instance.reason or 'Ctest failed' |
| 1059 | + self.instance.add_missing_case_status(TwisterStatus.BLOCK, self.instance.reason) |
| 1060 | + |
| 1061 | + def _parse_report_file(self, report): |
| 1062 | + suite = junit.JUnitXml.fromfile(report) |
| 1063 | + if suite is None: |
| 1064 | + self.status = TwisterStatus.SKIP |
| 1065 | + self.instance.reason = 'No tests collected' |
| 1066 | + return |
| 1067 | + |
| 1068 | + assert isinstance(suite, junit.TestSuite) |
| 1069 | + |
| 1070 | + if suite.failures and suite.failures > 0: |
| 1071 | + self.status = TwisterStatus.FAIL |
| 1072 | + self.instance.reason = f"{suite.failures}/{suite.tests} ctest scenario(s) failed" |
| 1073 | + elif suite.errors and suite.errors > 0: |
| 1074 | + self.status = TwisterStatus.ERROR |
| 1075 | + self.instance.reason = 'Error during ctest execution' |
| 1076 | + elif suite.skipped and suite.skipped > 0: |
| 1077 | + self.status = TwisterStatus.SKIP |
| 1078 | + else: |
| 1079 | + self.status = TwisterStatus.PASS |
| 1080 | + self.instance.execution_time = suite.time |
| 1081 | + |
| 1082 | + for case in suite: |
| 1083 | + tc = self.instance.add_testcase(f"{self.id}.{case.name}") |
| 1084 | + tc.duration = case.time |
| 1085 | + if any(isinstance(r, junit.Failure) for r in case.result): |
| 1086 | + tc.status = TwisterStatus.FAIL |
| 1087 | + tc.output = case.system_out |
| 1088 | + elif any(isinstance(r, junit.Error) for r in case.result): |
| 1089 | + tc.status = TwisterStatus.ERROR |
| 1090 | + tc.output = case.system_out |
| 1091 | + elif any(isinstance(r, junit.Skipped) for r in case.result): |
| 1092 | + tc.status = TwisterStatus.SKIP |
| 1093 | + else: |
| 1094 | + tc.status = TwisterStatus.PASS |
958 | 1095 |
|
959 | 1096 | class HarnessImporter:
|
960 | 1097 |
|
|
0 commit comments