initial commit for next-gen sanity checks
The online help ./scripts/sanitycheck --help describes usage.
Most users will simply want to run with no arguments.
Change-Id: Icedbbfc22599a64a6e3dbbb808ff3276db06f2e0
Signed-off-by: Andrew Boie <andrew.p.boie@intel.com>
diff --git a/scripts/sanitycheck b/scripts/sanitycheck
new file mode 100755
index 0000000..1c41568
--- /dev/null
+++ b/scripts/sanitycheck
@@ -0,0 +1,1509 @@
+#!/usr/bin/env python
+"""Zephyr Sanity Tests
+
+This script scans for the set of unit test applications in the git
+repository and attempts to execute them. By default, it tries to
+build each test case on one platform per architecture, using a precedence
+list defined in an archtecture configuration file, and if possible
+run the tests in the QEMU emulator.
+
+Test cases are detected by the presence of a 'testcase.ini' file in
+the application's project directory. This file may contain one or
+more blocks, each identifying a test scenario. The title of the block
+is a name for the test case, which only needs to be unique for the
+test cases specified in that testcase.ini file. The full canonical
+name for each test case is <path to test case under samples/>/<block>.
+
+Each testcase.ini block can define the following key/value pairs:
+
+ tags = <list of tags> (required)
+ A set of string tags for the testcase. Usually pertains to
+ functional domains but can be anything. Command line invocations
+ of this script can filter the set of tests to run based on tag.
+
+ extra_args = <list of extra arguments>
+ Extra arguments to pass to Make when building or running the
+ test case.
+
+ build_only = <True|False>
+ If true, don't try to run the test under QEMU even if the
+ selected platform supports it.
+
+ timeout = <number of seconds>
+ Length of time to run test in QEMU before automatically killing it.
+ Default to 60 seconds.
+
+ arch_whitelist = <list of arches, such as x86, arm, arc>
+ Set of architectures that this test case should only be run for.
+
+ platform_whitelist = <list of platforms>
+ Set of platforms that this test case should only be run for for.
+
+ config_whitelist = <list of config options>
+ Config options can either be config names like CONFIG_FOO which
+ match if the configuration is defined to any value, or key/value
+ pairs like CONFIG_FOO=bar which match if it is set to a specific
+ value. May prepend a '!' to invert the match.
+
+Architectures and platforms are defined in an archtecture configuration
+file which are stored by default in scripts/sanity_chk/arches/. These
+each define an [arch] block with the following key/value pairs:
+
+ name = <arch name>
+ The name of the arch. Example: x86
+
+ platforms = <list of supported platforms in order of precedence>
+ List of supported platforms for this arch. The ordering here
+ is used to select a default platform to build for that arch.
+
+For every platform defined, there must be a corresponding block for it
+in the arch configuration file. This block can be empty if there are
+no special definitions for that arch. Options are:
+
+ qemu_support = <True|False> (default False)
+ Indicates whether binaries for this platform can run under QEMU
+
+ microkernel_support = <True|False> (default True)
+ Indicates whether this platform supports microkernel or just nanokernel
+
+The set of test cases that actually run depends on directives in the
+testcase and archtecture .ini file and options passed in on the command
+line. If there is every any confusion, running with -v or --discard-report
+can help show why particular test cases were skipped.
+
+Metrics (such as pass/fail state and binary size) for the last code
+release are stored in scripts/sanity_chk/sanity_last_release.csv.
+To update this, pass the --all --release options.
+
+Most everyday users will run with no arguments.
+"""
+
+import argparse
+import os
+import sys
+import ConfigParser
+import re
+import tempfile
+import subprocess
+import multiprocessing
+import select
+import shutil
+import signal
+import threading
+import time
+import csv
+
+if "ZEPHYR_BASE" not in os.environ:
+ sys.stderr.write("$ZEPHYR_BASE environment variable undefined")
+ exit(1)
+ZEPHYR_BASE = os.environ["ZEPHYR_BASE"]
+VERBOSE = 0
+LAST_SANITY = os.path.join(ZEPHYR_BASE, "scripts", "sanity_chk",
+ "last_sanity.csv")
+RELEASE_DATA = os.path.join(ZEPHYR_BASE, "scripts", "sanity_chk",
+ "sanity_last_release.csv")
+PARALLEL = multiprocessing.cpu_count() * 2
+
+if os.isatty(sys.stdout.fileno()):
+ TERMINAL = True
+ COLOR_NORMAL = '\033[0m'
+ COLOR_RED = '\033[91m'
+ COLOR_GREEN = '\033[92m'
+ COLOR_YELLOW = '\033[93m'
+else:
+ TERMINAL = False
+ COLOR_NORMAL = ""
+ COLOR_RED = ""
+ COLOR_GREEN = ""
+ COLOR_YELLOW = ""
+
+class SanityCheckException(Exception):
+ pass
+
+class SanityRuntimeError(SanityCheckException):
+ pass
+
+class ConfigurationError(SanityCheckException):
+ def __init__(self, cfile, message):
+ self.cfile = cfile
+ self.message = message
+
+ def __str__(self):
+ return repr(self.cfile + ": " + self.message)
+
+class MakeError(SanityCheckException):
+ pass
+
+class BuildError(MakeError):
+ pass
+
+class ExecutionError(MakeError):
+ pass
+
+# Debug Functions
+
+def debug(what):
+ if VERBOSE >= 1:
+ print what
+
+def error(what):
+ sys.stderr.write(COLOR_RED + what + COLOR_NORMAL + "\n")
+
+def verbose(what):
+ if VERBOSE >= 2:
+ print what
+
+def info(what):
+ sys.stdout.write(what + "\n")
+
+# Utility functions
+class QEMUHandler:
+ """Spawns a thread to monitor QEMU output from pipes
+
+ We pass QEMU_PIPE to 'make qemu' and monitor the pipes for output.
+ We need to do this as once qemu starts, it runs forever until killed.
+ Test cases emit special messages to the console as they run, we check
+ for these to collect whether the test passed or failed.
+ """
+ RUN_PASSED = "PROJECT EXECUTION SUCCESSFUL"
+ RUN_FAILED = "PROJECT EXECUTION FAILED"
+
+ @staticmethod
+ def _thread(handler, timeout, outdir, logfile, fifo_fn, pid_fn, results):
+ fifo_in = fifo_fn + ".in"
+ fifo_out = fifo_fn + ".out"
+
+ # These in/out nodes are named from QEMU's perspective, not ours
+ if os.path.exists(fifo_in):
+ os.unlink(fifo_in)
+ os.mkfifo(fifo_in)
+ if os.path.exists(fifo_out):
+ os.unlink(fifo_out)
+ os.mkfifo(fifo_out)
+
+ # We don't do anything with out_fp but we need to open it for
+ # writing so that QEMU doesn't block, due to the way pipes work
+ out_fp = open(fifo_in, "wb")
+ # Disable internal buffering, we don't
+ # want read() or poll() to ever block if there is data in there
+ in_fp = open(fifo_out, "rb", buffering=0)
+ log_out_fp = open(logfile, "w")
+
+ start_time = time.time()
+ timeout_time = start_time + timeout
+ p = select.poll()
+ p.register(in_fp, select.POLLIN)
+
+ metrics = {}
+ line = ""
+ while True:
+ this_timeout = int((timeout_time - time.time()) * 1000)
+ if this_timeout < 0 or not p.poll(this_timeout):
+ out_state = "timeout"
+ break
+
+ c = in_fp.read(1)
+ if c == "":
+ # EOF, this shouldn't happen unless QEMU crashes
+ out_state = "unexpected eof"
+ break
+ line = line + c
+ if c != "\n":
+ continue
+
+ # If we get here, line contains a full line of data output from QEMU
+ log_out_fp.write(line)
+ log_out_fp.flush()
+ line = line.strip()
+ verbose("QEMU: %s" % line)
+
+ if line == QEMUHandler.RUN_PASSED:
+ out_state = "passed"
+ break
+
+ if line == QEMUHandler.RUN_FAILED:
+ out_state = "failed"
+ break
+
+ # TODO: Add support for getting numerical performance data
+ # from test cases. Will involve extending test case reporting
+ # APIs. Add whatever gets reported to the metrics dictionary
+ line = ""
+
+ metrics["qemu_time"] = time.time() - start_time
+ verbose("QEMU complete (%s) after %f seconds" %
+ (out_state, metrics["qemu_time"]))
+ handler.set_state(out_state, metrics)
+
+ log_out_fp.close()
+ out_fp.close()
+ in_fp.close()
+
+ pid = int(open(pid_fn).read())
+ os.unlink(pid_fn)
+ os.kill(pid, signal.SIGTERM)
+ os.unlink(fifo_in)
+ os.unlink(fifo_out)
+
+
+ def __init__(self, name, outdir, log_fn, timeout):
+ """Constructor
+
+ @param name Arbitrary name of the created thread
+ @param outdir Working directory, shoudl be where qemu.pid gets created
+ by kbuild
+ @param log_fn Absolute path to write out QEMU's log data
+ @param timeout Kill the QEMU process if it doesn't finish up within
+ the given number of seconds
+ """
+ # Create pipe to get QEMU's serial output
+ self.results = {}
+ self.state = "waiting"
+ self.lock = threading.Lock()
+
+ # We pass this to QEMU which looks for fifos with .in and .out
+ # suffixes.
+ self.fifo_fn = os.path.join(outdir, "qemu-fifo")
+
+ self.pid_fn = os.path.join(outdir, "qemu.pid")
+ if os.path.exists(self.pid_fn):
+ os.unlink(self.pid_fn)
+
+ self.log_fn = log_fn
+ self.thread = threading.Thread(name=name, target=QEMUHandler._thread,
+ args=(self, timeout, outdir, self.log_fn,
+ self.fifo_fn, self.pid_fn,
+ self.results))
+ self.thread.daemon = True
+ verbose("Spawning QEMU process for %s" % name)
+ self.thread.start()
+
+ def set_state(self, state, metrics):
+ self.lock.acquire()
+ self.state = state
+ self.metrics = metrics
+ self.lock.release()
+
+ def get_state(self):
+ self.lock.acquire()
+ ret = (self.state, self.metrics)
+ self.lock.release()
+ return ret
+
+ def get_fifo(self):
+ return self.fifo_fn
+
+
+class SizeCalculator:
+ def __init__(self, filename_stem):
+ """Constructor
+
+ @param filename_stem Path to the output binary, minus file extension.
+ The <filename_stem>.elf is parsed by objdump. Either .elf or .bin
+ is sized for the ROM size depending on whether XIP is supported
+ """
+ elf_filename = filename_stem + ".elf"
+
+ # Make sure this is an ELF binary
+ with open(elf_filename, "rb") as f:
+ magic = f.read(4)
+
+ if (magic != "\x7fELF"):
+ raise SanityRuntimeError("%s is not an ELF binary" % elf_filename)
+
+ # Search for CONFIG_XIP in the ELF's list of symbols using NM and AWK.
+ # GREP can not be used as it returns an error if the symbol is not found.
+ is_xip_command = "nm " + elf_filename + " | awk '/CONFIG_XIP/ { print $3 }'"
+ is_xip_output = subprocess.check_output(is_xip_command, shell=True)
+ self.is_xip = (len(is_xip_output) != 0)
+
+ self.elf_filename = elf_filename
+ self.sections = {}
+ self.xip_rom_size = 0
+ self.xip_ram_size = 0
+ self.ram_size = 0
+
+ self._calculate_sizes()
+
+ def get_ram_size(self):
+ """Get the amount of RAM the application will use up on the device
+
+ @return amount of RAM, in bytes
+ """
+ if self.is_xip:
+ return self.xip_ram_size
+ else:
+ return self.ram_size
+
+ def get_rom_size(self):
+ """Get the size of the data that this application uses on device's flash
+
+ @return amount of ROM, in bytes
+ """
+ return self.xip_rom_size
+
+ def unrecognized_sections(self):
+ """Get a list of sections inside the binary that weren't recognized
+
+ @return list of unrecogized section names
+ """
+ slist = []
+ for k, v in self.sections.iteritems():
+ if not v["recognized"]:
+ slist.append(k)
+ return slist
+
+ def _calculate_sizes(self):
+ """ Calculate RAM and ROM usage by section """
+ objdump_command = "objdump -h " + self.elf_filename
+ objdump_output = subprocess.check_output(objdump_command,
+ shell=True).splitlines()
+
+ for line in objdump_output:
+ words = line.split()
+
+ if (len(words) == 0): # Skip lines that are too short
+ continue
+
+ index = words[0]
+ if (not index[0].isdigit()): # Skip lines that do not start
+ continue # with a digit
+
+ name = words[1] # Skip lines with section names
+ if (name[0] == '.'): # starting with '.'
+ continue
+
+ size = int(words[2], 16)
+ phys_addr = int(words[4], 16)
+
+ # Add section to memory use totals (for both non-XIP and XIP scenarios)
+ #
+ # In an XIP image, the following sections are placed into ROM:
+ # text, ctors, rodata and datas
+ # In an XIP image, the following sections are placed into RAM:
+ # datas, bss and noinit
+ # In a non-XIP image, the following sections are placed into RAM
+ # text, ctors, rodata, datas, bss and noinit
+ # Unrecognized section names are not included in the calculations.
+
+ self.ram_size += size
+ recognized = True
+
+ if ((name == "text") or (name == "ctors") or (name == "rodata")):
+ self.xip_rom_size += size
+ elif (name == "datas"):
+ self.xip_rom_size += size
+ self.xip_ram_size += size
+ elif ((name == "bss") or (name == "noinit")):
+ self.xip_ram_size += size
+ else:
+ recognized = False
+ self.ram_size -= size # Undo the calculation
+
+ self.sections[name] = {"phys_addr" : phys_addr, "size" : size,
+ "recognized" : recognized}
+
+
+class MakeGoal:
+ """Metadata class representing one of the sub-makes called by MakeGenerator
+
+ MakeGenerator returns a dictionary of these which can then be associdated
+ with TestInstances to get a complete picture of what happened during a test.
+ MakeGenerator is used for tasks outside of building tests (such as
+ defconfigs) which is why MakeGoal is a separate class from TestInstance.
+ """
+ def __init__(self, name, text, qemu, make_log, build_log, run_log,
+ qemu_log):
+ self.name = name
+ self.text = text
+ self.qemu = qemu
+ self.make_log = make_log
+ self.build_log = build_log
+ self.run_log = run_log
+ self.qemu_log = qemu_log
+ self.make_state = "waiting"
+ self.failed = False
+ self.finished = False
+ self.reason = None
+ self.metrics = {}
+
+ def get_error_log(self):
+ if self.make_state == "waiting":
+ # Shouldn't ever see this; breakage in the main Makefile itself.
+ return self.make_log
+ elif self.make_state == "building":
+ # Failure when calling the sub-make to build the code
+ return self.build_log
+ elif self.make_state == "running":
+ # Failure in sub-make for "make qemu", qemu probably failed to start
+ return self.run_log
+ elif self.make_state == "finished":
+ # QEMU finished, but timed out or otherwise wasn't successful
+ return self.qemu_log
+
+ def fail(self, reason):
+ self.failed = True
+ self.finished = True
+ self.reason = reason
+
+ def success(self):
+ self.finished = True
+
+ def __str__(self):
+ if self.finished:
+ if self.failed:
+ return "[%s] failed (%s: see %s)" % (self.name, self.reason,
+ self.get_error_log())
+ else:
+ return "[%s] passed" % self.name
+ else:
+ return "[%s] in progress (%s)" % (self.name, self.make_state)
+
+
+class MakeGenerator:
+ """Generates a Makefile which just calls a bunch of sub-make sessions
+
+ In any given test suite we may need to build dozens if not hundreds of
+ test cases. The cleanest way to parallelize this is to just let Make
+ do the parallelization, sharing the jobserver among all the different
+ sub-make targets.
+ """
+
+ GOAL_HEADER_TMPL = """.PHONY: {goal}
+{goal}:
+"""
+
+ MAKE_RULE_TMPL = """\t@echo sanity_test_{phase} {goal} >&2
+\t$(MAKE) -C {directory} O={outdir} V={verb} {args} >{logfile} 2>&1
+"""
+
+ GOAL_FOOTER_TMPL = "\t@echo sanity_test_finished {goal} >&2\n\n"
+
+ re_make = re.compile("sanity_test_([A-Za-z0-9]+) (.+)|$|make[:] \*\*\* [[](.+)[]] Error.+$")
+
+ def __init__(self, base_outdir):
+ """MakeGenerator constructor
+
+ @param base_outdir Intended to be the base out directory. A make.log
+ file will be created here which contains the output of the
+ top-level Make session, as well as the dynamic control Makefile
+ @param verbose If true, pass V=1 to all the sub-makes which greatly
+ increases their verbosity
+ """
+ self.goals = {}
+ if not os.path.exists(base_outdir):
+ os.makedirs(base_outdir)
+ self.logfile = os.path.join(base_outdir, "make.log")
+ self.makefile = os.path.join(base_outdir, "Makefile")
+
+ def _get_rule_header(self, name):
+ return MakeGenerator.GOAL_HEADER_TMPL.format(goal=name)
+
+ def _get_sub_make(self, name, phase, workdir, outdir, logfile, args):
+ verb = "1" if VERBOSE else "0"
+ args = " ".join(args)
+ return MakeGenerator.MAKE_RULE_TMPL.format(phase=phase, goal=name,
+ outdir=outdir,
+ directory=workdir, verb=verb,
+ args=args, logfile=logfile)
+
+ def _get_rule_footer(self, name):
+ return MakeGenerator.GOAL_FOOTER_TMPL.format(goal=name)
+
+ def _add_goal(self, outdir):
+ if not os.path.exists(outdir):
+ os.makedirs(outdir)
+
+ def add_build_goal(self, name, directory, outdir, args):
+ """Add a goal to invoke a Kbuild session
+
+ @param name A unique string name for this build goal. The results
+ dictionary returned by execute() will be keyed by this name.
+ @param directory Absolute path to working directory, will be passed
+ to make -C
+ @param outdir Absolute path to output directory, will be passed to
+ Kbuild via -O=<path>
+ @param args Extra command line arguments to pass to 'make', typically
+ environment variables or specific Make goals
+ """
+ self._add_goal(outdir)
+ build_logfile = os.path.join(outdir, "build.log")
+ text = (self._get_rule_header(name) +
+ self._get_sub_make(name, "building", directory,
+ outdir, build_logfile, args) +
+ self._get_rule_footer(name))
+ self.goals[name] = MakeGoal(name, text, None, self.logfile, build_logfile,
+ None, None)
+
+ def add_qemu_goal(self, name, directory, outdir, args, timeout=30):
+ """Add a goal to build a Zephyr project and then run it under QEMU
+
+ The generated make goal invokes Make twice, the first time it will
+ build the default goal, and the second will invoke the 'qemu' goal.
+ The output of the QEMU session will be monitored, and terminated
+ either upon pass/fail result of the test program, or the timeout
+ is reached.
+
+ @param name A unique string name for this build goal. The results
+ dictionary returned by execute() will be keyed by this name.
+ @param directory Absolute path to working directory, will be passed
+ to make -C
+ @param outdir Absolute path to output directory, will be passed to
+ Kbuild via -O=<path>
+ @param args Extra command line arguments to pass to 'make', typically
+ environment variables. Do not pass specific Make goals here.
+ @param timeout Maximum length of time QEMU session should be allowed
+ to run before automatically killing it. Default is 30 seconds.
+ """
+
+ self._add_goal(outdir)
+ build_logfile = os.path.join(outdir, "build.log")
+ run_logfile = os.path.join(outdir, "run.log")
+ qemu_logfile = os.path.join(outdir, "qemu.log")
+
+ q = QEMUHandler(name, outdir, qemu_logfile, timeout)
+ args.append("QEMU_PIPE=%s" % q.get_fifo())
+ text = (self._get_rule_header(name) +
+ self._get_sub_make(name, "building", directory,
+ outdir, build_logfile, args) +
+ self._get_sub_make(name, "running", directory,
+ outdir, run_logfile,
+ args + ["qemu"]) +
+ self._get_rule_footer(name))
+ self.goals[name] = MakeGoal(name, text, q, self.logfile, build_logfile,
+ run_logfile, qemu_logfile)
+
+
+ def add_test_instance(self, ti, build_only=False):
+ """Add a goal to build/test a TestInstance object
+
+ @param ti TestInstance object to build. The status dictionary returned
+ by execute() will be keyed by its .name field.
+ """
+ args = ti.test.extra_args[:]
+ args.extend(["ARCH=%s" % ti.platform.arch.name,
+ "PLATFORM_CONFIG=%s" % ti.platform.name])
+ if ti.platform.qemu_support and not ti.build_only and not build_only:
+ self.add_qemu_goal(ti.name, ti.test.code_location, ti.outdir,
+ args, ti.test.timeout)
+ else:
+ self.add_build_goal(ti.name, ti.test.code_location, ti.outdir, args)
+
+ def execute(self, callback_fn=None, context=None):
+ """Execute all the registered build goals
+
+ @param callback_fn If not None, a callback function will be called
+ as individual goals transition between states. This function
+ should accept two parameters: a string state and an arbitrary
+ context object, supplied here
+ @param context Context object to pass to the callback function.
+ Type and semantics are specific to that callback function.
+ @return A dictionary mapping goal names to final status.
+ """
+
+ with open(self.makefile, "w") as tf, \
+ open(os.devnull, "wb") as devnull, \
+ open(self.logfile, "w") as make_log:
+ # Create our dynamic Makefile and execute it.
+ # Watch stderr output which is where we will keep
+ # track of build state
+ for name, goal in self.goals.iteritems():
+ tf.write(goal.text)
+ tf.write("all: %s\n" % (" ".join(self.goals.keys())))
+ tf.flush()
+
+ # os.environ["CC"] = "ccache gcc" FIXME doesn't work
+
+ cmd = ["make", "-k", "-j", str(PARALLEL), "-f", tf.name, "all"]
+ p = subprocess.Popen(cmd, stderr=subprocess.PIPE,
+ stdout=devnull)
+
+ for line in iter(p.stderr.readline, b''):
+ make_log.write(line)
+ verbose("MAKE: " + repr(line.strip()))
+ m = MakeGenerator.re_make.match(line)
+ if not m:
+ continue
+
+ state, name, error = m.groups()
+ if error:
+ goal = self.goals[error]
+ else:
+ goal = self.goals[name]
+ goal.make_state = state
+
+
+ if error:
+ goal.fail("build_error")
+ else:
+ if state == "finished":
+ if goal.qemu:
+ thread_status, metrics = goal.qemu.get_state()
+ goal.metrics.update(metrics)
+ if thread_status == "passed":
+ goal.success()
+ else:
+ goal.fail(thread_status)
+ else:
+ goal.success()
+
+ if callback_fn:
+ callback_fn(context, self.goals, goal)
+
+ p.wait()
+ return self.goals
+
+
+# "list" - List of strings
+# "list:<type>" - List of <type>
+# "set" - Set of unordered, unique strings
+# "set:<type>" - Set of <type>
+# "float" - Floating point
+# "int" - Integer
+# "bool" - Boolean
+# "str" - String
+
+# XXX Be sure to update __doc__ if you change any of this!!
+
+arch_valid_keys = {"name" : {"type" : "str", "required" : True},
+ "platforms" : {"type" : "list", "required" : True}}
+
+platform_valid_keys = {"qemu_support" : {"type" : "bool", "default" : False},
+ "microkernel_support" : {"type" : "bool",
+ "default" : True}}
+
+testcase_valid_keys = {"tags" : {"type" : "set", "required" : True},
+ "extra_args" : {"type" : "list"},
+ "build_only" : {"type" : "bool", "default" : False},
+ "timeout" : {"type" : "int", "default" : 60},
+ "arch_whitelist" : {"type" : "set"},
+ "platform_whitelist" : {"type" : "set"},
+ "config_whitelist" : {"type" : "set"}}
+
+
+class SanityConfigParser:
+ """Class to read architecture and test case .ini files with semantic checking
+ """
+ def __init__(self, filename):
+ """Instantiate a new SanityConfigParser object
+
+ @param filename Source .ini file to read
+ """
+ cp = ConfigParser.SafeConfigParser()
+ cp.readfp(open(filename))
+ self.filename = filename
+ self.cp = cp
+
+ def _cast_value(self, value, typestr):
+ v = value.strip()
+ if typestr == "str":
+ return v
+
+ elif typestr == "float":
+ return float(v)
+
+ elif typestr == "int":
+ return int(v)
+
+ elif typestr == "bool":
+ v = v.lower()
+ if v == "true" or v == "1":
+ return True
+ elif v == "" or v == "false" or v == "0":
+ return False
+ raise ConfigurationError(self.filename,
+ "bad value for boolean: '%s'" % value)
+
+ elif typestr.startswith("list"):
+ vs = v.split()
+ if len(typestr) > 4 and typestr[4] == ":":
+ return [self._cast_value(vsi, typestr[5:]) for vsi in vs]
+ else:
+ return vs
+
+ elif typestr.startswith("set"):
+ vs = v.split()
+ if len(typestr) > 3 and typestr[3] == ":":
+ return set([self._cast_value(vsi, typestr[4:]) for vsi in vs])
+ else:
+ return set(vs)
+
+ else:
+ raise ConfigurationError(self.filename, "unknown type '%s'" % value)
+
+
+ def sections(self):
+ """Get the set of sections within the .ini file
+
+ @return a list of string section names"""
+ return self.cp.sections()
+
+ def get_section(self, section, valid_keys):
+ """Get a dictionary representing the keys/values within a section
+
+ @param section The section in the .ini file to retrieve data from
+ @param valid_keys A dictionary representing the intended semantics
+ for this section. Each key in this dictionary is a key that could
+ be specified, if a key is given in the .ini file which isn't in
+ here, it will generate an error. Each value in this dictionary
+ is another dictionary containing metadata:
+
+ "default" - Default value if not given
+ "type" - Data type to convert the text value to. Simple types
+ supported are "str", "float", "int", "bool" which will get
+ converted to respective Python data types. "set" and "list"
+ may also be specified which will split the value by
+ whitespace (but keep the elements as strings). finally,
+ "list:<type>" and "set:<type>" may be given which will
+ perform a type conversion after splitting the value up.
+ "required" - If true, raise an error if not defined. If false
+ and "default" isn't specified, a type conversion will be
+ done on an empty string
+ @return A dictionary containing the section key-value pairs with
+ type conversion and default values filled in per valid_keys
+ """
+
+ d = {}
+ cp = self.cp
+
+ if not cp.has_section(section):
+ raise ConfigurationError(self.filename, "Missing section '%s'" % section)
+
+ for k, v in cp.items(section):
+ if k not in valid_keys:
+ raise ConfigurationError(self.filename,
+ "Unknown config key '%s' in defintiion for '%s'"
+ % (k, section))
+ d[k] = v
+
+ for k, kinfo in valid_keys.iteritems():
+ if k not in d:
+ if "required" in kinfo:
+ required = kinfo["required"]
+ else:
+ required = False
+
+ if required:
+ raise ConfigurationError(self.filename,
+ "missing required value for '%s' in section '%s'"
+ % (k, section))
+ else:
+ if "default" in kinfo:
+ default = kinfo["default"]
+ else:
+ default = self._cast_value("", kinfo["type"])
+ d[k] = default
+ else:
+ try:
+ d[k] = self._cast_value(d[k], kinfo["type"])
+ except ValueError, ve:
+ raise ConfigurationError(self.filename,
+ "bad %s value '%s' for key '%s' in section '%s'"
+ % (kinfo["type"], d[k], k, section))
+
+ return d
+
+
+class Platform:
+ """Class representing metadata for a particular platform
+
+ Maps directly to PLATFORM_CONFIG when building"""
+ def __init__(self, arch, name, plat_dict):
+ """Constructor.
+
+ @param arch Architecture object for this platform
+ @param name String name for this platform, same as PLATFORM_CONFIG
+ @param plat_dict SanityConfigParser output on the relevant section
+ in the architecture configuration file which has lots of metadata.
+ See the Architecture class.
+ """
+ self.name = name
+ self.qemu_support = plat_dict["qemu_support"]
+ self.microkernel_support = plat_dict["microkernel_support"]
+ self.arch = arch
+ # Gets populated in a separate step
+ self.defconfig = {"micro" : None, "nano" : None}
+ pass
+
+ def set_defconfig(self, ktype, defconfig):
+ """Set defconfig information for a particular kernel type.
+
+ We do this in another step because all the defconfigs are generated
+ at once from a sub-make, see TestSuite constructor
+
+ @param ktype Kernel type, either "micro" or "nano"
+ @param defconfig Dictionary containing defconfig information
+ """
+ self.defconfig[ktype] = defconfig
+
+ def get_defconfig(self, ktype):
+ """Return a dictionary representing the key/value pairs expressed
+ in the kernel defconfig used for this arch/platform. Used to identify
+ platform features.
+
+ @param ktype Kernel type, either "micro" or "nano"
+ @return dictionary corresponding to the defconfig contents. unset
+ values will not be defined
+ """
+
+ if ktype == "micro" and not self.microkernel_support:
+ raise SanityRuntimeError("Invalid kernel type queried")
+
+ return self.defconfig[ktype]
+
+ def __repr__(self):
+ return "<%s on %s>" % (self.name, self.arch.name)
+
+
+class Architecture:
+ """Class representing metadata for a particular architecture
+ """
+ def __init__(self, cfile):
+ """Architecture constructor
+
+ @param cfile Path to Architecture configuration file, which gives
+ info about the arch and all the platforms for it
+ """
+ cp = SanityConfigParser(cfile)
+ self.platforms = []
+
+ arch = cp.get_section("arch", arch_valid_keys)
+
+ self.name = arch["name"]
+
+ for plat_name in arch["platforms"]:
+ verbose("Platform: %s" % plat_name)
+ plat_dict = cp.get_section(plat_name, platform_valid_keys)
+ self.platforms.append(Platform(self, plat_name, plat_dict))
+
+ def __repr__(self):
+ return "<arch %s>" % self.name
+
+
+class TestCase:
+ """Class representing a test application
+ """
+ makefile_re = re.compile("\s*KERNEL_TYPE\s*[?=]+\s*(micro|nano)\s*")
+
+ def __init__(self, testcase_root, workdir, name, tc_dict):
+ """TestCase constructor.
+
+ This gets called by TestSuite as it finds and reads testcase.ini files.
+ Multiple TestCase instances may be generated from a single testcase.ini,
+ each one corresponds to a section within that file.
+
+ Reads the Makefile inside the testcase directory to figure out the
+ kernel type for purposes of configuration filtering
+
+ We need to have a unique name for every single test case. Since
+ a testcase.ini can define multiple tests, the canonical name for
+ the test case is <workdir>/<name>.
+
+ @param testcase_root Absolute path to the root directory where
+ all the test cases live
+ @param workdir Relative path to the project directory for this
+ test application from the test_case root.
+ @param name Name of this test case, corresponding to the section name
+ in the test case configuration file. For many test cases that just
+ define one test, can be anything and is usually "test". This is
+ really only used to distinguish between different cases when
+ the testcase.ini defines multiple tests
+ @param tc_dict Dictionary with section values for this test case
+ from the testcase.ini file
+ """
+ self.code_location = os.path.join(testcase_root, workdir)
+ self.tags = tc_dict["tags"]
+ self.extra_args = tc_dict["extra_args"]
+ self.arch_whitelist = tc_dict["arch_whitelist"]
+ self.platform_whitelist = tc_dict["platform_whitelist"]
+ self.config_whitelist = tc_dict["config_whitelist"]
+ self.timeout = tc_dict["timeout"]
+ self.build_only = tc_dict["build_only"]
+ self.path = os.path.join(workdir, name)
+ self.name = self.path # for now
+ self.ktype = None
+
+ with open(os.path.join(testcase_root, workdir, "Makefile")) as makefile:
+ for line in makefile.readlines():
+ m = TestCase.makefile_re.match(line)
+ if m:
+ self.ktype = m.group(1)
+ break
+ if not self.ktype:
+ raise ConfigurationError(os.path.join(workdir, "Makefile"),
+ "KERNEL_TYPE not found")
+
+ def __repr__(self):
+ return self.name
+
+
+
+class TestInstance:
+ """Class representing the execution of a particular TestCase on a platform
+
+ @param test The TestCase object we want to build/execute
+ @param platform Platform object that we want to build and run against
+ @param base_outdir Base directory for all test results. The actual
+ out directory used is <outdir>/<platform>/<test case name>
+ """
+ def __init__(self, test, platform, base_outdir, build_only=False):
+ self.test = test
+ self.platform = platform
+ self.name = os.path.join(platform.name, test.path)
+ self.outdir = os.path.join(base_outdir, platform.name, test.path)
+ self.build_only = build_only or test.build_only
+
+ def calculate_sizes(self):
+ """Get the RAM/ROM sizes of a test case.
+
+ This can only be run after the instance has been executed by
+ MakeGenerator, otherwise there won't be any binaries to measure.
+
+ @return A SizeCalculator object
+ """
+ if self.test.ktype == "micro":
+ binary_stem = os.path.join(self.outdir, "microkernel")
+ else:
+ binary_stem = os.path.join(self.outdir, "nanokernel")
+
+ return SizeCalculator(binary_stem)
+
+ def __repr__(self):
+ return "<TestCase %s on %s>" % (self.test.name, self.platform.name)
+
+
+
+class TestSuite:
+ config_re = re.compile('(CONFIG_[A-Z0-9_]+)[=](.+)$')
+
+ def __init__(self, arch_root, testcase_root, outdir):
+ # Keep track of which test cases we've filtered out and why
+ discards = {}
+ self.arches = {}
+ self.testcases = {}
+ self.platforms = []
+ self.outdir = outdir
+ self.instances = {}
+ self.goals = None
+ self.discards = None
+
+ arch_root = os.path.abspath(arch_root)
+ testcase_root = os.path.abspath(testcase_root)
+ outdir = os.path.abspath(outdir)
+
+ debug("Reading test case configuration files under %s..." % testcase_root)
+ for dirpath, dirnames, filenames in os.walk(testcase_root,
+ topdown=True):
+ if "testcase.ini" in filenames:
+ verbose("Found test case in " + dirpath)
+ dirnames[:] = []
+ cp = SanityConfigParser(os.path.join(dirpath, "testcase.ini"))
+ workdir = os.path.relpath(dirpath, testcase_root)
+
+ for section in cp.sections():
+ tc_dict = cp.get_section(section, testcase_valid_keys)
+ tc = TestCase(testcase_root, workdir, section, tc_dict)
+ self.testcases[tc.name] = tc
+
+ debug("Reading architecture configuration files under %s..." % arch_root)
+ for dirpath, dirnames, filenames in os.walk(arch_root):
+ for filename in filenames:
+ if filename.endswith(".ini"):
+ fn = os.path.join(dirpath, filename)
+ verbose("Found arch configuration " + fn)
+ arch = Architecture(fn)
+ self.arches[arch.name] = arch
+ self.platforms.extend(arch.platforms)
+
+
+ # Now that we know the full set of arches/platforms, get the defconfig
+ # information from them by calling Make
+ info("Building platform defconfigs...")
+ dlist = {}
+ config_outdir = os.path.join(outdir, "configs")
+ mg = MakeGenerator(config_outdir)
+
+ for plat in self.platforms:
+ ktypes = ["nano"]
+ if plat.microkernel_support:
+ ktypes.append("micro")
+
+ for ktype in ktypes:
+ stem = ktype + "_" + plat.name
+
+ in_defconfig = stem + "_defconfig"
+ out_config = os.path.join(config_outdir, stem + "_config")
+ dlist[plat, ktype] = out_config
+
+ args = ["ARCH=" + plat.arch.name,
+ "KBUILD_DEFCONFIG=" + in_defconfig,
+ "KCONFIG_CONFIG=" + out_config, "defconfig"]
+ # FIXME would be nice to use a common outdir for this so that
+ # conf, gen_idt, etc aren't rebuilt for every plat/ktype combo,
+ # need a way to avoid different Make processe from clobbering
+ # each other since they all try to build them simultaneously
+ mg.add_build_goal(stem, ZEPHYR_BASE, os.path.join(config_outdir,
+ plat.name,
+ ktype), args)
+
+ results = mg.execute(None)
+
+ for k, out_config in dlist.iteritems():
+ plat, ktype = k
+ defconfig = {}
+ with open(out_config, "r") as fp:
+ for line in fp.readlines():
+ m = TestSuite.config_re.match(line)
+ if not m:
+ continue
+ defconfig[m.group(1)] = m.group(2).strip()
+ plat.set_defconfig(ktype, defconfig)
+
+ self.instances = {}
+
+ def get_last_failed(self):
+ if not os.path.exists(LAST_SANITY):
+ return []
+ result = []
+ with open(LAST_SANITY, "r") as fp:
+ cr = csv.DictReader(fp)
+ for row in cr:
+ if row["passed"] == "True":
+ continue
+ test = row["test"]
+ platform = row["platform"]
+ result.append((test, platform))
+ return result
+
+ def apply_filters(self, platform_filter, arch_filter, tag_filter,
+ config_filter, testcase_filter, last_failed):
+ instances = []
+ discards = {}
+ verbose("platform filter: " + str(platform_filter))
+ verbose(" arch_filter: " + str(arch_filter))
+ verbose(" tag_filter: " + str(tag_filter))
+ verbose(" config_filter: " + str(config_filter))
+
+ if last_failed:
+ failed_tests = self.get_last_failed()
+
+ if not platform_filter or "default" in platform_filter:
+ info("Selecting default platforms per test case")
+ default_platforms = True
+ platform_filter = []
+ else:
+ default_platforms = False
+
+ if "all" in platform_filter:
+ info("Selecting all possible platforms per test case")
+ platform_filter = []
+
+ for tc_name, tc in self.testcases.iteritems():
+ for arch_name, arch in self.arches.iteritems():
+ instance_list = []
+ for plat in arch.platforms:
+ instance = TestInstance(tc, plat, self.outdir)
+
+ if tag_filter and not tc.tags.intersection(tag_filter):
+ discards[instance] = "Command line testcase tag filter"
+ continue
+
+ if testcase_filter and tc_name not in testcase_filter:
+ discards[instance] = "Testcase name filter"
+ continue
+
+ if last_failed and (tc.name, plat.name) not in failed_tests:
+ discards[instance] = "Passed or skipped during last run"
+ continue
+
+ if arch_filter and arch_name not in arch_filter:
+ discards[instance] = "Command line testcase arch filter"
+ continue
+
+ if tc.arch_whitelist and arch.name not in tc.arch_whitelist:
+ discards[instance] = "Not in test case arch whitelist"
+ continue
+
+ if platform_filter and plat.name not in platform_filter:
+ discards[instance] = "Command line platform filter"
+ continue
+
+ if tc.platform_whitelist and plat.name not in tc.platform_whitelist:
+ discards[instance] = "Not in testcase platform whitelist"
+ continue
+
+ if not plat.microkernel_support and tc.ktype == "micro":
+ discards[instance] = "No microkernel support for platform"
+ continue
+
+ defconfig = plat.get_defconfig(tc.ktype)
+ config_pass = True
+ # FIXME this is kind of gross clean it up
+ for cw in tc.config_whitelist:
+ invert = (cw[0] == "!")
+ if invert:
+ cw = cw[1:]
+
+ if "=" in cw:
+ k, v = cw.split("=")
+ testval = k not in defconfig or defconfig[k] != v
+ if invert:
+ testval = not testval
+ if testval:
+ discards[instance] = "%s%s in platform defconfig" % (
+ cw, " not" if not invert else "")
+ config_pass = False
+ break
+ else:
+ testval = cw not in defconfig
+ if invert:
+ testval = not testval
+ if testval:
+ discards[instance] = "%s%s set in platform defconfig" % (
+ cw, " not" if not invert else "")
+ config_pass = False
+ break
+
+ if not config_pass:
+ continue
+
+ instance_list.append(instance)
+
+ if not instance_list:
+ # Every platform in this arch was rejected already
+ continue
+
+ if default_platforms:
+ self.add_instance(instance_list[0])
+ for instance in instance_list[1:]:
+ discards[instance] = "Not in default set for arch"
+ else:
+ for instance in instance_list:
+ self.add_instance(instance)
+ self.discards = discards
+ return discards
+
+ def add_instance(self, ti):
+ self.instances[ti.name] = ti
+
+ def execute(self, cb, cb_context, build_only):
+ mg = MakeGenerator(self.outdir)
+ for i in self.instances.values():
+ mg.add_test_instance(i, build_only)
+ self.goals = mg.execute(cb, cb_context)
+ for name, goal in self.goals.iteritems():
+ i = self.instances[name]
+ if goal.failed:
+ continue
+ sc = i.calculate_sizes()
+ goal.metrics["ram_size"] = sc.get_ram_size()
+ goal.metrics["rom_size"] = sc.get_rom_size()
+ return self.goals
+
+ def discard_report(self, filename):
+ if self.discards == None:
+ raise SanityRuntimeException("apply_filters() hasn't been run!")
+
+ with open(filename, "wb") as csvfile:
+ fieldnames = ["test", "arch", "platform", "reason"]
+ cw = csv.DictWriter(csvfile, fieldnames, lineterminator=os.linesep)
+ cw.writeheader()
+ for instance, reason in self.discards.iteritems():
+ rowdict = {"test" : i.test.name,
+ "arch" : i.platform.arch.name,
+ "platform" : i.platform.name,
+ "reason" : reason}
+ cw.writerow(rowdict)
+
+ def compare_metrics(self, filename):
+ # name, datatype, lower results better
+ interesting_metrics = [("ram_size", int, True),
+ ("rom_size", int, True)]
+
+ if self.goals == None:
+ raise SanityRuntimeException("execute() hasn't been run!")
+
+ if not os.path.exists(filename):
+ info("Cannot compare metrics, %s not found" % filename)
+ return []
+
+ results = []
+ saved_metrics = {}
+ with open(filename) as fp:
+ cr = csv.DictReader(fp)
+ for row in cr:
+ d = {}
+ for m, _, _ in interesting_metrics:
+ d[m] = row[m]
+ saved_metrics[(row["test"], row["platform"])] = d
+
+ for name, goal in self.goals.iteritems():
+ i = self.instances[name]
+ mkey = (i.test.name, i.platform.name)
+ if mkey not in saved_metrics:
+ continue
+ sm = saved_metrics[mkey]
+ for metric, mtype, lower_better in interesting_metrics:
+ if metric not in goal.metrics:
+ continue
+ if sm[metric] == "":
+ continue
+ delta = goal.metrics[metric] - mtype(sm[metric])
+ if ((lower_better and delta > 0) or
+ (not lower_better and delta < 0)):
+ results.append((i, metric, goal.metrics[metric], delta))
+ return results
+
+ def testcase_report(self, filename):
+ if self.goals == None:
+ raise SanityRuntimeException("execute() hasn't been run!")
+
+ with open(filename, "wb") as csvfile:
+ fieldnames = ["test", "arch", "platform", "passed", "status",
+ "extra_args", "qemu", "qemu_time", "ram_size",
+ "rom_size"]
+ cw = csv.DictWriter(csvfile, fieldnames, lineterminator=os.linesep)
+ cw.writeheader()
+ for name, goal in self.goals.iteritems():
+ i = self.instances[name]
+ rowdict = {"test" : i.test.name,
+ "arch" : i.platform.arch.name,
+ "platform" : i.platform.name,
+ "extra_args" : " ".join(i.test.extra_args),
+ "qemu" : i.platform.qemu_support}
+ if goal.failed:
+ rowdict["passed"] = False
+ rowdict["status"] = goal.reason
+ else:
+ rowdict["passed"] = True
+ if goal.qemu:
+ rowdict["qemu_time"] = goal.metrics["qemu_time"]
+ rowdict["ram_size"] = goal.metrics["ram_size"]
+ rowdict["rom_size"] = goal.metrics["rom_size"]
+ cw.writerow(rowdict)
+
+
+def parse_arguments():
+
+ parser = argparse.ArgumentParser(description = __doc__,
+ formatter_class = argparse.RawDescriptionHelpFormatter)
+
+ parser.add_argument("-p", "--platform", action="append",
+ help="Platform filter for testing. If unspecified, default to the "
+ "set of default platforms in the arch configuration files for "
+ "the selected arches. May also specify 'all' to match all "
+ "platforms for the selected arches. Multiple invocations "
+ "are treated as a logical 'or' relationship")
+ parser.add_argument("-a", "--arch", action="append",
+ help="Arch filter for testing. Takes precedence over --platform. "
+ "If unspecified, test all arches. Multiple invocations "
+ "are treated as a logical 'or' relationship")
+ parser.add_argument("-t", "--tag", action="append",
+ help="Specify tags to restrict which tests to run by tag value. "
+ "Default is to not do any tag filtering. Multiple invocations "
+ "are treated as a logical 'or' relationship")
+ parser.add_argument("-f", "--only-failed", action="store_true",
+ help="Run only those tests that failed the previous sanity check "
+ "invocation.")
+ parser.add_argument("-c", "--config", action="append",
+ help="Specify platform configuration values filtering. This can be "
+ "specified two ways: <config>=<value> or just <config>. The "
+ "defconfig for all platforms, for all kernel types will be "
+ "checked. For the <config>=<value> case, only match defconfig "
+ "that have that value defined. For the <config> case, match "
+ "defconfig that have that value assigned to any value. "
+ "Prepend a '!' to invert the match.")
+ parser.add_argument("-s", "--test", action="append",
+ help="Run only the specified test cases. These are named by "
+ "<path to test project relative to "
+ "--testcase-root>/<testcase.ini section name>")
+ parser.add_argument("-l", "--all", action="store_true",
+ help="Same as --platform all")
+
+ parser.add_argument("-o", "--testcase-report",
+ help="Output a CSV spreadsheet containing results of the test run")
+ parser.add_argument("-d", "--discard-report",
+ help="Output a CSV spreadhseet showing tests that were skipped "
+ "and why")
+ parser.add_argument("-y", "--dry-run", action="store_true",
+ help="Create the filtered list of test cases, but don't actually "
+ "run them. Useful if you're just interested in "
+ "--discard-report")
+
+ parser.add_argument("-r", "--release", action="store_true",
+ help="Update the benchmark database with the results of this test "
+ "run. Intended to be run by CI when tagging an official "
+ "release. This database is used as a basis for comparison "
+ "when looking for deltas in metrics such as footprint")
+ parser.add_argument("-w", "--warnings-as-errors", action="store_true",
+ help="Treat warning conditions as errors")
+ parser.add_argument("-v", "--verbose", action="count", default=0,
+ help="Emit debugging information, call multiple times to increase "
+ "verbosity")
+ parser.add_argument("-i", "--inline-logs", action="store_true",
+ help="Upon test failure, print relevant log data to stdout "
+ "instead of just a path to it")
+ parser.add_argument("-m", "--last-metrics", action="store_true",
+ help="Instead of comparing metrics from the last --release, "
+ "compare with the results of the previous sanity check "
+ "invocation")
+ parser.add_argument("-u", "--no-update", action="store_true",
+ help="do not update the results of the last run of the sanity "
+ "checks")
+ parser.add_argument("-b", "--build-only", action="store_true",
+ help="Only build the code, do not execute any of it in QEMU")
+ parser.add_argument("-j", "--jobs", type=int,
+ help="Number of cores to use when building, defaults to "
+ "number of CPUs * 2")
+ parser.add_argument("-H", "--footprint-threshold", type=float, default=5,
+ help="When checking test case footprint sizes, warn the user if "
+ "the new app size is greater then the specified percentage "
+ "from the last release. Default is 5. 0 to warn on any "
+ "increase on app size")
+
+ parser.add_argument("-O", "--outdir",
+ default="%s/sanity-out" % ZEPHYR_BASE,
+ help="Output directory for logs and binaries.")
+ parser.add_argument("-C", "--clean", action="store_true",
+ help="Delete the outdir before building")
+ parser.add_argument("-T", "--testcase-root",
+ default="%s/samples" % ZEPHYR_BASE,
+ help="Base directory to recursively search for test cases. All "
+ "testcase.ini files under here will be processed")
+ parser.add_argument("-A", "--arch-root",
+ default="%s/scripts/sanity_chk/arches" % ZEPHYR_BASE,
+ help="Directory to search for arch configuration files. All .ini "
+ "files in the directory will be processed.")
+
+ return parser.parse_args()
+
+def log_info(filename):
+ filename = os.path.relpath(filename)
+ if INLINE_LOGS:
+ print "{:-^100}".format(filename)
+ with open(filename) as fp:
+ sys.stdout.write(fp.read())
+ print "{:-^100}".format(filename)
+ else:
+ print "\tsee: " + COLOR_YELLOW + filename + COLOR_NORMAL
+
+def terse_test_cb(instances, goals, goal):
+ total_tests = len(goals)
+ total_done = 0
+ total_failed = 0
+
+ for k, g in goals.iteritems():
+ if g.finished:
+ total_done += 1
+ if g.failed:
+ total_failed += 1
+
+ if goal.failed:
+ i = instances[goal.name]
+ info("\n\n{:<25} {:<50} {}FAILED{}: {}".format(i.platform.name,
+ i.test.name, COLOR_RED, COLOR_NORMAL, goal.reason))
+ log_info(goal.get_error_log())
+ info("")
+
+ sys.stdout.write("\rtotal complete: %s%3d/%3d%s failed: %s%3d%s" % (
+ COLOR_GREEN, total_done, total_tests, COLOR_NORMAL,
+ COLOR_RED if total_failed > 0 else COLOR_NORMAL,
+ total_failed, COLOR_NORMAL))
+ sys.stdout.flush()
+
+def chatty_test_cb(instances, goals, goal):
+ i = instances[goal.name]
+
+ if VERBOSE < 2 and not goal.finished:
+ return
+
+ if goal.failed:
+ status = COLOR_RED + "FAILED" + COLOR_NORMAL + ": " + goal.reason
+ elif goal.finished:
+ status = COLOR_GREEN + "PASSED" + COLOR_NORMAL
+ else:
+ status = goal.make_state
+
+ info("{:<25} {:<50} {}".format(i.platform.name, i.test.name, status))
+ if goal.failed:
+ log_info(goal.get_error_log())
+
+def main():
+ global VERBOSE, INLINE_LOGS, PARALLEL
+ args = parse_arguments()
+ VERBOSE += args.verbose
+ INLINE_LOGS = args.inline_logs
+ if args.jobs:
+ PARALLEL = args.jobs
+ if args.all:
+ args.platform = ["all"]
+
+ if os.path.exists(args.outdir) and args.clean:
+ info("Cleaning output directory " + args.outdir)
+ shutil.rmtree(args.outdir)
+
+ ts = TestSuite(args.arch_root, args.testcase_root, args.outdir)
+ discards = ts.apply_filters(args.platform, args.arch, args.tag, args.config,
+ args.test, args.only_failed)
+
+ if args.discard_report:
+ ts.discard_report(args.discard_report)
+
+ if VERBOSE:
+ for i, reason in discards.iteritems():
+ debug("{:<25} {:<50} {}SKIPPED{}: {}".format(i.platform.name,
+ i.test.name, COLOR_YELLOW, COLOR_NORMAL, reason))
+
+ info("%d tests selected, %d tests discarded due to filters" %
+ (len(ts.instances), len(discards)))
+
+ if args.dry_run:
+ return
+
+ if VERBOSE or not TERMINAL:
+ goals = ts.execute(chatty_test_cb, ts.instances, args.build_only)
+ else:
+ goals = ts.execute(terse_test_cb, ts.instances, args.build_only)
+ print
+
+ deltas = ts.compare_metrics(LAST_SANITY if args.last_metrics
+ else RELEASE_DATA)
+ warnings = 0
+ if deltas:
+ for i, metric, value, delta in deltas:
+ percentage = (float(delta) / float(value - delta))
+ if percentage < (args.footprint_threshold / 100.0):
+ continue
+
+ info("{:<25} {:<50} {}WARNING{}: {} is now {} {:+.2%}".format(
+ i.platform.name, i.test.name, COLOR_YELLOW, COLOR_NORMAL,
+ metric, value, percentage))
+ warnings += 1
+
+ if warnings:
+ info("Deltas based on metrics from last %s" %
+ ("release" if not args.last_metrics else "run"))
+
+ failed = 0
+ for name, goal in goals.iteritems():
+ if goal.failed:
+ failed += 1
+
+ info("%s%d of %d%s tests passed with %s%d%s warnings" %
+ (COLOR_RED if failed else COLOR_GREEN, len(goals) - failed,
+ len(goals), COLOR_NORMAL, COLOR_YELLOW if warnings else COLOR_NORMAL,
+ warnings, COLOR_NORMAL))
+
+ if args.testcase_report:
+ ts.testcase_report(args.testcase_report)
+ if not args.no_update:
+ ts.testcase_report(LAST_SANITY)
+ if args.release:
+ ts.testcase_report(RELEASE_DATA)
+
+ if failed or (warnings and args.warnings_as_errors):
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
+