| #!/usr/bin/env python |
| |
| """ |
| This script determines the given package's openbmc dependencies from its |
| configure.ac file where it downloads, configures, builds, and installs each of |
| these dependencies. Then the given package is configured, built, and installed |
| prior to executing its unit tests. |
| """ |
| |
| from git import Repo |
| from urlparse import urljoin |
| from subprocess import check_call, call, CalledProcessError |
| import os |
| import sys |
| import argparse |
| import multiprocessing |
| import re |
| import sets |
| import subprocess |
| import shutil |
| import platform |
| |
| |
| class DepTree(): |
| """ |
| Represents package dependency tree, where each node is a DepTree with a |
| name and DepTree children. |
| """ |
| |
| def __init__(self, name): |
| """ |
| Create new DepTree. |
| |
| Parameter descriptions: |
| name Name of new tree node. |
| """ |
| self.name = name |
| self.children = list() |
| |
| def AddChild(self, name): |
| """ |
| Add new child node to current node. |
| |
| Parameter descriptions: |
| name Name of new child |
| """ |
| new_child = DepTree(name) |
| self.children.append(new_child) |
| return new_child |
| |
| def AddChildNode(self, node): |
| """ |
| Add existing child node to current node. |
| |
| Parameter descriptions: |
| node Tree node to add |
| """ |
| self.children.append(node) |
| |
| def RemoveChild(self, name): |
| """ |
| Remove child node. |
| |
| Parameter descriptions: |
| name Name of child to remove |
| """ |
| for child in self.children: |
| if child.name == name: |
| self.children.remove(child) |
| return |
| |
| def GetNode(self, name): |
| """ |
| Return node with matching name. Return None if not found. |
| |
| Parameter descriptions: |
| name Name of node to return |
| """ |
| if self.name == name: |
| return self |
| for child in self.children: |
| node = child.GetNode(name) |
| if node: |
| return node |
| return None |
| |
| def GetParentNode(self, name, parent_node=None): |
| """ |
| Return parent of node with matching name. Return none if not found. |
| |
| Parameter descriptions: |
| name Name of node to get parent of |
| parent_node Parent of current node |
| """ |
| if self.name == name: |
| return parent_node |
| for child in self.children: |
| found_node = child.GetParentNode(name, self) |
| if found_node: |
| return found_node |
| return None |
| |
| def GetPath(self, name, path=None): |
| """ |
| Return list of node names from head to matching name. |
| Return None if not found. |
| |
| Parameter descriptions: |
| name Name of node |
| path List of node names from head to current node |
| """ |
| if not path: |
| path = [] |
| if self.name == name: |
| path.append(self.name) |
| return path |
| for child in self.children: |
| match = child.GetPath(name, path + [self.name]) |
| if match: |
| return match |
| return None |
| |
| def GetPathRegex(self, name, regex_str, path=None): |
| """ |
| Return list of node paths that end in name, or match regex_str. |
| Return empty list if not found. |
| |
| Parameter descriptions: |
| name Name of node to search for |
| regex_str Regex string to match node names |
| path Path of node names from head to current node |
| """ |
| new_paths = [] |
| if not path: |
| path = [] |
| match = re.match(regex_str, self.name) |
| if (self.name == name) or (match): |
| new_paths.append(path + [self.name]) |
| for child in self.children: |
| return_paths = None |
| full_path = path + [self.name] |
| return_paths = child.GetPathRegex(name, regex_str, full_path) |
| for i in return_paths: |
| new_paths.append(i) |
| return new_paths |
| |
| def MoveNode(self, from_name, to_name): |
| """ |
| Mode existing from_name node to become child of to_name node. |
| |
| Parameter descriptions: |
| from_name Name of node to make a child of to_name |
| to_name Name of node to make parent of from_name |
| """ |
| parent_from_node = self.GetParentNode(from_name) |
| from_node = self.GetNode(from_name) |
| parent_from_node.RemoveChild(from_name) |
| to_node = self.GetNode(to_name) |
| to_node.AddChildNode(from_node) |
| |
| def ReorderDeps(self, name, regex_str): |
| """ |
| Reorder dependency tree. If tree contains nodes with names that |
| match 'name' and 'regex_str', move 'regex_str' nodes that are |
| to the right of 'name' node, so that they become children of the |
| 'name' node. |
| |
| Parameter descriptions: |
| name Name of node to look for |
| regex_str Regex string to match names to |
| """ |
| name_path = self.GetPath(name) |
| if not name_path: |
| return |
| paths = self.GetPathRegex(name, regex_str) |
| is_name_in_paths = False |
| name_index = 0 |
| for i in range(len(paths)): |
| path = paths[i] |
| if path[-1] == name: |
| is_name_in_paths = True |
| name_index = i |
| break |
| if not is_name_in_paths: |
| return |
| for i in range(name_index + 1, len(paths)): |
| path = paths[i] |
| if name in path: |
| continue |
| from_name = path[-1] |
| self.MoveNode(from_name, name) |
| |
| def GetInstallList(self): |
| """ |
| Return post-order list of node names. |
| |
| Parameter descriptions: |
| """ |
| install_list = [] |
| for child in self.children: |
| child_install_list = child.GetInstallList() |
| install_list.extend(child_install_list) |
| install_list.append(self.name) |
| return install_list |
| |
| def PrintTree(self, level=0): |
| """ |
| Print pre-order node names with indentation denoting node depth level. |
| |
| Parameter descriptions: |
| level Current depth level |
| """ |
| INDENT_PER_LEVEL = 4 |
| print ' ' * (level * INDENT_PER_LEVEL) + self.name |
| for child in self.children: |
| child.PrintTree(level + 1) |
| |
| |
| def check_call_cmd(*cmd): |
| """ |
| Verbose prints the directory location the given command is called from and |
| the command, then executes the command using check_call. |
| |
| Parameter descriptions: |
| dir Directory location command is to be called from |
| cmd List of parameters constructing the complete command |
| """ |
| printline(os.getcwd(), ">", " ".join(cmd)) |
| check_call(cmd) |
| |
| |
| def clone_pkg(pkg, branch): |
| """ |
| Clone the given openbmc package's git repository from gerrit into |
| the WORKSPACE location |
| |
| Parameter descriptions: |
| pkg Name of the package to clone |
| branch Branch to clone from pkg |
| """ |
| pkg_dir = os.path.join(WORKSPACE, pkg) |
| if os.path.exists(os.path.join(pkg_dir, '.git')): |
| return pkg_dir |
| pkg_repo = urljoin('https://gerrit.openbmc-project.xyz/openbmc/', pkg) |
| os.mkdir(pkg_dir) |
| printline(pkg_dir, "> git clone", pkg_repo, branch, "./") |
| try: |
| # first try the branch |
| repo_inst = Repo.clone_from(pkg_repo, pkg_dir, |
| branch=branch).working_dir |
| except: |
| printline("Input branch not found, default to master") |
| repo_inst = Repo.clone_from(pkg_repo, pkg_dir, |
| branch="master").working_dir |
| return repo_inst |
| |
| |
| def get_autoconf_deps(pkgdir): |
| """ |
| Parse the given 'configure.ac' file for package dependencies and return |
| a list of the dependencies found. If the package is not autoconf it is just |
| ignored. |
| |
| Parameter descriptions: |
| pkgdir Directory where package source is located |
| """ |
| configure_ac = os.path.join(pkgdir, 'configure.ac') |
| if not os.path.exists(configure_ac): |
| return [] |
| |
| configure_ac_contents = '' |
| # Prepend some special function overrides so we can parse out dependencies |
| for macro in DEPENDENCIES.iterkeys(): |
| configure_ac_contents += ('m4_define([' + macro + '], [' + |
| macro + '_START$' + str(DEPENDENCIES_OFFSET[macro] + 1) + |
| macro + '_END])\n') |
| with open(configure_ac, "rt") as f: |
| configure_ac_contents += f.read() |
| |
| autoconf_process = subprocess.Popen(['autoconf', '-Wno-undefined', '-'], |
| stdin=subprocess.PIPE, stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE) |
| (stdout, stderr) = autoconf_process.communicate(input=configure_ac_contents) |
| if not stdout: |
| print(stderr) |
| raise Exception("Failed to run autoconf for parsing dependencies") |
| |
| # Parse out all of the dependency text |
| matches = [] |
| for macro in DEPENDENCIES.iterkeys(): |
| pattern = '(' + macro + ')_START(.*?)' + macro + '_END' |
| for match in re.compile(pattern).finditer(stdout): |
| matches.append((match.group(1), match.group(2))) |
| |
| # Look up dependencies from the text |
| found_deps = [] |
| for macro, deptext in matches: |
| for potential_dep in deptext.split(' '): |
| for known_dep in DEPENDENCIES[macro].iterkeys(): |
| if potential_dep.startswith(known_dep): |
| found_deps.append(DEPENDENCIES[macro][known_dep]) |
| |
| return found_deps |
| |
| def get_meson_deps(pkgdir): |
| """ |
| Parse the given 'meson.build' file for package dependencies and return |
| a list of the dependencies found. If the package is not meson compatible |
| it is just ignored. |
| |
| Parameter descriptions: |
| pkgdir Directory where package source is located |
| """ |
| meson_build = os.path.join(pkgdir, 'meson.build') |
| if not os.path.exists(meson_build): |
| return [] |
| |
| found_deps = [] |
| for root, dirs, files in os.walk(pkgdir): |
| if 'meson.build' not in files: |
| continue |
| with open(os.path.join(root, 'meson.build'), 'rt') as f: |
| build_contents = f.read() |
| for match in re.finditer(r"dependency\('([^']*)'.*?\)\n", build_contents): |
| maybe_dep = DEPENDENCIES['PKG_CHECK_MODULES'].get(match.group(1)) |
| if maybe_dep is not None: |
| found_deps.append(maybe_dep) |
| |
| return found_deps |
| |
| make_parallel = [ |
| 'make', |
| # Run enough jobs to saturate all the cpus |
| '-j', str(multiprocessing.cpu_count()), |
| # Don't start more jobs if the load avg is too high |
| '-l', str(multiprocessing.cpu_count()), |
| # Synchronize the output so logs aren't intermixed in stdout / stderr |
| '-O', |
| ] |
| |
| def enFlag(flag, enabled): |
| """ |
| Returns an configure flag as a string |
| |
| Parameters: |
| flag The name of the flag |
| enabled Whether the flag is enabled or disabled |
| """ |
| return '--' + ('enable' if enabled else 'disable') + '-' + flag |
| |
| def mesonFeature(val): |
| """ |
| Returns the meson flag which signifies the value |
| |
| True is enabled which requires the feature. |
| False is disabled which disables the feature. |
| None is auto which autodetects the feature. |
| |
| Parameters: |
| val The value being converted |
| """ |
| if val is True: |
| return "enabled" |
| elif val is False: |
| return "disabled" |
| elif val is None: |
| return "auto" |
| else: |
| raise Exception("Bad meson feature value") |
| |
| def parse_meson_options(options_file): |
| """ |
| Returns a set of options defined in the provides meson_options.txt file |
| |
| Parameters: |
| options_file The file containing options |
| """ |
| options_contents = '' |
| with open(options_file, "rt") as f: |
| options_contents += f.read() |
| options = sets.Set() |
| pattern = 'option\\(\\s*\'([^\']*)\'' |
| for match in re.compile(pattern).finditer(options_contents): |
| options.add(match.group(1)) |
| return options |
| |
| def build_and_install(name, build_for_testing=False): |
| """ |
| Builds and installs the package in the environment. Optionally |
| builds the examples and test cases for package. |
| |
| Parameter description: |
| name The name of the package we are building |
| build_for_testing Enable options related to testing on the package? |
| """ |
| os.chdir(os.path.join(WORKSPACE, name)) |
| |
| # Refresh dynamic linker run time bindings for dependencies |
| check_call_cmd('sudo', '-n', '--', 'ldconfig') |
| |
| # Build & install this package |
| # Always try using meson first |
| if os.path.exists('meson.build'): |
| meson_options = sets.Set() |
| if os.path.exists("meson_options.txt"): |
| meson_options = parse_meson_options("meson_options.txt") |
| meson_flags = [ |
| '-Db_colorout=never', |
| '-Dwerror=true', |
| '-Dwarning_level=3', |
| ] |
| if build_for_testing: |
| meson_flags.append('--buildtype=debug') |
| else: |
| meson_flags.append('--buildtype=debugoptimized') |
| if 'tests' in meson_options: |
| meson_flags.append('-Dtests=' + mesonFeature(build_for_testing)) |
| if 'examples' in meson_options: |
| meson_flags.append('-Dexamples=' + str(build_for_testing).lower()) |
| if MESON_FLAGS.get(name) is not None: |
| meson_flags.extend(MESON_FLAGS.get(name)) |
| try: |
| check_call_cmd('meson', 'setup', '--reconfigure', 'build', *meson_flags) |
| except: |
| shutil.rmtree('build') |
| check_call_cmd('meson', 'setup', 'build', *meson_flags) |
| check_call_cmd('ninja', '-C', 'build') |
| check_call_cmd('sudo', '-n', '--', 'ninja', '-C', 'build', 'install') |
| # Assume we are autoconf otherwise |
| else: |
| conf_flags = [ |
| enFlag('silent-rules', False), |
| enFlag('examples', build_for_testing), |
| enFlag('tests', build_for_testing), |
| ] |
| if not TEST_ONLY: |
| conf_flags.extend([ |
| enFlag('code-coverage', build_for_testing), |
| enFlag('valgrind', build_for_testing), |
| ]) |
| # Add any necessary configure flags for package |
| if CONFIGURE_FLAGS.get(name) is not None: |
| conf_flags.extend(CONFIGURE_FLAGS.get(name)) |
| for bootstrap in ['bootstrap.sh', 'bootstrap', 'autogen.sh']: |
| if os.path.exists(bootstrap): |
| check_call_cmd('./' + bootstrap) |
| break |
| check_call_cmd('./configure', *conf_flags) |
| check_call_cmd(*make_parallel) |
| check_call_cmd('sudo', '-n', '--', *(make_parallel + [ 'install' ])) |
| |
| def build_dep_tree(pkg, pkgdir, dep_added, head, branch, dep_tree=None): |
| """ |
| For each package(pkg), starting with the package to be unit tested, |
| parse its 'configure.ac' file from within the package's directory(pkgdir) |
| for each package dependency defined recursively doing the same thing |
| on each package found as a dependency. |
| |
| Parameter descriptions: |
| pkg Name of the package |
| pkgdir Directory where package source is located |
| dep_added Current dict of dependencies and added status |
| head Head node of the dependency tree |
| branch Branch to clone from pkg |
| dep_tree Current dependency tree node |
| """ |
| if not dep_tree: |
| dep_tree = head |
| |
| with open("/tmp/depcache", "r") as depcache: |
| cache = depcache.readline() |
| |
| # Read out pkg dependencies |
| pkg_deps = [] |
| pkg_deps += get_autoconf_deps(pkgdir) |
| pkg_deps += get_meson_deps(pkgdir) |
| |
| for dep in sets.Set(pkg_deps): |
| if dep in cache: |
| continue |
| # Dependency package not already known |
| if dep_added.get(dep) is None: |
| # Dependency package not added |
| new_child = dep_tree.AddChild(dep) |
| dep_added[dep] = False |
| dep_pkgdir = clone_pkg(dep,branch) |
| # Determine this dependency package's |
| # dependencies and add them before |
| # returning to add this package |
| dep_added = build_dep_tree(dep, |
| dep_pkgdir, |
| dep_added, |
| head, |
| branch, |
| new_child) |
| else: |
| # Dependency package known and added |
| if dep_added[dep]: |
| continue |
| else: |
| # Cyclic dependency failure |
| raise Exception("Cyclic dependencies found in "+pkg) |
| |
| if not dep_added[pkg]: |
| dep_added[pkg] = True |
| |
| return dep_added |
| |
| def make_target_exists(target): |
| """ |
| Runs a check against the makefile in the current directory to determine |
| if the target exists so that it can be built. |
| |
| Parameter descriptions: |
| target The make target we are checking |
| """ |
| try: |
| cmd = [ 'make', '-n', target ] |
| with open(os.devnull, 'w') as devnull: |
| check_call(cmd, stdout=devnull, stderr=devnull) |
| return True |
| except CalledProcessError: |
| return False |
| |
| def run_unit_tests(): |
| """ |
| Runs the unit tests for the package via `make check` |
| """ |
| try: |
| cmd = make_parallel + [ 'check' ] |
| for i in range(0, args.repeat): |
| check_call_cmd(*cmd) |
| except CalledProcessError: |
| for root, _, files in os.walk(os.getcwd()): |
| if 'test-suite.log' not in files: |
| continue |
| check_call_cmd('cat', os.path.join(root, 'test-suite.log')) |
| raise Exception('Unit tests failed') |
| |
| def run_cppcheck(): |
| match_re = re.compile('((?!\.mako\.).)*\.[ch](?:pp)?$', re.I) |
| cppcheck_files = [] |
| stdout = subprocess.check_output(['git', 'ls-files']) |
| |
| for f in stdout.decode('utf-8').split(): |
| if match_re.match(f): |
| cppcheck_files.append(f) |
| |
| if not cppcheck_files: |
| # skip cppcheck if there arent' any c or cpp sources. |
| print("no files") |
| return None |
| |
| # http://cppcheck.sourceforge.net/manual.pdf |
| params = ['cppcheck', '-j', str(multiprocessing.cpu_count()), |
| '--enable=all', '--file-list=-'] |
| |
| cppcheck_process = subprocess.Popen( |
| params, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE, |
| stdin=subprocess.PIPE) |
| (stdout, stderr) = cppcheck_process.communicate( |
| input='\n'.join(cppcheck_files)) |
| |
| if cppcheck_process.wait(): |
| raise Exception('Cppcheck failed') |
| print(stdout) |
| print(stderr) |
| |
| def is_valgrind_safe(): |
| """ |
| Returns whether it is safe to run valgrind on our platform |
| """ |
| src = 'unit-test-vg.c' |
| exe = './unit-test-vg' |
| with open(src, 'w') as h: |
| h.write('#include <errno.h>\n') |
| h.write('#include <stdio.h>\n') |
| h.write('#include <stdlib.h>\n') |
| h.write('#include <string.h>\n') |
| h.write('int main() {\n') |
| h.write('char *heap_str = malloc(16);\n') |
| h.write('strcpy(heap_str, "RandString");\n') |
| h.write('int res = strcmp("RandString", heap_str);\n') |
| h.write('free(heap_str);\n') |
| h.write('char errstr[64];\n') |
| h.write('strerror_r(EINVAL, errstr, sizeof(errstr));\n') |
| h.write('printf("%s\\n", errstr);\n') |
| h.write('return res;\n') |
| h.write('}\n') |
| try: |
| with open(os.devnull, 'w') as devnull: |
| check_call(['gcc', '-O2', '-o', exe, src], |
| stdout=devnull, stderr=devnull) |
| check_call(['valgrind', '--error-exitcode=99', exe], |
| stdout=devnull, stderr=devnull) |
| return True |
| except: |
| sys.stderr.write("###### Platform is not valgrind safe ######\n") |
| return False |
| finally: |
| os.remove(src) |
| os.remove(exe) |
| |
| def is_sanitize_safe(): |
| """ |
| Returns whether it is safe to run sanitizers on our platform |
| """ |
| src = 'unit-test-sanitize.c' |
| exe = './unit-test-sanitize' |
| with open(src, 'w') as h: |
| h.write('int main() { return 0; }\n') |
| try: |
| with open(os.devnull, 'w') as devnull: |
| check_call(['gcc', '-O2', '-fsanitize=address', |
| '-fsanitize=undefined', '-o', exe, src], |
| stdout=devnull, stderr=devnull) |
| check_call([exe], stdout=devnull, stderr=devnull) |
| return True |
| except: |
| sys.stderr.write("###### Platform is not sanitize safe ######\n") |
| return False |
| finally: |
| os.remove(src) |
| os.remove(exe) |
| |
| def meson_setup_exists(setup): |
| """ |
| Returns whether the meson build supports the named test setup. |
| |
| Parameter descriptions: |
| setup The setup target to check |
| """ |
| try: |
| with open(os.devnull, 'w') as devnull: |
| output = subprocess.check_output( |
| ['meson', 'test', '-C', 'build', |
| '--setup', setup, '-t', '0'], |
| stderr=subprocess.STDOUT) |
| except CalledProcessError as e: |
| output = e.output |
| return not re.search('Test setup .* not found from project', output) |
| |
| def run_unit_tests_meson(): |
| """ |
| Runs the unit tests for the meson based package |
| """ |
| try: |
| check_call_cmd('meson', 'test', '-C', 'build') |
| except CalledProcessError: |
| for root, _, files in os.walk(os.getcwd()): |
| if 'testlog.txt' not in files: |
| continue |
| check_call_cmd('cat', os.path.join(root, 'testlog.txt')) |
| raise Exception('Unit tests failed') |
| |
| def maybe_meson_valgrind(): |
| """ |
| Potentially runs the unit tests through valgrind for the package |
| via `meson test`. The package can specify custom valgrind configurations |
| by utilizing add_test_setup() in a meson.build |
| """ |
| if not is_valgrind_safe(): |
| sys.stderr.write("###### Skipping valgrind ######\n") |
| return |
| try: |
| if meson_setup_exists('valgrind'): |
| check_call_cmd('meson', 'test', '-C', 'build', |
| '--setup', 'valgrind') |
| else: |
| check_call_cmd('meson', 'test', '-C', 'build', |
| '--wrapper', 'valgrind') |
| except CalledProcessError: |
| for root, _, files in os.walk(os.getcwd()): |
| if 'testlog-valgrind.txt' not in files: |
| continue |
| check_call_cmd('cat', os.path.join(root, 'testlog-valgrind.txt')) |
| raise Exception('Valgrind tests failed') |
| |
| def maybe_make_valgrind(): |
| """ |
| Potentially runs the unit tests through valgrind for the package |
| via `make check-valgrind`. If the package does not have valgrind testing |
| then it just skips over this. |
| """ |
| # Valgrind testing is currently broken by an aggressive strcmp optimization |
| # that is inlined into optimized code for POWER by gcc 7+. Until we find |
| # a workaround, just don't run valgrind tests on POWER. |
| # https://github.com/openbmc/openbmc/issues/3315 |
| if not is_valgrind_safe(): |
| sys.stderr.write("###### Skipping valgrind ######\n") |
| return |
| if not make_target_exists('check-valgrind'): |
| return |
| |
| try: |
| cmd = make_parallel + [ 'check-valgrind' ] |
| check_call_cmd(*cmd) |
| except CalledProcessError: |
| for root, _, files in os.walk(os.getcwd()): |
| for f in files: |
| if re.search('test-suite-[a-z]+.log', f) is None: |
| continue |
| check_call_cmd('cat', os.path.join(root, f)) |
| raise Exception('Valgrind tests failed') |
| |
| def maybe_make_coverage(): |
| """ |
| Potentially runs the unit tests through code coverage for the package |
| via `make check-code-coverage`. If the package does not have code coverage |
| testing then it just skips over this. |
| """ |
| if not make_target_exists('check-code-coverage'): |
| return |
| |
| # Actually run code coverage |
| try: |
| cmd = make_parallel + [ 'check-code-coverage' ] |
| check_call_cmd(*cmd) |
| except CalledProcessError: |
| raise Exception('Code coverage failed') |
| |
| def find_file(filename, basedir): |
| """ |
| Finds all occurrences of a file in the base directory |
| and passes them back with their relative paths. |
| |
| Parameter descriptions: |
| filename The name of the file to find |
| basedir The base directory search in |
| """ |
| |
| filepaths = [] |
| for root, dirs, files in os.walk(basedir): |
| if filename in files: |
| filepaths.append(os.path.join(root, filename)) |
| return filepaths |
| |
| if __name__ == '__main__': |
| # CONFIGURE_FLAGS = [GIT REPO]:[CONFIGURE FLAGS] |
| CONFIGURE_FLAGS = { |
| 'sdbusplus': ['--enable-transaction'], |
| 'phosphor-logging': |
| ['--enable-metadata-processing', '--enable-openpower-pel-extension', |
| 'YAML_DIR=/usr/local/share/phosphor-dbus-yaml/yaml'] |
| } |
| |
| # MESON_FLAGS = [GIT REPO]:[MESON FLAGS] |
| MESON_FLAGS = { |
| } |
| |
| # DEPENDENCIES = [MACRO]:[library/header]:[GIT REPO] |
| DEPENDENCIES = { |
| 'AC_CHECK_LIB': {'mapper': 'phosphor-objmgr'}, |
| 'AC_CHECK_HEADER': { |
| 'host-ipmid': 'phosphor-host-ipmid', |
| 'blobs-ipmid': 'phosphor-ipmi-blobs', |
| 'sdbusplus': 'sdbusplus', |
| 'sdeventplus': 'sdeventplus', |
| 'stdplus': 'stdplus', |
| 'gpioplus': 'gpioplus', |
| 'phosphor-logging/log.hpp': 'phosphor-logging', |
| }, |
| 'AC_PATH_PROG': {'sdbus++': 'sdbusplus'}, |
| 'PKG_CHECK_MODULES': { |
| 'phosphor-dbus-interfaces': 'phosphor-dbus-interfaces', |
| 'openpower-dbus-interfaces': 'openpower-dbus-interfaces', |
| 'ibm-dbus-interfaces': 'ibm-dbus-interfaces', |
| 'libipmid': 'phosphor-host-ipmid', |
| 'libipmid-host': 'phosphor-host-ipmid', |
| 'sdbusplus': 'sdbusplus', |
| 'sdeventplus': 'sdeventplus', |
| 'stdplus': 'stdplus', |
| 'gpioplus': 'gpioplus', |
| 'phosphor-logging': 'phosphor-logging', |
| 'phosphor-snmp': 'phosphor-snmp', |
| 'ipmiblob': 'ipmi-blob-tool', |
| }, |
| } |
| |
| # Offset into array of macro parameters MACRO(0, 1, ...N) |
| DEPENDENCIES_OFFSET = { |
| 'AC_CHECK_LIB': 0, |
| 'AC_CHECK_HEADER': 0, |
| 'AC_PATH_PROG': 1, |
| 'PKG_CHECK_MODULES': 1, |
| } |
| |
| # DEPENDENCIES_REGEX = [GIT REPO]:[REGEX STRING] |
| DEPENDENCIES_REGEX = { |
| 'phosphor-logging': r'\S+-dbus-interfaces$' |
| } |
| |
| # Set command line arguments |
| parser = argparse.ArgumentParser() |
| parser.add_argument("-w", "--workspace", dest="WORKSPACE", required=True, |
| help="Workspace directory location(i.e. /home)") |
| parser.add_argument("-p", "--package", dest="PACKAGE", required=True, |
| help="OpenBMC package to be unit tested") |
| parser.add_argument("-t", "--test-only", dest="TEST_ONLY", |
| action="store_true", required=False, default=False, |
| help="Only run test cases, no other validation") |
| parser.add_argument("-v", "--verbose", action="store_true", |
| help="Print additional package status messages") |
| parser.add_argument("-r", "--repeat", help="Repeat tests N times", |
| type=int, default=1) |
| parser.add_argument("-b", "--branch", dest="BRANCH", required=False, |
| help="Branch to target for dependent repositories", |
| default="master") |
| parser.add_argument("-n", "--noformat", dest="FORMAT", |
| action="store_false", required=False, |
| help="Whether or not to run format code") |
| args = parser.parse_args(sys.argv[1:]) |
| WORKSPACE = args.WORKSPACE |
| UNIT_TEST_PKG = args.PACKAGE |
| TEST_ONLY = args.TEST_ONLY |
| BRANCH = args.BRANCH |
| FORMAT_CODE = args.FORMAT |
| if args.verbose: |
| def printline(*line): |
| for arg in line: |
| print arg, |
| print |
| else: |
| printline = lambda *l: None |
| |
| CODE_SCAN_DIR = WORKSPACE + "/" + UNIT_TEST_PKG |
| |
| # First validate code formatting if repo has style formatting files. |
| # The format-code.sh checks for these files. |
| if FORMAT_CODE: |
| check_call_cmd("./format-code.sh", CODE_SCAN_DIR) |
| |
| # Automake and meson |
| if (os.path.isfile(CODE_SCAN_DIR + "/configure.ac") or |
| os.path.isfile(CODE_SCAN_DIR + '/meson.build')): |
| prev_umask = os.umask(000) |
| # Determine dependencies and add them |
| dep_added = dict() |
| dep_added[UNIT_TEST_PKG] = False |
| # Create dependency tree |
| dep_tree = DepTree(UNIT_TEST_PKG) |
| build_dep_tree(UNIT_TEST_PKG, |
| os.path.join(WORKSPACE, UNIT_TEST_PKG), |
| dep_added, |
| dep_tree, |
| BRANCH) |
| |
| # Reorder Dependency Tree |
| for pkg_name, regex_str in DEPENDENCIES_REGEX.iteritems(): |
| dep_tree.ReorderDeps(pkg_name, regex_str) |
| if args.verbose: |
| dep_tree.PrintTree() |
| install_list = dep_tree.GetInstallList() |
| # We don't want to treat our package as a dependency |
| install_list.remove(UNIT_TEST_PKG) |
| # install reordered dependencies |
| for dep in install_list: |
| build_and_install(dep, False) |
| os.chdir(os.path.join(WORKSPACE, UNIT_TEST_PKG)) |
| # Run package unit tests |
| build_and_install(UNIT_TEST_PKG, True) |
| if os.path.isfile(CODE_SCAN_DIR + '/meson.build'): |
| if not TEST_ONLY: |
| maybe_meson_valgrind() |
| |
| # Run clang-tidy only if the project has a configuration |
| if os.path.isfile('.clang-tidy'): |
| check_call_cmd('run-clang-tidy-8.py', '-p', |
| 'build') |
| # Run the basic clang static analyzer otherwise |
| else: |
| check_call_cmd('ninja', '-C', 'build', |
| 'scan-build') |
| |
| # Run tests through sanitizers |
| # b_lundef is needed if clang++ is CXX since it resolves the |
| # asan symbols at runtime only. We don't want to set it earlier |
| # in the build process to ensure we don't have undefined |
| # runtime code. |
| if is_sanitize_safe(): |
| check_call_cmd('meson', 'configure', 'build', |
| '-Db_sanitize=address,undefined', |
| '-Db_lundef=false') |
| check_call_cmd('meson', 'test', '-C', 'build', |
| '--logbase', 'testlog-ubasan') |
| # TODO: Fix memory sanitizer |
| #check_call_cmd('meson', 'configure', 'build', |
| # '-Db_sanitize=memory') |
| #check_call_cmd('meson', 'test', '-C', 'build' |
| # '--logbase', 'testlog-msan') |
| check_call_cmd('meson', 'configure', 'build', |
| '-Db_sanitize=none', '-Db_lundef=true') |
| else: |
| sys.stderr.write("###### Skipping sanitizers ######\n") |
| |
| # Run coverage checks |
| check_call_cmd('meson', 'configure', 'build', |
| '-Db_coverage=true') |
| run_unit_tests_meson() |
| # Only build coverage HTML if coverage files were produced |
| for root, dirs, files in os.walk('build'): |
| if any([f.endswith('.gcda') for f in files]): |
| check_call_cmd('ninja', '-C', 'build', |
| 'coverage-html') |
| break |
| check_call_cmd('meson', 'configure', 'build', |
| '-Db_coverage=false') |
| else: |
| run_unit_tests_meson() |
| |
| else: |
| run_unit_tests() |
| if not TEST_ONLY: |
| maybe_make_valgrind() |
| maybe_make_coverage() |
| if not TEST_ONLY: |
| run_cppcheck() |
| |
| os.umask(prev_umask) |
| |
| # Cmake |
| elif os.path.isfile(CODE_SCAN_DIR + "/CMakeLists.txt"): |
| os.chdir(os.path.join(WORKSPACE, UNIT_TEST_PKG)) |
| check_call_cmd('cmake', '-DCMAKE_EXPORT_COMPILE_COMMANDS=ON', '.') |
| check_call_cmd('cmake', '--build', '.', '--', '-j', |
| str(multiprocessing.cpu_count())) |
| if make_target_exists('test'): |
| check_call_cmd('ctest', '.') |
| if not TEST_ONLY: |
| maybe_make_valgrind() |
| maybe_make_coverage() |
| run_cppcheck() |
| if os.path.isfile('.clang-tidy'): |
| check_call_cmd('run-clang-tidy-8.py', '-p', '.') |
| |
| else: |
| print "Not a supported repo for CI Tests, exit" |
| quit() |
| |
| # Run any custom CI scripts the repo has, of which there can be |
| # multiple of and anywhere in the repository. |
| ci_scripts = find_file('run-ci.sh', os.path.join(WORKSPACE, UNIT_TEST_PKG)) |
| if ci_scripts: |
| os.chdir(os.path.join(WORKSPACE, UNIT_TEST_PKG)) |
| for ci_script in ci_scripts: |
| check_call_cmd('sh', ci_script) |