blob: dc49f6e3e6debbd9e7366472fd22e2d8088918cf [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
21from threading import BoundedSemaphore
22import queue
23import time
24import csv
25import glob
26import concurrent
27import xml.etree.ElementTree as ET
28import logging
Andrei Emeltchenkod8b845b2020-03-31 13:22:30 +030029import pty
Anas Nashifce2b4182020-03-24 14:40:28 -040030from pathlib import Path
Martí Bolívar9861e5d2020-07-23 10:02:19 -070031import traceback
Anas Nashifce2b4182020-03-24 14:40:28 -040032from distutils.spawn import find_executable
33from colorama import Fore
Martí Bolívar9c92baa2020-07-08 14:43:07 -070034import pickle
Martí Bolívar07dce822020-04-13 16:50:51 -070035import platform
Anas Nashifae61b7e2020-07-06 11:30:55 -040036import yaml
37try:
38 # Use the C LibYAML parser if available, rather than the Python parser.
39 # It's much faster.
Anas Nashifae61b7e2020-07-06 11:30:55 -040040 from yaml import CSafeLoader as SafeLoader
41 from yaml import CDumper as Dumper
42except ImportError:
Martí Bolívard8698cb2020-07-08 14:55:14 -070043 from yaml import SafeLoader, Dumper
Anas Nashifce2b4182020-03-24 14:40:28 -040044
45try:
46 import serial
47except ImportError:
48 print("Install pyserial python module with pip to use --device-testing option.")
49
50try:
51 from tabulate import tabulate
52except ImportError:
53 print("Install tabulate python module with pip to use --device-testing option.")
54
Wentong Wu0d619ae2020-05-05 19:46:49 -040055try:
56 import psutil
57except ImportError:
Anas Nashif77946fa2020-05-21 18:19:01 -040058 print("Install psutil python module with pip to run in Qemu.")
Wentong Wu0d619ae2020-05-05 19:46:49 -040059
Anas Nashifce2b4182020-03-24 14:40:28 -040060ZEPHYR_BASE = os.getenv("ZEPHYR_BASE")
61if not ZEPHYR_BASE:
62 sys.exit("$ZEPHYR_BASE environment variable undefined")
63
Martí Bolívar9c92baa2020-07-08 14:43:07 -070064# This is needed to load edt.pickle files.
Anas Nashifce2b4182020-03-24 14:40:28 -040065sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts", "dts"))
Martí Bolívar469f53c2020-07-24 11:19:53 -070066import edtlib # pylint: disable=unused-import
Anas Nashifce2b4182020-03-24 14:40:28 -040067
68hw_map_local = threading.Lock()
69report_lock = threading.Lock()
70
71# Use this for internal comparisons; that's what canonicalization is
72# for. Don't use it when invoking other components of the build system
73# to avoid confusing and hard to trace inconsistencies in error messages
74# and logs, generated Makefiles, etc. compared to when users invoke these
75# components directly.
76# Note "normalization" is different from canonicalization, see os.path.
77canonical_zephyr_base = os.path.realpath(ZEPHYR_BASE)
78
79sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts/"))
80
81from sanity_chk import scl
82from sanity_chk import expr_parser
83
Anas Nashifce2b4182020-03-24 14:40:28 -040084logger = logging.getLogger('sanitycheck')
85logger.setLevel(logging.DEBUG)
86
Anas Nashifce2b4182020-03-24 14:40:28 -040087pipeline = queue.LifoQueue()
88
89class CMakeCacheEntry:
90 '''Represents a CMake cache entry.
91
92 This class understands the type system in a CMakeCache.txt, and
93 converts the following cache types to Python types:
94
95 Cache Type Python type
96 ---------- -------------------------------------------
97 FILEPATH str
98 PATH str
99 STRING str OR list of str (if ';' is in the value)
100 BOOL bool
101 INTERNAL str OR list of str (if ';' is in the value)
102 ---------- -------------------------------------------
103 '''
104
105 # Regular expression for a cache entry.
106 #
107 # CMake variable names can include escape characters, allowing a
108 # wider set of names than is easy to match with a regular
109 # expression. To be permissive here, use a non-greedy match up to
110 # the first colon (':'). This breaks if the variable name has a
111 # colon inside, but it's good enough.
112 CACHE_ENTRY = re.compile(
113 r'''(?P<name>.*?) # name
114 :(?P<type>FILEPATH|PATH|STRING|BOOL|INTERNAL) # type
115 =(?P<value>.*) # value
116 ''', re.X)
117
118 @classmethod
119 def _to_bool(cls, val):
120 # Convert a CMake BOOL string into a Python bool.
121 #
122 # "True if the constant is 1, ON, YES, TRUE, Y, or a
123 # non-zero number. False if the constant is 0, OFF, NO,
124 # FALSE, N, IGNORE, NOTFOUND, the empty string, or ends in
125 # the suffix -NOTFOUND. Named boolean constants are
126 # case-insensitive. If the argument is not one of these
127 # constants, it is treated as a variable."
128 #
129 # https://cmake.org/cmake/help/v3.0/command/if.html
130 val = val.upper()
131 if val in ('ON', 'YES', 'TRUE', 'Y'):
132 return 1
133 elif val in ('OFF', 'NO', 'FALSE', 'N', 'IGNORE', 'NOTFOUND', ''):
134 return 0
135 elif val.endswith('-NOTFOUND'):
136 return 0
137 else:
138 try:
139 v = int(val)
140 return v != 0
141 except ValueError as exc:
142 raise ValueError('invalid bool {}'.format(val)) from exc
143
144 @classmethod
145 def from_line(cls, line, line_no):
146 # Comments can only occur at the beginning of a line.
147 # (The value of an entry could contain a comment character).
148 if line.startswith('//') or line.startswith('#'):
149 return None
150
151 # Whitespace-only lines do not contain cache entries.
152 if not line.strip():
153 return None
154
155 m = cls.CACHE_ENTRY.match(line)
156 if not m:
157 return None
158
159 name, type_, value = (m.group(g) for g in ('name', 'type', 'value'))
160 if type_ == 'BOOL':
161 try:
162 value = cls._to_bool(value)
163 except ValueError as exc:
164 args = exc.args + ('on line {}: {}'.format(line_no, line),)
165 raise ValueError(args) from exc
166 elif type_ in ['STRING', 'INTERNAL']:
167 # If the value is a CMake list (i.e. is a string which
168 # contains a ';'), convert to a Python list.
169 if ';' in value:
170 value = value.split(';')
171
172 return CMakeCacheEntry(name, value)
173
174 def __init__(self, name, value):
175 self.name = name
176 self.value = value
177
178 def __str__(self):
179 fmt = 'CMakeCacheEntry(name={}, value={})'
180 return fmt.format(self.name, self.value)
181
182
183class CMakeCache:
184 '''Parses and represents a CMake cache file.'''
185
186 @staticmethod
187 def from_file(cache_file):
188 return CMakeCache(cache_file)
189
190 def __init__(self, cache_file):
191 self.cache_file = cache_file
192 self.load(cache_file)
193
194 def load(self, cache_file):
195 entries = []
196 with open(cache_file, 'r') as cache:
197 for line_no, line in enumerate(cache):
198 entry = CMakeCacheEntry.from_line(line, line_no)
199 if entry:
200 entries.append(entry)
201 self._entries = OrderedDict((e.name, e) for e in entries)
202
203 def get(self, name, default=None):
204 entry = self._entries.get(name)
205 if entry is not None:
206 return entry.value
207 else:
208 return default
209
210 def get_list(self, name, default=None):
211 if default is None:
212 default = []
213 entry = self._entries.get(name)
214 if entry is not None:
215 value = entry.value
216 if isinstance(value, list):
217 return value
218 elif isinstance(value, str):
219 return [value] if value else []
220 else:
221 msg = 'invalid value {} type {}'
222 raise RuntimeError(msg.format(value, type(value)))
223 else:
224 return default
225
226 def __contains__(self, name):
227 return name in self._entries
228
229 def __getitem__(self, name):
230 return self._entries[name].value
231
232 def __setitem__(self, name, entry):
233 if not isinstance(entry, CMakeCacheEntry):
234 msg = 'improper type {} for value {}, expecting CMakeCacheEntry'
235 raise TypeError(msg.format(type(entry), entry))
236 self._entries[name] = entry
237
238 def __delitem__(self, name):
239 del self._entries[name]
240
241 def __iter__(self):
242 return iter(self._entries.values())
243
244
245class SanityCheckException(Exception):
246 pass
247
248
249class SanityRuntimeError(SanityCheckException):
250 pass
251
252
253class ConfigurationError(SanityCheckException):
254 def __init__(self, cfile, message):
255 SanityCheckException.__init__(self, cfile + ": " + message)
256
257
258class BuildError(SanityCheckException):
259 pass
260
261
262class ExecutionError(SanityCheckException):
263 pass
264
265
266class HarnessImporter:
267
268 def __init__(self, name):
269 sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts/sanity_chk"))
270 module = __import__("harness")
271 if name:
272 my_class = getattr(module, name)
273 else:
274 my_class = getattr(module, "Test")
275
276 self.instance = my_class()
277
278
279class Handler:
280 def __init__(self, instance, type_str="build"):
281 """Constructor
282
283 """
284 self.lock = threading.Lock()
285
286 self.state = "waiting"
287 self.run = False
288 self.duration = 0
289 self.type_str = type_str
290
291 self.binary = None
292 self.pid_fn = None
293 self.call_make_run = False
294
295 self.name = instance.name
296 self.instance = instance
297 self.timeout = instance.testcase.timeout
298 self.sourcedir = instance.testcase.source_dir
299 self.build_dir = instance.build_dir
300 self.log = os.path.join(self.build_dir, "handler.log")
301 self.returncode = 0
302 self.set_state("running", self.duration)
303 self.generator = None
304 self.generator_cmd = None
305
306 self.args = []
307
308 def set_state(self, state, duration):
309 self.lock.acquire()
310 self.state = state
311 self.duration = duration
312 self.lock.release()
313
314 def get_state(self):
315 self.lock.acquire()
316 ret = (self.state, self.duration)
317 self.lock.release()
318 return ret
319
320 def record(self, harness):
321 if harness.recording:
322 filename = os.path.join(self.build_dir, "recording.csv")
323 with open(filename, "at") as csvfile:
324 cw = csv.writer(csvfile, harness.fieldnames, lineterminator=os.linesep)
325 cw.writerow(harness.fieldnames)
326 for instance in harness.recording:
327 cw.writerow(instance)
328
329
330class BinaryHandler(Handler):
331 def __init__(self, instance, type_str):
332 """Constructor
333
334 @param instance Test Instance
335 """
336 super().__init__(instance, type_str)
337
338 self.terminated = False
339
340 # Tool options
341 self.valgrind = False
342 self.lsan = False
343 self.asan = False
Christian Taedcke3dbe9f22020-07-06 16:00:57 +0200344 self.ubsan = False
Anas Nashifce2b4182020-03-24 14:40:28 -0400345 self.coverage = False
346
347 def try_kill_process_by_pid(self):
348 if self.pid_fn:
349 pid = int(open(self.pid_fn).read())
350 os.unlink(self.pid_fn)
351 self.pid_fn = None # clear so we don't try to kill the binary twice
352 try:
353 os.kill(pid, signal.SIGTERM)
354 except ProcessLookupError:
355 pass
356
357 def terminate(self, proc):
358 # encapsulate terminate functionality so we do it consistently where ever
359 # we might want to terminate the proc. We need try_kill_process_by_pid
360 # because of both how newer ninja (1.6.0 or greater) and .NET / renode
361 # work. Newer ninja's don't seem to pass SIGTERM down to the children
362 # so we need to use try_kill_process_by_pid.
363 self.try_kill_process_by_pid()
364 proc.terminate()
Anas Nashif227392c2020-04-27 20:31:56 -0400365 # sleep for a while before attempting to kill
366 time.sleep(0.5)
367 proc.kill()
Anas Nashifce2b4182020-03-24 14:40:28 -0400368 self.terminated = True
369
370 def _output_reader(self, proc, harness):
371 log_out_fp = open(self.log, "wt")
372 for line in iter(proc.stdout.readline, b''):
373 logger.debug("OUTPUT: {0}".format(line.decode('utf-8').rstrip()))
374 log_out_fp.write(line.decode('utf-8'))
375 log_out_fp.flush()
376 harness.handle(line.decode('utf-8').rstrip())
377 if harness.state:
378 try:
379 # POSIX arch based ztests end on their own,
380 # so let's give it up to 100ms to do so
381 proc.wait(0.1)
382 except subprocess.TimeoutExpired:
383 self.terminate(proc)
384 break
385
386 log_out_fp.close()
387
388 def handle(self):
389
390 harness_name = self.instance.testcase.harness.capitalize()
391 harness_import = HarnessImporter(harness_name)
392 harness = harness_import.instance
393 harness.configure(self.instance)
394
395 if self.call_make_run:
396 command = [self.generator_cmd, "run"]
397 else:
398 command = [self.binary]
399
400 run_valgrind = False
401 if self.valgrind and shutil.which("valgrind"):
402 command = ["valgrind", "--error-exitcode=2",
403 "--leak-check=full",
404 "--suppressions=" + ZEPHYR_BASE + "/scripts/valgrind.supp",
405 "--log-file=" + self.build_dir + "/valgrind.log"
406 ] + command
407 run_valgrind = True
408
409 logger.debug("Spawning process: " +
410 " ".join(shlex.quote(word) for word in command) + os.linesep +
411 "in directory: " + self.build_dir)
412
413 start_time = time.time()
414
415 env = os.environ.copy()
416 if self.asan:
417 env["ASAN_OPTIONS"] = "log_path=stdout:" + \
418 env.get("ASAN_OPTIONS", "")
419 if not self.lsan:
420 env["ASAN_OPTIONS"] += "detect_leaks=0"
421
Christian Taedcke3dbe9f22020-07-06 16:00:57 +0200422 if self.ubsan:
423 env["UBSAN_OPTIONS"] = "log_path=stdout:halt_on_error=1:" + \
424 env.get("UBSAN_OPTIONS", "")
425
Anas Nashifce2b4182020-03-24 14:40:28 -0400426 with subprocess.Popen(command, stdout=subprocess.PIPE,
427 stderr=subprocess.PIPE, cwd=self.build_dir, env=env) as proc:
428 logger.debug("Spawning BinaryHandler Thread for %s" % self.name)
429 t = threading.Thread(target=self._output_reader, args=(proc, harness,), daemon=True)
430 t.start()
431 t.join(self.timeout)
432 if t.is_alive():
433 self.terminate(proc)
434 t.join()
435 proc.wait()
436 self.returncode = proc.returncode
437
438 handler_time = time.time() - start_time
439
440 if self.coverage:
441 subprocess.call(["GCOV_PREFIX=" + self.build_dir,
442 "gcov", self.sourcedir, "-b", "-s", self.build_dir], shell=True)
443
444 self.try_kill_process_by_pid()
445
446 # FIXME: This is needed when killing the simulator, the console is
447 # garbled and needs to be reset. Did not find a better way to do that.
448
449 subprocess.call(["stty", "sane"])
450 self.instance.results = harness.tests
451
452 if not self.terminated and self.returncode != 0:
453 # When a process is killed, the default handler returns 128 + SIGTERM
454 # so in that case the return code itself is not meaningful
455 self.set_state("failed", handler_time)
456 self.instance.reason = "Failed"
457 elif run_valgrind and self.returncode == 2:
458 self.set_state("failed", handler_time)
459 self.instance.reason = "Valgrind error"
460 elif harness.state:
461 self.set_state(harness.state, handler_time)
Anas Nashifb802af82020-04-26 21:57:38 -0400462 if harness.state == "failed":
463 self.instance.reason = "Failed"
Anas Nashifce2b4182020-03-24 14:40:28 -0400464 else:
465 self.set_state("timeout", handler_time)
466 self.instance.reason = "Timeout"
467
468 self.record(harness)
469
470
471class DeviceHandler(Handler):
472
473 def __init__(self, instance, type_str):
474 """Constructor
475
476 @param instance Test Instance
477 """
478 super().__init__(instance, type_str)
479
480 self.suite = None
Anas Nashifce2b4182020-03-24 14:40:28 -0400481
482 def monitor_serial(self, ser, halt_fileno, harness):
483 log_out_fp = open(self.log, "wt")
484
485 ser_fileno = ser.fileno()
486 readlist = [halt_fileno, ser_fileno]
487
488 while ser.isOpen():
489 readable, _, _ = select.select(readlist, [], [], self.timeout)
490
491 if halt_fileno in readable:
492 logger.debug('halted')
493 ser.close()
494 break
495 if ser_fileno not in readable:
496 continue # Timeout.
497
498 serial_line = None
499 try:
500 serial_line = ser.readline()
501 except TypeError:
502 pass
503 except serial.SerialException:
504 ser.close()
505 break
506
507 # Just because ser_fileno has data doesn't mean an entire line
508 # is available yet.
509 if serial_line:
510 sl = serial_line.decode('utf-8', 'ignore').lstrip()
511 logger.debug("DEVICE: {0}".format(sl.rstrip()))
512
513 log_out_fp.write(sl)
514 log_out_fp.flush()
515 harness.handle(sl.rstrip())
516
517 if harness.state:
518 ser.close()
519 break
520
521 log_out_fp.close()
522
Anas Nashif3b86f132020-05-21 10:35:33 -0400523 def device_is_available(self, instance):
524 device = instance.platform.name
525 fixture = instance.testcase.harness_config.get("fixture")
Anas Nashifce2b4182020-03-24 14:40:28 -0400526 for i in self.suite.connected_hardware:
Anas Nashif3b86f132020-05-21 10:35:33 -0400527 if fixture and fixture not in i.get('fixtures', []):
528 continue
Anas Nashife47866c2020-07-22 10:49:24 -0400529 if i['platform'] == device and i['available'] and (i['serial'] or i.get('serial_pty', None)):
Anas Nashifce2b4182020-03-24 14:40:28 -0400530 return True
531
532 return False
533
Anas Nashif3b86f132020-05-21 10:35:33 -0400534 def get_available_device(self, instance):
535 device = instance.platform.name
Anas Nashifce2b4182020-03-24 14:40:28 -0400536 for i in self.suite.connected_hardware:
Anas Nashife47866c2020-07-22 10:49:24 -0400537 if i['platform'] == device and i['available'] and (i['serial'] or i.get('serial_pty', None)):
Anas Nashifce2b4182020-03-24 14:40:28 -0400538 i['available'] = False
539 i['counter'] += 1
540 return i
541
542 return None
543
544 def make_device_available(self, serial):
545 with hw_map_local:
546 for i in self.suite.connected_hardware:
Anas Nashife47866c2020-07-22 10:49:24 -0400547 if i['serial'] == serial or i.get('serial_pty', None):
Anas Nashifce2b4182020-03-24 14:40:28 -0400548 i['available'] = True
549
550 @staticmethod
551 def run_custom_script(script, timeout):
552 with subprocess.Popen(script, stderr=subprocess.PIPE, stdout=subprocess.PIPE) as proc:
553 try:
554 stdout, _ = proc.communicate(timeout=timeout)
555 logger.debug(stdout.decode())
556
557 except subprocess.TimeoutExpired:
558 proc.kill()
559 proc.communicate()
560 logger.error("{} timed out".format(script))
561
562 def handle(self):
563 out_state = "failed"
564
Kumar Gala8ca56912020-06-17 06:20:10 -0500565 if self.suite.west_flash is not None:
Anas Nashifce2b4182020-03-24 14:40:28 -0400566 command = ["west", "flash", "--skip-rebuild", "-d", self.build_dir]
Anas Nashifc11f8ac2020-03-31 08:37:55 -0400567 if self.suite.west_runner:
Anas Nashifce2b4182020-03-24 14:40:28 -0400568 command.append("--runner")
Anas Nashifc11f8ac2020-03-31 08:37:55 -0400569 command.append(self.suite.west_runner)
Anas Nashifce2b4182020-03-24 14:40:28 -0400570 # There are three ways this option is used.
571 # 1) bare: --west-flash
572 # This results in options.west_flash == []
573 # 2) with a value: --west-flash="--board-id=42"
574 # This results in options.west_flash == "--board-id=42"
575 # 3) Multiple values: --west-flash="--board-id=42,--erase"
576 # This results in options.west_flash == "--board-id=42 --erase"
Anas Nashifc11f8ac2020-03-31 08:37:55 -0400577 if self.suite.west_flash != []:
Anas Nashifce2b4182020-03-24 14:40:28 -0400578 command.append('--')
Anas Nashifc11f8ac2020-03-31 08:37:55 -0400579 command.extend(self.suite.west_flash.split(','))
Anas Nashifce2b4182020-03-24 14:40:28 -0400580 else:
581 command = [self.generator_cmd, "-C", self.build_dir, "flash"]
582
Anas Nashif3b86f132020-05-21 10:35:33 -0400583 while not self.device_is_available(self.instance):
Anas Nashifce2b4182020-03-24 14:40:28 -0400584 logger.debug("Waiting for device {} to become available".format(self.instance.platform.name))
585 time.sleep(1)
586
Anas Nashif3b86f132020-05-21 10:35:33 -0400587 hardware = self.get_available_device(self.instance)
Anas Nashifce2b4182020-03-24 14:40:28 -0400588
Anas Nashif3b86f132020-05-21 10:35:33 -0400589 if hardware:
590 runner = hardware.get('runner', None)
Anas Nashifce2b4182020-03-24 14:40:28 -0400591 if runner:
592 board_id = hardware.get("probe_id", hardware.get("id", None))
593 product = hardware.get("product", None)
594 command = ["west", "flash", "--skip-rebuild", "-d", self.build_dir]
595 command.append("--runner")
596 command.append(hardware.get('runner', None))
597 if runner == "pyocd":
598 command.append("--board-id")
599 command.append(board_id)
600 elif runner == "nrfjprog":
601 command.append('--')
602 command.append("--snr")
603 command.append(board_id)
604 elif runner == "openocd" and product == "STM32 STLink":
605 command.append('--')
606 command.append("--cmd-pre-init")
607 command.append("hla_serial %s" % (board_id))
Erwan Gouriou2339fa02020-07-07 17:15:22 +0200608 elif runner == "openocd" and product == "STLINK-V3":
609 command.append('--')
610 command.append("--cmd-pre-init")
611 command.append("hla_serial %s" % (board_id))
Anas Nashifce2b4182020-03-24 14:40:28 -0400612 elif runner == "openocd" and product == "EDBG CMSIS-DAP":
613 command.append('--')
614 command.append("--cmd-pre-init")
615 command.append("cmsis_dap_serial %s" % (board_id))
616 elif runner == "jlink":
617 command.append("--tool-opt=-SelectEmuBySN %s" % (board_id))
618
Anas Nashife47866c2020-07-22 10:49:24 -0400619 serial_pty = hardware.get('serial_pty', None)
Andrei Emeltchenkod8b845b2020-03-31 13:22:30 +0300620 if serial_pty:
621 master, slave = pty.openpty()
622
623 try:
624 ser_pty_process = subprocess.Popen(serial_pty, stdout=master, stdin=master, stderr=master)
625 except subprocess.CalledProcessError as error:
626 logger.error("Failed to run subprocess {}, error {}".format(serial_pty, error.output))
627 return
628
629 serial_device = os.ttyname(slave)
630 else:
631 serial_device = hardware['serial']
632
633 logger.debug("Using serial device {}".format(serial_device))
Anas Nashifce2b4182020-03-24 14:40:28 -0400634
635 try:
636 ser = serial.Serial(
637 serial_device,
638 baudrate=115200,
639 parity=serial.PARITY_NONE,
640 stopbits=serial.STOPBITS_ONE,
641 bytesize=serial.EIGHTBITS,
642 timeout=self.timeout
643 )
644 except serial.SerialException as e:
645 self.set_state("failed", 0)
646 self.instance.reason = "Failed"
647 logger.error("Serial device error: %s" % (str(e)))
Andrei Emeltchenkod8b845b2020-03-31 13:22:30 +0300648
649 if serial_pty:
650 ser_pty_process.terminate()
651 outs, errs = ser_pty_process.communicate()
652 logger.debug("Process {} terminated outs: {} errs {}".format(serial_pty, outs, errs))
653
Anas Nashifce2b4182020-03-24 14:40:28 -0400654 self.make_device_available(serial_device)
655 return
656
657 ser.flush()
658
659 harness_name = self.instance.testcase.harness.capitalize()
660 harness_import = HarnessImporter(harness_name)
661 harness = harness_import.instance
662 harness.configure(self.instance)
663 read_pipe, write_pipe = os.pipe()
664 start_time = time.time()
665
666 pre_script = hardware.get('pre_script')
667 post_flash_script = hardware.get('post_flash_script')
668 post_script = hardware.get('post_script')
669
670 if pre_script:
671 self.run_custom_script(pre_script, 30)
672
673 t = threading.Thread(target=self.monitor_serial, daemon=True,
674 args=(ser, read_pipe, harness))
675 t.start()
676
677 d_log = "{}/device.log".format(self.instance.build_dir)
678 logger.debug('Flash command: %s', command)
679 try:
680 stdout = stderr = None
681 with subprocess.Popen(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE) as proc:
682 try:
683 (stdout, stderr) = proc.communicate(timeout=30)
684 logger.debug(stdout.decode())
685
686 if proc.returncode != 0:
687 self.instance.reason = "Device issue (Flash?)"
688 with open(d_log, "w") as dlog_fp:
689 dlog_fp.write(stderr.decode())
690 except subprocess.TimeoutExpired:
691 proc.kill()
692 (stdout, stderr) = proc.communicate()
693 self.instance.reason = "Device issue (Timeout)"
694
695 with open(d_log, "w") as dlog_fp:
696 dlog_fp.write(stderr.decode())
697
698 except subprocess.CalledProcessError:
699 os.write(write_pipe, b'x') # halt the thread
700
701 if post_flash_script:
702 self.run_custom_script(post_flash_script, 30)
703
Anas Nashifce2b4182020-03-24 14:40:28 -0400704 t.join(self.timeout)
705 if t.is_alive():
706 logger.debug("Timed out while monitoring serial output on {}".format(self.instance.platform.name))
707 out_state = "timeout"
708
709 if ser.isOpen():
710 ser.close()
711
Andrei Emeltchenkod8b845b2020-03-31 13:22:30 +0300712 if serial_pty:
713 ser_pty_process.terminate()
714 outs, errs = ser_pty_process.communicate()
715 logger.debug("Process {} terminated outs: {} errs {}".format(serial_pty, outs, errs))
716
Anas Nashifce2b4182020-03-24 14:40:28 -0400717 os.close(write_pipe)
718 os.close(read_pipe)
719
720 handler_time = time.time() - start_time
721
722 if out_state == "timeout":
723 for c in self.instance.testcase.cases:
724 if c not in harness.tests:
725 harness.tests[c] = "BLOCK"
726
727 self.instance.reason = "Timeout"
728
729 self.instance.results = harness.tests
730
731 if harness.state:
732 self.set_state(harness.state, handler_time)
733 if harness.state == "failed":
734 self.instance.reason = "Failed"
735 else:
736 self.set_state(out_state, handler_time)
737
738 if post_script:
739 self.run_custom_script(post_script, 30)
740
741 self.make_device_available(serial_device)
742
743 self.record(harness)
744
745
746class QEMUHandler(Handler):
747 """Spawns a thread to monitor QEMU output from pipes
748
749 We pass QEMU_PIPE to 'make run' and monitor the pipes for output.
750 We need to do this as once qemu starts, it runs forever until killed.
751 Test cases emit special messages to the console as they run, we check
752 for these to collect whether the test passed or failed.
753 """
754
755 def __init__(self, instance, type_str):
756 """Constructor
757
758 @param instance Test instance
759 """
760
761 super().__init__(instance, type_str)
762 self.fifo_fn = os.path.join(instance.build_dir, "qemu-fifo")
763
764 self.pid_fn = os.path.join(instance.build_dir, "qemu.pid")
765
766 @staticmethod
Wentong Wu0d619ae2020-05-05 19:46:49 -0400767 def _get_cpu_time(pid):
768 """get process CPU time.
769
770 The guest virtual time in QEMU icount mode isn't host time and
771 it's maintained by counting guest instructions, so we use QEMU
772 process exection time to mostly simulate the time of guest OS.
773 """
774 proc = psutil.Process(pid)
775 cpu_time = proc.cpu_times()
776 return cpu_time.user + cpu_time.system
777
778 @staticmethod
Anas Nashifce2b4182020-03-24 14:40:28 -0400779 def _thread(handler, timeout, outdir, logfile, fifo_fn, pid_fn, results, harness):
780 fifo_in = fifo_fn + ".in"
781 fifo_out = fifo_fn + ".out"
782
783 # These in/out nodes are named from QEMU's perspective, not ours
784 if os.path.exists(fifo_in):
785 os.unlink(fifo_in)
786 os.mkfifo(fifo_in)
787 if os.path.exists(fifo_out):
788 os.unlink(fifo_out)
789 os.mkfifo(fifo_out)
790
791 # We don't do anything with out_fp but we need to open it for
792 # writing so that QEMU doesn't block, due to the way pipes work
793 out_fp = open(fifo_in, "wb")
794 # Disable internal buffering, we don't
795 # want read() or poll() to ever block if there is data in there
796 in_fp = open(fifo_out, "rb", buffering=0)
797 log_out_fp = open(logfile, "wt")
798
799 start_time = time.time()
800 timeout_time = start_time + timeout
801 p = select.poll()
802 p.register(in_fp, select.POLLIN)
803 out_state = None
804
805 line = ""
806 timeout_extended = False
Wentong Wu0d619ae2020-05-05 19:46:49 -0400807
808 pid = 0
809 if os.path.exists(pid_fn):
810 pid = int(open(pid_fn).read())
811
Anas Nashifce2b4182020-03-24 14:40:28 -0400812 while True:
813 this_timeout = int((timeout_time - time.time()) * 1000)
814 if this_timeout < 0 or not p.poll(this_timeout):
Wentong Wu517633c2020-07-24 21:13:01 +0800815 try:
816 if pid and this_timeout > 0:
817 #there's possibility we polled nothing because
818 #of not enough CPU time scheduled by host for
819 #QEMU process during p.poll(this_timeout)
820 cpu_time = QEMUHandler._get_cpu_time(pid)
821 if cpu_time < timeout and not out_state:
822 timeout_time = time.time() + (timeout - cpu_time)
823 continue
824 except ProcessLookupError:
825 out_state = "failed"
826 break
Wentong Wu0d619ae2020-05-05 19:46:49 -0400827
Anas Nashifce2b4182020-03-24 14:40:28 -0400828 if not out_state:
829 out_state = "timeout"
830 break
831
Wentong Wu0d619ae2020-05-05 19:46:49 -0400832 if pid == 0 and os.path.exists(pid_fn):
833 pid = int(open(pid_fn).read())
834
Anas Nashifce2b4182020-03-24 14:40:28 -0400835 try:
836 c = in_fp.read(1).decode("utf-8")
837 except UnicodeDecodeError:
838 # Test is writing something weird, fail
839 out_state = "unexpected byte"
840 break
841
842 if c == "":
843 # EOF, this shouldn't happen unless QEMU crashes
844 out_state = "unexpected eof"
845 break
846 line = line + c
847 if c != "\n":
848 continue
849
850 # line contains a full line of data output from QEMU
851 log_out_fp.write(line)
852 log_out_fp.flush()
853 line = line.strip()
854 logger.debug("QEMU: %s" % line)
855
856 harness.handle(line)
857 if harness.state:
858 # if we have registered a fail make sure the state is not
859 # overridden by a false success message coming from the
860 # testsuite
Anas Nashif869ca052020-07-07 14:29:07 -0400861 if out_state not in ['failed', 'unexpected eof', 'unexpected byte']:
Anas Nashifce2b4182020-03-24 14:40:28 -0400862 out_state = harness.state
863
864 # if we get some state, that means test is doing well, we reset
865 # the timeout and wait for 2 more seconds to catch anything
866 # printed late. We wait much longer if code
867 # coverage is enabled since dumping this information can
868 # take some time.
869 if not timeout_extended or harness.capture_coverage:
870 timeout_extended = True
871 if harness.capture_coverage:
872 timeout_time = time.time() + 30
873 else:
874 timeout_time = time.time() + 2
Anas Nashifce2b4182020-03-24 14:40:28 -0400875 line = ""
876
877 handler.record(harness)
878
879 handler_time = time.time() - start_time
880 logger.debug("QEMU complete (%s) after %f seconds" %
881 (out_state, handler_time))
Anas Nashif869ca052020-07-07 14:29:07 -0400882
Anas Nashifce2b4182020-03-24 14:40:28 -0400883 if out_state == "timeout":
884 handler.instance.reason = "Timeout"
Anas Nashif06052922020-07-15 22:44:24 -0400885 handler.set_state("failed", handler_time)
Anas Nashifce2b4182020-03-24 14:40:28 -0400886 elif out_state == "failed":
887 handler.instance.reason = "Failed"
Anas Nashif869ca052020-07-07 14:29:07 -0400888 handler.set_state("failed", handler_time)
Anas Nashif06052922020-07-15 22:44:24 -0400889 elif out_state in ['unexpected eof', 'unexpected byte']:
Anas Nashif869ca052020-07-07 14:29:07 -0400890 handler.instance.reason = out_state
Anas Nashif06052922020-07-15 22:44:24 -0400891 handler.set_state("failed", handler_time)
892 else:
893 handler.set_state(out_state, handler_time)
Anas Nashifce2b4182020-03-24 14:40:28 -0400894
895 log_out_fp.close()
896 out_fp.close()
897 in_fp.close()
Wentong Wu0d619ae2020-05-05 19:46:49 -0400898 if pid:
Anas Nashifce2b4182020-03-24 14:40:28 -0400899 try:
900 if pid:
901 os.kill(pid, signal.SIGTERM)
902 except ProcessLookupError:
903 # Oh well, as long as it's dead! User probably sent Ctrl-C
904 pass
905
906 os.unlink(fifo_in)
907 os.unlink(fifo_out)
908
909 def handle(self):
910 self.results = {}
911 self.run = True
912
913 # We pass this to QEMU which looks for fifos with .in and .out
914 # suffixes.
915 self.fifo_fn = os.path.join(self.instance.build_dir, "qemu-fifo")
916
917 self.pid_fn = os.path.join(self.instance.build_dir, "qemu.pid")
918 if os.path.exists(self.pid_fn):
919 os.unlink(self.pid_fn)
920
921 self.log_fn = self.log
922
923 harness_import = HarnessImporter(self.instance.testcase.harness.capitalize())
924 harness = harness_import.instance
925 harness.configure(self.instance)
926 self.thread = threading.Thread(name=self.name, target=QEMUHandler._thread,
927 args=(self, self.timeout, self.build_dir,
928 self.log_fn, self.fifo_fn,
929 self.pid_fn, self.results, harness))
930
931 self.instance.results = harness.tests
932 self.thread.daemon = True
933 logger.debug("Spawning QEMUHandler Thread for %s" % self.name)
934 self.thread.start()
935 subprocess.call(["stty", "sane"])
936
937 logger.debug("Running %s (%s)" % (self.name, self.type_str))
938 command = [self.generator_cmd]
939 command += ["-C", self.build_dir, "run"]
940
941 with subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.build_dir) as proc:
942 logger.debug("Spawning QEMUHandler Thread for %s" % self.name)
Wentong Wu7ec57b42020-05-05 19:19:18 -0400943 try:
944 proc.wait(self.timeout)
945 except subprocess.TimeoutExpired:
946 #sometimes QEMU can't handle SIGTERM signal correctly
947 #in that case kill -9 QEMU process directly and leave
948 #sanitycheck judge testing result by console output
949 if os.path.exists(self.pid_fn):
950 qemu_pid = int(open(self.pid_fn).read())
951 try:
952 os.kill(qemu_pid, signal.SIGKILL)
953 except ProcessLookupError:
954 pass
955 proc.wait()
Wentong Wu6fae53c2020-06-24 22:55:59 +0800956 if harness.state == "passed":
957 self.returncode = 0
958 else:
959 self.returncode = proc.returncode
Wentong Wu7ec57b42020-05-05 19:19:18 -0400960 else:
961 proc.terminate()
962 proc.kill()
963 self.returncode = proc.returncode
964 else:
Anas Nashif869ca052020-07-07 14:29:07 -0400965 logger.debug(f"No timeout, return code from qemu: {self.returncode}")
Wentong Wu7ec57b42020-05-05 19:19:18 -0400966 self.returncode = proc.returncode
967
968 if os.path.exists(self.pid_fn):
969 os.unlink(self.pid_fn)
Anas Nashifce2b4182020-03-24 14:40:28 -0400970
Anas Nashif869ca052020-07-07 14:29:07 -0400971 logger.debug(f"return code from qemu: {self.returncode}")
972
Anas Nashif06052922020-07-15 22:44:24 -0400973 if self.returncode != 0 or not harness.state:
Anas Nashifce2b4182020-03-24 14:40:28 -0400974 self.set_state("failed", 0)
975 self.instance.reason = "Exited with {}".format(self.returncode)
976
977 def get_fifo(self):
978 return self.fifo_fn
979
980
981class SizeCalculator:
982 alloc_sections = [
983 "bss",
984 "noinit",
985 "app_bss",
986 "app_noinit",
987 "ccm_bss",
988 "ccm_noinit"
989 ]
990
991 rw_sections = [
992 "datas",
993 "initlevel",
994 "exceptions",
995 "initshell",
Andrew Boie45979da2020-05-23 14:38:39 -0700996 "_static_thread_data_area",
997 "k_timer_area",
998 "k_mem_slab_area",
999 "k_mem_pool_area",
Anas Nashifce2b4182020-03-24 14:40:28 -04001000 "sw_isr_table",
Andrew Boie45979da2020-05-23 14:38:39 -07001001 "k_sem_area",
1002 "k_mutex_area",
Anas Nashifce2b4182020-03-24 14:40:28 -04001003 "app_shmem_regions",
1004 "_k_fifo_area",
1005 "_k_lifo_area",
Andrew Boie45979da2020-05-23 14:38:39 -07001006 "k_stack_area",
1007 "k_msgq_area",
1008 "k_mbox_area",
1009 "k_pipe_area",
Anas Nashifce2b4182020-03-24 14:40:28 -04001010 "net_if",
1011 "net_if_dev",
Anas Nashifce2b4182020-03-24 14:40:28 -04001012 "net_l2_data",
Andrew Boie45979da2020-05-23 14:38:39 -07001013 "k_queue_area",
Anas Nashifce2b4182020-03-24 14:40:28 -04001014 "_net_buf_pool_area",
1015 "app_datas",
1016 "kobject_data",
1017 "mmu_tables",
1018 "app_pad",
1019 "priv_stacks",
1020 "ccm_data",
1021 "usb_descriptor",
1022 "usb_data", "usb_bos_desc",
Jukka Rissanen420b1952020-04-01 12:47:53 +03001023 "uart_mux",
Anas Nashifce2b4182020-03-24 14:40:28 -04001024 'log_backends_sections',
1025 'log_dynamic_sections',
1026 'log_const_sections',
1027 "app_smem",
1028 'shell_root_cmds_sections',
1029 'log_const_sections',
1030 "font_entry_sections",
1031 "priv_stacks_noinit",
1032 "_GCOV_BSS_SECTION_NAME",
1033 "gcov",
1034 "nocache"
1035 ]
1036
1037 # These get copied into RAM only on non-XIP
1038 ro_sections = [
1039 "rom_start",
1040 "text",
1041 "ctors",
1042 "init_array",
1043 "reset",
Andrew Boie45979da2020-05-23 14:38:39 -07001044 "z_object_assignment_area",
Anas Nashifce2b4182020-03-24 14:40:28 -04001045 "rodata",
1046 "devconfig",
1047 "net_l2",
1048 "vector",
1049 "sw_isr_table",
Andrew Boie45979da2020-05-23 14:38:39 -07001050 "settings_handler_static_area",
1051 "bt_l2cap_fixed_chan",
1052 "bt_l2cap_br_fixec_chan",
1053 "bt_gatt_service_static",
Anas Nashifce2b4182020-03-24 14:40:28 -04001054 "vectors",
Andrew Boie45979da2020-05-23 14:38:39 -07001055 "net_socket_register_area",
1056 "net_ppp_proto",
1057 "shell_area",
1058 "tracing_backend_area",
Anas Nashifce2b4182020-03-24 14:40:28 -04001059 ]
1060
1061 def __init__(self, filename, extra_sections):
1062 """Constructor
1063
1064 @param filename Path to the output binary
1065 The <filename> is parsed by objdump to determine section sizes
1066 """
1067 # Make sure this is an ELF binary
1068 with open(filename, "rb") as f:
1069 magic = f.read(4)
1070
1071 try:
1072 if magic != b'\x7fELF':
1073 raise SanityRuntimeError("%s is not an ELF binary" % filename)
1074 except Exception as e:
1075 print(str(e))
1076 sys.exit(2)
1077
1078 # Search for CONFIG_XIP in the ELF's list of symbols using NM and AWK.
1079 # GREP can not be used as it returns an error if the symbol is not
1080 # found.
1081 is_xip_command = "nm " + filename + \
1082 " | awk '/CONFIG_XIP/ { print $3 }'"
1083 is_xip_output = subprocess.check_output(
1084 is_xip_command, shell=True, stderr=subprocess.STDOUT).decode(
1085 "utf-8").strip()
1086 try:
1087 if is_xip_output.endswith("no symbols"):
1088 raise SanityRuntimeError("%s has no symbol information" % filename)
1089 except Exception as e:
1090 print(str(e))
1091 sys.exit(2)
1092
1093 self.is_xip = (len(is_xip_output) != 0)
1094
1095 self.filename = filename
1096 self.sections = []
1097 self.rom_size = 0
1098 self.ram_size = 0
1099 self.extra_sections = extra_sections
1100
1101 self._calculate_sizes()
1102
1103 def get_ram_size(self):
1104 """Get the amount of RAM the application will use up on the device
1105
1106 @return amount of RAM, in bytes
1107 """
1108 return self.ram_size
1109
1110 def get_rom_size(self):
1111 """Get the size of the data that this application uses on device's flash
1112
1113 @return amount of ROM, in bytes
1114 """
1115 return self.rom_size
1116
1117 def unrecognized_sections(self):
1118 """Get a list of sections inside the binary that weren't recognized
1119
1120 @return list of unrecognized section names
1121 """
1122 slist = []
1123 for v in self.sections:
1124 if not v["recognized"]:
1125 slist.append(v["name"])
1126 return slist
1127
1128 def _calculate_sizes(self):
1129 """ Calculate RAM and ROM usage by section """
1130 objdump_command = "objdump -h " + self.filename
1131 objdump_output = subprocess.check_output(
1132 objdump_command, shell=True).decode("utf-8").splitlines()
1133
1134 for line in objdump_output:
1135 words = line.split()
1136
1137 if not words: # Skip lines that are too short
1138 continue
1139
1140 index = words[0]
1141 if not index[0].isdigit(): # Skip lines that do not start
1142 continue # with a digit
1143
1144 name = words[1] # Skip lines with section names
1145 if name[0] == '.': # starting with '.'
1146 continue
1147
1148 # TODO this doesn't actually reflect the size in flash or RAM as
1149 # it doesn't include linker-imposed padding between sections.
1150 # It is close though.
1151 size = int(words[2], 16)
1152 if size == 0:
1153 continue
1154
1155 load_addr = int(words[4], 16)
1156 virt_addr = int(words[3], 16)
1157
1158 # Add section to memory use totals (for both non-XIP and XIP scenarios)
1159 # Unrecognized section names are not included in the calculations.
1160 recognized = True
1161 if name in SizeCalculator.alloc_sections:
1162 self.ram_size += size
1163 stype = "alloc"
1164 elif name in SizeCalculator.rw_sections:
1165 self.ram_size += size
1166 self.rom_size += size
1167 stype = "rw"
1168 elif name in SizeCalculator.ro_sections:
1169 self.rom_size += size
1170 if not self.is_xip:
1171 self.ram_size += size
1172 stype = "ro"
1173 else:
1174 stype = "unknown"
1175 if name not in self.extra_sections:
1176 recognized = False
1177
1178 self.sections.append({"name": name, "load_addr": load_addr,
1179 "size": size, "virt_addr": virt_addr,
1180 "type": stype, "recognized": recognized})
1181
1182
1183
1184class SanityConfigParser:
1185 """Class to read test case files with semantic checking
1186 """
1187
1188 def __init__(self, filename, schema):
1189 """Instantiate a new SanityConfigParser object
1190
1191 @param filename Source .yaml file to read
1192 """
1193 self.data = {}
1194 self.schema = schema
1195 self.filename = filename
1196 self.tests = {}
1197 self.common = {}
1198
1199 def load(self):
1200 self.data = scl.yaml_load_verify(self.filename, self.schema)
1201
1202 if 'tests' in self.data:
1203 self.tests = self.data['tests']
1204 if 'common' in self.data:
1205 self.common = self.data['common']
1206
1207 def _cast_value(self, value, typestr):
1208 if isinstance(value, str):
1209 v = value.strip()
1210 if typestr == "str":
1211 return v
1212
1213 elif typestr == "float":
1214 return float(value)
1215
1216 elif typestr == "int":
1217 return int(value)
1218
1219 elif typestr == "bool":
1220 return value
1221
1222 elif typestr.startswith("list") and isinstance(value, list):
1223 return value
1224 elif typestr.startswith("list") and isinstance(value, str):
1225 vs = v.split()
1226 if len(typestr) > 4 and typestr[4] == ":":
1227 return [self._cast_value(vsi, typestr[5:]) for vsi in vs]
1228 else:
1229 return vs
1230
1231 elif typestr.startswith("set"):
1232 vs = v.split()
1233 if len(typestr) > 3 and typestr[3] == ":":
1234 return {self._cast_value(vsi, typestr[4:]) for vsi in vs}
1235 else:
1236 return set(vs)
1237
1238 elif typestr.startswith("map"):
1239 return value
1240 else:
1241 raise ConfigurationError(
1242 self.filename, "unknown type '%s'" % value)
1243
1244 def get_test(self, name, valid_keys):
1245 """Get a dictionary representing the keys/values within a test
1246
1247 @param name The test in the .yaml file to retrieve data from
1248 @param valid_keys A dictionary representing the intended semantics
1249 for this test. Each key in this dictionary is a key that could
1250 be specified, if a key is given in the .yaml file which isn't in
1251 here, it will generate an error. Each value in this dictionary
1252 is another dictionary containing metadata:
1253
1254 "default" - Default value if not given
1255 "type" - Data type to convert the text value to. Simple types
1256 supported are "str", "float", "int", "bool" which will get
1257 converted to respective Python data types. "set" and "list"
1258 may also be specified which will split the value by
1259 whitespace (but keep the elements as strings). finally,
1260 "list:<type>" and "set:<type>" may be given which will
1261 perform a type conversion after splitting the value up.
1262 "required" - If true, raise an error if not defined. If false
1263 and "default" isn't specified, a type conversion will be
1264 done on an empty string
1265 @return A dictionary containing the test key-value pairs with
1266 type conversion and default values filled in per valid_keys
1267 """
1268
1269 d = {}
1270 for k, v in self.common.items():
1271 d[k] = v
1272
1273 for k, v in self.tests[name].items():
1274 if k not in valid_keys:
1275 raise ConfigurationError(
1276 self.filename,
1277 "Unknown config key '%s' in definition for '%s'" %
1278 (k, name))
1279
1280 if k in d:
1281 if isinstance(d[k], str):
1282 # By default, we just concatenate string values of keys
1283 # which appear both in "common" and per-test sections,
1284 # but some keys are handled in adhoc way based on their
1285 # semantics.
1286 if k == "filter":
1287 d[k] = "(%s) and (%s)" % (d[k], v)
1288 else:
1289 d[k] += " " + v
1290 else:
1291 d[k] = v
1292
1293 for k, kinfo in valid_keys.items():
1294 if k not in d:
1295 if "required" in kinfo:
1296 required = kinfo["required"]
1297 else:
1298 required = False
1299
1300 if required:
1301 raise ConfigurationError(
1302 self.filename,
1303 "missing required value for '%s' in test '%s'" %
1304 (k, name))
1305 else:
1306 if "default" in kinfo:
1307 default = kinfo["default"]
1308 else:
1309 default = self._cast_value("", kinfo["type"])
1310 d[k] = default
1311 else:
1312 try:
1313 d[k] = self._cast_value(d[k], kinfo["type"])
1314 except ValueError:
1315 raise ConfigurationError(
1316 self.filename, "bad %s value '%s' for key '%s' in name '%s'" %
1317 (kinfo["type"], d[k], k, name))
1318
1319 return d
1320
1321
1322class Platform:
1323 """Class representing metadata for a particular platform
1324
1325 Maps directly to BOARD when building"""
1326
1327 platform_schema = scl.yaml_load(os.path.join(ZEPHYR_BASE,
1328 "scripts", "sanity_chk", "platform-schema.yaml"))
1329
1330 def __init__(self):
1331 """Constructor.
1332
1333 """
1334
1335 self.name = ""
1336 self.sanitycheck = True
1337 # if no RAM size is specified by the board, take a default of 128K
1338 self.ram = 128
1339
1340 self.ignore_tags = []
Anas Nashife8e367a2020-07-16 16:27:04 -04001341 self.only_tags = []
Anas Nashifce2b4182020-03-24 14:40:28 -04001342 self.default = False
1343 # if no flash size is specified by the board, take a default of 512K
1344 self.flash = 512
1345 self.supported = set()
1346
1347 self.arch = ""
1348 self.type = "na"
1349 self.simulation = "na"
1350 self.supported_toolchains = []
1351 self.env = []
1352 self.env_satisfied = True
1353 self.filter_data = dict()
1354
1355 def load(self, platform_file):
1356 scp = SanityConfigParser(platform_file, self.platform_schema)
1357 scp.load()
1358 data = scp.data
1359
1360 self.name = data['identifier']
1361 self.sanitycheck = data.get("sanitycheck", True)
1362 # if no RAM size is specified by the board, take a default of 128K
1363 self.ram = data.get("ram", 128)
1364 testing = data.get("testing", {})
1365 self.ignore_tags = testing.get("ignore_tags", [])
Anas Nashife8e367a2020-07-16 16:27:04 -04001366 self.only_tags = testing.get("only_tags", [])
Anas Nashifce2b4182020-03-24 14:40:28 -04001367 self.default = testing.get("default", False)
1368 # if no flash size is specified by the board, take a default of 512K
1369 self.flash = data.get("flash", 512)
1370 self.supported = set()
1371 for supp_feature in data.get("supported", []):
1372 for item in supp_feature.split(":"):
1373 self.supported.add(item)
1374
1375 self.arch = data['arch']
1376 self.type = data.get('type', "na")
1377 self.simulation = data.get('simulation', "na")
1378 self.supported_toolchains = data.get("toolchain", [])
1379 self.env = data.get("env", [])
1380 self.env_satisfied = True
1381 for env in self.env:
1382 if not os.environ.get(env, None):
1383 self.env_satisfied = False
1384
1385 def __repr__(self):
1386 return "<%s on %s>" % (self.name, self.arch)
1387
1388
Anas Nashifaff616d2020-04-17 21:24:57 -04001389class DisablePyTestCollectionMixin(object):
1390 __test__ = False
1391
1392
1393class TestCase(DisablePyTestCollectionMixin):
Anas Nashifce2b4182020-03-24 14:40:28 -04001394 """Class representing a test application
1395 """
1396
Anas Nashifaff616d2020-04-17 21:24:57 -04001397 def __init__(self, testcase_root, workdir, name):
Anas Nashifce2b4182020-03-24 14:40:28 -04001398 """TestCase constructor.
1399
1400 This gets called by TestSuite as it finds and reads test yaml files.
1401 Multiple TestCase instances may be generated from a single testcase.yaml,
1402 each one corresponds to an entry within that file.
1403
1404 We need to have a unique name for every single test case. Since
1405 a testcase.yaml can define multiple tests, the canonical name for
1406 the test case is <workdir>/<name>.
1407
1408 @param testcase_root os.path.abspath() of one of the --testcase-root
1409 @param workdir Sub-directory of testcase_root where the
1410 .yaml test configuration file was found
1411 @param name Name of this test case, corresponding to the entry name
1412 in the test case configuration file. For many test cases that just
1413 define one test, can be anything and is usually "test". This is
1414 really only used to distinguish between different cases when
1415 the testcase.yaml defines multiple tests
Anas Nashifce2b4182020-03-24 14:40:28 -04001416 """
1417
Anas Nashifaff616d2020-04-17 21:24:57 -04001418
Anas Nashifce2b4182020-03-24 14:40:28 -04001419 self.source_dir = ""
1420 self.yamlfile = ""
1421 self.cases = []
Anas Nashifaff616d2020-04-17 21:24:57 -04001422 self.name = self.get_unique(testcase_root, workdir, name)
1423 self.id = name
Anas Nashifce2b4182020-03-24 14:40:28 -04001424
1425 self.type = None
Anas Nashifaff616d2020-04-17 21:24:57 -04001426 self.tags = set()
Anas Nashifce2b4182020-03-24 14:40:28 -04001427 self.extra_args = None
1428 self.extra_configs = None
1429 self.arch_whitelist = None
1430 self.arch_exclude = None
Anas Nashifaff616d2020-04-17 21:24:57 -04001431 self.skip = False
Anas Nashifce2b4182020-03-24 14:40:28 -04001432 self.platform_exclude = None
1433 self.platform_whitelist = None
1434 self.toolchain_exclude = None
1435 self.toolchain_whitelist = None
1436 self.tc_filter = None
1437 self.timeout = 60
1438 self.harness = ""
1439 self.harness_config = {}
1440 self.build_only = True
1441 self.build_on_all = False
1442 self.slow = False
Anas Nashifaff616d2020-04-17 21:24:57 -04001443 self.min_ram = -1
Anas Nashifce2b4182020-03-24 14:40:28 -04001444 self.depends_on = None
Anas Nashifaff616d2020-04-17 21:24:57 -04001445 self.min_flash = -1
Anas Nashifce2b4182020-03-24 14:40:28 -04001446 self.extra_sections = None
1447
1448 @staticmethod
1449 def get_unique(testcase_root, workdir, name):
1450
1451 canonical_testcase_root = os.path.realpath(testcase_root)
1452 if Path(canonical_zephyr_base) in Path(canonical_testcase_root).parents:
1453 # This is in ZEPHYR_BASE, so include path in name for uniqueness
1454 # FIXME: We should not depend on path of test for unique names.
1455 relative_tc_root = os.path.relpath(canonical_testcase_root,
1456 start=canonical_zephyr_base)
1457 else:
1458 relative_tc_root = ""
1459
1460 # workdir can be "."
1461 unique = os.path.normpath(os.path.join(relative_tc_root, workdir, name))
Anas Nashif7a691252020-05-07 07:47:51 -04001462 check = name.split(".")
1463 if len(check) < 2:
1464 raise SanityCheckException(f"""bad test name '{name}' in {testcase_root}/{workdir}. \
1465Tests should reference the category and subsystem with a dot as a separator.
1466 """
1467 )
Anas Nashifce2b4182020-03-24 14:40:28 -04001468 return unique
1469
1470 @staticmethod
1471 def scan_file(inf_name):
1472 suite_regex = re.compile(
1473 # do not match until end-of-line, otherwise we won't allow
1474 # stc_regex below to catch the ones that are declared in the same
1475 # line--as we only search starting the end of this match
1476 br"^\s*ztest_test_suite\(\s*(?P<suite_name>[a-zA-Z0-9_]+)\s*,",
1477 re.MULTILINE)
1478 stc_regex = re.compile(
1479 br"^\s*" # empy space at the beginning is ok
1480 # catch the case where it is declared in the same sentence, e.g:
1481 #
1482 # ztest_test_suite(mutex_complex, ztest_user_unit_test(TESTNAME));
1483 br"(?:ztest_test_suite\([a-zA-Z0-9_]+,\s*)?"
1484 # Catch ztest[_user]_unit_test-[_setup_teardown](TESTNAME)
1485 br"ztest_(?:1cpu_)?(?:user_)?unit_test(?:_setup_teardown)?"
1486 # Consume the argument that becomes the extra testcse
1487 br"\(\s*"
1488 br"(?P<stc_name>[a-zA-Z0-9_]+)"
1489 # _setup_teardown() variant has two extra arguments that we ignore
1490 br"(?:\s*,\s*[a-zA-Z0-9_]+\s*,\s*[a-zA-Z0-9_]+)?"
1491 br"\s*\)",
1492 # We don't check how it finishes; we don't care
1493 re.MULTILINE)
1494 suite_run_regex = re.compile(
1495 br"^\s*ztest_run_test_suite\((?P<suite_name>[a-zA-Z0-9_]+)\)",
1496 re.MULTILINE)
1497 achtung_regex = re.compile(
1498 br"(#ifdef|#endif)",
1499 re.MULTILINE)
1500 warnings = None
1501
1502 with open(inf_name) as inf:
1503 if os.name == 'nt':
1504 mmap_args = {'fileno': inf.fileno(), 'length': 0, 'access': mmap.ACCESS_READ}
1505 else:
1506 mmap_args = {'fileno': inf.fileno(), 'length': 0, 'flags': mmap.MAP_PRIVATE, 'prot': mmap.PROT_READ,
1507 'offset': 0}
1508
1509 with contextlib.closing(mmap.mmap(**mmap_args)) as main_c:
Anas Nashifce2b4182020-03-24 14:40:28 -04001510 suite_regex_match = suite_regex.search(main_c)
1511 if not suite_regex_match:
1512 # can't find ztest_test_suite, maybe a client, because
1513 # it includes ztest.h
1514 return None, None
1515
1516 suite_run_match = suite_run_regex.search(main_c)
1517 if not suite_run_match:
1518 raise ValueError("can't find ztest_run_test_suite")
1519
1520 achtung_matches = re.findall(
1521 achtung_regex,
1522 main_c[suite_regex_match.end():suite_run_match.start()])
1523 if achtung_matches:
1524 warnings = "found invalid %s in ztest_test_suite()" \
Spoorthy Priya Yeraboluad4d4fc2020-06-25 02:57:05 -07001525 % ", ".join(sorted({match.decode() for match in achtung_matches},reverse = True))
Anas Nashifce2b4182020-03-24 14:40:28 -04001526 _matches = re.findall(
1527 stc_regex,
1528 main_c[suite_regex_match.end():suite_run_match.start()])
Anas Nashif44f7ba02020-05-12 12:26:41 -04001529 for match in _matches:
1530 if not match.decode().startswith("test_"):
1531 warnings = "Found a test that does not start with test_"
Anas Nashifce2b4182020-03-24 14:40:28 -04001532 matches = [match.decode().replace("test_", "") for match in _matches]
1533 return matches, warnings
1534
1535 def scan_path(self, path):
1536 subcases = []
Anas Nashif91fd68d2020-05-08 07:22:58 -04001537 for filename in glob.glob(os.path.join(path, "src", "*.c*")):
Anas Nashifce2b4182020-03-24 14:40:28 -04001538 try:
1539 _subcases, warnings = self.scan_file(filename)
1540 if warnings:
1541 logger.error("%s: %s" % (filename, warnings))
Anas Nashif61c6e2b2020-05-07 07:03:30 -04001542 raise SanityRuntimeError("%s: %s" % (filename, warnings))
Anas Nashifce2b4182020-03-24 14:40:28 -04001543 if _subcases:
1544 subcases += _subcases
1545 except ValueError as e:
1546 logger.error("%s: can't find: %s" % (filename, e))
Anas Nashif61c6e2b2020-05-07 07:03:30 -04001547
Anas Nashifce2b4182020-03-24 14:40:28 -04001548 for filename in glob.glob(os.path.join(path, "*.c")):
1549 try:
1550 _subcases, warnings = self.scan_file(filename)
1551 if warnings:
1552 logger.error("%s: %s" % (filename, warnings))
1553 if _subcases:
1554 subcases += _subcases
1555 except ValueError as e:
1556 logger.error("%s: can't find: %s" % (filename, e))
1557 return subcases
1558
1559 def parse_subcases(self, test_path):
1560 results = self.scan_path(test_path)
1561 for sub in results:
1562 name = "{}.{}".format(self.id, sub)
1563 self.cases.append(name)
1564
1565 if not results:
1566 self.cases.append(self.id)
1567
1568 def __str__(self):
1569 return self.name
1570
1571
Anas Nashifaff616d2020-04-17 21:24:57 -04001572class TestInstance(DisablePyTestCollectionMixin):
Anas Nashifce2b4182020-03-24 14:40:28 -04001573 """Class representing the execution of a particular TestCase on a platform
1574
1575 @param test The TestCase object we want to build/execute
1576 @param platform Platform object that we want to build and run against
1577 @param base_outdir Base directory for all test results. The actual
1578 out directory used is <outdir>/<platform>/<test case name>
1579 """
1580
1581 def __init__(self, testcase, platform, outdir):
1582
1583 self.testcase = testcase
1584 self.platform = platform
1585
1586 self.status = None
1587 self.reason = "Unknown"
1588 self.metrics = dict()
1589 self.handler = None
1590 self.outdir = outdir
1591
1592 self.name = os.path.join(platform.name, testcase.name)
1593 self.build_dir = os.path.join(outdir, platform.name, testcase.name)
1594
1595 self.build_only = True
1596 self.run = False
1597
1598 self.results = {}
1599
1600 def __lt__(self, other):
1601 return self.name < other.name
1602
Anas Nashifaff616d2020-04-17 21:24:57 -04001603 # Global testsuite parameters
Anas Nashifce8c12e2020-05-21 09:11:40 -04001604 def check_build_or_run(self, build_only=False, enable_slow=False, device_testing=False, fixtures=[]):
Anas Nashifce2b4182020-03-24 14:40:28 -04001605
1606 # right now we only support building on windows. running is still work
1607 # in progress.
1608 if os.name == 'nt':
1609 self.build_only = True
1610 self.run = False
1611 return
1612
1613 _build_only = True
1614
1615 # we asked for build-only on the command line
1616 if build_only or self.testcase.build_only:
1617 self.build_only = True
1618 self.run = False
1619 return
1620
1621 # Do not run slow tests:
1622 skip_slow = self.testcase.slow and not enable_slow
1623 if skip_slow:
1624 self.build_only = True
1625 self.run = False
1626 return
1627
1628 runnable = bool(self.testcase.type == "unit" or \
1629 self.platform.type == "native" or \
1630 self.platform.simulation in ["nsim", "renode", "qemu"] or \
1631 device_testing)
1632
1633 if self.platform.simulation == "nsim":
1634 if not find_executable("nsimdrv"):
1635 runnable = False
1636
1637 if self.platform.simulation == "renode":
1638 if not find_executable("renode"):
1639 runnable = False
1640
1641 # console harness allows us to run the test and capture data.
Anas Nashifce8c12e2020-05-21 09:11:40 -04001642 if self.testcase.harness in [ 'console', 'ztest']:
Anas Nashifce2b4182020-03-24 14:40:28 -04001643
1644 # if we have a fixture that is also being supplied on the
1645 # command-line, then we need to run the test, not just build it.
Anas Nashifce8c12e2020-05-21 09:11:40 -04001646 fixture = self.testcase.harness_config.get('fixture')
1647 if fixture:
1648 if fixture in fixtures:
Anas Nashifce2b4182020-03-24 14:40:28 -04001649 _build_only = False
1650 else:
1651 _build_only = True
1652 else:
1653 _build_only = False
Anas Nashif3b86f132020-05-21 10:35:33 -04001654
Anas Nashifce2b4182020-03-24 14:40:28 -04001655 elif self.testcase.harness:
1656 _build_only = True
1657 else:
1658 _build_only = False
1659
1660 self.build_only = not (not _build_only and runnable)
1661 self.run = not self.build_only
1662 return
1663
Christian Taedcke3dbe9f22020-07-06 16:00:57 +02001664 def create_overlay(self, platform, enable_asan=False, enable_ubsan=False, enable_coverage=False, coverage_platform=[]):
Anas Nashifce2b4182020-03-24 14:40:28 -04001665 # Create this in a "sanitycheck/" subdirectory otherwise this
1666 # will pass this overlay to kconfig.py *twice* and kconfig.cmake
1667 # will silently give that second time precedence over any
1668 # --extra-args=CONFIG_*
1669 subdir = os.path.join(self.build_dir, "sanitycheck")
1670 os.makedirs(subdir, exist_ok=True)
1671 file = os.path.join(subdir, "testcase_extra.conf")
1672
1673 with open(file, "w") as f:
1674 content = ""
1675
1676 if self.testcase.extra_configs:
1677 content = "\n".join(self.testcase.extra_configs)
1678
1679 if enable_coverage:
1680 if platform.name in coverage_platform:
1681 content = content + "\nCONFIG_COVERAGE=y"
1682 content = content + "\nCONFIG_COVERAGE_DUMP=y"
1683
1684 if enable_asan:
1685 if platform.type == "native":
1686 content = content + "\nCONFIG_ASAN=y"
1687
Christian Taedcke3dbe9f22020-07-06 16:00:57 +02001688 if enable_ubsan:
1689 if platform.type == "native":
1690 content = content + "\nCONFIG_UBSAN=y"
1691
Anas Nashifce2b4182020-03-24 14:40:28 -04001692 f.write(content)
Spoorthy Priya Yerabolud434dfc2020-05-30 03:38:35 -07001693 return content
Anas Nashifce2b4182020-03-24 14:40:28 -04001694
1695 def calculate_sizes(self):
1696 """Get the RAM/ROM sizes of a test case.
1697
1698 This can only be run after the instance has been executed by
1699 MakeGenerator, otherwise there won't be any binaries to measure.
1700
1701 @return A SizeCalculator object
1702 """
1703 fns = glob.glob(os.path.join(self.build_dir, "zephyr", "*.elf"))
1704 fns.extend(glob.glob(os.path.join(self.build_dir, "zephyr", "*.exe")))
1705 fns = [x for x in fns if not x.endswith('_prebuilt.elf')]
1706 if len(fns) != 1:
1707 raise BuildError("Missing/multiple output ELF binary")
1708
1709 return SizeCalculator(fns[0], self.testcase.extra_sections)
1710
1711 def __repr__(self):
1712 return "<TestCase %s on %s>" % (self.testcase.name, self.platform.name)
1713
1714
1715class CMake():
1716 config_re = re.compile('(CONFIG_[A-Za-z0-9_]+)[=]\"?([^\"]*)\"?$')
1717 dt_re = re.compile('([A-Za-z0-9_]+)[=]\"?([^\"]*)\"?$')
1718
1719 def __init__(self, testcase, platform, source_dir, build_dir):
1720
1721 self.cwd = None
1722 self.capture_output = True
1723
1724 self.defconfig = {}
1725 self.cmake_cache = {}
1726
1727 self.instance = None
1728 self.testcase = testcase
1729 self.platform = platform
1730 self.source_dir = source_dir
1731 self.build_dir = build_dir
1732 self.log = "build.log"
1733 self.generator = None
1734 self.generator_cmd = None
1735
1736 def parse_generated(self):
1737 self.defconfig = {}
1738 return {}
1739
1740 def run_build(self, args=[]):
1741
1742 logger.debug("Building %s for %s" % (self.source_dir, self.platform.name))
1743
1744 cmake_args = []
1745 cmake_args.extend(args)
1746 cmake = shutil.which('cmake')
1747 cmd = [cmake] + cmake_args
1748 kwargs = dict()
1749
1750 if self.capture_output:
1751 kwargs['stdout'] = subprocess.PIPE
1752 # CMake sends the output of message() to stderr unless it's STATUS
1753 kwargs['stderr'] = subprocess.STDOUT
1754
1755 if self.cwd:
1756 kwargs['cwd'] = self.cwd
1757
1758 p = subprocess.Popen(cmd, **kwargs)
1759 out, _ = p.communicate()
1760
1761 results = {}
1762 if p.returncode == 0:
1763 msg = "Finished building %s for %s" % (self.source_dir, self.platform.name)
1764
1765 self.instance.status = "passed"
1766 results = {'msg': msg, "returncode": p.returncode, "instance": self.instance}
1767
1768 if out:
1769 log_msg = out.decode(sys.getdefaultencoding())
1770 with open(os.path.join(self.build_dir, self.log), "a") as log:
1771 log.write(log_msg)
1772
1773 else:
1774 return None
1775 else:
1776 # A real error occurred, raise an exception
1777 if out:
1778 log_msg = out.decode(sys.getdefaultencoding())
1779 with open(os.path.join(self.build_dir, self.log), "a") as log:
1780 log.write(log_msg)
1781
1782 if log_msg:
1783 res = re.findall("region `(FLASH|RAM|SRAM)' overflowed by", log_msg)
1784 if res:
1785 logger.debug("Test skipped due to {} Overflow".format(res[0]))
1786 self.instance.status = "skipped"
1787 self.instance.reason = "{} overflow".format(res[0])
1788 else:
Anas Nashiff04461e2020-06-29 10:07:02 -04001789 self.instance.status = "error"
Anas Nashifce2b4182020-03-24 14:40:28 -04001790 self.instance.reason = "Build failure"
1791
1792 results = {
1793 "returncode": p.returncode,
1794 "instance": self.instance,
1795 }
1796
1797 return results
1798
1799 def run_cmake(self, args=[]):
1800
Anas Nashif50925412020-07-16 17:25:19 -04001801 if self.warnings_as_errors:
1802 ldflags = "-Wl,--fatal-warnings"
1803 cflags = "-Werror"
1804 aflags = "-Wa,--fatal-warnings"
1805 else:
1806 ldflags = cflags = aflags = ""
Anas Nashifce2b4182020-03-24 14:40:28 -04001807
Anas Nashif50925412020-07-16 17:25:19 -04001808 logger.debug("Running cmake on %s for %s" % (self.source_dir, self.platform.name))
Anas Nashifce2b4182020-03-24 14:40:28 -04001809 cmake_args = [
Anas Nashif50925412020-07-16 17:25:19 -04001810 f'-B{self.build_dir}',
1811 f'-S{self.source_dir}',
1812 f'-DEXTRA_CFLAGS="{cflags}"',
1813 f'-DEXTRA_AFLAGS="{aflags}',
1814 f'-DEXTRA_LDFLAGS="{ldflags}"',
1815 f'-G{self.generator}'
Anas Nashifce2b4182020-03-24 14:40:28 -04001816 ]
1817
1818 if self.cmake_only:
1819 cmake_args.append("-DCMAKE_EXPORT_COMPILE_COMMANDS=1")
1820
1821 args = ["-D{}".format(a.replace('"', '')) for a in args]
1822 cmake_args.extend(args)
1823
1824 cmake_opts = ['-DBOARD={}'.format(self.platform.name)]
1825 cmake_args.extend(cmake_opts)
1826
1827
1828 logger.debug("Calling cmake with arguments: {}".format(cmake_args))
1829 cmake = shutil.which('cmake')
1830 cmd = [cmake] + cmake_args
1831 kwargs = dict()
1832
1833 if self.capture_output:
1834 kwargs['stdout'] = subprocess.PIPE
1835 # CMake sends the output of message() to stderr unless it's STATUS
1836 kwargs['stderr'] = subprocess.STDOUT
1837
1838 if self.cwd:
1839 kwargs['cwd'] = self.cwd
1840
1841 p = subprocess.Popen(cmd, **kwargs)
1842 out, _ = p.communicate()
1843
1844 if p.returncode == 0:
1845 filter_results = self.parse_generated()
1846 msg = "Finished building %s for %s" % (self.source_dir, self.platform.name)
1847 logger.debug(msg)
1848 results = {'msg': msg, 'filter': filter_results}
1849
1850 else:
Anas Nashiff04461e2020-06-29 10:07:02 -04001851 self.instance.status = "error"
Anas Nashifce2b4182020-03-24 14:40:28 -04001852 self.instance.reason = "Cmake build failure"
1853 logger.error("Cmake build failure: %s for %s" % (self.source_dir, self.platform.name))
1854 results = {"returncode": p.returncode}
1855
1856 if out:
1857 with open(os.path.join(self.build_dir, self.log), "a") as log:
1858 log_msg = out.decode(sys.getdefaultencoding())
1859 log.write(log_msg)
1860
1861 return results
1862
1863
1864class FilterBuilder(CMake):
1865
1866 def __init__(self, testcase, platform, source_dir, build_dir):
1867 super().__init__(testcase, platform, source_dir, build_dir)
1868
1869 self.log = "config-sanitycheck.log"
1870
1871 def parse_generated(self):
1872
1873 if self.platform.name == "unit_testing":
1874 return {}
1875
1876 cmake_cache_path = os.path.join(self.build_dir, "CMakeCache.txt")
1877 defconfig_path = os.path.join(self.build_dir, "zephyr", ".config")
1878
1879 with open(defconfig_path, "r") as fp:
1880 defconfig = {}
1881 for line in fp.readlines():
1882 m = self.config_re.match(line)
1883 if not m:
1884 if line.strip() and not line.startswith("#"):
1885 sys.stderr.write("Unrecognized line %s\n" % line)
1886 continue
1887 defconfig[m.group(1)] = m.group(2).strip()
1888
1889 self.defconfig = defconfig
1890
1891 cmake_conf = {}
1892 try:
1893 cache = CMakeCache.from_file(cmake_cache_path)
1894 except FileNotFoundError:
1895 cache = {}
1896
1897 for k in iter(cache):
1898 cmake_conf[k.name] = k.value
1899
1900 self.cmake_cache = cmake_conf
1901
1902 filter_data = {
1903 "ARCH": self.platform.arch,
1904 "PLATFORM": self.platform.name
1905 }
1906 filter_data.update(os.environ)
1907 filter_data.update(self.defconfig)
1908 filter_data.update(self.cmake_cache)
1909
Martí Bolívar9c92baa2020-07-08 14:43:07 -07001910 edt_pickle = os.path.join(self.build_dir, "zephyr", "edt.pickle")
Anas Nashifce2b4182020-03-24 14:40:28 -04001911 if self.testcase and self.testcase.tc_filter:
1912 try:
Martí Bolívar9c92baa2020-07-08 14:43:07 -07001913 if os.path.exists(edt_pickle):
1914 with open(edt_pickle, 'rb') as f:
1915 edt = pickle.load(f)
Anas Nashifce2b4182020-03-24 14:40:28 -04001916 else:
1917 edt = None
1918 res = expr_parser.parse(self.testcase.tc_filter, filter_data, edt)
1919
1920 except (ValueError, SyntaxError) as se:
1921 sys.stderr.write(
1922 "Failed processing %s\n" % self.testcase.yamlfile)
1923 raise se
1924
1925 if not res:
1926 return {os.path.join(self.platform.name, self.testcase.name): True}
1927 else:
1928 return {os.path.join(self.platform.name, self.testcase.name): False}
1929 else:
1930 self.platform.filter_data = filter_data
1931 return filter_data
1932
1933
1934class ProjectBuilder(FilterBuilder):
1935
1936 def __init__(self, suite, instance, **kwargs):
1937 super().__init__(instance.testcase, instance.platform, instance.testcase.source_dir, instance.build_dir)
1938
1939 self.log = "build.log"
1940 self.instance = instance
1941 self.suite = suite
1942
1943 self.lsan = kwargs.get('lsan', False)
1944 self.asan = kwargs.get('asan', False)
Christian Taedcke3dbe9f22020-07-06 16:00:57 +02001945 self.ubsan = kwargs.get('ubsan', False)
Anas Nashifce2b4182020-03-24 14:40:28 -04001946 self.valgrind = kwargs.get('valgrind', False)
1947 self.extra_args = kwargs.get('extra_args', [])
1948 self.device_testing = kwargs.get('device_testing', False)
1949 self.cmake_only = kwargs.get('cmake_only', False)
1950 self.cleanup = kwargs.get('cleanup', False)
1951 self.coverage = kwargs.get('coverage', False)
1952 self.inline_logs = kwargs.get('inline_logs', False)
Anas Nashifce2b4182020-03-24 14:40:28 -04001953 self.generator = kwargs.get('generator', None)
1954 self.generator_cmd = kwargs.get('generator_cmd', None)
Anas Nashiff6462a32020-03-29 19:02:51 -04001955 self.verbose = kwargs.get('verbose', None)
Anas Nashif50925412020-07-16 17:25:19 -04001956 self.warnings_as_errors = kwargs.get('warnings_as_errors', True)
Anas Nashifce2b4182020-03-24 14:40:28 -04001957
1958 @staticmethod
1959 def log_info(filename, inline_logs):
1960 filename = os.path.abspath(os.path.realpath(filename))
1961 if inline_logs:
1962 logger.info("{:-^100}".format(filename))
1963
1964 try:
1965 with open(filename) as fp:
1966 data = fp.read()
1967 except Exception as e:
1968 data = "Unable to read log data (%s)\n" % (str(e))
1969
1970 logger.error(data)
1971
1972 logger.info("{:-^100}".format(filename))
1973 else:
1974 logger.error("see: " + Fore.YELLOW + filename + Fore.RESET)
1975
1976 def log_info_file(self, inline_logs):
1977 build_dir = self.instance.build_dir
1978 h_log = "{}/handler.log".format(build_dir)
1979 b_log = "{}/build.log".format(build_dir)
1980 v_log = "{}/valgrind.log".format(build_dir)
1981 d_log = "{}/device.log".format(build_dir)
1982
1983 if os.path.exists(v_log) and "Valgrind" in self.instance.reason:
1984 self.log_info("{}".format(v_log), inline_logs)
1985 elif os.path.exists(h_log) and os.path.getsize(h_log) > 0:
1986 self.log_info("{}".format(h_log), inline_logs)
1987 elif os.path.exists(d_log) and os.path.getsize(d_log) > 0:
1988 self.log_info("{}".format(d_log), inline_logs)
1989 else:
1990 self.log_info("{}".format(b_log), inline_logs)
1991
1992 def setup_handler(self):
1993
1994 instance = self.instance
1995 args = []
1996
1997 # FIXME: Needs simplification
1998 if instance.platform.simulation == "qemu":
1999 instance.handler = QEMUHandler(instance, "qemu")
2000 args.append("QEMU_PIPE=%s" % instance.handler.get_fifo())
2001 instance.handler.call_make_run = True
2002 elif instance.testcase.type == "unit":
2003 instance.handler = BinaryHandler(instance, "unit")
2004 instance.handler.binary = os.path.join(instance.build_dir, "testbinary")
Anas Nashif051602f2020-04-28 14:27:46 -04002005 if self.coverage:
2006 args.append("COVERAGE=1")
Anas Nashifce2b4182020-03-24 14:40:28 -04002007 elif instance.platform.type == "native":
2008 handler = BinaryHandler(instance, "native")
2009
2010 handler.asan = self.asan
2011 handler.valgrind = self.valgrind
2012 handler.lsan = self.lsan
Christian Taedcke3dbe9f22020-07-06 16:00:57 +02002013 handler.ubsan = self.ubsan
Anas Nashifce2b4182020-03-24 14:40:28 -04002014 handler.coverage = self.coverage
2015
2016 handler.binary = os.path.join(instance.build_dir, "zephyr", "zephyr.exe")
2017 instance.handler = handler
2018 elif instance.platform.simulation == "nsim":
2019 if find_executable("nsimdrv"):
2020 instance.handler = BinaryHandler(instance, "nsim")
2021 instance.handler.call_make_run = True
2022 elif instance.platform.simulation == "renode":
2023 if find_executable("renode"):
2024 instance.handler = BinaryHandler(instance, "renode")
2025 instance.handler.pid_fn = os.path.join(instance.build_dir, "renode.pid")
2026 instance.handler.call_make_run = True
2027 elif self.device_testing:
2028 instance.handler = DeviceHandler(instance, "device")
2029
2030 if instance.handler:
2031 instance.handler.args = args
Anas Nashifb3669492020-03-24 22:33:50 -04002032 instance.handler.generator_cmd = self.generator_cmd
2033 instance.handler.generator = self.generator
Anas Nashifce2b4182020-03-24 14:40:28 -04002034
2035 def process(self, message):
2036 op = message.get('op')
2037
2038 if not self.instance.handler:
2039 self.setup_handler()
2040
2041 # The build process, call cmake and build with configured generator
2042 if op == "cmake":
2043 results = self.cmake()
Anas Nashiff04461e2020-06-29 10:07:02 -04002044 if self.instance.status in ["failed", "error"]:
Anas Nashifce2b4182020-03-24 14:40:28 -04002045 pipeline.put({"op": "report", "test": self.instance})
2046 elif self.cmake_only:
2047 pipeline.put({"op": "report", "test": self.instance})
2048 else:
2049 if self.instance.name in results['filter'] and results['filter'][self.instance.name]:
2050 logger.debug("filtering %s" % self.instance.name)
2051 self.instance.status = "skipped"
2052 self.instance.reason = "filter"
Maciej Perkowskib2fa99c2020-05-21 14:45:29 +02002053 for case in self.instance.testcase.cases:
2054 self.instance.results.update({case: 'SKIP'})
Anas Nashifce2b4182020-03-24 14:40:28 -04002055 pipeline.put({"op": "report", "test": self.instance})
2056 else:
2057 pipeline.put({"op": "build", "test": self.instance})
2058
2059 elif op == "build":
2060 logger.debug("build test: %s" % self.instance.name)
2061 results = self.build()
2062
2063 if not results:
Anas Nashiff04461e2020-06-29 10:07:02 -04002064 self.instance.status = "error"
Anas Nashifce2b4182020-03-24 14:40:28 -04002065 self.instance.reason = "Build Failure"
2066 pipeline.put({"op": "report", "test": self.instance})
2067 else:
2068 if results.get('returncode', 1) > 0:
2069 pipeline.put({"op": "report", "test": self.instance})
2070 else:
2071 if self.instance.run:
2072 pipeline.put({"op": "run", "test": self.instance})
2073 else:
2074 pipeline.put({"op": "report", "test": self.instance})
2075 # Run the generated binary using one of the supported handlers
2076 elif op == "run":
2077 logger.debug("run test: %s" % self.instance.name)
2078 self.run()
2079 self.instance.status, _ = self.instance.handler.get_state()
Anas Nashif869ca052020-07-07 14:29:07 -04002080 logger.debug(f"run status: {self.instance.status}")
Anas Nashifce2b4182020-03-24 14:40:28 -04002081 pipeline.put({
2082 "op": "report",
2083 "test": self.instance,
2084 "state": "executed",
2085 "status": self.instance.status,
2086 "reason": self.instance.reason}
2087 )
2088
2089 # Report results and output progress to screen
2090 elif op == "report":
2091 with report_lock:
2092 self.report_out()
2093
2094 if self.cleanup and not self.coverage and self.instance.status == "passed":
2095 pipeline.put({
2096 "op": "cleanup",
2097 "test": self.instance
2098 })
2099
2100 elif op == "cleanup":
2101 self.cleanup_artifacts()
2102
2103 def cleanup_artifacts(self):
2104 logger.debug("Cleaning up {}".format(self.instance.build_dir))
2105 whitelist = [
2106 'zephyr/.config',
2107 'handler.log',
2108 'build.log',
2109 'device.log',
Anas Nashif9ace63e2020-04-28 07:14:43 -04002110 'recording.csv',
Anas Nashifce2b4182020-03-24 14:40:28 -04002111 ]
2112 whitelist = [os.path.join(self.instance.build_dir, file) for file in whitelist]
2113
2114 for dirpath, dirnames, filenames in os.walk(self.instance.build_dir, topdown=False):
2115 for name in filenames:
2116 path = os.path.join(dirpath, name)
2117 if path not in whitelist:
2118 os.remove(path)
2119 # Remove empty directories and symbolic links to directories
2120 for dir in dirnames:
2121 path = os.path.join(dirpath, dir)
2122 if os.path.islink(path):
2123 os.remove(path)
2124 elif not os.listdir(path):
2125 os.rmdir(path)
2126
2127 def report_out(self):
2128 total_tests_width = len(str(self.suite.total_tests))
2129 self.suite.total_done += 1
2130 instance = self.instance
2131
Anas Nashiff04461e2020-06-29 10:07:02 -04002132 if instance.status in ["error", "failed", "timeout"]:
Anas Nashifdc43c292020-07-09 09:46:45 -04002133 if instance.status == "error":
2134 self.suite.total_errors += 1
Anas Nashifce2b4182020-03-24 14:40:28 -04002135 self.suite.total_failed += 1
Anas Nashiff6462a32020-03-29 19:02:51 -04002136 if self.verbose:
Anas Nashifce2b4182020-03-24 14:40:28 -04002137 status = Fore.RED + "FAILED " + Fore.RESET + instance.reason
2138 else:
2139 print("")
2140 logger.error(
2141 "{:<25} {:<50} {}FAILED{}: {}".format(
2142 instance.platform.name,
2143 instance.testcase.name,
2144 Fore.RED,
2145 Fore.RESET,
2146 instance.reason))
Anas Nashiff6462a32020-03-29 19:02:51 -04002147 if not self.verbose:
Anas Nashifce2b4182020-03-24 14:40:28 -04002148 self.log_info_file(self.inline_logs)
2149 elif instance.status == "skipped":
2150 self.suite.total_skipped += 1
2151 status = Fore.YELLOW + "SKIPPED" + Fore.RESET
Anas Nashif869ca052020-07-07 14:29:07 -04002152 elif instance.status == "passed":
Anas Nashifdc43c292020-07-09 09:46:45 -04002153 self.suite.total_passed += 1
Anas Nashifce2b4182020-03-24 14:40:28 -04002154 status = Fore.GREEN + "PASSED" + Fore.RESET
Anas Nashif869ca052020-07-07 14:29:07 -04002155 else:
2156 logger.debug(f"Unknown status = {instance.status}")
2157 status = Fore.YELLOW + "UNKNOWN" + Fore.RESET
Anas Nashifce2b4182020-03-24 14:40:28 -04002158
Anas Nashiff6462a32020-03-29 19:02:51 -04002159 if self.verbose:
Anas Nashifce2b4182020-03-24 14:40:28 -04002160 if self.cmake_only:
2161 more_info = "cmake"
2162 elif instance.status == "skipped":
2163 more_info = instance.reason
2164 else:
2165 if instance.handler and instance.run:
2166 more_info = instance.handler.type_str
2167 htime = instance.handler.duration
2168 if htime:
2169 more_info += " {:.3f}s".format(htime)
2170 else:
2171 more_info = "build"
2172
2173 logger.info("{:>{}}/{} {:<25} {:<50} {} ({})".format(
2174 self.suite.total_done, total_tests_width, self.suite.total_tests, instance.platform.name,
2175 instance.testcase.name, status, more_info))
2176
Anas Nashiff04461e2020-06-29 10:07:02 -04002177 if instance.status in ["error", "failed", "timeout"]:
Anas Nashifce2b4182020-03-24 14:40:28 -04002178 self.log_info_file(self.inline_logs)
2179 else:
2180 sys.stdout.write("\rINFO - Total complete: %s%4d/%4d%s %2d%% skipped: %s%4d%s, failed: %s%4d%s" % (
2181 Fore.GREEN,
2182 self.suite.total_done,
2183 self.suite.total_tests,
2184 Fore.RESET,
2185 int((float(self.suite.total_done) / self.suite.total_tests) * 100),
2186 Fore.YELLOW if self.suite.total_skipped > 0 else Fore.RESET,
2187 self.suite.total_skipped,
2188 Fore.RESET,
2189 Fore.RED if self.suite.total_failed > 0 else Fore.RESET,
2190 self.suite.total_failed,
2191 Fore.RESET
2192 )
2193 )
2194 sys.stdout.flush()
2195
2196 def cmake(self):
2197
2198 instance = self.instance
2199 args = self.testcase.extra_args[:]
2200 args += self.extra_args
2201
2202 if instance.handler:
2203 args += instance.handler.args
2204
2205 # merge overlay files into one variable
2206 def extract_overlays(args):
2207 re_overlay = re.compile('OVERLAY_CONFIG=(.*)')
2208 other_args = []
2209 overlays = []
2210 for arg in args:
2211 match = re_overlay.search(arg)
2212 if match:
2213 overlays.append(match.group(1).strip('\'"'))
2214 else:
2215 other_args.append(arg)
2216
2217 args[:] = other_args
2218 return overlays
2219
2220 overlays = extract_overlays(args)
2221
2222 if (self.testcase.extra_configs or self.coverage or
Christian Taedcke3dbe9f22020-07-06 16:00:57 +02002223 self.asan or self.ubsan):
Anas Nashifce2b4182020-03-24 14:40:28 -04002224 overlays.append(os.path.join(instance.build_dir,
2225 "sanitycheck", "testcase_extra.conf"))
2226
2227 if overlays:
2228 args.append("OVERLAY_CONFIG=\"%s\"" % (" ".join(overlays)))
2229
2230 results = self.run_cmake(args)
2231 return results
2232
2233 def build(self):
2234 results = self.run_build(['--build', self.build_dir])
2235 return results
2236
2237 def run(self):
2238
2239 instance = self.instance
2240
2241 if instance.handler.type_str == "device":
2242 instance.handler.suite = self.suite
2243
2244 instance.handler.handle()
2245
2246 sys.stdout.flush()
2247
2248
2249class BoundedExecutor(concurrent.futures.ThreadPoolExecutor):
2250 """BoundedExecutor behaves as a ThreadPoolExecutor which will block on
2251 calls to submit() once the limit given as "bound" work items are queued for
2252 execution.
2253 :param bound: Integer - the maximum number of items in the work queue
2254 :param max_workers: Integer - the size of the thread pool
2255 """
2256
2257 def __init__(self, bound, max_workers, **kwargs):
2258 super().__init__(max_workers)
2259 # self.executor = ThreadPoolExecutor(max_workers=max_workers)
2260 self.semaphore = BoundedSemaphore(bound + max_workers)
2261
2262 def submit(self, fn, *args, **kwargs):
2263 self.semaphore.acquire()
2264 try:
2265 future = super().submit(fn, *args, **kwargs)
2266 except Exception:
2267 self.semaphore.release()
2268 raise
2269 else:
2270 future.add_done_callback(lambda x: self.semaphore.release())
2271 return future
2272
2273
Anas Nashifaff616d2020-04-17 21:24:57 -04002274class TestSuite(DisablePyTestCollectionMixin):
Anas Nashifce2b4182020-03-24 14:40:28 -04002275 config_re = re.compile('(CONFIG_[A-Za-z0-9_]+)[=]\"?([^\"]*)\"?$')
2276 dt_re = re.compile('([A-Za-z0-9_]+)[=]\"?([^\"]*)\"?$')
2277
2278 tc_schema = scl.yaml_load(
2279 os.path.join(ZEPHYR_BASE,
2280 "scripts", "sanity_chk", "testcase-schema.yaml"))
2281
2282 testcase_valid_keys = {"tags": {"type": "set", "required": False},
2283 "type": {"type": "str", "default": "integration"},
2284 "extra_args": {"type": "list"},
2285 "extra_configs": {"type": "list"},
2286 "build_only": {"type": "bool", "default": False},
2287 "build_on_all": {"type": "bool", "default": False},
2288 "skip": {"type": "bool", "default": False},
2289 "slow": {"type": "bool", "default": False},
2290 "timeout": {"type": "int", "default": 60},
2291 "min_ram": {"type": "int", "default": 8},
2292 "depends_on": {"type": "set"},
2293 "min_flash": {"type": "int", "default": 32},
2294 "arch_whitelist": {"type": "set"},
2295 "arch_exclude": {"type": "set"},
2296 "extra_sections": {"type": "list", "default": []},
2297 "platform_exclude": {"type": "set"},
2298 "platform_whitelist": {"type": "set"},
2299 "toolchain_exclude": {"type": "set"},
2300 "toolchain_whitelist": {"type": "set"},
2301 "filter": {"type": "str"},
2302 "harness": {"type": "str"},
2303 "harness_config": {"type": "map", "default": {}}
2304 }
2305
2306 RELEASE_DATA = os.path.join(ZEPHYR_BASE, "scripts", "sanity_chk",
2307 "sanity_last_release.csv")
2308
Aastha Grovera0ae5342020-05-13 13:34:00 -07002309 SAMPLE_FILENAME = 'sample.yaml'
2310 TESTCASE_FILENAME = 'testcase.yaml'
2311
Anas Nashifaff616d2020-04-17 21:24:57 -04002312 def __init__(self, board_root_list=[], testcase_roots=[], outdir=None):
Anas Nashifce2b4182020-03-24 14:40:28 -04002313
2314 self.roots = testcase_roots
2315 if not isinstance(board_root_list, list):
2316 self.board_roots = [board_root_list]
2317 else:
2318 self.board_roots = board_root_list
2319
2320 # Testsuite Options
2321 self.coverage_platform = []
2322 self.build_only = False
2323 self.cmake_only = False
2324 self.cleanup = False
2325 self.enable_slow = False
2326 self.device_testing = False
Anas Nashifce8c12e2020-05-21 09:11:40 -04002327 self.fixtures = []
Anas Nashifce2b4182020-03-24 14:40:28 -04002328 self.enable_coverage = False
Christian Taedcke3dbe9f22020-07-06 16:00:57 +02002329 self.enable_ubsan = False
Anas Nashifce2b4182020-03-24 14:40:28 -04002330 self.enable_lsan = False
2331 self.enable_asan = False
2332 self.enable_valgrind = False
2333 self.extra_args = []
2334 self.inline_logs = False
2335 self.enable_sizes_report = False
2336 self.west_flash = None
2337 self.west_runner = None
2338 self.generator = None
2339 self.generator_cmd = None
Anas Nashif50925412020-07-16 17:25:19 -04002340 self.warnings_as_errors = True
Anas Nashifce2b4182020-03-24 14:40:28 -04002341
2342 # Keep track of which test cases we've filtered out and why
2343 self.testcases = {}
2344 self.platforms = []
2345 self.selected_platforms = []
2346 self.default_platforms = []
2347 self.outdir = os.path.abspath(outdir)
Anas Nashifaff616d2020-04-17 21:24:57 -04002348 self.discards = {}
Anas Nashifce2b4182020-03-24 14:40:28 -04002349 self.load_errors = 0
2350 self.instances = dict()
2351
2352 self.total_tests = 0 # number of test instances
2353 self.total_cases = 0 # number of test cases
2354 self.total_done = 0 # tests completed
2355 self.total_failed = 0
2356 self.total_skipped = 0
Anas Nashifdc43c292020-07-09 09:46:45 -04002357 self.total_passed = 0
2358 self.total_errors = 0
Anas Nashifce2b4182020-03-24 14:40:28 -04002359
2360 self.total_platforms = 0
2361 self.start_time = 0
2362 self.duration = 0
2363 self.warnings = 0
2364 self.cv = threading.Condition()
2365
2366 # hardcoded for now
2367 self.connected_hardware = []
2368
Anas Nashifbb280352020-05-07 12:02:48 -04002369 def get_platform_instances(self, platform):
2370 filtered_dict = {k:v for k,v in self.instances.items() if k.startswith(platform + "/")}
2371 return filtered_dict
2372
Anas Nashifce2b4182020-03-24 14:40:28 -04002373 def config(self):
2374 logger.info("coverage platform: {}".format(self.coverage_platform))
2375
2376 # Debug Functions
2377 @staticmethod
2378 def info(what):
2379 sys.stdout.write(what + "\n")
2380 sys.stdout.flush()
2381
2382 def update(self):
2383 self.total_tests = len(self.instances)
2384 self.total_cases = len(self.testcases)
2385
2386 def compare_metrics(self, filename):
2387 # name, datatype, lower results better
2388 interesting_metrics = [("ram_size", int, True),
2389 ("rom_size", int, True)]
2390
2391 if not os.path.exists(filename):
2392 logger.info("Cannot compare metrics, %s not found" % filename)
2393 return []
2394
2395 results = []
2396 saved_metrics = {}
2397 with open(filename) as fp:
2398 cr = csv.DictReader(fp)
2399 for row in cr:
2400 d = {}
2401 for m, _, _ in interesting_metrics:
2402 d[m] = row[m]
2403 saved_metrics[(row["test"], row["platform"])] = d
2404
2405 for instance in self.instances.values():
2406 mkey = (instance.testcase.name, instance.platform.name)
2407 if mkey not in saved_metrics:
2408 continue
2409 sm = saved_metrics[mkey]
2410 for metric, mtype, lower_better in interesting_metrics:
2411 if metric not in instance.metrics:
2412 continue
2413 if sm[metric] == "":
2414 continue
2415 delta = instance.metrics.get(metric, 0) - mtype(sm[metric])
2416 if delta == 0:
2417 continue
2418 results.append((instance, metric, instance.metrics.get(metric, 0), delta,
2419 lower_better))
2420 return results
2421
2422 def misc_reports(self, report, show_footprint, all_deltas,
2423 footprint_threshold, last_metrics):
2424
2425 if not report:
2426 return
2427
2428 deltas = self.compare_metrics(report)
2429 warnings = 0
2430 if deltas and show_footprint:
2431 for i, metric, value, delta, lower_better in deltas:
2432 if not all_deltas and ((delta < 0 and lower_better) or
2433 (delta > 0 and not lower_better)):
2434 continue
2435
2436 percentage = (float(delta) / float(value - delta))
2437 if not all_deltas and (percentage <
2438 (footprint_threshold / 100.0)):
2439 continue
2440
2441 logger.info("{:<25} {:<60} {}{}{}: {} {:<+4}, is now {:6} {:+.2%}".format(
2442 i.platform.name, i.testcase.name, Fore.YELLOW,
2443 "INFO" if all_deltas else "WARNING", Fore.RESET,
2444 metric, delta, value, percentage))
2445 warnings += 1
2446
2447 if warnings:
2448 logger.warning("Deltas based on metrics from last %s" %
2449 ("release" if not last_metrics else "run"))
2450
2451 def summary(self, unrecognized_sections):
2452 failed = 0
Anas Nashif4258d8d2020-05-08 08:40:27 -04002453 run = 0
Anas Nashifce2b4182020-03-24 14:40:28 -04002454 for instance in self.instances.values():
2455 if instance.status == "failed":
2456 failed += 1
2457 elif instance.metrics.get("unrecognized") and not unrecognized_sections:
2458 logger.error("%sFAILED%s: %s has unrecognized binary sections: %s" %
2459 (Fore.RED, Fore.RESET, instance.name,
2460 str(instance.metrics.get("unrecognized", []))))
2461 failed += 1
2462
Anas Nashif4258d8d2020-05-08 08:40:27 -04002463 if instance.metrics['handler_time']:
2464 run += 1
2465
Anas Nashifce2b4182020-03-24 14:40:28 -04002466 if self.total_tests and self.total_tests != self.total_skipped:
Anas Nashifdc43c292020-07-09 09:46:45 -04002467 pass_rate = (float(self.total_passed) / float(
Anas Nashifce2b4182020-03-24 14:40:28 -04002468 self.total_tests - self.total_skipped))
2469 else:
2470 pass_rate = 0
2471
2472 logger.info(
2473 "{}{} of {}{} tests passed ({:.2%}), {}{}{} failed, {} skipped with {}{}{} warnings in {:.2f} seconds".format(
2474 Fore.RED if failed else Fore.GREEN,
Anas Nashifdc43c292020-07-09 09:46:45 -04002475 self.total_passed,
Anas Nashifce2b4182020-03-24 14:40:28 -04002476 self.total_tests - self.total_skipped,
2477 Fore.RESET,
2478 pass_rate,
2479 Fore.RED if self.total_failed else Fore.RESET,
2480 self.total_failed,
2481 Fore.RESET,
2482 self.total_skipped,
2483 Fore.YELLOW if self.warnings else Fore.RESET,
2484 self.warnings,
2485 Fore.RESET,
2486 self.duration))
2487
2488 self.total_platforms = len(self.platforms)
2489 if self.platforms:
2490 logger.info("In total {} test cases were executed on {} out of total {} platforms ({:02.2f}%)".format(
2491 self.total_cases,
2492 len(self.selected_platforms),
2493 self.total_platforms,
2494 (100 * len(self.selected_platforms) / len(self.platforms))
2495 ))
2496
Anas Nashif4258d8d2020-05-08 08:40:27 -04002497 logger.info(f"{Fore.GREEN}{run}{Fore.RESET} tests executed on platforms, \
2498{Fore.RED}{self.total_tests - run}{Fore.RESET} tests were only built.")
2499
Anas Nashif6915adf2020-04-22 09:39:42 -04002500 def save_reports(self, name, suffix, report_dir, no_update, release, only_failed):
Anas Nashifce2b4182020-03-24 14:40:28 -04002501 if not self.instances:
2502 return
2503
2504 if name:
2505 report_name = name
2506 else:
2507 report_name = "sanitycheck"
2508
2509 if report_dir:
2510 os.makedirs(report_dir, exist_ok=True)
2511 filename = os.path.join(report_dir, report_name)
2512 outdir = report_dir
2513 else:
2514 filename = os.path.join(self.outdir, report_name)
2515 outdir = self.outdir
2516
Anas Nashif6915adf2020-04-22 09:39:42 -04002517 if suffix:
2518 filename = "{}_{}".format(filename, suffix)
2519
Anas Nashifce2b4182020-03-24 14:40:28 -04002520 if not no_update:
Anas Nashif90415502020-04-11 22:15:04 -04002521 self.xunit_report(filename + ".xml", full_report=False, append=only_failed)
2522 self.xunit_report(filename + "_report.xml", full_report=True, append=only_failed)
Anas Nashifce2b4182020-03-24 14:40:28 -04002523 self.csv_report(filename + ".csv")
Anas Nashif90415502020-04-11 22:15:04 -04002524
Anas Nashif6915adf2020-04-22 09:39:42 -04002525 self.target_report(outdir, suffix, append=only_failed)
Anas Nashifce2b4182020-03-24 14:40:28 -04002526 if self.discards:
2527 self.discard_report(filename + "_discard.csv")
2528
2529 if release:
2530 self.csv_report(self.RELEASE_DATA)
2531
2532 def add_configurations(self):
2533
2534 for board_root in self.board_roots:
2535 board_root = os.path.abspath(board_root)
2536
2537 logger.debug("Reading platform configuration files under %s..." %
2538 board_root)
2539
2540 for file in glob.glob(os.path.join(board_root, "*", "*", "*.yaml")):
2541 logger.debug("Found platform configuration " + file)
2542 try:
2543 platform = Platform()
2544 platform.load(file)
2545 if platform.sanitycheck:
2546 self.platforms.append(platform)
2547 if platform.default:
2548 self.default_platforms.append(platform.name)
2549
2550 except RuntimeError as e:
2551 logger.error("E: %s: can't load: %s" % (file, e))
2552 self.load_errors += 1
2553
2554 def get_all_tests(self):
2555 tests = []
2556 for _, tc in self.testcases.items():
2557 for case in tc.cases:
2558 tests.append(case)
2559
2560 return tests
2561
2562 @staticmethod
2563 def get_toolchain():
2564 toolchain = os.environ.get("ZEPHYR_TOOLCHAIN_VARIANT", None) or \
2565 os.environ.get("ZEPHYR_GCC_VARIANT", None)
2566
2567 if toolchain == "gccarmemb":
2568 # Remove this translation when gccarmemb is no longer supported.
2569 toolchain = "gnuarmemb"
2570
2571 try:
2572 if not toolchain:
2573 raise SanityRuntimeError("E: Variable ZEPHYR_TOOLCHAIN_VARIANT is not defined")
2574 except Exception as e:
2575 print(str(e))
2576 sys.exit(2)
2577
2578 return toolchain
2579
2580 def add_testcases(self, testcase_filter=[]):
2581 for root in self.roots:
2582 root = os.path.abspath(root)
2583
2584 logger.debug("Reading test case configuration files under %s..." % root)
2585
2586 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
2587 logger.debug("scanning %s" % dirpath)
Aastha Grovera0ae5342020-05-13 13:34:00 -07002588 if self.SAMPLE_FILENAME in filenames:
2589 filename = self.SAMPLE_FILENAME
2590 elif self.TESTCASE_FILENAME in filenames:
2591 filename = self.TESTCASE_FILENAME
Anas Nashifce2b4182020-03-24 14:40:28 -04002592 else:
2593 continue
2594
2595 logger.debug("Found possible test case in " + dirpath)
2596
2597 dirnames[:] = []
2598 tc_path = os.path.join(dirpath, filename)
2599
2600 try:
2601 parsed_data = SanityConfigParser(tc_path, self.tc_schema)
2602 parsed_data.load()
2603
2604 tc_path = os.path.dirname(tc_path)
2605 workdir = os.path.relpath(tc_path, root)
2606
2607 for name in parsed_data.tests.keys():
Anas Nashifaff616d2020-04-17 21:24:57 -04002608 tc = TestCase(root, workdir, name)
Anas Nashifce2b4182020-03-24 14:40:28 -04002609
2610 tc_dict = parsed_data.get_test(name, self.testcase_valid_keys)
2611
2612 tc.source_dir = tc_path
2613 tc.yamlfile = tc_path
2614
Anas Nashifce2b4182020-03-24 14:40:28 -04002615 tc.type = tc_dict["type"]
2616 tc.tags = tc_dict["tags"]
2617 tc.extra_args = tc_dict["extra_args"]
2618 tc.extra_configs = tc_dict["extra_configs"]
2619 tc.arch_whitelist = tc_dict["arch_whitelist"]
2620 tc.arch_exclude = tc_dict["arch_exclude"]
2621 tc.skip = tc_dict["skip"]
2622 tc.platform_exclude = tc_dict["platform_exclude"]
2623 tc.platform_whitelist = tc_dict["platform_whitelist"]
2624 tc.toolchain_exclude = tc_dict["toolchain_exclude"]
2625 tc.toolchain_whitelist = tc_dict["toolchain_whitelist"]
2626 tc.tc_filter = tc_dict["filter"]
2627 tc.timeout = tc_dict["timeout"]
2628 tc.harness = tc_dict["harness"]
2629 tc.harness_config = tc_dict["harness_config"]
Anas Nashif43275c82020-05-04 18:22:16 -04002630 if tc.harness == 'console' and not tc.harness_config:
2631 raise Exception('Harness config error: console harness defined without a configuration.')
Anas Nashifce2b4182020-03-24 14:40:28 -04002632 tc.build_only = tc_dict["build_only"]
2633 tc.build_on_all = tc_dict["build_on_all"]
2634 tc.slow = tc_dict["slow"]
2635 tc.min_ram = tc_dict["min_ram"]
2636 tc.depends_on = tc_dict["depends_on"]
2637 tc.min_flash = tc_dict["min_flash"]
2638 tc.extra_sections = tc_dict["extra_sections"]
2639
2640 tc.parse_subcases(tc_path)
2641
2642 if testcase_filter:
2643 if tc.name and tc.name in testcase_filter:
2644 self.testcases[tc.name] = tc
2645 else:
2646 self.testcases[tc.name] = tc
2647
2648 except Exception as e:
2649 logger.error("%s: can't load (skipping): %s" % (tc_path, e))
2650 self.load_errors += 1
2651
2652
2653 def get_platform(self, name):
2654 selected_platform = None
2655 for platform in self.platforms:
2656 if platform.name == name:
2657 selected_platform = platform
2658 break
2659 return selected_platform
2660
2661 def load_from_file(self, file, filter_status=[]):
2662 try:
2663 with open(file, "r") as fp:
2664 cr = csv.DictReader(fp)
2665 instance_list = []
2666 for row in cr:
2667 if row["status"] in filter_status:
2668 continue
2669 test = row["test"]
2670
2671 platform = self.get_platform(row["platform"])
2672 instance = TestInstance(self.testcases[test], platform, self.outdir)
2673 instance.check_build_or_run(
2674 self.build_only,
2675 self.enable_slow,
2676 self.device_testing,
Anas Nashifce8c12e2020-05-21 09:11:40 -04002677 self.fixtures
Anas Nashifce2b4182020-03-24 14:40:28 -04002678 )
Christian Taedcke3dbe9f22020-07-06 16:00:57 +02002679 instance.create_overlay(platform, self.enable_asan, self.enable_ubsan, self.enable_coverage, self.coverage_platform)
Anas Nashifce2b4182020-03-24 14:40:28 -04002680 instance_list.append(instance)
2681 self.add_instances(instance_list)
2682
2683 except KeyError as e:
2684 logger.error("Key error while parsing tests file.({})".format(str(e)))
2685 sys.exit(2)
2686
2687 except FileNotFoundError as e:
2688 logger.error("Couldn't find input file with list of tests. ({})".format(e))
2689 sys.exit(2)
2690
2691 def apply_filters(self, **kwargs):
2692
2693 toolchain = self.get_toolchain()
2694
2695 discards = {}
2696 platform_filter = kwargs.get('platform')
Anas Nashifaff616d2020-04-17 21:24:57 -04002697 exclude_platform = kwargs.get('exclude_platform', [])
2698 testcase_filter = kwargs.get('run_individual_tests', [])
Anas Nashifce2b4182020-03-24 14:40:28 -04002699 arch_filter = kwargs.get('arch')
2700 tag_filter = kwargs.get('tag')
2701 exclude_tag = kwargs.get('exclude_tag')
2702 all_filter = kwargs.get('all')
2703 device_testing_filter = kwargs.get('device_testing')
2704 force_toolchain = kwargs.get('force_toolchain')
Anas Nashif1a5defa2020-05-01 14:57:00 -04002705 force_platform = kwargs.get('force_platform')
Anas Nashifce2b4182020-03-24 14:40:28 -04002706
2707 logger.debug("platform filter: " + str(platform_filter))
2708 logger.debug(" arch_filter: " + str(arch_filter))
2709 logger.debug(" tag_filter: " + str(tag_filter))
2710 logger.debug(" exclude_tag: " + str(exclude_tag))
2711
2712 default_platforms = False
2713
2714 if platform_filter:
2715 platforms = list(filter(lambda p: p.name in platform_filter, self.platforms))
2716 else:
2717 platforms = self.platforms
2718
2719 if all_filter:
2720 logger.info("Selecting all possible platforms per test case")
2721 # When --all used, any --platform arguments ignored
2722 platform_filter = []
2723 elif not platform_filter:
2724 logger.info("Selecting default platforms per test case")
2725 default_platforms = True
2726
2727 logger.info("Building initial testcase list...")
2728
2729 for tc_name, tc in self.testcases.items():
2730 # list of instances per testcase, aka configurations.
2731 instance_list = []
2732 for plat in platforms:
2733 instance = TestInstance(tc, plat, self.outdir)
2734 instance.check_build_or_run(
2735 self.build_only,
2736 self.enable_slow,
2737 self.device_testing,
Anas Nashifce8c12e2020-05-21 09:11:40 -04002738 self.fixtures
Anas Nashifce2b4182020-03-24 14:40:28 -04002739 )
Anas Nashiff04461e2020-06-29 10:07:02 -04002740 for t in tc.cases:
2741 instance.results[t] = None
Anas Nashif3b86f132020-05-21 10:35:33 -04002742
2743 if device_testing_filter:
2744 for h in self.connected_hardware:
2745 if h['platform'] == plat.name:
2746 if tc.harness_config.get('fixture') in h.get('fixtures', []):
2747 instance.build_only = False
2748 instance.run = True
2749
Anas Nashif1a5defa2020-05-01 14:57:00 -04002750 if not force_platform and plat.name in exclude_platform:
Anas Nashifce2b4182020-03-24 14:40:28 -04002751 discards[instance] = "Platform is excluded on command line."
2752 continue
2753
2754 if (plat.arch == "unit") != (tc.type == "unit"):
2755 # Discard silently
2756 continue
2757
2758 if device_testing_filter and instance.build_only:
2759 discards[instance] = "Not runnable on device"
2760 continue
2761
2762 if tc.skip:
2763 discards[instance] = "Skip filter"
2764 continue
2765
2766 if tc.build_on_all and not platform_filter:
2767 platform_filter = []
2768
2769 if tag_filter and not tc.tags.intersection(tag_filter):
2770 discards[instance] = "Command line testcase tag filter"
2771 continue
2772
2773 if exclude_tag and tc.tags.intersection(exclude_tag):
2774 discards[instance] = "Command line testcase exclude filter"
2775 continue
2776
2777 if testcase_filter and tc_name not in testcase_filter:
2778 discards[instance] = "Testcase name filter"
2779 continue
2780
2781 if arch_filter and plat.arch not in arch_filter:
2782 discards[instance] = "Command line testcase arch filter"
2783 continue
2784
Anas Nashif1a5defa2020-05-01 14:57:00 -04002785 if not force_platform:
Anas Nashifce2b4182020-03-24 14:40:28 -04002786
Anas Nashif1a5defa2020-05-01 14:57:00 -04002787 if tc.arch_whitelist and plat.arch not in tc.arch_whitelist:
2788 discards[instance] = "Not in test case arch whitelist"
2789 continue
Anas Nashifce2b4182020-03-24 14:40:28 -04002790
Anas Nashif1a5defa2020-05-01 14:57:00 -04002791 if tc.arch_exclude and plat.arch in tc.arch_exclude:
2792 discards[instance] = "In test case arch exclude"
2793 continue
2794
2795 if tc.platform_exclude and plat.name in tc.platform_exclude:
2796 discards[instance] = "In test case platform exclude"
2797 continue
Anas Nashifce2b4182020-03-24 14:40:28 -04002798
2799 if tc.toolchain_exclude and toolchain in tc.toolchain_exclude:
2800 discards[instance] = "In test case toolchain exclude"
2801 continue
2802
2803 if platform_filter and plat.name not in platform_filter:
2804 discards[instance] = "Command line platform filter"
2805 continue
2806
2807 if tc.platform_whitelist and plat.name not in tc.platform_whitelist:
2808 discards[instance] = "Not in testcase platform whitelist"
2809 continue
2810
2811 if tc.toolchain_whitelist and toolchain not in tc.toolchain_whitelist:
2812 discards[instance] = "Not in testcase toolchain whitelist"
2813 continue
2814
2815 if not plat.env_satisfied:
2816 discards[instance] = "Environment ({}) not satisfied".format(", ".join(plat.env))
2817 continue
2818
2819 if not force_toolchain \
2820 and toolchain and (toolchain not in plat.supported_toolchains) \
2821 and tc.type != 'unit':
2822 discards[instance] = "Not supported by the toolchain"
2823 continue
2824
2825 if plat.ram < tc.min_ram:
2826 discards[instance] = "Not enough RAM"
2827 continue
2828
2829 if tc.depends_on:
2830 dep_intersection = tc.depends_on.intersection(set(plat.supported))
2831 if dep_intersection != set(tc.depends_on):
2832 discards[instance] = "No hardware support"
2833 continue
2834
2835 if plat.flash < tc.min_flash:
2836 discards[instance] = "Not enough FLASH"
2837 continue
2838
2839 if set(plat.ignore_tags) & tc.tags:
Anas Nashife8e367a2020-07-16 16:27:04 -04002840 discards[instance] = "Excluded tags per platform (exclude_tags)"
2841 continue
2842
2843 if not tc.tags or (plat.only_tags and tc.tags - set(plat.only_tags)):
2844 discards[instance] = "Excluded tags per platform (only_tags)"
Anas Nashifce2b4182020-03-24 14:40:28 -04002845 continue
2846
2847 # if nothing stopped us until now, it means this configuration
2848 # needs to be added.
2849 instance_list.append(instance)
2850
2851 # no configurations, so jump to next testcase
2852 if not instance_list:
2853 continue
2854
2855 # if sanitycheck was launched with no platform options at all, we
2856 # take all default platforms
2857 if default_platforms and not tc.build_on_all:
2858 if tc.platform_whitelist:
2859 a = set(self.default_platforms)
2860 b = set(tc.platform_whitelist)
2861 c = a.intersection(b)
2862 if c:
2863 aa = list(filter(lambda tc: tc.platform.name in c, instance_list))
2864 self.add_instances(aa)
2865 else:
2866 self.add_instances(instance_list[:1])
2867 else:
2868 instances = list(filter(lambda tc: tc.platform.default, instance_list))
2869 self.add_instances(instances)
2870
Anas Nashifaff616d2020-04-17 21:24:57 -04002871 for instance in list(filter(lambda inst: not inst.platform.default, instance_list)):
Anas Nashifce2b4182020-03-24 14:40:28 -04002872 discards[instance] = "Not a default test platform"
2873
2874 else:
2875 self.add_instances(instance_list)
2876
2877 for _, case in self.instances.items():
Christian Taedcke3dbe9f22020-07-06 16:00:57 +02002878 case.create_overlay(case.platform, self.enable_asan, self.enable_ubsan, self.enable_coverage, self.coverage_platform)
Anas Nashifce2b4182020-03-24 14:40:28 -04002879
2880 self.discards = discards
2881 self.selected_platforms = set(p.platform.name for p in self.instances.values())
2882
2883 return discards
2884
2885 def add_instances(self, instance_list):
2886 for instance in instance_list:
2887 self.instances[instance.name] = instance
2888
2889 def add_tasks_to_queue(self, test_only=False):
2890 for instance in self.instances.values():
2891 if test_only:
2892 if instance.run:
2893 pipeline.put({"op": "run", "test": instance, "status": "built"})
2894 else:
Anas Nashifdc43c292020-07-09 09:46:45 -04002895 if instance.status not in ['passed', 'skipped', 'error']:
Anas Nashifce2b4182020-03-24 14:40:28 -04002896 instance.status = None
2897 pipeline.put({"op": "cmake", "test": instance})
2898
2899 return "DONE FEEDING"
2900
2901 def execute(self):
Anas Nashifdc43c292020-07-09 09:46:45 -04002902
Anas Nashifce2b4182020-03-24 14:40:28 -04002903 def calc_one_elf_size(instance):
Anas Nashiff04461e2020-06-29 10:07:02 -04002904 if instance.status not in ["error", "failed", "skipped"]:
Anas Nashifce2b4182020-03-24 14:40:28 -04002905 if instance.platform.type != "native":
2906 size_calc = instance.calculate_sizes()
2907 instance.metrics["ram_size"] = size_calc.get_ram_size()
2908 instance.metrics["rom_size"] = size_calc.get_rom_size()
2909 instance.metrics["unrecognized"] = size_calc.unrecognized_sections()
2910 else:
2911 instance.metrics["ram_size"] = 0
2912 instance.metrics["rom_size"] = 0
2913 instance.metrics["unrecognized"] = []
2914
2915 instance.metrics["handler_time"] = instance.handler.duration if instance.handler else 0
2916
2917 logger.info("Adding tasks to the queue...")
2918 # We can use a with statement to ensure threads are cleaned up promptly
2919 with BoundedExecutor(bound=self.jobs, max_workers=self.jobs) as executor:
2920
2921 # start a future for a thread which sends work in through the queue
2922 future_to_test = {
2923 executor.submit(self.add_tasks_to_queue, self.test_only): 'FEEDER DONE'}
2924
2925 while future_to_test:
2926 # check for status of the futures which are currently working
2927 done, pending = concurrent.futures.wait(future_to_test, timeout=1,
2928 return_when=concurrent.futures.FIRST_COMPLETED)
2929
2930 # if there is incoming work, start a new future
2931 while not pipeline.empty():
2932 # fetch a url from the queue
2933 message = pipeline.get()
2934 test = message['test']
2935
2936 pb = ProjectBuilder(self,
2937 test,
2938 lsan=self.enable_lsan,
2939 asan=self.enable_asan,
Christian Taedcke3dbe9f22020-07-06 16:00:57 +02002940 ubsan=self.enable_ubsan,
Anas Nashifce2b4182020-03-24 14:40:28 -04002941 coverage=self.enable_coverage,
2942 extra_args=self.extra_args,
2943 device_testing=self.device_testing,
2944 cmake_only=self.cmake_only,
2945 cleanup=self.cleanup,
2946 valgrind=self.enable_valgrind,
2947 inline_logs=self.inline_logs,
Anas Nashifce2b4182020-03-24 14:40:28 -04002948 generator=self.generator,
Anas Nashiff6462a32020-03-29 19:02:51 -04002949 generator_cmd=self.generator_cmd,
Anas Nashif50925412020-07-16 17:25:19 -04002950 verbose=self.verbose,
2951 warnings_as_errors=self.warnings_as_errors
Anas Nashifce2b4182020-03-24 14:40:28 -04002952 )
2953 future_to_test[executor.submit(pb.process, message)] = test.name
2954
2955 # process any completed futures
2956 for future in done:
2957 test = future_to_test[future]
2958 try:
2959 data = future.result()
2960 except Exception as exc:
Martí Bolívar9861e5d2020-07-23 10:02:19 -07002961 logger.error('%r generated an exception:' % (test,))
2962 for line in traceback.format_exc().splitlines():
2963 logger.error(line)
Anas Nashifce2b4182020-03-24 14:40:28 -04002964 sys.exit('%r generated an exception: %s' % (test, exc))
2965
2966 else:
2967 if data:
2968 logger.debug(data)
2969
2970 # remove the now completed future
2971 del future_to_test[future]
2972
2973 for future in pending:
2974 test = future_to_test[future]
2975
2976 try:
2977 future.result(timeout=180)
2978 except concurrent.futures.TimeoutError:
2979 logger.warning("{} stuck?".format(test))
2980
2981 if self.enable_size_report and not self.cmake_only:
2982 # Parallelize size calculation
2983 executor = concurrent.futures.ThreadPoolExecutor(self.jobs)
2984 futures = [executor.submit(calc_one_elf_size, instance)
2985 for instance in self.instances.values()]
2986 concurrent.futures.wait(futures)
2987 else:
2988 for instance in self.instances.values():
2989 instance.metrics["ram_size"] = 0
2990 instance.metrics["rom_size"] = 0
2991 instance.metrics["handler_time"] = instance.handler.duration if instance.handler else 0
2992 instance.metrics["unrecognized"] = []
2993
2994 def discard_report(self, filename):
2995
2996 try:
Aastha Groverdcbd9152020-06-16 10:19:51 -07002997 if not self.discards:
Anas Nashifce2b4182020-03-24 14:40:28 -04002998 raise SanityRuntimeError("apply_filters() hasn't been run!")
2999 except Exception as e:
3000 logger.error(str(e))
3001 sys.exit(2)
3002
3003 with open(filename, "wt") as csvfile:
3004 fieldnames = ["test", "arch", "platform", "reason"]
3005 cw = csv.DictWriter(csvfile, fieldnames, lineterminator=os.linesep)
3006 cw.writeheader()
3007 for instance, reason in sorted(self.discards.items()):
3008 rowdict = {"test": instance.testcase.name,
3009 "arch": instance.platform.arch,
3010 "platform": instance.platform.name,
3011 "reason": reason}
3012 cw.writerow(rowdict)
3013
Anas Nashif6915adf2020-04-22 09:39:42 -04003014 def target_report(self, outdir, suffix, append=False):
Anas Nashifce2b4182020-03-24 14:40:28 -04003015 platforms = {inst.platform.name for _, inst in self.instances.items()}
3016 for platform in platforms:
Anas Nashif6915adf2020-04-22 09:39:42 -04003017 if suffix:
3018 filename = os.path.join(outdir,"{}_{}.xml".format(platform, suffix))
3019 else:
3020 filename = os.path.join(outdir,"{}.xml".format(platform))
Anas Nashif90415502020-04-11 22:15:04 -04003021 self.xunit_report(filename, platform, full_report=True, append=append)
Anas Nashifce2b4182020-03-24 14:40:28 -04003022
Anas Nashif90415502020-04-11 22:15:04 -04003023
3024 @staticmethod
3025 def process_log(log_file):
3026 filtered_string = ""
3027 if os.path.exists(log_file):
3028 with open(log_file, "rb") as f:
3029 log = f.read().decode("utf-8")
3030 filtered_string = ''.join(filter(lambda x: x in string.printable, log))
3031
3032 return filtered_string
3033
Anas Nashifa53c8132020-05-05 09:32:46 -04003034
Anas Nashif90415502020-04-11 22:15:04 -04003035 def xunit_report(self, filename, platform=None, full_report=False, append=False):
Anas Nashifa53c8132020-05-05 09:32:46 -04003036 total = 0
3037 if platform:
3038 selected = [platform]
3039 else:
3040 selected = self.selected_platforms
Anas Nashif90415502020-04-11 22:15:04 -04003041
Anas Nashif90415502020-04-11 22:15:04 -04003042 if os.path.exists(filename) and append:
3043 tree = ET.parse(filename)
3044 eleTestsuites = tree.getroot()
Anas Nashif90415502020-04-11 22:15:04 -04003045 else:
Anas Nashifce2b4182020-03-24 14:40:28 -04003046 eleTestsuites = ET.Element('testsuites')
Anas Nashifce2b4182020-03-24 14:40:28 -04003047
Anas Nashifa53c8132020-05-05 09:32:46 -04003048 for p in selected:
3049 inst = self.get_platform_instances(p)
3050 fails = 0
3051 passes = 0
3052 errors = 0
3053 skips = 0
3054 duration = 0
3055
3056 for _, instance in inst.items():
3057 handler_time = instance.metrics.get('handler_time', 0)
3058 duration += handler_time
Anas Nashif9e1be4c2020-07-23 08:20:49 -04003059 if full_report and not instance.build_only:
Anas Nashifa53c8132020-05-05 09:32:46 -04003060 for k in instance.results.keys():
3061 if instance.results[k] == 'PASS':
3062 passes += 1
3063 elif instance.results[k] == 'BLOCK':
3064 errors += 1
3065 elif instance.results[k] == 'SKIP':
3066 skips += 1
3067 else:
3068 fails += 1
3069 else:
Anas Nashiff04461e2020-06-29 10:07:02 -04003070 if instance.status in ["error", "failed", "timeout"]:
Anas Nashifa53c8132020-05-05 09:32:46 -04003071 if instance.reason in ['build_error', 'handler_crash']:
3072 errors += 1
3073 else:
3074 fails += 1
3075 elif instance.status == 'skipped':
3076 skips += 1
Anas Nashif9e1be4c2020-07-23 08:20:49 -04003077 elif instance.status == 'passed':
Anas Nashifa53c8132020-05-05 09:32:46 -04003078 passes += 1
Anas Nashif9e1be4c2020-07-23 08:20:49 -04003079 else:
3080 logger.error(f"Unknown status {instance.status}")
Anas Nashifa53c8132020-05-05 09:32:46 -04003081
3082 total = (errors + passes + fails + skips)
3083 # do not produce a report if no tests were actually run (only built)
3084 if total == 0:
Anas Nashif90415502020-04-11 22:15:04 -04003085 continue
Anas Nashifce2b4182020-03-24 14:40:28 -04003086
Anas Nashifa53c8132020-05-05 09:32:46 -04003087 run = p
3088 eleTestsuite = None
3089
3090 # When we re-run the tests, we re-use the results and update only with
3091 # the newly run tests.
3092 if os.path.exists(filename) and append:
Anas Nashiff04461e2020-06-29 10:07:02 -04003093 ts = eleTestsuites.findall(f'testsuite/[@name="{p}"]')
3094 if ts:
3095 eleTestsuite = ts[0]
3096 eleTestsuite.attrib['failures'] = "%d" % fails
3097 eleTestsuite.attrib['errors'] = "%d" % errors
3098 eleTestsuite.attrib['skip'] = "%d" % skips
3099 else:
3100 logger.info(f"Did not find any existing results for {p}")
3101 eleTestsuite = ET.SubElement(eleTestsuites, 'testsuite',
3102 name=run, time="%f" % duration,
3103 tests="%d" % (total),
3104 failures="%d" % fails,
3105 errors="%d" % (errors), skip="%s" % (skips))
3106
Anas Nashif90415502020-04-11 22:15:04 -04003107 else:
Anas Nashifa53c8132020-05-05 09:32:46 -04003108 eleTestsuite = ET.SubElement(eleTestsuites, 'testsuite',
3109 name=run, time="%f" % duration,
3110 tests="%d" % (total),
3111 failures="%d" % fails,
3112 errors="%d" % (errors), skip="%s" % (skips))
Anas Nashif90415502020-04-11 22:15:04 -04003113
Anas Nashifa53c8132020-05-05 09:32:46 -04003114 for _, instance in inst.items():
3115 if full_report:
3116 tname = os.path.basename(instance.testcase.name)
3117 else:
3118 tname = instance.testcase.id
Anas Nashif90415502020-04-11 22:15:04 -04003119
Anas Nashifa53c8132020-05-05 09:32:46 -04003120
3121 handler_time = instance.metrics.get('handler_time', 0)
3122
3123 if full_report:
3124 for k in instance.results.keys():
Anas Nashifa53c8132020-05-05 09:32:46 -04003125 # remove testcases that are being re-run from exiting reports
3126 for tc in eleTestsuite.findall(f'testcase/[@name="{k}"]'):
3127 eleTestsuite.remove(tc)
3128
3129 classname = ".".join(tname.split(".")[:2])
3130 eleTestcase = ET.SubElement(
3131 eleTestsuite, 'testcase',
3132 classname=classname,
3133 name="%s" % (k), time="%f" % handler_time)
Anas Nashif9e1be4c2020-07-23 08:20:49 -04003134
3135 if instance.results[k] in ['FAIL', 'BLOCK'] or \
3136 (instance.build_only and instance.status in ["error", "failed", "timeout"]):
Anas Nashifa53c8132020-05-05 09:32:46 -04003137 if instance.results[k] == 'FAIL':
3138 el = ET.SubElement(
3139 eleTestcase,
3140 'failure',
3141 type="failure",
3142 message="failed")
3143 else:
Anas Nashifa53c8132020-05-05 09:32:46 -04003144 el = ET.SubElement(
3145 eleTestcase,
3146 'error',
3147 type="failure",
3148 message="failed")
3149 p = os.path.join(self.outdir, instance.platform.name, instance.testcase.name)
3150 log_file = os.path.join(p, "handler.log")
3151 el.text = self.process_log(log_file)
3152
Anas Nashif9e1be4c2020-07-23 08:20:49 -04003153 elif instance.results[k] == 'PASS' \
3154 or (instance.build_only and instance.status in ["passed"]):
Anas Nashiff04461e2020-06-29 10:07:02 -04003155 pass
Anas Nashif9e1be4c2020-07-23 08:20:49 -04003156 elif instance.results[k] == 'SKIP' \
3157 or (instance.build_only and instance.status in ["skipped"]):
Anas Nashiff04461e2020-06-29 10:07:02 -04003158 el = ET.SubElement(eleTestcase, 'skipped', type="skipped", message="Skipped")
3159 else:
Anas Nashifce2b4182020-03-24 14:40:28 -04003160 el = ET.SubElement(
3161 eleTestcase,
Anas Nashiff04461e2020-06-29 10:07:02 -04003162 'error',
3163 type="error",
3164 message=f"{instance.reason}")
Anas Nashifa53c8132020-05-05 09:32:46 -04003165 else:
3166 if platform:
3167 classname = ".".join(instance.testcase.name.split(".")[:2])
3168 else:
3169 classname = p + ":" + ".".join(instance.testcase.name.split(".")[:2])
Anas Nashifce2b4182020-03-24 14:40:28 -04003170
Anas Nashiff04461e2020-06-29 10:07:02 -04003171 # remove testcases that are being re-run from exiting reports
3172 for tc in eleTestsuite.findall(f'testcase/[@classname="{classname}"]'):
3173 eleTestsuite.remove(tc)
3174
Anas Nashifa53c8132020-05-05 09:32:46 -04003175 eleTestcase = ET.SubElement(eleTestsuite, 'testcase',
3176 classname=classname,
3177 name="%s" % (instance.testcase.name),
3178 time="%f" % handler_time)
Anas Nashif9e1be4c2020-07-23 08:20:49 -04003179
Anas Nashiff04461e2020-06-29 10:07:02 -04003180 if instance.status in ["error", "failed", "timeout"]:
Anas Nashifa53c8132020-05-05 09:32:46 -04003181 failure = ET.SubElement(
Anas Nashifce2b4182020-03-24 14:40:28 -04003182 eleTestcase,
Anas Nashifa53c8132020-05-05 09:32:46 -04003183 'failure',
3184 type="failure",
Maciej Perkowskib2fa99c2020-05-21 14:45:29 +02003185 message=instance.reason)
Anas Nashiff04461e2020-06-29 10:07:02 -04003186
Anas Nashifa53c8132020-05-05 09:32:46 -04003187 p = ("%s/%s/%s" % (self.outdir, instance.platform.name, instance.testcase.name))
3188 bl = os.path.join(p, "build.log")
3189 hl = os.path.join(p, "handler.log")
3190 log_file = bl
3191 if instance.reason != 'Build error':
3192 if os.path.exists(hl):
3193 log_file = hl
3194 else:
3195 log_file = bl
Anas Nashifce2b4182020-03-24 14:40:28 -04003196
Anas Nashifa53c8132020-05-05 09:32:46 -04003197 failure.text = self.process_log(log_file)
Anas Nashifce2b4182020-03-24 14:40:28 -04003198
Anas Nashifa53c8132020-05-05 09:32:46 -04003199 elif instance.status == "skipped":
3200 ET.SubElement(eleTestcase, 'skipped', type="skipped", message="Skipped")
Anas Nashifce2b4182020-03-24 14:40:28 -04003201
3202 result = ET.tostring(eleTestsuites)
3203 with open(filename, 'wb') as report:
3204 report.write(result)
3205
Anas Nashif1c2f1272020-07-23 09:56:01 -04003206 return fails, passes, errors, skips
Anas Nashif90415502020-04-11 22:15:04 -04003207
Anas Nashifce2b4182020-03-24 14:40:28 -04003208 def csv_report(self, filename):
3209 with open(filename, "wt") as csvfile:
3210 fieldnames = ["test", "arch", "platform", "status",
3211 "extra_args", "handler", "handler_time", "ram_size",
3212 "rom_size"]
3213 cw = csv.DictWriter(csvfile, fieldnames, lineterminator=os.linesep)
3214 cw.writeheader()
3215 for instance in self.instances.values():
3216 rowdict = {"test": instance.testcase.name,
3217 "arch": instance.platform.arch,
3218 "platform": instance.platform.name,
3219 "extra_args": " ".join(instance.testcase.extra_args),
3220 "handler": instance.platform.simulation}
3221
3222 rowdict["status"] = instance.status
Anas Nashiff04461e2020-06-29 10:07:02 -04003223 if instance.status not in ["error", "failed", "timeout"]:
Anas Nashifce2b4182020-03-24 14:40:28 -04003224 if instance.handler:
3225 rowdict["handler_time"] = instance.metrics.get("handler_time", 0)
3226 ram_size = instance.metrics.get("ram_size", 0)
3227 rom_size = instance.metrics.get("rom_size", 0)
3228 rowdict["ram_size"] = ram_size
3229 rowdict["rom_size"] = rom_size
3230 cw.writerow(rowdict)
3231
3232 def get_testcase(self, identifier):
3233 results = []
3234 for _, tc in self.testcases.items():
3235 for case in tc.cases:
3236 if case == identifier:
3237 results.append(tc)
3238 return results
3239
3240
3241class CoverageTool:
3242 """ Base class for every supported coverage tool
3243 """
3244
3245 def __init__(self):
Anas Nashiff6462a32020-03-29 19:02:51 -04003246 self.gcov_tool = None
3247 self.base_dir = None
Anas Nashifce2b4182020-03-24 14:40:28 -04003248
3249 @staticmethod
3250 def factory(tool):
3251 if tool == 'lcov':
Anas Nashiff6462a32020-03-29 19:02:51 -04003252 t = Lcov()
3253 elif tool == 'gcovr':
3254 t = Lcov()
3255 else:
3256 logger.error("Unsupported coverage tool specified: {}".format(tool))
3257 return None
3258
Anas Nashiff6462a32020-03-29 19:02:51 -04003259 return t
Anas Nashifce2b4182020-03-24 14:40:28 -04003260
3261 @staticmethod
3262 def retrieve_gcov_data(intput_file):
Anas Nashiff6462a32020-03-29 19:02:51 -04003263 logger.debug("Working on %s" % intput_file)
Anas Nashifce2b4182020-03-24 14:40:28 -04003264 extracted_coverage_info = {}
3265 capture_data = False
3266 capture_complete = False
3267 with open(intput_file, 'r') as fp:
3268 for line in fp.readlines():
3269 if re.search("GCOV_COVERAGE_DUMP_START", line):
3270 capture_data = True
3271 continue
3272 if re.search("GCOV_COVERAGE_DUMP_END", line):
3273 capture_complete = True
3274 break
3275 # Loop until the coverage data is found.
3276 if not capture_data:
3277 continue
3278 if line.startswith("*"):
3279 sp = line.split("<")
3280 if len(sp) > 1:
3281 # Remove the leading delimiter "*"
3282 file_name = sp[0][1:]
3283 # Remove the trailing new line char
3284 hex_dump = sp[1][:-1]
3285 else:
3286 continue
3287 else:
3288 continue
3289 extracted_coverage_info.update({file_name: hex_dump})
3290 if not capture_data:
3291 capture_complete = True
3292 return {'complete': capture_complete, 'data': extracted_coverage_info}
3293
3294 @staticmethod
3295 def create_gcda_files(extracted_coverage_info):
Anas Nashiff6462a32020-03-29 19:02:51 -04003296 logger.debug("Generating gcda files")
Anas Nashifce2b4182020-03-24 14:40:28 -04003297 for filename, hexdump_val in extracted_coverage_info.items():
3298 # if kobject_hash is given for coverage gcovr fails
3299 # hence skipping it problem only in gcovr v4.1
3300 if "kobject_hash" in filename:
3301 filename = (filename[:-4]) + "gcno"
3302 try:
3303 os.remove(filename)
3304 except Exception:
3305 pass
3306 continue
3307
3308 with open(filename, 'wb') as fp:
3309 fp.write(bytes.fromhex(hexdump_val))
3310
3311 def generate(self, outdir):
3312 for filename in glob.glob("%s/**/handler.log" % outdir, recursive=True):
3313 gcov_data = self.__class__.retrieve_gcov_data(filename)
3314 capture_complete = gcov_data['complete']
3315 extracted_coverage_info = gcov_data['data']
3316 if capture_complete:
3317 self.__class__.create_gcda_files(extracted_coverage_info)
3318 logger.debug("Gcov data captured: {}".format(filename))
3319 else:
3320 logger.error("Gcov data capture incomplete: {}".format(filename))
3321
3322 with open(os.path.join(outdir, "coverage.log"), "a") as coveragelog:
3323 ret = self._generate(outdir, coveragelog)
3324 if ret == 0:
3325 logger.info("HTML report generated: {}".format(
3326 os.path.join(outdir, "coverage", "index.html")))
3327
3328
3329class Lcov(CoverageTool):
3330
3331 def __init__(self):
3332 super().__init__()
3333 self.ignores = []
3334
3335 def add_ignore_file(self, pattern):
3336 self.ignores.append('*' + pattern + '*')
3337
3338 def add_ignore_directory(self, pattern):
3339 self.ignores.append(pattern + '/*')
3340
3341 def _generate(self, outdir, coveragelog):
3342 coveragefile = os.path.join(outdir, "coverage.info")
3343 ztestfile = os.path.join(outdir, "ztest.info")
3344 subprocess.call(["lcov", "--gcov-tool", self.gcov_tool,
3345 "--capture", "--directory", outdir,
3346 "--rc", "lcov_branch_coverage=1",
3347 "--output-file", coveragefile], stdout=coveragelog)
3348 # We want to remove tests/* and tests/ztest/test/* but save tests/ztest
3349 subprocess.call(["lcov", "--gcov-tool", self.gcov_tool, "--extract",
3350 coveragefile,
Anas Nashiff6462a32020-03-29 19:02:51 -04003351 os.path.join(self.base_dir, "tests", "ztest", "*"),
Anas Nashifce2b4182020-03-24 14:40:28 -04003352 "--output-file", ztestfile,
3353 "--rc", "lcov_branch_coverage=1"], stdout=coveragelog)
3354
3355 if os.path.exists(ztestfile) and os.path.getsize(ztestfile) > 0:
3356 subprocess.call(["lcov", "--gcov-tool", self.gcov_tool, "--remove",
3357 ztestfile,
Anas Nashiff6462a32020-03-29 19:02:51 -04003358 os.path.join(self.base_dir, "tests/ztest/test/*"),
Anas Nashifce2b4182020-03-24 14:40:28 -04003359 "--output-file", ztestfile,
3360 "--rc", "lcov_branch_coverage=1"],
3361 stdout=coveragelog)
3362 files = [coveragefile, ztestfile]
3363 else:
3364 files = [coveragefile]
3365
3366 for i in self.ignores:
3367 subprocess.call(
3368 ["lcov", "--gcov-tool", self.gcov_tool, "--remove",
3369 coveragefile, i, "--output-file",
3370 coveragefile, "--rc", "lcov_branch_coverage=1"],
3371 stdout=coveragelog)
3372
3373 # The --ignore-errors source option is added to avoid it exiting due to
3374 # samples/application_development/external_lib/
3375 return subprocess.call(["genhtml", "--legend", "--branch-coverage",
3376 "--ignore-errors", "source",
3377 "-output-directory",
3378 os.path.join(outdir, "coverage")] + files,
3379 stdout=coveragelog)
3380
3381
3382class Gcovr(CoverageTool):
3383
3384 def __init__(self):
3385 super().__init__()
3386 self.ignores = []
3387
3388 def add_ignore_file(self, pattern):
3389 self.ignores.append('.*' + pattern + '.*')
3390
3391 def add_ignore_directory(self, pattern):
3392 self.ignores.append(pattern + '/.*')
3393
3394 @staticmethod
3395 def _interleave_list(prefix, list):
3396 tuple_list = [(prefix, item) for item in list]
3397 return [item for sublist in tuple_list for item in sublist]
3398
3399 def _generate(self, outdir, coveragelog):
3400 coveragefile = os.path.join(outdir, "coverage.json")
3401 ztestfile = os.path.join(outdir, "ztest.json")
3402
3403 excludes = Gcovr._interleave_list("-e", self.ignores)
3404
3405 # We want to remove tests/* and tests/ztest/test/* but save tests/ztest
Anas Nashiff6462a32020-03-29 19:02:51 -04003406 subprocess.call(["gcovr", "-r", self.base_dir, "--gcov-executable",
Anas Nashifce2b4182020-03-24 14:40:28 -04003407 self.gcov_tool, "-e", "tests/*"] + excludes +
3408 ["--json", "-o", coveragefile, outdir],
3409 stdout=coveragelog)
3410
Anas Nashiff6462a32020-03-29 19:02:51 -04003411 subprocess.call(["gcovr", "-r", self.base_dir, "--gcov-executable",
Anas Nashifce2b4182020-03-24 14:40:28 -04003412 self.gcov_tool, "-f", "tests/ztest", "-e",
3413 "tests/ztest/test/*", "--json", "-o", ztestfile,
3414 outdir], stdout=coveragelog)
3415
3416 if os.path.exists(ztestfile) and os.path.getsize(ztestfile) > 0:
3417 files = [coveragefile, ztestfile]
3418 else:
3419 files = [coveragefile]
3420
3421 subdir = os.path.join(outdir, "coverage")
3422 os.makedirs(subdir, exist_ok=True)
3423
3424 tracefiles = self._interleave_list("--add-tracefile", files)
3425
Anas Nashiff6462a32020-03-29 19:02:51 -04003426 return subprocess.call(["gcovr", "-r", self.base_dir, "--html",
Anas Nashifce2b4182020-03-24 14:40:28 -04003427 "--html-details"] + tracefiles +
3428 ["-o", os.path.join(subdir, "index.html")],
3429 stdout=coveragelog)
3430class HardwareMap:
3431
3432 schema_path = os.path.join(ZEPHYR_BASE, "scripts", "sanity_chk", "hwmap-schema.yaml")
3433
3434 manufacturer = [
3435 'ARM',
3436 'SEGGER',
3437 'MBED',
3438 'STMicroelectronics',
3439 'Atmel Corp.',
3440 'Texas Instruments',
3441 'Silicon Labs',
3442 'NXP Semiconductors',
3443 'Microchip Technology Inc.',
3444 'FTDI',
3445 'Digilent'
3446 ]
3447
3448 runner_mapping = {
3449 'pyocd': [
3450 'DAPLink CMSIS-DAP',
3451 'MBED CMSIS-DAP'
3452 ],
3453 'jlink': [
3454 'J-Link',
3455 'J-Link OB'
3456 ],
3457 'openocd': [
Erwan Gouriou2339fa02020-07-07 17:15:22 +02003458 'STM32 STLink', '^XDS110.*', 'STLINK-V3'
Anas Nashifce2b4182020-03-24 14:40:28 -04003459 ],
3460 'dediprog': [
3461 'TTL232R-3V3',
3462 'MCP2200 USB Serial Port Emulator'
3463 ]
3464 }
3465
3466 def __init__(self):
3467 self.detected = []
3468 self.connected_hardware = []
3469
Andrei Emeltchenkod8b845b2020-03-31 13:22:30 +03003470 def load_device_from_cmdline(self, serial, platform, is_pty):
Anas Nashifce2b4182020-03-24 14:40:28 -04003471 device = {
Andrei Emeltchenkod8b845b2020-03-31 13:22:30 +03003472 "serial": None,
Anas Nashifce2b4182020-03-24 14:40:28 -04003473 "platform": platform,
Andrei Emeltchenkod8b845b2020-03-31 13:22:30 +03003474 "serial_pty": None,
Anas Nashifce2b4182020-03-24 14:40:28 -04003475 "counter": 0,
3476 "available": True,
3477 "connected": True
3478 }
Andrei Emeltchenkod8b845b2020-03-31 13:22:30 +03003479
3480 if is_pty:
3481 device['serial_pty'] = serial
3482 else:
3483 device['serial'] = serial
3484
Anas Nashifce2b4182020-03-24 14:40:28 -04003485 self.connected_hardware.append(device)
3486
3487 def load_hardware_map(self, map_file):
3488 hwm_schema = scl.yaml_load(self.schema_path)
3489 self.connected_hardware = scl.yaml_load_verify(map_file, hwm_schema)
3490 for i in self.connected_hardware:
3491 i['counter'] = 0
3492
Martí Bolívar07dce822020-04-13 16:50:51 -07003493 def scan_hw(self, persistent=False):
Anas Nashifce2b4182020-03-24 14:40:28 -04003494 from serial.tools import list_ports
3495
Martí Bolívar07dce822020-04-13 16:50:51 -07003496 if persistent and platform.system() == 'Linux':
3497 # On Linux, /dev/serial/by-id provides symlinks to
3498 # '/dev/ttyACMx' nodes using names which are unique as
3499 # long as manufacturers fill out USB metadata nicely.
3500 #
3501 # This creates a map from '/dev/ttyACMx' device nodes
3502 # to '/dev/serial/by-id/usb-...' symlinks. The symlinks
3503 # go into the hardware map because they stay the same
3504 # even when the user unplugs / replugs the device.
3505 #
3506 # Some inexpensive USB/serial adapters don't result
3507 # in unique names here, though, so use of this feature
3508 # requires explicitly setting persistent=True.
3509 by_id = Path('/dev/serial/by-id')
3510 def readlink(link):
3511 return str((by_id / link).resolve())
3512
3513 persistent_map = {readlink(link): str(link)
3514 for link in by_id.iterdir()}
3515 else:
3516 persistent_map = {}
3517
Anas Nashifce2b4182020-03-24 14:40:28 -04003518 serial_devices = list_ports.comports()
3519 logger.info("Scanning connected hardware...")
3520 for d in serial_devices:
3521 if d.manufacturer in self.manufacturer:
3522
3523 # TI XDS110 can have multiple serial devices for a single board
3524 # assume endpoint 0 is the serial, skip all others
3525 if d.manufacturer == 'Texas Instruments' and not d.location.endswith('0'):
3526 continue
3527 s_dev = {}
3528 s_dev['platform'] = "unknown"
3529 s_dev['id'] = d.serial_number
Martí Bolívar07dce822020-04-13 16:50:51 -07003530 s_dev['serial'] = persistent_map.get(d.device, d.device)
Anas Nashifce2b4182020-03-24 14:40:28 -04003531 s_dev['product'] = d.product
3532 s_dev['runner'] = 'unknown'
3533 for runner, _ in self.runner_mapping.items():
3534 products = self.runner_mapping.get(runner)
3535 if d.product in products:
3536 s_dev['runner'] = runner
3537 continue
3538 # Try regex matching
3539 for p in products:
3540 if re.match(p, d.product):
3541 s_dev['runner'] = runner
3542
3543 s_dev['available'] = True
3544 s_dev['connected'] = True
3545 self.detected.append(s_dev)
3546 else:
3547 logger.warning("Unsupported device (%s): %s" % (d.manufacturer, d))
3548
3549 def write_map(self, hwm_file):
3550 # use existing map
3551 if os.path.exists(hwm_file):
3552 with open(hwm_file, 'r') as yaml_file:
Anas Nashifae61b7e2020-07-06 11:30:55 -04003553 hwm = yaml.load(yaml_file, Loader=SafeLoader)
Øyvind Rønningstad4813f462020-07-01 16:49:38 +02003554 hwm.sort(key=lambda x: x['serial'] or '')
3555
Anas Nashifce2b4182020-03-24 14:40:28 -04003556 # disconnect everything
3557 for h in hwm:
3558 h['connected'] = False
3559 h['serial'] = None
3560
Øyvind Rønningstad4813f462020-07-01 16:49:38 +02003561 self.detected.sort(key=lambda x: x['serial'] or '')
Anas Nashifce2b4182020-03-24 14:40:28 -04003562 for d in self.detected:
3563 for h in hwm:
Øyvind Rønningstad4813f462020-07-01 16:49:38 +02003564 if d['id'] == h['id'] and d['product'] == h['product'] and not h['connected'] and not d.get('match', False):
Anas Nashifce2b4182020-03-24 14:40:28 -04003565 h['connected'] = True
3566 h['serial'] = d['serial']
3567 d['match'] = True
3568
3569 new = list(filter(lambda n: not n.get('match', False), self.detected))
3570 hwm = hwm + new
3571
3572 logger.info("Registered devices:")
3573 self.dump(hwm)
3574
3575 with open(hwm_file, 'w') as yaml_file:
Anas Nashifae61b7e2020-07-06 11:30:55 -04003576 yaml.dump(hwm, yaml_file, Dumper=Dumper, default_flow_style=False)
Anas Nashifce2b4182020-03-24 14:40:28 -04003577
3578 else:
3579 # create new file
3580 with open(hwm_file, 'w') as yaml_file:
Anas Nashifae61b7e2020-07-06 11:30:55 -04003581 yaml.dump(self.detected, yaml_file, Dumper=Dumper, default_flow_style=False)
Anas Nashifce2b4182020-03-24 14:40:28 -04003582 logger.info("Detected devices:")
3583 self.dump(self.detected)
3584
3585 @staticmethod
3586 def dump(hwmap=[], filtered=[], header=[], connected_only=False):
3587 print("")
3588 table = []
3589 if not header:
3590 header = ["Platform", "ID", "Serial device"]
3591 for p in sorted(hwmap, key=lambda i: i['platform']):
3592 platform = p.get('platform')
3593 connected = p.get('connected', False)
3594 if filtered and platform not in filtered:
3595 continue
3596
3597 if not connected_only or connected:
3598 table.append([platform, p.get('id', None), p.get('serial')])
3599
3600 print(tabulate(table, headers=header, tablefmt="github"))
3601
3602
3603def size_report(sc):
3604 logger.info(sc.filename)
3605 logger.info("SECTION NAME VMA LMA SIZE HEX SZ TYPE")
3606 for i in range(len(sc.sections)):
3607 v = sc.sections[i]
3608
3609 logger.info("%-17s 0x%08x 0x%08x %8d 0x%05x %-7s" %
3610 (v["name"], v["virt_addr"], v["load_addr"], v["size"], v["size"],
3611 v["type"]))
3612
3613 logger.info("Totals: %d bytes (ROM), %d bytes (RAM)" %
3614 (sc.rom_size, sc.ram_size))
3615 logger.info("")
3616
3617
3618
3619def export_tests(filename, tests):
3620 with open(filename, "wt") as csvfile:
3621 fieldnames = ['section', 'subsection', 'title', 'reference']
3622 cw = csv.DictWriter(csvfile, fieldnames, lineterminator=os.linesep)
3623 for test in tests:
3624 data = test.split(".")
3625 if len(data) > 1:
3626 subsec = " ".join(data[1].split("_")).title()
3627 rowdict = {
3628 "section": data[0].capitalize(),
3629 "subsection": subsec,
3630 "title": test,
3631 "reference": test
3632 }
3633 cw.writerow(rowdict)
3634 else:
3635 logger.info("{} can't be exported".format(test))