blob: 4277935153f910de6c8f0cd681b7e4b9be728ebd [file] [log] [blame]
Andrew Boie08ce5a52016-02-22 13:28:10 -08001#!/usr/bin/env python3
Anas Nashifa792a3d2017-04-04 18:47:49 -04002# vim: set syntax=python ts=4 :
Andrew Boie6acbe632015-07-17 12:03:52 -07003"""Zephyr Sanity Tests
4
5This script scans for the set of unit test applications in the git
6repository and attempts to execute them. By default, it tries to
7build each test case on one platform per architecture, using a precedence
Paul Sokolovskyff70add2017-06-16 01:31:54 +03008list defined in an architecture configuration file, and if possible
Andrew Boie6acbe632015-07-17 12:03:52 -07009run the tests in the QEMU emulator.
10
Anas Nashifa792a3d2017-04-04 18:47:49 -040011Test cases are detected by the presence of a 'testcase.yaml' or a sample.yaml
12files in the application's project directory. This file may contain one or more
13blocks, each identifying a test scenario. The title of the block is a name for
14the test case, which only needs to be unique for the test cases specified in
15that testcase meta-data. The full canonical name for each test case is <path to
16test case>/<block>.
Andrew Boie6acbe632015-07-17 12:03:52 -070017
Anas Nashifa792a3d2017-04-04 18:47:49 -040018Each test block in the testcase meta data can define the following key/value pairs:
Andrew Boie6acbe632015-07-17 12:03:52 -070019
Anas Nashifa792a3d2017-04-04 18:47:49 -040020 tags: <list of tags> (required)
Andrew Boie6acbe632015-07-17 12:03:52 -070021 A set of string tags for the testcase. Usually pertains to
22 functional domains but can be anything. Command line invocations
23 of this script can filter the set of tests to run based on tag.
24
Anas Nashifa792a3d2017-04-04 18:47:49 -040025 skip: <True|False> (default False)
Anas Nashif2bd99bc2015-10-12 13:10:57 -040026 skip testcase unconditionally. This can be used for broken tests.
27
Anas Nashifa792a3d2017-04-04 18:47:49 -040028 slow: <True|False> (default False)
Andrew Boie6bb087c2016-02-10 13:39:00 -080029 Don't run this test case unless --enable-slow was passed in on the
30 command line. Intended for time-consuming test cases that are only
31 run under certain circumstances, like daily builds. These test cases
32 are still compiled.
33
Anas Nashifa792a3d2017-04-04 18:47:49 -040034 extra_args: <list of extra arguments>
Andrew Boie6acbe632015-07-17 12:03:52 -070035 Extra arguments to pass to Make when building or running the
36 test case.
37
Anas Nashifa792a3d2017-04-04 18:47:49 -040038 build_only: <True|False> (default False)
Andrew Boie6acbe632015-07-17 12:03:52 -070039 If true, don't try to run the test under QEMU even if the
40 selected platform supports it.
41
Anas Nashifa792a3d2017-04-04 18:47:49 -040042 build_on_all: <True|False> (default False)
43 If true, attempt to build test on all available platforms.
44
45 depends_on: <list of features>
46 A board or platform can announce what features it supports, this option
47 will enable the test only those platforms that provide this feature.
48
49 min_ram: <integer>
50 minimum amount of RAM needed for this test to build and run. This is
51 compared with information provided by the board metadata.
52
53 min_flash: <integer>
54 minimum amount of ROM needed for this test to build and run. This is
55 compared with information provided by the board metadata.
56
57 timeout: <number of seconds>
Andrew Boie6acbe632015-07-17 12:03:52 -070058 Length of time to run test in QEMU before automatically killing it.
59 Default to 60 seconds.
60
Anas Nashifa792a3d2017-04-04 18:47:49 -040061 arch_whitelist: <list of arches, such as x86, arm, arc>
Andrew Boie6acbe632015-07-17 12:03:52 -070062 Set of architectures that this test case should only be run for.
63
Anas Nashifa792a3d2017-04-04 18:47:49 -040064 arch_exclude: <list of arches, such as x86, arm, arc>
Anas Nashif30d13872015-10-05 10:02:45 -040065 Set of architectures that this test case should not run on.
66
Anas Nashifa792a3d2017-04-04 18:47:49 -040067 platform_whitelist: <list of platforms>
Anas Nashif30d13872015-10-05 10:02:45 -040068 Set of platforms that this test case should only be run for.
69
Anas Nashifa792a3d2017-04-04 18:47:49 -040070 platform_exclude: <list of platforms>
Anas Nashif30d13872015-10-05 10:02:45 -040071 Set of platforms that this test case should not run on.
Andrew Boie6acbe632015-07-17 12:03:52 -070072
Anas Nashifa792a3d2017-04-04 18:47:49 -040073 extra_sections: <list of extra binary sections>
Andrew Boie52fef672016-11-29 12:21:59 -080074 When computing sizes, sanitycheck will report errors if it finds
75 extra, unexpected sections in the Zephyr binary unless they are named
76 here. They will not be included in the size calculation.
77
Anas Nashifa792a3d2017-04-04 18:47:49 -040078 filter: <expression>
Andrew Boie3ea78922016-03-24 14:46:00 -070079 Filter whether the testcase should be run by evaluating an expression
80 against an environment containing the following values:
81
82 { ARCH : <architecture>,
83 PLATFORM : <platform>,
Javier B Perez79414542016-08-08 12:24:59 -050084 <all CONFIG_* key/value pairs in the test's generated defconfig>,
85 *<env>: any environment variable available
Andrew Boie3ea78922016-03-24 14:46:00 -070086 }
87
88 The grammar for the expression language is as follows:
89
90 expression ::= expression "and" expression
91 | expression "or" expression
92 | "not" expression
93 | "(" expression ")"
94 | symbol "==" constant
95 | symbol "!=" constant
96 | symbol "<" number
97 | symbol ">" number
98 | symbol ">=" number
99 | symbol "<=" number
100 | symbol "in" list
Andrew Boie7a87f9f2016-06-02 12:27:54 -0700101 | symbol ":" string
Andrew Boie3ea78922016-03-24 14:46:00 -0700102 | symbol
103
104 list ::= "[" list_contents "]"
105
106 list_contents ::= constant
107 | list_contents "," constant
108
109 constant ::= number
110 | string
111
112
113 For the case where expression ::= symbol, it evaluates to true
114 if the symbol is defined to a non-empty string.
115
116 Operator precedence, starting from lowest to highest:
117
118 or (left associative)
119 and (left associative)
120 not (right associative)
121 all comparison operators (non-associative)
122
123 arch_whitelist, arch_exclude, platform_whitelist, platform_exclude
124 are all syntactic sugar for these expressions. For instance
125
126 arch_exclude = x86 arc
127
128 Is the same as:
129
130 filter = not ARCH in ["x86", "arc"]
Andrew Boie6acbe632015-07-17 12:03:52 -0700131
Andrew Boie7a87f9f2016-06-02 12:27:54 -0700132 The ':' operator compiles the string argument as a regular expression,
133 and then returns a true value only if the symbol's value in the environment
134 matches. For example, if CONFIG_SOC="quark_se" then
135
136 filter = CONFIG_SOC : "quark.*"
137
138 Would match it.
139
Anas Nashifa792a3d2017-04-04 18:47:49 -0400140The set of test cases that actually run depends on directives in the testcase
141filed and options passed in on the command line. If there is any confusion,
142running with -v or --discard-report can help show why particular test cases
143were skipped.
Andrew Boie6acbe632015-07-17 12:03:52 -0700144
145Metrics (such as pass/fail state and binary size) for the last code
146release are stored in scripts/sanity_chk/sanity_last_release.csv.
147To update this, pass the --all --release options.
148
Genaro Saucedo Tejada28bba922016-10-24 18:00:58 -0500149To load arguments from a file, write '+' before the file name, e.g.,
150+file_name. File content must be one or more valid arguments separated by
151line break instead of white spaces.
152
Andrew Boie6acbe632015-07-17 12:03:52 -0700153Most everyday users will run with no arguments.
154"""
155
156import argparse
157import os
158import sys
Andrew Boie08ce5a52016-02-22 13:28:10 -0800159import configparser
Andrew Boie6acbe632015-07-17 12:03:52 -0700160import re
161import tempfile
162import subprocess
163import multiprocessing
164import select
165import shutil
166import signal
167import threading
168import time
169import csv
Andrew Boie5d4eb782015-10-02 10:04:56 -0700170import glob
Daniel Leung6b170072016-04-07 12:10:25 -0700171import concurrent
172import concurrent.futures
Anas Nashifb3311ed2017-04-13 14:44:48 -0400173import xml.etree.ElementTree as ET
Anas Nashife6fcc012017-05-17 09:29:09 -0400174from xml.sax.saxutils import escape
Anas Nashif035799f2017-05-13 21:31:53 -0400175from collections import OrderedDict
176from itertools import islice
Anas Nashifa792a3d2017-04-04 18:47:49 -0400177import yaml
Andrew Boie6acbe632015-07-17 12:03:52 -0700178
179if "ZEPHYR_BASE" not in os.environ:
Anas Nashif427cdd32015-08-06 07:25:42 -0400180 sys.stderr.write("$ZEPHYR_BASE environment variable undefined.\n")
Andrew Boie6acbe632015-07-17 12:03:52 -0700181 exit(1)
182ZEPHYR_BASE = os.environ["ZEPHYR_BASE"]
Andrew Boie3ea78922016-03-24 14:46:00 -0700183
184sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts/"))
185
186import expr_parser
187
Andrew Boie6acbe632015-07-17 12:03:52 -0700188VERBOSE = 0
189LAST_SANITY = os.path.join(ZEPHYR_BASE, "scripts", "sanity_chk",
190 "last_sanity.csv")
Anas Nashifb3311ed2017-04-13 14:44:48 -0400191LAST_SANITY_XUNIT = os.path.join(ZEPHYR_BASE, "scripts", "sanity_chk",
192 "last_sanity.xml")
Andrew Boie6acbe632015-07-17 12:03:52 -0700193RELEASE_DATA = os.path.join(ZEPHYR_BASE, "scripts", "sanity_chk",
194 "sanity_last_release.csv")
Daniel Leung6b170072016-04-07 12:10:25 -0700195CPU_COUNTS = multiprocessing.cpu_count()
Andrew Boie6acbe632015-07-17 12:03:52 -0700196
197if os.isatty(sys.stdout.fileno()):
198 TERMINAL = True
199 COLOR_NORMAL = '\033[0m'
200 COLOR_RED = '\033[91m'
201 COLOR_GREEN = '\033[92m'
202 COLOR_YELLOW = '\033[93m'
203else:
204 TERMINAL = False
205 COLOR_NORMAL = ""
206 COLOR_RED = ""
207 COLOR_GREEN = ""
208 COLOR_YELLOW = ""
209
210class SanityCheckException(Exception):
211 pass
212
213class SanityRuntimeError(SanityCheckException):
214 pass
215
216class ConfigurationError(SanityCheckException):
217 def __init__(self, cfile, message):
218 self.cfile = cfile
219 self.message = message
220
221 def __str__(self):
222 return repr(self.cfile + ": " + self.message)
223
224class MakeError(SanityCheckException):
225 pass
226
227class BuildError(MakeError):
228 pass
229
230class ExecutionError(MakeError):
231 pass
232
Inaky Perez-Gonzalez9a36cb62016-11-29 10:43:40 -0800233log_file = None
234
Andrew Boie6acbe632015-07-17 12:03:52 -0700235# Debug Functions
Andrew Boie08ce5a52016-02-22 13:28:10 -0800236def info(what):
237 sys.stdout.write(what + "\n")
Inaky Perez-Gonzalez9a36cb62016-11-29 10:43:40 -0800238 if log_file:
239 log_file.write(what + "\n")
240 log_file.flush()
Andrew Boie6acbe632015-07-17 12:03:52 -0700241
242def error(what):
243 sys.stderr.write(COLOR_RED + what + COLOR_NORMAL + "\n")
Inaky Perez-Gonzalez9a36cb62016-11-29 10:43:40 -0800244 if log_file:
245 log_file(what + "\n")
246 log_file.flush()
Andrew Boie6acbe632015-07-17 12:03:52 -0700247
Andrew Boie08ce5a52016-02-22 13:28:10 -0800248def debug(what):
249 if VERBOSE >= 1:
250 info(what)
251
Andrew Boie6acbe632015-07-17 12:03:52 -0700252def verbose(what):
253 if VERBOSE >= 2:
Andrew Boie08ce5a52016-02-22 13:28:10 -0800254 info(what)
Andrew Boie6acbe632015-07-17 12:03:52 -0700255
Jaakko Hannikainenca505f82016-08-22 15:03:46 +0300256class Handler:
257 RUN_PASSED = "PROJECT EXECUTION SUCCESSFUL"
258 RUN_FAILED = "PROJECT EXECUTION FAILED"
259 def __init__(self, name, outdir, log_fn, timeout, unit=False):
260 """Constructor
261
262 @param name Arbitrary name of the created thread
263 @param outdir Working directory, should be where qemu.pid gets created
264 by kbuild
265 @param log_fn Absolute path to write out QEMU's log data
266 @param timeout Kill the QEMU process if it doesn't finish up within
267 the given number of seconds
268 """
269 self.lock = threading.Lock()
270 self.state = "waiting"
271 self.metrics = {}
272 self.metrics["qemu_time"] = 0
273 self.metrics["ram_size"] = 0
274 self.metrics["rom_size"] = 0
275 self.unit = unit
276
277 def set_state(self, state, metrics):
278 self.lock.acquire()
279 self.state = state
280 self.metrics.update(metrics)
281 self.lock.release()
282
283 def get_state(self):
284 self.lock.acquire()
285 ret = (self.state, self.metrics)
286 self.lock.release()
287 return ret
288
289class UnitHandler(Handler):
Jaakko Hannikainen54c90bc2016-08-31 14:17:03 +0300290 def __init__(self, name, sourcedir, outdir, run_log, valgrind_log, timeout):
Jaakko Hannikainenca505f82016-08-22 15:03:46 +0300291 """Constructor
292
293 @param name Arbitrary name of the created thread
294 @param outdir Working directory containing the test binary
295 @param run_log Absolute path to runtime logs
296 @param valgrind Absolute path to valgrind's log
297 @param timeout Kill the QEMU process if it doesn't finish up within
298 the given number of seconds
299 """
300 super().__init__(name, outdir, run_log, timeout, True)
301
302 self.timeout = timeout
Jaakko Hannikainen54c90bc2016-08-31 14:17:03 +0300303 self.sourcedir = sourcedir
Jaakko Hannikainenca505f82016-08-22 15:03:46 +0300304 self.outdir = outdir
305 self.run_log = run_log
306 self.valgrind_log = valgrind_log
307 self.returncode = 0
308 self.set_state("running", {})
309
310 def handle(self):
311 out_state = "failed"
312
313 with open(self.run_log, "wt") as rl, open(self.valgrind_log, "wt") as vl:
314 try:
315 binary = os.path.join(self.outdir, "testbinary")
316 command = [binary]
317 if shutil.which("valgrind"):
318 command = ["valgrind", "--error-exitcode=2",
319 "--leak-check=full"] + command
320 returncode = subprocess.call(command, timeout=self.timeout,
321 stdout=rl, stderr=vl)
322 self.returncode = returncode
323 if returncode != 0:
324 if self.returncode == 1:
325 out_state = "failed"
326 else:
327 out_state = "failed valgrind"
328 else:
329 out_state = "passed"
330 except subprocess.TimeoutExpired:
331 out_state = "timeout"
332 self.returncode = 1
333
Jaakko Hannikainen54c90bc2016-08-31 14:17:03 +0300334 returncode = subprocess.call(["GCOV_PREFIX=" + self.outdir, "gcov", self.sourcedir, "-s", self.outdir], shell=True)
335
Jaakko Hannikainenca505f82016-08-22 15:03:46 +0300336 self.set_state(out_state, {})
337
338class QEMUHandler(Handler):
Andrew Boie6acbe632015-07-17 12:03:52 -0700339 """Spawns a thread to monitor QEMU output from pipes
340
341 We pass QEMU_PIPE to 'make qemu' and monitor the pipes for output.
342 We need to do this as once qemu starts, it runs forever until killed.
343 Test cases emit special messages to the console as they run, we check
344 for these to collect whether the test passed or failed.
345 """
Andrew Boie6acbe632015-07-17 12:03:52 -0700346
347 @staticmethod
348 def _thread(handler, timeout, outdir, logfile, fifo_fn, pid_fn, results):
349 fifo_in = fifo_fn + ".in"
350 fifo_out = fifo_fn + ".out"
351
352 # These in/out nodes are named from QEMU's perspective, not ours
353 if os.path.exists(fifo_in):
354 os.unlink(fifo_in)
355 os.mkfifo(fifo_in)
356 if os.path.exists(fifo_out):
357 os.unlink(fifo_out)
358 os.mkfifo(fifo_out)
359
360 # We don't do anything with out_fp but we need to open it for
361 # writing so that QEMU doesn't block, due to the way pipes work
362 out_fp = open(fifo_in, "wb")
363 # Disable internal buffering, we don't
364 # want read() or poll() to ever block if there is data in there
365 in_fp = open(fifo_out, "rb", buffering=0)
Andrew Boie08ce5a52016-02-22 13:28:10 -0800366 log_out_fp = open(logfile, "wt")
Andrew Boie6acbe632015-07-17 12:03:52 -0700367
368 start_time = time.time()
369 timeout_time = start_time + timeout
370 p = select.poll()
371 p.register(in_fp, select.POLLIN)
372
373 metrics = {}
374 line = ""
375 while True:
376 this_timeout = int((timeout_time - time.time()) * 1000)
377 if this_timeout < 0 or not p.poll(this_timeout):
378 out_state = "timeout"
379 break
380
Genaro Saucedo Tejada2968b362016-08-09 15:11:53 -0500381 try:
382 c = in_fp.read(1).decode("utf-8")
383 except UnicodeDecodeError:
384 # Test is writing something weird, fail
385 out_state = "unexpected byte"
386 break
387
Andrew Boie6acbe632015-07-17 12:03:52 -0700388 if c == "":
389 # EOF, this shouldn't happen unless QEMU crashes
390 out_state = "unexpected eof"
391 break
392 line = line + c
393 if c != "\n":
394 continue
395
Jaakko Hannikainenca505f82016-08-22 15:03:46 +0300396 # line contains a full line of data output from QEMU
Andrew Boie6acbe632015-07-17 12:03:52 -0700397 log_out_fp.write(line)
398 log_out_fp.flush()
399 line = line.strip()
400 verbose("QEMU: %s" % line)
401
Jaakko Hannikainenca505f82016-08-22 15:03:46 +0300402 if line == handler.RUN_PASSED:
Andrew Boie6acbe632015-07-17 12:03:52 -0700403 out_state = "passed"
404 break
405
Jaakko Hannikainenca505f82016-08-22 15:03:46 +0300406 if line == handler.RUN_FAILED:
Andrew Boie6acbe632015-07-17 12:03:52 -0700407 out_state = "failed"
408 break
409
410 # TODO: Add support for getting numerical performance data
411 # from test cases. Will involve extending test case reporting
412 # APIs. Add whatever gets reported to the metrics dictionary
413 line = ""
414
415 metrics["qemu_time"] = time.time() - start_time
416 verbose("QEMU complete (%s) after %f seconds" %
417 (out_state, metrics["qemu_time"]))
418 handler.set_state(out_state, metrics)
419
420 log_out_fp.close()
421 out_fp.close()
422 in_fp.close()
423
424 pid = int(open(pid_fn).read())
425 os.unlink(pid_fn)
Andrew Boie08ce5a52016-02-22 13:28:10 -0800426 try:
427 os.kill(pid, signal.SIGTERM)
428 except ProcessLookupError:
429 # Oh well, as long as it's dead! User probably sent Ctrl-C
430 pass
431
Andrew Boie6acbe632015-07-17 12:03:52 -0700432 os.unlink(fifo_in)
433 os.unlink(fifo_out)
434
Andrew Boie6acbe632015-07-17 12:03:52 -0700435 def __init__(self, name, outdir, log_fn, timeout):
436 """Constructor
437
438 @param name Arbitrary name of the created thread
Jaakko Hannikainenca505f82016-08-22 15:03:46 +0300439 @param outdir Working directory, should be where qemu.pid gets created
Andrew Boie6acbe632015-07-17 12:03:52 -0700440 by kbuild
441 @param log_fn Absolute path to write out QEMU's log data
442 @param timeout Kill the QEMU process if it doesn't finish up within
443 the given number of seconds
444 """
Jaakko Hannikainenca505f82016-08-22 15:03:46 +0300445 super().__init__(name, outdir, log_fn, timeout)
Andrew Boie6acbe632015-07-17 12:03:52 -0700446 self.results = {}
Andrew Boie6acbe632015-07-17 12:03:52 -0700447
448 # We pass this to QEMU which looks for fifos with .in and .out
449 # suffixes.
450 self.fifo_fn = os.path.join(outdir, "qemu-fifo")
451
452 self.pid_fn = os.path.join(outdir, "qemu.pid")
453 if os.path.exists(self.pid_fn):
454 os.unlink(self.pid_fn)
455
456 self.log_fn = log_fn
457 self.thread = threading.Thread(name=name, target=QEMUHandler._thread,
Jaakko Hannikainenca505f82016-08-22 15:03:46 +0300458 args=(self, timeout, outdir,
459 self.log_fn, self.fifo_fn,
460 self.pid_fn, self.results))
Andrew Boie6acbe632015-07-17 12:03:52 -0700461 self.thread.daemon = True
462 verbose("Spawning QEMU process for %s" % name)
463 self.thread.start()
464
Andrew Boie6acbe632015-07-17 12:03:52 -0700465 def get_fifo(self):
466 return self.fifo_fn
467
Andrew Boie6acbe632015-07-17 12:03:52 -0700468class SizeCalculator:
Andrew Boie73b4ee62015-10-07 11:33:22 -0700469
Andrew Boie8eed4b02017-06-14 16:08:16 -0700470 alloc_sections = ["bss", "noinit", "app_bss", "app_noinit"]
Andrew Boie18ba1532017-01-17 13:47:06 -0800471 rw_sections = ["datas", "initlevel", "_k_task_list", "_k_event_list",
Allan Stephens3f5c74c2016-11-01 14:37:15 -0500472 "_k_memory_pool", "exceptions", "initshell",
473 "_static_thread_area", "_k_timer_area",
474 "_k_mem_slab_area", "_k_mem_pool_area",
475 "_k_sem_area", "_k_mutex_area", "_k_alert_area",
476 "_k_fifo_area", "_k_lifo_area", "_k_stack_area",
Tomasz Bursztykab56c4df2016-08-18 14:07:22 +0200477 "_k_msgq_area", "_k_mbox_area", "_k_pipe_area",
Tomasz Bursztykae38a9e82017-03-08 09:30:03 +0100478 "net_if", "net_if_event", "net_stack", "net_l2_data",
Andrew Boie8eed4b02017-06-14 16:08:16 -0700479 "_k_queue_area", "_net_buf_pool_area", "app_datas" ]
Andrew Boie73b4ee62015-10-07 11:33:22 -0700480 # These get copied into RAM only on non-XIP
Andrew Boie3b930212016-06-07 13:11:45 -0700481 ro_sections = ["text", "ctors", "init_array", "reset",
Anas Nashifbfcdfaf2016-12-15 10:02:14 -0500482 "rodata", "devconfig", "net_l2", "vector"]
Andrew Boie73b4ee62015-10-07 11:33:22 -0700483
Andrew Boie52fef672016-11-29 12:21:59 -0800484 def __init__(self, filename, extra_sections):
Andrew Boie6acbe632015-07-17 12:03:52 -0700485 """Constructor
486
Andrew Boiebbd670c2015-08-17 13:16:11 -0700487 @param filename Path to the output binary
488 The <filename> is parsed by objdump to determine section sizes
Andrew Boie6acbe632015-07-17 12:03:52 -0700489 """
Andrew Boie6acbe632015-07-17 12:03:52 -0700490 # Make sure this is an ELF binary
Andrew Boiebbd670c2015-08-17 13:16:11 -0700491 with open(filename, "rb") as f:
Andrew Boie6acbe632015-07-17 12:03:52 -0700492 magic = f.read(4)
493
Andrew Boie08ce5a52016-02-22 13:28:10 -0800494 if (magic != b'\x7fELF'):
Andrew Boiebbd670c2015-08-17 13:16:11 -0700495 raise SanityRuntimeError("%s is not an ELF binary" % filename)
Andrew Boie6acbe632015-07-17 12:03:52 -0700496
497 # Search for CONFIG_XIP in the ELF's list of symbols using NM and AWK.
498 # GREP can not be used as it returns an error if the symbol is not found.
Andrew Boiebbd670c2015-08-17 13:16:11 -0700499 is_xip_command = "nm " + filename + " | awk '/CONFIG_XIP/ { print $3 }'"
Andrew Boie8f0211d2016-03-02 20:40:29 -0800500 is_xip_output = subprocess.check_output(is_xip_command, shell=True,
501 stderr=subprocess.STDOUT).decode("utf-8").strip()
502 if is_xip_output.endswith("no symbols"):
503 raise SanityRuntimeError("%s has no symbol information" % filename)
Andrew Boie6acbe632015-07-17 12:03:52 -0700504 self.is_xip = (len(is_xip_output) != 0)
505
Andrew Boiebbd670c2015-08-17 13:16:11 -0700506 self.filename = filename
Andrew Boie73b4ee62015-10-07 11:33:22 -0700507 self.sections = []
508 self.rom_size = 0
Andrew Boie6acbe632015-07-17 12:03:52 -0700509 self.ram_size = 0
Andrew Boie52fef672016-11-29 12:21:59 -0800510 self.extra_sections = extra_sections
Andrew Boie6acbe632015-07-17 12:03:52 -0700511
512 self._calculate_sizes()
513
514 def get_ram_size(self):
515 """Get the amount of RAM the application will use up on the device
516
517 @return amount of RAM, in bytes
518 """
Andrew Boie73b4ee62015-10-07 11:33:22 -0700519 return self.ram_size
Andrew Boie6acbe632015-07-17 12:03:52 -0700520
521 def get_rom_size(self):
522 """Get the size of the data that this application uses on device's flash
523
524 @return amount of ROM, in bytes
525 """
Andrew Boie73b4ee62015-10-07 11:33:22 -0700526 return self.rom_size
Andrew Boie6acbe632015-07-17 12:03:52 -0700527
528 def unrecognized_sections(self):
529 """Get a list of sections inside the binary that weren't recognized
530
David B. Kinder29963c32017-06-16 12:32:42 -0700531 @return list of unrecognized section names
Andrew Boie6acbe632015-07-17 12:03:52 -0700532 """
533 slist = []
Andrew Boie73b4ee62015-10-07 11:33:22 -0700534 for v in self.sections:
Andrew Boie6acbe632015-07-17 12:03:52 -0700535 if not v["recognized"]:
Andrew Boie73b4ee62015-10-07 11:33:22 -0700536 slist.append(v["name"])
Andrew Boie6acbe632015-07-17 12:03:52 -0700537 return slist
538
539 def _calculate_sizes(self):
540 """ Calculate RAM and ROM usage by section """
Andrew Boiebbd670c2015-08-17 13:16:11 -0700541 objdump_command = "objdump -h " + self.filename
Andrew Boie6acbe632015-07-17 12:03:52 -0700542 objdump_output = subprocess.check_output(objdump_command,
Andrew Boie08ce5a52016-02-22 13:28:10 -0800543 shell=True).decode("utf-8").splitlines()
Andrew Boie6acbe632015-07-17 12:03:52 -0700544
545 for line in objdump_output:
546 words = line.split()
547
548 if (len(words) == 0): # Skip lines that are too short
549 continue
550
551 index = words[0]
552 if (not index[0].isdigit()): # Skip lines that do not start
553 continue # with a digit
554
555 name = words[1] # Skip lines with section names
556 if (name[0] == '.'): # starting with '.'
557 continue
558
Andrew Boie73b4ee62015-10-07 11:33:22 -0700559 # TODO this doesn't actually reflect the size in flash or RAM as
560 # it doesn't include linker-imposed padding between sections.
561 # It is close though.
Andrew Boie6acbe632015-07-17 12:03:52 -0700562 size = int(words[2], 16)
Andrew Boie9882dcd2015-10-07 14:25:51 -0700563 if size == 0:
564 continue
565
Andrew Boie73b4ee62015-10-07 11:33:22 -0700566 load_addr = int(words[4], 16)
567 virt_addr = int(words[3], 16)
Andrew Boie6acbe632015-07-17 12:03:52 -0700568
569 # Add section to memory use totals (for both non-XIP and XIP scenarios)
Andrew Boie6acbe632015-07-17 12:03:52 -0700570 # Unrecognized section names are not included in the calculations.
Andrew Boie6acbe632015-07-17 12:03:52 -0700571 recognized = True
Andrew Boie73b4ee62015-10-07 11:33:22 -0700572 if name in SizeCalculator.alloc_sections:
573 self.ram_size += size
574 stype = "alloc"
575 elif name in SizeCalculator.rw_sections:
576 self.ram_size += size
577 self.rom_size += size
578 stype = "rw"
579 elif name in SizeCalculator.ro_sections:
580 self.rom_size += size
581 if not self.is_xip:
582 self.ram_size += size
583 stype = "ro"
Andrew Boie6acbe632015-07-17 12:03:52 -0700584 else:
Andrew Boie73b4ee62015-10-07 11:33:22 -0700585 stype = "unknown"
Andrew Boie52fef672016-11-29 12:21:59 -0800586 if name not in self.extra_sections:
587 recognized = False
Andrew Boie6acbe632015-07-17 12:03:52 -0700588
Andrew Boie73b4ee62015-10-07 11:33:22 -0700589 self.sections.append({"name" : name, "load_addr" : load_addr,
590 "size" : size, "virt_addr" : virt_addr,
Andy Ross8d801a52016-10-04 11:48:21 -0700591 "type" : stype, "recognized" : recognized})
Andrew Boie6acbe632015-07-17 12:03:52 -0700592
593
594class MakeGoal:
595 """Metadata class representing one of the sub-makes called by MakeGenerator
596
David B. Kinder29963c32017-06-16 12:32:42 -0700597 MakeGenerator returns a dictionary of these which can then be associated
Andrew Boie6acbe632015-07-17 12:03:52 -0700598 with TestInstances to get a complete picture of what happened during a test.
599 MakeGenerator is used for tasks outside of building tests (such as
600 defconfigs) which is why MakeGoal is a separate class from TestInstance.
601 """
602 def __init__(self, name, text, qemu, make_log, build_log, run_log,
603 qemu_log):
604 self.name = name
605 self.text = text
606 self.qemu = qemu
607 self.make_log = make_log
608 self.build_log = build_log
609 self.run_log = run_log
610 self.qemu_log = qemu_log
611 self.make_state = "waiting"
612 self.failed = False
613 self.finished = False
614 self.reason = None
615 self.metrics = {}
616
617 def get_error_log(self):
618 if self.make_state == "waiting":
619 # Shouldn't ever see this; breakage in the main Makefile itself.
620 return self.make_log
621 elif self.make_state == "building":
622 # Failure when calling the sub-make to build the code
623 return self.build_log
624 elif self.make_state == "running":
625 # Failure in sub-make for "make qemu", qemu probably failed to start
626 return self.run_log
627 elif self.make_state == "finished":
628 # QEMU finished, but timed out or otherwise wasn't successful
629 return self.qemu_log
630
631 def fail(self, reason):
632 self.failed = True
633 self.finished = True
634 self.reason = reason
635
636 def success(self):
637 self.finished = True
638
639 def __str__(self):
640 if self.finished:
641 if self.failed:
642 return "[%s] failed (%s: see %s)" % (self.name, self.reason,
643 self.get_error_log())
644 else:
645 return "[%s] passed" % self.name
646 else:
647 return "[%s] in progress (%s)" % (self.name, self.make_state)
648
649
650class MakeGenerator:
651 """Generates a Makefile which just calls a bunch of sub-make sessions
652
653 In any given test suite we may need to build dozens if not hundreds of
654 test cases. The cleanest way to parallelize this is to just let Make
655 do the parallelization, sharing the jobserver among all the different
656 sub-make targets.
657 """
658
659 GOAL_HEADER_TMPL = """.PHONY: {goal}
660{goal}:
661"""
662
663 MAKE_RULE_TMPL = """\t@echo sanity_test_{phase} {goal} >&2
Andrew Boief7a6e282017-02-02 13:04:57 -0800664\t$(MAKE) -C {directory} O={outdir} V={verb} EXTRA_CFLAGS="-Werror {cflags}" EXTRA_ASMFLAGS=-Wa,--fatal-warnings EXTRA_LDFLAGS=--fatal-warnings {args} >{logfile} 2>&1
Andrew Boie6acbe632015-07-17 12:03:52 -0700665"""
666
667 GOAL_FOOTER_TMPL = "\t@echo sanity_test_finished {goal} >&2\n\n"
668
Jaakko Hannikainenca505f82016-08-22 15:03:46 +0300669 re_make = re.compile("sanity_test_([A-Za-z0-9]+) (.+)|$|make[:] \*\*\* \[(.+:.+: )?(.+)\] Error.+$")
Andrew Boie6acbe632015-07-17 12:03:52 -0700670
Anas Nashife3febe92016-11-30 14:25:44 -0500671 def __init__(self, base_outdir, asserts=False, deprecations=False, ccache=0):
Andrew Boie6acbe632015-07-17 12:03:52 -0700672 """MakeGenerator constructor
673
674 @param base_outdir Intended to be the base out directory. A make.log
675 file will be created here which contains the output of the
676 top-level Make session, as well as the dynamic control Makefile
677 @param verbose If true, pass V=1 to all the sub-makes which greatly
678 increases their verbosity
679 """
680 self.goals = {}
681 if not os.path.exists(base_outdir):
682 os.makedirs(base_outdir)
683 self.logfile = os.path.join(base_outdir, "make.log")
684 self.makefile = os.path.join(base_outdir, "Makefile")
Andrew Boie55121052016-07-20 11:52:04 -0700685 self.asserts = asserts
Anas Nashife3febe92016-11-30 14:25:44 -0500686 self.deprecations = deprecations
Kumar Galad5719a62016-10-26 12:30:26 -0500687 self.ccache = ccache
Andrew Boie6acbe632015-07-17 12:03:52 -0700688
689 def _get_rule_header(self, name):
690 return MakeGenerator.GOAL_HEADER_TMPL.format(goal=name)
691
692 def _get_sub_make(self, name, phase, workdir, outdir, logfile, args):
693 verb = "1" if VERBOSE else "0"
694 args = " ".join(args)
Anas Nashife3febe92016-11-30 14:25:44 -0500695
Andrew Boie55121052016-07-20 11:52:04 -0700696 if self.asserts:
697 cflags="-DCONFIG_ASSERT=1 -D__ASSERT_ON=2"
698 else:
699 cflags=""
Anas Nashife3febe92016-11-30 14:25:44 -0500700
701 if self.deprecations:
702 cflags = cflags + " -Wno-deprecated-declarations"
Andrew Boief7a6e282017-02-02 13:04:57 -0800703
704 if self.ccache:
705 args = args + " USE_CCACHE=1"
706
707 return MakeGenerator.MAKE_RULE_TMPL.format(phase=phase, goal=name,
Andrew Boie55121052016-07-20 11:52:04 -0700708 outdir=outdir, cflags=cflags,
Andrew Boie6acbe632015-07-17 12:03:52 -0700709 directory=workdir, verb=verb,
710 args=args, logfile=logfile)
711
712 def _get_rule_footer(self, name):
713 return MakeGenerator.GOAL_FOOTER_TMPL.format(goal=name)
714
715 def _add_goal(self, outdir):
716 if not os.path.exists(outdir):
717 os.makedirs(outdir)
718
Kumar Gala914d92e2017-06-22 16:19:11 -0500719 def add_build_goal(self, name, directory, outdir, args, buildlog):
Andrew Boie6acbe632015-07-17 12:03:52 -0700720 """Add a goal to invoke a Kbuild session
721
722 @param name A unique string name for this build goal. The results
723 dictionary returned by execute() will be keyed by this name.
724 @param directory Absolute path to working directory, will be passed
725 to make -C
726 @param outdir Absolute path to output directory, will be passed to
727 Kbuild via -O=<path>
728 @param args Extra command line arguments to pass to 'make', typically
729 environment variables or specific Make goals
730 """
731 self._add_goal(outdir)
Kumar Gala914d92e2017-06-22 16:19:11 -0500732 build_logfile = os.path.join(outdir, buildlog)
Andrew Boie6acbe632015-07-17 12:03:52 -0700733 text = (self._get_rule_header(name) +
734 self._get_sub_make(name, "building", directory,
735 outdir, build_logfile, args) +
736 self._get_rule_footer(name))
737 self.goals[name] = MakeGoal(name, text, None, self.logfile, build_logfile,
738 None, None)
739
740 def add_qemu_goal(self, name, directory, outdir, args, timeout=30):
741 """Add a goal to build a Zephyr project and then run it under QEMU
742
743 The generated make goal invokes Make twice, the first time it will
744 build the default goal, and the second will invoke the 'qemu' goal.
745 The output of the QEMU session will be monitored, and terminated
746 either upon pass/fail result of the test program, or the timeout
747 is reached.
748
749 @param name A unique string name for this build goal. The results
750 dictionary returned by execute() will be keyed by this name.
751 @param directory Absolute path to working directory, will be passed
752 to make -C
753 @param outdir Absolute path to output directory, will be passed to
754 Kbuild via -O=<path>
755 @param args Extra command line arguments to pass to 'make', typically
756 environment variables. Do not pass specific Make goals here.
757 @param timeout Maximum length of time QEMU session should be allowed
758 to run before automatically killing it. Default is 30 seconds.
759 """
760
761 self._add_goal(outdir)
762 build_logfile = os.path.join(outdir, "build.log")
763 run_logfile = os.path.join(outdir, "run.log")
764 qemu_logfile = os.path.join(outdir, "qemu.log")
765
766 q = QEMUHandler(name, outdir, qemu_logfile, timeout)
767 args.append("QEMU_PIPE=%s" % q.get_fifo())
768 text = (self._get_rule_header(name) +
769 self._get_sub_make(name, "building", directory,
770 outdir, build_logfile, args) +
771 self._get_sub_make(name, "running", directory,
772 outdir, run_logfile,
Mazen NEIFER7f046372017-01-31 12:41:33 +0100773 args + ["run"]) +
Andrew Boie6acbe632015-07-17 12:03:52 -0700774 self._get_rule_footer(name))
775 self.goals[name] = MakeGoal(name, text, q, self.logfile, build_logfile,
776 run_logfile, qemu_logfile)
777
Jaakko Hannikainen54c90bc2016-08-31 14:17:03 +0300778 def add_unit_goal(self, name, directory, outdir, args, timeout=30, coverage=False):
Jaakko Hannikainenca505f82016-08-22 15:03:46 +0300779 self._add_goal(outdir)
780 build_logfile = os.path.join(outdir, "build.log")
781 run_logfile = os.path.join(outdir, "run.log")
782 qemu_logfile = os.path.join(outdir, "qemu.log")
783 valgrind_logfile = os.path.join(outdir, "valgrind.log")
Jaakko Hannikainen54c90bc2016-08-31 14:17:03 +0300784 if coverage:
785 args += ["COVERAGE=1"]
Jaakko Hannikainenca505f82016-08-22 15:03:46 +0300786
787 # we handle running in the UnitHandler class
788 text = (self._get_rule_header(name) +
789 self._get_sub_make(name, "building", directory,
790 outdir, build_logfile, args) +
791 self._get_rule_footer(name))
Jaakko Hannikainen54c90bc2016-08-31 14:17:03 +0300792 q = UnitHandler(name, directory, outdir, run_logfile, valgrind_logfile, timeout)
Jaakko Hannikainenca505f82016-08-22 15:03:46 +0300793 self.goals[name] = MakeGoal(name, text, q, self.logfile, build_logfile,
794 run_logfile, valgrind_logfile)
795
Andrew Boie6acbe632015-07-17 12:03:52 -0700796
Jaakko Hannikainen54c90bc2016-08-31 14:17:03 +0300797 def add_test_instance(self, ti, build_only=False, enable_slow=False, coverage=False,
Andrew Boieba612002016-09-01 10:41:03 -0700798 extra_args=[]):
Andrew Boie6acbe632015-07-17 12:03:52 -0700799 """Add a goal to build/test a TestInstance object
800
801 @param ti TestInstance object to build. The status dictionary returned
802 by execute() will be keyed by its .name field.
803 """
804 args = ti.test.extra_args[:]
Anas Nashifa792a3d2017-04-04 18:47:49 -0400805 args.extend(["ARCH=%s" % ti.platform.arch,
Anas Nashifc7406082015-12-13 15:00:31 -0500806 "BOARD=%s" % ti.platform.name])
Andrew Boieba612002016-09-01 10:41:03 -0700807 args.extend(extra_args)
Andrew Boie6bb087c2016-02-10 13:39:00 -0800808 if (ti.platform.qemu_support and (not ti.build_only) and
809 (not build_only) and (enable_slow or not ti.test.slow)):
Andrew Boie6acbe632015-07-17 12:03:52 -0700810 self.add_qemu_goal(ti.name, ti.test.code_location, ti.outdir,
811 args, ti.test.timeout)
Jaakko Hannikainenca505f82016-08-22 15:03:46 +0300812 elif ti.test.type == "unit":
813 self.add_unit_goal(ti.name, ti.test.code_location, ti.outdir,
Jaakko Hannikainen54c90bc2016-08-31 14:17:03 +0300814 args, ti.test.timeout, coverage)
Andrew Boie6acbe632015-07-17 12:03:52 -0700815 else:
Kumar Gala914d92e2017-06-22 16:19:11 -0500816 self.add_build_goal(ti.name, ti.test.code_location, ti.outdir,
817 args, "build.log")
Andrew Boie6acbe632015-07-17 12:03:52 -0700818
819 def execute(self, callback_fn=None, context=None):
820 """Execute all the registered build goals
821
822 @param callback_fn If not None, a callback function will be called
823 as individual goals transition between states. This function
824 should accept two parameters: a string state and an arbitrary
825 context object, supplied here
826 @param context Context object to pass to the callback function.
827 Type and semantics are specific to that callback function.
828 @return A dictionary mapping goal names to final status.
829 """
830
Andrew Boie08ce5a52016-02-22 13:28:10 -0800831 with open(self.makefile, "wt") as tf, \
Andrew Boie6acbe632015-07-17 12:03:52 -0700832 open(os.devnull, "wb") as devnull, \
Andrew Boie08ce5a52016-02-22 13:28:10 -0800833 open(self.logfile, "wt") as make_log:
Andrew Boie6acbe632015-07-17 12:03:52 -0700834 # Create our dynamic Makefile and execute it.
835 # Watch stderr output which is where we will keep
836 # track of build state
Andrew Boie08ce5a52016-02-22 13:28:10 -0800837 for name, goal in self.goals.items():
Andrew Boie6acbe632015-07-17 12:03:52 -0700838 tf.write(goal.text)
839 tf.write("all: %s\n" % (" ".join(self.goals.keys())))
840 tf.flush()
841
Daniel Leung6b170072016-04-07 12:10:25 -0700842 cmd = ["make", "-k", "-j", str(CPU_COUNTS * 2), "-f", tf.name, "all"]
Andrew Boie6acbe632015-07-17 12:03:52 -0700843 p = subprocess.Popen(cmd, stderr=subprocess.PIPE,
844 stdout=devnull)
845
846 for line in iter(p.stderr.readline, b''):
Andrew Boie08ce5a52016-02-22 13:28:10 -0800847 line = line.decode("utf-8")
Andrew Boie6acbe632015-07-17 12:03:52 -0700848 make_log.write(line)
849 verbose("MAKE: " + repr(line.strip()))
850 m = MakeGenerator.re_make.match(line)
851 if not m:
852 continue
853
Jaakko Hannikainenca505f82016-08-22 15:03:46 +0300854 state, name, _, error = m.groups()
Andrew Boie6acbe632015-07-17 12:03:52 -0700855 if error:
856 goal = self.goals[error]
857 else:
858 goal = self.goals[name]
859 goal.make_state = state
860
861
862 if error:
Andrew Boie822b0872017-01-10 13:32:40 -0800863 # Sometimes QEMU will run an image and then crash out, which
864 # will cause the 'make qemu' invocation to exit with
865 # nonzero status.
866 # Need to distinguish this case from a compilation failure.
867 if goal.qemu:
868 goal.fail("qemu_crash")
869 else:
870 goal.fail("build_error")
Andrew Boie6acbe632015-07-17 12:03:52 -0700871 else:
872 if state == "finished":
873 if goal.qemu:
Jaakko Hannikainenca505f82016-08-22 15:03:46 +0300874 if goal.qemu.unit:
875 # We can't run unit tests with Make
876 goal.qemu.handle()
877 if goal.qemu.returncode == 2:
878 goal.qemu_log = goal.qemu.valgrind_log
879 elif goal.qemu.returncode:
880 goal.qemu_log = goal.qemu.run_log
Andrew Boie6acbe632015-07-17 12:03:52 -0700881 thread_status, metrics = goal.qemu.get_state()
882 goal.metrics.update(metrics)
883 if thread_status == "passed":
884 goal.success()
885 else:
886 goal.fail(thread_status)
887 else:
888 goal.success()
889
890 if callback_fn:
891 callback_fn(context, self.goals, goal)
892
893 p.wait()
894 return self.goals
895
896
897# "list" - List of strings
898# "list:<type>" - List of <type>
899# "set" - Set of unordered, unique strings
900# "set:<type>" - Set of <type>
901# "float" - Floating point
902# "int" - Integer
903# "bool" - Boolean
904# "str" - String
905
906# XXX Be sure to update __doc__ if you change any of this!!
907
908arch_valid_keys = {"name" : {"type" : "str", "required" : True},
Javier B Perez4b554ba2016-08-15 13:25:33 -0500909 "platforms" : {"type" : "list", "required" : True},
910 "supported_toolchains" : {"type" : "list", "required" : True}}
Andrew Boie6acbe632015-07-17 12:03:52 -0700911
912platform_valid_keys = {"qemu_support" : {"type" : "bool", "default" : False},
Javier B Perez4b554ba2016-08-15 13:25:33 -0500913 "supported_toolchains" : {"type" : "list", "default" : []}}
Andrew Boie6acbe632015-07-17 12:03:52 -0700914
915testcase_valid_keys = {"tags" : {"type" : "set", "required" : True},
Jaakko Hannikainenca505f82016-08-22 15:03:46 +0300916 "type" : {"type" : "str", "default": "integration"},
Andrew Boie6acbe632015-07-17 12:03:52 -0700917 "extra_args" : {"type" : "list"},
918 "build_only" : {"type" : "bool", "default" : False},
Anas Nashifa792a3d2017-04-04 18:47:49 -0400919 "build_on_all" : {"type" : "bool", "default" : False},
Anas Nashif2bd99bc2015-10-12 13:10:57 -0400920 "skip" : {"type" : "bool", "default" : False},
Andrew Boie6bb087c2016-02-10 13:39:00 -0800921 "slow" : {"type" : "bool", "default" : False},
Andrew Boie6acbe632015-07-17 12:03:52 -0700922 "timeout" : {"type" : "int", "default" : 60},
Anas Nashifa792a3d2017-04-04 18:47:49 -0400923 "min_ram" : {"type" : "int", "default" : 8},
924 "depends_on": {"type" : "set"},
925 "min_flash" : {"type" : "int", "default" : 32},
Andrew Boie6acbe632015-07-17 12:03:52 -0700926 "arch_whitelist" : {"type" : "set"},
Anas Nashif30d13872015-10-05 10:02:45 -0400927 "arch_exclude" : {"type" : "set"},
Andrew Boie52fef672016-11-29 12:21:59 -0800928 "extra_sections" : {"type" : "list", "default" : []},
Anas Nashif30d13872015-10-05 10:02:45 -0400929 "platform_exclude" : {"type" : "set"},
Andrew Boie6acbe632015-07-17 12:03:52 -0700930 "platform_whitelist" : {"type" : "set"},
Anas Nashifb17e1ca2017-06-27 18:05:30 -0400931 "toolchain_exclude" : {"type" : "set"},
932 "toolchain_whitelist" : {"type" : "set"},
Andrew Boie3ea78922016-03-24 14:46:00 -0700933 "filter" : {"type" : "str"}}
Andrew Boie6acbe632015-07-17 12:03:52 -0700934
935
936class SanityConfigParser:
Anas Nashifa792a3d2017-04-04 18:47:49 -0400937 """Class to read test case files with semantic checking
Andrew Boie6acbe632015-07-17 12:03:52 -0700938 """
939 def __init__(self, filename):
940 """Instantiate a new SanityConfigParser object
941
Anas Nashifa792a3d2017-04-04 18:47:49 -0400942 @param filename Source .yaml file to read
Andrew Boie6acbe632015-07-17 12:03:52 -0700943 """
Anas Nashifa792a3d2017-04-04 18:47:49 -0400944 with open(filename, 'r') as stream:
945 cp = yaml.load(stream)
Andrew Boie6acbe632015-07-17 12:03:52 -0700946 self.filename = filename
947 self.cp = cp
948
949 def _cast_value(self, value, typestr):
Anas Nashifa792a3d2017-04-04 18:47:49 -0400950
951 if type(value) is str:
952 v = value.strip()
Andrew Boie6acbe632015-07-17 12:03:52 -0700953 if typestr == "str":
954 return v
955
956 elif typestr == "float":
Anas Nashifa792a3d2017-04-04 18:47:49 -0400957 return float(value)
Andrew Boie6acbe632015-07-17 12:03:52 -0700958
959 elif typestr == "int":
Anas Nashifa792a3d2017-04-04 18:47:49 -0400960 return int(value)
Andrew Boie6acbe632015-07-17 12:03:52 -0700961
962 elif typestr == "bool":
Anas Nashifa792a3d2017-04-04 18:47:49 -0400963 return value
Andrew Boie6acbe632015-07-17 12:03:52 -0700964
965 elif typestr.startswith("list"):
966 vs = v.split()
967 if len(typestr) > 4 and typestr[4] == ":":
968 return [self._cast_value(vsi, typestr[5:]) for vsi in vs]
969 else:
970 return vs
971
972 elif typestr.startswith("set"):
973 vs = v.split()
974 if len(typestr) > 3 and typestr[3] == ":":
975 return set([self._cast_value(vsi, typestr[4:]) for vsi in vs])
976 else:
977 return set(vs)
978
979 else:
980 raise ConfigurationError(self.filename, "unknown type '%s'" % value)
981
Anas Nashifa792a3d2017-04-04 18:47:49 -0400982 def section(self,name):
983 for s in self.sections():
984 if name in s:
985 return s.get(name, {})
Andrew Boie6acbe632015-07-17 12:03:52 -0700986
987 def sections(self):
Anas Nashifa792a3d2017-04-04 18:47:49 -0400988 """Get the set of test sections within the .yaml file
Andrew Boie6acbe632015-07-17 12:03:52 -0700989
990 @return a list of string section names"""
Anas Nashifa792a3d2017-04-04 18:47:49 -0400991 return self.cp['tests']
Andrew Boie6acbe632015-07-17 12:03:52 -0700992
993 def get_section(self, section, valid_keys):
994 """Get a dictionary representing the keys/values within a section
995
Anas Nashifa792a3d2017-04-04 18:47:49 -0400996 @param section The section in the .yaml file to retrieve data from
Andrew Boie6acbe632015-07-17 12:03:52 -0700997 @param valid_keys A dictionary representing the intended semantics
998 for this section. Each key in this dictionary is a key that could
Anas Nashifa792a3d2017-04-04 18:47:49 -0400999 be specified, if a key is given in the .yaml file which isn't in
Andrew Boie6acbe632015-07-17 12:03:52 -07001000 here, it will generate an error. Each value in this dictionary
1001 is another dictionary containing metadata:
1002
1003 "default" - Default value if not given
1004 "type" - Data type to convert the text value to. Simple types
1005 supported are "str", "float", "int", "bool" which will get
1006 converted to respective Python data types. "set" and "list"
1007 may also be specified which will split the value by
1008 whitespace (but keep the elements as strings). finally,
1009 "list:<type>" and "set:<type>" may be given which will
1010 perform a type conversion after splitting the value up.
1011 "required" - If true, raise an error if not defined. If false
1012 and "default" isn't specified, a type conversion will be
1013 done on an empty string
1014 @return A dictionary containing the section key-value pairs with
1015 type conversion and default values filled in per valid_keys
1016 """
1017
1018 d = {}
Anas Nashifa792a3d2017-04-04 18:47:49 -04001019 for k, v in self.section(section).items():
Andrew Boie6acbe632015-07-17 12:03:52 -07001020 if k not in valid_keys:
1021 raise ConfigurationError(self.filename,
Anas Nashifa792a3d2017-04-04 18:47:49 -04001022 "Unknown config key '%s' in definition for '%s'"
1023 % (k, section))
Andrew Boie6acbe632015-07-17 12:03:52 -07001024 d[k] = v
1025
Andrew Boie08ce5a52016-02-22 13:28:10 -08001026 for k, kinfo in valid_keys.items():
Andrew Boie6acbe632015-07-17 12:03:52 -07001027 if k not in d:
1028 if "required" in kinfo:
1029 required = kinfo["required"]
1030 else:
1031 required = False
1032
1033 if required:
1034 raise ConfigurationError(self.filename,
1035 "missing required value for '%s' in section '%s'"
1036 % (k, section))
1037 else:
1038 if "default" in kinfo:
1039 default = kinfo["default"]
1040 else:
1041 default = self._cast_value("", kinfo["type"])
1042 d[k] = default
1043 else:
1044 try:
1045 d[k] = self._cast_value(d[k], kinfo["type"])
Andrew Boie08ce5a52016-02-22 13:28:10 -08001046 except ValueError as ve:
Andrew Boie6acbe632015-07-17 12:03:52 -07001047 raise ConfigurationError(self.filename,
1048 "bad %s value '%s' for key '%s' in section '%s'"
1049 % (kinfo["type"], d[k], k, section))
1050
1051 return d
1052
1053
1054class Platform:
1055 """Class representing metadata for a particular platform
1056
Anas Nashifc7406082015-12-13 15:00:31 -05001057 Maps directly to BOARD when building"""
Anas Nashifa792a3d2017-04-04 18:47:49 -04001058 def __init__(self, cfile):
Andrew Boie6acbe632015-07-17 12:03:52 -07001059 """Constructor.
1060
1061 @param arch Architecture object for this platform
Anas Nashifc7406082015-12-13 15:00:31 -05001062 @param name String name for this platform, same as BOARD
Andrew Boie6acbe632015-07-17 12:03:52 -07001063 @param plat_dict SanityConfigParser output on the relevant section
1064 in the architecture configuration file which has lots of metadata.
1065 See the Architecture class.
1066 """
Anas Nashifa792a3d2017-04-04 18:47:49 -04001067 scp = SanityConfigParser(cfile)
1068 cp = scp.cp
1069
1070 self.name = cp['identifier']
1071 # if no RAM size is specified by the board, take a default of 128K
1072 self.ram = cp.get("ram", 128)
1073 testing = cp.get("testing", {})
1074 self.ignore_tags = testing.get("ignore_tags", [])
1075 self.default = testing.get("default", False)
1076 # if no flash size is specified by the board, take a default of 512K
1077 self.flash = cp.get("flash", 512)
1078 self.supported = set(cp.get("supported", []))
1079 self.qemu_support = True if cp.get('type', "na") == 'qemu' else False
1080 self.arch = cp['arch']
1081 self.supported_toolchains = cp.get("toolchain", [])
Andrew Boie41878222016-11-03 11:58:53 -07001082 self.defconfig = None
Andrew Boie6acbe632015-07-17 12:03:52 -07001083 pass
1084
Andrew Boie6acbe632015-07-17 12:03:52 -07001085 def __repr__(self):
Anas Nashifa792a3d2017-04-04 18:47:49 -04001086 return "<%s on %s>" % (self.name, self.arch)
Andrew Boie6acbe632015-07-17 12:03:52 -07001087
1088
1089class Architecture:
1090 """Class representing metadata for a particular architecture
1091 """
Anas Nashifa792a3d2017-04-04 18:47:49 -04001092 def __init__(self, name, platforms):
Andrew Boie6acbe632015-07-17 12:03:52 -07001093 """Architecture constructor
1094
1095 @param cfile Path to Architecture configuration file, which gives
1096 info about the arch and all the platforms for it
1097 """
Anas Nashifa792a3d2017-04-04 18:47:49 -04001098 self.platforms = platforms
Andrew Boie6acbe632015-07-17 12:03:52 -07001099
Anas Nashifa792a3d2017-04-04 18:47:49 -04001100 self.name = name
Andrew Boie6acbe632015-07-17 12:03:52 -07001101
1102 def __repr__(self):
1103 return "<arch %s>" % self.name
1104
1105
1106class TestCase:
1107 """Class representing a test application
1108 """
Andrew Boie3ea78922016-03-24 14:46:00 -07001109 def __init__(self, testcase_root, workdir, name, tc_dict, inifile):
Andrew Boie6acbe632015-07-17 12:03:52 -07001110 """TestCase constructor.
1111
Anas Nashifa792a3d2017-04-04 18:47:49 -04001112 This gets called by TestSuite as it finds and reads test yaml files.
1113 Multiple TestCase instances may be generated from a single testcase.yaml,
Andrew Boie6acbe632015-07-17 12:03:52 -07001114 each one corresponds to a section within that file.
1115
Andrew Boie6acbe632015-07-17 12:03:52 -07001116 We need to have a unique name for every single test case. Since
Anas Nashifa792a3d2017-04-04 18:47:49 -04001117 a testcase.yaml can define multiple tests, the canonical name for
Andrew Boie6acbe632015-07-17 12:03:52 -07001118 the test case is <workdir>/<name>.
1119
1120 @param testcase_root Absolute path to the root directory where
1121 all the test cases live
1122 @param workdir Relative path to the project directory for this
1123 test application from the test_case root.
1124 @param name Name of this test case, corresponding to the section name
1125 in the test case configuration file. For many test cases that just
1126 define one test, can be anything and is usually "test". This is
1127 really only used to distinguish between different cases when
Anas Nashifa792a3d2017-04-04 18:47:49 -04001128 the testcase.yaml defines multiple tests
Andrew Boie6acbe632015-07-17 12:03:52 -07001129 @param tc_dict Dictionary with section values for this test case
Anas Nashifa792a3d2017-04-04 18:47:49 -04001130 from the testcase.yaml file
Andrew Boie6acbe632015-07-17 12:03:52 -07001131 """
1132 self.code_location = os.path.join(testcase_root, workdir)
Jaakko Hannikainenca505f82016-08-22 15:03:46 +03001133 self.type = tc_dict["type"]
Andrew Boie6acbe632015-07-17 12:03:52 -07001134 self.tags = tc_dict["tags"]
1135 self.extra_args = tc_dict["extra_args"]
1136 self.arch_whitelist = tc_dict["arch_whitelist"]
Anas Nashif30d13872015-10-05 10:02:45 -04001137 self.arch_exclude = tc_dict["arch_exclude"]
Anas Nashif2bd99bc2015-10-12 13:10:57 -04001138 self.skip = tc_dict["skip"]
Anas Nashif30d13872015-10-05 10:02:45 -04001139 self.platform_exclude = tc_dict["platform_exclude"]
Andrew Boie6acbe632015-07-17 12:03:52 -07001140 self.platform_whitelist = tc_dict["platform_whitelist"]
Anas Nashifb17e1ca2017-06-27 18:05:30 -04001141 self.toolchain_exclude = tc_dict["toolchain_exclude"]
1142 self.toolchain_whitelist = tc_dict["toolchain_whitelist"]
Andrew Boie3ea78922016-03-24 14:46:00 -07001143 self.tc_filter = tc_dict["filter"]
Andrew Boie6acbe632015-07-17 12:03:52 -07001144 self.timeout = tc_dict["timeout"]
1145 self.build_only = tc_dict["build_only"]
Anas Nashifa792a3d2017-04-04 18:47:49 -04001146 self.build_on_all = tc_dict["build_on_all"]
Andrew Boie6bb087c2016-02-10 13:39:00 -08001147 self.slow = tc_dict["slow"]
Anas Nashifa792a3d2017-04-04 18:47:49 -04001148 self.min_ram = tc_dict["min_ram"]
1149 self.depends_on = tc_dict["depends_on"]
1150 self.min_flash = tc_dict["min_flash"]
Andrew Boie52fef672016-11-29 12:21:59 -08001151 self.extra_sections = tc_dict["extra_sections"]
Andrew Boie14ac9022016-04-08 13:31:53 -07001152 self.path = os.path.join(os.path.basename(os.path.abspath(testcase_root)),
1153 workdir, name)
Andrew Boie6acbe632015-07-17 12:03:52 -07001154 self.name = self.path # for now
Anas Nashif2cf0df02015-10-10 09:29:43 -04001155 self.defconfig = {}
Andrew Boie3ea78922016-03-24 14:46:00 -07001156 self.inifile = inifile
Andrew Boie6acbe632015-07-17 12:03:52 -07001157
Andrew Boie6acbe632015-07-17 12:03:52 -07001158 def __repr__(self):
1159 return self.name
1160
1161
1162
1163class TestInstance:
1164 """Class representing the execution of a particular TestCase on a platform
1165
1166 @param test The TestCase object we want to build/execute
1167 @param platform Platform object that we want to build and run against
1168 @param base_outdir Base directory for all test results. The actual
1169 out directory used is <outdir>/<platform>/<test case name>
1170 """
Andrew Boie6bb087c2016-02-10 13:39:00 -08001171 def __init__(self, test, platform, base_outdir, build_only=False,
Jaakko Hannikainen54c90bc2016-08-31 14:17:03 +03001172 slow=False, coverage=False):
Andrew Boie6acbe632015-07-17 12:03:52 -07001173 self.test = test
1174 self.platform = platform
1175 self.name = os.path.join(platform.name, test.path)
1176 self.outdir = os.path.join(base_outdir, platform.name, test.path)
1177 self.build_only = build_only or test.build_only
1178
1179 def calculate_sizes(self):
1180 """Get the RAM/ROM sizes of a test case.
1181
1182 This can only be run after the instance has been executed by
1183 MakeGenerator, otherwise there won't be any binaries to measure.
1184
1185 @return A SizeCalculator object
1186 """
Andrew Boie5d4eb782015-10-02 10:04:56 -07001187 fns = glob.glob(os.path.join(self.outdir, "*.elf"))
Anas Nashiff8dcad42016-10-27 18:10:08 -04001188 fns = [x for x in fns if not x.endswith('_prebuilt.elf')]
Andrew Boie5d4eb782015-10-02 10:04:56 -07001189 if (len(fns) != 1):
1190 raise BuildError("Missing/multiple output ELF binary")
Andrew Boie52fef672016-11-29 12:21:59 -08001191 return SizeCalculator(fns[0], self.test.extra_sections)
Andrew Boie6acbe632015-07-17 12:03:52 -07001192
1193 def __repr__(self):
1194 return "<TestCase %s on %s>" % (self.test.name, self.platform.name)
1195
1196
Andrew Boie4ef16c52015-08-28 12:36:03 -07001197def defconfig_cb(context, goals, goal):
1198 if not goal.failed:
1199 return
1200
Jaakko Hannikainenca505f82016-08-22 15:03:46 +03001201
Andrew Boie4ef16c52015-08-28 12:36:03 -07001202 info("%sCould not build defconfig for %s%s" %
1203 (COLOR_RED, goal.name, COLOR_NORMAL));
1204 if INLINE_LOGS:
1205 with open(goal.get_error_log()) as fp:
Inaky Perez-Gonzalez9a36cb62016-11-29 10:43:40 -08001206 data = fp.read()
1207 sys.stdout.write(data)
1208 if log_file:
1209 log_file.write(data)
Andrew Boie4ef16c52015-08-28 12:36:03 -07001210 else:
Andrew Boie08ce5a52016-02-22 13:28:10 -08001211 info("\tsee: " + COLOR_YELLOW + goal.get_error_log() + COLOR_NORMAL)
Andrew Boie4ef16c52015-08-28 12:36:03 -07001212
Andrew Boie6acbe632015-07-17 12:03:52 -07001213
1214class TestSuite:
Andrew Boie60823c22017-02-10 09:43:07 -08001215 config_re = re.compile('(CONFIG_[A-Za-z0-9_]+)[=]\"?([^\"]*)\"?$')
Andrew Boie6acbe632015-07-17 12:03:52 -07001216
Jaakko Hannikainen54c90bc2016-08-31 14:17:03 +03001217 def __init__(self, arch_root, testcase_roots, outdir, coverage):
Andrew Boie6acbe632015-07-17 12:03:52 -07001218 # Keep track of which test cases we've filtered out and why
1219 discards = {}
1220 self.arches = {}
1221 self.testcases = {}
1222 self.platforms = []
Andrew Boieb391e662015-08-31 15:25:45 -07001223 self.outdir = os.path.abspath(outdir)
Andrew Boie6acbe632015-07-17 12:03:52 -07001224 self.instances = {}
1225 self.goals = None
1226 self.discards = None
Jaakko Hannikainen54c90bc2016-08-31 14:17:03 +03001227 self.coverage = coverage
Andrew Boie6acbe632015-07-17 12:03:52 -07001228
1229 arch_root = os.path.abspath(arch_root)
Andrew Boie6acbe632015-07-17 12:03:52 -07001230
Andrew Boie3d348712016-04-08 11:52:13 -07001231 for testcase_root in testcase_roots:
1232 testcase_root = os.path.abspath(testcase_root)
Andrew Boie6acbe632015-07-17 12:03:52 -07001233
Andrew Boie3d348712016-04-08 11:52:13 -07001234 debug("Reading test case configuration files under %s..." %
1235 testcase_root)
1236 for dirpath, dirnames, filenames in os.walk(testcase_root,
1237 topdown=True):
1238 verbose("scanning %s" % dirpath)
Anas Nashifa792a3d2017-04-04 18:47:49 -04001239 if "sample.yaml" in filenames or "testcase.yaml" in filenames:
1240 verbose("Found possible test case in " + dirpath)
Andrew Boie3d348712016-04-08 11:52:13 -07001241 dirnames[:] = []
Anas Nashifa792a3d2017-04-04 18:47:49 -04001242 if "sample.yaml" in filenames:
1243 yaml_path = os.path.join(dirpath, "sample.yaml")
1244 else:
1245 yaml_path = os.path.join(dirpath, "testcase.yaml")
1246 cp = SanityConfigParser(yaml_path)
Andrew Boie3d348712016-04-08 11:52:13 -07001247 workdir = os.path.relpath(dirpath, testcase_root)
1248
1249 for section in cp.sections():
Anas Nashifa792a3d2017-04-04 18:47:49 -04001250 name = list(section.keys())[0]
1251 tc_dict = cp.get_section(name, testcase_valid_keys)
1252 tc = TestCase(testcase_root, workdir, name, tc_dict,
1253 yaml_path)
Andrew Boie3d348712016-04-08 11:52:13 -07001254 self.testcases[tc.name] = tc
Andrew Boie6acbe632015-07-17 12:03:52 -07001255
Anas Nashifa792a3d2017-04-04 18:47:49 -04001256 debug("Reading platform configuration files under %s..." % arch_root)
Andrew Boie6acbe632015-07-17 12:03:52 -07001257 for dirpath, dirnames, filenames in os.walk(arch_root):
1258 for filename in filenames:
Anas Nashifa792a3d2017-04-04 18:47:49 -04001259 if filename.endswith(".yaml"):
Andrew Boie6acbe632015-07-17 12:03:52 -07001260 fn = os.path.join(dirpath, filename)
Anas Nashifa792a3d2017-04-04 18:47:49 -04001261 verbose("Found plaform configuration " + fn)
1262 platform = Platform(fn)
1263 self.platforms.append(platform)
Andrew Boie6acbe632015-07-17 12:03:52 -07001264
Anas Nashifa792a3d2017-04-04 18:47:49 -04001265 arches = []
1266 for p in self.platforms:
1267 arches.append(p.arch)
1268 for a in list(set(arches)):
1269 aplatforms = [ p for p in self.platforms if p.arch == a ]
1270 arch = Architecture(a, aplatforms)
1271 self.arches[a] = arch
1272
Andrew Boie6acbe632015-07-17 12:03:52 -07001273 self.instances = {}
1274
1275 def get_last_failed(self):
1276 if not os.path.exists(LAST_SANITY):
Anas Nashifd9616b92016-11-29 13:34:53 -05001277 raise SanityRuntimeError("Couldn't find last sanity run.")
Andrew Boie6acbe632015-07-17 12:03:52 -07001278 result = []
1279 with open(LAST_SANITY, "r") as fp:
1280 cr = csv.DictReader(fp)
1281 for row in cr:
1282 if row["passed"] == "True":
1283 continue
1284 test = row["test"]
1285 platform = row["platform"]
1286 result.append((test, platform))
1287 return result
1288
Anas Nashifdfa86e22016-10-24 17:08:56 -04001289 def apply_filters(self, platform_filter, arch_filter, tag_filter, exclude_tag,
Andrew Boie821d8322016-03-22 10:08:35 -07001290 config_filter, testcase_filter, last_failed, all_plats,
Kumar Galad5719a62016-10-26 12:30:26 -05001291 platform_limit, toolchain, extra_args, enable_ccache):
Andrew Boie6acbe632015-07-17 12:03:52 -07001292 instances = []
1293 discards = {}
1294 verbose("platform filter: " + str(platform_filter))
1295 verbose(" arch_filter: " + str(arch_filter))
1296 verbose(" tag_filter: " + str(tag_filter))
Anas Nashifdfa86e22016-10-24 17:08:56 -04001297 verbose(" exclude_tag: " + str(exclude_tag))
Andrew Boie6acbe632015-07-17 12:03:52 -07001298 verbose(" config_filter: " + str(config_filter))
Kumar Galad5719a62016-10-26 12:30:26 -05001299 verbose(" enable_ccache: " + str(enable_ccache))
Andrew Boie6acbe632015-07-17 12:03:52 -07001300
1301 if last_failed:
1302 failed_tests = self.get_last_failed()
1303
Andrew Boie821d8322016-03-22 10:08:35 -07001304 default_platforms = False
1305
1306 if all_plats:
1307 info("Selecting all possible platforms per test case")
1308 # When --all used, any --platform arguments ignored
1309 platform_filter = []
1310 elif not platform_filter:
Andrew Boie6acbe632015-07-17 12:03:52 -07001311 info("Selecting default platforms per test case")
1312 default_platforms = True
Andrew Boie6acbe632015-07-17 12:03:52 -07001313
Kumar Galad5719a62016-10-26 12:30:26 -05001314 mg = MakeGenerator(self.outdir, ccache=enable_ccache)
Anas Nashif2cf0df02015-10-10 09:29:43 -04001315 dlist = {}
Andrew Boie08ce5a52016-02-22 13:28:10 -08001316 for tc_name, tc in self.testcases.items():
1317 for arch_name, arch in self.arches.items():
Anas Nashif2cf0df02015-10-10 09:29:43 -04001318 for plat in arch.platforms:
1319 instance = TestInstance(tc, plat, self.outdir)
1320
Jaakko Hannikainenca505f82016-08-22 15:03:46 +03001321 if (arch_name == "unit") != (tc.type == "unit"):
1322 continue
1323
Anas Nashifbfab06b2017-06-22 09:22:24 -04001324 if tc.build_on_all and not platform_filter:
Anas Nashifa792a3d2017-04-04 18:47:49 -04001325 platform_filter = []
1326
Anas Nashif2bd99bc2015-10-12 13:10:57 -04001327 if tc.skip:
1328 continue
1329
Anas Nashif2cf0df02015-10-10 09:29:43 -04001330 if tag_filter and not tc.tags.intersection(tag_filter):
1331 continue
1332
Anas Nashifdfa86e22016-10-24 17:08:56 -04001333 if exclude_tag and tc.tags.intersection(exclude_tag):
1334 continue
1335
Anas Nashif2cf0df02015-10-10 09:29:43 -04001336 if testcase_filter and tc_name not in testcase_filter:
1337 continue
1338
1339 if last_failed and (tc.name, plat.name) not in failed_tests:
1340 continue
1341
1342 if arch_filter and arch_name not in arch_filter:
1343 continue
1344
1345 if tc.arch_whitelist and arch.name not in tc.arch_whitelist:
1346 continue
1347
1348 if tc.arch_exclude and arch.name in tc.arch_exclude:
1349 continue
1350
1351 if tc.platform_exclude and plat.name in tc.platform_exclude:
1352 continue
1353
Anas Nashifb17e1ca2017-06-27 18:05:30 -04001354 if tc.toolchain_exclude and toolchain in tc.toolchain_exclude:
1355 continue
1356
Anas Nashif2cf0df02015-10-10 09:29:43 -04001357 if platform_filter and plat.name not in platform_filter:
1358 continue
1359
Anas Nashifa792a3d2017-04-04 18:47:49 -04001360 if plat.ram <= tc.min_ram:
1361 continue
1362
1363 if set(plat.ignore_tags) & tc.tags:
1364 continue
1365
1366 if not tc.depends_on.issubset(set(plat.supported)):
1367 continue
1368
1369 if plat.flash < tc.min_flash:
1370 continue
1371
Anas Nashif2cf0df02015-10-10 09:29:43 -04001372 if tc.platform_whitelist and plat.name not in tc.platform_whitelist:
1373 continue
1374
Anas Nashifb17e1ca2017-06-27 18:05:30 -04001375 if tc.toolchain_whitelist and toolchain not in tc.toolchain_whitelist:
1376 continue
1377
Anas Nashifcf21f5f2017-06-22 06:48:34 -04001378 if (tc.tc_filter and (plat.default or all_plats or platform_filter) and
1379 toolchain in plat.supported_toolchains):
Anas Nashif8ea9d022015-11-10 12:24:20 -05001380 args = tc.extra_args[:]
Anas Nashifa792a3d2017-04-04 18:47:49 -04001381 args.extend(["ARCH=" + plat.arch,
Andy Gross25309ab2017-06-06 21:29:09 -05001382 "BOARD=" + plat.name, "config-sanitycheck"])
Andrew Boieba612002016-09-01 10:41:03 -07001383 args.extend(extra_args)
Anas Nashif2cf0df02015-10-10 09:29:43 -04001384 # FIXME would be nice to use a common outdir for this so that
Andrew Boie41878222016-11-03 11:58:53 -07001385 # conf, gen_idt, etc aren't rebuilt for every combination,
David B. Kinder29963c32017-06-16 12:32:42 -07001386 # need a way to avoid different Make processes from clobbering
Anas Nashif2cf0df02015-10-10 09:29:43 -04001387 # each other since they all try to build them simultaneously
1388
1389 o = os.path.join(self.outdir, plat.name, tc.path)
Andy Gross25309ab2017-06-06 21:29:09 -05001390 dlist[tc, plat, tc.name.split("/")[-1]] = os.path.join(o,".config-sanitycheck")
1391 goal = "_".join([plat.name, "_".join(tc.name.split("/")), "config-sanitycheck"])
Kumar Gala914d92e2017-06-22 16:19:11 -05001392 mg.add_build_goal(goal, os.path.join(ZEPHYR_BASE, tc.code_location), o,
1393 args, "config-sanitycheck.log")
Anas Nashif2cf0df02015-10-10 09:29:43 -04001394
1395 info("Building testcase defconfigs...")
1396 results = mg.execute(defconfig_cb)
1397
Andrew Boie08ce5a52016-02-22 13:28:10 -08001398 for name, goal in results.items():
Anas Nashif2cf0df02015-10-10 09:29:43 -04001399 if goal.failed:
1400 raise SanityRuntimeError("Couldn't build some defconfigs")
1401
Andrew Boie08ce5a52016-02-22 13:28:10 -08001402 for k, out_config in dlist.items():
Andrew Boie41878222016-11-03 11:58:53 -07001403 test, plat, name = k
Anas Nashif2cf0df02015-10-10 09:29:43 -04001404 defconfig = {}
1405 with open(out_config, "r") as fp:
1406 for line in fp.readlines():
1407 m = TestSuite.config_re.match(line)
1408 if not m:
Andrew Boie3ea78922016-03-24 14:46:00 -07001409 if line.strip() and not line.startswith("#"):
1410 sys.stderr.write("Unrecognized line %s\n" % line)
Anas Nashif2cf0df02015-10-10 09:29:43 -04001411 continue
1412 defconfig[m.group(1)] = m.group(2).strip()
Andrew Boie41878222016-11-03 11:58:53 -07001413 test.defconfig[plat] = defconfig
Anas Nashif2cf0df02015-10-10 09:29:43 -04001414
Andrew Boie08ce5a52016-02-22 13:28:10 -08001415 for tc_name, tc in self.testcases.items():
1416 for arch_name, arch in self.arches.items():
Andrew Boie6acbe632015-07-17 12:03:52 -07001417 instance_list = []
1418 for plat in arch.platforms:
1419 instance = TestInstance(tc, plat, self.outdir)
1420
Jaakko Hannikainenca505f82016-08-22 15:03:46 +03001421 if (arch_name == "unit") != (tc.type == "unit"):
1422 # Discard silently
1423 continue
1424
Anas Nashif2bd99bc2015-10-12 13:10:57 -04001425 if tc.skip:
1426 discards[instance] = "Skip filter"
1427 continue
1428
Anas Nashifbfab06b2017-06-22 09:22:24 -04001429 if tc.build_on_all and not platform_filter:
Anas Nashifa792a3d2017-04-04 18:47:49 -04001430 platform_filter = []
1431
Andrew Boie6acbe632015-07-17 12:03:52 -07001432 if tag_filter and not tc.tags.intersection(tag_filter):
1433 discards[instance] = "Command line testcase tag filter"
1434 continue
1435
Anas Nashifdfa86e22016-10-24 17:08:56 -04001436 if exclude_tag and tc.tags.intersection(exclude_tag):
1437 discards[instance] = "Command line testcase exclude filter"
1438 continue
1439
Andrew Boie6acbe632015-07-17 12:03:52 -07001440 if testcase_filter and tc_name not in testcase_filter:
1441 discards[instance] = "Testcase name filter"
1442 continue
1443
1444 if last_failed and (tc.name, plat.name) not in failed_tests:
1445 discards[instance] = "Passed or skipped during last run"
1446 continue
1447
1448 if arch_filter and arch_name not in arch_filter:
1449 discards[instance] = "Command line testcase arch filter"
1450 continue
1451
1452 if tc.arch_whitelist and arch.name not in tc.arch_whitelist:
1453 discards[instance] = "Not in test case arch whitelist"
1454 continue
1455
Anas Nashif30d13872015-10-05 10:02:45 -04001456 if tc.arch_exclude and arch.name in tc.arch_exclude:
1457 discards[instance] = "In test case arch exclude"
1458 continue
1459
1460 if tc.platform_exclude and plat.name in tc.platform_exclude:
1461 discards[instance] = "In test case platform exclude"
1462 continue
1463
Anas Nashifb17e1ca2017-06-27 18:05:30 -04001464 if tc.toolchain_exclude and toolchain in tc.toolchain_exclude:
1465 discards[instance] = "In test case toolchain exclude"
1466 continue
1467
Andrew Boie6acbe632015-07-17 12:03:52 -07001468 if platform_filter and plat.name not in platform_filter:
1469 discards[instance] = "Command line platform filter"
1470 continue
1471
1472 if tc.platform_whitelist and plat.name not in tc.platform_whitelist:
1473 discards[instance] = "Not in testcase platform whitelist"
1474 continue
1475
Anas Nashifb17e1ca2017-06-27 18:05:30 -04001476 if tc.toolchain_whitelist and toolchain not in tc.toolchain_whitelist:
1477 discards[instance] = "Not in testcase toolchain whitelist"
1478 continue
1479
Javier B Perez4b554ba2016-08-15 13:25:33 -05001480 if toolchain and toolchain not in plat.supported_toolchains:
1481 discards[instance] = "Not supported by the toolchain"
1482 continue
1483
Anas Nashifa792a3d2017-04-04 18:47:49 -04001484 if plat.ram <= tc.min_ram:
1485 discards[instance] = "Not enough RAM"
1486 continue
1487
1488 if not tc.depends_on.issubset(set(plat.supported)):
1489 discards[instance] = "No hardware support"
1490 continue
1491
1492 if plat.flash< tc.min_flash:
1493 discards[instance] = "Not enough FLASH"
1494 continue
1495
1496 if set(plat.ignore_tags) & tc.tags:
1497 discards[instance] = "Excluded tags per platform"
1498 continue
1499
Andrew Boie3ea78922016-03-24 14:46:00 -07001500 defconfig = {"ARCH" : arch.name, "PLATFORM" : plat.name}
Javier B Perez79414542016-08-08 12:24:59 -05001501 defconfig.update(os.environ)
Andrew Boie41878222016-11-03 11:58:53 -07001502 for p, tdefconfig in tc.defconfig.items():
1503 if p == plat:
Andrew Boie3ea78922016-03-24 14:46:00 -07001504 defconfig.update(tdefconfig)
Anas Nashif2cf0df02015-10-10 09:29:43 -04001505 break
1506
Andrew Boie3ea78922016-03-24 14:46:00 -07001507 if tc.tc_filter:
1508 try:
1509 res = expr_parser.parse(tc.tc_filter, defconfig)
Andrew Boiec09b4b82017-04-18 11:46:07 -07001510 except (ValueError, SyntaxError) as se:
Andrew Boie3ea78922016-03-24 14:46:00 -07001511 sys.stderr.write("Failed processing %s\n" % tc.inifile)
1512 raise se
1513 if not res:
1514 discards[instance] = ("defconfig doesn't satisfy expression '%s'" %
1515 tc.tc_filter)
1516 continue
Anas Nashif2cf0df02015-10-10 09:29:43 -04001517
Andrew Boie6acbe632015-07-17 12:03:52 -07001518 instance_list.append(instance)
1519
1520 if not instance_list:
1521 # Every platform in this arch was rejected already
1522 continue
1523
Anas Nashifa792a3d2017-04-04 18:47:49 -04001524 if default_platforms and not tc.build_on_all:
1525 if not tc.platform_whitelist:
1526 instances = list(filter(lambda tc: tc.platform.default, instance_list))
1527 self.add_instances(instances)
1528 else:
1529 self.add_instances(instance_list[:platform_limit])
1530
1531 for instance in list(filter(lambda tc: not tc.platform.default, instance_list)):
1532 discards[instance] = "Not a default test platform"
Andrew Boie6acbe632015-07-17 12:03:52 -07001533 else:
Andrew Boie821d8322016-03-22 10:08:35 -07001534 self.add_instances(instance_list)
Andrew Boie6acbe632015-07-17 12:03:52 -07001535 self.discards = discards
1536 return discards
1537
Andrew Boie821d8322016-03-22 10:08:35 -07001538 def add_instances(self, ti_list):
1539 for ti in ti_list:
1540 self.instances[ti.name] = ti
Andrew Boie6acbe632015-07-17 12:03:52 -07001541
Anas Nashife3febe92016-11-30 14:25:44 -05001542 def execute(self, cb, cb_context, build_only, enable_slow, enable_asserts, enable_deprecations,
Kumar Galad5719a62016-10-26 12:30:26 -05001543 extra_args, enable_ccache):
Daniel Leung6b170072016-04-07 12:10:25 -07001544
1545 def calc_one_elf_size(name, goal):
1546 if not goal.failed:
1547 i = self.instances[name]
1548 sc = i.calculate_sizes()
1549 goal.metrics["ram_size"] = sc.get_ram_size()
1550 goal.metrics["rom_size"] = sc.get_rom_size()
1551 goal.metrics["unrecognized"] = sc.unrecognized_sections()
Daniel Leung6b170072016-04-07 12:10:25 -07001552
Anas Nashife3febe92016-11-30 14:25:44 -05001553 mg = MakeGenerator(self.outdir, asserts=enable_asserts, deprecations=enable_deprecations,
1554 ccache=enable_ccache)
Andrew Boie6acbe632015-07-17 12:03:52 -07001555 for i in self.instances.values():
Jaakko Hannikainen54c90bc2016-08-31 14:17:03 +03001556 mg.add_test_instance(i, build_only, enable_slow, self.coverage, extra_args)
Andrew Boie6acbe632015-07-17 12:03:52 -07001557 self.goals = mg.execute(cb, cb_context)
Daniel Leung6b170072016-04-07 12:10:25 -07001558
1559 # Parallelize size calculation
1560 executor = concurrent.futures.ThreadPoolExecutor(CPU_COUNTS)
1561 futures = [executor.submit(calc_one_elf_size, name, goal) \
1562 for name, goal in self.goals.items()]
1563 concurrent.futures.wait(futures)
1564
Andrew Boie6acbe632015-07-17 12:03:52 -07001565 return self.goals
1566
1567 def discard_report(self, filename):
1568 if self.discards == None:
1569 raise SanityRuntimeException("apply_filters() hasn't been run!")
1570
1571 with open(filename, "wb") as csvfile:
1572 fieldnames = ["test", "arch", "platform", "reason"]
1573 cw = csv.DictWriter(csvfile, fieldnames, lineterminator=os.linesep)
1574 cw.writeheader()
Andrew Boie08ce5a52016-02-22 13:28:10 -08001575 for instance, reason in self.discards.items():
Andrew Boie6acbe632015-07-17 12:03:52 -07001576 rowdict = {"test" : i.test.name,
Anas Nashifa792a3d2017-04-04 18:47:49 -04001577 "arch" : i.platform.arch,
Andrew Boie6acbe632015-07-17 12:03:52 -07001578 "platform" : i.platform.name,
1579 "reason" : reason}
1580 cw.writerow(rowdict)
1581
1582 def compare_metrics(self, filename):
1583 # name, datatype, lower results better
1584 interesting_metrics = [("ram_size", int, True),
1585 ("rom_size", int, True)]
1586
1587 if self.goals == None:
1588 raise SanityRuntimeException("execute() hasn't been run!")
1589
1590 if not os.path.exists(filename):
1591 info("Cannot compare metrics, %s not found" % filename)
1592 return []
1593
1594 results = []
1595 saved_metrics = {}
1596 with open(filename) as fp:
1597 cr = csv.DictReader(fp)
1598 for row in cr:
1599 d = {}
1600 for m, _, _ in interesting_metrics:
1601 d[m] = row[m]
1602 saved_metrics[(row["test"], row["platform"])] = d
1603
Andrew Boie08ce5a52016-02-22 13:28:10 -08001604 for name, goal in self.goals.items():
Andrew Boie6acbe632015-07-17 12:03:52 -07001605 i = self.instances[name]
1606 mkey = (i.test.name, i.platform.name)
1607 if mkey not in saved_metrics:
1608 continue
1609 sm = saved_metrics[mkey]
1610 for metric, mtype, lower_better in interesting_metrics:
1611 if metric not in goal.metrics:
1612 continue
1613 if sm[metric] == "":
1614 continue
1615 delta = goal.metrics[metric] - mtype(sm[metric])
Andrew Boieea7928f2015-08-14 14:27:38 -07001616 if delta == 0:
1617 continue
1618 results.append((i, metric, goal.metrics[metric], delta,
1619 lower_better))
Andrew Boie6acbe632015-07-17 12:03:52 -07001620 return results
1621
Anas Nashif0605fa32017-05-07 08:51:02 -04001622 def testcase_xunit_report(self, filename, duration, args):
Anas Nashifb3311ed2017-04-13 14:44:48 -04001623 if self.goals == None:
1624 raise SanityRuntimeException("execute() hasn't been run!")
1625
1626 fails = 0
1627 passes = 0
1628 errors = 0
1629
1630 for name, goal in self.goals.items():
1631 if goal.failed:
1632 if goal.reason in ['build_error', 'qemu_crash']:
1633 errors += 1
1634 else:
1635 fails += 1
1636 else:
1637 passes += 1
1638
1639 run = "Sanitycheck"
1640 eleTestsuite = None
1641 append = args.only_failed
1642
Anas Nashif0605fa32017-05-07 08:51:02 -04001643 if os.path.exists(filename) and append:
Anas Nashifb3311ed2017-04-13 14:44:48 -04001644 tree = ET.parse(filename)
1645 eleTestsuites = tree.getroot()
1646 eleTestsuite = tree.findall('testsuite')[0];
1647 else:
1648 eleTestsuites = ET.Element('testsuites')
Anas Nashif0605fa32017-05-07 08:51:02 -04001649 eleTestsuite = ET.SubElement(eleTestsuites, 'testsuite', name=run, time="%d" %duration,
Anas Nashifb3311ed2017-04-13 14:44:48 -04001650 tests="%d" %(errors + passes + fails), failures="%d" %fails, errors="%d" %errors, skip="0")
1651
1652 qemu_time = "0"
1653 for name, goal in self.goals.items():
1654
1655 i = self.instances[name]
1656 if append:
1657 for tc in eleTestsuite.findall('testcase'):
Anas Nashif202d1302017-05-07 08:19:20 -04001658 if tc.get('classname') == "%s:%s" %(i.platform.name, i.test.name):
Anas Nashifb3311ed2017-04-13 14:44:48 -04001659 eleTestsuite.remove(tc)
1660
1661 if not goal.failed and goal.qemu:
1662 qemu_time = "%s" %(goal.metrics["qemu_time"])
1663
Anas Nashif202d1302017-05-07 08:19:20 -04001664 eleTestcase = ET.SubElement(eleTestsuite, 'testcase', classname="%s:%s" %(i.platform.name, i.test.name), name="%s" %(name), time=qemu_time)
Anas Nashifb3311ed2017-04-13 14:44:48 -04001665 if goal.failed:
1666 failure = ET.SubElement(eleTestcase, 'failure', type="failure", message=goal.reason)
1667 p = ("%s/%s/%s" %(args.outdir, i.platform.name, i.test.name))
1668 bl = os.path.join(p, "build.log")
Anas Nashifaccc8eb2017-05-01 16:33:43 -04001669 if goal.reason != 'build_error':
1670 bl = os.path.join(p, "qemu.log")
1671
Anas Nashifb3311ed2017-04-13 14:44:48 -04001672 if os.path.exists(bl):
1673 with open(bl, "r") as f:
1674 log = f.read()
Anas Nashife6fcc012017-05-17 09:29:09 -04001675 ansi_escape = re.compile(r'\x1b[^m]*m')
1676 output = ansi_escape.sub('', str(log))
1677 failure.text = (escape(output))
Anas Nashifb3311ed2017-04-13 14:44:48 -04001678
1679 result = ET.tostring(eleTestsuites)
1680 f = open(filename, 'wb')
1681 f.write(result)
1682 f.close()
1683
Andrew Boie6acbe632015-07-17 12:03:52 -07001684 def testcase_report(self, filename):
1685 if self.goals == None:
1686 raise SanityRuntimeException("execute() hasn't been run!")
1687
Andrew Boie08ce5a52016-02-22 13:28:10 -08001688 with open(filename, "wt") as csvfile:
Andrew Boie6acbe632015-07-17 12:03:52 -07001689 fieldnames = ["test", "arch", "platform", "passed", "status",
1690 "extra_args", "qemu", "qemu_time", "ram_size",
1691 "rom_size"]
1692 cw = csv.DictWriter(csvfile, fieldnames, lineterminator=os.linesep)
1693 cw.writeheader()
Andrew Boie08ce5a52016-02-22 13:28:10 -08001694 for name, goal in self.goals.items():
Andrew Boie6acbe632015-07-17 12:03:52 -07001695 i = self.instances[name]
1696 rowdict = {"test" : i.test.name,
Anas Nashifa792a3d2017-04-04 18:47:49 -04001697 "arch" : i.platform.arch,
Andrew Boie6acbe632015-07-17 12:03:52 -07001698 "platform" : i.platform.name,
1699 "extra_args" : " ".join(i.test.extra_args),
1700 "qemu" : i.platform.qemu_support}
1701 if goal.failed:
1702 rowdict["passed"] = False
1703 rowdict["status"] = goal.reason
1704 else:
1705 rowdict["passed"] = True
1706 if goal.qemu:
1707 rowdict["qemu_time"] = goal.metrics["qemu_time"]
1708 rowdict["ram_size"] = goal.metrics["ram_size"]
1709 rowdict["rom_size"] = goal.metrics["rom_size"]
1710 cw.writerow(rowdict)
1711
1712
1713def parse_arguments():
1714
1715 parser = argparse.ArgumentParser(description = __doc__,
1716 formatter_class = argparse.RawDescriptionHelpFormatter)
Genaro Saucedo Tejada28bba922016-10-24 18:00:58 -05001717 parser.fromfile_prefix_chars = "+"
Andrew Boie6acbe632015-07-17 12:03:52 -07001718
1719 parser.add_argument("-p", "--platform", action="append",
Andrew Boie821d8322016-03-22 10:08:35 -07001720 help="Platform filter for testing. This option may be used multiple "
1721 "times. Testcases will only be built/run on the platforms "
Anas Nashifa792a3d2017-04-04 18:47:49 -04001722 "specified. If this option is not used, then platforms marked "
1723 "as default in the platform metadata file will be chosen "
1724 "to build and test. ")
Andrew Boie821d8322016-03-22 10:08:35 -07001725 parser.add_argument("-L", "--platform-limit", action="store", type=int,
1726 metavar="N", default=1,
1727 help="Controls what platforms are tested if --platform or "
1728 "--all are not used. For each architecture specified by "
1729 "--arch (defaults to all of them), choose the first "
1730 "N platforms to test in the arch-specific .ini file "
1731 "'platforms' list. Defaults to 1.")
Andrew Boie6acbe632015-07-17 12:03:52 -07001732 parser.add_argument("-a", "--arch", action="append",
1733 help="Arch filter for testing. Takes precedence over --platform. "
1734 "If unspecified, test all arches. Multiple invocations "
1735 "are treated as a logical 'or' relationship")
1736 parser.add_argument("-t", "--tag", action="append",
1737 help="Specify tags to restrict which tests to run by tag value. "
1738 "Default is to not do any tag filtering. Multiple invocations "
1739 "are treated as a logical 'or' relationship")
Anas Nashifdfa86e22016-10-24 17:08:56 -04001740 parser.add_argument("-e", "--exclude-tag", action="append",
Paul Sokolovskyff70add2017-06-16 01:31:54 +03001741 help="Specify tags of tests that should not run. "
Anas Nashifdfa86e22016-10-24 17:08:56 -04001742 "Default is to run all tests with all tags.")
Andrew Boie6acbe632015-07-17 12:03:52 -07001743 parser.add_argument("-f", "--only-failed", action="store_true",
1744 help="Run only those tests that failed the previous sanity check "
1745 "invocation.")
1746 parser.add_argument("-c", "--config", action="append",
1747 help="Specify platform configuration values filtering. This can be "
1748 "specified two ways: <config>=<value> or just <config>. The "
Andrew Boie41878222016-11-03 11:58:53 -07001749 "defconfig for all platforms will be "
Andrew Boie6acbe632015-07-17 12:03:52 -07001750 "checked. For the <config>=<value> case, only match defconfig "
1751 "that have that value defined. For the <config> case, match "
1752 "defconfig that have that value assigned to any value. "
1753 "Prepend a '!' to invert the match.")
1754 parser.add_argument("-s", "--test", action="append",
1755 help="Run only the specified test cases. These are named by "
1756 "<path to test project relative to "
1757 "--testcase-root>/<testcase.ini section name>")
1758 parser.add_argument("-l", "--all", action="store_true",
Andrew Boie821d8322016-03-22 10:08:35 -07001759 help="Build/test on all platforms. Any --platform arguments "
1760 "ignored.")
Andrew Boie6acbe632015-07-17 12:03:52 -07001761
1762 parser.add_argument("-o", "--testcase-report",
1763 help="Output a CSV spreadsheet containing results of the test run")
1764 parser.add_argument("-d", "--discard-report",
David B. Kinder29963c32017-06-16 12:32:42 -07001765 help="Output a CSV spreadsheet showing tests that were skipped "
Andrew Boie6acbe632015-07-17 12:03:52 -07001766 "and why")
Daniel Leung7f850102016-04-08 11:07:32 -07001767 parser.add_argument("--compare-report",
David B. Kinder29963c32017-06-16 12:32:42 -07001768 help="Use this report file for size comparison")
Daniel Leung7f850102016-04-08 11:07:32 -07001769
Kumar Galad5719a62016-10-26 12:30:26 -05001770 parser.add_argument("--ccache", action="store_const", const=1, default=0,
1771 help="Enable the use of ccache when building")
1772
Anas Nashif035799f2017-05-13 21:31:53 -04001773 parser.add_argument("-B", "--subset",
1774 help="Only run a subset of the tests, 1/4 for running the first 25%%, "
1775 "3/5 means run the 3rd fifth of the total. "
1776 "This option is useful when running a large number of tests on "
1777 "different hosts to speed up execution time.")
Andrew Boie6acbe632015-07-17 12:03:52 -07001778 parser.add_argument("-y", "--dry-run", action="store_true",
1779 help="Create the filtered list of test cases, but don't actually "
1780 "run them. Useful if you're just interested in "
1781 "--discard-report")
1782
1783 parser.add_argument("-r", "--release", action="store_true",
1784 help="Update the benchmark database with the results of this test "
1785 "run. Intended to be run by CI when tagging an official "
1786 "release. This database is used as a basis for comparison "
1787 "when looking for deltas in metrics such as footprint")
1788 parser.add_argument("-w", "--warnings-as-errors", action="store_true",
1789 help="Treat warning conditions as errors")
1790 parser.add_argument("-v", "--verbose", action="count", default=0,
1791 help="Emit debugging information, call multiple times to increase "
1792 "verbosity")
1793 parser.add_argument("-i", "--inline-logs", action="store_true",
1794 help="Upon test failure, print relevant log data to stdout "
1795 "instead of just a path to it")
Inaky Perez-Gonzalez9a36cb62016-11-29 10:43:40 -08001796 parser.add_argument("--log-file", metavar="FILENAME", action="store",
1797 help="log also to file")
Andrew Boie6acbe632015-07-17 12:03:52 -07001798 parser.add_argument("-m", "--last-metrics", action="store_true",
1799 help="Instead of comparing metrics from the last --release, "
1800 "compare with the results of the previous sanity check "
1801 "invocation")
1802 parser.add_argument("-u", "--no-update", action="store_true",
1803 help="do not update the results of the last run of the sanity "
1804 "checks")
1805 parser.add_argument("-b", "--build-only", action="store_true",
1806 help="Only build the code, do not execute any of it in QEMU")
1807 parser.add_argument("-j", "--jobs", type=int,
1808 help="Number of cores to use when building, defaults to "
1809 "number of CPUs * 2")
1810 parser.add_argument("-H", "--footprint-threshold", type=float, default=5,
1811 help="When checking test case footprint sizes, warn the user if "
1812 "the new app size is greater then the specified percentage "
1813 "from the last release. Default is 5. 0 to warn on any "
1814 "increase on app size")
Andrew Boieea7928f2015-08-14 14:27:38 -07001815 parser.add_argument("-D", "--all-deltas", action="store_true",
1816 help="Show all footprint deltas, positive or negative. Implies "
1817 "--footprint-threshold=0")
Andrew Boie6acbe632015-07-17 12:03:52 -07001818 parser.add_argument("-O", "--outdir",
1819 default="%s/sanity-out" % ZEPHYR_BASE,
1820 help="Output directory for logs and binaries.")
Andrew Boieae9e7f7b2015-07-31 12:26:12 -07001821 parser.add_argument("-n", "--no-clean", action="store_true",
1822 help="Do not delete the outdir before building. Will result in "
1823 "faster compilation since builds will be incremental")
Andrew Boie3d348712016-04-08 11:52:13 -07001824 parser.add_argument("-T", "--testcase-root", action="append", default=[],
Andrew Boie6acbe632015-07-17 12:03:52 -07001825 help="Base directory to recursively search for test cases. All "
Andrew Boie3d348712016-04-08 11:52:13 -07001826 "testcase.ini files under here will be processed. May be "
1827 "called multiple times. Defaults to the 'samples' and "
1828 "'tests' directories in the Zephyr tree.")
Andrew Boie6acbe632015-07-17 12:03:52 -07001829 parser.add_argument("-A", "--arch-root",
Anas Nashifa792a3d2017-04-04 18:47:49 -04001830 default="%s/boards" % ZEPHYR_BASE,
Andrew Boie6acbe632015-07-17 12:03:52 -07001831 help="Directory to search for arch configuration files. All .ini "
1832 "files in the directory will be processed.")
Andrew Boiebbd670c2015-08-17 13:16:11 -07001833 parser.add_argument("-z", "--size", action="append",
1834 help="Don't run sanity checks. Instead, produce a report to "
1835 "stdout detailing RAM/ROM sizes on the specified filenames. "
1836 "All other command line arguments ignored.")
Andrew Boie6bb087c2016-02-10 13:39:00 -08001837 parser.add_argument("-S", "--enable-slow", action="store_true",
1838 help="Execute time-consuming test cases that have been marked "
1839 "as 'slow' in testcase.ini. Normally these are only built.")
Andrew Boie55121052016-07-20 11:52:04 -07001840 parser.add_argument("-R", "--enable-asserts", action="store_true",
1841 help="Build all test cases with assertions enabled.")
Anas Nashife3febe92016-11-30 14:25:44 -05001842 parser.add_argument("-Q", "--error-on-deprecations", action="store_false",
1843 help="Error on deprecation warnings.")
Andrew Boieba612002016-09-01 10:41:03 -07001844 parser.add_argument("-x", "--extra-args", action="append", default=[],
1845 help="Extra arguments to pass to the build when compiling test "
1846 "cases. May be called multiple times. These will be passed "
1847 "in after any sanitycheck-supplied options.")
Jaakko Hannikainen54c90bc2016-08-31 14:17:03 +03001848 parser.add_argument("-C", "--coverage", action="store_true",
1849 help="Scan for unit test coverage with gcov + lcov.")
Andrew Boie6acbe632015-07-17 12:03:52 -07001850
1851 return parser.parse_args()
1852
1853def log_info(filename):
Mazen NEIFER8f1dd3a2017-02-04 14:32:04 +01001854 filename = os.path.relpath(os.path.realpath(filename))
Andrew Boie6acbe632015-07-17 12:03:52 -07001855 if INLINE_LOGS:
Andrew Boie08ce5a52016-02-22 13:28:10 -08001856 info("{:-^100}".format(filename))
Andrew Boied01535c2017-01-10 13:15:02 -08001857
1858 try:
1859 with open(filename) as fp:
1860 data = fp.read()
1861 except Exception as e:
1862 data = "Unable to read log data (%s)\n" % (str(e))
1863
1864 sys.stdout.write(data)
1865 if log_file:
1866 log_file.write(data)
Andrew Boie08ce5a52016-02-22 13:28:10 -08001867 info("{:-^100}".format(filename))
Andrew Boie6acbe632015-07-17 12:03:52 -07001868 else:
Andrew Boie08ce5a52016-02-22 13:28:10 -08001869 info("\tsee: " + COLOR_YELLOW + filename + COLOR_NORMAL)
Andrew Boie6acbe632015-07-17 12:03:52 -07001870
1871def terse_test_cb(instances, goals, goal):
1872 total_tests = len(goals)
1873 total_done = 0
1874 total_failed = 0
1875
Andrew Boie08ce5a52016-02-22 13:28:10 -08001876 for k, g in goals.items():
Andrew Boie6acbe632015-07-17 12:03:52 -07001877 if g.finished:
1878 total_done += 1
1879 if g.failed:
1880 total_failed += 1
1881
1882 if goal.failed:
1883 i = instances[goal.name]
1884 info("\n\n{:<25} {:<50} {}FAILED{}: {}".format(i.platform.name,
1885 i.test.name, COLOR_RED, COLOR_NORMAL, goal.reason))
1886 log_info(goal.get_error_log())
1887 info("")
1888
Andrew Boie7a992ae2017-01-17 10:40:02 -08001889 sys.stdout.write("\rtotal complete: %s%4d/%4d%s %2d%% failed: %s%4d%s" % (
Andrew Boie6acbe632015-07-17 12:03:52 -07001890 COLOR_GREEN, total_done, total_tests, COLOR_NORMAL,
Andrew Boie7a992ae2017-01-17 10:40:02 -08001891 int((float(total_done) / total_tests) * 100),
Andrew Boie6acbe632015-07-17 12:03:52 -07001892 COLOR_RED if total_failed > 0 else COLOR_NORMAL,
1893 total_failed, COLOR_NORMAL))
1894 sys.stdout.flush()
1895
1896def chatty_test_cb(instances, goals, goal):
1897 i = instances[goal.name]
1898
1899 if VERBOSE < 2 and not goal.finished:
1900 return
1901
1902 if goal.failed:
1903 status = COLOR_RED + "FAILED" + COLOR_NORMAL + ": " + goal.reason
1904 elif goal.finished:
1905 status = COLOR_GREEN + "PASSED" + COLOR_NORMAL
1906 else:
1907 status = goal.make_state
1908
1909 info("{:<25} {:<50} {}".format(i.platform.name, i.test.name, status))
1910 if goal.failed:
1911 log_info(goal.get_error_log())
1912
Andrew Boiebbd670c2015-08-17 13:16:11 -07001913
1914def size_report(sc):
1915 info(sc.filename)
Andrew Boie73b4ee62015-10-07 11:33:22 -07001916 info("SECTION NAME VMA LMA SIZE HEX SZ TYPE")
Andrew Boie9882dcd2015-10-07 14:25:51 -07001917 for i in range(len(sc.sections)):
1918 v = sc.sections[i]
1919
Andrew Boie73b4ee62015-10-07 11:33:22 -07001920 info("%-17s 0x%08x 0x%08x %8d 0x%05x %-7s" %
1921 (v["name"], v["virt_addr"], v["load_addr"], v["size"], v["size"],
1922 v["type"]))
Andrew Boie9882dcd2015-10-07 14:25:51 -07001923
Andrew Boie73b4ee62015-10-07 11:33:22 -07001924 info("Totals: %d bytes (ROM), %d bytes (RAM)" %
1925 (sc.rom_size, sc.ram_size))
Andrew Boiebbd670c2015-08-17 13:16:11 -07001926 info("")
1927
Jaakko Hannikainen54c90bc2016-08-31 14:17:03 +03001928def generate_coverage(outdir, ignores):
1929 with open(os.path.join(outdir, "coverage.log"), "a") as coveragelog:
1930 coveragefile = os.path.join(outdir, "coverage.info")
1931 ztestfile = os.path.join(outdir, "ztest.info")
1932 subprocess.call(["lcov", "--capture", "--directory", outdir,
1933 "--output-file", coveragefile], stdout=coveragelog)
1934 # We want to remove tests/* and tests/ztest/test/* but save tests/ztest
1935 subprocess.call(["lcov", "--extract", coveragefile,
1936 os.path.join(ZEPHYR_BASE, "tests", "ztest", "*"),
1937 "--output-file", ztestfile], stdout=coveragelog)
1938 subprocess.call(["lcov", "--remove", ztestfile,
1939 os.path.join(ZEPHYR_BASE, "tests/ztest/test/*"),
1940 "--output-file", ztestfile], stdout=coveragelog)
1941 for i in ignores:
1942 subprocess.call(["lcov", "--remove", coveragefile, i,
1943 "--output-file", coveragefile], stdout=coveragelog)
1944 subprocess.call(["genhtml", "-output-directory",
1945 os.path.join(outdir, "coverage"),
1946 coveragefile, ztestfile], stdout=coveragelog)
Andrew Boiebbd670c2015-08-17 13:16:11 -07001947
Andrew Boie6acbe632015-07-17 12:03:52 -07001948def main():
Andrew Boie4b182472015-07-31 12:25:22 -07001949 start_time = time.time()
Inaky Perez-Gonzalez9a36cb62016-11-29 10:43:40 -08001950 global VERBOSE, INLINE_LOGS, CPU_COUNTS, log_file
Andrew Boie6acbe632015-07-17 12:03:52 -07001951 args = parse_arguments()
Javier B Perez4b554ba2016-08-15 13:25:33 -05001952 toolchain = os.environ.get("ZEPHYR_GCC_VARIANT", None)
Andrew Boie1e4e68b2017-02-10 11:40:54 -08001953 if toolchain == "zephyr":
1954 os.environ["DISABLE_TRYRUN"] = "1"
Andrew Boiebbd670c2015-08-17 13:16:11 -07001955
1956 if args.size:
1957 for fn in args.size:
Andrew Boie52fef672016-11-29 12:21:59 -08001958 size_report(SizeCalculator(fn, []))
Andrew Boiebbd670c2015-08-17 13:16:11 -07001959 sys.exit(0)
1960
Andrew Boie6acbe632015-07-17 12:03:52 -07001961 VERBOSE += args.verbose
1962 INLINE_LOGS = args.inline_logs
Inaky Perez-Gonzalez9a36cb62016-11-29 10:43:40 -08001963 if args.log_file:
1964 log_file = open(args.log_file, "w")
Andrew Boie6acbe632015-07-17 12:03:52 -07001965 if args.jobs:
Daniel Leung6b170072016-04-07 12:10:25 -07001966 CPU_COUNTS = args.jobs
Andrew Boie6acbe632015-07-17 12:03:52 -07001967
Anas Nashif035799f2017-05-13 21:31:53 -04001968 if args.subset:
1969 subset, sets = args.subset.split("/")
1970 if int(subset) > 0 and int(sets) >= int(subset):
1971 info("Running only a subset: %s/%s" %(subset,sets))
1972 else:
1973 error("You have provided a wrong subset value: %s." %args.subset)
1974 return
1975
Andrew Boieae9e7f7b2015-07-31 12:26:12 -07001976 if os.path.exists(args.outdir) and not args.no_clean:
Andrew Boie6acbe632015-07-17 12:03:52 -07001977 info("Cleaning output directory " + args.outdir)
1978 shutil.rmtree(args.outdir)
1979
Andrew Boie3d348712016-04-08 11:52:13 -07001980 if not args.testcase_root:
1981 args.testcase_root = [os.path.join(ZEPHYR_BASE, "tests"),
1982 os.path.join(ZEPHYR_BASE, "samples")]
1983
Jaakko Hannikainen54c90bc2016-08-31 14:17:03 +03001984 ts = TestSuite(args.arch_root, args.testcase_root, args.outdir, args.coverage)
Anas Nashifdfa86e22016-10-24 17:08:56 -04001985 discards = ts.apply_filters(args.platform, args.arch, args.tag, args.exclude_tag, args.config,
Andrew Boie821d8322016-03-22 10:08:35 -07001986 args.test, args.only_failed, args.all,
Kumar Galad5719a62016-10-26 12:30:26 -05001987 args.platform_limit, toolchain, args.extra_args, args.ccache)
Andrew Boie6acbe632015-07-17 12:03:52 -07001988
1989 if args.discard_report:
1990 ts.discard_report(args.discard_report)
1991
1992 if VERBOSE:
Andrew Boie08ce5a52016-02-22 13:28:10 -08001993 for i, reason in discards.items():
Andrew Boie6acbe632015-07-17 12:03:52 -07001994 debug("{:<25} {:<50} {}SKIPPED{}: {}".format(i.platform.name,
1995 i.test.name, COLOR_YELLOW, COLOR_NORMAL, reason))
1996
Anas Nashif035799f2017-05-13 21:31:53 -04001997 ts.instancets = OrderedDict(sorted(ts.instances.items(), key=lambda t: t[0]))
1998
1999 if args.subset:
2000 subset, sets = args.subset.split("/")
2001 total = len(ts.instancets)
2002 per_set = round(total / int(sets))
2003 start = (int(subset) - 1 ) * per_set
2004 if subset == sets:
2005 end = total
2006 else:
2007 end = start + per_set
2008
2009 sliced_instances = islice(ts.instancets.items(),start, end)
2010 ts.instances = OrderedDict(sliced_instances)
2011
Andrew Boie6acbe632015-07-17 12:03:52 -07002012 info("%d tests selected, %d tests discarded due to filters" %
2013 (len(ts.instances), len(discards)))
2014
2015 if args.dry_run:
2016 return
2017
2018 if VERBOSE or not TERMINAL:
Andrew Boie6bb087c2016-02-10 13:39:00 -08002019 goals = ts.execute(chatty_test_cb, ts.instances, args.build_only,
Anas Nashife3febe92016-11-30 14:25:44 -05002020 args.enable_slow, args.enable_asserts, args.error_on_deprecations,
Kumar Galad5719a62016-10-26 12:30:26 -05002021 args.extra_args, args.ccache)
Andrew Boie6acbe632015-07-17 12:03:52 -07002022 else:
Andrew Boie6bb087c2016-02-10 13:39:00 -08002023 goals = ts.execute(terse_test_cb, ts.instances, args.build_only,
Anas Nashife3febe92016-11-30 14:25:44 -05002024 args.enable_slow, args.enable_asserts, args.error_on_deprecations,
Kumar Galad5719a62016-10-26 12:30:26 -05002025 args.extra_args, args.ccache)
Andrew Boie08ce5a52016-02-22 13:28:10 -08002026 info("")
Andrew Boie6acbe632015-07-17 12:03:52 -07002027
Daniel Leung7f850102016-04-08 11:07:32 -07002028 # figure out which report to use for size comparison
2029 if args.compare_report:
2030 report_to_use = args.compare_report
2031 elif args.last_metrics:
2032 report_to_use = LAST_SANITY
2033 else:
2034 report_to_use = RELEASE_DATA
2035
2036 deltas = ts.compare_metrics(report_to_use)
Andrew Boie6acbe632015-07-17 12:03:52 -07002037 warnings = 0
2038 if deltas:
Andrew Boieea7928f2015-08-14 14:27:38 -07002039 for i, metric, value, delta, lower_better in deltas:
2040 if not args.all_deltas and ((delta < 0 and lower_better) or
2041 (delta > 0 and not lower_better)):
Andrew Boie6acbe632015-07-17 12:03:52 -07002042 continue
2043
Andrew Boieea7928f2015-08-14 14:27:38 -07002044 percentage = (float(delta) / float(value - delta))
2045 if not args.all_deltas and (percentage <
2046 (args.footprint_threshold / 100.0)):
2047 continue
2048
Daniel Leung00525c22016-04-11 10:27:56 -07002049 info("{:<25} {:<60} {}{}{}: {} {:<+4}, is now {:6} {:+.2%}".format(
Andrew Boieea7928f2015-08-14 14:27:38 -07002050 i.platform.name, i.test.name, COLOR_YELLOW,
2051 "INFO" if args.all_deltas else "WARNING", COLOR_NORMAL,
Andrew Boie829c0562015-10-13 09:44:19 -07002052 metric, delta, value, percentage))
Andrew Boie6acbe632015-07-17 12:03:52 -07002053 warnings += 1
2054
2055 if warnings:
2056 info("Deltas based on metrics from last %s" %
2057 ("release" if not args.last_metrics else "run"))
2058
2059 failed = 0
Andrew Boie08ce5a52016-02-22 13:28:10 -08002060 for name, goal in goals.items():
Andrew Boie6acbe632015-07-17 12:03:52 -07002061 if goal.failed:
2062 failed += 1
Jaakko Hannikainenca505f82016-08-22 15:03:46 +03002063 elif goal.metrics.get("unrecognized"):
Andrew Boie73b4ee62015-10-07 11:33:22 -07002064 info("%sFAILED%s: %s has unrecognized binary sections: %s" %
2065 (COLOR_RED, COLOR_NORMAL, goal.name,
2066 str(goal.metrics["unrecognized"])))
2067 failed += 1
Andrew Boie6acbe632015-07-17 12:03:52 -07002068
Jaakko Hannikainen54c90bc2016-08-31 14:17:03 +03002069 if args.coverage:
2070 info("Generating coverage files...")
2071 generate_coverage(args.outdir, ["tests/*", "samples/*"])
2072
Anas Nashif0605fa32017-05-07 08:51:02 -04002073 duration = time.time() - start_time
Andrew Boie4b182472015-07-31 12:25:22 -07002074 info("%s%d of %d%s tests passed with %s%d%s warnings in %d seconds" %
Andrew Boie6acbe632015-07-17 12:03:52 -07002075 (COLOR_RED if failed else COLOR_GREEN, len(goals) - failed,
2076 len(goals), COLOR_NORMAL, COLOR_YELLOW if warnings else COLOR_NORMAL,
Anas Nashif0605fa32017-05-07 08:51:02 -04002077 warnings, COLOR_NORMAL, duration))
Andrew Boie6acbe632015-07-17 12:03:52 -07002078
2079 if args.testcase_report:
2080 ts.testcase_report(args.testcase_report)
2081 if not args.no_update:
Anas Nashif0605fa32017-05-07 08:51:02 -04002082 ts.testcase_xunit_report(LAST_SANITY_XUNIT, duration, args)
Andrew Boie6acbe632015-07-17 12:03:52 -07002083 ts.testcase_report(LAST_SANITY)
2084 if args.release:
2085 ts.testcase_report(RELEASE_DATA)
Inaky Perez-Gonzalez9a36cb62016-11-29 10:43:40 -08002086 if log_file:
2087 log_file.close()
Andrew Boie6acbe632015-07-17 12:03:52 -07002088 if failed or (warnings and args.warnings_as_errors):
2089 sys.exit(1)
2090
2091if __name__ == "__main__":
2092 main()