blob: 9313e5ed0f73d44caf577fdc61eb28419545be34 [file] [log] [blame]
#!/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 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(dir, *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(dir, ">", " ".join(cmd))
check_call(cmd)
def clone_pkg(pkg):
"""
Clone the given openbmc package's git repository from gerrit into
the WORKSPACE location
Parameter descriptions:
pkg Name of the package to clone
"""
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, "./")
return Repo.clone_from(pkg_repo, pkg_dir).working_dir
def get_deps(configure_ac):
"""
Parse the given 'configure.ac' file for package dependencies and return
a list of the dependencies found.
Parameter descriptions:
configure_ac Opened 'configure.ac' file object
"""
line = ""
dep_pkgs = set()
for cfg_line in configure_ac:
# Remove whitespace & newline
cfg_line = cfg_line.rstrip()
# Check for line breaks
if cfg_line.endswith('\\'):
line += str(cfg_line[:-1])
continue
line = line+cfg_line
# Find any defined dependency
line_has = lambda x: x if x in line else None
macros = set(filter(line_has, DEPENDENCIES.iterkeys()))
if len(macros) == 1:
macro = ''.join(macros)
deps = filter(line_has, DEPENDENCIES[macro].iterkeys())
dep_pkgs.update(map(lambda x: DEPENDENCIES[macro][x], deps))
line = ""
deps = list(dep_pkgs)
return 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 build_and_install(pkg):
"""
Builds and installs the package in the environment. Optionally
builds the examples and test cases for package.
Parameter description:
pkg The package we are building
"""
pkgdir = os.path.join(WORKSPACE, pkg)
# Build & install this package
conf_flags = [
'--disable-silent-rules',
'--enable-tests',
'--enable-code-coverage',
'--enable-valgrind'
]
os.chdir(pkgdir)
# Add any necessary configure flags for package
if CONFIGURE_FLAGS.get(pkg) is not None:
conf_flags.extend(CONFIGURE_FLAGS.get(pkg))
for bootstrap in ['bootstrap.sh', 'bootstrap', 'autogen.sh']:
if os.path.exists(bootstrap):
check_call_cmd(pkgdir, './' + bootstrap)
break
check_call_cmd(pkgdir, './configure', *conf_flags)
check_call_cmd(pkgdir, *make_parallel)
check_call_cmd(pkgdir, 'sudo', '-n', '--', *(make_parallel + [ 'install' ]))
def install_deps(dep_list):
"""
Install each package in the ordered dep_list.
Parameter descriptions:
dep_list Ordered list of dependencies
"""
for pkg in dep_list:
build_and_install(pkg)
def build_dep_tree(pkg, pkgdir, dep_added, head, 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 list of dependencies and added status
head Head node of the dependency tree
dep_tree Current dependency tree node
"""
if not dep_tree:
dep_tree = head
os.chdir(pkgdir)
# Open package's configure.ac
with open("/tmp/depcache", "r") as depcache:
cached = depcache.readline()
with open("configure.ac", "rt") as configure_ac:
# Retrieve dependency list from package's configure.ac
configure_ac_deps = get_deps(configure_ac)
for dep_pkg in configure_ac_deps:
if dep_pkg in cached:
continue
# Dependency package not already known
if dep_added.get(dep_pkg) is None:
# Dependency package not added
new_child = dep_tree.AddChild(dep_pkg)
dep_added[dep_pkg] = False
dep_pkgdir = clone_pkg(dep_pkg)
# Determine this dependency package's
# dependencies and add them before
# returning to add this package
dep_added = build_dep_tree(dep_pkg,
dep_pkgdir,
dep_added,
head,
new_child)
else:
# Dependency package known and added
if dep_added[dep_pkg]:
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(top_dir):
"""
Runs the unit tests for the package via `make check`
Parameter descriptions:
top_dir The root directory of our project
"""
try:
cmd = make_parallel + [ 'check' ]
for i in range(0, args.repeat):
check_call_cmd(top_dir, *cmd)
except CalledProcessError:
for root, _, files in os.walk(top_dir):
if 'test-suite.log' not in files:
continue
check_call_cmd(root, 'cat', os.path.join(root, 'test-suite.log'))
raise Exception('Unit tests failed')
def run_cppcheck(top_dir):
try:
# http://cppcheck.sourceforge.net/manual.pdf
ignore_list = ['-i%s' % path for path in os.listdir(top_dir) \
if path.endswith('-src') or path.endswith('-build')]
ignore_list.extend(('-itest', '-iscripts'))
params = ['cppcheck', '-j', str(multiprocessing.cpu_count()),
'--enable=all']
params.extend(ignore_list)
params.append('.')
check_call_cmd(top_dir, *params)
except CalledProcessError:
raise Exception('Cppcheck failed')
def maybe_run_valgrind(top_dir):
"""
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.
Parameter descriptions:
top_dir The root directory of our project
"""
# 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 re.match('ppc64', platform.machine()) is not None:
return
if not make_target_exists('check-valgrind'):
return
try:
cmd = make_parallel + [ 'check-valgrind' ]
check_call_cmd(top_dir, *cmd)
except CalledProcessError:
for root, _, files in os.walk(top_dir):
for f in files:
if re.search('test-suite-[a-z]+.log', f) is None:
continue
check_call_cmd(root, 'cat', os.path.join(root, f))
raise Exception('Valgrind tests failed')
def maybe_run_coverage(top_dir):
"""
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.
Parameter descriptions:
top_dir The root directory of our project
"""
if not make_target_exists('check-code-coverage'):
return
# Actually run code coverage
try:
cmd = make_parallel + [ 'check-code-coverage' ]
check_call_cmd(top_dir, *cmd)
except CalledProcessError:
raise Exception('Code coverage failed')
if __name__ == '__main__':
# CONFIGURE_FLAGS = [GIT REPO]:[CONFIGURE FLAGS]
CONFIGURE_FLAGS = {
'phosphor-objmgr': ['--enable-unpatched-systemd'],
'sdbusplus': ['--enable-transaction'],
'phosphor-logging':
['--enable-metadata-processing',
'YAML_DIR=/usr/local/share/phosphor-dbus-yaml/yaml']
}
# 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',
'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',
'sdbusplus': 'sdbusplus',
'sdeventplus': 'sdeventplus',
'gpioplus': 'gpioplus',
'phosphor-logging': 'phosphor-logging',
'phosphor-snmp': 'phosphor-snmp',
},
}
# 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("-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)
args = parser.parse_args(sys.argv[1:])
WORKSPACE = args.WORKSPACE
UNIT_TEST_PKG = args.PACKAGE
if args.verbose:
def printline(*line):
for arg in line:
print arg,
print
else:
printline = lambda *l: None
# First validate code formatting if repo has style formatting files.
# The format-code.sh checks for these files.
CODE_SCAN_DIR = WORKSPACE + "/" + UNIT_TEST_PKG
check_call_cmd(WORKSPACE, "./format-code.sh", CODE_SCAN_DIR)
# Automake
if os.path.isfile(CODE_SCAN_DIR + "/configure.ac"):
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)
# 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()
# install reordered dependencies
install_deps(install_list)
top_dir = os.path.join(WORKSPACE, UNIT_TEST_PKG)
os.chdir(top_dir)
# Refresh dynamic linker run time bindings for dependencies
check_call_cmd(top_dir, 'sudo', '-n', '--', 'ldconfig')
# Run package unit tests
run_unit_tests(top_dir)
maybe_run_valgrind(top_dir)
maybe_run_coverage(top_dir)
run_cppcheck(top_dir)
os.umask(prev_umask)
# Cmake
elif os.path.isfile(CODE_SCAN_DIR + "/CMakeLists.txt"):
top_dir = os.path.join(WORKSPACE, UNIT_TEST_PKG)
os.chdir(top_dir)
check_call_cmd(top_dir, 'cmake', '.')
check_call_cmd(top_dir, 'cmake', '--build', '.', '--', '-j',
str(multiprocessing.cpu_count()))
if make_target_exists('test'):
check_call_cmd(top_dir, 'ctest', '.')
maybe_run_valgrind(top_dir)
maybe_run_coverage(top_dir)
run_cppcheck(top_dir)
else:
print "Not a supported repo for CI Tests, exit"
quit()