blob: ec4e1138f23f494b96f0ce88dcce150d7b6a4718 [file] [log] [blame]
Andrew Boie08ce5a52016-02-22 13:28:10 -08001#!/usr/bin/env python3
Andrew Boie6acbe632015-07-17 12:03:52 -07002"""Zephyr Sanity Tests
3
4This script scans for the set of unit test applications in the git
5repository and attempts to execute them. By default, it tries to
6build each test case on one platform per architecture, using a precedence
7list defined in an archtecture configuration file, and if possible
8run the tests in the QEMU emulator.
9
10Test cases are detected by the presence of a 'testcase.ini' file in
11the application's project directory. This file may contain one or
12more blocks, each identifying a test scenario. The title of the block
13is a name for the test case, which only needs to be unique for the
14test cases specified in that testcase.ini file. The full canonical
15name for each test case is <path to test case under samples/>/<block>.
16
17Each testcase.ini block can define the following key/value pairs:
18
19 tags = <list of tags> (required)
20 A set of string tags for the testcase. Usually pertains to
21 functional domains but can be anything. Command line invocations
22 of this script can filter the set of tests to run based on tag.
23
Andrew Boie6bb087c2016-02-10 13:39:00 -080024 skip = <True|False> (default False)
Anas Nashif2bd99bc2015-10-12 13:10:57 -040025 skip testcase unconditionally. This can be used for broken tests.
26
Andrew Boie6bb087c2016-02-10 13:39:00 -080027 slow = <True|False> (default False)
28 Don't run this test case unless --enable-slow was passed in on the
29 command line. Intended for time-consuming test cases that are only
30 run under certain circumstances, like daily builds. These test cases
31 are still compiled.
32
Anas Nashifa7c4aa22016-02-09 20:31:26 -050033 kernel = <nano|micro>
34 Specify the kernel being tested instead of relying on parsing
35 Makefile for KERNEL_TYPE
36
Andrew Boie6acbe632015-07-17 12:03:52 -070037 extra_args = <list of extra arguments>
38 Extra arguments to pass to Make when building or running the
39 test case.
40
Andrew Boie6bb087c2016-02-10 13:39:00 -080041 build_only = <True|False> (default False)
Andrew Boie6acbe632015-07-17 12:03:52 -070042 If true, don't try to run the test under QEMU even if the
43 selected platform supports it.
44
45 timeout = <number of seconds>
46 Length of time to run test in QEMU before automatically killing it.
47 Default to 60 seconds.
48
49 arch_whitelist = <list of arches, such as x86, arm, arc>
50 Set of architectures that this test case should only be run for.
51
Anas Nashif30d13872015-10-05 10:02:45 -040052 arch_exclude = <list of arches, such as x86, arm, arc>
53 Set of architectures that this test case should not run on.
54
Andrew Boie6acbe632015-07-17 12:03:52 -070055 platform_whitelist = <list of platforms>
Anas Nashif30d13872015-10-05 10:02:45 -040056 Set of platforms that this test case should only be run for.
57
58 platform_exclude = <list of platforms>
59 Set of platforms that this test case should not run on.
Andrew Boie6acbe632015-07-17 12:03:52 -070060
Andrew Boie3ea78922016-03-24 14:46:00 -070061 filter = <expression>
62 Filter whether the testcase should be run by evaluating an expression
63 against an environment containing the following values:
64
65 { ARCH : <architecture>,
66 PLATFORM : <platform>,
Javier B Perez79414542016-08-08 12:24:59 -050067 <all CONFIG_* key/value pairs in the test's generated defconfig>,
68 *<env>: any environment variable available
Andrew Boie3ea78922016-03-24 14:46:00 -070069 }
70
71 The grammar for the expression language is as follows:
72
73 expression ::= expression "and" expression
74 | expression "or" expression
75 | "not" expression
76 | "(" expression ")"
77 | symbol "==" constant
78 | symbol "!=" constant
79 | symbol "<" number
80 | symbol ">" number
81 | symbol ">=" number
82 | symbol "<=" number
83 | symbol "in" list
Andrew Boie7a87f9f2016-06-02 12:27:54 -070084 | symbol ":" string
Andrew Boie3ea78922016-03-24 14:46:00 -070085 | symbol
86
87 list ::= "[" list_contents "]"
88
89 list_contents ::= constant
90 | list_contents "," constant
91
92 constant ::= number
93 | string
94
95
96 For the case where expression ::= symbol, it evaluates to true
97 if the symbol is defined to a non-empty string.
98
99 Operator precedence, starting from lowest to highest:
100
101 or (left associative)
102 and (left associative)
103 not (right associative)
104 all comparison operators (non-associative)
105
106 arch_whitelist, arch_exclude, platform_whitelist, platform_exclude
107 are all syntactic sugar for these expressions. For instance
108
109 arch_exclude = x86 arc
110
111 Is the same as:
112
113 filter = not ARCH in ["x86", "arc"]
Andrew Boie6acbe632015-07-17 12:03:52 -0700114
Andrew Boie7a87f9f2016-06-02 12:27:54 -0700115 The ':' operator compiles the string argument as a regular expression,
116 and then returns a true value only if the symbol's value in the environment
117 matches. For example, if CONFIG_SOC="quark_se" then
118
119 filter = CONFIG_SOC : "quark.*"
120
121 Would match it.
122
Andrew Boie6acbe632015-07-17 12:03:52 -0700123Architectures and platforms are defined in an archtecture configuration
124file which are stored by default in scripts/sanity_chk/arches/. These
125each define an [arch] block with the following key/value pairs:
126
127 name = <arch name>
128 The name of the arch. Example: x86
129
130 platforms = <list of supported platforms in order of precedence>
131 List of supported platforms for this arch. The ordering here
132 is used to select a default platform to build for that arch.
133
134For every platform defined, there must be a corresponding block for it
135in the arch configuration file. This block can be empty if there are
136no special definitions for that arch. Options are:
137
138 qemu_support = <True|False> (default False)
139 Indicates whether binaries for this platform can run under QEMU
140
141 microkernel_support = <True|False> (default True)
142 Indicates whether this platform supports microkernel or just nanokernel
143
144The set of test cases that actually run depends on directives in the
145testcase and archtecture .ini file and options passed in on the command
146line. If there is every any confusion, running with -v or --discard-report
147can help show why particular test cases were skipped.
148
149Metrics (such as pass/fail state and binary size) for the last code
150release are stored in scripts/sanity_chk/sanity_last_release.csv.
151To update this, pass the --all --release options.
152
153Most 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
Andrew Boie6acbe632015-07-17 12:03:52 -0700173
Anas Nashifc0ddcb72016-02-01 13:18:14 -0500174os.environ["DISABLE_TRYRUN"] = "1"
Andrew Boie6acbe632015-07-17 12:03:52 -0700175if "ZEPHYR_BASE" not in os.environ:
Anas Nashif427cdd32015-08-06 07:25:42 -0400176 sys.stderr.write("$ZEPHYR_BASE environment variable undefined.\n")
Andrew Boie6acbe632015-07-17 12:03:52 -0700177 exit(1)
178ZEPHYR_BASE = os.environ["ZEPHYR_BASE"]
Andrew Boie3ea78922016-03-24 14:46:00 -0700179
180sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts/"))
181
182import expr_parser
183
Andrew Boie6acbe632015-07-17 12:03:52 -0700184VERBOSE = 0
185LAST_SANITY = os.path.join(ZEPHYR_BASE, "scripts", "sanity_chk",
186 "last_sanity.csv")
187RELEASE_DATA = os.path.join(ZEPHYR_BASE, "scripts", "sanity_chk",
188 "sanity_last_release.csv")
Daniel Leung6b170072016-04-07 12:10:25 -0700189CPU_COUNTS = multiprocessing.cpu_count()
Andrew Boie6acbe632015-07-17 12:03:52 -0700190
191if os.isatty(sys.stdout.fileno()):
192 TERMINAL = True
193 COLOR_NORMAL = '\033[0m'
194 COLOR_RED = '\033[91m'
195 COLOR_GREEN = '\033[92m'
196 COLOR_YELLOW = '\033[93m'
197else:
198 TERMINAL = False
199 COLOR_NORMAL = ""
200 COLOR_RED = ""
201 COLOR_GREEN = ""
202 COLOR_YELLOW = ""
203
204class SanityCheckException(Exception):
205 pass
206
207class SanityRuntimeError(SanityCheckException):
208 pass
209
210class ConfigurationError(SanityCheckException):
211 def __init__(self, cfile, message):
212 self.cfile = cfile
213 self.message = message
214
215 def __str__(self):
216 return repr(self.cfile + ": " + self.message)
217
218class MakeError(SanityCheckException):
219 pass
220
221class BuildError(MakeError):
222 pass
223
224class ExecutionError(MakeError):
225 pass
226
227# Debug Functions
Andrew Boie08ce5a52016-02-22 13:28:10 -0800228def info(what):
229 sys.stdout.write(what + "\n")
Andrew Boie6acbe632015-07-17 12:03:52 -0700230
231def error(what):
232 sys.stderr.write(COLOR_RED + what + COLOR_NORMAL + "\n")
233
Andrew Boie08ce5a52016-02-22 13:28:10 -0800234def debug(what):
235 if VERBOSE >= 1:
236 info(what)
237
Andrew Boie6acbe632015-07-17 12:03:52 -0700238def verbose(what):
239 if VERBOSE >= 2:
Andrew Boie08ce5a52016-02-22 13:28:10 -0800240 info(what)
Andrew Boie6acbe632015-07-17 12:03:52 -0700241
242# Utility functions
243class QEMUHandler:
244 """Spawns a thread to monitor QEMU output from pipes
245
246 We pass QEMU_PIPE to 'make qemu' and monitor the pipes for output.
247 We need to do this as once qemu starts, it runs forever until killed.
248 Test cases emit special messages to the console as they run, we check
249 for these to collect whether the test passed or failed.
250 """
251 RUN_PASSED = "PROJECT EXECUTION SUCCESSFUL"
252 RUN_FAILED = "PROJECT EXECUTION FAILED"
253
254 @staticmethod
255 def _thread(handler, timeout, outdir, logfile, fifo_fn, pid_fn, results):
256 fifo_in = fifo_fn + ".in"
257 fifo_out = fifo_fn + ".out"
258
259 # These in/out nodes are named from QEMU's perspective, not ours
260 if os.path.exists(fifo_in):
261 os.unlink(fifo_in)
262 os.mkfifo(fifo_in)
263 if os.path.exists(fifo_out):
264 os.unlink(fifo_out)
265 os.mkfifo(fifo_out)
266
267 # We don't do anything with out_fp but we need to open it for
268 # writing so that QEMU doesn't block, due to the way pipes work
269 out_fp = open(fifo_in, "wb")
270 # Disable internal buffering, we don't
271 # want read() or poll() to ever block if there is data in there
272 in_fp = open(fifo_out, "rb", buffering=0)
Andrew Boie08ce5a52016-02-22 13:28:10 -0800273 log_out_fp = open(logfile, "wt")
Andrew Boie6acbe632015-07-17 12:03:52 -0700274
275 start_time = time.time()
276 timeout_time = start_time + timeout
277 p = select.poll()
278 p.register(in_fp, select.POLLIN)
279
280 metrics = {}
281 line = ""
282 while True:
283 this_timeout = int((timeout_time - time.time()) * 1000)
284 if this_timeout < 0 or not p.poll(this_timeout):
285 out_state = "timeout"
286 break
287
Genaro Saucedo Tejada2968b362016-08-09 15:11:53 -0500288 try:
289 c = in_fp.read(1).decode("utf-8")
290 except UnicodeDecodeError:
291 # Test is writing something weird, fail
292 out_state = "unexpected byte"
293 break
294
Andrew Boie6acbe632015-07-17 12:03:52 -0700295 if c == "":
296 # EOF, this shouldn't happen unless QEMU crashes
297 out_state = "unexpected eof"
298 break
299 line = line + c
300 if c != "\n":
301 continue
302
303 # If we get here, line contains a full line of data output from QEMU
304 log_out_fp.write(line)
305 log_out_fp.flush()
306 line = line.strip()
307 verbose("QEMU: %s" % line)
308
309 if line == QEMUHandler.RUN_PASSED:
310 out_state = "passed"
311 break
312
313 if line == QEMUHandler.RUN_FAILED:
314 out_state = "failed"
315 break
316
317 # TODO: Add support for getting numerical performance data
318 # from test cases. Will involve extending test case reporting
319 # APIs. Add whatever gets reported to the metrics dictionary
320 line = ""
321
322 metrics["qemu_time"] = time.time() - start_time
323 verbose("QEMU complete (%s) after %f seconds" %
324 (out_state, metrics["qemu_time"]))
325 handler.set_state(out_state, metrics)
326
327 log_out_fp.close()
328 out_fp.close()
329 in_fp.close()
330
331 pid = int(open(pid_fn).read())
332 os.unlink(pid_fn)
Andrew Boie08ce5a52016-02-22 13:28:10 -0800333 try:
334 os.kill(pid, signal.SIGTERM)
335 except ProcessLookupError:
336 # Oh well, as long as it's dead! User probably sent Ctrl-C
337 pass
338
Andrew Boie6acbe632015-07-17 12:03:52 -0700339 os.unlink(fifo_in)
340 os.unlink(fifo_out)
341
342
343 def __init__(self, name, outdir, log_fn, timeout):
344 """Constructor
345
346 @param name Arbitrary name of the created thread
347 @param outdir Working directory, shoudl be where qemu.pid gets created
348 by kbuild
349 @param log_fn Absolute path to write out QEMU's log data
350 @param timeout Kill the QEMU process if it doesn't finish up within
351 the given number of seconds
352 """
353 # Create pipe to get QEMU's serial output
354 self.results = {}
355 self.state = "waiting"
356 self.lock = threading.Lock()
357
358 # We pass this to QEMU which looks for fifos with .in and .out
359 # suffixes.
360 self.fifo_fn = os.path.join(outdir, "qemu-fifo")
361
362 self.pid_fn = os.path.join(outdir, "qemu.pid")
363 if os.path.exists(self.pid_fn):
364 os.unlink(self.pid_fn)
365
366 self.log_fn = log_fn
367 self.thread = threading.Thread(name=name, target=QEMUHandler._thread,
368 args=(self, timeout, outdir, self.log_fn,
369 self.fifo_fn, self.pid_fn,
370 self.results))
371 self.thread.daemon = True
372 verbose("Spawning QEMU process for %s" % name)
373 self.thread.start()
374
375 def set_state(self, state, metrics):
376 self.lock.acquire()
377 self.state = state
378 self.metrics = metrics
379 self.lock.release()
380
381 def get_state(self):
382 self.lock.acquire()
383 ret = (self.state, self.metrics)
384 self.lock.release()
385 return ret
386
387 def get_fifo(self):
388 return self.fifo_fn
389
390
391class SizeCalculator:
Andrew Boie73b4ee62015-10-07 11:33:22 -0700392
393 alloc_sections = ["bss", "noinit"]
394 rw_sections = ["datas", "initlevel", "_k_mem_map_ptr", "_k_pipe_ptr",
Andrew Boie3b930212016-06-07 13:11:45 -0700395 "_k_task_ptr", "_k_task_list", "_k_event_list",
396 "exceptions"]
Andrew Boie73b4ee62015-10-07 11:33:22 -0700397 # These get copied into RAM only on non-XIP
Andrew Boie3b930212016-06-07 13:11:45 -0700398 ro_sections = ["text", "ctors", "init_array", "reset",
Tomasz Bursztykaa6cf6032016-09-12 08:53:55 +0200399 "rodata", "devconfig"]
Andrew Boie73b4ee62015-10-07 11:33:22 -0700400
Andrew Boiebbd670c2015-08-17 13:16:11 -0700401 def __init__(self, filename):
Andrew Boie6acbe632015-07-17 12:03:52 -0700402 """Constructor
403
Andrew Boiebbd670c2015-08-17 13:16:11 -0700404 @param filename Path to the output binary
405 The <filename> is parsed by objdump to determine section sizes
Andrew Boie6acbe632015-07-17 12:03:52 -0700406 """
Andrew Boie6acbe632015-07-17 12:03:52 -0700407 # Make sure this is an ELF binary
Andrew Boiebbd670c2015-08-17 13:16:11 -0700408 with open(filename, "rb") as f:
Andrew Boie6acbe632015-07-17 12:03:52 -0700409 magic = f.read(4)
410
Andrew Boie08ce5a52016-02-22 13:28:10 -0800411 if (magic != b'\x7fELF'):
Andrew Boiebbd670c2015-08-17 13:16:11 -0700412 raise SanityRuntimeError("%s is not an ELF binary" % filename)
Andrew Boie6acbe632015-07-17 12:03:52 -0700413
414 # Search for CONFIG_XIP in the ELF's list of symbols using NM and AWK.
415 # GREP can not be used as it returns an error if the symbol is not found.
Andrew Boiebbd670c2015-08-17 13:16:11 -0700416 is_xip_command = "nm " + filename + " | awk '/CONFIG_XIP/ { print $3 }'"
Andrew Boie8f0211d2016-03-02 20:40:29 -0800417 is_xip_output = subprocess.check_output(is_xip_command, shell=True,
418 stderr=subprocess.STDOUT).decode("utf-8").strip()
419 if is_xip_output.endswith("no symbols"):
420 raise SanityRuntimeError("%s has no symbol information" % filename)
Andrew Boie6acbe632015-07-17 12:03:52 -0700421 self.is_xip = (len(is_xip_output) != 0)
422
Andrew Boiebbd670c2015-08-17 13:16:11 -0700423 self.filename = filename
Andrew Boie73b4ee62015-10-07 11:33:22 -0700424 self.sections = []
425 self.rom_size = 0
Andrew Boie6acbe632015-07-17 12:03:52 -0700426 self.ram_size = 0
Andrew Boie9882dcd2015-10-07 14:25:51 -0700427 self.mismatches = []
Andrew Boie6acbe632015-07-17 12:03:52 -0700428
429 self._calculate_sizes()
430
431 def get_ram_size(self):
432 """Get the amount of RAM the application will use up on the device
433
434 @return amount of RAM, in bytes
435 """
Andrew Boie73b4ee62015-10-07 11:33:22 -0700436 return self.ram_size
Andrew Boie6acbe632015-07-17 12:03:52 -0700437
438 def get_rom_size(self):
439 """Get the size of the data that this application uses on device's flash
440
441 @return amount of ROM, in bytes
442 """
Andrew Boie73b4ee62015-10-07 11:33:22 -0700443 return self.rom_size
Andrew Boie6acbe632015-07-17 12:03:52 -0700444
445 def unrecognized_sections(self):
446 """Get a list of sections inside the binary that weren't recognized
447
448 @return list of unrecogized section names
449 """
450 slist = []
Andrew Boie73b4ee62015-10-07 11:33:22 -0700451 for v in self.sections:
Andrew Boie6acbe632015-07-17 12:03:52 -0700452 if not v["recognized"]:
Andrew Boie73b4ee62015-10-07 11:33:22 -0700453 slist.append(v["name"])
Andrew Boie6acbe632015-07-17 12:03:52 -0700454 return slist
455
Andrew Boie9882dcd2015-10-07 14:25:51 -0700456 def mismatched_sections(self):
457 """Get a list of sections in the binary whose LMA and VMA offsets
458 from the previous section aren't proportional. This leads to issues
459 on XIP systems as they aren't correctly copied in to RAM
460 """
461 slist = []
462 for v in self.sections:
463 if v["lma_off"] != v["vma_off"]:
464 slist.append((v["name"], v["lma_off"], v["vma_off"]))
465 return slist
466
Andrew Boie6acbe632015-07-17 12:03:52 -0700467 def _calculate_sizes(self):
468 """ Calculate RAM and ROM usage by section """
Andrew Boiebbd670c2015-08-17 13:16:11 -0700469 objdump_command = "objdump -h " + self.filename
Andrew Boie6acbe632015-07-17 12:03:52 -0700470 objdump_output = subprocess.check_output(objdump_command,
Andrew Boie08ce5a52016-02-22 13:28:10 -0800471 shell=True).decode("utf-8").splitlines()
Andrew Boie6acbe632015-07-17 12:03:52 -0700472
473 for line in objdump_output:
474 words = line.split()
475
476 if (len(words) == 0): # Skip lines that are too short
477 continue
478
479 index = words[0]
480 if (not index[0].isdigit()): # Skip lines that do not start
481 continue # with a digit
482
483 name = words[1] # Skip lines with section names
484 if (name[0] == '.'): # starting with '.'
485 continue
486
Andrew Boie73b4ee62015-10-07 11:33:22 -0700487 # TODO this doesn't actually reflect the size in flash or RAM as
488 # it doesn't include linker-imposed padding between sections.
489 # It is close though.
Andrew Boie6acbe632015-07-17 12:03:52 -0700490 size = int(words[2], 16)
Andrew Boie9882dcd2015-10-07 14:25:51 -0700491 if size == 0:
492 continue
493
Andrew Boie73b4ee62015-10-07 11:33:22 -0700494 load_addr = int(words[4], 16)
495 virt_addr = int(words[3], 16)
Andrew Boie6acbe632015-07-17 12:03:52 -0700496
497 # Add section to memory use totals (for both non-XIP and XIP scenarios)
Andrew Boie6acbe632015-07-17 12:03:52 -0700498 # Unrecognized section names are not included in the calculations.
Andrew Boie6acbe632015-07-17 12:03:52 -0700499 recognized = True
Andrew Boie73b4ee62015-10-07 11:33:22 -0700500 if name in SizeCalculator.alloc_sections:
501 self.ram_size += size
502 stype = "alloc"
503 elif name in SizeCalculator.rw_sections:
504 self.ram_size += size
505 self.rom_size += size
506 stype = "rw"
507 elif name in SizeCalculator.ro_sections:
508 self.rom_size += size
509 if not self.is_xip:
510 self.ram_size += size
511 stype = "ro"
Andrew Boie6acbe632015-07-17 12:03:52 -0700512 else:
Andrew Boie73b4ee62015-10-07 11:33:22 -0700513 stype = "unknown"
Andrew Boie6acbe632015-07-17 12:03:52 -0700514 recognized = False
Andrew Boie6acbe632015-07-17 12:03:52 -0700515
Andrew Boie9882dcd2015-10-07 14:25:51 -0700516 lma_off = 0
517 vma_off = 0
518
519 # Look for different section padding for LMA and VMA, if present
520 # this really messes up XIP systems as __csSet() copies all of
521 # them off flash into RAM as a single large block of memory
522 if self.is_xip and len(self.sections) > 0:
523 p = self.sections[-1]
524
525 if stype == "rw" and p["type"] == "rw":
526 lma_off = load_addr - p["load_addr"]
527 vma_off = virt_addr - p["virt_addr"]
528
Andrew Boie73b4ee62015-10-07 11:33:22 -0700529 self.sections.append({"name" : name, "load_addr" : load_addr,
530 "size" : size, "virt_addr" : virt_addr,
Andrew Boie9882dcd2015-10-07 14:25:51 -0700531 "type" : stype, "recognized" : recognized,
532 "lma_off" : lma_off, "vma_off" : vma_off})
Andrew Boie6acbe632015-07-17 12:03:52 -0700533
534
535class MakeGoal:
536 """Metadata class representing one of the sub-makes called by MakeGenerator
537
538 MakeGenerator returns a dictionary of these which can then be associdated
539 with TestInstances to get a complete picture of what happened during a test.
540 MakeGenerator is used for tasks outside of building tests (such as
541 defconfigs) which is why MakeGoal is a separate class from TestInstance.
542 """
543 def __init__(self, name, text, qemu, make_log, build_log, run_log,
544 qemu_log):
545 self.name = name
546 self.text = text
547 self.qemu = qemu
548 self.make_log = make_log
549 self.build_log = build_log
550 self.run_log = run_log
551 self.qemu_log = qemu_log
552 self.make_state = "waiting"
553 self.failed = False
554 self.finished = False
555 self.reason = None
556 self.metrics = {}
557
558 def get_error_log(self):
559 if self.make_state == "waiting":
560 # Shouldn't ever see this; breakage in the main Makefile itself.
561 return self.make_log
562 elif self.make_state == "building":
563 # Failure when calling the sub-make to build the code
564 return self.build_log
565 elif self.make_state == "running":
566 # Failure in sub-make for "make qemu", qemu probably failed to start
567 return self.run_log
568 elif self.make_state == "finished":
569 # QEMU finished, but timed out or otherwise wasn't successful
570 return self.qemu_log
571
572 def fail(self, reason):
573 self.failed = True
574 self.finished = True
575 self.reason = reason
576
577 def success(self):
578 self.finished = True
579
580 def __str__(self):
581 if self.finished:
582 if self.failed:
583 return "[%s] failed (%s: see %s)" % (self.name, self.reason,
584 self.get_error_log())
585 else:
586 return "[%s] passed" % self.name
587 else:
588 return "[%s] in progress (%s)" % (self.name, self.make_state)
589
590
591class MakeGenerator:
592 """Generates a Makefile which just calls a bunch of sub-make sessions
593
594 In any given test suite we may need to build dozens if not hundreds of
595 test cases. The cleanest way to parallelize this is to just let Make
596 do the parallelization, sharing the jobserver among all the different
597 sub-make targets.
598 """
599
600 GOAL_HEADER_TMPL = """.PHONY: {goal}
601{goal}:
602"""
603
604 MAKE_RULE_TMPL = """\t@echo sanity_test_{phase} {goal} >&2
Andrew Boie12244a32016-07-28 09:31:56 -0700605\t$(MAKE) -C {directory} O={outdir} V={verb} EXTRA_CFLAGS="-Werror {cflags} -Wno-deprecated-declarations" EXTRA_ASMFLAGS=-Wa,--fatal-warnings EXTRA_LDFLAGS=--fatal-warnings {args} >{logfile} 2>&1
Andrew Boie6acbe632015-07-17 12:03:52 -0700606"""
607
608 GOAL_FOOTER_TMPL = "\t@echo sanity_test_finished {goal} >&2\n\n"
609
610 re_make = re.compile("sanity_test_([A-Za-z0-9]+) (.+)|$|make[:] \*\*\* [[](.+)[]] Error.+$")
611
Andrew Boie55121052016-07-20 11:52:04 -0700612 def __init__(self, base_outdir, asserts=False):
Andrew Boie6acbe632015-07-17 12:03:52 -0700613 """MakeGenerator constructor
614
615 @param base_outdir Intended to be the base out directory. A make.log
616 file will be created here which contains the output of the
617 top-level Make session, as well as the dynamic control Makefile
618 @param verbose If true, pass V=1 to all the sub-makes which greatly
619 increases their verbosity
620 """
621 self.goals = {}
622 if not os.path.exists(base_outdir):
623 os.makedirs(base_outdir)
624 self.logfile = os.path.join(base_outdir, "make.log")
625 self.makefile = os.path.join(base_outdir, "Makefile")
Andrew Boie55121052016-07-20 11:52:04 -0700626 self.asserts = asserts
Andrew Boie6acbe632015-07-17 12:03:52 -0700627
628 def _get_rule_header(self, name):
629 return MakeGenerator.GOAL_HEADER_TMPL.format(goal=name)
630
631 def _get_sub_make(self, name, phase, workdir, outdir, logfile, args):
632 verb = "1" if VERBOSE else "0"
633 args = " ".join(args)
Andrew Boie55121052016-07-20 11:52:04 -0700634 if self.asserts:
635 cflags="-DCONFIG_ASSERT=1 -D__ASSERT_ON=2"
636 else:
637 cflags=""
Andrew Boie6acbe632015-07-17 12:03:52 -0700638 return MakeGenerator.MAKE_RULE_TMPL.format(phase=phase, goal=name,
Andrew Boie55121052016-07-20 11:52:04 -0700639 outdir=outdir, cflags=cflags,
Andrew Boie6acbe632015-07-17 12:03:52 -0700640 directory=workdir, verb=verb,
641 args=args, logfile=logfile)
642
643 def _get_rule_footer(self, name):
644 return MakeGenerator.GOAL_FOOTER_TMPL.format(goal=name)
645
646 def _add_goal(self, outdir):
647 if not os.path.exists(outdir):
648 os.makedirs(outdir)
649
650 def add_build_goal(self, name, directory, outdir, args):
651 """Add a goal to invoke a Kbuild session
652
653 @param name A unique string name for this build goal. The results
654 dictionary returned by execute() will be keyed by this name.
655 @param directory Absolute path to working directory, will be passed
656 to make -C
657 @param outdir Absolute path to output directory, will be passed to
658 Kbuild via -O=<path>
659 @param args Extra command line arguments to pass to 'make', typically
660 environment variables or specific Make goals
661 """
662 self._add_goal(outdir)
663 build_logfile = os.path.join(outdir, "build.log")
664 text = (self._get_rule_header(name) +
665 self._get_sub_make(name, "building", directory,
666 outdir, build_logfile, args) +
667 self._get_rule_footer(name))
668 self.goals[name] = MakeGoal(name, text, None, self.logfile, build_logfile,
669 None, None)
670
671 def add_qemu_goal(self, name, directory, outdir, args, timeout=30):
672 """Add a goal to build a Zephyr project and then run it under QEMU
673
674 The generated make goal invokes Make twice, the first time it will
675 build the default goal, and the second will invoke the 'qemu' goal.
676 The output of the QEMU session will be monitored, and terminated
677 either upon pass/fail result of the test program, or the timeout
678 is reached.
679
680 @param name A unique string name for this build goal. The results
681 dictionary returned by execute() will be keyed by this name.
682 @param directory Absolute path to working directory, will be passed
683 to make -C
684 @param outdir Absolute path to output directory, will be passed to
685 Kbuild via -O=<path>
686 @param args Extra command line arguments to pass to 'make', typically
687 environment variables. Do not pass specific Make goals here.
688 @param timeout Maximum length of time QEMU session should be allowed
689 to run before automatically killing it. Default is 30 seconds.
690 """
691
692 self._add_goal(outdir)
693 build_logfile = os.path.join(outdir, "build.log")
694 run_logfile = os.path.join(outdir, "run.log")
695 qemu_logfile = os.path.join(outdir, "qemu.log")
696
697 q = QEMUHandler(name, outdir, qemu_logfile, timeout)
698 args.append("QEMU_PIPE=%s" % q.get_fifo())
699 text = (self._get_rule_header(name) +
700 self._get_sub_make(name, "building", directory,
701 outdir, build_logfile, args) +
702 self._get_sub_make(name, "running", directory,
703 outdir, run_logfile,
704 args + ["qemu"]) +
705 self._get_rule_footer(name))
706 self.goals[name] = MakeGoal(name, text, q, self.logfile, build_logfile,
707 run_logfile, qemu_logfile)
708
709
Andrew Boieba612002016-09-01 10:41:03 -0700710 def add_test_instance(self, ti, build_only=False, enable_slow=False,
711 extra_args=[]):
Andrew Boie6acbe632015-07-17 12:03:52 -0700712 """Add a goal to build/test a TestInstance object
713
714 @param ti TestInstance object to build. The status dictionary returned
715 by execute() will be keyed by its .name field.
716 """
717 args = ti.test.extra_args[:]
718 args.extend(["ARCH=%s" % ti.platform.arch.name,
Anas Nashifc7406082015-12-13 15:00:31 -0500719 "BOARD=%s" % ti.platform.name])
Andrew Boieba612002016-09-01 10:41:03 -0700720 args.extend(extra_args)
Andrew Boie6bb087c2016-02-10 13:39:00 -0800721 if (ti.platform.qemu_support and (not ti.build_only) and
722 (not build_only) and (enable_slow or not ti.test.slow)):
Andrew Boie6acbe632015-07-17 12:03:52 -0700723 self.add_qemu_goal(ti.name, ti.test.code_location, ti.outdir,
724 args, ti.test.timeout)
725 else:
726 self.add_build_goal(ti.name, ti.test.code_location, ti.outdir, args)
727
728 def execute(self, callback_fn=None, context=None):
729 """Execute all the registered build goals
730
731 @param callback_fn If not None, a callback function will be called
732 as individual goals transition between states. This function
733 should accept two parameters: a string state and an arbitrary
734 context object, supplied here
735 @param context Context object to pass to the callback function.
736 Type and semantics are specific to that callback function.
737 @return A dictionary mapping goal names to final status.
738 """
739
Andrew Boie08ce5a52016-02-22 13:28:10 -0800740 with open(self.makefile, "wt") as tf, \
Andrew Boie6acbe632015-07-17 12:03:52 -0700741 open(os.devnull, "wb") as devnull, \
Andrew Boie08ce5a52016-02-22 13:28:10 -0800742 open(self.logfile, "wt") as make_log:
Andrew Boie6acbe632015-07-17 12:03:52 -0700743 # Create our dynamic Makefile and execute it.
744 # Watch stderr output which is where we will keep
745 # track of build state
Andrew Boie08ce5a52016-02-22 13:28:10 -0800746 for name, goal in self.goals.items():
Andrew Boie6acbe632015-07-17 12:03:52 -0700747 tf.write(goal.text)
748 tf.write("all: %s\n" % (" ".join(self.goals.keys())))
749 tf.flush()
750
751 # os.environ["CC"] = "ccache gcc" FIXME doesn't work
752
Daniel Leung6b170072016-04-07 12:10:25 -0700753 cmd = ["make", "-k", "-j", str(CPU_COUNTS * 2), "-f", tf.name, "all"]
Andrew Boie6acbe632015-07-17 12:03:52 -0700754 p = subprocess.Popen(cmd, stderr=subprocess.PIPE,
755 stdout=devnull)
756
757 for line in iter(p.stderr.readline, b''):
Andrew Boie08ce5a52016-02-22 13:28:10 -0800758 line = line.decode("utf-8")
Andrew Boie6acbe632015-07-17 12:03:52 -0700759 make_log.write(line)
760 verbose("MAKE: " + repr(line.strip()))
761 m = MakeGenerator.re_make.match(line)
762 if not m:
763 continue
764
765 state, name, error = m.groups()
766 if error:
767 goal = self.goals[error]
768 else:
769 goal = self.goals[name]
770 goal.make_state = state
771
772
773 if error:
774 goal.fail("build_error")
775 else:
776 if state == "finished":
777 if goal.qemu:
778 thread_status, metrics = goal.qemu.get_state()
779 goal.metrics.update(metrics)
780 if thread_status == "passed":
781 goal.success()
782 else:
783 goal.fail(thread_status)
784 else:
785 goal.success()
786
787 if callback_fn:
788 callback_fn(context, self.goals, goal)
789
790 p.wait()
791 return self.goals
792
793
794# "list" - List of strings
795# "list:<type>" - List of <type>
796# "set" - Set of unordered, unique strings
797# "set:<type>" - Set of <type>
798# "float" - Floating point
799# "int" - Integer
800# "bool" - Boolean
801# "str" - String
802
803# XXX Be sure to update __doc__ if you change any of this!!
804
805arch_valid_keys = {"name" : {"type" : "str", "required" : True},
Javier B Perez4b554ba2016-08-15 13:25:33 -0500806 "platforms" : {"type" : "list", "required" : True},
807 "supported_toolchains" : {"type" : "list", "required" : True}}
Andrew Boie6acbe632015-07-17 12:03:52 -0700808
809platform_valid_keys = {"qemu_support" : {"type" : "bool", "default" : False},
810 "microkernel_support" : {"type" : "bool",
Javier B Perez4b554ba2016-08-15 13:25:33 -0500811 "default" : True},
812 "supported_toolchains" : {"type" : "list", "default" : []}}
Andrew Boie6acbe632015-07-17 12:03:52 -0700813
814testcase_valid_keys = {"tags" : {"type" : "set", "required" : True},
815 "extra_args" : {"type" : "list"},
816 "build_only" : {"type" : "bool", "default" : False},
Anas Nashif2bd99bc2015-10-12 13:10:57 -0400817 "skip" : {"type" : "bool", "default" : False},
Andrew Boie6bb087c2016-02-10 13:39:00 -0800818 "slow" : {"type" : "bool", "default" : False},
Andrew Boie6acbe632015-07-17 12:03:52 -0700819 "timeout" : {"type" : "int", "default" : 60},
Anas Nashifa7c4aa22016-02-09 20:31:26 -0500820 "kernel" : {"type": "str", "required" : False},
Andrew Boie6acbe632015-07-17 12:03:52 -0700821 "arch_whitelist" : {"type" : "set"},
Anas Nashif30d13872015-10-05 10:02:45 -0400822 "arch_exclude" : {"type" : "set"},
823 "platform_exclude" : {"type" : "set"},
Andrew Boie6acbe632015-07-17 12:03:52 -0700824 "platform_whitelist" : {"type" : "set"},
Andrew Boie3ea78922016-03-24 14:46:00 -0700825 "filter" : {"type" : "str"}}
Andrew Boie6acbe632015-07-17 12:03:52 -0700826
827
828class SanityConfigParser:
829 """Class to read architecture and test case .ini files with semantic checking
830 """
831 def __init__(self, filename):
832 """Instantiate a new SanityConfigParser object
833
834 @param filename Source .ini file to read
835 """
Andrew Boie08ce5a52016-02-22 13:28:10 -0800836 cp = configparser.SafeConfigParser()
Andrew Boie6acbe632015-07-17 12:03:52 -0700837 cp.readfp(open(filename))
838 self.filename = filename
839 self.cp = cp
840
841 def _cast_value(self, value, typestr):
842 v = value.strip()
843 if typestr == "str":
844 return v
845
846 elif typestr == "float":
847 return float(v)
848
849 elif typestr == "int":
850 return int(v)
851
852 elif typestr == "bool":
853 v = v.lower()
854 if v == "true" or v == "1":
855 return True
856 elif v == "" or v == "false" or v == "0":
857 return False
858 raise ConfigurationError(self.filename,
859 "bad value for boolean: '%s'" % value)
860
861 elif typestr.startswith("list"):
862 vs = v.split()
863 if len(typestr) > 4 and typestr[4] == ":":
864 return [self._cast_value(vsi, typestr[5:]) for vsi in vs]
865 else:
866 return vs
867
868 elif typestr.startswith("set"):
869 vs = v.split()
870 if len(typestr) > 3 and typestr[3] == ":":
871 return set([self._cast_value(vsi, typestr[4:]) for vsi in vs])
872 else:
873 return set(vs)
874
875 else:
876 raise ConfigurationError(self.filename, "unknown type '%s'" % value)
877
878
879 def sections(self):
880 """Get the set of sections within the .ini file
881
882 @return a list of string section names"""
883 return self.cp.sections()
884
885 def get_section(self, section, valid_keys):
886 """Get a dictionary representing the keys/values within a section
887
888 @param section The section in the .ini file to retrieve data from
889 @param valid_keys A dictionary representing the intended semantics
890 for this section. Each key in this dictionary is a key that could
891 be specified, if a key is given in the .ini file which isn't in
892 here, it will generate an error. Each value in this dictionary
893 is another dictionary containing metadata:
894
895 "default" - Default value if not given
896 "type" - Data type to convert the text value to. Simple types
897 supported are "str", "float", "int", "bool" which will get
898 converted to respective Python data types. "set" and "list"
899 may also be specified which will split the value by
900 whitespace (but keep the elements as strings). finally,
901 "list:<type>" and "set:<type>" may be given which will
902 perform a type conversion after splitting the value up.
903 "required" - If true, raise an error if not defined. If false
904 and "default" isn't specified, a type conversion will be
905 done on an empty string
906 @return A dictionary containing the section key-value pairs with
907 type conversion and default values filled in per valid_keys
908 """
909
910 d = {}
911 cp = self.cp
912
913 if not cp.has_section(section):
Andrew Boiee57a1e52016-03-22 10:29:14 -0700914 # Just fill it with defaults
915 cp.add_section(section)
Andrew Boie6acbe632015-07-17 12:03:52 -0700916
917 for k, v in cp.items(section):
918 if k not in valid_keys:
919 raise ConfigurationError(self.filename,
920 "Unknown config key '%s' in defintiion for '%s'"
921 % (k, section))
922 d[k] = v
923
Andrew Boie08ce5a52016-02-22 13:28:10 -0800924 for k, kinfo in valid_keys.items():
Andrew Boie6acbe632015-07-17 12:03:52 -0700925 if k not in d:
926 if "required" in kinfo:
927 required = kinfo["required"]
928 else:
929 required = False
930
931 if required:
932 raise ConfigurationError(self.filename,
933 "missing required value for '%s' in section '%s'"
934 % (k, section))
935 else:
936 if "default" in kinfo:
937 default = kinfo["default"]
938 else:
939 default = self._cast_value("", kinfo["type"])
940 d[k] = default
941 else:
942 try:
943 d[k] = self._cast_value(d[k], kinfo["type"])
Andrew Boie08ce5a52016-02-22 13:28:10 -0800944 except ValueError as ve:
Andrew Boie6acbe632015-07-17 12:03:52 -0700945 raise ConfigurationError(self.filename,
946 "bad %s value '%s' for key '%s' in section '%s'"
947 % (kinfo["type"], d[k], k, section))
948
949 return d
950
951
952class Platform:
953 """Class representing metadata for a particular platform
954
Anas Nashifc7406082015-12-13 15:00:31 -0500955 Maps directly to BOARD when building"""
Andrew Boie6acbe632015-07-17 12:03:52 -0700956 def __init__(self, arch, name, plat_dict):
957 """Constructor.
958
959 @param arch Architecture object for this platform
Anas Nashifc7406082015-12-13 15:00:31 -0500960 @param name String name for this platform, same as BOARD
Andrew Boie6acbe632015-07-17 12:03:52 -0700961 @param plat_dict SanityConfigParser output on the relevant section
962 in the architecture configuration file which has lots of metadata.
963 See the Architecture class.
964 """
965 self.name = name
966 self.qemu_support = plat_dict["qemu_support"]
967 self.microkernel_support = plat_dict["microkernel_support"]
968 self.arch = arch
Javier B Perez4b554ba2016-08-15 13:25:33 -0500969 self.supported_toolchains = arch.supported_toolchains
970 if plat_dict["supported_toolchains"]:
971 self.supported_toolchains = plat_dict["supported_toolchains"]
Andrew Boie6acbe632015-07-17 12:03:52 -0700972 # Gets populated in a separate step
973 self.defconfig = {"micro" : None, "nano" : None}
974 pass
975
976 def set_defconfig(self, ktype, defconfig):
977 """Set defconfig information for a particular kernel type.
978
979 We do this in another step because all the defconfigs are generated
980 at once from a sub-make, see TestSuite constructor
981
982 @param ktype Kernel type, either "micro" or "nano"
983 @param defconfig Dictionary containing defconfig information
984 """
985 self.defconfig[ktype] = defconfig
986
987 def get_defconfig(self, ktype):
988 """Return a dictionary representing the key/value pairs expressed
989 in the kernel defconfig used for this arch/platform. Used to identify
990 platform features.
991
992 @param ktype Kernel type, either "micro" or "nano"
993 @return dictionary corresponding to the defconfig contents. unset
994 values will not be defined
995 """
996
997 if ktype == "micro" and not self.microkernel_support:
998 raise SanityRuntimeError("Invalid kernel type queried")
999
1000 return self.defconfig[ktype]
1001
1002 def __repr__(self):
1003 return "<%s on %s>" % (self.name, self.arch.name)
1004
1005
1006class Architecture:
1007 """Class representing metadata for a particular architecture
1008 """
1009 def __init__(self, cfile):
1010 """Architecture constructor
1011
1012 @param cfile Path to Architecture configuration file, which gives
1013 info about the arch and all the platforms for it
1014 """
1015 cp = SanityConfigParser(cfile)
1016 self.platforms = []
1017
1018 arch = cp.get_section("arch", arch_valid_keys)
1019
1020 self.name = arch["name"]
Javier B Perez4b554ba2016-08-15 13:25:33 -05001021 self.supported_toolchains = arch["supported_toolchains"]
Andrew Boie6acbe632015-07-17 12:03:52 -07001022
1023 for plat_name in arch["platforms"]:
1024 verbose("Platform: %s" % plat_name)
1025 plat_dict = cp.get_section(plat_name, platform_valid_keys)
1026 self.platforms.append(Platform(self, plat_name, plat_dict))
1027
1028 def __repr__(self):
1029 return "<arch %s>" % self.name
1030
1031
1032class TestCase:
1033 """Class representing a test application
1034 """
1035 makefile_re = re.compile("\s*KERNEL_TYPE\s*[?=]+\s*(micro|nano)\s*")
1036
Andrew Boie3ea78922016-03-24 14:46:00 -07001037 def __init__(self, testcase_root, workdir, name, tc_dict, inifile):
Andrew Boie6acbe632015-07-17 12:03:52 -07001038 """TestCase constructor.
1039
1040 This gets called by TestSuite as it finds and reads testcase.ini files.
1041 Multiple TestCase instances may be generated from a single testcase.ini,
1042 each one corresponds to a section within that file.
1043
1044 Reads the Makefile inside the testcase directory to figure out the
1045 kernel type for purposes of configuration filtering
1046
1047 We need to have a unique name for every single test case. Since
1048 a testcase.ini can define multiple tests, the canonical name for
1049 the test case is <workdir>/<name>.
1050
1051 @param testcase_root Absolute path to the root directory where
1052 all the test cases live
1053 @param workdir Relative path to the project directory for this
1054 test application from the test_case root.
1055 @param name Name of this test case, corresponding to the section name
1056 in the test case configuration file. For many test cases that just
1057 define one test, can be anything and is usually "test". This is
1058 really only used to distinguish between different cases when
1059 the testcase.ini defines multiple tests
1060 @param tc_dict Dictionary with section values for this test case
1061 from the testcase.ini file
1062 """
1063 self.code_location = os.path.join(testcase_root, workdir)
1064 self.tags = tc_dict["tags"]
1065 self.extra_args = tc_dict["extra_args"]
1066 self.arch_whitelist = tc_dict["arch_whitelist"]
Anas Nashif30d13872015-10-05 10:02:45 -04001067 self.arch_exclude = tc_dict["arch_exclude"]
Anas Nashif2bd99bc2015-10-12 13:10:57 -04001068 self.skip = tc_dict["skip"]
Anas Nashifa7c4aa22016-02-09 20:31:26 -05001069 self.kernel = tc_dict["kernel"]
Anas Nashif30d13872015-10-05 10:02:45 -04001070 self.platform_exclude = tc_dict["platform_exclude"]
Andrew Boie6acbe632015-07-17 12:03:52 -07001071 self.platform_whitelist = tc_dict["platform_whitelist"]
Andrew Boie3ea78922016-03-24 14:46:00 -07001072 self.tc_filter = tc_dict["filter"]
Andrew Boie6acbe632015-07-17 12:03:52 -07001073 self.timeout = tc_dict["timeout"]
1074 self.build_only = tc_dict["build_only"]
Andrew Boie6bb087c2016-02-10 13:39:00 -08001075 self.slow = tc_dict["slow"]
Andrew Boie14ac9022016-04-08 13:31:53 -07001076 self.path = os.path.join(os.path.basename(os.path.abspath(testcase_root)),
1077 workdir, name)
Andrew Boie6acbe632015-07-17 12:03:52 -07001078 self.name = self.path # for now
Anas Nashif2cf0df02015-10-10 09:29:43 -04001079 self.defconfig = {}
Anas Nashifa7c4aa22016-02-09 20:31:26 -05001080 self.ktype = None
Andrew Boie3ea78922016-03-24 14:46:00 -07001081 self.inifile = inifile
Andrew Boie6acbe632015-07-17 12:03:52 -07001082
Anas Nashifa7c4aa22016-02-09 20:31:26 -05001083 if self.kernel:
1084 self.ktype = self.kernel
1085 else:
1086 with open(os.path.join(testcase_root, workdir, "Makefile")) as makefile:
1087 for line in makefile.readlines():
1088 m = TestCase.makefile_re.match(line)
1089 if m:
1090 self.ktype = m.group(1)
1091 break
1092 if not self.ktype:
1093 raise ConfigurationError(os.path.join(workdir, "Makefile"),
1094 "KERNEL_TYPE not found")
Andrew Boie6acbe632015-07-17 12:03:52 -07001095
1096 def __repr__(self):
1097 return self.name
1098
1099
1100
1101class TestInstance:
1102 """Class representing the execution of a particular TestCase on a platform
1103
1104 @param test The TestCase object we want to build/execute
1105 @param platform Platform object that we want to build and run against
1106 @param base_outdir Base directory for all test results. The actual
1107 out directory used is <outdir>/<platform>/<test case name>
1108 """
Andrew Boie6bb087c2016-02-10 13:39:00 -08001109 def __init__(self, test, platform, base_outdir, build_only=False,
1110 slow=False):
Andrew Boie6acbe632015-07-17 12:03:52 -07001111 self.test = test
1112 self.platform = platform
1113 self.name = os.path.join(platform.name, test.path)
1114 self.outdir = os.path.join(base_outdir, platform.name, test.path)
1115 self.build_only = build_only or test.build_only
1116
1117 def calculate_sizes(self):
1118 """Get the RAM/ROM sizes of a test case.
1119
1120 This can only be run after the instance has been executed by
1121 MakeGenerator, otherwise there won't be any binaries to measure.
1122
1123 @return A SizeCalculator object
1124 """
Andrew Boie5d4eb782015-10-02 10:04:56 -07001125 fns = glob.glob(os.path.join(self.outdir, "*.elf"))
1126 if (len(fns) != 1):
1127 raise BuildError("Missing/multiple output ELF binary")
Andrew Boie6acbe632015-07-17 12:03:52 -07001128
Andrew Boie5d4eb782015-10-02 10:04:56 -07001129 return SizeCalculator(fns[0])
Andrew Boie6acbe632015-07-17 12:03:52 -07001130
1131 def __repr__(self):
1132 return "<TestCase %s on %s>" % (self.test.name, self.platform.name)
1133
1134
Andrew Boie4ef16c52015-08-28 12:36:03 -07001135def defconfig_cb(context, goals, goal):
1136 if not goal.failed:
1137 return
1138
1139 info("%sCould not build defconfig for %s%s" %
1140 (COLOR_RED, goal.name, COLOR_NORMAL));
1141 if INLINE_LOGS:
1142 with open(goal.get_error_log()) as fp:
1143 sys.stdout.write(fp.read())
1144 else:
Andrew Boie08ce5a52016-02-22 13:28:10 -08001145 info("\tsee: " + COLOR_YELLOW + goal.get_error_log() + COLOR_NORMAL)
Andrew Boie4ef16c52015-08-28 12:36:03 -07001146
Andrew Boie6acbe632015-07-17 12:03:52 -07001147
1148class TestSuite:
Andrew Boie3ea78922016-03-24 14:46:00 -07001149 config_re = re.compile('(CONFIG_[A-Z0-9_]+)[=]\"?([^\"]*)\"?$')
Andrew Boie6acbe632015-07-17 12:03:52 -07001150
Andrew Boie3d348712016-04-08 11:52:13 -07001151 def __init__(self, arch_root, testcase_roots, outdir):
Andrew Boie6acbe632015-07-17 12:03:52 -07001152 # Keep track of which test cases we've filtered out and why
1153 discards = {}
1154 self.arches = {}
1155 self.testcases = {}
1156 self.platforms = []
Andrew Boieb391e662015-08-31 15:25:45 -07001157 self.outdir = os.path.abspath(outdir)
Andrew Boie6acbe632015-07-17 12:03:52 -07001158 self.instances = {}
1159 self.goals = None
1160 self.discards = None
1161
1162 arch_root = os.path.abspath(arch_root)
Andrew Boie6acbe632015-07-17 12:03:52 -07001163
Andrew Boie3d348712016-04-08 11:52:13 -07001164 for testcase_root in testcase_roots:
1165 testcase_root = os.path.abspath(testcase_root)
Andrew Boie6acbe632015-07-17 12:03:52 -07001166
Andrew Boie3d348712016-04-08 11:52:13 -07001167 debug("Reading test case configuration files under %s..." %
1168 testcase_root)
1169 for dirpath, dirnames, filenames in os.walk(testcase_root,
1170 topdown=True):
1171 verbose("scanning %s" % dirpath)
1172 if "testcase.ini" in filenames:
1173 verbose("Found test case in " + dirpath)
1174 dirnames[:] = []
Andrew Boie3ea78922016-03-24 14:46:00 -07001175 ini_path = os.path.join(dirpath, "testcase.ini")
1176 cp = SanityConfigParser(ini_path)
Andrew Boie3d348712016-04-08 11:52:13 -07001177 workdir = os.path.relpath(dirpath, testcase_root)
1178
1179 for section in cp.sections():
1180 tc_dict = cp.get_section(section, testcase_valid_keys)
Andrew Boie3ea78922016-03-24 14:46:00 -07001181 tc = TestCase(testcase_root, workdir, section, tc_dict,
1182 ini_path)
Andrew Boie3d348712016-04-08 11:52:13 -07001183 self.testcases[tc.name] = tc
Andrew Boie6acbe632015-07-17 12:03:52 -07001184
1185 debug("Reading architecture configuration files under %s..." % arch_root)
1186 for dirpath, dirnames, filenames in os.walk(arch_root):
1187 for filename in filenames:
1188 if filename.endswith(".ini"):
1189 fn = os.path.join(dirpath, filename)
1190 verbose("Found arch configuration " + fn)
1191 arch = Architecture(fn)
1192 self.arches[arch.name] = arch
1193 self.platforms.extend(arch.platforms)
1194
Andrew Boie05e3d7f2016-08-09 11:29:34 -07001195 # Build up a list of boards based on the presence of
1196 # boards/*/*_defconfig files. We want to make sure that the arch.ini
1197 # files are not missing any boards
1198 all_plats = [plat.name for plat in self.platforms]
1199 for dirpath, dirnames, filenames in os.walk(os.path.join(ZEPHYR_BASE,
1200 "boards")):
1201 for filename in filenames:
1202 if filename.endswith("_defconfig"):
1203 board_name = filename.replace("_defconfig", "")
1204 if board_name not in all_plats:
1205 error("Platform '%s' not specified in any arch .ini file and will not be tested"
1206 % board_name)
Andrew Boie6acbe632015-07-17 12:03:52 -07001207 self.instances = {}
1208
1209 def get_last_failed(self):
1210 if not os.path.exists(LAST_SANITY):
1211 return []
1212 result = []
1213 with open(LAST_SANITY, "r") as fp:
1214 cr = csv.DictReader(fp)
1215 for row in cr:
1216 if row["passed"] == "True":
1217 continue
1218 test = row["test"]
1219 platform = row["platform"]
1220 result.append((test, platform))
1221 return result
1222
1223 def apply_filters(self, platform_filter, arch_filter, tag_filter,
Andrew Boie821d8322016-03-22 10:08:35 -07001224 config_filter, testcase_filter, last_failed, all_plats,
Andrew Boieba612002016-09-01 10:41:03 -07001225 platform_limit, toolchain, extra_args):
Andrew Boie6acbe632015-07-17 12:03:52 -07001226 instances = []
1227 discards = {}
1228 verbose("platform filter: " + str(platform_filter))
1229 verbose(" arch_filter: " + str(arch_filter))
1230 verbose(" tag_filter: " + str(tag_filter))
1231 verbose(" config_filter: " + str(config_filter))
1232
1233 if last_failed:
1234 failed_tests = self.get_last_failed()
1235
Andrew Boie821d8322016-03-22 10:08:35 -07001236 default_platforms = False
1237
1238 if all_plats:
1239 info("Selecting all possible platforms per test case")
1240 # When --all used, any --platform arguments ignored
1241 platform_filter = []
1242 elif not platform_filter:
Andrew Boie6acbe632015-07-17 12:03:52 -07001243 info("Selecting default platforms per test case")
1244 default_platforms = True
Andrew Boie6acbe632015-07-17 12:03:52 -07001245
Anas Nashif2cf0df02015-10-10 09:29:43 -04001246 mg = MakeGenerator(self.outdir)
1247 dlist = {}
Andrew Boie08ce5a52016-02-22 13:28:10 -08001248 for tc_name, tc in self.testcases.items():
1249 for arch_name, arch in self.arches.items():
Anas Nashif2cf0df02015-10-10 09:29:43 -04001250 instance_list = []
1251 for plat in arch.platforms:
1252 instance = TestInstance(tc, plat, self.outdir)
1253
Anas Nashif2bd99bc2015-10-12 13:10:57 -04001254 if tc.skip:
1255 continue
1256
Anas Nashif2cf0df02015-10-10 09:29:43 -04001257 if tag_filter and not tc.tags.intersection(tag_filter):
1258 continue
1259
1260 if testcase_filter and tc_name not in testcase_filter:
1261 continue
1262
1263 if last_failed and (tc.name, plat.name) not in failed_tests:
1264 continue
1265
1266 if arch_filter and arch_name not in arch_filter:
1267 continue
1268
1269 if tc.arch_whitelist and arch.name not in tc.arch_whitelist:
1270 continue
1271
1272 if tc.arch_exclude and arch.name in tc.arch_exclude:
1273 continue
1274
1275 if tc.platform_exclude and plat.name in tc.platform_exclude:
1276 continue
1277
1278 if platform_filter and plat.name not in platform_filter:
1279 continue
1280
1281 if tc.platform_whitelist and plat.name not in tc.platform_whitelist:
1282 continue
1283
1284 if not plat.microkernel_support and tc.ktype == "micro":
1285 continue
1286
Andrew Boie3ea78922016-03-24 14:46:00 -07001287 if tc.tc_filter:
Anas Nashif8ea9d022015-11-10 12:24:20 -05001288 args = tc.extra_args[:]
1289 args.extend(["ARCH=" + plat.arch.name,
Anas Nashifc7406082015-12-13 15:00:31 -05001290 "BOARD=" + plat.name, "initconfig"])
Andrew Boieba612002016-09-01 10:41:03 -07001291 args.extend(extra_args)
Anas Nashif2cf0df02015-10-10 09:29:43 -04001292 # FIXME would be nice to use a common outdir for this so that
1293 # conf, gen_idt, etc aren't rebuilt for every plat/ktype combo,
1294 # need a way to avoid different Make processe from clobbering
1295 # each other since they all try to build them simultaneously
1296
1297 o = os.path.join(self.outdir, plat.name, tc.path)
1298 dlist[tc, plat, tc.ktype, tc.name.split("/")[-1]] = os.path.join(o,".config")
1299 goal = "_".join([plat.name, tc.ktype, "_".join(tc.name.split("/")), "initconfig"])
1300 mg.add_build_goal(goal, os.path.join(ZEPHYR_BASE, tc.code_location), o, args)
1301
1302 info("Building testcase defconfigs...")
1303 results = mg.execute(defconfig_cb)
1304
Andrew Boie08ce5a52016-02-22 13:28:10 -08001305 for name, goal in results.items():
Anas Nashif2cf0df02015-10-10 09:29:43 -04001306 if goal.failed:
1307 raise SanityRuntimeError("Couldn't build some defconfigs")
1308
Andrew Boie08ce5a52016-02-22 13:28:10 -08001309 for k, out_config in dlist.items():
Anas Nashif2cf0df02015-10-10 09:29:43 -04001310 test, plat, ktype, name = k
1311 defconfig = {}
1312 with open(out_config, "r") as fp:
1313 for line in fp.readlines():
1314 m = TestSuite.config_re.match(line)
1315 if not m:
Andrew Boie3ea78922016-03-24 14:46:00 -07001316 if line.strip() and not line.startswith("#"):
1317 sys.stderr.write("Unrecognized line %s\n" % line)
Anas Nashif2cf0df02015-10-10 09:29:43 -04001318 continue
1319 defconfig[m.group(1)] = m.group(2).strip()
1320 test.defconfig[plat,ktype] = defconfig
1321
Andrew Boie08ce5a52016-02-22 13:28:10 -08001322 for tc_name, tc in self.testcases.items():
1323 for arch_name, arch in self.arches.items():
Andrew Boie6acbe632015-07-17 12:03:52 -07001324 instance_list = []
1325 for plat in arch.platforms:
1326 instance = TestInstance(tc, plat, self.outdir)
1327
Anas Nashif2bd99bc2015-10-12 13:10:57 -04001328 if tc.skip:
1329 discards[instance] = "Skip filter"
1330 continue
1331
Andrew Boie6acbe632015-07-17 12:03:52 -07001332 if tag_filter and not tc.tags.intersection(tag_filter):
1333 discards[instance] = "Command line testcase tag filter"
1334 continue
1335
1336 if testcase_filter and tc_name not in testcase_filter:
1337 discards[instance] = "Testcase name filter"
1338 continue
1339
1340 if last_failed and (tc.name, plat.name) not in failed_tests:
1341 discards[instance] = "Passed or skipped during last run"
1342 continue
1343
1344 if arch_filter and arch_name not in arch_filter:
1345 discards[instance] = "Command line testcase arch filter"
1346 continue
1347
1348 if tc.arch_whitelist and arch.name not in tc.arch_whitelist:
1349 discards[instance] = "Not in test case arch whitelist"
1350 continue
1351
Anas Nashif30d13872015-10-05 10:02:45 -04001352 if tc.arch_exclude and arch.name in tc.arch_exclude:
1353 discards[instance] = "In test case arch exclude"
1354 continue
1355
1356 if tc.platform_exclude and plat.name in tc.platform_exclude:
1357 discards[instance] = "In test case platform exclude"
1358 continue
1359
Andrew Boie6acbe632015-07-17 12:03:52 -07001360 if platform_filter and plat.name not in platform_filter:
1361 discards[instance] = "Command line platform filter"
1362 continue
1363
1364 if tc.platform_whitelist and plat.name not in tc.platform_whitelist:
1365 discards[instance] = "Not in testcase platform whitelist"
1366 continue
1367
1368 if not plat.microkernel_support and tc.ktype == "micro":
1369 discards[instance] = "No microkernel support for platform"
1370 continue
1371
Javier B Perez4b554ba2016-08-15 13:25:33 -05001372 if toolchain and toolchain not in plat.supported_toolchains:
1373 discards[instance] = "Not supported by the toolchain"
1374 continue
1375
Andrew Boie3ea78922016-03-24 14:46:00 -07001376 defconfig = {"ARCH" : arch.name, "PLATFORM" : plat.name}
Javier B Perez79414542016-08-08 12:24:59 -05001377 defconfig.update(os.environ)
Andrew Boie08ce5a52016-02-22 13:28:10 -08001378 for tcase, tdefconfig in tc.defconfig.items():
Anas Nashif2cf0df02015-10-10 09:29:43 -04001379 p, k = tcase
1380 if k == tc.ktype and p == plat:
Andrew Boie3ea78922016-03-24 14:46:00 -07001381 defconfig.update(tdefconfig)
Anas Nashif2cf0df02015-10-10 09:29:43 -04001382 break
1383
Andrew Boie3ea78922016-03-24 14:46:00 -07001384 if tc.tc_filter:
1385 try:
1386 res = expr_parser.parse(tc.tc_filter, defconfig)
1387 except SyntaxError as se:
1388 sys.stderr.write("Failed processing %s\n" % tc.inifile)
1389 raise se
1390 if not res:
1391 discards[instance] = ("defconfig doesn't satisfy expression '%s'" %
1392 tc.tc_filter)
1393 continue
Anas Nashif2cf0df02015-10-10 09:29:43 -04001394
Andrew Boie6acbe632015-07-17 12:03:52 -07001395 instance_list.append(instance)
1396
1397 if not instance_list:
1398 # Every platform in this arch was rejected already
1399 continue
1400
1401 if default_platforms:
Andrew Boie821d8322016-03-22 10:08:35 -07001402 self.add_instances(instance_list[:platform_limit])
1403 for instance in instance_list[platform_limit:]:
1404 discards[instance] = "Not in first %d platform(s) for arch" % platform_limit
Andrew Boie6acbe632015-07-17 12:03:52 -07001405 else:
Andrew Boie821d8322016-03-22 10:08:35 -07001406 self.add_instances(instance_list)
Andrew Boie6acbe632015-07-17 12:03:52 -07001407 self.discards = discards
1408 return discards
1409
Andrew Boie821d8322016-03-22 10:08:35 -07001410 def add_instances(self, ti_list):
1411 for ti in ti_list:
1412 self.instances[ti.name] = ti
Andrew Boie6acbe632015-07-17 12:03:52 -07001413
Andrew Boieba612002016-09-01 10:41:03 -07001414 def execute(self, cb, cb_context, build_only, enable_slow, enable_asserts,
1415 extra_args):
Daniel Leung6b170072016-04-07 12:10:25 -07001416
1417 def calc_one_elf_size(name, goal):
1418 if not goal.failed:
1419 i = self.instances[name]
1420 sc = i.calculate_sizes()
1421 goal.metrics["ram_size"] = sc.get_ram_size()
1422 goal.metrics["rom_size"] = sc.get_rom_size()
1423 goal.metrics["unrecognized"] = sc.unrecognized_sections()
1424 goal.metrics["mismatched"] = sc.mismatched_sections()
1425
Andrew Boie55121052016-07-20 11:52:04 -07001426 mg = MakeGenerator(self.outdir, asserts=enable_asserts)
Andrew Boie6acbe632015-07-17 12:03:52 -07001427 for i in self.instances.values():
Andrew Boieba612002016-09-01 10:41:03 -07001428 mg.add_test_instance(i, build_only, enable_slow, extra_args)
Andrew Boie6acbe632015-07-17 12:03:52 -07001429 self.goals = mg.execute(cb, cb_context)
Daniel Leung6b170072016-04-07 12:10:25 -07001430
1431 # Parallelize size calculation
1432 executor = concurrent.futures.ThreadPoolExecutor(CPU_COUNTS)
1433 futures = [executor.submit(calc_one_elf_size, name, goal) \
1434 for name, goal in self.goals.items()]
1435 concurrent.futures.wait(futures)
1436
Andrew Boie6acbe632015-07-17 12:03:52 -07001437 return self.goals
1438
1439 def discard_report(self, filename):
1440 if self.discards == None:
1441 raise SanityRuntimeException("apply_filters() hasn't been run!")
1442
1443 with open(filename, "wb") as csvfile:
1444 fieldnames = ["test", "arch", "platform", "reason"]
1445 cw = csv.DictWriter(csvfile, fieldnames, lineterminator=os.linesep)
1446 cw.writeheader()
Andrew Boie08ce5a52016-02-22 13:28:10 -08001447 for instance, reason in self.discards.items():
Andrew Boie6acbe632015-07-17 12:03:52 -07001448 rowdict = {"test" : i.test.name,
1449 "arch" : i.platform.arch.name,
1450 "platform" : i.platform.name,
1451 "reason" : reason}
1452 cw.writerow(rowdict)
1453
1454 def compare_metrics(self, filename):
1455 # name, datatype, lower results better
1456 interesting_metrics = [("ram_size", int, True),
1457 ("rom_size", int, True)]
1458
1459 if self.goals == None:
1460 raise SanityRuntimeException("execute() hasn't been run!")
1461
1462 if not os.path.exists(filename):
1463 info("Cannot compare metrics, %s not found" % filename)
1464 return []
1465
1466 results = []
1467 saved_metrics = {}
1468 with open(filename) as fp:
1469 cr = csv.DictReader(fp)
1470 for row in cr:
1471 d = {}
1472 for m, _, _ in interesting_metrics:
1473 d[m] = row[m]
1474 saved_metrics[(row["test"], row["platform"])] = d
1475
Andrew Boie08ce5a52016-02-22 13:28:10 -08001476 for name, goal in self.goals.items():
Andrew Boie6acbe632015-07-17 12:03:52 -07001477 i = self.instances[name]
1478 mkey = (i.test.name, i.platform.name)
1479 if mkey not in saved_metrics:
1480 continue
1481 sm = saved_metrics[mkey]
1482 for metric, mtype, lower_better in interesting_metrics:
1483 if metric not in goal.metrics:
1484 continue
1485 if sm[metric] == "":
1486 continue
1487 delta = goal.metrics[metric] - mtype(sm[metric])
Andrew Boieea7928f2015-08-14 14:27:38 -07001488 if delta == 0:
1489 continue
1490 results.append((i, metric, goal.metrics[metric], delta,
1491 lower_better))
Andrew Boie6acbe632015-07-17 12:03:52 -07001492 return results
1493
1494 def testcase_report(self, filename):
1495 if self.goals == None:
1496 raise SanityRuntimeException("execute() hasn't been run!")
1497
Andrew Boie08ce5a52016-02-22 13:28:10 -08001498 with open(filename, "wt") as csvfile:
Andrew Boie6acbe632015-07-17 12:03:52 -07001499 fieldnames = ["test", "arch", "platform", "passed", "status",
1500 "extra_args", "qemu", "qemu_time", "ram_size",
1501 "rom_size"]
1502 cw = csv.DictWriter(csvfile, fieldnames, lineterminator=os.linesep)
1503 cw.writeheader()
Andrew Boie08ce5a52016-02-22 13:28:10 -08001504 for name, goal in self.goals.items():
Andrew Boie6acbe632015-07-17 12:03:52 -07001505 i = self.instances[name]
1506 rowdict = {"test" : i.test.name,
1507 "arch" : i.platform.arch.name,
1508 "platform" : i.platform.name,
1509 "extra_args" : " ".join(i.test.extra_args),
1510 "qemu" : i.platform.qemu_support}
1511 if goal.failed:
1512 rowdict["passed"] = False
1513 rowdict["status"] = goal.reason
1514 else:
1515 rowdict["passed"] = True
1516 if goal.qemu:
1517 rowdict["qemu_time"] = goal.metrics["qemu_time"]
1518 rowdict["ram_size"] = goal.metrics["ram_size"]
1519 rowdict["rom_size"] = goal.metrics["rom_size"]
1520 cw.writerow(rowdict)
1521
1522
1523def parse_arguments():
1524
1525 parser = argparse.ArgumentParser(description = __doc__,
1526 formatter_class = argparse.RawDescriptionHelpFormatter)
1527
1528 parser.add_argument("-p", "--platform", action="append",
Andrew Boie821d8322016-03-22 10:08:35 -07001529 help="Platform filter for testing. This option may be used multiple "
1530 "times. Testcases will only be built/run on the platforms "
1531 "specified. If this option is not used, then N platforms will "
1532 "automatically be chosen from each arch to build and test, "
1533 "where N is provided by the --platform-limit option.")
1534 parser.add_argument("-L", "--platform-limit", action="store", type=int,
1535 metavar="N", default=1,
1536 help="Controls what platforms are tested if --platform or "
1537 "--all are not used. For each architecture specified by "
1538 "--arch (defaults to all of them), choose the first "
1539 "N platforms to test in the arch-specific .ini file "
1540 "'platforms' list. Defaults to 1.")
Andrew Boie6acbe632015-07-17 12:03:52 -07001541 parser.add_argument("-a", "--arch", action="append",
1542 help="Arch filter for testing. Takes precedence over --platform. "
1543 "If unspecified, test all arches. Multiple invocations "
1544 "are treated as a logical 'or' relationship")
1545 parser.add_argument("-t", "--tag", action="append",
1546 help="Specify tags to restrict which tests to run by tag value. "
1547 "Default is to not do any tag filtering. Multiple invocations "
1548 "are treated as a logical 'or' relationship")
1549 parser.add_argument("-f", "--only-failed", action="store_true",
1550 help="Run only those tests that failed the previous sanity check "
1551 "invocation.")
1552 parser.add_argument("-c", "--config", action="append",
1553 help="Specify platform configuration values filtering. This can be "
1554 "specified two ways: <config>=<value> or just <config>. The "
1555 "defconfig for all platforms, for all kernel types will be "
1556 "checked. For the <config>=<value> case, only match defconfig "
1557 "that have that value defined. For the <config> case, match "
1558 "defconfig that have that value assigned to any value. "
1559 "Prepend a '!' to invert the match.")
1560 parser.add_argument("-s", "--test", action="append",
1561 help="Run only the specified test cases. These are named by "
1562 "<path to test project relative to "
1563 "--testcase-root>/<testcase.ini section name>")
1564 parser.add_argument("-l", "--all", action="store_true",
Andrew Boie821d8322016-03-22 10:08:35 -07001565 help="Build/test on all platforms. Any --platform arguments "
1566 "ignored.")
Andrew Boie6acbe632015-07-17 12:03:52 -07001567
1568 parser.add_argument("-o", "--testcase-report",
1569 help="Output a CSV spreadsheet containing results of the test run")
1570 parser.add_argument("-d", "--discard-report",
1571 help="Output a CSV spreadhseet showing tests that were skipped "
1572 "and why")
Daniel Leung7f850102016-04-08 11:07:32 -07001573 parser.add_argument("--compare-report",
1574 help="Use this report file for size comparision")
1575
Andrew Boie6acbe632015-07-17 12:03:52 -07001576 parser.add_argument("-y", "--dry-run", action="store_true",
1577 help="Create the filtered list of test cases, but don't actually "
1578 "run them. Useful if you're just interested in "
1579 "--discard-report")
1580
1581 parser.add_argument("-r", "--release", action="store_true",
1582 help="Update the benchmark database with the results of this test "
1583 "run. Intended to be run by CI when tagging an official "
1584 "release. This database is used as a basis for comparison "
1585 "when looking for deltas in metrics such as footprint")
1586 parser.add_argument("-w", "--warnings-as-errors", action="store_true",
1587 help="Treat warning conditions as errors")
1588 parser.add_argument("-v", "--verbose", action="count", default=0,
1589 help="Emit debugging information, call multiple times to increase "
1590 "verbosity")
1591 parser.add_argument("-i", "--inline-logs", action="store_true",
1592 help="Upon test failure, print relevant log data to stdout "
1593 "instead of just a path to it")
1594 parser.add_argument("-m", "--last-metrics", action="store_true",
1595 help="Instead of comparing metrics from the last --release, "
1596 "compare with the results of the previous sanity check "
1597 "invocation")
1598 parser.add_argument("-u", "--no-update", action="store_true",
1599 help="do not update the results of the last run of the sanity "
1600 "checks")
1601 parser.add_argument("-b", "--build-only", action="store_true",
1602 help="Only build the code, do not execute any of it in QEMU")
1603 parser.add_argument("-j", "--jobs", type=int,
1604 help="Number of cores to use when building, defaults to "
1605 "number of CPUs * 2")
1606 parser.add_argument("-H", "--footprint-threshold", type=float, default=5,
1607 help="When checking test case footprint sizes, warn the user if "
1608 "the new app size is greater then the specified percentage "
1609 "from the last release. Default is 5. 0 to warn on any "
1610 "increase on app size")
Andrew Boieea7928f2015-08-14 14:27:38 -07001611 parser.add_argument("-D", "--all-deltas", action="store_true",
1612 help="Show all footprint deltas, positive or negative. Implies "
1613 "--footprint-threshold=0")
Andrew Boie6acbe632015-07-17 12:03:52 -07001614 parser.add_argument("-O", "--outdir",
1615 default="%s/sanity-out" % ZEPHYR_BASE,
1616 help="Output directory for logs and binaries.")
Andrew Boieae9e7f7b2015-07-31 12:26:12 -07001617 parser.add_argument("-n", "--no-clean", action="store_true",
1618 help="Do not delete the outdir before building. Will result in "
1619 "faster compilation since builds will be incremental")
Andrew Boie3d348712016-04-08 11:52:13 -07001620 parser.add_argument("-T", "--testcase-root", action="append", default=[],
Andrew Boie6acbe632015-07-17 12:03:52 -07001621 help="Base directory to recursively search for test cases. All "
Andrew Boie3d348712016-04-08 11:52:13 -07001622 "testcase.ini files under here will be processed. May be "
1623 "called multiple times. Defaults to the 'samples' and "
1624 "'tests' directories in the Zephyr tree.")
Andrew Boie6acbe632015-07-17 12:03:52 -07001625 parser.add_argument("-A", "--arch-root",
1626 default="%s/scripts/sanity_chk/arches" % ZEPHYR_BASE,
1627 help="Directory to search for arch configuration files. All .ini "
1628 "files in the directory will be processed.")
Andrew Boiebbd670c2015-08-17 13:16:11 -07001629 parser.add_argument("-z", "--size", action="append",
1630 help="Don't run sanity checks. Instead, produce a report to "
1631 "stdout detailing RAM/ROM sizes on the specified filenames. "
1632 "All other command line arguments ignored.")
Andrew Boie6bb087c2016-02-10 13:39:00 -08001633 parser.add_argument("-S", "--enable-slow", action="store_true",
1634 help="Execute time-consuming test cases that have been marked "
1635 "as 'slow' in testcase.ini. Normally these are only built.")
Andrew Boie55121052016-07-20 11:52:04 -07001636 parser.add_argument("-R", "--enable-asserts", action="store_true",
1637 help="Build all test cases with assertions enabled.")
Andrew Boieba612002016-09-01 10:41:03 -07001638 parser.add_argument("-x", "--extra-args", action="append", default=[],
1639 help="Extra arguments to pass to the build when compiling test "
1640 "cases. May be called multiple times. These will be passed "
1641 "in after any sanitycheck-supplied options.")
Andrew Boie6acbe632015-07-17 12:03:52 -07001642
1643 return parser.parse_args()
1644
1645def log_info(filename):
1646 filename = os.path.relpath(filename)
1647 if INLINE_LOGS:
Andrew Boie08ce5a52016-02-22 13:28:10 -08001648 info("{:-^100}".format(filename))
Andrew Boie6acbe632015-07-17 12:03:52 -07001649 with open(filename) as fp:
1650 sys.stdout.write(fp.read())
Andrew Boie08ce5a52016-02-22 13:28:10 -08001651 info("{:-^100}".format(filename))
Andrew Boie6acbe632015-07-17 12:03:52 -07001652 else:
Andrew Boie08ce5a52016-02-22 13:28:10 -08001653 info("\tsee: " + COLOR_YELLOW + filename + COLOR_NORMAL)
Andrew Boie6acbe632015-07-17 12:03:52 -07001654
1655def terse_test_cb(instances, goals, goal):
1656 total_tests = len(goals)
1657 total_done = 0
1658 total_failed = 0
1659
Andrew Boie08ce5a52016-02-22 13:28:10 -08001660 for k, g in goals.items():
Andrew Boie6acbe632015-07-17 12:03:52 -07001661 if g.finished:
1662 total_done += 1
1663 if g.failed:
1664 total_failed += 1
1665
1666 if goal.failed:
1667 i = instances[goal.name]
1668 info("\n\n{:<25} {:<50} {}FAILED{}: {}".format(i.platform.name,
1669 i.test.name, COLOR_RED, COLOR_NORMAL, goal.reason))
1670 log_info(goal.get_error_log())
1671 info("")
1672
1673 sys.stdout.write("\rtotal complete: %s%3d/%3d%s failed: %s%3d%s" % (
1674 COLOR_GREEN, total_done, total_tests, COLOR_NORMAL,
1675 COLOR_RED if total_failed > 0 else COLOR_NORMAL,
1676 total_failed, COLOR_NORMAL))
1677 sys.stdout.flush()
1678
1679def chatty_test_cb(instances, goals, goal):
1680 i = instances[goal.name]
1681
1682 if VERBOSE < 2 and not goal.finished:
1683 return
1684
1685 if goal.failed:
1686 status = COLOR_RED + "FAILED" + COLOR_NORMAL + ": " + goal.reason
1687 elif goal.finished:
1688 status = COLOR_GREEN + "PASSED" + COLOR_NORMAL
1689 else:
1690 status = goal.make_state
1691
1692 info("{:<25} {:<50} {}".format(i.platform.name, i.test.name, status))
1693 if goal.failed:
1694 log_info(goal.get_error_log())
1695
Andrew Boiebbd670c2015-08-17 13:16:11 -07001696
1697def size_report(sc):
1698 info(sc.filename)
Andrew Boie73b4ee62015-10-07 11:33:22 -07001699 info("SECTION NAME VMA LMA SIZE HEX SZ TYPE")
Andrew Boie9882dcd2015-10-07 14:25:51 -07001700 for i in range(len(sc.sections)):
1701 v = sc.sections[i]
1702
Andrew Boie73b4ee62015-10-07 11:33:22 -07001703 info("%-17s 0x%08x 0x%08x %8d 0x%05x %-7s" %
1704 (v["name"], v["virt_addr"], v["load_addr"], v["size"], v["size"],
1705 v["type"]))
Andrew Boie9882dcd2015-10-07 14:25:51 -07001706 if v["lma_off"] != v["vma_off"]:
1707 info(" WARNING: LMA and VMA offsets between %s and %s differ: %d vs %d" %
1708 (sc.sections[i-1]["name"], v["name"], v["lma_off"],
1709 v["vma_off"]))
1710
Andrew Boie73b4ee62015-10-07 11:33:22 -07001711 info("Totals: %d bytes (ROM), %d bytes (RAM)" %
1712 (sc.rom_size, sc.ram_size))
Andrew Boiebbd670c2015-08-17 13:16:11 -07001713 info("")
1714
1715
Andrew Boie6acbe632015-07-17 12:03:52 -07001716def main():
Andrew Boie4b182472015-07-31 12:25:22 -07001717 start_time = time.time()
Daniel Leung6b170072016-04-07 12:10:25 -07001718 global VERBOSE, INLINE_LOGS, CPU_COUNTS
Andrew Boie6acbe632015-07-17 12:03:52 -07001719 args = parse_arguments()
Javier B Perez4b554ba2016-08-15 13:25:33 -05001720 toolchain = os.environ.get("ZEPHYR_GCC_VARIANT", None)
Andrew Boiebbd670c2015-08-17 13:16:11 -07001721
1722 if args.size:
1723 for fn in args.size:
1724 size_report(SizeCalculator(fn))
1725 sys.exit(0)
1726
Andrew Boie6acbe632015-07-17 12:03:52 -07001727 VERBOSE += args.verbose
1728 INLINE_LOGS = args.inline_logs
1729 if args.jobs:
Daniel Leung6b170072016-04-07 12:10:25 -07001730 CPU_COUNTS = args.jobs
Andrew Boie6acbe632015-07-17 12:03:52 -07001731
Andrew Boieae9e7f7b2015-07-31 12:26:12 -07001732 if os.path.exists(args.outdir) and not args.no_clean:
Andrew Boie6acbe632015-07-17 12:03:52 -07001733 info("Cleaning output directory " + args.outdir)
1734 shutil.rmtree(args.outdir)
1735
Andrew Boie3d348712016-04-08 11:52:13 -07001736 if not args.testcase_root:
1737 args.testcase_root = [os.path.join(ZEPHYR_BASE, "tests"),
1738 os.path.join(ZEPHYR_BASE, "samples")]
1739
Andrew Boie6acbe632015-07-17 12:03:52 -07001740 ts = TestSuite(args.arch_root, args.testcase_root, args.outdir)
1741 discards = ts.apply_filters(args.platform, args.arch, args.tag, args.config,
Andrew Boie821d8322016-03-22 10:08:35 -07001742 args.test, args.only_failed, args.all,
Andrew Boieba612002016-09-01 10:41:03 -07001743 args.platform_limit, toolchain, args.extra_args)
Andrew Boie6acbe632015-07-17 12:03:52 -07001744
1745 if args.discard_report:
1746 ts.discard_report(args.discard_report)
1747
1748 if VERBOSE:
Andrew Boie08ce5a52016-02-22 13:28:10 -08001749 for i, reason in discards.items():
Andrew Boie6acbe632015-07-17 12:03:52 -07001750 debug("{:<25} {:<50} {}SKIPPED{}: {}".format(i.platform.name,
1751 i.test.name, COLOR_YELLOW, COLOR_NORMAL, reason))
1752
1753 info("%d tests selected, %d tests discarded due to filters" %
1754 (len(ts.instances), len(discards)))
1755
1756 if args.dry_run:
1757 return
1758
1759 if VERBOSE or not TERMINAL:
Andrew Boie6bb087c2016-02-10 13:39:00 -08001760 goals = ts.execute(chatty_test_cb, ts.instances, args.build_only,
Andrew Boieba612002016-09-01 10:41:03 -07001761 args.enable_slow, args.enable_asserts,
1762 args.extra_args)
Andrew Boie6acbe632015-07-17 12:03:52 -07001763 else:
Andrew Boie6bb087c2016-02-10 13:39:00 -08001764 goals = ts.execute(terse_test_cb, ts.instances, args.build_only,
Andrew Boieba612002016-09-01 10:41:03 -07001765 args.enable_slow, args.enable_asserts,
1766 args.extra_args)
Andrew Boie08ce5a52016-02-22 13:28:10 -08001767 info("")
Andrew Boie6acbe632015-07-17 12:03:52 -07001768
Daniel Leung7f850102016-04-08 11:07:32 -07001769 # figure out which report to use for size comparison
1770 if args.compare_report:
1771 report_to_use = args.compare_report
1772 elif args.last_metrics:
1773 report_to_use = LAST_SANITY
1774 else:
1775 report_to_use = RELEASE_DATA
1776
1777 deltas = ts.compare_metrics(report_to_use)
Andrew Boie6acbe632015-07-17 12:03:52 -07001778 warnings = 0
1779 if deltas:
Andrew Boieea7928f2015-08-14 14:27:38 -07001780 for i, metric, value, delta, lower_better in deltas:
1781 if not args.all_deltas and ((delta < 0 and lower_better) or
1782 (delta > 0 and not lower_better)):
Andrew Boie6acbe632015-07-17 12:03:52 -07001783 continue
1784
Andrew Boieea7928f2015-08-14 14:27:38 -07001785 percentage = (float(delta) / float(value - delta))
1786 if not args.all_deltas and (percentage <
1787 (args.footprint_threshold / 100.0)):
1788 continue
1789
Daniel Leung00525c22016-04-11 10:27:56 -07001790 info("{:<25} {:<60} {}{}{}: {} {:<+4}, is now {:6} {:+.2%}".format(
Andrew Boieea7928f2015-08-14 14:27:38 -07001791 i.platform.name, i.test.name, COLOR_YELLOW,
1792 "INFO" if args.all_deltas else "WARNING", COLOR_NORMAL,
Andrew Boie829c0562015-10-13 09:44:19 -07001793 metric, delta, value, percentage))
Andrew Boie6acbe632015-07-17 12:03:52 -07001794 warnings += 1
1795
1796 if warnings:
1797 info("Deltas based on metrics from last %s" %
1798 ("release" if not args.last_metrics else "run"))
1799
1800 failed = 0
Andrew Boie08ce5a52016-02-22 13:28:10 -08001801 for name, goal in goals.items():
Andrew Boie6acbe632015-07-17 12:03:52 -07001802 if goal.failed:
1803 failed += 1
Andrew Boie73b4ee62015-10-07 11:33:22 -07001804 elif goal.metrics["unrecognized"]:
1805 info("%sFAILED%s: %s has unrecognized binary sections: %s" %
1806 (COLOR_RED, COLOR_NORMAL, goal.name,
1807 str(goal.metrics["unrecognized"])))
1808 failed += 1
Andrew Boie9882dcd2015-10-07 14:25:51 -07001809 elif goal.metrics["mismatched"]:
1810 info("%sFAILED%s: %s has mismatched section offsets for: %s" %
1811 (COLOR_RED, COLOR_NORMAL, goal.name,
1812 str(goal.metrics["mismatched"])))
1813 failed += 1
Andrew Boie6acbe632015-07-17 12:03:52 -07001814
Andrew Boie4b182472015-07-31 12:25:22 -07001815 info("%s%d of %d%s tests passed with %s%d%s warnings in %d seconds" %
Andrew Boie6acbe632015-07-17 12:03:52 -07001816 (COLOR_RED if failed else COLOR_GREEN, len(goals) - failed,
1817 len(goals), COLOR_NORMAL, COLOR_YELLOW if warnings else COLOR_NORMAL,
Andrew Boie4b182472015-07-31 12:25:22 -07001818 warnings, COLOR_NORMAL, time.time() - start_time))
Andrew Boie6acbe632015-07-17 12:03:52 -07001819
1820 if args.testcase_report:
1821 ts.testcase_report(args.testcase_report)
1822 if not args.no_update:
1823 ts.testcase_report(LAST_SANITY)
1824 if args.release:
1825 ts.testcase_report(RELEASE_DATA)
1826
1827 if failed or (warnings and args.warnings_as_errors):
1828 sys.exit(1)
1829
1830if __name__ == "__main__":
1831 main()