|
9 | 9 | from collections import OrderedDict
|
10 | 10 | import xml.etree.ElementTree as ET
|
11 | 11 | import logging
|
| 12 | +import threading |
12 | 13 | import time
|
13 | 14 |
|
14 | 15 | from twisterlib.environment import ZEPHYR_BASE, PYTEST_PLUGIN_INSTALLED
|
| 16 | +from twisterlib.handlers import terminate_process |
15 | 17 |
|
16 | 18 |
|
17 | 19 | logger = logging.getLogger('twister')
|
@@ -229,13 +231,13 @@ def configure(self, instance):
|
229 | 231 | self.report_file = os.path.join(self.running_dir, 'report.xml')
|
230 | 232 | self.reserved_serial = None
|
231 | 233 |
|
232 |
| - def pytest_run(self): |
| 234 | + def pytest_run(self, timeout): |
233 | 235 | try:
|
234 | 236 | cmd = self.generate_command()
|
235 | 237 | if not cmd:
|
236 | 238 | logger.error('Pytest command not generated, check logs')
|
237 | 239 | return
|
238 |
| - self.run_command(cmd) |
| 240 | + self.run_command(cmd, timeout) |
239 | 241 | except PytestHarnessException as pytest_exception:
|
240 | 242 | logger.error(str(pytest_exception))
|
241 | 243 | finally:
|
@@ -314,33 +316,36 @@ def _generate_parameters_for_hardware(self, handler):
|
314 | 316 |
|
315 | 317 | return command
|
316 | 318 |
|
317 |
| - def run_command(self, cmd): |
| 319 | + def run_command(self, cmd, timeout): |
318 | 320 | cmd, env = self._update_command_with_env_dependencies(cmd)
|
319 |
| - |
320 | 321 | with subprocess.Popen(cmd,
|
321 | 322 | stdout=subprocess.PIPE,
|
322 | 323 | stderr=subprocess.STDOUT,
|
323 | 324 | env=env) as proc:
|
324 | 325 | try:
|
325 |
| - while proc.stdout.readable() and proc.poll() is None: |
326 |
| - line = proc.stdout.readline().decode().strip() |
327 |
| - if not line: |
328 |
| - continue |
329 |
| - logger.debug("PYTEST: %s", line) |
330 |
| - proc.communicate() |
331 |
| - tree = ET.parse(self.report_file) |
332 |
| - root = tree.getroot() |
333 |
| - for child in root: |
334 |
| - if child.tag == 'testsuite': |
335 |
| - if child.attrib['failures'] != '0': |
336 |
| - self.state = "failed" |
337 |
| - elif child.attrib['skipped'] != '0': |
338 |
| - self.state = "skipped" |
339 |
| - elif child.attrib['errors'] != '0': |
340 |
| - self.state = "error" |
341 |
| - else: |
342 |
| - self.state = "passed" |
343 |
| - self.instance.execution_time = float(child.attrib['time']) |
| 326 | + reader_t = threading.Thread(target=self._output_reader, args=(proc,), daemon=True) |
| 327 | + reader_t.start() |
| 328 | + reader_t.join(timeout) |
| 329 | + if reader_t.is_alive(): |
| 330 | + terminate_process(proc) |
| 331 | + logger.warning('Timeout has occurred.') |
| 332 | + self.state = 'failed' |
| 333 | + proc.wait(timeout) |
| 334 | + |
| 335 | + if self.state != 'failed': |
| 336 | + tree = ET.parse(self.report_file) |
| 337 | + root = tree.getroot() |
| 338 | + for child in root: |
| 339 | + if child.tag == 'testsuite': |
| 340 | + if child.attrib['failures'] != '0': |
| 341 | + self.state = "failed" |
| 342 | + elif child.attrib['skipped'] != '0': |
| 343 | + self.state = "skipped" |
| 344 | + elif child.attrib['errors'] != '0': |
| 345 | + self.state = "error" |
| 346 | + else: |
| 347 | + self.state = "passed" |
| 348 | + self.instance.execution_time = float(child.attrib['time']) |
344 | 349 | except subprocess.TimeoutExpired:
|
345 | 350 | proc.kill()
|
346 | 351 | self.state = "failed"
|
@@ -383,6 +388,15 @@ def _update_command_with_env_dependencies(cmd):
|
383 | 388 |
|
384 | 389 | return cmd, env
|
385 | 390 |
|
| 391 | + @staticmethod |
| 392 | + def _output_reader(proc): |
| 393 | + while proc.stdout.readable() and proc.poll() is None: |
| 394 | + line = proc.stdout.readline().decode().strip() |
| 395 | + if not line: |
| 396 | + continue |
| 397 | + logger.debug('PYTEST: %s', line) |
| 398 | + proc.communicate() |
| 399 | + |
386 | 400 | def _apply_instance_status(self):
|
387 | 401 | if self.state:
|
388 | 402 | self.instance.status = self.state
|
|
0 commit comments