blob: 66de95c0c31535d57bd628361a80c2a9ad82736d [file] [log] [blame]
Anas Nashifce2b4182020-03-24 14:40:28 -04001#!/usr/bin/env python3
2# vim: set syntax=python ts=4 :
3#
4# Copyright (c) 2018 Intel Corporation
5# SPDX-License-Identifier: Apache-2.0
6
7import os
8import contextlib
9import string
10import mmap
11import sys
12import re
13import subprocess
14import select
15import shutil
16import shlex
17import signal
18import threading
19import concurrent.futures
20from collections import OrderedDict
Anas Nashifce2b4182020-03-24 14:40:28 -040021import queue
22import time
23import csv
24import glob
25import concurrent
26import xml.etree.ElementTree as ET
27import logging
Andrei Emeltchenkod8b845b2020-03-31 13:22:30 +030028import pty
Anas Nashifce2b4182020-03-24 14:40:28 -040029from pathlib import Path
30from distutils.spawn import find_executable
31from colorama import Fore
Martí Bolívar9c92baa2020-07-08 14:43:07 -070032import pickle
Martí Bolívar07dce822020-04-13 16:50:51 -070033import platform
Anas Nashifae61b7e2020-07-06 11:30:55 -040034import yaml
Spoorthy Priya Yeraboluf8f220a2020-09-23 06:28:50 -070035import json
Anas Nashif531fe892020-09-11 13:56:33 -040036from multiprocessing import Lock, Process, Value
Spoorthy Priya Yeraboluf8f220a2020-09-23 06:28:50 -070037
Anas Nashifae61b7e2020-07-06 11:30:55 -040038try:
39 # Use the C LibYAML parser if available, rather than the Python parser.
40 # It's much faster.
Anas Nashifae61b7e2020-07-06 11:30:55 -040041 from yaml import CSafeLoader as SafeLoader
42 from yaml import CDumper as Dumper
43except ImportError:
Martí Bolívard8698cb2020-07-08 14:55:14 -070044 from yaml import SafeLoader, Dumper
Anas Nashifce2b4182020-03-24 14:40:28 -040045
46try:
47 import serial
48except ImportError:
49 print("Install pyserial python module with pip to use --device-testing option.")
50
51try:
52 from tabulate import tabulate
53except ImportError:
54 print("Install tabulate python module with pip to use --device-testing option.")
55
Wentong Wu0d619ae2020-05-05 19:46:49 -040056try:
57 import psutil
58except ImportError:
Anas Nashif77946fa2020-05-21 18:19:01 -040059 print("Install psutil python module with pip to run in Qemu.")
Wentong Wu0d619ae2020-05-05 19:46:49 -040060
Anas Nashifce2b4182020-03-24 14:40:28 -040061ZEPHYR_BASE = os.getenv("ZEPHYR_BASE")
62if not ZEPHYR_BASE:
63 sys.exit("$ZEPHYR_BASE environment variable undefined")
64
Martí Bolívar9c92baa2020-07-08 14:43:07 -070065# This is needed to load edt.pickle files.
Martí Bolívar53328472021-03-26 16:18:58 -070066sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts", "dts",
67 "python-devicetree", "src"))
68from devicetree import edtlib # pylint: disable=unused-import
Anas Nashifce2b4182020-03-24 14:40:28 -040069
70# Use this for internal comparisons; that's what canonicalization is
71# for. Don't use it when invoking other components of the build system
72# to avoid confusing and hard to trace inconsistencies in error messages
73# and logs, generated Makefiles, etc. compared to when users invoke these
74# components directly.
75# Note "normalization" is different from canonicalization, see os.path.
76canonical_zephyr_base = os.path.realpath(ZEPHYR_BASE)
77
78sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts/"))
79
Anas Nashife508bab2020-12-07 11:27:32 -050080import scl
81import expr_parser
Anas Nashifce2b4182020-03-24 14:40:28 -040082
Anas Nashifb18f3112020-12-07 11:40:19 -050083logger = logging.getLogger('twister')
Anas Nashifce2b4182020-03-24 14:40:28 -040084logger.setLevel(logging.DEBUG)
85
Anas Nashif531fe892020-09-11 13:56:33 -040086
87class ExecutionCounter(object):
88 def __init__(self, total=0):
89 self._done = Value('i', 0)
90 self._passed = Value('i', 0)
Anas Nashif3b939da2020-11-24 13:21:27 -050091 self._skipped_configs = Value('i', 0)
92 self._skipped_runtime = Value('i', 0)
93 self._skipped_cases = Value('i', 0)
Anas Nashif531fe892020-09-11 13:56:33 -040094 self._error = Value('i', 0)
95 self._failed = Value('i', 0)
96 self._total = Value('i', total)
97 self._cases = Value('i', 0)
Anas Nashif3b939da2020-11-24 13:21:27 -050098
Anas Nashif531fe892020-09-11 13:56:33 -040099
100 self.lock = Lock()
101
102 @property
103 def cases(self):
104 with self._cases.get_lock():
105 return self._cases.value
106
107 @cases.setter
108 def cases(self, value):
109 with self._cases.get_lock():
110 self._cases.value = value
111
112 @property
113 def skipped_cases(self):
114 with self._skipped_cases.get_lock():
115 return self._skipped_cases.value
116
117 @skipped_cases.setter
118 def skipped_cases(self, value):
119 with self._skipped_cases.get_lock():
120 self._skipped_cases.value = value
121
122 @property
123 def error(self):
124 with self._error.get_lock():
125 return self._error.value
126
127 @error.setter
128 def error(self, value):
129 with self._error.get_lock():
130 self._error.value = value
131
132 @property
133 def done(self):
134 with self._done.get_lock():
135 return self._done.value
136
137 @done.setter
138 def done(self, value):
139 with self._done.get_lock():
140 self._done.value = value
141
142 @property
143 def passed(self):
144 with self._passed.get_lock():
145 return self._passed.value
146
147 @passed.setter
148 def passed(self, value):
149 with self._passed.get_lock():
150 self._passed.value = value
151
152 @property
Anas Nashif3b939da2020-11-24 13:21:27 -0500153 def skipped_configs(self):
154 with self._skipped_configs.get_lock():
155 return self._skipped_configs.value
Anas Nashif531fe892020-09-11 13:56:33 -0400156
Anas Nashif3b939da2020-11-24 13:21:27 -0500157 @skipped_configs.setter
158 def skipped_configs(self, value):
159 with self._skipped_configs.get_lock():
160 self._skipped_configs.value = value
Anas Nashif531fe892020-09-11 13:56:33 -0400161
162 @property
Anas Nashif3b939da2020-11-24 13:21:27 -0500163 def skipped_runtime(self):
164 with self._skipped_runtime.get_lock():
165 return self._skipped_runtime.value
Anas Nashif531fe892020-09-11 13:56:33 -0400166
Anas Nashif3b939da2020-11-24 13:21:27 -0500167 @skipped_runtime.setter
168 def skipped_runtime(self, value):
169 with self._skipped_runtime.get_lock():
170 self._skipped_runtime.value = value
Anas Nashif531fe892020-09-11 13:56:33 -0400171
172 @property
173 def failed(self):
174 with self._failed.get_lock():
175 return self._failed.value
176
177 @failed.setter
178 def failed(self, value):
179 with self._failed.get_lock():
180 self._failed.value = value
181
182 @property
183 def total(self):
184 with self._total.get_lock():
185 return self._total.value
Anas Nashifce2b4182020-03-24 14:40:28 -0400186
187class CMakeCacheEntry:
188 '''Represents a CMake cache entry.
189
190 This class understands the type system in a CMakeCache.txt, and
191 converts the following cache types to Python types:
192
193 Cache Type Python type
194 ---------- -------------------------------------------
195 FILEPATH str
196 PATH str
197 STRING str OR list of str (if ';' is in the value)
198 BOOL bool
199 INTERNAL str OR list of str (if ';' is in the value)
200 ---------- -------------------------------------------
201 '''
202
203 # Regular expression for a cache entry.
204 #
205 # CMake variable names can include escape characters, allowing a
206 # wider set of names than is easy to match with a regular
207 # expression. To be permissive here, use a non-greedy match up to
208 # the first colon (':'). This breaks if the variable name has a
209 # colon inside, but it's good enough.
210 CACHE_ENTRY = re.compile(
211 r'''(?P<name>.*?) # name
212 :(?P<type>FILEPATH|PATH|STRING|BOOL|INTERNAL) # type
213 =(?P<value>.*) # value
214 ''', re.X)
215
216 @classmethod
217 def _to_bool(cls, val):
218 # Convert a CMake BOOL string into a Python bool.
219 #
220 # "True if the constant is 1, ON, YES, TRUE, Y, or a
221 # non-zero number. False if the constant is 0, OFF, NO,
222 # FALSE, N, IGNORE, NOTFOUND, the empty string, or ends in
223 # the suffix -NOTFOUND. Named boolean constants are
224 # case-insensitive. If the argument is not one of these
225 # constants, it is treated as a variable."
226 #
227 # https://cmake.org/cmake/help/v3.0/command/if.html
228 val = val.upper()
229 if val in ('ON', 'YES', 'TRUE', 'Y'):
230 return 1
231 elif val in ('OFF', 'NO', 'FALSE', 'N', 'IGNORE', 'NOTFOUND', ''):
232 return 0
233 elif val.endswith('-NOTFOUND'):
234 return 0
235 else:
236 try:
237 v = int(val)
238 return v != 0
239 except ValueError as exc:
240 raise ValueError('invalid bool {}'.format(val)) from exc
241
242 @classmethod
243 def from_line(cls, line, line_no):
244 # Comments can only occur at the beginning of a line.
245 # (The value of an entry could contain a comment character).
246 if line.startswith('//') or line.startswith('#'):
247 return None
248
249 # Whitespace-only lines do not contain cache entries.
250 if not line.strip():
251 return None
252
253 m = cls.CACHE_ENTRY.match(line)
254 if not m:
255 return None
256
257 name, type_, value = (m.group(g) for g in ('name', 'type', 'value'))
258 if type_ == 'BOOL':
259 try:
260 value = cls._to_bool(value)
261 except ValueError as exc:
262 args = exc.args + ('on line {}: {}'.format(line_no, line),)
263 raise ValueError(args) from exc
264 elif type_ in ['STRING', 'INTERNAL']:
265 # If the value is a CMake list (i.e. is a string which
266 # contains a ';'), convert to a Python list.
267 if ';' in value:
268 value = value.split(';')
269
270 return CMakeCacheEntry(name, value)
271
272 def __init__(self, name, value):
273 self.name = name
274 self.value = value
275
276 def __str__(self):
277 fmt = 'CMakeCacheEntry(name={}, value={})'
278 return fmt.format(self.name, self.value)
279
280
281class CMakeCache:
282 '''Parses and represents a CMake cache file.'''
283
284 @staticmethod
285 def from_file(cache_file):
286 return CMakeCache(cache_file)
287
288 def __init__(self, cache_file):
289 self.cache_file = cache_file
290 self.load(cache_file)
291
292 def load(self, cache_file):
293 entries = []
294 with open(cache_file, 'r') as cache:
295 for line_no, line in enumerate(cache):
296 entry = CMakeCacheEntry.from_line(line, line_no)
297 if entry:
298 entries.append(entry)
299 self._entries = OrderedDict((e.name, e) for e in entries)
300
301 def get(self, name, default=None):
302 entry = self._entries.get(name)
303 if entry is not None:
304 return entry.value
305 else:
306 return default
307
308 def get_list(self, name, default=None):
309 if default is None:
310 default = []
311 entry = self._entries.get(name)
312 if entry is not None:
313 value = entry.value
314 if isinstance(value, list):
315 return value
316 elif isinstance(value, str):
317 return [value] if value else []
318 else:
319 msg = 'invalid value {} type {}'
320 raise RuntimeError(msg.format(value, type(value)))
321 else:
322 return default
323
324 def __contains__(self, name):
325 return name in self._entries
326
327 def __getitem__(self, name):
328 return self._entries[name].value
329
330 def __setitem__(self, name, entry):
331 if not isinstance(entry, CMakeCacheEntry):
332 msg = 'improper type {} for value {}, expecting CMakeCacheEntry'
333 raise TypeError(msg.format(type(entry), entry))
334 self._entries[name] = entry
335
336 def __delitem__(self, name):
337 del self._entries[name]
338
339 def __iter__(self):
340 return iter(self._entries.values())
341
342
Anas Nashif45943702020-12-11 17:55:15 -0500343class TwisterException(Exception):
Anas Nashifce2b4182020-03-24 14:40:28 -0400344 pass
345
346
Anas Nashif45943702020-12-11 17:55:15 -0500347class TwisterRuntimeError(TwisterException):
Anas Nashifce2b4182020-03-24 14:40:28 -0400348 pass
349
350
Anas Nashif45943702020-12-11 17:55:15 -0500351class ConfigurationError(TwisterException):
Anas Nashifce2b4182020-03-24 14:40:28 -0400352 def __init__(self, cfile, message):
Anas Nashif45943702020-12-11 17:55:15 -0500353 TwisterException.__init__(self, cfile + ": " + message)
Anas Nashifce2b4182020-03-24 14:40:28 -0400354
355
Anas Nashif45943702020-12-11 17:55:15 -0500356class BuildError(TwisterException):
Anas Nashifce2b4182020-03-24 14:40:28 -0400357 pass
358
359
Anas Nashif45943702020-12-11 17:55:15 -0500360class ExecutionError(TwisterException):
Anas Nashifce2b4182020-03-24 14:40:28 -0400361 pass
362
363
364class HarnessImporter:
365
366 def __init__(self, name):
Anas Nashife508bab2020-12-07 11:27:32 -0500367 sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts/pylib/twister"))
Anas Nashifce2b4182020-03-24 14:40:28 -0400368 module = __import__("harness")
369 if name:
370 my_class = getattr(module, name)
371 else:
372 my_class = getattr(module, "Test")
373
374 self.instance = my_class()
375
376
377class Handler:
378 def __init__(self, instance, type_str="build"):
379 """Constructor
380
381 """
Anas Nashifce2b4182020-03-24 14:40:28 -0400382 self.state = "waiting"
383 self.run = False
384 self.duration = 0
385 self.type_str = type_str
386
387 self.binary = None
388 self.pid_fn = None
389 self.call_make_run = False
390
391 self.name = instance.name
392 self.instance = instance
393 self.timeout = instance.testcase.timeout
394 self.sourcedir = instance.testcase.source_dir
395 self.build_dir = instance.build_dir
396 self.log = os.path.join(self.build_dir, "handler.log")
397 self.returncode = 0
398 self.set_state("running", self.duration)
399 self.generator = None
400 self.generator_cmd = None
401
402 self.args = []
Jingru Wangfed1c542021-06-11 11:54:55 +0800403 self.terminated = False
Anas Nashifce2b4182020-03-24 14:40:28 -0400404
405 def set_state(self, state, duration):
Anas Nashifce2b4182020-03-24 14:40:28 -0400406 self.state = state
407 self.duration = duration
Anas Nashifce2b4182020-03-24 14:40:28 -0400408
409 def get_state(self):
Anas Nashifce2b4182020-03-24 14:40:28 -0400410 ret = (self.state, self.duration)
Anas Nashifce2b4182020-03-24 14:40:28 -0400411 return ret
412
413 def record(self, harness):
414 if harness.recording:
415 filename = os.path.join(self.build_dir, "recording.csv")
416 with open(filename, "at") as csvfile:
417 cw = csv.writer(csvfile, harness.fieldnames, lineterminator=os.linesep)
418 cw.writerow(harness.fieldnames)
419 for instance in harness.recording:
420 cw.writerow(instance)
421
Jingru Wangfed1c542021-06-11 11:54:55 +0800422 def terminate(self, proc):
423 # encapsulate terminate functionality so we do it consistently where ever
424 # we might want to terminate the proc. We need try_kill_process_by_pid
425 # because of both how newer ninja (1.6.0 or greater) and .NET / renode
426 # work. Newer ninja's don't seem to pass SIGTERM down to the children
427 # so we need to use try_kill_process_by_pid.
428 for child in psutil.Process(proc.pid).children(recursive=True):
429 try:
430 os.kill(child.pid, signal.SIGTERM)
431 except ProcessLookupError:
432 pass
433 proc.terminate()
434 # sleep for a while before attempting to kill
435 time.sleep(0.5)
436 proc.kill()
437 self.terminated = True
438
Piotr Golyzniak0e1ecb32021-09-23 00:14:04 +0200439 def add_missing_testscases(self, harness):
440 """
441 If testsuite was broken by some error (e.g. timeout) it is necessary to
442 add information about next testcases, which were not be
443 performed due to this error.
444 """
445 for c in self.instance.testcase.cases:
446 if c not in harness.tests:
447 harness.tests[c] = "BLOCK"
448
Anas Nashifce2b4182020-03-24 14:40:28 -0400449
450class BinaryHandler(Handler):
451 def __init__(self, instance, type_str):
452 """Constructor
453
454 @param instance Test Instance
455 """
456 super().__init__(instance, type_str)
457
Eugeniy Paltsev16032b672020-10-08 22:42:02 +0300458 self.call_west_flash = False
Anas Nashifce2b4182020-03-24 14:40:28 -0400459
460 # Tool options
461 self.valgrind = False
462 self.lsan = False
463 self.asan = False
Christian Taedcke3dbe9f22020-07-06 16:00:57 +0200464 self.ubsan = False
Anas Nashifce2b4182020-03-24 14:40:28 -0400465 self.coverage = False
466
467 def try_kill_process_by_pid(self):
468 if self.pid_fn:
469 pid = int(open(self.pid_fn).read())
470 os.unlink(self.pid_fn)
471 self.pid_fn = None # clear so we don't try to kill the binary twice
472 try:
473 os.kill(pid, signal.SIGTERM)
474 except ProcessLookupError:
475 pass
476
Anas Nashif531fe892020-09-11 13:56:33 -0400477 def _output_reader(self, proc):
478 self.line = proc.stdout.readline()
479
480 def _output_handler(self, proc, harness):
YouhuaX Zhu965c8b92020-10-22 09:37:27 +0800481 if harness.is_pytest:
482 harness.handle(None)
483 return
484
Anas Nashifce2b4182020-03-24 14:40:28 -0400485 log_out_fp = open(self.log, "wt")
Anas Nashif531fe892020-09-11 13:56:33 -0400486 timeout_extended = False
487 timeout_time = time.time() + self.timeout
488 while True:
489 this_timeout = timeout_time - time.time()
490 if this_timeout < 0:
Anas Nashifce2b4182020-03-24 14:40:28 -0400491 break
Anas Nashif531fe892020-09-11 13:56:33 -0400492 reader_t = threading.Thread(target=self._output_reader, args=(proc,), daemon=True)
493 reader_t.start()
494 reader_t.join(this_timeout)
495 if not reader_t.is_alive():
496 line = self.line
497 logger.debug("OUTPUT: {0}".format(line.decode('utf-8').rstrip()))
498 log_out_fp.write(line.decode('utf-8'))
499 log_out_fp.flush()
500 harness.handle(line.decode('utf-8').rstrip())
501 if harness.state:
502 if not timeout_extended or harness.capture_coverage:
503 timeout_extended = True
504 if harness.capture_coverage:
505 timeout_time = time.time() + 30
506 else:
507 timeout_time = time.time() + 2
508 else:
509 reader_t.join(0)
510 break
511 try:
512 # POSIX arch based ztests end on their own,
513 # so let's give it up to 100ms to do so
514 proc.wait(0.1)
515 except subprocess.TimeoutExpired:
516 self.terminate(proc)
Anas Nashifce2b4182020-03-24 14:40:28 -0400517
518 log_out_fp.close()
519
520 def handle(self):
521
522 harness_name = self.instance.testcase.harness.capitalize()
523 harness_import = HarnessImporter(harness_name)
524 harness = harness_import.instance
525 harness.configure(self.instance)
526
527 if self.call_make_run:
528 command = [self.generator_cmd, "run"]
Eugeniy Paltsev16032b672020-10-08 22:42:02 +0300529 elif self.call_west_flash:
530 command = ["west", "flash", "--skip-rebuild", "-d", self.build_dir]
Anas Nashifce2b4182020-03-24 14:40:28 -0400531 else:
532 command = [self.binary]
533
534 run_valgrind = False
535 if self.valgrind and shutil.which("valgrind"):
536 command = ["valgrind", "--error-exitcode=2",
537 "--leak-check=full",
538 "--suppressions=" + ZEPHYR_BASE + "/scripts/valgrind.supp",
539 "--log-file=" + self.build_dir + "/valgrind.log"
540 ] + command
541 run_valgrind = True
542
543 logger.debug("Spawning process: " +
544 " ".join(shlex.quote(word) for word in command) + os.linesep +
545 "in directory: " + self.build_dir)
546
547 start_time = time.time()
548
549 env = os.environ.copy()
550 if self.asan:
551 env["ASAN_OPTIONS"] = "log_path=stdout:" + \
552 env.get("ASAN_OPTIONS", "")
553 if not self.lsan:
554 env["ASAN_OPTIONS"] += "detect_leaks=0"
555
Christian Taedcke3dbe9f22020-07-06 16:00:57 +0200556 if self.ubsan:
557 env["UBSAN_OPTIONS"] = "log_path=stdout:halt_on_error=1:" + \
558 env.get("UBSAN_OPTIONS", "")
559
Anas Nashifce2b4182020-03-24 14:40:28 -0400560 with subprocess.Popen(command, stdout=subprocess.PIPE,
561 stderr=subprocess.PIPE, cwd=self.build_dir, env=env) as proc:
562 logger.debug("Spawning BinaryHandler Thread for %s" % self.name)
Anas Nashif531fe892020-09-11 13:56:33 -0400563 t = threading.Thread(target=self._output_handler, args=(proc, harness,), daemon=True)
Anas Nashifce2b4182020-03-24 14:40:28 -0400564 t.start()
Anas Nashif531fe892020-09-11 13:56:33 -0400565 t.join()
Anas Nashifce2b4182020-03-24 14:40:28 -0400566 if t.is_alive():
567 self.terminate(proc)
568 t.join()
569 proc.wait()
570 self.returncode = proc.returncode
Eugeniy Paltsev79b3a502020-12-22 20:00:01 +0300571 self.try_kill_process_by_pid()
Anas Nashifce2b4182020-03-24 14:40:28 -0400572
573 handler_time = time.time() - start_time
574
575 if self.coverage:
576 subprocess.call(["GCOV_PREFIX=" + self.build_dir,
577 "gcov", self.sourcedir, "-b", "-s", self.build_dir], shell=True)
578
Anas Nashifce2b4182020-03-24 14:40:28 -0400579 # FIXME: This is needed when killing the simulator, the console is
580 # garbled and needs to be reset. Did not find a better way to do that.
Eugeniy Paltsevafb34e32020-11-29 21:28:28 +0300581 if sys.stdout.isatty():
582 subprocess.call(["stty", "sane"])
Anas Nashifce2b4182020-03-24 14:40:28 -0400583
YouhuaX Zhu965c8b92020-10-22 09:37:27 +0800584 if harness.is_pytest:
585 harness.pytest_run(self.log)
Anas Nashifce2b4182020-03-24 14:40:28 -0400586 self.instance.results = harness.tests
587
588 if not self.terminated and self.returncode != 0:
589 # When a process is killed, the default handler returns 128 + SIGTERM
590 # so in that case the return code itself is not meaningful
591 self.set_state("failed", handler_time)
592 self.instance.reason = "Failed"
593 elif run_valgrind and self.returncode == 2:
594 self.set_state("failed", handler_time)
595 self.instance.reason = "Valgrind error"
596 elif harness.state:
597 self.set_state(harness.state, handler_time)
Anas Nashifb802af82020-04-26 21:57:38 -0400598 if harness.state == "failed":
599 self.instance.reason = "Failed"
Anas Nashifce2b4182020-03-24 14:40:28 -0400600 else:
601 self.set_state("timeout", handler_time)
602 self.instance.reason = "Timeout"
Piotr Golyzniak0e1ecb32021-09-23 00:14:04 +0200603 self.add_missing_testscases(harness)
Anas Nashifce2b4182020-03-24 14:40:28 -0400604
605 self.record(harness)
606
607
608class DeviceHandler(Handler):
609
610 def __init__(self, instance, type_str):
611 """Constructor
612
613 @param instance Test Instance
614 """
615 super().__init__(instance, type_str)
616
617 self.suite = None
Anas Nashifce2b4182020-03-24 14:40:28 -0400618
619 def monitor_serial(self, ser, halt_fileno, harness):
YouhuaX Zhu965c8b92020-10-22 09:37:27 +0800620 if harness.is_pytest:
621 harness.handle(None)
622 return
623
Anas Nashifce2b4182020-03-24 14:40:28 -0400624 log_out_fp = open(self.log, "wt")
625
626 ser_fileno = ser.fileno()
627 readlist = [halt_fileno, ser_fileno]
628
Andrei Emeltchenko10fa48d2021-01-12 09:56:32 +0200629 if self.coverage:
630 # Set capture_coverage to True to indicate that right after
631 # test results we should get coverage data, otherwise we exit
632 # from the test.
633 harness.capture_coverage = True
634
635 ser.flush()
636
Anas Nashifce2b4182020-03-24 14:40:28 -0400637 while ser.isOpen():
638 readable, _, _ = select.select(readlist, [], [], self.timeout)
639
640 if halt_fileno in readable:
641 logger.debug('halted')
642 ser.close()
643 break
644 if ser_fileno not in readable:
645 continue # Timeout.
646
647 serial_line = None
648 try:
649 serial_line = ser.readline()
650 except TypeError:
651 pass
652 except serial.SerialException:
653 ser.close()
654 break
655
656 # Just because ser_fileno has data doesn't mean an entire line
657 # is available yet.
658 if serial_line:
659 sl = serial_line.decode('utf-8', 'ignore').lstrip()
660 logger.debug("DEVICE: {0}".format(sl.rstrip()))
661
662 log_out_fp.write(sl)
663 log_out_fp.flush()
664 harness.handle(sl.rstrip())
665
666 if harness.state:
Andrei Emeltchenko10fa48d2021-01-12 09:56:32 +0200667 if not harness.capture_coverage:
668 ser.close()
669 break
Anas Nashifce2b4182020-03-24 14:40:28 -0400670
671 log_out_fp.close()
672
Anas Nashif3b86f132020-05-21 10:35:33 -0400673 def device_is_available(self, instance):
674 device = instance.platform.name
675 fixture = instance.testcase.harness_config.get("fixture")
Anas Nashif8305d1b2020-11-26 11:55:02 -0500676 for d in self.suite.duts:
Anas Nashif531fe892020-09-11 13:56:33 -0400677 if fixture and fixture not in d.fixtures:
Anas Nashif3b86f132020-05-21 10:35:33 -0400678 continue
Andy Ross098fce32021-03-02 05:13:07 -0800679 if d.platform != device or not (d.serial or d.serial_pty):
680 continue
681 d.lock.acquire()
682 avail = False
683 if d.available:
Anas Nashif531fe892020-09-11 13:56:33 -0400684 d.available = 0
685 d.counter += 1
Andy Ross098fce32021-03-02 05:13:07 -0800686 avail = True
687 d.lock.release()
688 if avail:
Anas Nashif2a740532021-02-04 08:08:20 -0500689 return d
Anas Nashifce2b4182020-03-24 14:40:28 -0400690
Anas Nashif2a740532021-02-04 08:08:20 -0500691 return None
Anas Nashifce2b4182020-03-24 14:40:28 -0400692
693 def make_device_available(self, serial):
Anas Nashif8305d1b2020-11-26 11:55:02 -0500694 for d in self.suite.duts:
Anas Nashif531fe892020-09-11 13:56:33 -0400695 if d.serial == serial or d.serial_pty:
696 d.available = 1
Anas Nashifce2b4182020-03-24 14:40:28 -0400697
698 @staticmethod
699 def run_custom_script(script, timeout):
700 with subprocess.Popen(script, stderr=subprocess.PIPE, stdout=subprocess.PIPE) as proc:
701 try:
702 stdout, _ = proc.communicate(timeout=timeout)
703 logger.debug(stdout.decode())
704
705 except subprocess.TimeoutExpired:
706 proc.kill()
707 proc.communicate()
708 logger.error("{} timed out".format(script))
709
710 def handle(self):
711 out_state = "failed"
Anas Nashif531fe892020-09-11 13:56:33 -0400712 runner = None
Anas Nashifce2b4182020-03-24 14:40:28 -0400713
Anas Nashif2a740532021-02-04 08:08:20 -0500714 hardware = self.device_is_available(self.instance)
715 while not hardware:
Anas Nashifce2b4182020-03-24 14:40:28 -0400716 logger.debug("Waiting for device {} to become available".format(self.instance.platform.name))
717 time.sleep(1)
Anas Nashif2a740532021-02-04 08:08:20 -0500718 hardware = self.device_is_available(self.instance)
Anas Nashifce2b4182020-03-24 14:40:28 -0400719
Anas Nashif2a740532021-02-04 08:08:20 -0500720 runner = hardware.runner or self.suite.west_runner
Anas Nashif531fe892020-09-11 13:56:33 -0400721 serial_pty = hardware.serial_pty
Anas Nashif2a740532021-02-04 08:08:20 -0500722
Anas Nashif531fe892020-09-11 13:56:33 -0400723 ser_pty_process = None
Andrei Emeltchenkod8b845b2020-03-31 13:22:30 +0300724 if serial_pty:
725 master, slave = pty.openpty()
Andrei Emeltchenkod8b845b2020-03-31 13:22:30 +0300726 try:
Andrei Emeltchenko9f7a9032020-09-02 11:43:07 +0300727 ser_pty_process = subprocess.Popen(re.split(',| ', serial_pty), stdout=master, stdin=master, stderr=master)
Andrei Emeltchenkod8b845b2020-03-31 13:22:30 +0300728 except subprocess.CalledProcessError as error:
729 logger.error("Failed to run subprocess {}, error {}".format(serial_pty, error.output))
730 return
731
732 serial_device = os.ttyname(slave)
733 else:
Anas Nashif531fe892020-09-11 13:56:33 -0400734 serial_device = hardware.serial
Andrei Emeltchenkod8b845b2020-03-31 13:22:30 +0300735
736 logger.debug("Using serial device {}".format(serial_device))
Anas Nashifce2b4182020-03-24 14:40:28 -0400737
Øyvind Rønningstadf72aef12020-07-01 16:58:54 +0200738 if (self.suite.west_flash is not None) or runner:
739 command = ["west", "flash", "--skip-rebuild", "-d", self.build_dir]
740 command_extra_args = []
741
742 # There are three ways this option is used.
743 # 1) bare: --west-flash
744 # This results in options.west_flash == []
745 # 2) with a value: --west-flash="--board-id=42"
746 # This results in options.west_flash == "--board-id=42"
747 # 3) Multiple values: --west-flash="--board-id=42,--erase"
748 # This results in options.west_flash == "--board-id=42 --erase"
749 if self.suite.west_flash and self.suite.west_flash != []:
750 command_extra_args.extend(self.suite.west_flash.split(','))
751
752 if runner:
753 command.append("--runner")
754 command.append(runner)
755
Anas Nashif531fe892020-09-11 13:56:33 -0400756 board_id = hardware.probe_id or hardware.id
757 product = hardware.product
Øyvind Rønningstadf72aef12020-07-01 16:58:54 +0200758 if board_id is not None:
759 if runner == "pyocd":
760 command_extra_args.append("--board-id")
761 command_extra_args.append(board_id)
762 elif runner == "nrfjprog":
763 command_extra_args.append("--snr")
764 command_extra_args.append(board_id)
765 elif runner == "openocd" and product == "STM32 STLink":
766 command_extra_args.append("--cmd-pre-init")
767 command_extra_args.append("hla_serial %s" % (board_id))
768 elif runner == "openocd" and product == "STLINK-V3":
769 command_extra_args.append("--cmd-pre-init")
770 command_extra_args.append("hla_serial %s" % (board_id))
771 elif runner == "openocd" and product == "EDBG CMSIS-DAP":
772 command_extra_args.append("--cmd-pre-init")
773 command_extra_args.append("cmsis_dap_serial %s" % (board_id))
774 elif runner == "jlink":
775 command.append("--tool-opt=-SelectEmuBySN %s" % (board_id))
776
777 if command_extra_args != []:
778 command.append('--')
779 command.extend(command_extra_args)
780 else:
781 command = [self.generator_cmd, "-C", self.build_dir, "flash"]
782
Anas Nashif531fe892020-09-11 13:56:33 -0400783 pre_script = hardware.pre_script
784 post_flash_script = hardware.post_flash_script
785 post_script = hardware.post_script
Watson Zeng3b43d942020-08-25 15:22:39 +0800786
787 if pre_script:
788 self.run_custom_script(pre_script, 30)
789
Anas Nashifce2b4182020-03-24 14:40:28 -0400790 try:
791 ser = serial.Serial(
792 serial_device,
793 baudrate=115200,
794 parity=serial.PARITY_NONE,
795 stopbits=serial.STOPBITS_ONE,
796 bytesize=serial.EIGHTBITS,
797 timeout=self.timeout
798 )
799 except serial.SerialException as e:
800 self.set_state("failed", 0)
801 self.instance.reason = "Failed"
802 logger.error("Serial device error: %s" % (str(e)))
Andrei Emeltchenkod8b845b2020-03-31 13:22:30 +0300803
Anas Nashif531fe892020-09-11 13:56:33 -0400804 if serial_pty and ser_pty_process:
Andrei Emeltchenkod8b845b2020-03-31 13:22:30 +0300805 ser_pty_process.terminate()
806 outs, errs = ser_pty_process.communicate()
807 logger.debug("Process {} terminated outs: {} errs {}".format(serial_pty, outs, errs))
808
Anas Nashifce2b4182020-03-24 14:40:28 -0400809 self.make_device_available(serial_device)
810 return
811
812 ser.flush()
813
814 harness_name = self.instance.testcase.harness.capitalize()
815 harness_import = HarnessImporter(harness_name)
816 harness = harness_import.instance
817 harness.configure(self.instance)
818 read_pipe, write_pipe = os.pipe()
819 start_time = time.time()
820
Anas Nashifce2b4182020-03-24 14:40:28 -0400821 t = threading.Thread(target=self.monitor_serial, daemon=True,
822 args=(ser, read_pipe, harness))
823 t.start()
824
825 d_log = "{}/device.log".format(self.instance.build_dir)
826 logger.debug('Flash command: %s', command)
827 try:
828 stdout = stderr = None
829 with subprocess.Popen(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE) as proc:
830 try:
831 (stdout, stderr) = proc.communicate(timeout=30)
832 logger.debug(stdout.decode())
833
834 if proc.returncode != 0:
835 self.instance.reason = "Device issue (Flash?)"
836 with open(d_log, "w") as dlog_fp:
837 dlog_fp.write(stderr.decode())
Maciej Perkowskif050a992021-02-24 13:43:05 +0100838 os.write(write_pipe, b'x') # halt the thread
839 out_state = "flash_error"
Anas Nashifce2b4182020-03-24 14:40:28 -0400840 except subprocess.TimeoutExpired:
841 proc.kill()
842 (stdout, stderr) = proc.communicate()
843 self.instance.reason = "Device issue (Timeout)"
844
845 with open(d_log, "w") as dlog_fp:
846 dlog_fp.write(stderr.decode())
847
848 except subprocess.CalledProcessError:
849 os.write(write_pipe, b'x') # halt the thread
850
851 if post_flash_script:
852 self.run_custom_script(post_flash_script, 30)
853
Anas Nashifce2b4182020-03-24 14:40:28 -0400854 t.join(self.timeout)
855 if t.is_alive():
856 logger.debug("Timed out while monitoring serial output on {}".format(self.instance.platform.name))
857 out_state = "timeout"
858
859 if ser.isOpen():
860 ser.close()
861
Andrei Emeltchenkod8b845b2020-03-31 13:22:30 +0300862 if serial_pty:
863 ser_pty_process.terminate()
864 outs, errs = ser_pty_process.communicate()
865 logger.debug("Process {} terminated outs: {} errs {}".format(serial_pty, outs, errs))
866
Anas Nashifce2b4182020-03-24 14:40:28 -0400867 os.close(write_pipe)
868 os.close(read_pipe)
869
870 handler_time = time.time() - start_time
871
Maciej Perkowskif050a992021-02-24 13:43:05 +0100872 if out_state in ["timeout", "flash_error"]:
Piotr Golyzniak0e1ecb32021-09-23 00:14:04 +0200873 self.add_missing_testscases(harness)
Anas Nashifce2b4182020-03-24 14:40:28 -0400874
Maciej Perkowskif050a992021-02-24 13:43:05 +0100875 if out_state == "timeout":
876 self.instance.reason = "Timeout"
877 elif out_state == "flash_error":
878 self.instance.reason = "Flash error"
Anas Nashifce2b4182020-03-24 14:40:28 -0400879
YouhuaX Zhu965c8b92020-10-22 09:37:27 +0800880 if harness.is_pytest:
881 harness.pytest_run(self.log)
Anas Nashifce2b4182020-03-24 14:40:28 -0400882 self.instance.results = harness.tests
883
peng1 chen64787352021-01-27 11:00:05 +0800884 # sometimes a test instance hasn't been executed successfully with an
885 # empty dictionary results, in order to include it into final report,
886 # so fill the results as BLOCK
887 if self.instance.results == {}:
888 for k in self.instance.testcase.cases:
889 self.instance.results[k] = 'BLOCK'
890
Anas Nashifce2b4182020-03-24 14:40:28 -0400891 if harness.state:
892 self.set_state(harness.state, handler_time)
Maciej Perkowskif050a992021-02-24 13:43:05 +0100893 if harness.state == "failed":
Anas Nashifce2b4182020-03-24 14:40:28 -0400894 self.instance.reason = "Failed"
895 else:
896 self.set_state(out_state, handler_time)
897
898 if post_script:
899 self.run_custom_script(post_script, 30)
900
901 self.make_device_available(serial_device)
Anas Nashifce2b4182020-03-24 14:40:28 -0400902 self.record(harness)
903
904
905class QEMUHandler(Handler):
906 """Spawns a thread to monitor QEMU output from pipes
907
908 We pass QEMU_PIPE to 'make run' and monitor the pipes for output.
909 We need to do this as once qemu starts, it runs forever until killed.
910 Test cases emit special messages to the console as they run, we check
911 for these to collect whether the test passed or failed.
912 """
913
914 def __init__(self, instance, type_str):
915 """Constructor
916
917 @param instance Test instance
918 """
919
920 super().__init__(instance, type_str)
921 self.fifo_fn = os.path.join(instance.build_dir, "qemu-fifo")
922
923 self.pid_fn = os.path.join(instance.build_dir, "qemu.pid")
924
Daniel Leungfaae15d2020-08-18 10:13:35 -0700925 if "ignore_qemu_crash" in instance.testcase.tags:
926 self.ignore_qemu_crash = True
927 self.ignore_unexpected_eof = True
928 else:
929 self.ignore_qemu_crash = False
930 self.ignore_unexpected_eof = False
931
Anas Nashifce2b4182020-03-24 14:40:28 -0400932 @staticmethod
Wentong Wu0d619ae2020-05-05 19:46:49 -0400933 def _get_cpu_time(pid):
934 """get process CPU time.
935
936 The guest virtual time in QEMU icount mode isn't host time and
937 it's maintained by counting guest instructions, so we use QEMU
938 process exection time to mostly simulate the time of guest OS.
939 """
940 proc = psutil.Process(pid)
941 cpu_time = proc.cpu_times()
942 return cpu_time.user + cpu_time.system
943
944 @staticmethod
Daniel Leungfaae15d2020-08-18 10:13:35 -0700945 def _thread(handler, timeout, outdir, logfile, fifo_fn, pid_fn, results, harness,
946 ignore_unexpected_eof=False):
Anas Nashifce2b4182020-03-24 14:40:28 -0400947 fifo_in = fifo_fn + ".in"
948 fifo_out = fifo_fn + ".out"
949
950 # These in/out nodes are named from QEMU's perspective, not ours
951 if os.path.exists(fifo_in):
952 os.unlink(fifo_in)
953 os.mkfifo(fifo_in)
954 if os.path.exists(fifo_out):
955 os.unlink(fifo_out)
956 os.mkfifo(fifo_out)
957
958 # We don't do anything with out_fp but we need to open it for
959 # writing so that QEMU doesn't block, due to the way pipes work
960 out_fp = open(fifo_in, "wb")
961 # Disable internal buffering, we don't
962 # want read() or poll() to ever block if there is data in there
963 in_fp = open(fifo_out, "rb", buffering=0)
964 log_out_fp = open(logfile, "wt")
965
966 start_time = time.time()
967 timeout_time = start_time + timeout
968 p = select.poll()
969 p.register(in_fp, select.POLLIN)
970 out_state = None
971
972 line = ""
973 timeout_extended = False
Wentong Wu0d619ae2020-05-05 19:46:49 -0400974
975 pid = 0
976 if os.path.exists(pid_fn):
977 pid = int(open(pid_fn).read())
978
Anas Nashifce2b4182020-03-24 14:40:28 -0400979 while True:
980 this_timeout = int((timeout_time - time.time()) * 1000)
981 if this_timeout < 0 or not p.poll(this_timeout):
Wentong Wu517633c2020-07-24 21:13:01 +0800982 try:
983 if pid and this_timeout > 0:
984 #there's possibility we polled nothing because
985 #of not enough CPU time scheduled by host for
986 #QEMU process during p.poll(this_timeout)
987 cpu_time = QEMUHandler._get_cpu_time(pid)
988 if cpu_time < timeout and not out_state:
989 timeout_time = time.time() + (timeout - cpu_time)
990 continue
991 except ProcessLookupError:
992 out_state = "failed"
993 break
Wentong Wu0d619ae2020-05-05 19:46:49 -0400994
Anas Nashifce2b4182020-03-24 14:40:28 -0400995 if not out_state:
996 out_state = "timeout"
997 break
998
Wentong Wu0d619ae2020-05-05 19:46:49 -0400999 if pid == 0 and os.path.exists(pid_fn):
1000 pid = int(open(pid_fn).read())
1001
YouhuaX Zhu965c8b92020-10-22 09:37:27 +08001002 if harness.is_pytest:
1003 harness.handle(None)
1004 out_state = harness.state
1005 break
1006
Anas Nashifce2b4182020-03-24 14:40:28 -04001007 try:
1008 c = in_fp.read(1).decode("utf-8")
1009 except UnicodeDecodeError:
1010 # Test is writing something weird, fail
1011 out_state = "unexpected byte"
1012 break
1013
1014 if c == "":
1015 # EOF, this shouldn't happen unless QEMU crashes
Daniel Leungfaae15d2020-08-18 10:13:35 -07001016 if not ignore_unexpected_eof:
1017 out_state = "unexpected eof"
Anas Nashifce2b4182020-03-24 14:40:28 -04001018 break
1019 line = line + c
1020 if c != "\n":
1021 continue
1022
1023 # line contains a full line of data output from QEMU
1024 log_out_fp.write(line)
1025 log_out_fp.flush()
1026 line = line.strip()
Anas Nashif743594f2020-09-10 08:19:47 -04001027 logger.debug(f"QEMU ({pid}): {line}")
Anas Nashifce2b4182020-03-24 14:40:28 -04001028
1029 harness.handle(line)
1030 if harness.state:
1031 # if we have registered a fail make sure the state is not
1032 # overridden by a false success message coming from the
1033 # testsuite
Anas Nashif869ca052020-07-07 14:29:07 -04001034 if out_state not in ['failed', 'unexpected eof', 'unexpected byte']:
Anas Nashifce2b4182020-03-24 14:40:28 -04001035 out_state = harness.state
1036
1037 # if we get some state, that means test is doing well, we reset
1038 # the timeout and wait for 2 more seconds to catch anything
1039 # printed late. We wait much longer if code
1040 # coverage is enabled since dumping this information can
1041 # take some time.
1042 if not timeout_extended or harness.capture_coverage:
1043 timeout_extended = True
1044 if harness.capture_coverage:
1045 timeout_time = time.time() + 30
1046 else:
1047 timeout_time = time.time() + 2
1048 line = ""
1049
YouhuaX Zhu965c8b92020-10-22 09:37:27 +08001050 if harness.is_pytest:
1051 harness.pytest_run(logfile)
1052 out_state = harness.state
1053
Anas Nashifce2b4182020-03-24 14:40:28 -04001054 handler.record(harness)
1055
1056 handler_time = time.time() - start_time
Anas Nashif743594f2020-09-10 08:19:47 -04001057 logger.debug(f"QEMU ({pid}) complete ({out_state}) after {handler_time} seconds")
Anas Nashif869ca052020-07-07 14:29:07 -04001058
Anas Nashifce2b4182020-03-24 14:40:28 -04001059 if out_state == "timeout":
1060 handler.instance.reason = "Timeout"
Anas Nashif06052922020-07-15 22:44:24 -04001061 handler.set_state("failed", handler_time)
Anas Nashifce2b4182020-03-24 14:40:28 -04001062 elif out_state == "failed":
1063 handler.instance.reason = "Failed"
Anas Nashif869ca052020-07-07 14:29:07 -04001064 handler.set_state("failed", handler_time)
Anas Nashif06052922020-07-15 22:44:24 -04001065 elif out_state in ['unexpected eof', 'unexpected byte']:
Anas Nashif869ca052020-07-07 14:29:07 -04001066 handler.instance.reason = out_state
Anas Nashif06052922020-07-15 22:44:24 -04001067 handler.set_state("failed", handler_time)
1068 else:
1069 handler.set_state(out_state, handler_time)
Anas Nashifce2b4182020-03-24 14:40:28 -04001070
1071 log_out_fp.close()
1072 out_fp.close()
1073 in_fp.close()
Wentong Wu0d619ae2020-05-05 19:46:49 -04001074 if pid:
Anas Nashifce2b4182020-03-24 14:40:28 -04001075 try:
1076 if pid:
1077 os.kill(pid, signal.SIGTERM)
1078 except ProcessLookupError:
1079 # Oh well, as long as it's dead! User probably sent Ctrl-C
1080 pass
1081
1082 os.unlink(fifo_in)
1083 os.unlink(fifo_out)
1084
1085 def handle(self):
1086 self.results = {}
1087 self.run = True
1088
1089 # We pass this to QEMU which looks for fifos with .in and .out
1090 # suffixes.
Anas Nashifce2b4182020-03-24 14:40:28 -04001091
Anas Nashifc1c10992020-09-10 07:36:00 -04001092 self.fifo_fn = os.path.join(self.instance.build_dir, "qemu-fifo")
Anas Nashifce2b4182020-03-24 14:40:28 -04001093 self.pid_fn = os.path.join(self.instance.build_dir, "qemu.pid")
Anas Nashifc1c10992020-09-10 07:36:00 -04001094
Anas Nashifce2b4182020-03-24 14:40:28 -04001095 if os.path.exists(self.pid_fn):
1096 os.unlink(self.pid_fn)
1097
1098 self.log_fn = self.log
1099
1100 harness_import = HarnessImporter(self.instance.testcase.harness.capitalize())
1101 harness = harness_import.instance
1102 harness.configure(self.instance)
YouhuaX Zhu965c8b92020-10-22 09:37:27 +08001103
Anas Nashifce2b4182020-03-24 14:40:28 -04001104 self.thread = threading.Thread(name=self.name, target=QEMUHandler._thread,
1105 args=(self, self.timeout, self.build_dir,
1106 self.log_fn, self.fifo_fn,
Daniel Leungfaae15d2020-08-18 10:13:35 -07001107 self.pid_fn, self.results, harness,
1108 self.ignore_unexpected_eof))
Anas Nashifce2b4182020-03-24 14:40:28 -04001109
1110 self.instance.results = harness.tests
1111 self.thread.daemon = True
1112 logger.debug("Spawning QEMUHandler Thread for %s" % self.name)
1113 self.thread.start()
Eugeniy Paltsevafb34e32020-11-29 21:28:28 +03001114 if sys.stdout.isatty():
1115 subprocess.call(["stty", "sane"])
Anas Nashifce2b4182020-03-24 14:40:28 -04001116
1117 logger.debug("Running %s (%s)" % (self.name, self.type_str))
1118 command = [self.generator_cmd]
1119 command += ["-C", self.build_dir, "run"]
1120
Anas Nashiffdc02b62020-09-09 19:11:19 -04001121 is_timeout = False
Anas Nashif743594f2020-09-10 08:19:47 -04001122 qemu_pid = None
Anas Nashiffdc02b62020-09-09 19:11:19 -04001123
Anas Nashifce2b4182020-03-24 14:40:28 -04001124 with subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.build_dir) as proc:
1125 logger.debug("Spawning QEMUHandler Thread for %s" % self.name)
Anas Nashif743594f2020-09-10 08:19:47 -04001126
Wentong Wu7ec57b42020-05-05 19:19:18 -04001127 try:
1128 proc.wait(self.timeout)
1129 except subprocess.TimeoutExpired:
Anas Nashiffdc02b62020-09-09 19:11:19 -04001130 # sometimes QEMU can't handle SIGTERM signal correctly
1131 # in that case kill -9 QEMU process directly and leave
Anas Nashifb18f3112020-12-07 11:40:19 -05001132 # twister to judge testing result by console output
Anas Nashiffdc02b62020-09-09 19:11:19 -04001133
1134 is_timeout = True
Jingru Wangfed1c542021-06-11 11:54:55 +08001135 self.terminate(proc)
1136 if harness.state == "passed":
1137 self.returncode = 0
Wentong Wu7ec57b42020-05-05 19:19:18 -04001138 else:
Wentong Wu7ec57b42020-05-05 19:19:18 -04001139 self.returncode = proc.returncode
1140 else:
Anas Nashif743594f2020-09-10 08:19:47 -04001141 if os.path.exists(self.pid_fn):
1142 qemu_pid = int(open(self.pid_fn).read())
1143 logger.debug(f"No timeout, return code from QEMU ({qemu_pid}): {proc.returncode}")
Wentong Wu7ec57b42020-05-05 19:19:18 -04001144 self.returncode = proc.returncode
Daniel Leung5b1b4a32020-08-18 10:10:36 -07001145 # Need to wait for harness to finish processing
1146 # output from QEMU. Otherwise it might miss some
1147 # error messages.
Jingru Wangfed1c542021-06-11 11:54:55 +08001148 self.thread.join(0)
1149 if self.thread.is_alive():
1150 logger.debug("Timed out while monitoring QEMU output")
Daniel Leung5b1b4a32020-08-18 10:10:36 -07001151
Wentong Wu7ec57b42020-05-05 19:19:18 -04001152 if os.path.exists(self.pid_fn):
Anas Nashif743594f2020-09-10 08:19:47 -04001153 qemu_pid = int(open(self.pid_fn).read())
Wentong Wu7ec57b42020-05-05 19:19:18 -04001154 os.unlink(self.pid_fn)
Anas Nashifce2b4182020-03-24 14:40:28 -04001155
Anas Nashif743594f2020-09-10 08:19:47 -04001156 logger.debug(f"return code from QEMU ({qemu_pid}): {self.returncode}")
Anas Nashif869ca052020-07-07 14:29:07 -04001157
Daniel Leungfaae15d2020-08-18 10:13:35 -07001158 if (self.returncode != 0 and not self.ignore_qemu_crash) or not harness.state:
Anas Nashifce2b4182020-03-24 14:40:28 -04001159 self.set_state("failed", 0)
Anas Nashiffdc02b62020-09-09 19:11:19 -04001160 if is_timeout:
1161 self.instance.reason = "Timeout"
1162 else:
1163 self.instance.reason = "Exited with {}".format(self.returncode)
Piotr Golyzniak0e1ecb32021-09-23 00:14:04 +02001164 self.add_missing_testscases(harness)
Anas Nashifce2b4182020-03-24 14:40:28 -04001165
1166 def get_fifo(self):
1167 return self.fifo_fn
1168
1169
1170class SizeCalculator:
1171 alloc_sections = [
1172 "bss",
1173 "noinit",
1174 "app_bss",
1175 "app_noinit",
1176 "ccm_bss",
1177 "ccm_noinit"
1178 ]
1179
1180 rw_sections = [
1181 "datas",
1182 "initlevel",
1183 "exceptions",
1184 "initshell",
Andrew Boie45979da2020-05-23 14:38:39 -07001185 "_static_thread_data_area",
1186 "k_timer_area",
1187 "k_mem_slab_area",
1188 "k_mem_pool_area",
Anas Nashifce2b4182020-03-24 14:40:28 -04001189 "sw_isr_table",
Andrew Boie45979da2020-05-23 14:38:39 -07001190 "k_sem_area",
1191 "k_mutex_area",
Anas Nashifce2b4182020-03-24 14:40:28 -04001192 "app_shmem_regions",
1193 "_k_fifo_area",
1194 "_k_lifo_area",
Andrew Boie45979da2020-05-23 14:38:39 -07001195 "k_stack_area",
1196 "k_msgq_area",
1197 "k_mbox_area",
1198 "k_pipe_area",
Daniel Leung203556c2020-08-24 12:40:26 -07001199 "net_if_area",
1200 "net_if_dev_area",
1201 "net_l2_area",
Anas Nashifce2b4182020-03-24 14:40:28 -04001202 "net_l2_data",
Andrew Boie45979da2020-05-23 14:38:39 -07001203 "k_queue_area",
Anas Nashifce2b4182020-03-24 14:40:28 -04001204 "_net_buf_pool_area",
1205 "app_datas",
1206 "kobject_data",
1207 "mmu_tables",
1208 "app_pad",
1209 "priv_stacks",
1210 "ccm_data",
1211 "usb_descriptor",
1212 "usb_data", "usb_bos_desc",
Jukka Rissanen420b1952020-04-01 12:47:53 +03001213 "uart_mux",
Anas Nashifce2b4182020-03-24 14:40:28 -04001214 'log_backends_sections',
1215 'log_dynamic_sections',
1216 'log_const_sections',
1217 "app_smem",
1218 'shell_root_cmds_sections',
1219 'log_const_sections',
1220 "font_entry_sections",
1221 "priv_stacks_noinit",
1222 "_GCOV_BSS_SECTION_NAME",
1223 "gcov",
Daniel Leung203556c2020-08-24 12:40:26 -07001224 "nocache",
1225 "devices",
1226 "k_heap_area",
Anas Nashifce2b4182020-03-24 14:40:28 -04001227 ]
1228
1229 # These get copied into RAM only on non-XIP
1230 ro_sections = [
1231 "rom_start",
1232 "text",
1233 "ctors",
1234 "init_array",
1235 "reset",
Andrew Boie45979da2020-05-23 14:38:39 -07001236 "z_object_assignment_area",
Anas Nashifce2b4182020-03-24 14:40:28 -04001237 "rodata",
Anas Nashifce2b4182020-03-24 14:40:28 -04001238 "net_l2",
1239 "vector",
1240 "sw_isr_table",
Andrew Boie45979da2020-05-23 14:38:39 -07001241 "settings_handler_static_area",
Daniel Leung203556c2020-08-24 12:40:26 -07001242 "bt_l2cap_fixed_chan_area",
1243 "bt_l2cap_br_fixed_chan_area",
1244 "bt_gatt_service_static_area",
Anas Nashifce2b4182020-03-24 14:40:28 -04001245 "vectors",
Andrew Boie45979da2020-05-23 14:38:39 -07001246 "net_socket_register_area",
1247 "net_ppp_proto",
1248 "shell_area",
1249 "tracing_backend_area",
Daniel Leung203556c2020-08-24 12:40:26 -07001250 "ppp_protocol_handler_area",
Anas Nashifce2b4182020-03-24 14:40:28 -04001251 ]
1252
1253 def __init__(self, filename, extra_sections):
1254 """Constructor
1255
1256 @param filename Path to the output binary
1257 The <filename> is parsed by objdump to determine section sizes
1258 """
1259 # Make sure this is an ELF binary
1260 with open(filename, "rb") as f:
1261 magic = f.read(4)
1262
1263 try:
1264 if magic != b'\x7fELF':
Anas Nashif45943702020-12-11 17:55:15 -05001265 raise TwisterRuntimeError("%s is not an ELF binary" % filename)
Anas Nashifce2b4182020-03-24 14:40:28 -04001266 except Exception as e:
1267 print(str(e))
1268 sys.exit(2)
1269
1270 # Search for CONFIG_XIP in the ELF's list of symbols using NM and AWK.
1271 # GREP can not be used as it returns an error if the symbol is not
1272 # found.
1273 is_xip_command = "nm " + filename + \
1274 " | awk '/CONFIG_XIP/ { print $3 }'"
1275 is_xip_output = subprocess.check_output(
1276 is_xip_command, shell=True, stderr=subprocess.STDOUT).decode(
1277 "utf-8").strip()
1278 try:
1279 if is_xip_output.endswith("no symbols"):
Anas Nashif45943702020-12-11 17:55:15 -05001280 raise TwisterRuntimeError("%s has no symbol information" % filename)
Anas Nashifce2b4182020-03-24 14:40:28 -04001281 except Exception as e:
1282 print(str(e))
1283 sys.exit(2)
1284
1285 self.is_xip = (len(is_xip_output) != 0)
1286
1287 self.filename = filename
1288 self.sections = []
1289 self.rom_size = 0
1290 self.ram_size = 0
1291 self.extra_sections = extra_sections
1292
1293 self._calculate_sizes()
1294
1295 def get_ram_size(self):
1296 """Get the amount of RAM the application will use up on the device
1297
1298 @return amount of RAM, in bytes
1299 """
1300 return self.ram_size
1301
1302 def get_rom_size(self):
1303 """Get the size of the data that this application uses on device's flash
1304
1305 @return amount of ROM, in bytes
1306 """
1307 return self.rom_size
1308
1309 def unrecognized_sections(self):
1310 """Get a list of sections inside the binary that weren't recognized
1311
1312 @return list of unrecognized section names
1313 """
1314 slist = []
1315 for v in self.sections:
1316 if not v["recognized"]:
1317 slist.append(v["name"])
1318 return slist
1319
1320 def _calculate_sizes(self):
1321 """ Calculate RAM and ROM usage by section """
1322 objdump_command = "objdump -h " + self.filename
1323 objdump_output = subprocess.check_output(
1324 objdump_command, shell=True).decode("utf-8").splitlines()
1325
1326 for line in objdump_output:
1327 words = line.split()
1328
1329 if not words: # Skip lines that are too short
1330 continue
1331
1332 index = words[0]
1333 if not index[0].isdigit(): # Skip lines that do not start
1334 continue # with a digit
1335
1336 name = words[1] # Skip lines with section names
1337 if name[0] == '.': # starting with '.'
1338 continue
1339
1340 # TODO this doesn't actually reflect the size in flash or RAM as
1341 # it doesn't include linker-imposed padding between sections.
1342 # It is close though.
1343 size = int(words[2], 16)
1344 if size == 0:
1345 continue
1346
1347 load_addr = int(words[4], 16)
1348 virt_addr = int(words[3], 16)
1349
1350 # Add section to memory use totals (for both non-XIP and XIP scenarios)
1351 # Unrecognized section names are not included in the calculations.
1352 recognized = True
1353 if name in SizeCalculator.alloc_sections:
1354 self.ram_size += size
1355 stype = "alloc"
1356 elif name in SizeCalculator.rw_sections:
1357 self.ram_size += size
1358 self.rom_size += size
1359 stype = "rw"
1360 elif name in SizeCalculator.ro_sections:
1361 self.rom_size += size
1362 if not self.is_xip:
1363 self.ram_size += size
1364 stype = "ro"
1365 else:
1366 stype = "unknown"
1367 if name not in self.extra_sections:
1368 recognized = False
1369
1370 self.sections.append({"name": name, "load_addr": load_addr,
1371 "size": size, "virt_addr": virt_addr,
1372 "type": stype, "recognized": recognized})
1373
1374
1375
Anas Nashif45943702020-12-11 17:55:15 -05001376class TwisterConfigParser:
Anas Nashifce2b4182020-03-24 14:40:28 -04001377 """Class to read test case files with semantic checking
1378 """
1379
1380 def __init__(self, filename, schema):
Anas Nashif45943702020-12-11 17:55:15 -05001381 """Instantiate a new TwisterConfigParser object
Anas Nashifce2b4182020-03-24 14:40:28 -04001382
1383 @param filename Source .yaml file to read
1384 """
1385 self.data = {}
1386 self.schema = schema
1387 self.filename = filename
1388 self.tests = {}
1389 self.common = {}
1390
1391 def load(self):
1392 self.data = scl.yaml_load_verify(self.filename, self.schema)
1393
1394 if 'tests' in self.data:
1395 self.tests = self.data['tests']
1396 if 'common' in self.data:
1397 self.common = self.data['common']
1398
1399 def _cast_value(self, value, typestr):
1400 if isinstance(value, str):
1401 v = value.strip()
1402 if typestr == "str":
1403 return v
1404
1405 elif typestr == "float":
1406 return float(value)
1407
1408 elif typestr == "int":
1409 return int(value)
1410
1411 elif typestr == "bool":
1412 return value
1413
1414 elif typestr.startswith("list") and isinstance(value, list):
1415 return value
1416 elif typestr.startswith("list") and isinstance(value, str):
1417 vs = v.split()
1418 if len(typestr) > 4 and typestr[4] == ":":
1419 return [self._cast_value(vsi, typestr[5:]) for vsi in vs]
1420 else:
1421 return vs
1422
1423 elif typestr.startswith("set"):
1424 vs = v.split()
1425 if len(typestr) > 3 and typestr[3] == ":":
1426 return {self._cast_value(vsi, typestr[4:]) for vsi in vs}
1427 else:
1428 return set(vs)
1429
1430 elif typestr.startswith("map"):
1431 return value
1432 else:
1433 raise ConfigurationError(
1434 self.filename, "unknown type '%s'" % value)
1435
1436 def get_test(self, name, valid_keys):
1437 """Get a dictionary representing the keys/values within a test
1438
1439 @param name The test in the .yaml file to retrieve data from
1440 @param valid_keys A dictionary representing the intended semantics
1441 for this test. Each key in this dictionary is a key that could
1442 be specified, if a key is given in the .yaml file which isn't in
1443 here, it will generate an error. Each value in this dictionary
1444 is another dictionary containing metadata:
1445
1446 "default" - Default value if not given
1447 "type" - Data type to convert the text value to. Simple types
1448 supported are "str", "float", "int", "bool" which will get
1449 converted to respective Python data types. "set" and "list"
1450 may also be specified which will split the value by
1451 whitespace (but keep the elements as strings). finally,
1452 "list:<type>" and "set:<type>" may be given which will
1453 perform a type conversion after splitting the value up.
1454 "required" - If true, raise an error if not defined. If false
1455 and "default" isn't specified, a type conversion will be
1456 done on an empty string
1457 @return A dictionary containing the test key-value pairs with
1458 type conversion and default values filled in per valid_keys
1459 """
1460
1461 d = {}
1462 for k, v in self.common.items():
1463 d[k] = v
1464
1465 for k, v in self.tests[name].items():
Anas Nashifce2b4182020-03-24 14:40:28 -04001466 if k in d:
1467 if isinstance(d[k], str):
1468 # By default, we just concatenate string values of keys
1469 # which appear both in "common" and per-test sections,
1470 # but some keys are handled in adhoc way based on their
1471 # semantics.
1472 if k == "filter":
1473 d[k] = "(%s) and (%s)" % (d[k], v)
1474 else:
1475 d[k] += " " + v
1476 else:
1477 d[k] = v
1478
1479 for k, kinfo in valid_keys.items():
1480 if k not in d:
1481 if "required" in kinfo:
1482 required = kinfo["required"]
1483 else:
1484 required = False
1485
1486 if required:
1487 raise ConfigurationError(
1488 self.filename,
1489 "missing required value for '%s' in test '%s'" %
1490 (k, name))
1491 else:
1492 if "default" in kinfo:
1493 default = kinfo["default"]
1494 else:
1495 default = self._cast_value("", kinfo["type"])
1496 d[k] = default
1497 else:
1498 try:
1499 d[k] = self._cast_value(d[k], kinfo["type"])
1500 except ValueError:
1501 raise ConfigurationError(
1502 self.filename, "bad %s value '%s' for key '%s' in name '%s'" %
1503 (kinfo["type"], d[k], k, name))
1504
1505 return d
1506
1507
1508class Platform:
1509 """Class representing metadata for a particular platform
1510
1511 Maps directly to BOARD when building"""
1512
1513 platform_schema = scl.yaml_load(os.path.join(ZEPHYR_BASE,
Anas Nashifc24bf6f2020-12-07 11:18:07 -05001514 "scripts", "schemas", "twister", "platform-schema.yaml"))
Anas Nashifce2b4182020-03-24 14:40:28 -04001515
1516 def __init__(self):
1517 """Constructor.
1518
1519 """
1520
1521 self.name = ""
Anas Nashifb18f3112020-12-07 11:40:19 -05001522 self.twister = True
Anas Nashifce2b4182020-03-24 14:40:28 -04001523 # if no RAM size is specified by the board, take a default of 128K
1524 self.ram = 128
1525
1526 self.ignore_tags = []
Anas Nashife8e367a2020-07-16 16:27:04 -04001527 self.only_tags = []
Anas Nashifce2b4182020-03-24 14:40:28 -04001528 self.default = False
1529 # if no flash size is specified by the board, take a default of 512K
1530 self.flash = 512
1531 self.supported = set()
1532
1533 self.arch = ""
1534 self.type = "na"
1535 self.simulation = "na"
1536 self.supported_toolchains = []
1537 self.env = []
1538 self.env_satisfied = True
1539 self.filter_data = dict()
1540
1541 def load(self, platform_file):
Anas Nashif45943702020-12-11 17:55:15 -05001542 scp = TwisterConfigParser(platform_file, self.platform_schema)
Anas Nashifce2b4182020-03-24 14:40:28 -04001543 scp.load()
1544 data = scp.data
1545
1546 self.name = data['identifier']
Anas Nashifb18f3112020-12-07 11:40:19 -05001547 self.twister = data.get("twister", True)
Anas Nashifce2b4182020-03-24 14:40:28 -04001548 # if no RAM size is specified by the board, take a default of 128K
1549 self.ram = data.get("ram", 128)
1550 testing = data.get("testing", {})
1551 self.ignore_tags = testing.get("ignore_tags", [])
Anas Nashife8e367a2020-07-16 16:27:04 -04001552 self.only_tags = testing.get("only_tags", [])
Anas Nashifce2b4182020-03-24 14:40:28 -04001553 self.default = testing.get("default", False)
1554 # if no flash size is specified by the board, take a default of 512K
1555 self.flash = data.get("flash", 512)
1556 self.supported = set()
1557 for supp_feature in data.get("supported", []):
1558 for item in supp_feature.split(":"):
1559 self.supported.add(item)
1560
1561 self.arch = data['arch']
1562 self.type = data.get('type', "na")
1563 self.simulation = data.get('simulation', "na")
1564 self.supported_toolchains = data.get("toolchain", [])
1565 self.env = data.get("env", [])
1566 self.env_satisfied = True
1567 for env in self.env:
1568 if not os.environ.get(env, None):
1569 self.env_satisfied = False
1570
1571 def __repr__(self):
1572 return "<%s on %s>" % (self.name, self.arch)
1573
1574
Anas Nashifaff616d2020-04-17 21:24:57 -04001575class DisablePyTestCollectionMixin(object):
1576 __test__ = False
1577
1578
1579class TestCase(DisablePyTestCollectionMixin):
Anas Nashifce2b4182020-03-24 14:40:28 -04001580 """Class representing a test application
1581 """
1582
Anas Nashifaff616d2020-04-17 21:24:57 -04001583 def __init__(self, testcase_root, workdir, name):
Anas Nashifce2b4182020-03-24 14:40:28 -04001584 """TestCase constructor.
1585
1586 This gets called by TestSuite as it finds and reads test yaml files.
1587 Multiple TestCase instances may be generated from a single testcase.yaml,
1588 each one corresponds to an entry within that file.
1589
1590 We need to have a unique name for every single test case. Since
1591 a testcase.yaml can define multiple tests, the canonical name for
1592 the test case is <workdir>/<name>.
1593
1594 @param testcase_root os.path.abspath() of one of the --testcase-root
1595 @param workdir Sub-directory of testcase_root where the
1596 .yaml test configuration file was found
1597 @param name Name of this test case, corresponding to the entry name
1598 in the test case configuration file. For many test cases that just
1599 define one test, can be anything and is usually "test". This is
1600 really only used to distinguish between different cases when
1601 the testcase.yaml defines multiple tests
Anas Nashifce2b4182020-03-24 14:40:28 -04001602 """
1603
Anas Nashifaff616d2020-04-17 21:24:57 -04001604
Anas Nashifce2b4182020-03-24 14:40:28 -04001605 self.source_dir = ""
1606 self.yamlfile = ""
1607 self.cases = []
Anas Nashifaff616d2020-04-17 21:24:57 -04001608 self.name = self.get_unique(testcase_root, workdir, name)
1609 self.id = name
Anas Nashifce2b4182020-03-24 14:40:28 -04001610
1611 self.type = None
Anas Nashifaff616d2020-04-17 21:24:57 -04001612 self.tags = set()
Anas Nashifce2b4182020-03-24 14:40:28 -04001613 self.extra_args = None
1614 self.extra_configs = None
Anas Nashifdca317c2020-08-26 11:28:25 -04001615 self.arch_allow = None
Anas Nashifce2b4182020-03-24 14:40:28 -04001616 self.arch_exclude = None
Anas Nashifaff616d2020-04-17 21:24:57 -04001617 self.skip = False
Anas Nashifce2b4182020-03-24 14:40:28 -04001618 self.platform_exclude = None
Anas Nashifdca317c2020-08-26 11:28:25 -04001619 self.platform_allow = None
Anas Nashifce2b4182020-03-24 14:40:28 -04001620 self.toolchain_exclude = None
Anas Nashifdca317c2020-08-26 11:28:25 -04001621 self.toolchain_allow = None
Anas Nashifce2b4182020-03-24 14:40:28 -04001622 self.tc_filter = None
1623 self.timeout = 60
1624 self.harness = ""
1625 self.harness_config = {}
1626 self.build_only = True
1627 self.build_on_all = False
1628 self.slow = False
Anas Nashifaff616d2020-04-17 21:24:57 -04001629 self.min_ram = -1
Anas Nashifce2b4182020-03-24 14:40:28 -04001630 self.depends_on = None
Anas Nashifaff616d2020-04-17 21:24:57 -04001631 self.min_flash = -1
Anas Nashifce2b4182020-03-24 14:40:28 -04001632 self.extra_sections = None
Anas Nashif1636c312020-05-28 08:02:54 -04001633 self.integration_platforms = []
Anas Nashifce2b4182020-03-24 14:40:28 -04001634
1635 @staticmethod
1636 def get_unique(testcase_root, workdir, name):
1637
1638 canonical_testcase_root = os.path.realpath(testcase_root)
1639 if Path(canonical_zephyr_base) in Path(canonical_testcase_root).parents:
1640 # This is in ZEPHYR_BASE, so include path in name for uniqueness
1641 # FIXME: We should not depend on path of test for unique names.
1642 relative_tc_root = os.path.relpath(canonical_testcase_root,
1643 start=canonical_zephyr_base)
1644 else:
1645 relative_tc_root = ""
1646
1647 # workdir can be "."
1648 unique = os.path.normpath(os.path.join(relative_tc_root, workdir, name))
Anas Nashif7a691252020-05-07 07:47:51 -04001649 check = name.split(".")
1650 if len(check) < 2:
Anas Nashif45943702020-12-11 17:55:15 -05001651 raise TwisterException(f"""bad test name '{name}' in {testcase_root}/{workdir}. \
Anas Nashif7a691252020-05-07 07:47:51 -04001652Tests should reference the category and subsystem with a dot as a separator.
1653 """
1654 )
Anas Nashifce2b4182020-03-24 14:40:28 -04001655 return unique
1656
1657 @staticmethod
1658 def scan_file(inf_name):
1659 suite_regex = re.compile(
1660 # do not match until end-of-line, otherwise we won't allow
1661 # stc_regex below to catch the ones that are declared in the same
1662 # line--as we only search starting the end of this match
1663 br"^\s*ztest_test_suite\(\s*(?P<suite_name>[a-zA-Z0-9_]+)\s*,",
1664 re.MULTILINE)
1665 stc_regex = re.compile(
1666 br"^\s*" # empy space at the beginning is ok
1667 # catch the case where it is declared in the same sentence, e.g:
1668 #
1669 # ztest_test_suite(mutex_complex, ztest_user_unit_test(TESTNAME));
1670 br"(?:ztest_test_suite\([a-zA-Z0-9_]+,\s*)?"
1671 # Catch ztest[_user]_unit_test-[_setup_teardown](TESTNAME)
1672 br"ztest_(?:1cpu_)?(?:user_)?unit_test(?:_setup_teardown)?"
1673 # Consume the argument that becomes the extra testcse
1674 br"\(\s*"
1675 br"(?P<stc_name>[a-zA-Z0-9_]+)"
1676 # _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*\)",
1679 # We don't check how it finishes; we don't care
1680 re.MULTILINE)
1681 suite_run_regex = re.compile(
1682 br"^\s*ztest_run_test_suite\((?P<suite_name>[a-zA-Z0-9_]+)\)",
1683 re.MULTILINE)
1684 achtung_regex = re.compile(
1685 br"(#ifdef|#endif)",
1686 re.MULTILINE)
1687 warnings = None
1688
1689 with open(inf_name) as inf:
1690 if os.name == 'nt':
1691 mmap_args = {'fileno': inf.fileno(), 'length': 0, 'access': mmap.ACCESS_READ}
1692 else:
1693 mmap_args = {'fileno': inf.fileno(), 'length': 0, 'flags': mmap.MAP_PRIVATE, 'prot': mmap.PROT_READ,
1694 'offset': 0}
1695
1696 with contextlib.closing(mmap.mmap(**mmap_args)) as main_c:
Anas Nashifce2b4182020-03-24 14:40:28 -04001697 suite_regex_match = suite_regex.search(main_c)
1698 if not suite_regex_match:
1699 # can't find ztest_test_suite, maybe a client, because
1700 # it includes ztest.h
1701 return None, None
1702
1703 suite_run_match = suite_run_regex.search(main_c)
1704 if not suite_run_match:
1705 raise ValueError("can't find ztest_run_test_suite")
1706
1707 achtung_matches = re.findall(
1708 achtung_regex,
1709 main_c[suite_regex_match.end():suite_run_match.start()])
1710 if achtung_matches:
1711 warnings = "found invalid %s in ztest_test_suite()" \
Spoorthy Priya Yeraboluad4d4fc2020-06-25 02:57:05 -07001712 % ", ".join(sorted({match.decode() for match in achtung_matches},reverse = True))
Anas Nashifce2b4182020-03-24 14:40:28 -04001713 _matches = re.findall(
1714 stc_regex,
1715 main_c[suite_regex_match.end():suite_run_match.start()])
Anas Nashif44f7ba02020-05-12 12:26:41 -04001716 for match in _matches:
1717 if not match.decode().startswith("test_"):
1718 warnings = "Found a test that does not start with test_"
Maciej Perkowski034d4f22020-08-03 14:11:11 +02001719 matches = [match.decode().replace("test_", "", 1) for match in _matches]
Anas Nashifce2b4182020-03-24 14:40:28 -04001720 return matches, warnings
1721
1722 def scan_path(self, path):
1723 subcases = []
Anas Nashif91fd68d2020-05-08 07:22:58 -04001724 for filename in glob.glob(os.path.join(path, "src", "*.c*")):
Anas Nashifce2b4182020-03-24 14:40:28 -04001725 try:
1726 _subcases, warnings = self.scan_file(filename)
1727 if warnings:
1728 logger.error("%s: %s" % (filename, warnings))
Anas Nashif45943702020-12-11 17:55:15 -05001729 raise TwisterRuntimeError("%s: %s" % (filename, warnings))
Anas Nashifce2b4182020-03-24 14:40:28 -04001730 if _subcases:
1731 subcases += _subcases
1732 except ValueError as e:
1733 logger.error("%s: can't find: %s" % (filename, e))
Anas Nashif61c6e2b2020-05-07 07:03:30 -04001734
Anas Nashifce2b4182020-03-24 14:40:28 -04001735 for filename in glob.glob(os.path.join(path, "*.c")):
1736 try:
1737 _subcases, warnings = self.scan_file(filename)
1738 if warnings:
1739 logger.error("%s: %s" % (filename, warnings))
1740 if _subcases:
1741 subcases += _subcases
1742 except ValueError as e:
1743 logger.error("%s: can't find: %s" % (filename, e))
1744 return subcases
1745
1746 def parse_subcases(self, test_path):
1747 results = self.scan_path(test_path)
1748 for sub in results:
1749 name = "{}.{}".format(self.id, sub)
1750 self.cases.append(name)
1751
1752 if not results:
1753 self.cases.append(self.id)
1754
1755 def __str__(self):
1756 return self.name
1757
1758
Anas Nashifaff616d2020-04-17 21:24:57 -04001759class TestInstance(DisablePyTestCollectionMixin):
Anas Nashifce2b4182020-03-24 14:40:28 -04001760 """Class representing the execution of a particular TestCase on a platform
1761
1762 @param test The TestCase object we want to build/execute
1763 @param platform Platform object that we want to build and run against
1764 @param base_outdir Base directory for all test results. The actual
1765 out directory used is <outdir>/<platform>/<test case name>
1766 """
1767
1768 def __init__(self, testcase, platform, outdir):
1769
1770 self.testcase = testcase
1771 self.platform = platform
1772
1773 self.status = None
1774 self.reason = "Unknown"
1775 self.metrics = dict()
1776 self.handler = None
1777 self.outdir = outdir
1778
1779 self.name = os.path.join(platform.name, testcase.name)
1780 self.build_dir = os.path.join(outdir, platform.name, testcase.name)
1781
Anas Nashifce2b4182020-03-24 14:40:28 -04001782 self.run = False
1783
1784 self.results = {}
1785
Anas Nashif531fe892020-09-11 13:56:33 -04001786 def __getstate__(self):
1787 d = self.__dict__.copy()
1788 return d
1789
1790 def __setstate__(self, d):
1791 self.__dict__.update(d)
1792
Anas Nashifce2b4182020-03-24 14:40:28 -04001793 def __lt__(self, other):
1794 return self.name < other.name
1795
Anas Nashif4ca0b952020-07-24 09:22:25 -04001796
Anas Nashif405f1b62020-07-27 12:27:13 -04001797 @staticmethod
1798 def testcase_runnable(testcase, fixtures):
Anas Nashif4ca0b952020-07-24 09:22:25 -04001799 can_run = False
1800 # console harness allows us to run the test and capture data.
YouhuaX Zhu965c8b92020-10-22 09:37:27 +08001801 if testcase.harness in [ 'console', 'ztest', 'pytest']:
Anas Nashif405f1b62020-07-27 12:27:13 -04001802 can_run = True
Anas Nashif4ca0b952020-07-24 09:22:25 -04001803 # if we have a fixture that is also being supplied on the
1804 # command-line, then we need to run the test, not just build it.
1805 fixture = testcase.harness_config.get('fixture')
1806 if fixture:
Anas Nashif405f1b62020-07-27 12:27:13 -04001807 can_run = (fixture in fixtures)
Anas Nashif4ca0b952020-07-24 09:22:25 -04001808
1809 elif testcase.harness:
1810 can_run = False
1811 else:
1812 can_run = True
1813
1814 return can_run
1815
1816
Anas Nashifaff616d2020-04-17 21:24:57 -04001817 # Global testsuite parameters
Anas Nashif405f1b62020-07-27 12:27:13 -04001818 def check_runnable(self, enable_slow=False, filter='buildable', fixtures=[]):
Anas Nashifce2b4182020-03-24 14:40:28 -04001819
1820 # right now we only support building on windows. running is still work
1821 # in progress.
1822 if os.name == 'nt':
Anas Nashif4ca0b952020-07-24 09:22:25 -04001823 return False
Anas Nashifce2b4182020-03-24 14:40:28 -04001824
1825 # we asked for build-only on the command line
Anas Nashif405f1b62020-07-27 12:27:13 -04001826 if self.testcase.build_only:
Anas Nashif4ca0b952020-07-24 09:22:25 -04001827 return False
Anas Nashifce2b4182020-03-24 14:40:28 -04001828
1829 # Do not run slow tests:
1830 skip_slow = self.testcase.slow and not enable_slow
1831 if skip_slow:
Anas Nashif4ca0b952020-07-24 09:22:25 -04001832 return False
Anas Nashifce2b4182020-03-24 14:40:28 -04001833
Anas Nashif4ca0b952020-07-24 09:22:25 -04001834 target_ready = bool(self.testcase.type == "unit" or \
Anas Nashifce2b4182020-03-24 14:40:28 -04001835 self.platform.type == "native" or \
Jaxson Han8af11d42021-02-24 10:23:46 +08001836 self.platform.simulation in ["mdb-nsim", "nsim", "renode", "qemu", "tsim", "armfvp"] or \
Anas Nashif405f1b62020-07-27 12:27:13 -04001837 filter == 'runnable')
Anas Nashifce2b4182020-03-24 14:40:28 -04001838
1839 if self.platform.simulation == "nsim":
1840 if not find_executable("nsimdrv"):
Anas Nashif4ca0b952020-07-24 09:22:25 -04001841 target_ready = False
Anas Nashifce2b4182020-03-24 14:40:28 -04001842
Eugeniy Paltsev91d7ec52020-10-07 22:12:42 +03001843 if self.platform.simulation == "mdb-nsim":
Anas Nashif0f831b12020-09-03 12:33:16 -04001844 if not find_executable("mdb"):
Anas Nashif405f1b62020-07-27 12:27:13 -04001845 target_ready = False
Anas Nashif0f831b12020-09-03 12:33:16 -04001846
Anas Nashifce2b4182020-03-24 14:40:28 -04001847 if self.platform.simulation == "renode":
1848 if not find_executable("renode"):
Anas Nashif4ca0b952020-07-24 09:22:25 -04001849 target_ready = False
Anas Nashifce2b4182020-03-24 14:40:28 -04001850
Martin Åbergc1077142020-10-19 18:21:52 +02001851 if self.platform.simulation == "tsim":
1852 if not find_executable("tsim-leon3"):
1853 target_ready = False
1854
Anas Nashif4ca0b952020-07-24 09:22:25 -04001855 testcase_runnable = self.testcase_runnable(self.testcase, fixtures)
Anas Nashifce2b4182020-03-24 14:40:28 -04001856
Anas Nashif4ca0b952020-07-24 09:22:25 -04001857 return testcase_runnable and target_ready
Anas Nashifce2b4182020-03-24 14:40:28 -04001858
Christian Taedcke3dbe9f22020-07-06 16:00:57 +02001859 def create_overlay(self, platform, enable_asan=False, enable_ubsan=False, enable_coverage=False, coverage_platform=[]):
Anas Nashifb18f3112020-12-07 11:40:19 -05001860 # Create this in a "twister/" subdirectory otherwise this
Anas Nashifce2b4182020-03-24 14:40:28 -04001861 # will pass this overlay to kconfig.py *twice* and kconfig.cmake
1862 # will silently give that second time precedence over any
1863 # --extra-args=CONFIG_*
Anas Nashifb18f3112020-12-07 11:40:19 -05001864 subdir = os.path.join(self.build_dir, "twister")
Anas Nashifce2b4182020-03-24 14:40:28 -04001865
Kumar Gala51d69312020-09-24 13:28:50 -05001866 content = ""
Anas Nashifce2b4182020-03-24 14:40:28 -04001867
Kumar Gala51d69312020-09-24 13:28:50 -05001868 if self.testcase.extra_configs:
1869 content = "\n".join(self.testcase.extra_configs)
Anas Nashifce2b4182020-03-24 14:40:28 -04001870
Kumar Gala51d69312020-09-24 13:28:50 -05001871 if enable_coverage:
1872 if platform.name in coverage_platform:
1873 content = content + "\nCONFIG_COVERAGE=y"
1874 content = content + "\nCONFIG_COVERAGE_DUMP=y"
Anas Nashifce2b4182020-03-24 14:40:28 -04001875
Kumar Gala51d69312020-09-24 13:28:50 -05001876 if enable_asan:
1877 if platform.type == "native":
1878 content = content + "\nCONFIG_ASAN=y"
Anas Nashifce2b4182020-03-24 14:40:28 -04001879
Kumar Gala51d69312020-09-24 13:28:50 -05001880 if enable_ubsan:
1881 if platform.type == "native":
1882 content = content + "\nCONFIG_UBSAN=y"
Christian Taedcke3dbe9f22020-07-06 16:00:57 +02001883
Kumar Gala51d69312020-09-24 13:28:50 -05001884 if content:
1885 os.makedirs(subdir, exist_ok=True)
1886 file = os.path.join(subdir, "testcase_extra.conf")
1887 with open(file, "w") as f:
1888 f.write(content)
1889
1890 return content
Anas Nashifce2b4182020-03-24 14:40:28 -04001891
1892 def calculate_sizes(self):
1893 """Get the RAM/ROM sizes of a test case.
1894
1895 This can only be run after the instance has been executed by
1896 MakeGenerator, otherwise there won't be any binaries to measure.
1897
1898 @return A SizeCalculator object
1899 """
1900 fns = glob.glob(os.path.join(self.build_dir, "zephyr", "*.elf"))
1901 fns.extend(glob.glob(os.path.join(self.build_dir, "zephyr", "*.exe")))
1902 fns = [x for x in fns if not x.endswith('_prebuilt.elf')]
1903 if len(fns) != 1:
1904 raise BuildError("Missing/multiple output ELF binary")
1905
1906 return SizeCalculator(fns[0], self.testcase.extra_sections)
1907
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02001908 def fill_results_by_status(self):
1909 """Fills results according to self.status
1910
1911 The method is used to propagate the instance level status
1912 to the test cases inside. Useful when the whole instance is skipped
1913 and the info is required also at the test cases level for reporting.
1914 Should be used with caution, e.g. should not be used
1915 to fill all results with passes
1916 """
1917 status_to_verdict = {
1918 'skipped': 'SKIP',
1919 'error': 'BLOCK',
1920 'failure': 'FAILED'
1921 }
1922
1923 for k in self.results:
1924 self.results[k] = status_to_verdict[self.status]
1925
Anas Nashifce2b4182020-03-24 14:40:28 -04001926 def __repr__(self):
1927 return "<TestCase %s on %s>" % (self.testcase.name, self.platform.name)
1928
1929
1930class CMake():
1931 config_re = re.compile('(CONFIG_[A-Za-z0-9_]+)[=]\"?([^\"]*)\"?$')
1932 dt_re = re.compile('([A-Za-z0-9_]+)[=]\"?([^\"]*)\"?$')
1933
1934 def __init__(self, testcase, platform, source_dir, build_dir):
1935
1936 self.cwd = None
1937 self.capture_output = True
1938
1939 self.defconfig = {}
1940 self.cmake_cache = {}
1941
1942 self.instance = None
1943 self.testcase = testcase
1944 self.platform = platform
1945 self.source_dir = source_dir
1946 self.build_dir = build_dir
1947 self.log = "build.log"
1948 self.generator = None
1949 self.generator_cmd = None
1950
1951 def parse_generated(self):
1952 self.defconfig = {}
1953 return {}
1954
1955 def run_build(self, args=[]):
1956
1957 logger.debug("Building %s for %s" % (self.source_dir, self.platform.name))
1958
1959 cmake_args = []
1960 cmake_args.extend(args)
1961 cmake = shutil.which('cmake')
1962 cmd = [cmake] + cmake_args
1963 kwargs = dict()
1964
1965 if self.capture_output:
1966 kwargs['stdout'] = subprocess.PIPE
1967 # CMake sends the output of message() to stderr unless it's STATUS
1968 kwargs['stderr'] = subprocess.STDOUT
1969
1970 if self.cwd:
1971 kwargs['cwd'] = self.cwd
1972
1973 p = subprocess.Popen(cmd, **kwargs)
1974 out, _ = p.communicate()
1975
1976 results = {}
1977 if p.returncode == 0:
1978 msg = "Finished building %s for %s" % (self.source_dir, self.platform.name)
1979
1980 self.instance.status = "passed"
1981 results = {'msg': msg, "returncode": p.returncode, "instance": self.instance}
1982
1983 if out:
1984 log_msg = out.decode(sys.getdefaultencoding())
1985 with open(os.path.join(self.build_dir, self.log), "a") as log:
1986 log.write(log_msg)
1987
1988 else:
1989 return None
1990 else:
1991 # A real error occurred, raise an exception
Anas Nashif5a6e64f2020-10-30 13:01:50 -04001992 log_msg = ""
Anas Nashifce2b4182020-03-24 14:40:28 -04001993 if out:
1994 log_msg = out.decode(sys.getdefaultencoding())
1995 with open(os.path.join(self.build_dir, self.log), "a") as log:
1996 log.write(log_msg)
1997
1998 if log_msg:
Henrik Brix Andersen9aa4a702021-09-27 21:21:03 +02001999 res = re.findall("region `(FLASH|ROM|RAM|ICCM|DCCM|SRAM)' overflowed by", log_msg)
Anas Nashiff68146f2020-11-28 11:07:06 -05002000 if res and not self.overflow_as_errors:
Anas Nashifce2b4182020-03-24 14:40:28 -04002001 logger.debug("Test skipped due to {} Overflow".format(res[0]))
2002 self.instance.status = "skipped"
2003 self.instance.reason = "{} overflow".format(res[0])
2004 else:
Anas Nashiff04461e2020-06-29 10:07:02 -04002005 self.instance.status = "error"
Anas Nashifce2b4182020-03-24 14:40:28 -04002006 self.instance.reason = "Build failure"
2007
2008 results = {
2009 "returncode": p.returncode,
2010 "instance": self.instance,
2011 }
2012
2013 return results
2014
2015 def run_cmake(self, args=[]):
2016
Anas Nashif50925412020-07-16 17:25:19 -04002017 if self.warnings_as_errors:
2018 ldflags = "-Wl,--fatal-warnings"
2019 cflags = "-Werror"
2020 aflags = "-Wa,--fatal-warnings"
Martí Bolívarc4079e42021-07-30 15:43:27 -07002021 gen_defines_args = "--edtlib-Werror"
Anas Nashif50925412020-07-16 17:25:19 -04002022 else:
2023 ldflags = cflags = aflags = ""
Martí Bolívarfbd34dc2021-02-14 18:02:12 -08002024 gen_defines_args = ""
Anas Nashifce2b4182020-03-24 14:40:28 -04002025
Anas Nashif50925412020-07-16 17:25:19 -04002026 logger.debug("Running cmake on %s for %s" % (self.source_dir, self.platform.name))
Anas Nashifce2b4182020-03-24 14:40:28 -04002027 cmake_args = [
Anas Nashif50925412020-07-16 17:25:19 -04002028 f'-B{self.build_dir}',
2029 f'-S{self.source_dir}',
2030 f'-DEXTRA_CFLAGS="{cflags}"',
2031 f'-DEXTRA_AFLAGS="{aflags}',
2032 f'-DEXTRA_LDFLAGS="{ldflags}"',
Martí Bolívarfbd34dc2021-02-14 18:02:12 -08002033 f'-DEXTRA_GEN_DEFINES_ARGS={gen_defines_args}',
Anas Nashif50925412020-07-16 17:25:19 -04002034 f'-G{self.generator}'
Anas Nashifce2b4182020-03-24 14:40:28 -04002035 ]
2036
2037 if self.cmake_only:
2038 cmake_args.append("-DCMAKE_EXPORT_COMPILE_COMMANDS=1")
2039
2040 args = ["-D{}".format(a.replace('"', '')) for a in args]
2041 cmake_args.extend(args)
2042
2043 cmake_opts = ['-DBOARD={}'.format(self.platform.name)]
2044 cmake_args.extend(cmake_opts)
2045
2046
2047 logger.debug("Calling cmake with arguments: {}".format(cmake_args))
2048 cmake = shutil.which('cmake')
2049 cmd = [cmake] + cmake_args
2050 kwargs = dict()
2051
2052 if self.capture_output:
2053 kwargs['stdout'] = subprocess.PIPE
2054 # CMake sends the output of message() to stderr unless it's STATUS
2055 kwargs['stderr'] = subprocess.STDOUT
2056
2057 if self.cwd:
2058 kwargs['cwd'] = self.cwd
2059
2060 p = subprocess.Popen(cmd, **kwargs)
2061 out, _ = p.communicate()
2062
2063 if p.returncode == 0:
2064 filter_results = self.parse_generated()
2065 msg = "Finished building %s for %s" % (self.source_dir, self.platform.name)
2066 logger.debug(msg)
2067 results = {'msg': msg, 'filter': filter_results}
2068
2069 else:
Anas Nashiff04461e2020-06-29 10:07:02 -04002070 self.instance.status = "error"
Anas Nashifce2b4182020-03-24 14:40:28 -04002071 self.instance.reason = "Cmake build failure"
Piotr Golyzniak35a05202021-07-23 11:17:11 +02002072 self.instance.fill_results_by_status()
Anas Nashifce2b4182020-03-24 14:40:28 -04002073 logger.error("Cmake build failure: %s for %s" % (self.source_dir, self.platform.name))
2074 results = {"returncode": p.returncode}
2075
2076 if out:
2077 with open(os.path.join(self.build_dir, self.log), "a") as log:
2078 log_msg = out.decode(sys.getdefaultencoding())
2079 log.write(log_msg)
2080
2081 return results
2082
Torsten Rasmussend162e9e2021-02-04 15:55:23 +01002083 @staticmethod
2084 def run_cmake_script(args=[]):
2085
2086 logger.debug("Running cmake script %s" % (args[0]))
2087
2088 cmake_args = ["-D{}".format(a.replace('"', '')) for a in args[1:]]
2089 cmake_args.extend(['-P', args[0]])
2090
2091 logger.debug("Calling cmake with arguments: {}".format(cmake_args))
2092 cmake = shutil.which('cmake')
Steven Huang1413c552021-03-14 01:54:40 -05002093 if not cmake:
2094 msg = "Unable to find `cmake` in path"
2095 logger.error(msg)
2096 raise Exception(msg)
Torsten Rasmussend162e9e2021-02-04 15:55:23 +01002097 cmd = [cmake] + cmake_args
2098
2099 kwargs = dict()
2100 kwargs['stdout'] = subprocess.PIPE
2101 # CMake sends the output of message() to stderr unless it's STATUS
2102 kwargs['stderr'] = subprocess.STDOUT
2103
2104 p = subprocess.Popen(cmd, **kwargs)
2105 out, _ = p.communicate()
2106
Benedikt Schmidtb83681d2021-08-23 14:35:55 +02002107 # It might happen that the environment adds ANSI escape codes like \x1b[0m,
2108 # for instance if twister is executed from inside a makefile. In such a
2109 # scenario it is then necessary to remove them, as otherwise the JSON decoding
2110 # will fail.
2111 ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
2112 out = ansi_escape.sub('', out.decode())
2113
Torsten Rasmussend162e9e2021-02-04 15:55:23 +01002114 if p.returncode == 0:
2115 msg = "Finished running %s" % (args[0])
2116 logger.debug(msg)
2117 results = {"returncode": p.returncode, "msg": msg, "stdout": out}
2118
2119 else:
2120 logger.error("Cmake script failure: %s" % (args[0]))
Torsten Rasmussenaf875992021-09-28 13:04:52 +02002121 results = {"returncode": p.returncode, "returnmsg": out}
Torsten Rasmussend162e9e2021-02-04 15:55:23 +01002122
2123 return results
2124
Anas Nashifce2b4182020-03-24 14:40:28 -04002125
2126class FilterBuilder(CMake):
2127
2128 def __init__(self, testcase, platform, source_dir, build_dir):
2129 super().__init__(testcase, platform, source_dir, build_dir)
2130
Anas Nashifb18f3112020-12-07 11:40:19 -05002131 self.log = "config-twister.log"
Anas Nashifce2b4182020-03-24 14:40:28 -04002132
2133 def parse_generated(self):
2134
2135 if self.platform.name == "unit_testing":
2136 return {}
2137
2138 cmake_cache_path = os.path.join(self.build_dir, "CMakeCache.txt")
2139 defconfig_path = os.path.join(self.build_dir, "zephyr", ".config")
2140
2141 with open(defconfig_path, "r") as fp:
2142 defconfig = {}
2143 for line in fp.readlines():
2144 m = self.config_re.match(line)
2145 if not m:
2146 if line.strip() and not line.startswith("#"):
2147 sys.stderr.write("Unrecognized line %s\n" % line)
2148 continue
2149 defconfig[m.group(1)] = m.group(2).strip()
2150
2151 self.defconfig = defconfig
2152
2153 cmake_conf = {}
2154 try:
2155 cache = CMakeCache.from_file(cmake_cache_path)
2156 except FileNotFoundError:
2157 cache = {}
2158
2159 for k in iter(cache):
2160 cmake_conf[k.name] = k.value
2161
2162 self.cmake_cache = cmake_conf
2163
2164 filter_data = {
2165 "ARCH": self.platform.arch,
2166 "PLATFORM": self.platform.name
2167 }
2168 filter_data.update(os.environ)
2169 filter_data.update(self.defconfig)
2170 filter_data.update(self.cmake_cache)
2171
Martí Bolívar9c92baa2020-07-08 14:43:07 -07002172 edt_pickle = os.path.join(self.build_dir, "zephyr", "edt.pickle")
Anas Nashifce2b4182020-03-24 14:40:28 -04002173 if self.testcase and self.testcase.tc_filter:
2174 try:
Martí Bolívar9c92baa2020-07-08 14:43:07 -07002175 if os.path.exists(edt_pickle):
2176 with open(edt_pickle, 'rb') as f:
2177 edt = pickle.load(f)
Anas Nashifce2b4182020-03-24 14:40:28 -04002178 else:
2179 edt = None
2180 res = expr_parser.parse(self.testcase.tc_filter, filter_data, edt)
2181
2182 except (ValueError, SyntaxError) as se:
2183 sys.stderr.write(
2184 "Failed processing %s\n" % self.testcase.yamlfile)
2185 raise se
2186
2187 if not res:
2188 return {os.path.join(self.platform.name, self.testcase.name): True}
2189 else:
2190 return {os.path.join(self.platform.name, self.testcase.name): False}
2191 else:
2192 self.platform.filter_data = filter_data
2193 return filter_data
2194
2195
2196class ProjectBuilder(FilterBuilder):
2197
2198 def __init__(self, suite, instance, **kwargs):
2199 super().__init__(instance.testcase, instance.platform, instance.testcase.source_dir, instance.build_dir)
2200
2201 self.log = "build.log"
2202 self.instance = instance
2203 self.suite = suite
Maciej Perkowskic67a0cd2020-08-13 15:20:13 +02002204 self.filtered_tests = 0
Anas Nashifce2b4182020-03-24 14:40:28 -04002205
2206 self.lsan = kwargs.get('lsan', False)
2207 self.asan = kwargs.get('asan', False)
Christian Taedcke3dbe9f22020-07-06 16:00:57 +02002208 self.ubsan = kwargs.get('ubsan', False)
Anas Nashifce2b4182020-03-24 14:40:28 -04002209 self.valgrind = kwargs.get('valgrind', False)
2210 self.extra_args = kwargs.get('extra_args', [])
2211 self.device_testing = kwargs.get('device_testing', False)
2212 self.cmake_only = kwargs.get('cmake_only', False)
2213 self.cleanup = kwargs.get('cleanup', False)
2214 self.coverage = kwargs.get('coverage', False)
2215 self.inline_logs = kwargs.get('inline_logs', False)
Anas Nashifce2b4182020-03-24 14:40:28 -04002216 self.generator = kwargs.get('generator', None)
2217 self.generator_cmd = kwargs.get('generator_cmd', None)
Anas Nashiff6462a32020-03-29 19:02:51 -04002218 self.verbose = kwargs.get('verbose', None)
Anas Nashif50925412020-07-16 17:25:19 -04002219 self.warnings_as_errors = kwargs.get('warnings_as_errors', True)
Anas Nashiff68146f2020-11-28 11:07:06 -05002220 self.overflow_as_errors = kwargs.get('overflow_as_errors', False)
Anas Nashifce2b4182020-03-24 14:40:28 -04002221
2222 @staticmethod
2223 def log_info(filename, inline_logs):
2224 filename = os.path.abspath(os.path.realpath(filename))
2225 if inline_logs:
2226 logger.info("{:-^100}".format(filename))
2227
2228 try:
2229 with open(filename) as fp:
2230 data = fp.read()
2231 except Exception as e:
2232 data = "Unable to read log data (%s)\n" % (str(e))
2233
2234 logger.error(data)
2235
2236 logger.info("{:-^100}".format(filename))
2237 else:
2238 logger.error("see: " + Fore.YELLOW + filename + Fore.RESET)
2239
2240 def log_info_file(self, inline_logs):
2241 build_dir = self.instance.build_dir
2242 h_log = "{}/handler.log".format(build_dir)
2243 b_log = "{}/build.log".format(build_dir)
2244 v_log = "{}/valgrind.log".format(build_dir)
2245 d_log = "{}/device.log".format(build_dir)
2246
2247 if os.path.exists(v_log) and "Valgrind" in self.instance.reason:
2248 self.log_info("{}".format(v_log), inline_logs)
2249 elif os.path.exists(h_log) and os.path.getsize(h_log) > 0:
2250 self.log_info("{}".format(h_log), inline_logs)
2251 elif os.path.exists(d_log) and os.path.getsize(d_log) > 0:
2252 self.log_info("{}".format(d_log), inline_logs)
2253 else:
2254 self.log_info("{}".format(b_log), inline_logs)
2255
2256 def setup_handler(self):
2257
2258 instance = self.instance
2259 args = []
2260
2261 # FIXME: Needs simplification
2262 if instance.platform.simulation == "qemu":
2263 instance.handler = QEMUHandler(instance, "qemu")
2264 args.append("QEMU_PIPE=%s" % instance.handler.get_fifo())
2265 instance.handler.call_make_run = True
2266 elif instance.testcase.type == "unit":
2267 instance.handler = BinaryHandler(instance, "unit")
2268 instance.handler.binary = os.path.join(instance.build_dir, "testbinary")
Anas Nashif051602f2020-04-28 14:27:46 -04002269 if self.coverage:
2270 args.append("COVERAGE=1")
Anas Nashifce2b4182020-03-24 14:40:28 -04002271 elif instance.platform.type == "native":
2272 handler = BinaryHandler(instance, "native")
2273
2274 handler.asan = self.asan
2275 handler.valgrind = self.valgrind
2276 handler.lsan = self.lsan
Christian Taedcke3dbe9f22020-07-06 16:00:57 +02002277 handler.ubsan = self.ubsan
Anas Nashifce2b4182020-03-24 14:40:28 -04002278 handler.coverage = self.coverage
2279
2280 handler.binary = os.path.join(instance.build_dir, "zephyr", "zephyr.exe")
2281 instance.handler = handler
Anas Nashifce2b4182020-03-24 14:40:28 -04002282 elif instance.platform.simulation == "renode":
2283 if find_executable("renode"):
2284 instance.handler = BinaryHandler(instance, "renode")
2285 instance.handler.pid_fn = os.path.join(instance.build_dir, "renode.pid")
2286 instance.handler.call_make_run = True
Martin Åbergc1077142020-10-19 18:21:52 +02002287 elif instance.platform.simulation == "tsim":
2288 instance.handler = BinaryHandler(instance, "tsim")
2289 instance.handler.call_make_run = True
Anas Nashifce2b4182020-03-24 14:40:28 -04002290 elif self.device_testing:
2291 instance.handler = DeviceHandler(instance, "device")
Andrei Emeltchenko10fa48d2021-01-12 09:56:32 +02002292 instance.handler.coverage = self.coverage
Eugeniy Paltsev9f2006c2020-10-08 22:31:19 +03002293 elif instance.platform.simulation == "nsim":
2294 if find_executable("nsimdrv"):
2295 instance.handler = BinaryHandler(instance, "nsim")
2296 instance.handler.call_make_run = True
Eugeniy Paltsev16032b672020-10-08 22:42:02 +03002297 elif instance.platform.simulation == "mdb-nsim":
2298 if find_executable("mdb"):
2299 instance.handler = BinaryHandler(instance, "nsim")
Jingru Wangb78d6742021-09-01 09:53:57 +08002300 instance.handler.call_make_run = True
Jaxson Han8af11d42021-02-24 10:23:46 +08002301 elif instance.platform.simulation == "armfvp":
2302 instance.handler = BinaryHandler(instance, "armfvp")
2303 instance.handler.call_make_run = True
Anas Nashifce2b4182020-03-24 14:40:28 -04002304
2305 if instance.handler:
2306 instance.handler.args = args
Anas Nashifb3669492020-03-24 22:33:50 -04002307 instance.handler.generator_cmd = self.generator_cmd
2308 instance.handler.generator = self.generator
Anas Nashifce2b4182020-03-24 14:40:28 -04002309
Anas Nashif531fe892020-09-11 13:56:33 -04002310 def process(self, pipeline, done, message, lock, results):
Anas Nashifce2b4182020-03-24 14:40:28 -04002311 op = message.get('op')
2312
2313 if not self.instance.handler:
2314 self.setup_handler()
2315
2316 # The build process, call cmake and build with configured generator
2317 if op == "cmake":
Anas Nashif531fe892020-09-11 13:56:33 -04002318 res = self.cmake()
Anas Nashiff04461e2020-06-29 10:07:02 -04002319 if self.instance.status in ["failed", "error"]:
Anas Nashifce2b4182020-03-24 14:40:28 -04002320 pipeline.put({"op": "report", "test": self.instance})
2321 elif self.cmake_only:
Kumar Gala659b24b2020-10-09 09:51:02 -05002322 if self.instance.status is None:
2323 self.instance.status = "passed"
Anas Nashifce2b4182020-03-24 14:40:28 -04002324 pipeline.put({"op": "report", "test": self.instance})
2325 else:
Anas Nashif531fe892020-09-11 13:56:33 -04002326 if self.instance.name in res['filter'] and res['filter'][self.instance.name]:
Anas Nashifce2b4182020-03-24 14:40:28 -04002327 logger.debug("filtering %s" % self.instance.name)
2328 self.instance.status = "skipped"
2329 self.instance.reason = "filter"
Anas Nashif3b939da2020-11-24 13:21:27 -05002330 results.skipped_runtime += 1
Maciej Perkowskib2fa99c2020-05-21 14:45:29 +02002331 for case in self.instance.testcase.cases:
2332 self.instance.results.update({case: 'SKIP'})
Anas Nashifce2b4182020-03-24 14:40:28 -04002333 pipeline.put({"op": "report", "test": self.instance})
2334 else:
2335 pipeline.put({"op": "build", "test": self.instance})
2336
2337 elif op == "build":
2338 logger.debug("build test: %s" % self.instance.name)
Anas Nashif531fe892020-09-11 13:56:33 -04002339 res = self.build()
Anas Nashifce2b4182020-03-24 14:40:28 -04002340
Anas Nashif531fe892020-09-11 13:56:33 -04002341 if not res:
Anas Nashiff04461e2020-06-29 10:07:02 -04002342 self.instance.status = "error"
Anas Nashifce2b4182020-03-24 14:40:28 -04002343 self.instance.reason = "Build Failure"
2344 pipeline.put({"op": "report", "test": self.instance})
2345 else:
Anas Nashif531fe892020-09-11 13:56:33 -04002346 # Count skipped cases during build, for example
2347 # due to ram/rom overflow.
2348 inst = res.get("instance", None)
2349 if inst and inst.status == "skipped":
Anas Nashif3b939da2020-11-24 13:21:27 -05002350 results.skipped_runtime += 1
Anas Nashif531fe892020-09-11 13:56:33 -04002351
2352 if res.get('returncode', 1) > 0:
Anas Nashifce2b4182020-03-24 14:40:28 -04002353 pipeline.put({"op": "report", "test": self.instance})
2354 else:
Anas Nashif405f1b62020-07-27 12:27:13 -04002355 if self.instance.run and self.instance.handler:
Anas Nashifce2b4182020-03-24 14:40:28 -04002356 pipeline.put({"op": "run", "test": self.instance})
2357 else:
2358 pipeline.put({"op": "report", "test": self.instance})
2359 # Run the generated binary using one of the supported handlers
2360 elif op == "run":
2361 logger.debug("run test: %s" % self.instance.name)
2362 self.run()
2363 self.instance.status, _ = self.instance.handler.get_state()
Anas Nashif531fe892020-09-11 13:56:33 -04002364 logger.debug(f"run status: {self.instance.name} {self.instance.status}")
2365
2366 # to make it work with pickle
2367 self.instance.handler.thread = None
2368 self.instance.handler.suite = None
Anas Nashifce2b4182020-03-24 14:40:28 -04002369 pipeline.put({
2370 "op": "report",
2371 "test": self.instance,
Anas Nashifce2b4182020-03-24 14:40:28 -04002372 "status": self.instance.status,
Anas Nashif531fe892020-09-11 13:56:33 -04002373 "reason": self.instance.reason
2374 }
Anas Nashifce2b4182020-03-24 14:40:28 -04002375 )
2376
2377 # Report results and output progress to screen
2378 elif op == "report":
Anas Nashif531fe892020-09-11 13:56:33 -04002379 with lock:
2380 done.put(self.instance)
2381 self.report_out(results)
Anas Nashifce2b4182020-03-24 14:40:28 -04002382
2383 if self.cleanup and not self.coverage and self.instance.status == "passed":
2384 pipeline.put({
2385 "op": "cleanup",
2386 "test": self.instance
2387 })
2388
2389 elif op == "cleanup":
Kumar Gala1285c1f2020-09-24 13:21:07 -05002390 if self.device_testing:
2391 self.cleanup_device_testing_artifacts()
2392 else:
2393 self.cleanup_artifacts()
Anas Nashifce2b4182020-03-24 14:40:28 -04002394
Kumar Galaeb2e89a2020-10-20 15:44:15 -05002395 def cleanup_artifacts(self, additional_keep=[]):
Anas Nashifce2b4182020-03-24 14:40:28 -04002396 logger.debug("Cleaning up {}".format(self.instance.build_dir))
Anas Nashifdca317c2020-08-26 11:28:25 -04002397 allow = [
Anas Nashifce2b4182020-03-24 14:40:28 -04002398 'zephyr/.config',
2399 'handler.log',
2400 'build.log',
2401 'device.log',
Anas Nashif9ace63e2020-04-28 07:14:43 -04002402 'recording.csv',
Anas Nashifce2b4182020-03-24 14:40:28 -04002403 ]
Kumar Gala1285c1f2020-09-24 13:21:07 -05002404
2405 allow += additional_keep
2406
Anas Nashifdca317c2020-08-26 11:28:25 -04002407 allow = [os.path.join(self.instance.build_dir, file) for file in allow]
Anas Nashifce2b4182020-03-24 14:40:28 -04002408
2409 for dirpath, dirnames, filenames in os.walk(self.instance.build_dir, topdown=False):
2410 for name in filenames:
2411 path = os.path.join(dirpath, name)
Anas Nashifdca317c2020-08-26 11:28:25 -04002412 if path not in allow:
Anas Nashifce2b4182020-03-24 14:40:28 -04002413 os.remove(path)
2414 # Remove empty directories and symbolic links to directories
2415 for dir in dirnames:
2416 path = os.path.join(dirpath, dir)
2417 if os.path.islink(path):
2418 os.remove(path)
2419 elif not os.listdir(path):
2420 os.rmdir(path)
2421
Kumar Gala1285c1f2020-09-24 13:21:07 -05002422 def cleanup_device_testing_artifacts(self):
2423 logger.debug("Cleaning up for Device Testing {}".format(self.instance.build_dir))
2424
Kumar Galab2c07e42020-10-01 07:25:50 -05002425 sanitizelist = [
Kumar Gala1285c1f2020-09-24 13:21:07 -05002426 'CMakeCache.txt',
2427 'zephyr/runners.yaml',
Kumar Galab2c07e42020-10-01 07:25:50 -05002428 ]
2429 keep = [
Kumar Gala1285c1f2020-09-24 13:21:07 -05002430 'zephyr/zephyr.hex',
2431 'zephyr/zephyr.bin',
2432 'zephyr/zephyr.elf',
2433 ]
2434
Kumar Galab2c07e42020-10-01 07:25:50 -05002435 keep += sanitizelist
2436
Kumar Gala1285c1f2020-09-24 13:21:07 -05002437 self.cleanup_artifacts(keep)
2438
Kumar Galab2c07e42020-10-01 07:25:50 -05002439 # sanitize paths so files are relocatable
2440 for file in sanitizelist:
2441 file = os.path.join(self.instance.build_dir, file)
2442
2443 with open(file, "rt") as fin:
2444 data = fin.read()
2445 data = data.replace(canonical_zephyr_base+"/", "")
2446
2447 with open(file, "wt") as fin:
2448 fin.write(data)
2449
Anas Nashif531fe892020-09-11 13:56:33 -04002450 def report_out(self, results):
Anas Nashif3b939da2020-11-24 13:21:27 -05002451 total_to_do = results.total - results.skipped_configs
Anas Nashif531fe892020-09-11 13:56:33 -04002452 total_tests_width = len(str(total_to_do))
2453 results.done += 1
Anas Nashifce2b4182020-03-24 14:40:28 -04002454 instance = self.instance
2455
Maciej Perkowskif050a992021-02-24 13:43:05 +01002456 if instance.status in ["error", "failed", "timeout", "flash_error"]:
Anas Nashifdc43c292020-07-09 09:46:45 -04002457 if instance.status == "error":
Anas Nashif531fe892020-09-11 13:56:33 -04002458 results.error += 1
2459 results.failed += 1
Anas Nashiff6462a32020-03-29 19:02:51 -04002460 if self.verbose:
Anas Nashifce2b4182020-03-24 14:40:28 -04002461 status = Fore.RED + "FAILED " + Fore.RESET + instance.reason
2462 else:
2463 print("")
2464 logger.error(
2465 "{:<25} {:<50} {}FAILED{}: {}".format(
2466 instance.platform.name,
2467 instance.testcase.name,
2468 Fore.RED,
2469 Fore.RESET,
2470 instance.reason))
Anas Nashiff6462a32020-03-29 19:02:51 -04002471 if not self.verbose:
Anas Nashifce2b4182020-03-24 14:40:28 -04002472 self.log_info_file(self.inline_logs)
2473 elif instance.status == "skipped":
Anas Nashifce2b4182020-03-24 14:40:28 -04002474 status = Fore.YELLOW + "SKIPPED" + Fore.RESET
Anas Nashif869ca052020-07-07 14:29:07 -04002475 elif instance.status == "passed":
Anas Nashifce2b4182020-03-24 14:40:28 -04002476 status = Fore.GREEN + "PASSED" + Fore.RESET
Anas Nashif869ca052020-07-07 14:29:07 -04002477 else:
2478 logger.debug(f"Unknown status = {instance.status}")
2479 status = Fore.YELLOW + "UNKNOWN" + Fore.RESET
Anas Nashifce2b4182020-03-24 14:40:28 -04002480
Anas Nashiff6462a32020-03-29 19:02:51 -04002481 if self.verbose:
Anas Nashifce2b4182020-03-24 14:40:28 -04002482 if self.cmake_only:
2483 more_info = "cmake"
2484 elif instance.status == "skipped":
2485 more_info = instance.reason
2486 else:
2487 if instance.handler and instance.run:
2488 more_info = instance.handler.type_str
2489 htime = instance.handler.duration
2490 if htime:
2491 more_info += " {:.3f}s".format(htime)
2492 else:
2493 more_info = "build"
2494
2495 logger.info("{:>{}}/{} {:<25} {:<50} {} ({})".format(
Anas Nashif531fe892020-09-11 13:56:33 -04002496 results.done, total_tests_width, total_to_do, instance.platform.name,
Anas Nashifce2b4182020-03-24 14:40:28 -04002497 instance.testcase.name, status, more_info))
2498
Anas Nashiff04461e2020-06-29 10:07:02 -04002499 if instance.status in ["error", "failed", "timeout"]:
Anas Nashifce2b4182020-03-24 14:40:28 -04002500 self.log_info_file(self.inline_logs)
2501 else:
Maciej Perkowski2ec5ec22020-09-28 12:37:55 +02002502 completed_perc = 0
Anas Nashif531fe892020-09-11 13:56:33 -04002503 if total_to_do > 0:
2504 completed_perc = int((float(results.done) / total_to_do) * 100)
Maciej Perkowski2ec5ec22020-09-28 12:37:55 +02002505
Anas Nashif3b939da2020-11-24 13:21:27 -05002506 skipped = results.skipped_configs + results.skipped_runtime
Anas Nashifce2b4182020-03-24 14:40:28 -04002507 sys.stdout.write("\rINFO - Total complete: %s%4d/%4d%s %2d%% skipped: %s%4d%s, failed: %s%4d%s" % (
2508 Fore.GREEN,
Anas Nashif531fe892020-09-11 13:56:33 -04002509 results.done,
2510 total_to_do,
Anas Nashifce2b4182020-03-24 14:40:28 -04002511 Fore.RESET,
Maciej Perkowski2ec5ec22020-09-28 12:37:55 +02002512 completed_perc,
Anas Nashif531fe892020-09-11 13:56:33 -04002513 Fore.YELLOW if skipped > 0 else Fore.RESET,
2514 skipped,
Anas Nashifce2b4182020-03-24 14:40:28 -04002515 Fore.RESET,
Anas Nashif531fe892020-09-11 13:56:33 -04002516 Fore.RED if results.failed > 0 else Fore.RESET,
2517 results.failed,
Anas Nashifce2b4182020-03-24 14:40:28 -04002518 Fore.RESET
2519 )
2520 )
2521 sys.stdout.flush()
2522
2523 def cmake(self):
2524
2525 instance = self.instance
2526 args = self.testcase.extra_args[:]
2527 args += self.extra_args
2528
2529 if instance.handler:
2530 args += instance.handler.args
2531
2532 # merge overlay files into one variable
2533 def extract_overlays(args):
2534 re_overlay = re.compile('OVERLAY_CONFIG=(.*)')
2535 other_args = []
2536 overlays = []
2537 for arg in args:
2538 match = re_overlay.search(arg)
2539 if match:
2540 overlays.append(match.group(1).strip('\'"'))
2541 else:
2542 other_args.append(arg)
2543
2544 args[:] = other_args
2545 return overlays
2546
2547 overlays = extract_overlays(args)
2548
Torsten Rasmussenca08cc02020-11-25 21:56:46 +01002549 if os.path.exists(os.path.join(instance.build_dir,
Anas Nashifb18f3112020-12-07 11:40:19 -05002550 "twister", "testcase_extra.conf")):
Anas Nashifce2b4182020-03-24 14:40:28 -04002551 overlays.append(os.path.join(instance.build_dir,
Anas Nashifb18f3112020-12-07 11:40:19 -05002552 "twister", "testcase_extra.conf"))
Anas Nashifce2b4182020-03-24 14:40:28 -04002553
2554 if overlays:
2555 args.append("OVERLAY_CONFIG=\"%s\"" % (" ".join(overlays)))
2556
Anas Nashif531fe892020-09-11 13:56:33 -04002557 res = self.run_cmake(args)
2558 return res
Anas Nashifce2b4182020-03-24 14:40:28 -04002559
2560 def build(self):
Anas Nashif531fe892020-09-11 13:56:33 -04002561 res = self.run_build(['--build', self.build_dir])
2562 return res
Anas Nashifce2b4182020-03-24 14:40:28 -04002563
2564 def run(self):
2565
2566 instance = self.instance
2567
Anas Nashif405f1b62020-07-27 12:27:13 -04002568 if instance.handler:
2569 if instance.handler.type_str == "device":
2570 instance.handler.suite = self.suite
Anas Nashifce2b4182020-03-24 14:40:28 -04002571
Anas Nashif405f1b62020-07-27 12:27:13 -04002572 instance.handler.handle()
Anas Nashifce2b4182020-03-24 14:40:28 -04002573
2574 sys.stdout.flush()
2575
Anas Nashifaff616d2020-04-17 21:24:57 -04002576class TestSuite(DisablePyTestCollectionMixin):
Anas Nashifce2b4182020-03-24 14:40:28 -04002577 config_re = re.compile('(CONFIG_[A-Za-z0-9_]+)[=]\"?([^\"]*)\"?$')
2578 dt_re = re.compile('([A-Za-z0-9_]+)[=]\"?([^\"]*)\"?$')
2579
2580 tc_schema = scl.yaml_load(
2581 os.path.join(ZEPHYR_BASE,
Anas Nashifc24bf6f2020-12-07 11:18:07 -05002582 "scripts", "schemas", "twister", "testcase-schema.yaml"))
Maciej Perkowski9c6dfce2021-03-11 13:18:33 +01002583 quarantine_schema = scl.yaml_load(
2584 os.path.join(ZEPHYR_BASE,
2585 "scripts", "schemas", "twister", "quarantine-schema.yaml"))
Anas Nashifce2b4182020-03-24 14:40:28 -04002586
2587 testcase_valid_keys = {"tags": {"type": "set", "required": False},
2588 "type": {"type": "str", "default": "integration"},
2589 "extra_args": {"type": "list"},
2590 "extra_configs": {"type": "list"},
2591 "build_only": {"type": "bool", "default": False},
2592 "build_on_all": {"type": "bool", "default": False},
2593 "skip": {"type": "bool", "default": False},
2594 "slow": {"type": "bool", "default": False},
2595 "timeout": {"type": "int", "default": 60},
2596 "min_ram": {"type": "int", "default": 8},
2597 "depends_on": {"type": "set"},
2598 "min_flash": {"type": "int", "default": 32},
Anas Nashifdca317c2020-08-26 11:28:25 -04002599 "arch_allow": {"type": "set"},
Anas Nashifce2b4182020-03-24 14:40:28 -04002600 "arch_exclude": {"type": "set"},
2601 "extra_sections": {"type": "list", "default": []},
Anas Nashif1636c312020-05-28 08:02:54 -04002602 "integration_platforms": {"type": "list", "default": []},
Anas Nashifce2b4182020-03-24 14:40:28 -04002603 "platform_exclude": {"type": "set"},
Anas Nashifdca317c2020-08-26 11:28:25 -04002604 "platform_allow": {"type": "set"},
Anas Nashifce2b4182020-03-24 14:40:28 -04002605 "toolchain_exclude": {"type": "set"},
Anas Nashifdca317c2020-08-26 11:28:25 -04002606 "toolchain_allow": {"type": "set"},
Anas Nashifce2b4182020-03-24 14:40:28 -04002607 "filter": {"type": "str"},
2608 "harness": {"type": "str"},
2609 "harness_config": {"type": "map", "default": {}}
2610 }
2611
Anas Nashif94f68262020-12-07 11:21:04 -05002612 RELEASE_DATA = os.path.join(ZEPHYR_BASE, "scripts", "release",
2613 "twister_last_release.csv")
Anas Nashifce2b4182020-03-24 14:40:28 -04002614
Aastha Grovera0ae5342020-05-13 13:34:00 -07002615 SAMPLE_FILENAME = 'sample.yaml'
2616 TESTCASE_FILENAME = 'testcase.yaml'
2617
Anas Nashifaff616d2020-04-17 21:24:57 -04002618 def __init__(self, board_root_list=[], testcase_roots=[], outdir=None):
Anas Nashifce2b4182020-03-24 14:40:28 -04002619
2620 self.roots = testcase_roots
2621 if not isinstance(board_root_list, list):
2622 self.board_roots = [board_root_list]
2623 else:
2624 self.board_roots = board_root_list
2625
2626 # Testsuite Options
2627 self.coverage_platform = []
2628 self.build_only = False
2629 self.cmake_only = False
2630 self.cleanup = False
2631 self.enable_slow = False
2632 self.device_testing = False
Anas Nashifce8c12e2020-05-21 09:11:40 -04002633 self.fixtures = []
Anas Nashifce2b4182020-03-24 14:40:28 -04002634 self.enable_coverage = False
Christian Taedcke3dbe9f22020-07-06 16:00:57 +02002635 self.enable_ubsan = False
Anas Nashifce2b4182020-03-24 14:40:28 -04002636 self.enable_lsan = False
2637 self.enable_asan = False
2638 self.enable_valgrind = False
2639 self.extra_args = []
2640 self.inline_logs = False
2641 self.enable_sizes_report = False
2642 self.west_flash = None
2643 self.west_runner = None
2644 self.generator = None
2645 self.generator_cmd = None
Anas Nashif50925412020-07-16 17:25:19 -04002646 self.warnings_as_errors = True
Anas Nashiff68146f2020-11-28 11:07:06 -05002647 self.overflow_as_errors = False
Maciej Perkowski9c6dfce2021-03-11 13:18:33 +01002648 self.quarantine_verify = False
Anas Nashifce2b4182020-03-24 14:40:28 -04002649
2650 # Keep track of which test cases we've filtered out and why
2651 self.testcases = {}
Maciej Perkowski9c6dfce2021-03-11 13:18:33 +01002652 self.quarantine = {}
Anas Nashifce2b4182020-03-24 14:40:28 -04002653 self.platforms = []
2654 self.selected_platforms = []
Maciej Perkowskib6a69aa2021-02-04 16:05:21 +01002655 self.filtered_platforms = []
Anas Nashifce2b4182020-03-24 14:40:28 -04002656 self.default_platforms = []
2657 self.outdir = os.path.abspath(outdir)
Anas Nashifaff616d2020-04-17 21:24:57 -04002658 self.discards = {}
Anas Nashifce2b4182020-03-24 14:40:28 -04002659 self.load_errors = 0
2660 self.instances = dict()
2661
Anas Nashifce2b4182020-03-24 14:40:28 -04002662 self.total_platforms = 0
2663 self.start_time = 0
2664 self.duration = 0
2665 self.warnings = 0
Anas Nashifce2b4182020-03-24 14:40:28 -04002666
2667 # hardcoded for now
Anas Nashif8305d1b2020-11-26 11:55:02 -05002668 self.duts = []
Anas Nashifce2b4182020-03-24 14:40:28 -04002669
Anas Nashif1636c312020-05-28 08:02:54 -04002670 # run integration tests only
2671 self.integration = False
2672
Anas Nashif531fe892020-09-11 13:56:33 -04002673 self.pipeline = None
Maciej Perkowski060e00d2020-10-13 18:59:37 +02002674 self.version = "NA"
2675
2676 def check_zephyr_version(self):
2677 try:
Jingru Wangb9c953c2020-12-21 10:45:27 +08002678 subproc = subprocess.run(["git", "describe", "--abbrev=12"],
Maciej Perkowski060e00d2020-10-13 18:59:37 +02002679 stdout=subprocess.PIPE,
2680 universal_newlines=True,
2681 cwd=ZEPHYR_BASE)
2682 if subproc.returncode == 0:
2683 self.version = subproc.stdout.strip()
2684 logger.info(f"Zephyr version: {self.version}")
2685 except OSError:
2686 logger.info("Cannot read zephyr version.")
2687
Anas Nashifbb280352020-05-07 12:02:48 -04002688 def get_platform_instances(self, platform):
2689 filtered_dict = {k:v for k,v in self.instances.items() if k.startswith(platform + "/")}
2690 return filtered_dict
2691
Anas Nashifce2b4182020-03-24 14:40:28 -04002692 def config(self):
2693 logger.info("coverage platform: {}".format(self.coverage_platform))
2694
2695 # Debug Functions
2696 @staticmethod
2697 def info(what):
2698 sys.stdout.write(what + "\n")
2699 sys.stdout.flush()
2700
Anas Nashif531fe892020-09-11 13:56:33 -04002701 def update_counting(self, results=None, initial=False):
Anas Nashif3b939da2020-11-24 13:21:27 -05002702 results.skipped_configs = 0
2703 results.skipped_cases = 0
Maciej Perkowskic67a0cd2020-08-13 15:20:13 +02002704 for instance in self.instances.values():
Anas Nashif531fe892020-09-11 13:56:33 -04002705 if initial:
2706 results.cases += len(instance.testcase.cases)
Maciej Perkowskic67a0cd2020-08-13 15:20:13 +02002707 if instance.status == 'skipped':
Anas Nashif3b939da2020-11-24 13:21:27 -05002708 results.skipped_configs += 1
Anas Nashif531fe892020-09-11 13:56:33 -04002709 results.skipped_cases += len(instance.testcase.cases)
Maciej Perkowskic67a0cd2020-08-13 15:20:13 +02002710 elif instance.status == "passed":
Anas Nashif531fe892020-09-11 13:56:33 -04002711 results.passed += 1
Maciej Perkowskic67a0cd2020-08-13 15:20:13 +02002712 for res in instance.results.values():
2713 if res == 'SKIP':
Anas Nashif531fe892020-09-11 13:56:33 -04002714 results.skipped_cases += 1
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02002715
Anas Nashifce2b4182020-03-24 14:40:28 -04002716 def compare_metrics(self, filename):
2717 # name, datatype, lower results better
2718 interesting_metrics = [("ram_size", int, True),
2719 ("rom_size", int, True)]
2720
2721 if not os.path.exists(filename):
Anas Nashifad44bed2020-08-24 18:59:01 -04002722 logger.error("Cannot compare metrics, %s not found" % filename)
Anas Nashifce2b4182020-03-24 14:40:28 -04002723 return []
2724
2725 results = []
2726 saved_metrics = {}
2727 with open(filename) as fp:
2728 cr = csv.DictReader(fp)
2729 for row in cr:
2730 d = {}
2731 for m, _, _ in interesting_metrics:
2732 d[m] = row[m]
2733 saved_metrics[(row["test"], row["platform"])] = d
2734
2735 for instance in self.instances.values():
2736 mkey = (instance.testcase.name, instance.platform.name)
2737 if mkey not in saved_metrics:
2738 continue
2739 sm = saved_metrics[mkey]
2740 for metric, mtype, lower_better in interesting_metrics:
2741 if metric not in instance.metrics:
2742 continue
2743 if sm[metric] == "":
2744 continue
2745 delta = instance.metrics.get(metric, 0) - mtype(sm[metric])
2746 if delta == 0:
2747 continue
2748 results.append((instance, metric, instance.metrics.get(metric, 0), delta,
2749 lower_better))
2750 return results
2751
Anas Nashifad44bed2020-08-24 18:59:01 -04002752 def footprint_reports(self, report, show_footprint, all_deltas,
2753 footprint_threshold, last_metrics):
Anas Nashifce2b4182020-03-24 14:40:28 -04002754 if not report:
2755 return
2756
Anas Nashifad44bed2020-08-24 18:59:01 -04002757 logger.debug("running footprint_reports")
Anas Nashifce2b4182020-03-24 14:40:28 -04002758 deltas = self.compare_metrics(report)
2759 warnings = 0
2760 if deltas and show_footprint:
2761 for i, metric, value, delta, lower_better in deltas:
2762 if not all_deltas and ((delta < 0 and lower_better) or
2763 (delta > 0 and not lower_better)):
2764 continue
2765
Anas Nashifad44bed2020-08-24 18:59:01 -04002766 percentage = 0
2767 if value > delta:
2768 percentage = (float(delta) / float(value - delta))
2769
2770 if not all_deltas and (percentage < (footprint_threshold / 100.0)):
Anas Nashifce2b4182020-03-24 14:40:28 -04002771 continue
2772
2773 logger.info("{:<25} {:<60} {}{}{}: {} {:<+4}, is now {:6} {:+.2%}".format(
2774 i.platform.name, i.testcase.name, Fore.YELLOW,
2775 "INFO" if all_deltas else "WARNING", Fore.RESET,
2776 metric, delta, value, percentage))
2777 warnings += 1
2778
2779 if warnings:
2780 logger.warning("Deltas based on metrics from last %s" %
2781 ("release" if not last_metrics else "run"))
2782
Anas Nashif531fe892020-09-11 13:56:33 -04002783 def summary(self, results, unrecognized_sections):
Anas Nashifce2b4182020-03-24 14:40:28 -04002784 failed = 0
Anas Nashif4258d8d2020-05-08 08:40:27 -04002785 run = 0
Anas Nashifce2b4182020-03-24 14:40:28 -04002786 for instance in self.instances.values():
2787 if instance.status == "failed":
2788 failed += 1
2789 elif instance.metrics.get("unrecognized") and not unrecognized_sections:
2790 logger.error("%sFAILED%s: %s has unrecognized binary sections: %s" %
2791 (Fore.RED, Fore.RESET, instance.name,
2792 str(instance.metrics.get("unrecognized", []))))
2793 failed += 1
2794
Anas Nashifad44bed2020-08-24 18:59:01 -04002795 if instance.metrics.get('handler_time', None):
Anas Nashif4258d8d2020-05-08 08:40:27 -04002796 run += 1
2797
Anas Nashif3b939da2020-11-24 13:21:27 -05002798 if results.total and results.total != results.skipped_configs:
2799 pass_rate = (float(results.passed) / float(results.total - results.skipped_configs))
Anas Nashifce2b4182020-03-24 14:40:28 -04002800 else:
2801 pass_rate = 0
2802
2803 logger.info(
Anas Nashif3b939da2020-11-24 13:21:27 -05002804 "{}{} of {}{} test configurations passed ({:.2%}), {}{}{} failed, {} skipped with {}{}{} warnings in {:.2f} seconds".format(
Anas Nashifce2b4182020-03-24 14:40:28 -04002805 Fore.RED if failed else Fore.GREEN,
Anas Nashif531fe892020-09-11 13:56:33 -04002806 results.passed,
Anas Nashif3b939da2020-11-24 13:21:27 -05002807 results.total - results.skipped_configs,
Anas Nashifce2b4182020-03-24 14:40:28 -04002808 Fore.RESET,
2809 pass_rate,
Anas Nashif531fe892020-09-11 13:56:33 -04002810 Fore.RED if results.failed else Fore.RESET,
2811 results.failed,
Anas Nashifce2b4182020-03-24 14:40:28 -04002812 Fore.RESET,
Anas Nashif3b939da2020-11-24 13:21:27 -05002813 results.skipped_configs,
Anas Nashifce2b4182020-03-24 14:40:28 -04002814 Fore.YELLOW if self.warnings else Fore.RESET,
2815 self.warnings,
2816 Fore.RESET,
2817 self.duration))
2818
2819 self.total_platforms = len(self.platforms)
Anas Nashifbb427952020-11-24 08:39:42 -05002820 # if we are only building, do not report about tests being executed.
2821 if self.platforms and not self.build_only:
Anas Nashif531fe892020-09-11 13:56:33 -04002822 logger.info("In total {} test cases were executed, {} skipped on {} out of total {} platforms ({:02.2f}%)".format(
2823 results.cases - results.skipped_cases,
2824 results.skipped_cases,
Maciej Perkowskib6a69aa2021-02-04 16:05:21 +01002825 len(self.filtered_platforms),
Anas Nashifce2b4182020-03-24 14:40:28 -04002826 self.total_platforms,
Maciej Perkowskib6a69aa2021-02-04 16:05:21 +01002827 (100 * len(self.filtered_platforms) / len(self.platforms))
Anas Nashifce2b4182020-03-24 14:40:28 -04002828 ))
2829
Anas Nashif3b939da2020-11-24 13:21:27 -05002830 logger.info(f"{Fore.GREEN}{run}{Fore.RESET} test configurations executed on platforms, \
2831{Fore.RED}{results.total - run - results.skipped_configs}{Fore.RESET} test configurations were only built.")
Anas Nashif4258d8d2020-05-08 08:40:27 -04002832
Anas Nashifec479122021-01-25 08:13:27 -05002833 def save_reports(self, name, suffix, report_dir, no_update, release, only_failed, platform_reports, json_report):
Anas Nashifce2b4182020-03-24 14:40:28 -04002834 if not self.instances:
2835 return
2836
Anas Nashifa5d16ab2020-12-10 11:45:57 -05002837 logger.info("Saving reports...")
Anas Nashifce2b4182020-03-24 14:40:28 -04002838 if name:
2839 report_name = name
2840 else:
Anas Nashifb18f3112020-12-07 11:40:19 -05002841 report_name = "twister"
Anas Nashifce2b4182020-03-24 14:40:28 -04002842
2843 if report_dir:
2844 os.makedirs(report_dir, exist_ok=True)
2845 filename = os.path.join(report_dir, report_name)
2846 outdir = report_dir
2847 else:
2848 filename = os.path.join(self.outdir, report_name)
2849 outdir = self.outdir
2850
Anas Nashif6915adf2020-04-22 09:39:42 -04002851 if suffix:
2852 filename = "{}_{}".format(filename, suffix)
2853
Anas Nashifce2b4182020-03-24 14:40:28 -04002854 if not no_update:
Maciej Perkowski060e00d2020-10-13 18:59:37 +02002855 self.xunit_report(filename + ".xml", full_report=False,
2856 append=only_failed, version=self.version)
2857 self.xunit_report(filename + "_report.xml", full_report=True,
2858 append=only_failed, version=self.version)
Anas Nashifce2b4182020-03-24 14:40:28 -04002859 self.csv_report(filename + ".csv")
Anas Nashifec479122021-01-25 08:13:27 -05002860
2861 if json_report:
2862 self.json_report(filename + ".json", append=only_failed, version=self.version)
Anas Nashif90415502020-04-11 22:15:04 -04002863
Anas Nashifa5d16ab2020-12-10 11:45:57 -05002864 if platform_reports:
2865 self.target_report(outdir, suffix, append=only_failed)
Anas Nashifce2b4182020-03-24 14:40:28 -04002866 if self.discards:
2867 self.discard_report(filename + "_discard.csv")
2868
2869 if release:
2870 self.csv_report(self.RELEASE_DATA)
2871
2872 def add_configurations(self):
2873
2874 for board_root in self.board_roots:
2875 board_root = os.path.abspath(board_root)
2876
2877 logger.debug("Reading platform configuration files under %s..." %
2878 board_root)
2879
2880 for file in glob.glob(os.path.join(board_root, "*", "*", "*.yaml")):
Anas Nashifce2b4182020-03-24 14:40:28 -04002881 try:
2882 platform = Platform()
2883 platform.load(file)
Anas Nashif61c4a512020-08-13 07:46:45 -04002884 if platform.name in [p.name for p in self.platforms]:
2885 logger.error(f"Duplicate platform {platform.name} in {file}")
2886 raise Exception(f"Duplicate platform identifier {platform.name} found")
Anas Nashifb18f3112020-12-07 11:40:19 -05002887 if platform.twister:
Anas Nashifce2b4182020-03-24 14:40:28 -04002888 self.platforms.append(platform)
2889 if platform.default:
2890 self.default_platforms.append(platform.name)
2891
2892 except RuntimeError as e:
2893 logger.error("E: %s: can't load: %s" % (file, e))
2894 self.load_errors += 1
2895
2896 def get_all_tests(self):
2897 tests = []
2898 for _, tc in self.testcases.items():
2899 for case in tc.cases:
2900 tests.append(case)
2901
2902 return tests
2903
2904 @staticmethod
2905 def get_toolchain():
Torsten Rasmussena0889702021-02-05 09:55:41 +01002906 toolchain_script = Path(ZEPHYR_BASE) / Path('cmake/verify-toolchain.cmake')
Torsten Rasmussend162e9e2021-02-04 15:55:23 +01002907 result = CMake.run_cmake_script([toolchain_script, "FORMAT=json"])
2908
Anas Nashifce2b4182020-03-24 14:40:28 -04002909 try:
Torsten Rasmussend162e9e2021-02-04 15:55:23 +01002910 if result['returncode']:
Torsten Rasmussenaf875992021-09-28 13:04:52 +02002911 raise TwisterRuntimeError(f"E: {result['returnmsg']}")
Anas Nashifce2b4182020-03-24 14:40:28 -04002912 except Exception as e:
2913 print(str(e))
2914 sys.exit(2)
Torsten Rasmussend162e9e2021-02-04 15:55:23 +01002915 toolchain = json.loads(result['stdout'])['ZEPHYR_TOOLCHAIN_VARIANT']
2916 logger.info(f"Using '{toolchain}' toolchain.")
Anas Nashifce2b4182020-03-24 14:40:28 -04002917
2918 return toolchain
2919
2920 def add_testcases(self, testcase_filter=[]):
2921 for root in self.roots:
2922 root = os.path.abspath(root)
2923
2924 logger.debug("Reading test case configuration files under %s..." % root)
2925
Jorgen Kvalvaag36d96d22021-04-06 14:57:28 +02002926 for dirpath, _, filenames in os.walk(root, topdown=True):
Aastha Grovera0ae5342020-05-13 13:34:00 -07002927 if self.SAMPLE_FILENAME in filenames:
2928 filename = self.SAMPLE_FILENAME
2929 elif self.TESTCASE_FILENAME in filenames:
2930 filename = self.TESTCASE_FILENAME
Anas Nashifce2b4182020-03-24 14:40:28 -04002931 else:
2932 continue
2933
2934 logger.debug("Found possible test case in " + dirpath)
2935
Anas Nashifce2b4182020-03-24 14:40:28 -04002936 tc_path = os.path.join(dirpath, filename)
2937
2938 try:
Anas Nashif45943702020-12-11 17:55:15 -05002939 parsed_data = TwisterConfigParser(tc_path, self.tc_schema)
Anas Nashifce2b4182020-03-24 14:40:28 -04002940 parsed_data.load()
2941
2942 tc_path = os.path.dirname(tc_path)
2943 workdir = os.path.relpath(tc_path, root)
2944
2945 for name in parsed_data.tests.keys():
Anas Nashifaff616d2020-04-17 21:24:57 -04002946 tc = TestCase(root, workdir, name)
Anas Nashifce2b4182020-03-24 14:40:28 -04002947
2948 tc_dict = parsed_data.get_test(name, self.testcase_valid_keys)
2949
2950 tc.source_dir = tc_path
2951 tc.yamlfile = tc_path
2952
Anas Nashifce2b4182020-03-24 14:40:28 -04002953 tc.type = tc_dict["type"]
2954 tc.tags = tc_dict["tags"]
2955 tc.extra_args = tc_dict["extra_args"]
2956 tc.extra_configs = tc_dict["extra_configs"]
Anas Nashifdca317c2020-08-26 11:28:25 -04002957 tc.arch_allow = tc_dict["arch_allow"]
Anas Nashifce2b4182020-03-24 14:40:28 -04002958 tc.arch_exclude = tc_dict["arch_exclude"]
2959 tc.skip = tc_dict["skip"]
2960 tc.platform_exclude = tc_dict["platform_exclude"]
Anas Nashifdca317c2020-08-26 11:28:25 -04002961 tc.platform_allow = tc_dict["platform_allow"]
Anas Nashifce2b4182020-03-24 14:40:28 -04002962 tc.toolchain_exclude = tc_dict["toolchain_exclude"]
Anas Nashifdca317c2020-08-26 11:28:25 -04002963 tc.toolchain_allow = tc_dict["toolchain_allow"]
Anas Nashifce2b4182020-03-24 14:40:28 -04002964 tc.tc_filter = tc_dict["filter"]
2965 tc.timeout = tc_dict["timeout"]
2966 tc.harness = tc_dict["harness"]
2967 tc.harness_config = tc_dict["harness_config"]
Anas Nashif43275c82020-05-04 18:22:16 -04002968 if tc.harness == 'console' and not tc.harness_config:
2969 raise Exception('Harness config error: console harness defined without a configuration.')
Anas Nashifce2b4182020-03-24 14:40:28 -04002970 tc.build_only = tc_dict["build_only"]
2971 tc.build_on_all = tc_dict["build_on_all"]
2972 tc.slow = tc_dict["slow"]
2973 tc.min_ram = tc_dict["min_ram"]
2974 tc.depends_on = tc_dict["depends_on"]
2975 tc.min_flash = tc_dict["min_flash"]
2976 tc.extra_sections = tc_dict["extra_sections"]
Anas Nashif1636c312020-05-28 08:02:54 -04002977 tc.integration_platforms = tc_dict["integration_platforms"]
Anas Nashifce2b4182020-03-24 14:40:28 -04002978
2979 tc.parse_subcases(tc_path)
2980
2981 if testcase_filter:
2982 if tc.name and tc.name in testcase_filter:
2983 self.testcases[tc.name] = tc
2984 else:
2985 self.testcases[tc.name] = tc
2986
2987 except Exception as e:
2988 logger.error("%s: can't load (skipping): %s" % (tc_path, e))
2989 self.load_errors += 1
Anas Nashiffe07d572020-11-20 12:13:47 -05002990 return len(self.testcases)
Anas Nashifce2b4182020-03-24 14:40:28 -04002991
2992 def get_platform(self, name):
2993 selected_platform = None
2994 for platform in self.platforms:
2995 if platform.name == name:
2996 selected_platform = platform
2997 break
2998 return selected_platform
2999
Maciej Perkowski9c6dfce2021-03-11 13:18:33 +01003000 def load_quarantine(self, file):
3001 """
3002 Loads quarantine list from the given yaml file. Creates a dictionary
3003 of all tests configurations (platform + scenario: comment) that shall be
3004 skipped due to quarantine
3005 """
3006
3007 # Load yaml into quarantine_yaml
3008 quarantine_yaml = scl.yaml_load_verify(file, self.quarantine_schema)
3009
3010 # Create quarantine_list with a product of the listed
3011 # platforms and scenarios for each entry in quarantine yaml
3012 quarantine_list = []
3013 for quar_dict in quarantine_yaml:
3014 if quar_dict['platforms'][0] == "all":
3015 plat = [p.name for p in self.platforms]
3016 else:
3017 plat = quar_dict['platforms']
3018 comment = quar_dict.get('comment', "NA")
3019 quarantine_list.append([{".".join([p, s]): comment}
3020 for p in plat for s in quar_dict['scenarios']])
3021
3022 # Flatten the quarantine_list
3023 quarantine_list = [it for sublist in quarantine_list for it in sublist]
3024 # Change quarantine_list into a dictionary
3025 for d in quarantine_list:
3026 self.quarantine.update(d)
3027
Anas Nashif82a6a462020-11-02 10:47:36 -05003028 def load_from_file(self, file, filter_status=[], filter_platform=[]):
Anas Nashifce2b4182020-03-24 14:40:28 -04003029 try:
3030 with open(file, "r") as fp:
3031 cr = csv.DictReader(fp)
3032 instance_list = []
3033 for row in cr:
3034 if row["status"] in filter_status:
3035 continue
3036 test = row["test"]
3037
3038 platform = self.get_platform(row["platform"])
Anas Nashif82a6a462020-11-02 10:47:36 -05003039 if filter_platform and platform.name not in filter_platform:
3040 continue
Anas Nashifce2b4182020-03-24 14:40:28 -04003041 instance = TestInstance(self.testcases[test], platform, self.outdir)
Anas Nashif405f1b62020-07-27 12:27:13 -04003042 if self.device_testing:
3043 tfilter = 'runnable'
3044 else:
3045 tfilter = 'buildable'
3046 instance.run = instance.check_runnable(
Anas Nashifce2b4182020-03-24 14:40:28 -04003047 self.enable_slow,
Anas Nashif405f1b62020-07-27 12:27:13 -04003048 tfilter,
Anas Nashifce8c12e2020-05-21 09:11:40 -04003049 self.fixtures
Anas Nashifce2b4182020-03-24 14:40:28 -04003050 )
Christian Taedcke3dbe9f22020-07-06 16:00:57 +02003051 instance.create_overlay(platform, self.enable_asan, self.enable_ubsan, self.enable_coverage, self.coverage_platform)
Anas Nashifce2b4182020-03-24 14:40:28 -04003052 instance_list.append(instance)
3053 self.add_instances(instance_list)
3054
3055 except KeyError as e:
3056 logger.error("Key error while parsing tests file.({})".format(str(e)))
3057 sys.exit(2)
3058
3059 except FileNotFoundError as e:
3060 logger.error("Couldn't find input file with list of tests. ({})".format(e))
3061 sys.exit(2)
3062
3063 def apply_filters(self, **kwargs):
3064
3065 toolchain = self.get_toolchain()
3066
3067 discards = {}
3068 platform_filter = kwargs.get('platform')
Anas Nashifaff616d2020-04-17 21:24:57 -04003069 exclude_platform = kwargs.get('exclude_platform', [])
3070 testcase_filter = kwargs.get('run_individual_tests', [])
Anas Nashifce2b4182020-03-24 14:40:28 -04003071 arch_filter = kwargs.get('arch')
3072 tag_filter = kwargs.get('tag')
3073 exclude_tag = kwargs.get('exclude_tag')
3074 all_filter = kwargs.get('all')
Anas Nashif405f1b62020-07-27 12:27:13 -04003075 runnable = kwargs.get('runnable')
Anas Nashifce2b4182020-03-24 14:40:28 -04003076 force_toolchain = kwargs.get('force_toolchain')
Anas Nashif1a5defa2020-05-01 14:57:00 -04003077 force_platform = kwargs.get('force_platform')
Anas Nashif9eb9c4c2020-08-26 15:47:25 -04003078 emu_filter = kwargs.get('emulation_only')
Anas Nashifce2b4182020-03-24 14:40:28 -04003079
3080 logger.debug("platform filter: " + str(platform_filter))
3081 logger.debug(" arch_filter: " + str(arch_filter))
3082 logger.debug(" tag_filter: " + str(tag_filter))
3083 logger.debug(" exclude_tag: " + str(exclude_tag))
3084
3085 default_platforms = False
Anas Nashif9eb9c4c2020-08-26 15:47:25 -04003086 emulation_platforms = False
Anas Nashifce2b4182020-03-24 14:40:28 -04003087
Anas Nashifce2b4182020-03-24 14:40:28 -04003088
3089 if all_filter:
3090 logger.info("Selecting all possible platforms per test case")
3091 # When --all used, any --platform arguments ignored
3092 platform_filter = []
Anas Nashif9eb9c4c2020-08-26 15:47:25 -04003093 elif not platform_filter and not emu_filter:
Anas Nashifce2b4182020-03-24 14:40:28 -04003094 logger.info("Selecting default platforms per test case")
3095 default_platforms = True
Anas Nashif9eb9c4c2020-08-26 15:47:25 -04003096 elif emu_filter:
3097 logger.info("Selecting emulation platforms per test case")
3098 emulation_platforms = True
Anas Nashifce2b4182020-03-24 14:40:28 -04003099
Anas Nashif3b939da2020-11-24 13:21:27 -05003100 if platform_filter:
3101 platforms = list(filter(lambda p: p.name in platform_filter, self.platforms))
3102 elif emu_filter:
3103 platforms = list(filter(lambda p: p.simulation != 'na', self.platforms))
3104 elif arch_filter:
3105 platforms = list(filter(lambda p: p.arch in arch_filter, self.platforms))
3106 elif default_platforms:
3107 platforms = list(filter(lambda p: p.default, self.platforms))
3108 else:
3109 platforms = self.platforms
3110
Anas Nashifce2b4182020-03-24 14:40:28 -04003111 logger.info("Building initial testcase list...")
3112
3113 for tc_name, tc in self.testcases.items():
Anas Nashif827ecb72021-01-14 09:34:29 -05003114
3115 if tc.build_on_all and not platform_filter:
3116 platform_scope = self.platforms
Anas Nashife618a592021-03-03 14:05:54 -05003117 elif tc.integration_platforms and self.integration:
3118 platform_scope = list(filter(lambda item: item.name in tc.integration_platforms, \
3119 self.platforms))
Anas Nashif827ecb72021-01-14 09:34:29 -05003120 else:
3121 platform_scope = platforms
3122
Kumar Galad590e0d2021-04-02 10:18:01 -05003123 integration = self.integration and tc.integration_platforms
3124
3125 # If there isn't any overlap between the platform_allow list and the platform_scope
3126 # we set the scope to the platform_allow list
3127 if tc.platform_allow and not platform_filter and not integration:
3128 a = set(platform_scope)
3129 b = set(filter(lambda item: item.name in tc.platform_allow, self.platforms))
3130 c = a.intersection(b)
3131 if not c:
3132 platform_scope = list(filter(lambda item: item.name in tc.platform_allow, \
3133 self.platforms))
3134
Anas Nashifce2b4182020-03-24 14:40:28 -04003135 # list of instances per testcase, aka configurations.
3136 instance_list = []
Anas Nashif827ecb72021-01-14 09:34:29 -05003137 for plat in platform_scope:
Anas Nashifce2b4182020-03-24 14:40:28 -04003138 instance = TestInstance(tc, plat, self.outdir)
Anas Nashif405f1b62020-07-27 12:27:13 -04003139 if runnable:
3140 tfilter = 'runnable'
3141 else:
3142 tfilter = 'buildable'
3143
3144 instance.run = instance.check_runnable(
Anas Nashifce2b4182020-03-24 14:40:28 -04003145 self.enable_slow,
Anas Nashif405f1b62020-07-27 12:27:13 -04003146 tfilter,
Anas Nashifce8c12e2020-05-21 09:11:40 -04003147 self.fixtures
Anas Nashifce2b4182020-03-24 14:40:28 -04003148 )
Anas Nashiff86bc052020-11-19 16:05:03 -05003149
Anas Nashiff04461e2020-06-29 10:07:02 -04003150 for t in tc.cases:
3151 instance.results[t] = None
Anas Nashif3b86f132020-05-21 10:35:33 -04003152
Anas Nashif8305d1b2020-11-26 11:55:02 -05003153 if runnable and self.duts:
3154 for h in self.duts:
Anas Nashif531fe892020-09-11 13:56:33 -04003155 if h.platform == plat.name:
3156 if tc.harness_config.get('fixture') in h.fixtures:
Anas Nashif3b86f132020-05-21 10:35:33 -04003157 instance.run = True
3158
Anas Nashif1a5defa2020-05-01 14:57:00 -04003159 if not force_platform and plat.name in exclude_platform:
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003160 discards[instance] = discards.get(instance, "Platform is excluded on command line.")
Anas Nashifce2b4182020-03-24 14:40:28 -04003161
3162 if (plat.arch == "unit") != (tc.type == "unit"):
3163 # Discard silently
3164 continue
3165
Anas Nashif405f1b62020-07-27 12:27:13 -04003166 if runnable and not instance.run:
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003167 discards[instance] = discards.get(instance, "Not runnable on device")
Anas Nashifce2b4182020-03-24 14:40:28 -04003168
Anas Nashif1636c312020-05-28 08:02:54 -04003169 if self.integration and tc.integration_platforms and plat.name not in tc.integration_platforms:
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003170 discards[instance] = discards.get(instance, "Not part of integration platforms")
Anas Nashif1636c312020-05-28 08:02:54 -04003171
Anas Nashifce2b4182020-03-24 14:40:28 -04003172 if tc.skip:
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003173 discards[instance] = discards.get(instance, "Skip filter")
Anas Nashifce2b4182020-03-24 14:40:28 -04003174
Anas Nashifce2b4182020-03-24 14:40:28 -04003175 if tag_filter and not tc.tags.intersection(tag_filter):
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003176 discards[instance] = discards.get(instance, "Command line testcase tag filter")
Anas Nashifce2b4182020-03-24 14:40:28 -04003177
3178 if exclude_tag and tc.tags.intersection(exclude_tag):
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003179 discards[instance] = discards.get(instance, "Command line testcase exclude filter")
Anas Nashifce2b4182020-03-24 14:40:28 -04003180
3181 if testcase_filter and tc_name not in testcase_filter:
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003182 discards[instance] = discards.get(instance, "Testcase name filter")
Anas Nashifce2b4182020-03-24 14:40:28 -04003183
3184 if arch_filter and plat.arch not in arch_filter:
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003185 discards[instance] = discards.get(instance, "Command line testcase arch filter")
Anas Nashifce2b4182020-03-24 14:40:28 -04003186
Anas Nashif1a5defa2020-05-01 14:57:00 -04003187 if not force_platform:
Anas Nashifce2b4182020-03-24 14:40:28 -04003188
Anas Nashifdca317c2020-08-26 11:28:25 -04003189 if tc.arch_allow and plat.arch not in tc.arch_allow:
3190 discards[instance] = discards.get(instance, "Not in test case arch allow list")
Anas Nashifce2b4182020-03-24 14:40:28 -04003191
Anas Nashif1a5defa2020-05-01 14:57:00 -04003192 if tc.arch_exclude and plat.arch in tc.arch_exclude:
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003193 discards[instance] = discards.get(instance, "In test case arch exclude")
Anas Nashif1a5defa2020-05-01 14:57:00 -04003194
3195 if tc.platform_exclude and plat.name in tc.platform_exclude:
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003196 discards[instance] = discards.get(instance, "In test case platform exclude")
Anas Nashifce2b4182020-03-24 14:40:28 -04003197
3198 if tc.toolchain_exclude and toolchain in tc.toolchain_exclude:
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003199 discards[instance] = discards.get(instance, "In test case toolchain exclude")
Anas Nashifce2b4182020-03-24 14:40:28 -04003200
3201 if platform_filter and plat.name not in platform_filter:
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003202 discards[instance] = discards.get(instance, "Command line platform filter")
Anas Nashifce2b4182020-03-24 14:40:28 -04003203
Anas Nashifdca317c2020-08-26 11:28:25 -04003204 if tc.platform_allow and plat.name not in tc.platform_allow:
3205 discards[instance] = discards.get(instance, "Not in testcase platform allow list")
Anas Nashifce2b4182020-03-24 14:40:28 -04003206
Anas Nashifdca317c2020-08-26 11:28:25 -04003207 if tc.toolchain_allow and toolchain not in tc.toolchain_allow:
3208 discards[instance] = discards.get(instance, "Not in testcase toolchain allow list")
Anas Nashifce2b4182020-03-24 14:40:28 -04003209
3210 if not plat.env_satisfied:
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003211 discards[instance] = discards.get(instance, "Environment ({}) not satisfied".format(", ".join(plat.env)))
Anas Nashifce2b4182020-03-24 14:40:28 -04003212
3213 if not force_toolchain \
3214 and toolchain and (toolchain not in plat.supported_toolchains) \
Maciej Perkowskiedb23d72021-09-09 16:16:49 +02003215 and "host" not in plat.supported_toolchains \
Anas Nashifce2b4182020-03-24 14:40:28 -04003216 and tc.type != 'unit':
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003217 discards[instance] = discards.get(instance, "Not supported by the toolchain")
Anas Nashifce2b4182020-03-24 14:40:28 -04003218
3219 if plat.ram < tc.min_ram:
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003220 discards[instance] = discards.get(instance, "Not enough RAM")
Anas Nashifce2b4182020-03-24 14:40:28 -04003221
3222 if tc.depends_on:
3223 dep_intersection = tc.depends_on.intersection(set(plat.supported))
3224 if dep_intersection != set(tc.depends_on):
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003225 discards[instance] = discards.get(instance, "No hardware support")
Anas Nashifce2b4182020-03-24 14:40:28 -04003226
3227 if plat.flash < tc.min_flash:
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003228 discards[instance] = discards.get(instance, "Not enough FLASH")
Anas Nashifce2b4182020-03-24 14:40:28 -04003229
3230 if set(plat.ignore_tags) & tc.tags:
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003231 discards[instance] = discards.get(instance, "Excluded tags per platform (exclude_tags)")
Anas Nashife8e367a2020-07-16 16:27:04 -04003232
Anas Nashif555fc6d2020-07-30 07:23:54 -04003233 if plat.only_tags and not set(plat.only_tags) & tc.tags:
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003234 discards[instance] = discards.get(instance, "Excluded tags per platform (only_tags)")
Anas Nashifce2b4182020-03-24 14:40:28 -04003235
Maciej Perkowski9c6dfce2021-03-11 13:18:33 +01003236 test_configuration = ".".join([instance.platform.name,
3237 instance.testcase.id])
3238 # skip quarantined tests
3239 if test_configuration in self.quarantine and not self.quarantine_verify:
3240 discards[instance] = discards.get(instance,
3241 f"Quarantine: {self.quarantine[test_configuration]}")
3242 # run only quarantined test to verify their statuses (skip everything else)
3243 if self.quarantine_verify and test_configuration not in self.quarantine:
3244 discards[instance] = discards.get(instance, "Not under quarantine")
3245
Anas Nashifce2b4182020-03-24 14:40:28 -04003246 # if nothing stopped us until now, it means this configuration
3247 # needs to be added.
3248 instance_list.append(instance)
3249
3250 # no configurations, so jump to next testcase
3251 if not instance_list:
3252 continue
3253
Anas Nashifb18f3112020-12-07 11:40:19 -05003254 # if twister was launched with no platform options at all, we
Anas Nashifce2b4182020-03-24 14:40:28 -04003255 # take all default platforms
Anas Nashife618a592021-03-03 14:05:54 -05003256 if default_platforms and not tc.build_on_all and not integration:
Anas Nashifdca317c2020-08-26 11:28:25 -04003257 if tc.platform_allow:
Anas Nashifce2b4182020-03-24 14:40:28 -04003258 a = set(self.default_platforms)
Anas Nashifdca317c2020-08-26 11:28:25 -04003259 b = set(tc.platform_allow)
Anas Nashifce2b4182020-03-24 14:40:28 -04003260 c = a.intersection(b)
3261 if c:
3262 aa = list(filter(lambda tc: tc.platform.name in c, instance_list))
3263 self.add_instances(aa)
3264 else:
Kumar Galad590e0d2021-04-02 10:18:01 -05003265 self.add_instances(instance_list)
Anas Nashifce2b4182020-03-24 14:40:28 -04003266 else:
3267 instances = list(filter(lambda tc: tc.platform.default, instance_list))
3268 self.add_instances(instances)
Anas Nashif4bccc1d2021-03-12 18:21:29 -05003269 elif integration:
Anas Nashife618a592021-03-03 14:05:54 -05003270 instances = list(filter(lambda item: item.platform.name in tc.integration_platforms, instance_list))
3271 self.add_instances(instances)
3272
3273
Anas Nashifce2b4182020-03-24 14:40:28 -04003274
Anas Nashif9eb9c4c2020-08-26 15:47:25 -04003275 elif emulation_platforms:
3276 self.add_instances(instance_list)
3277 for instance in list(filter(lambda inst: not inst.platform.simulation != 'na', instance_list)):
3278 discards[instance] = discards.get(instance, "Not an emulated platform")
Anas Nashifce2b4182020-03-24 14:40:28 -04003279 else:
3280 self.add_instances(instance_list)
3281
3282 for _, case in self.instances.items():
Christian Taedcke3dbe9f22020-07-06 16:00:57 +02003283 case.create_overlay(case.platform, self.enable_asan, self.enable_ubsan, self.enable_coverage, self.coverage_platform)
Anas Nashifce2b4182020-03-24 14:40:28 -04003284
3285 self.discards = discards
3286 self.selected_platforms = set(p.platform.name for p in self.instances.values())
3287
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003288 for instance in self.discards:
3289 instance.reason = self.discards[instance]
Maciej Perkowskid08ee922021-03-30 14:12:02 +02003290 # If integration mode is on all skips on integration_platforms are treated as errors.
Maciej Perkowskid8716032021-07-22 15:41:18 +02003291 if self.integration and instance.platform.name in instance.testcase.integration_platforms \
3292 and "Quarantine" not in instance.reason:
Maciej Perkowskid08ee922021-03-30 14:12:02 +02003293 instance.status = "error"
3294 instance.reason += " but is one of the integration platforms"
3295 instance.fill_results_by_status()
3296 self.instances[instance.name] = instance
3297 else:
3298 instance.status = "skipped"
3299 instance.fill_results_by_status()
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003300
Maciej Perkowskib6a69aa2021-02-04 16:05:21 +01003301 self.filtered_platforms = set(p.platform.name for p in self.instances.values()
3302 if p.status != "skipped" )
3303
Anas Nashifce2b4182020-03-24 14:40:28 -04003304 return discards
3305
3306 def add_instances(self, instance_list):
3307 for instance in instance_list:
3308 self.instances[instance.name] = instance
3309
Anas Nashif531fe892020-09-11 13:56:33 -04003310 @staticmethod
3311 def calc_one_elf_size(instance):
3312 if instance.status not in ["error", "failed", "skipped"]:
3313 if instance.platform.type != "native":
3314 size_calc = instance.calculate_sizes()
3315 instance.metrics["ram_size"] = size_calc.get_ram_size()
3316 instance.metrics["rom_size"] = size_calc.get_rom_size()
3317 instance.metrics["unrecognized"] = size_calc.unrecognized_sections()
3318 else:
3319 instance.metrics["ram_size"] = 0
3320 instance.metrics["rom_size"] = 0
3321 instance.metrics["unrecognized"] = []
3322
3323 instance.metrics["handler_time"] = instance.handler.duration if instance.handler else 0
3324
3325 def add_tasks_to_queue(self, pipeline, build_only=False, test_only=False):
Anas Nashifce2b4182020-03-24 14:40:28 -04003326 for instance in self.instances.values():
Anas Nashif405f1b62020-07-27 12:27:13 -04003327 if build_only:
3328 instance.run = False
3329
Andreas Vibeto32fbf8e2021-07-16 10:01:49 +02003330 if instance.status not in ['passed', 'skipped', 'error']:
3331 logger.debug(f"adding {instance.name}")
3332 instance.status = None
3333 if test_only and instance.run:
3334 pipeline.put({"op": "run", "test": instance})
3335 else:
Anas Nashifce2b4182020-03-24 14:40:28 -04003336 pipeline.put({"op": "cmake", "test": instance})
Andreas Vibeto32fbf8e2021-07-16 10:01:49 +02003337 # If the instance got 'error' status before, proceed to the report stage
3338 if instance.status == "error":
3339 pipeline.put({"op": "report", "test": instance})
Anas Nashifce2b4182020-03-24 14:40:28 -04003340
Anas Nashif531fe892020-09-11 13:56:33 -04003341 def pipeline_mgr(self, pipeline, done_queue, lock, results):
3342 while True:
3343 try:
3344 task = pipeline.get_nowait()
3345 except queue.Empty:
3346 break
3347 else:
3348 test = task['test']
3349 pb = ProjectBuilder(self,
3350 test,
3351 lsan=self.enable_lsan,
3352 asan=self.enable_asan,
3353 ubsan=self.enable_ubsan,
3354 coverage=self.enable_coverage,
3355 extra_args=self.extra_args,
3356 device_testing=self.device_testing,
3357 cmake_only=self.cmake_only,
3358 cleanup=self.cleanup,
3359 valgrind=self.enable_valgrind,
3360 inline_logs=self.inline_logs,
3361 generator=self.generator,
3362 generator_cmd=self.generator_cmd,
3363 verbose=self.verbose,
Anas Nashiff68146f2020-11-28 11:07:06 -05003364 warnings_as_errors=self.warnings_as_errors,
3365 overflow_as_errors=self.overflow_as_errors
Anas Nashif531fe892020-09-11 13:56:33 -04003366 )
3367 pb.process(pipeline, done_queue, task, lock, results)
Anas Nashifce2b4182020-03-24 14:40:28 -04003368
Anas Nashif531fe892020-09-11 13:56:33 -04003369 return True
Anas Nashifdc43c292020-07-09 09:46:45 -04003370
Anas Nashif531fe892020-09-11 13:56:33 -04003371 def execute(self, pipeline, done, results):
3372 lock = Lock()
Anas Nashifce2b4182020-03-24 14:40:28 -04003373 logger.info("Adding tasks to the queue...")
Anas Nashif531fe892020-09-11 13:56:33 -04003374 self.add_tasks_to_queue(pipeline, self.build_only, self.test_only)
3375 logger.info("Added initial list of jobs to queue")
Anas Nashifce2b4182020-03-24 14:40:28 -04003376
Anas Nashif531fe892020-09-11 13:56:33 -04003377 processes = []
3378 for job in range(self.jobs):
3379 logger.debug(f"Launch process {job}")
3380 p = Process(target=self.pipeline_mgr, args=(pipeline, done, lock, results, ))
3381 processes.append(p)
3382 p.start()
Anas Nashifce2b4182020-03-24 14:40:28 -04003383
Anas Nashif2d489172020-12-09 09:06:57 -05003384 try:
3385 for p in processes:
3386 p.join()
3387 except KeyboardInterrupt:
3388 logger.info("Execution interrupted")
3389 for p in processes:
3390 p.terminate()
Anas Nashifce2b4182020-03-24 14:40:28 -04003391
Anas Nashif531fe892020-09-11 13:56:33 -04003392 # FIXME: This needs to move out.
Anas Nashifce2b4182020-03-24 14:40:28 -04003393 if self.enable_size_report and not self.cmake_only:
3394 # Parallelize size calculation
3395 executor = concurrent.futures.ThreadPoolExecutor(self.jobs)
Anas Nashif531fe892020-09-11 13:56:33 -04003396 futures = [executor.submit(self.calc_one_elf_size, instance)
Anas Nashifce2b4182020-03-24 14:40:28 -04003397 for instance in self.instances.values()]
3398 concurrent.futures.wait(futures)
3399 else:
3400 for instance in self.instances.values():
3401 instance.metrics["ram_size"] = 0
3402 instance.metrics["rom_size"] = 0
3403 instance.metrics["handler_time"] = instance.handler.duration if instance.handler else 0
3404 instance.metrics["unrecognized"] = []
3405
Anas Nashif531fe892020-09-11 13:56:33 -04003406 return results
3407
Anas Nashifce2b4182020-03-24 14:40:28 -04003408 def discard_report(self, filename):
3409
3410 try:
Aastha Groverdcbd9152020-06-16 10:19:51 -07003411 if not self.discards:
Anas Nashif45943702020-12-11 17:55:15 -05003412 raise TwisterRuntimeError("apply_filters() hasn't been run!")
Anas Nashifce2b4182020-03-24 14:40:28 -04003413 except Exception as e:
3414 logger.error(str(e))
3415 sys.exit(2)
Anas Nashifce2b4182020-03-24 14:40:28 -04003416 with open(filename, "wt") as csvfile:
3417 fieldnames = ["test", "arch", "platform", "reason"]
3418 cw = csv.DictWriter(csvfile, fieldnames, lineterminator=os.linesep)
3419 cw.writeheader()
3420 for instance, reason in sorted(self.discards.items()):
3421 rowdict = {"test": instance.testcase.name,
3422 "arch": instance.platform.arch,
3423 "platform": instance.platform.name,
3424 "reason": reason}
3425 cw.writerow(rowdict)
3426
Anas Nashif6915adf2020-04-22 09:39:42 -04003427 def target_report(self, outdir, suffix, append=False):
Anas Nashifce2b4182020-03-24 14:40:28 -04003428 platforms = {inst.platform.name for _, inst in self.instances.items()}
3429 for platform in platforms:
Anas Nashif6915adf2020-04-22 09:39:42 -04003430 if suffix:
3431 filename = os.path.join(outdir,"{}_{}.xml".format(platform, suffix))
3432 else:
3433 filename = os.path.join(outdir,"{}.xml".format(platform))
Maciej Perkowski060e00d2020-10-13 18:59:37 +02003434 self.xunit_report(filename, platform, full_report=True,
3435 append=append, version=self.version)
Anas Nashifce2b4182020-03-24 14:40:28 -04003436
Anas Nashif90415502020-04-11 22:15:04 -04003437
3438 @staticmethod
3439 def process_log(log_file):
3440 filtered_string = ""
3441 if os.path.exists(log_file):
3442 with open(log_file, "rb") as f:
3443 log = f.read().decode("utf-8")
3444 filtered_string = ''.join(filter(lambda x: x in string.printable, log))
3445
3446 return filtered_string
3447
Anas Nashifa53c8132020-05-05 09:32:46 -04003448
Maciej Perkowski725d19b2020-10-13 14:15:12 +02003449 def xunit_report(self, filename, platform=None, full_report=False, append=False, version="NA"):
Anas Nashifa53c8132020-05-05 09:32:46 -04003450 total = 0
Anas Nashifb517b1f2020-12-11 07:38:49 -05003451 fails = passes = errors = skips = 0
Anas Nashifa53c8132020-05-05 09:32:46 -04003452 if platform:
3453 selected = [platform]
Anas Nashifb517b1f2020-12-11 07:38:49 -05003454 logger.info(f"Writing target report for {platform}...")
Anas Nashifa53c8132020-05-05 09:32:46 -04003455 else:
Anas Nashifb517b1f2020-12-11 07:38:49 -05003456 logger.info(f"Writing xunit report {filename}...")
Anas Nashifa53c8132020-05-05 09:32:46 -04003457 selected = self.selected_platforms
Anas Nashif90415502020-04-11 22:15:04 -04003458
Anas Nashif90415502020-04-11 22:15:04 -04003459 if os.path.exists(filename) and append:
3460 tree = ET.parse(filename)
3461 eleTestsuites = tree.getroot()
Anas Nashif90415502020-04-11 22:15:04 -04003462 else:
Anas Nashifce2b4182020-03-24 14:40:28 -04003463 eleTestsuites = ET.Element('testsuites')
Anas Nashifce2b4182020-03-24 14:40:28 -04003464
Anas Nashifa53c8132020-05-05 09:32:46 -04003465 for p in selected:
3466 inst = self.get_platform_instances(p)
3467 fails = 0
3468 passes = 0
3469 errors = 0
3470 skips = 0
3471 duration = 0
3472
3473 for _, instance in inst.items():
3474 handler_time = instance.metrics.get('handler_time', 0)
3475 duration += handler_time
Anas Nashif405f1b62020-07-27 12:27:13 -04003476 if full_report and instance.run:
Anas Nashifa53c8132020-05-05 09:32:46 -04003477 for k in instance.results.keys():
3478 if instance.results[k] == 'PASS':
3479 passes += 1
3480 elif instance.results[k] == 'BLOCK':
3481 errors += 1
Anas Nashifbe1025a2020-10-31 08:04:48 -04003482 elif instance.results[k] == 'SKIP' or instance.status in ['skipped']:
Anas Nashifa53c8132020-05-05 09:32:46 -04003483 skips += 1
3484 else:
3485 fails += 1
3486 else:
Maciej Perkowskif050a992021-02-24 13:43:05 +01003487 if instance.status in ["error", "failed", "timeout", "flash_error"]:
Anas Nashifa53c8132020-05-05 09:32:46 -04003488 if instance.reason in ['build_error', 'handler_crash']:
3489 errors += 1
3490 else:
3491 fails += 1
3492 elif instance.status == 'skipped':
3493 skips += 1
Anas Nashif9e1be4c2020-07-23 08:20:49 -04003494 elif instance.status == 'passed':
Anas Nashifa53c8132020-05-05 09:32:46 -04003495 passes += 1
Anas Nashif9e1be4c2020-07-23 08:20:49 -04003496 else:
Anas Nashif696476e2020-11-24 08:42:46 -05003497 if instance.status:
3498 logger.error(f"{instance.name}: Unknown status {instance.status}")
3499 else:
3500 logger.error(f"{instance.name}: No status")
Anas Nashifa53c8132020-05-05 09:32:46 -04003501
3502 total = (errors + passes + fails + skips)
3503 # do not produce a report if no tests were actually run (only built)
3504 if total == 0:
Anas Nashif90415502020-04-11 22:15:04 -04003505 continue
Anas Nashifce2b4182020-03-24 14:40:28 -04003506
Anas Nashifa53c8132020-05-05 09:32:46 -04003507 run = p
3508 eleTestsuite = None
3509
3510 # When we re-run the tests, we re-use the results and update only with
3511 # the newly run tests.
3512 if os.path.exists(filename) and append:
Anas Nashiff04461e2020-06-29 10:07:02 -04003513 ts = eleTestsuites.findall(f'testsuite/[@name="{p}"]')
3514 if ts:
3515 eleTestsuite = ts[0]
3516 eleTestsuite.attrib['failures'] = "%d" % fails
3517 eleTestsuite.attrib['errors'] = "%d" % errors
Christian Taedckeb2be8042020-08-12 14:21:13 +02003518 eleTestsuite.attrib['skipped'] = "%d" % skips
Anas Nashiff04461e2020-06-29 10:07:02 -04003519 else:
3520 logger.info(f"Did not find any existing results for {p}")
3521 eleTestsuite = ET.SubElement(eleTestsuites, 'testsuite',
3522 name=run, time="%f" % duration,
3523 tests="%d" % (total),
3524 failures="%d" % fails,
Christian Taedckeb2be8042020-08-12 14:21:13 +02003525 errors="%d" % (errors), skipped="%s" % (skips))
Maciej Perkowski725d19b2020-10-13 14:15:12 +02003526 eleTSPropetries = ET.SubElement(eleTestsuite, 'properties')
3527 # Multiple 'property' can be added to 'properties'
3528 # differing by name and value
Maciej Perkowski060e00d2020-10-13 18:59:37 +02003529 ET.SubElement(eleTSPropetries, 'property', name="version", value=version)
Anas Nashiff04461e2020-06-29 10:07:02 -04003530
Anas Nashif90415502020-04-11 22:15:04 -04003531 else:
Anas Nashifa53c8132020-05-05 09:32:46 -04003532 eleTestsuite = ET.SubElement(eleTestsuites, 'testsuite',
3533 name=run, time="%f" % duration,
3534 tests="%d" % (total),
3535 failures="%d" % fails,
Christian Taedckeb2be8042020-08-12 14:21:13 +02003536 errors="%d" % (errors), skipped="%s" % (skips))
Maciej Perkowski725d19b2020-10-13 14:15:12 +02003537 eleTSPropetries = ET.SubElement(eleTestsuite, 'properties')
3538 # Multiple 'property' can be added to 'properties'
3539 # differing by name and value
Maciej Perkowski060e00d2020-10-13 18:59:37 +02003540 ET.SubElement(eleTSPropetries, 'property', name="version", value=version)
Anas Nashif90415502020-04-11 22:15:04 -04003541
Anas Nashifa53c8132020-05-05 09:32:46 -04003542 for _, instance in inst.items():
3543 if full_report:
3544 tname = os.path.basename(instance.testcase.name)
3545 else:
3546 tname = instance.testcase.id
Anas Nashifa53c8132020-05-05 09:32:46 -04003547 handler_time = instance.metrics.get('handler_time', 0)
3548
3549 if full_report:
3550 for k in instance.results.keys():
Anas Nashifa53c8132020-05-05 09:32:46 -04003551 # remove testcases that are being re-run from exiting reports
3552 for tc in eleTestsuite.findall(f'testcase/[@name="{k}"]'):
3553 eleTestsuite.remove(tc)
3554
3555 classname = ".".join(tname.split(".")[:2])
3556 eleTestcase = ET.SubElement(
3557 eleTestsuite, 'testcase',
3558 classname=classname,
3559 name="%s" % (k), time="%f" % handler_time)
Anas Nashif9e1be4c2020-07-23 08:20:49 -04003560 if instance.results[k] in ['FAIL', 'BLOCK'] or \
Anas Nashif405f1b62020-07-27 12:27:13 -04003561 (not instance.run and instance.status in ["error", "failed", "timeout"]):
Anas Nashifa53c8132020-05-05 09:32:46 -04003562 if instance.results[k] == 'FAIL':
3563 el = ET.SubElement(
3564 eleTestcase,
3565 'failure',
3566 type="failure",
3567 message="failed")
3568 else:
3569 el = ET.SubElement(
3570 eleTestcase,
3571 'error',
3572 type="failure",
Maciej Perkowskif050a992021-02-24 13:43:05 +01003573 message=instance.reason)
Jingru Wang99b07202021-01-29 12:42:32 +08003574 log_root = os.path.join(self.outdir, instance.platform.name, instance.testcase.name)
3575 log_file = os.path.join(log_root, "handler.log")
Anas Nashifa53c8132020-05-05 09:32:46 -04003576 el.text = self.process_log(log_file)
3577
Anas Nashif9e1be4c2020-07-23 08:20:49 -04003578 elif instance.results[k] == 'PASS' \
Anas Nashif405f1b62020-07-27 12:27:13 -04003579 or (not instance.run and instance.status in ["passed"]):
Anas Nashiff04461e2020-06-29 10:07:02 -04003580 pass
Anas Nashifbe1025a2020-10-31 08:04:48 -04003581 elif instance.results[k] == 'SKIP' or (instance.status in ["skipped"]):
Maciej Perkowskie3ff4cf2020-07-17 11:13:50 +02003582 el = ET.SubElement(eleTestcase, 'skipped', type="skipped", message=instance.reason)
Anas Nashiff04461e2020-06-29 10:07:02 -04003583 else:
Anas Nashifce2b4182020-03-24 14:40:28 -04003584 el = ET.SubElement(
3585 eleTestcase,
Anas Nashiff04461e2020-06-29 10:07:02 -04003586 'error',
3587 type="error",
3588 message=f"{instance.reason}")
Anas Nashifa53c8132020-05-05 09:32:46 -04003589 else:
3590 if platform:
3591 classname = ".".join(instance.testcase.name.split(".")[:2])
3592 else:
3593 classname = p + ":" + ".".join(instance.testcase.name.split(".")[:2])
Anas Nashifce2b4182020-03-24 14:40:28 -04003594
Anas Nashiff04461e2020-06-29 10:07:02 -04003595 # remove testcases that are being re-run from exiting reports
Sebastian Wezel148dbdc2021-02-22 13:04:56 +01003596 for tc in eleTestsuite.findall(f'testcase/[@classname="{classname}"][@name="{instance.testcase.name}"]'):
Anas Nashiff04461e2020-06-29 10:07:02 -04003597 eleTestsuite.remove(tc)
3598
Anas Nashifa53c8132020-05-05 09:32:46 -04003599 eleTestcase = ET.SubElement(eleTestsuite, 'testcase',
3600 classname=classname,
3601 name="%s" % (instance.testcase.name),
3602 time="%f" % handler_time)
Anas Nashif9e1be4c2020-07-23 08:20:49 -04003603
Maciej Perkowskif050a992021-02-24 13:43:05 +01003604 if instance.status in ["error", "failed", "timeout", "flash_error"]:
Anas Nashifa53c8132020-05-05 09:32:46 -04003605 failure = ET.SubElement(
Anas Nashifce2b4182020-03-24 14:40:28 -04003606 eleTestcase,
Anas Nashifa53c8132020-05-05 09:32:46 -04003607 'failure',
3608 type="failure",
Maciej Perkowskib2fa99c2020-05-21 14:45:29 +02003609 message=instance.reason)
Anas Nashiff04461e2020-06-29 10:07:02 -04003610
Jingru Wang99b07202021-01-29 12:42:32 +08003611 log_root = ("%s/%s/%s" % (self.outdir, instance.platform.name, instance.testcase.name))
3612 bl = os.path.join(log_root, "build.log")
3613 hl = os.path.join(log_root, "handler.log")
Anas Nashifa53c8132020-05-05 09:32:46 -04003614 log_file = bl
3615 if instance.reason != 'Build error':
3616 if os.path.exists(hl):
3617 log_file = hl
3618 else:
3619 log_file = bl
Anas Nashifce2b4182020-03-24 14:40:28 -04003620
Anas Nashifa53c8132020-05-05 09:32:46 -04003621 failure.text = self.process_log(log_file)
Anas Nashifce2b4182020-03-24 14:40:28 -04003622
Anas Nashifa53c8132020-05-05 09:32:46 -04003623 elif instance.status == "skipped":
3624 ET.SubElement(eleTestcase, 'skipped', type="skipped", message="Skipped")
Anas Nashifce2b4182020-03-24 14:40:28 -04003625
3626 result = ET.tostring(eleTestsuites)
3627 with open(filename, 'wb') as report:
3628 report.write(result)
3629
Anas Nashif1c2f1272020-07-23 09:56:01 -04003630 return fails, passes, errors, skips
Anas Nashif90415502020-04-11 22:15:04 -04003631
Anas Nashifce2b4182020-03-24 14:40:28 -04003632 def csv_report(self, filename):
3633 with open(filename, "wt") as csvfile:
3634 fieldnames = ["test", "arch", "platform", "status",
3635 "extra_args", "handler", "handler_time", "ram_size",
3636 "rom_size"]
3637 cw = csv.DictWriter(csvfile, fieldnames, lineterminator=os.linesep)
3638 cw.writeheader()
3639 for instance in self.instances.values():
3640 rowdict = {"test": instance.testcase.name,
3641 "arch": instance.platform.arch,
3642 "platform": instance.platform.name,
3643 "extra_args": " ".join(instance.testcase.extra_args),
3644 "handler": instance.platform.simulation}
3645
3646 rowdict["status"] = instance.status
Anas Nashiff04461e2020-06-29 10:07:02 -04003647 if instance.status not in ["error", "failed", "timeout"]:
Anas Nashifce2b4182020-03-24 14:40:28 -04003648 if instance.handler:
3649 rowdict["handler_time"] = instance.metrics.get("handler_time", 0)
3650 ram_size = instance.metrics.get("ram_size", 0)
3651 rom_size = instance.metrics.get("rom_size", 0)
3652 rowdict["ram_size"] = ram_size
3653 rowdict["rom_size"] = rom_size
3654 cw.writerow(rowdict)
3655
Anas Nashifcd1dccf2020-12-10 15:07:52 -05003656 def json_report(self, filename, append=False, version="NA"):
Anas Nashifb517b1f2020-12-11 07:38:49 -05003657 logger.info(f"Writing JSON report {filename}")
Anas Nashifcd1dccf2020-12-10 15:07:52 -05003658 report = {}
3659 selected = self.selected_platforms
3660 report["environment"] = {"os": os.name,
3661 "zephyr_version": version,
3662 "toolchain": self.get_toolchain()
3663 }
3664 json_data = {}
3665 if os.path.exists(filename) and append:
3666 with open(filename, 'r') as json_file:
3667 json_data = json.load(json_file)
Spoorthy Priya Yeraboluf8f220a2020-09-23 06:28:50 -07003668
Anas Nashifcd1dccf2020-12-10 15:07:52 -05003669 suites = json_data.get("testsuites", [])
3670 if suites:
3671 suite = suites[0]
3672 testcases = suite.get("testcases", [])
Spoorthy Priya Yeraboluf8f220a2020-09-23 06:28:50 -07003673 else:
Anas Nashifcd1dccf2020-12-10 15:07:52 -05003674 suite = {}
3675 testcases = []
Spoorthy Priya Yeraboluf8f220a2020-09-23 06:28:50 -07003676
Spoorthy Priya Yeraboluf8f220a2020-09-23 06:28:50 -07003677 for p in selected:
Spoorthy Priya Yeraboluf8f220a2020-09-23 06:28:50 -07003678 inst = self.get_platform_instances(p)
Spoorthy Priya Yeraboluf8f220a2020-09-23 06:28:50 -07003679 for _, instance in inst.items():
Anas Nashifcd1dccf2020-12-10 15:07:52 -05003680 testcase = {}
Spoorthy Priya Yeraboluf8f220a2020-09-23 06:28:50 -07003681 handler_log = os.path.join(instance.build_dir, "handler.log")
3682 build_log = os.path.join(instance.build_dir, "build.log")
3683 device_log = os.path.join(instance.build_dir, "device.log")
3684
3685 handler_time = instance.metrics.get('handler_time', 0)
3686 ram_size = instance.metrics.get ("ram_size", 0)
3687 rom_size = instance.metrics.get("rom_size",0)
Anas Nashifcd1dccf2020-12-10 15:07:52 -05003688 for k in instance.results.keys():
3689 testcases = list(filter(lambda d: not (d.get('testcase') == k and d.get('platform') == p), testcases ))
3690 testcase = {"testcase": k,
3691 "arch": instance.platform.arch,
Anas Nashifcd1dccf2020-12-10 15:07:52 -05003692 "platform": p,
3693 }
Maciej Perkowski484ef672021-04-15 11:37:39 +02003694 if ram_size:
3695 testcase["ram_size"] = ram_size
3696 if rom_size:
3697 testcase["rom_size"] = rom_size
3698
Anas Nashif55c3dde2021-09-16 08:54:19 -04003699 if instance.results[k] in ["PASS"] or instance.status == 'passed':
Anas Nashifcd1dccf2020-12-10 15:07:52 -05003700 testcase["status"] = "passed"
3701 if instance.handler:
3702 testcase["execution_time"] = handler_time
Anas Nashifcd1dccf2020-12-10 15:07:52 -05003703
Anas Nashif55c3dde2021-09-16 08:54:19 -04003704 elif instance.results[k] in ['FAIL', 'BLOCK'] or instance.status in ["error", "failed", "timeout", "flash_error"]:
Anas Nashifcd1dccf2020-12-10 15:07:52 -05003705 testcase["status"] = "failed"
3706 testcase["reason"] = instance.reason
3707 testcase["execution_time"] = handler_time
Spoorthy Priya Yeraboluf8f220a2020-09-23 06:28:50 -07003708 if os.path.exists(handler_log):
Anas Nashifcd1dccf2020-12-10 15:07:52 -05003709 testcase["test_output"] = self.process_log(handler_log)
Spoorthy Priya Yeraboluf8f220a2020-09-23 06:28:50 -07003710 elif os.path.exists(device_log):
Anas Nashifcd1dccf2020-12-10 15:07:52 -05003711 testcase["device_log"] = self.process_log(device_log)
Spoorthy Priya Yeraboluf8f220a2020-09-23 06:28:50 -07003712 else:
Anas Nashifcd1dccf2020-12-10 15:07:52 -05003713 testcase["build_log"] = self.process_log(build_log)
Anas Nashif55c3dde2021-09-16 08:54:19 -04003714 elif instance.status == 'skipped':
Anas Nashifcd1dccf2020-12-10 15:07:52 -05003715 testcase["status"] = "skipped"
3716 testcase["reason"] = instance.reason
3717 testcases.append(testcase)
Spoorthy Priya Yeraboluf8f220a2020-09-23 06:28:50 -07003718
Anas Nashifcd1dccf2020-12-10 15:07:52 -05003719 suites = [ {"testcases": testcases} ]
3720 report["testsuites"] = suites
Spoorthy Priya Yeraboluf8f220a2020-09-23 06:28:50 -07003721
3722 with open(filename, "wt") as json_file:
Anas Nashifcd1dccf2020-12-10 15:07:52 -05003723 json.dump(report, json_file, indent=4, separators=(',',':'))
Spoorthy Priya Yeraboluf8f220a2020-09-23 06:28:50 -07003724
Anas Nashifce2b4182020-03-24 14:40:28 -04003725 def get_testcase(self, identifier):
3726 results = []
3727 for _, tc in self.testcases.items():
3728 for case in tc.cases:
3729 if case == identifier:
3730 results.append(tc)
3731 return results
3732
Anas Nashifce2b4182020-03-24 14:40:28 -04003733class CoverageTool:
3734 """ Base class for every supported coverage tool
3735 """
3736
3737 def __init__(self):
Anas Nashiff6462a32020-03-29 19:02:51 -04003738 self.gcov_tool = None
3739 self.base_dir = None
Anas Nashifce2b4182020-03-24 14:40:28 -04003740
3741 @staticmethod
3742 def factory(tool):
3743 if tool == 'lcov':
Anas Nashiff6462a32020-03-29 19:02:51 -04003744 t = Lcov()
3745 elif tool == 'gcovr':
Marcin Niestroj2652dc72020-08-10 22:27:03 +02003746 t = Gcovr()
Anas Nashiff6462a32020-03-29 19:02:51 -04003747 else:
3748 logger.error("Unsupported coverage tool specified: {}".format(tool))
3749 return None
3750
Anas Nashif211ef412021-01-12 07:15:59 -05003751 logger.debug(f"Select {tool} as the coverage tool...")
Anas Nashiff6462a32020-03-29 19:02:51 -04003752 return t
Anas Nashifce2b4182020-03-24 14:40:28 -04003753
3754 @staticmethod
Kentaro Sugimotoe8ab9872021-07-01 16:44:52 +09003755 def retrieve_gcov_data(input_file):
3756 logger.debug("Working on %s" % input_file)
Anas Nashifce2b4182020-03-24 14:40:28 -04003757 extracted_coverage_info = {}
3758 capture_data = False
3759 capture_complete = False
Kentaro Sugimotoe8ab9872021-07-01 16:44:52 +09003760 with open(input_file, 'r') as fp:
Anas Nashifce2b4182020-03-24 14:40:28 -04003761 for line in fp.readlines():
3762 if re.search("GCOV_COVERAGE_DUMP_START", line):
3763 capture_data = True
3764 continue
3765 if re.search("GCOV_COVERAGE_DUMP_END", line):
3766 capture_complete = True
3767 break
3768 # Loop until the coverage data is found.
3769 if not capture_data:
3770 continue
3771 if line.startswith("*"):
3772 sp = line.split("<")
3773 if len(sp) > 1:
3774 # Remove the leading delimiter "*"
3775 file_name = sp[0][1:]
3776 # Remove the trailing new line char
3777 hex_dump = sp[1][:-1]
3778 else:
3779 continue
3780 else:
3781 continue
3782 extracted_coverage_info.update({file_name: hex_dump})
3783 if not capture_data:
3784 capture_complete = True
3785 return {'complete': capture_complete, 'data': extracted_coverage_info}
3786
3787 @staticmethod
3788 def create_gcda_files(extracted_coverage_info):
Anas Nashiff6462a32020-03-29 19:02:51 -04003789 logger.debug("Generating gcda files")
Anas Nashifce2b4182020-03-24 14:40:28 -04003790 for filename, hexdump_val in extracted_coverage_info.items():
3791 # if kobject_hash is given for coverage gcovr fails
3792 # hence skipping it problem only in gcovr v4.1
3793 if "kobject_hash" in filename:
3794 filename = (filename[:-4]) + "gcno"
3795 try:
3796 os.remove(filename)
3797 except Exception:
3798 pass
3799 continue
3800
3801 with open(filename, 'wb') as fp:
3802 fp.write(bytes.fromhex(hexdump_val))
3803
3804 def generate(self, outdir):
3805 for filename in glob.glob("%s/**/handler.log" % outdir, recursive=True):
3806 gcov_data = self.__class__.retrieve_gcov_data(filename)
3807 capture_complete = gcov_data['complete']
3808 extracted_coverage_info = gcov_data['data']
3809 if capture_complete:
3810 self.__class__.create_gcda_files(extracted_coverage_info)
3811 logger.debug("Gcov data captured: {}".format(filename))
3812 else:
3813 logger.error("Gcov data capture incomplete: {}".format(filename))
3814
3815 with open(os.path.join(outdir, "coverage.log"), "a") as coveragelog:
3816 ret = self._generate(outdir, coveragelog)
3817 if ret == 0:
3818 logger.info("HTML report generated: {}".format(
3819 os.path.join(outdir, "coverage", "index.html")))
3820
3821
3822class Lcov(CoverageTool):
3823
3824 def __init__(self):
3825 super().__init__()
3826 self.ignores = []
3827
3828 def add_ignore_file(self, pattern):
3829 self.ignores.append('*' + pattern + '*')
3830
3831 def add_ignore_directory(self, pattern):
Marcin Niestrojfc674092020-08-10 23:22:11 +02003832 self.ignores.append('*/' + pattern + '/*')
Anas Nashifce2b4182020-03-24 14:40:28 -04003833
3834 def _generate(self, outdir, coveragelog):
3835 coveragefile = os.path.join(outdir, "coverage.info")
3836 ztestfile = os.path.join(outdir, "ztest.info")
Anas Nashif211ef412021-01-12 07:15:59 -05003837 cmd = ["lcov", "--gcov-tool", self.gcov_tool,
Anas Nashifce2b4182020-03-24 14:40:28 -04003838 "--capture", "--directory", outdir,
3839 "--rc", "lcov_branch_coverage=1",
Anas Nashif211ef412021-01-12 07:15:59 -05003840 "--output-file", coveragefile]
3841 cmd_str = " ".join(cmd)
3842 logger.debug(f"Running {cmd_str}...")
3843 subprocess.call(cmd, stdout=coveragelog)
Anas Nashifce2b4182020-03-24 14:40:28 -04003844 # We want to remove tests/* and tests/ztest/test/* but save tests/ztest
3845 subprocess.call(["lcov", "--gcov-tool", self.gcov_tool, "--extract",
3846 coveragefile,
Anas Nashiff6462a32020-03-29 19:02:51 -04003847 os.path.join(self.base_dir, "tests", "ztest", "*"),
Anas Nashifce2b4182020-03-24 14:40:28 -04003848 "--output-file", ztestfile,
3849 "--rc", "lcov_branch_coverage=1"], stdout=coveragelog)
3850
3851 if os.path.exists(ztestfile) and os.path.getsize(ztestfile) > 0:
3852 subprocess.call(["lcov", "--gcov-tool", self.gcov_tool, "--remove",
3853 ztestfile,
Anas Nashiff6462a32020-03-29 19:02:51 -04003854 os.path.join(self.base_dir, "tests/ztest/test/*"),
Anas Nashifce2b4182020-03-24 14:40:28 -04003855 "--output-file", ztestfile,
3856 "--rc", "lcov_branch_coverage=1"],
3857 stdout=coveragelog)
3858 files = [coveragefile, ztestfile]
3859 else:
3860 files = [coveragefile]
3861
3862 for i in self.ignores:
3863 subprocess.call(
3864 ["lcov", "--gcov-tool", self.gcov_tool, "--remove",
3865 coveragefile, i, "--output-file",
3866 coveragefile, "--rc", "lcov_branch_coverage=1"],
3867 stdout=coveragelog)
3868
3869 # The --ignore-errors source option is added to avoid it exiting due to
3870 # samples/application_development/external_lib/
3871 return subprocess.call(["genhtml", "--legend", "--branch-coverage",
3872 "--ignore-errors", "source",
3873 "-output-directory",
3874 os.path.join(outdir, "coverage")] + files,
3875 stdout=coveragelog)
3876
3877
3878class Gcovr(CoverageTool):
3879
3880 def __init__(self):
3881 super().__init__()
3882 self.ignores = []
3883
3884 def add_ignore_file(self, pattern):
3885 self.ignores.append('.*' + pattern + '.*')
3886
3887 def add_ignore_directory(self, pattern):
Marcin Niestrojfc674092020-08-10 23:22:11 +02003888 self.ignores.append(".*/" + pattern + '/.*')
Anas Nashifce2b4182020-03-24 14:40:28 -04003889
3890 @staticmethod
3891 def _interleave_list(prefix, list):
3892 tuple_list = [(prefix, item) for item in list]
3893 return [item for sublist in tuple_list for item in sublist]
3894
3895 def _generate(self, outdir, coveragelog):
3896 coveragefile = os.path.join(outdir, "coverage.json")
3897 ztestfile = os.path.join(outdir, "ztest.json")
3898
3899 excludes = Gcovr._interleave_list("-e", self.ignores)
3900
3901 # We want to remove tests/* and tests/ztest/test/* but save tests/ztest
Anas Nashif211ef412021-01-12 07:15:59 -05003902 cmd = ["gcovr", "-r", self.base_dir, "--gcov-executable",
3903 self.gcov_tool, "-e", "tests/*"] + excludes + ["--json", "-o",
3904 coveragefile, outdir]
3905 cmd_str = " ".join(cmd)
3906 logger.debug(f"Running {cmd_str}...")
3907 subprocess.call(cmd, stdout=coveragelog)
Anas Nashifce2b4182020-03-24 14:40:28 -04003908
Anas Nashiff6462a32020-03-29 19:02:51 -04003909 subprocess.call(["gcovr", "-r", self.base_dir, "--gcov-executable",
Anas Nashifce2b4182020-03-24 14:40:28 -04003910 self.gcov_tool, "-f", "tests/ztest", "-e",
3911 "tests/ztest/test/*", "--json", "-o", ztestfile,
3912 outdir], stdout=coveragelog)
3913
3914 if os.path.exists(ztestfile) and os.path.getsize(ztestfile) > 0:
3915 files = [coveragefile, ztestfile]
3916 else:
3917 files = [coveragefile]
3918
3919 subdir = os.path.join(outdir, "coverage")
3920 os.makedirs(subdir, exist_ok=True)
3921
3922 tracefiles = self._interleave_list("--add-tracefile", files)
3923
Anas Nashiff6462a32020-03-29 19:02:51 -04003924 return subprocess.call(["gcovr", "-r", self.base_dir, "--html",
Anas Nashifce2b4182020-03-24 14:40:28 -04003925 "--html-details"] + tracefiles +
3926 ["-o", os.path.join(subdir, "index.html")],
3927 stdout=coveragelog)
Anas Nashifce2b4182020-03-24 14:40:28 -04003928
Anas Nashif8305d1b2020-11-26 11:55:02 -05003929class DUT(object):
Anas Nashif531fe892020-09-11 13:56:33 -04003930 def __init__(self,
3931 id=None,
3932 serial=None,
3933 platform=None,
3934 product=None,
3935 serial_pty=None,
Anas Nashif531fe892020-09-11 13:56:33 -04003936 connected=False,
3937 pre_script=None,
Anas Nashif8305d1b2020-11-26 11:55:02 -05003938 post_script=None,
3939 post_flash_script=None,
Anas Nashif531fe892020-09-11 13:56:33 -04003940 runner=None):
3941
3942 self.serial = serial
3943 self.platform = platform
3944 self.serial_pty = serial_pty
3945 self._counter = Value("i", 0)
3946 self._available = Value("i", 1)
3947 self.connected = connected
3948 self.pre_script = pre_script
3949 self.id = id
3950 self.product = product
3951 self.runner = runner
3952 self.fixtures = []
Anas Nashif8305d1b2020-11-26 11:55:02 -05003953 self.post_flash_script = post_flash_script
3954 self.post_script = post_script
3955 self.pre_script = pre_script
Anas Nashif531fe892020-09-11 13:56:33 -04003956 self.probe_id = None
3957 self.notes = None
Andy Ross098fce32021-03-02 05:13:07 -08003958 self.lock = Lock()
Anas Nashif531fe892020-09-11 13:56:33 -04003959 self.match = False
3960
3961
3962 @property
3963 def available(self):
3964 with self._available.get_lock():
3965 return self._available.value
3966
3967 @available.setter
3968 def available(self, value):
3969 with self._available.get_lock():
3970 self._available.value = value
3971
3972 @property
3973 def counter(self):
3974 with self._counter.get_lock():
3975 return self._counter.value
3976
3977 @counter.setter
3978 def counter(self, value):
3979 with self._counter.get_lock():
3980 self._counter.value = value
3981
Anas Nashif4fa82c42020-12-15 10:08:28 -05003982 def to_dict(self):
3983 d = {}
3984 exclude = ['_available', '_counter', 'match']
3985 v = vars(self)
3986 for k in v.keys():
3987 if k not in exclude and v[k]:
3988 d[k] = v[k]
3989 return d
3990
3991
Anas Nashif531fe892020-09-11 13:56:33 -04003992 def __repr__(self):
3993 return f"<{self.platform} ({self.product}) on {self.serial}>"
3994
3995class HardwareMap:
Anas Nashifc24bf6f2020-12-07 11:18:07 -05003996 schema_path = os.path.join(ZEPHYR_BASE, "scripts", "schemas", "twister", "hwmap-schema.yaml")
Anas Nashifce2b4182020-03-24 14:40:28 -04003997
3998 manufacturer = [
3999 'ARM',
4000 'SEGGER',
4001 'MBED',
4002 'STMicroelectronics',
4003 'Atmel Corp.',
4004 'Texas Instruments',
4005 'Silicon Labs',
4006 'NXP Semiconductors',
4007 'Microchip Technology Inc.',
4008 'FTDI',
4009 'Digilent'
4010 ]
4011
4012 runner_mapping = {
4013 'pyocd': [
4014 'DAPLink CMSIS-DAP',
4015 'MBED CMSIS-DAP'
4016 ],
4017 'jlink': [
4018 'J-Link',
4019 'J-Link OB'
4020 ],
4021 'openocd': [
Erwan Gouriou2339fa02020-07-07 17:15:22 +02004022 'STM32 STLink', '^XDS110.*', 'STLINK-V3'
Anas Nashifce2b4182020-03-24 14:40:28 -04004023 ],
4024 'dediprog': [
4025 'TTL232R-3V3',
4026 'MCP2200 USB Serial Port Emulator'
4027 ]
4028 }
4029
4030 def __init__(self):
4031 self.detected = []
Anas Nashif8305d1b2020-11-26 11:55:02 -05004032 self.duts = []
Anas Nashifce2b4182020-03-24 14:40:28 -04004033
Anas Nashifad70a692020-11-26 09:19:41 -05004034 def add_device(self, serial, platform, pre_script, is_pty):
Anas Nashif8305d1b2020-11-26 11:55:02 -05004035 device = DUT(platform=platform, connected=True, pre_script=pre_script)
Andrei Emeltchenkod8b845b2020-03-31 13:22:30 +03004036
4037 if is_pty:
Anas Nashif531fe892020-09-11 13:56:33 -04004038 device.serial_pty = serial
Andrei Emeltchenkod8b845b2020-03-31 13:22:30 +03004039 else:
Anas Nashif531fe892020-09-11 13:56:33 -04004040 device.serial = serial
Andrei Emeltchenkod8b845b2020-03-31 13:22:30 +03004041
Anas Nashif8305d1b2020-11-26 11:55:02 -05004042 self.duts.append(device)
Anas Nashifce2b4182020-03-24 14:40:28 -04004043
Anas Nashifad70a692020-11-26 09:19:41 -05004044 def load(self, map_file):
Anas Nashifce2b4182020-03-24 14:40:28 -04004045 hwm_schema = scl.yaml_load(self.schema_path)
Anas Nashif8305d1b2020-11-26 11:55:02 -05004046 duts = scl.yaml_load_verify(map_file, hwm_schema)
4047 for dut in duts:
4048 pre_script = dut.get('pre_script')
4049 post_script = dut.get('post_script')
4050 post_flash_script = dut.get('post_flash_script')
4051 platform = dut.get('platform')
4052 id = dut.get('id')
4053 runner = dut.get('runner')
4054 serial = dut.get('serial')
4055 product = dut.get('product')
Anas Nashifce954d42021-02-04 09:31:07 -05004056 fixtures = dut.get('fixtures', [])
Anas Nashif8305d1b2020-11-26 11:55:02 -05004057 new_dut = DUT(platform=platform,
4058 product=product,
4059 runner=runner,
4060 id=id,
4061 serial=serial,
4062 connected=serial is not None,
4063 pre_script=pre_script,
4064 post_script=post_script,
4065 post_flash_script=post_flash_script)
Anas Nashifce954d42021-02-04 09:31:07 -05004066 new_dut.fixtures = fixtures
Anas Nashif8305d1b2020-11-26 11:55:02 -05004067 new_dut.counter = 0
4068 self.duts.append(new_dut)
Anas Nashifce2b4182020-03-24 14:40:28 -04004069
Anas Nashifad70a692020-11-26 09:19:41 -05004070 def scan(self, persistent=False):
Anas Nashifce2b4182020-03-24 14:40:28 -04004071 from serial.tools import list_ports
4072
Martí Bolívar07dce822020-04-13 16:50:51 -07004073 if persistent and platform.system() == 'Linux':
4074 # On Linux, /dev/serial/by-id provides symlinks to
4075 # '/dev/ttyACMx' nodes using names which are unique as
4076 # long as manufacturers fill out USB metadata nicely.
4077 #
4078 # This creates a map from '/dev/ttyACMx' device nodes
4079 # to '/dev/serial/by-id/usb-...' symlinks. The symlinks
4080 # go into the hardware map because they stay the same
4081 # even when the user unplugs / replugs the device.
4082 #
4083 # Some inexpensive USB/serial adapters don't result
4084 # in unique names here, though, so use of this feature
4085 # requires explicitly setting persistent=True.
4086 by_id = Path('/dev/serial/by-id')
4087 def readlink(link):
4088 return str((by_id / link).resolve())
4089
4090 persistent_map = {readlink(link): str(link)
4091 for link in by_id.iterdir()}
4092 else:
4093 persistent_map = {}
4094
Anas Nashifce2b4182020-03-24 14:40:28 -04004095 serial_devices = list_ports.comports()
4096 logger.info("Scanning connected hardware...")
4097 for d in serial_devices:
4098 if d.manufacturer in self.manufacturer:
4099
4100 # TI XDS110 can have multiple serial devices for a single board
4101 # assume endpoint 0 is the serial, skip all others
4102 if d.manufacturer == 'Texas Instruments' and not d.location.endswith('0'):
4103 continue
Anas Nashif8305d1b2020-11-26 11:55:02 -05004104 s_dev = DUT(platform="unknown",
Anas Nashif531fe892020-09-11 13:56:33 -04004105 id=d.serial_number,
4106 serial=persistent_map.get(d.device, d.device),
4107 product=d.product,
Anas Nashif0e64b872021-05-17 09:45:03 -04004108 runner='unknown',
4109 connected=True)
Anas Nashif531fe892020-09-11 13:56:33 -04004110
Anas Nashifce2b4182020-03-24 14:40:28 -04004111 for runner, _ in self.runner_mapping.items():
4112 products = self.runner_mapping.get(runner)
4113 if d.product in products:
Anas Nashif531fe892020-09-11 13:56:33 -04004114 s_dev.runner = runner
Anas Nashifce2b4182020-03-24 14:40:28 -04004115 continue
4116 # Try regex matching
4117 for p in products:
4118 if re.match(p, d.product):
Anas Nashif531fe892020-09-11 13:56:33 -04004119 s_dev.runner = runner
Anas Nashifce2b4182020-03-24 14:40:28 -04004120
Anas Nashif531fe892020-09-11 13:56:33 -04004121 s_dev.connected = True
Anas Nashifce2b4182020-03-24 14:40:28 -04004122 self.detected.append(s_dev)
4123 else:
4124 logger.warning("Unsupported device (%s): %s" % (d.manufacturer, d))
4125
Anas Nashifad70a692020-11-26 09:19:41 -05004126 def save(self, hwm_file):
Anas Nashifce2b4182020-03-24 14:40:28 -04004127 # use existing map
Anas Nashif4fa82c42020-12-15 10:08:28 -05004128 self.detected.sort(key=lambda x: x.serial or '')
Anas Nashifce2b4182020-03-24 14:40:28 -04004129 if os.path.exists(hwm_file):
4130 with open(hwm_file, 'r') as yaml_file:
Anas Nashifae61b7e2020-07-06 11:30:55 -04004131 hwm = yaml.load(yaml_file, Loader=SafeLoader)
Anas Nashif4fa82c42020-12-15 10:08:28 -05004132 if hwm:
4133 hwm.sort(key=lambda x: x['serial'] or '')
Øyvind Rønningstad4813f462020-07-01 16:49:38 +02004134
Anas Nashif4fa82c42020-12-15 10:08:28 -05004135 # disconnect everything
Anas Nashifce2b4182020-03-24 14:40:28 -04004136 for h in hwm:
Anas Nashif4fa82c42020-12-15 10:08:28 -05004137 h['connected'] = False
4138 h['serial'] = None
Anas Nashifce2b4182020-03-24 14:40:28 -04004139
Anas Nashif4fa82c42020-12-15 10:08:28 -05004140 for _detected in self.detected:
4141 for h in hwm:
4142 if _detected.id == h['id'] and _detected.product == h['product'] and not _detected.match:
4143 h['connected'] = True
4144 h['serial'] = _detected.serial
4145 _detected.match = True
4146
4147 new_duts = list(filter(lambda d: not d.match, self.detected))
4148 new = []
4149 for d in new_duts:
4150 new.append(d.to_dict())
4151
4152 if hwm:
4153 hwm = hwm + new
4154 else:
4155 hwm = new
Anas Nashifce2b4182020-03-24 14:40:28 -04004156
Anas Nashifce2b4182020-03-24 14:40:28 -04004157 with open(hwm_file, 'w') as yaml_file:
Anas Nashifae61b7e2020-07-06 11:30:55 -04004158 yaml.dump(hwm, yaml_file, Dumper=Dumper, default_flow_style=False)
Anas Nashifce2b4182020-03-24 14:40:28 -04004159
Anas Nashifad70a692020-11-26 09:19:41 -05004160 self.load(hwm_file)
4161 logger.info("Registered devices:")
4162 self.dump()
4163
Anas Nashifce2b4182020-03-24 14:40:28 -04004164 else:
4165 # create new file
Anas Nashifad70a692020-11-26 09:19:41 -05004166 dl = []
4167 for _connected in self.detected:
4168 platform = _connected.platform
4169 id = _connected.id
4170 runner = _connected.runner
4171 serial = _connected.serial
4172 product = _connected.product
4173 d = {
4174 'platform': platform,
4175 'id': id,
4176 'runner': runner,
4177 'serial': serial,
Anas Nashif0e64b872021-05-17 09:45:03 -04004178 'product': product,
4179 'connected': _connected.connected
Anas Nashifad70a692020-11-26 09:19:41 -05004180 }
4181 dl.append(d)
Anas Nashifce2b4182020-03-24 14:40:28 -04004182 with open(hwm_file, 'w') as yaml_file:
Anas Nashifad70a692020-11-26 09:19:41 -05004183 yaml.dump(dl, yaml_file, Dumper=Dumper, default_flow_style=False)
Anas Nashifce2b4182020-03-24 14:40:28 -04004184 logger.info("Detected devices:")
Anas Nashifad70a692020-11-26 09:19:41 -05004185 self.dump(detected=True)
Anas Nashifce2b4182020-03-24 14:40:28 -04004186
Anas Nashif531fe892020-09-11 13:56:33 -04004187 def dump(self, filtered=[], header=[], connected_only=False, detected=False):
Anas Nashifce2b4182020-03-24 14:40:28 -04004188 print("")
4189 table = []
Anas Nashif531fe892020-09-11 13:56:33 -04004190 if detected:
4191 to_show = self.detected
4192 else:
Anas Nashif8305d1b2020-11-26 11:55:02 -05004193 to_show = self.duts
Anas Nashifad70a692020-11-26 09:19:41 -05004194
Anas Nashifce2b4182020-03-24 14:40:28 -04004195 if not header:
4196 header = ["Platform", "ID", "Serial device"]
Anas Nashif531fe892020-09-11 13:56:33 -04004197 for p in to_show:
4198 platform = p.platform
4199 connected = p.connected
Anas Nashifce2b4182020-03-24 14:40:28 -04004200 if filtered and platform not in filtered:
4201 continue
4202
4203 if not connected_only or connected:
Anas Nashif531fe892020-09-11 13:56:33 -04004204 table.append([platform, p.id, p.serial])
Anas Nashifce2b4182020-03-24 14:40:28 -04004205
4206 print(tabulate(table, headers=header, tablefmt="github"))