blob: d8e1d7f71fc69b140bad87a982be78bfe470c8e9 [file] [log] [blame]
Matthew Barthccb7f852016-11-23 17:43:02 -06001#!/usr/bin/env python
2
3"""
4This script determines the given package's openbmc dependencies from its
5configure.ac file where it downloads, configures, builds, and installs each of
6these dependencies. Then the given package is configured, built, and installed
7prior to executing its unit tests.
8"""
9
Matthew Barthd1810372016-12-19 16:57:21 -060010from git import Repo
Matthew Barthccb7f852016-11-23 17:43:02 -060011from urlparse import urljoin
Andrew Jefferya4e31c62018-03-08 13:45:28 +103012from subprocess import check_call, call, CalledProcessError
Matthew Barthccb7f852016-11-23 17:43:02 -060013import os
14import sys
Matthew Barth33df8792016-12-19 14:30:17 -060015import argparse
William A. Kennington IIIa2156732018-06-30 18:38:09 -070016import multiprocessing
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -050017import re
18
19
20class DepTree():
21 """
22 Represents package dependency tree, where each node is a DepTree with a
23 name and DepTree children.
24 """
25
26 def __init__(self, name):
27 """
28 Create new DepTree.
29
30 Parameter descriptions:
31 name Name of new tree node.
32 """
33 self.name = name
34 self.children = list()
35
36 def AddChild(self, name):
37 """
38 Add new child node to current node.
39
40 Parameter descriptions:
41 name Name of new child
42 """
43 new_child = DepTree(name)
44 self.children.append(new_child)
45 return new_child
46
47 def AddChildNode(self, node):
48 """
49 Add existing child node to current node.
50
51 Parameter descriptions:
52 node Tree node to add
53 """
54 self.children.append(node)
55
56 def RemoveChild(self, name):
57 """
58 Remove child node.
59
60 Parameter descriptions:
61 name Name of child to remove
62 """
63 for child in self.children:
64 if child.name == name:
65 self.children.remove(child)
66 return
67
68 def GetNode(self, name):
69 """
70 Return node with matching name. Return None if not found.
71
72 Parameter descriptions:
73 name Name of node to return
74 """
75 if self.name == name:
76 return self
77 for child in self.children:
78 node = child.GetNode(name)
79 if node:
80 return node
81 return None
82
83 def GetParentNode(self, name, parent_node=None):
84 """
85 Return parent of node with matching name. Return none if not found.
86
87 Parameter descriptions:
88 name Name of node to get parent of
89 parent_node Parent of current node
90 """
91 if self.name == name:
92 return parent_node
93 for child in self.children:
94 found_node = child.GetParentNode(name, self)
95 if found_node:
96 return found_node
97 return None
98
99 def GetPath(self, name, path=None):
100 """
101 Return list of node names from head to matching name.
102 Return None if not found.
103
104 Parameter descriptions:
105 name Name of node
106 path List of node names from head to current node
107 """
108 if not path:
109 path = []
110 if self.name == name:
111 path.append(self.name)
112 return path
113 for child in self.children:
114 match = child.GetPath(name, path + [self.name])
115 if match:
116 return match
117 return None
118
119 def GetPathRegex(self, name, regex_str, path=None):
120 """
121 Return list of node paths that end in name, or match regex_str.
122 Return empty list if not found.
123
124 Parameter descriptions:
125 name Name of node to search for
126 regex_str Regex string to match node names
127 path Path of node names from head to current node
128 """
129 new_paths = []
130 if not path:
131 path = []
132 match = re.match(regex_str, self.name)
133 if (self.name == name) or (match):
134 new_paths.append(path + [self.name])
135 for child in self.children:
136 return_paths = None
137 full_path = path + [self.name]
138 return_paths = child.GetPathRegex(name, regex_str, full_path)
139 for i in return_paths:
140 new_paths.append(i)
141 return new_paths
142
143 def MoveNode(self, from_name, to_name):
144 """
145 Mode existing from_name node to become child of to_name node.
146
147 Parameter descriptions:
148 from_name Name of node to make a child of to_name
149 to_name Name of node to make parent of from_name
150 """
151 parent_from_node = self.GetParentNode(from_name)
152 from_node = self.GetNode(from_name)
153 parent_from_node.RemoveChild(from_name)
154 to_node = self.GetNode(to_name)
155 to_node.AddChildNode(from_node)
156
157 def ReorderDeps(self, name, regex_str):
158 """
159 Reorder dependency tree. If tree contains nodes with names that
160 match 'name' and 'regex_str', move 'regex_str' nodes that are
161 to the right of 'name' node, so that they become children of the
162 'name' node.
163
164 Parameter descriptions:
165 name Name of node to look for
166 regex_str Regex string to match names to
167 """
168 name_path = self.GetPath(name)
169 if not name_path:
170 return
171 paths = self.GetPathRegex(name, regex_str)
172 is_name_in_paths = False
173 name_index = 0
174 for i in range(len(paths)):
175 path = paths[i]
176 if path[-1] == name:
177 is_name_in_paths = True
178 name_index = i
179 break
180 if not is_name_in_paths:
181 return
182 for i in range(name_index + 1, len(paths)):
183 path = paths[i]
184 if name in path:
185 continue
186 from_name = path[-1]
187 self.MoveNode(from_name, name)
188
189 def GetInstallList(self):
190 """
191 Return post-order list of node names.
192
193 Parameter descriptions:
194 """
195 install_list = []
196 for child in self.children:
197 child_install_list = child.GetInstallList()
198 install_list.extend(child_install_list)
199 install_list.append(self.name)
200 return install_list
201
202 def PrintTree(self, level=0):
203 """
204 Print pre-order node names with indentation denoting node depth level.
205
206 Parameter descriptions:
207 level Current depth level
208 """
209 INDENT_PER_LEVEL = 4
210 print ' ' * (level * INDENT_PER_LEVEL) + self.name
211 for child in self.children:
212 child.PrintTree(level + 1)
Matthew Barth33df8792016-12-19 14:30:17 -0600213
214
215def check_call_cmd(dir, *cmd):
216 """
217 Verbose prints the directory location the given command is called from and
218 the command, then executes the command using check_call.
219
220 Parameter descriptions:
221 dir Directory location command is to be called from
222 cmd List of parameters constructing the complete command
223 """
224 printline(dir, ">", " ".join(cmd))
225 check_call(cmd)
Matthew Barthccb7f852016-11-23 17:43:02 -0600226
227
228def clone_pkg(pkg):
Matthew Barth33df8792016-12-19 14:30:17 -0600229 """
230 Clone the given openbmc package's git repository from gerrit into
231 the WORKSPACE location
232
233 Parameter descriptions:
234 pkg Name of the package to clone
235 """
Andrew Jeffery7be94ca2018-03-08 13:15:33 +1030236 pkg_dir = os.path.join(WORKSPACE, pkg)
237 if os.path.exists(os.path.join(pkg_dir, '.git')):
238 return pkg_dir
Matthew Barthccb7f852016-11-23 17:43:02 -0600239 pkg_repo = urljoin('https://gerrit.openbmc-project.xyz/openbmc/', pkg)
Andrew Jeffery7be94ca2018-03-08 13:15:33 +1030240 os.mkdir(pkg_dir)
241 printline(pkg_dir, "> git clone", pkg_repo, "./")
242 return Repo.clone_from(pkg_repo, pkg_dir).working_dir
Matthew Barth33df8792016-12-19 14:30:17 -0600243
244
245def get_deps(configure_ac):
246 """
247 Parse the given 'configure.ac' file for package dependencies and return
248 a list of the dependencies found.
249
250 Parameter descriptions:
251 configure_ac Opened 'configure.ac' file object
252 """
253 line = ""
Brad Bishopebb49112017-02-13 11:07:26 -0500254 dep_pkgs = set()
Matthew Barth33df8792016-12-19 14:30:17 -0600255 for cfg_line in configure_ac:
256 # Remove whitespace & newline
257 cfg_line = cfg_line.rstrip()
258 # Check for line breaks
259 if cfg_line.endswith('\\'):
260 line += str(cfg_line[:-1])
261 continue
262 line = line+cfg_line
263
264 # Find any defined dependency
Brad Bishopebb49112017-02-13 11:07:26 -0500265 line_has = lambda x: x if x in line else None
266 macros = set(filter(line_has, DEPENDENCIES.iterkeys()))
267 if len(macros) == 1:
268 macro = ''.join(macros)
269 deps = filter(line_has, DEPENDENCIES[macro].iterkeys())
270 dep_pkgs.update(map(lambda x: DEPENDENCIES[macro][x], deps))
271
Matthew Barth33df8792016-12-19 14:30:17 -0600272 line = ""
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500273 deps = list(dep_pkgs)
Matthew Barth33df8792016-12-19 14:30:17 -0600274
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500275 return deps
Matthew Barthccb7f852016-11-23 17:43:02 -0600276
277
William A. Kennington IIIa2156732018-06-30 18:38:09 -0700278make_parallel = [
279 'make',
280 # Run enough jobs to saturate all the cpus
281 '-j', str(multiprocessing.cpu_count()),
282 # Don't start more jobs if the load avg is too high
283 '-l', str(multiprocessing.cpu_count()),
284 # Synchronize the output so logs aren't intermixed in stdout / stderr
285 '-O',
286]
287
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500288def install_deps(dep_list):
Matthew Barthccb7f852016-11-23 17:43:02 -0600289 """
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500290 Install each package in the ordered dep_list.
Matthew Barthccb7f852016-11-23 17:43:02 -0600291
292 Parameter descriptions:
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500293 dep_list Ordered list of dependencies
Matthew Barthccb7f852016-11-23 17:43:02 -0600294 """
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500295 for pkg in dep_list:
296 pkgdir = os.path.join(WORKSPACE, pkg)
297 # Build & install this package
William A. Kennington III7005cff2018-06-20 00:14:26 -0700298 conf_flags = [
299 '--disable-silent-rules',
William A. Kennington III82c8d732018-06-22 08:54:56 -0700300 '--enable-tests',
William A. Kennington III32383352018-06-20 00:18:05 -0700301 '--enable-code-coverage',
302 '--enable-valgrind',
William A. Kennington III7005cff2018-06-20 00:14:26 -0700303 ]
Matthew Barthccb7f852016-11-23 17:43:02 -0600304 os.chdir(pkgdir)
305 # Add any necessary configure flags for package
306 if CONFIGURE_FLAGS.get(pkg) is not None:
Matthew Barth1d1c6732017-03-24 10:00:28 -0500307 conf_flags.extend(CONFIGURE_FLAGS.get(pkg))
Matthew Barth33df8792016-12-19 14:30:17 -0600308 check_call_cmd(pkgdir, './bootstrap.sh')
Matthew Barth1d1c6732017-03-24 10:00:28 -0500309 check_call_cmd(pkgdir, './configure', *conf_flags)
William A. Kennington IIIa2156732018-06-30 18:38:09 -0700310 check_call_cmd(pkgdir, *make_parallel)
311 check_call_cmd(pkgdir, *(make_parallel + [ 'install' ]))
Matthew Barthccb7f852016-11-23 17:43:02 -0600312
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500313
314def build_dep_tree(pkg, pkgdir, dep_added, head, dep_tree=None):
315 """
316 For each package(pkg), starting with the package to be unit tested,
317 parse its 'configure.ac' file from within the package's directory(pkgdir)
318 for each package dependency defined recursively doing the same thing
319 on each package found as a dependency.
320
321 Parameter descriptions:
322 pkg Name of the package
323 pkgdir Directory where package source is located
324 dep_added Current list of dependencies and added status
325 head Head node of the dependency tree
326 dep_tree Current dependency tree node
327 """
328 if not dep_tree:
329 dep_tree = head
330 os.chdir(pkgdir)
331 # Open package's configure.ac
Andrew Jeffery2cb0c7a2018-03-08 13:19:08 +1030332 with open("/root/.depcache", "r") as depcache:
333 cached = depcache.readline()
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500334 with open("configure.ac", "rt") as configure_ac:
335 # Retrieve dependency list from package's configure.ac
336 configure_ac_deps = get_deps(configure_ac)
337 for dep_pkg in configure_ac_deps:
Andrew Jeffery2cb0c7a2018-03-08 13:19:08 +1030338 if dep_pkg in cached:
339 continue
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500340 # Dependency package not already known
341 if dep_added.get(dep_pkg) is None:
342 # Dependency package not added
343 new_child = dep_tree.AddChild(dep_pkg)
344 dep_added[dep_pkg] = False
Andrew Jeffery7be94ca2018-03-08 13:15:33 +1030345 dep_pkgdir = clone_pkg(dep_pkg)
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500346 # Determine this dependency package's
347 # dependencies and add them before
348 # returning to add this package
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500349 dep_added = build_dep_tree(dep_pkg,
Andrew Jeffery7be94ca2018-03-08 13:15:33 +1030350 dep_pkgdir,
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500351 dep_added,
352 head,
353 new_child)
354 else:
355 # Dependency package known and added
356 if dep_added[dep_pkg]:
357 continue
358 else:
359 # Cyclic dependency failure
360 raise Exception("Cyclic dependencies found in "+pkg)
361
362 if not dep_added[pkg]:
363 dep_added[pkg] = True
364
365 return dep_added
Matthew Barthccb7f852016-11-23 17:43:02 -0600366
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700367def make_target_exists(target):
368 """
369 Runs a check against the makefile in the current directory to determine
370 if the target exists so that it can be built.
371
372 Parameter descriptions:
373 target The make target we are checking
374 """
375 try:
376 cmd = [ 'make', '-n', target ]
377 with open(os.devnull, 'w') as devnull:
378 check_call(cmd, stdout=devnull, stderr=devnull)
379 return True
380 except CalledProcessError:
381 return False
382
383def run_unit_tests(top_dir):
384 """
385 Runs the unit tests for the package via `make check`
386
387 Parameter descriptions:
388 top_dir The root directory of our project
389 """
390 try:
391 cmd = make_parallel + [ 'check' ]
392 for i in range(0, args.repeat):
393 check_call_cmd(top_dir, *cmd)
394 except CalledProcessError:
395 for root, _, files in os.walk(top_dir):
396 if 'test-suite.log' not in files:
397 continue
398 check_call_cmd(root, 'cat', os.path.join(root, 'test-suite.log'))
399 raise Exception('Unit tests failed')
400
401
402def maybe_run_valgrind(top_dir):
403 """
404 Potentially runs the unit tests through valgrind for the package
405 via `make check-valgrind`. If the package does not have valgrind testing
406 then it just skips over this.
407
408 Parameter descriptions:
409 top_dir The root directory of our project
410 """
411 if not make_target_exists('check-valgrind'):
412 return
413
414 try:
415 cmd = make_parallel + [ 'check-valgrind' ]
416 check_call_cmd(top_dir, *cmd)
417 except CalledProcessError:
418 for root, _, files in os.walk(top_dir):
419 for f in files:
420 if re.search('test-suite-[a-z]+.log', f) is None:
421 continue
422 check_call_cmd(root, 'cat', os.path.join(root, f))
423 raise Exception('Valgrind tests failed')
424
425def maybe_run_coverage(top_dir):
426 """
427 Potentially runs the unit tests through code coverage for the package
428 via `make check-code-coverage`. If the package does not have code coverage
429 testing then it just skips over this.
430
431 Parameter descriptions:
432 top_dir The root directory of our project
433 """
434 if not make_target_exists('check-code-coverage'):
435 return
436
437 # Actually run code coverage
438 try:
439 cmd = make_parallel + [ 'check-code-coverage' ]
440 check_call_cmd(top_dir, *cmd)
441 except CalledProcessError:
442 raise Exception('Code coverage failed')
Matthew Barthccb7f852016-11-23 17:43:02 -0600443
444if __name__ == '__main__':
445 # CONFIGURE_FLAGS = [GIT REPO]:[CONFIGURE FLAGS]
446 CONFIGURE_FLAGS = {
Adriana Kobylak43c31e82017-02-13 09:28:35 -0600447 'phosphor-objmgr': ['--enable-unpatched-systemd'],
Matthew Barth1d1c6732017-03-24 10:00:28 -0500448 'sdbusplus': ['--enable-transaction'],
449 'phosphor-logging':
450 ['--enable-metadata-processing',
Deepak Kodihalli3a4e1b42017-06-08 09:52:35 -0500451 'YAML_DIR=/usr/local/share/phosphor-dbus-yaml/yaml']
Matthew Barthccb7f852016-11-23 17:43:02 -0600452 }
453
454 # DEPENDENCIES = [MACRO]:[library/header]:[GIT REPO]
455 DEPENDENCIES = {
456 'AC_CHECK_LIB': {'mapper': 'phosphor-objmgr'},
Matthew Barth710f3f02017-01-18 15:20:19 -0600457 'AC_CHECK_HEADER': {
458 'host-ipmid': 'phosphor-host-ipmid',
459 'sdbusplus': 'sdbusplus',
Saqib Khan66145052017-02-14 12:02:07 -0600460 'phosphor-logging/log.hpp': 'phosphor-logging',
Patrick Williamseab8a372017-01-30 11:21:32 -0600461 },
Brad Bishopebb49112017-02-13 11:07:26 -0500462 'AC_PATH_PROG': {'sdbus++': 'sdbusplus'},
Patrick Williamseab8a372017-01-30 11:21:32 -0600463 'PKG_CHECK_MODULES': {
Matthew Barth19e261e2017-02-01 12:55:22 -0600464 'phosphor-dbus-interfaces': 'phosphor-dbus-interfaces',
Patrick Williamsf128b402017-03-29 06:45:59 -0500465 'openpower-dbus-interfaces': 'openpower-dbus-interfaces',
Matt Spinler7be19032018-04-13 09:43:14 -0500466 'ibm-dbus-interfaces': 'ibm-dbus-interfaces',
Brad Bishopebb49112017-02-13 11:07:26 -0500467 'sdbusplus': 'sdbusplus',
468 'phosphor-logging': 'phosphor-logging',
469 },
Matthew Barthccb7f852016-11-23 17:43:02 -0600470 }
471
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500472 # DEPENDENCIES_REGEX = [GIT REPO]:[REGEX STRING]
473 DEPENDENCIES_REGEX = {
474 'phosphor-logging': '\S+-dbus-interfaces$'
475 }
476
Matthew Barth33df8792016-12-19 14:30:17 -0600477 # Set command line arguments
478 parser = argparse.ArgumentParser()
479 parser.add_argument("-w", "--workspace", dest="WORKSPACE", required=True,
480 help="Workspace directory location(i.e. /home)")
481 parser.add_argument("-p", "--package", dest="PACKAGE", required=True,
482 help="OpenBMC package to be unit tested")
483 parser.add_argument("-v", "--verbose", action="store_true",
484 help="Print additional package status messages")
Andrew Jeffery468309d2018-03-08 13:46:33 +1030485 parser.add_argument("-r", "--repeat", help="Repeat tests N times",
486 type=int, default=1)
Matthew Barth33df8792016-12-19 14:30:17 -0600487 args = parser.parse_args(sys.argv[1:])
488 WORKSPACE = args.WORKSPACE
489 UNIT_TEST_PKG = args.PACKAGE
490 if args.verbose:
491 def printline(*line):
492 for arg in line:
493 print arg,
494 print
495 else:
496 printline = lambda *l: None
Matthew Barthccb7f852016-11-23 17:43:02 -0600497
Adriana Kobylakbcee22b2018-01-10 16:58:27 -0600498 # First validate code formattting if repo has style formatting files.
499 # The format-code.sh checks for these files.
Andrew Geisslera28286d2018-01-10 11:00:00 -0800500 CODE_SCAN_DIR = WORKSPACE + "/" + UNIT_TEST_PKG
Adriana Kobylakbcee22b2018-01-10 16:58:27 -0600501 check_call_cmd(WORKSPACE, "./format-code.sh", CODE_SCAN_DIR)
Andrew Geisslera28286d2018-01-10 11:00:00 -0800502
Andrew Geissler71a7cc12018-01-31 14:18:37 -0800503 # The rest of this script is CI testing, which currently only supports
504 # Automake based repos. Check if this repo is Automake, if not exit
505 if not os.path.isfile(CODE_SCAN_DIR + "/configure.ac"):
506 print "Not a supported repo for CI Tests, exit"
507 quit()
508
Matthew Barthccb7f852016-11-23 17:43:02 -0600509 prev_umask = os.umask(000)
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500510 # Determine dependencies and add them
511 dep_added = dict()
512 dep_added[UNIT_TEST_PKG] = False
513 # Create dependency tree
514 dep_tree = DepTree(UNIT_TEST_PKG)
515 build_dep_tree(UNIT_TEST_PKG,
516 os.path.join(WORKSPACE, UNIT_TEST_PKG),
517 dep_added,
518 dep_tree)
519
520 # Reorder Dependency Tree
521 for pkg_name, regex_str in DEPENDENCIES_REGEX.iteritems():
522 dep_tree.ReorderDeps(pkg_name, regex_str)
523 if args.verbose:
524 dep_tree.PrintTree()
525 install_list = dep_tree.GetInstallList()
Gunnar Mills5f811802017-10-25 16:10:27 -0500526 # install reordered dependencies
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500527 install_deps(install_list)
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700528 top_dir = os.path.join(WORKSPACE, UNIT_TEST_PKG)
529 os.chdir(top_dir)
Matthew Barth948b7cc2017-02-21 09:13:54 -0600530 # Refresh dynamic linker run time bindings for dependencies
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700531 check_call_cmd(top_dir, 'ldconfig')
Matthew Barthccb7f852016-11-23 17:43:02 -0600532 # Run package unit tests
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700533 run_unit_tests(top_dir)
534 maybe_run_valgrind(top_dir)
535 maybe_run_coverage(top_dir)
William A. Kennington III32383352018-06-20 00:18:05 -0700536
Matthew Barthccb7f852016-11-23 17:43:02 -0600537 os.umask(prev_umask)