blob: 630ba63f4a961559b98577bdb088b37c4d653c83 [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
367
368if __name__ == '__main__':
369 # CONFIGURE_FLAGS = [GIT REPO]:[CONFIGURE FLAGS]
370 CONFIGURE_FLAGS = {
Adriana Kobylak43c31e82017-02-13 09:28:35 -0600371 'phosphor-objmgr': ['--enable-unpatched-systemd'],
Matthew Barth1d1c6732017-03-24 10:00:28 -0500372 'sdbusplus': ['--enable-transaction'],
373 'phosphor-logging':
374 ['--enable-metadata-processing',
Deepak Kodihalli3a4e1b42017-06-08 09:52:35 -0500375 'YAML_DIR=/usr/local/share/phosphor-dbus-yaml/yaml']
Matthew Barthccb7f852016-11-23 17:43:02 -0600376 }
377
378 # DEPENDENCIES = [MACRO]:[library/header]:[GIT REPO]
379 DEPENDENCIES = {
380 'AC_CHECK_LIB': {'mapper': 'phosphor-objmgr'},
Matthew Barth710f3f02017-01-18 15:20:19 -0600381 'AC_CHECK_HEADER': {
382 'host-ipmid': 'phosphor-host-ipmid',
383 'sdbusplus': 'sdbusplus',
Saqib Khan66145052017-02-14 12:02:07 -0600384 'phosphor-logging/log.hpp': 'phosphor-logging',
Patrick Williamseab8a372017-01-30 11:21:32 -0600385 },
Brad Bishopebb49112017-02-13 11:07:26 -0500386 'AC_PATH_PROG': {'sdbus++': 'sdbusplus'},
Patrick Williamseab8a372017-01-30 11:21:32 -0600387 'PKG_CHECK_MODULES': {
Matthew Barth19e261e2017-02-01 12:55:22 -0600388 'phosphor-dbus-interfaces': 'phosphor-dbus-interfaces',
Patrick Williamsf128b402017-03-29 06:45:59 -0500389 'openpower-dbus-interfaces': 'openpower-dbus-interfaces',
Matt Spinler7be19032018-04-13 09:43:14 -0500390 'ibm-dbus-interfaces': 'ibm-dbus-interfaces',
Brad Bishopebb49112017-02-13 11:07:26 -0500391 'sdbusplus': 'sdbusplus',
392 'phosphor-logging': 'phosphor-logging',
393 },
Matthew Barthccb7f852016-11-23 17:43:02 -0600394 }
395
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500396 # DEPENDENCIES_REGEX = [GIT REPO]:[REGEX STRING]
397 DEPENDENCIES_REGEX = {
398 'phosphor-logging': '\S+-dbus-interfaces$'
399 }
400
Matthew Barth33df8792016-12-19 14:30:17 -0600401 # Set command line arguments
402 parser = argparse.ArgumentParser()
403 parser.add_argument("-w", "--workspace", dest="WORKSPACE", required=True,
404 help="Workspace directory location(i.e. /home)")
405 parser.add_argument("-p", "--package", dest="PACKAGE", required=True,
406 help="OpenBMC package to be unit tested")
407 parser.add_argument("-v", "--verbose", action="store_true",
408 help="Print additional package status messages")
Andrew Jeffery468309d2018-03-08 13:46:33 +1030409 parser.add_argument("-r", "--repeat", help="Repeat tests N times",
410 type=int, default=1)
Matthew Barth33df8792016-12-19 14:30:17 -0600411 args = parser.parse_args(sys.argv[1:])
412 WORKSPACE = args.WORKSPACE
413 UNIT_TEST_PKG = args.PACKAGE
414 if args.verbose:
415 def printline(*line):
416 for arg in line:
417 print arg,
418 print
419 else:
420 printline = lambda *l: None
Matthew Barthccb7f852016-11-23 17:43:02 -0600421
Adriana Kobylakbcee22b2018-01-10 16:58:27 -0600422 # First validate code formattting if repo has style formatting files.
423 # The format-code.sh checks for these files.
Andrew Geisslera28286d2018-01-10 11:00:00 -0800424 CODE_SCAN_DIR = WORKSPACE + "/" + UNIT_TEST_PKG
Adriana Kobylakbcee22b2018-01-10 16:58:27 -0600425 check_call_cmd(WORKSPACE, "./format-code.sh", CODE_SCAN_DIR)
Andrew Geisslera28286d2018-01-10 11:00:00 -0800426
Andrew Geissler71a7cc12018-01-31 14:18:37 -0800427 # The rest of this script is CI testing, which currently only supports
428 # Automake based repos. Check if this repo is Automake, if not exit
429 if not os.path.isfile(CODE_SCAN_DIR + "/configure.ac"):
430 print "Not a supported repo for CI Tests, exit"
431 quit()
432
Matthew Barthccb7f852016-11-23 17:43:02 -0600433 prev_umask = os.umask(000)
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500434 # Determine dependencies and add them
435 dep_added = dict()
436 dep_added[UNIT_TEST_PKG] = False
437 # Create dependency tree
438 dep_tree = DepTree(UNIT_TEST_PKG)
439 build_dep_tree(UNIT_TEST_PKG,
440 os.path.join(WORKSPACE, UNIT_TEST_PKG),
441 dep_added,
442 dep_tree)
443
444 # Reorder Dependency Tree
445 for pkg_name, regex_str in DEPENDENCIES_REGEX.iteritems():
446 dep_tree.ReorderDeps(pkg_name, regex_str)
447 if args.verbose:
448 dep_tree.PrintTree()
449 install_list = dep_tree.GetInstallList()
Gunnar Mills5f811802017-10-25 16:10:27 -0500450 # install reordered dependencies
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500451 install_deps(install_list)
Matthew Barthccb7f852016-11-23 17:43:02 -0600452 os.chdir(os.path.join(WORKSPACE, UNIT_TEST_PKG))
Matthew Barth948b7cc2017-02-21 09:13:54 -0600453 # Refresh dynamic linker run time bindings for dependencies
454 check_call_cmd(os.path.join(WORKSPACE, UNIT_TEST_PKG), 'ldconfig')
Matthew Barthccb7f852016-11-23 17:43:02 -0600455 # Run package unit tests
Andrew Jefferya4e31c62018-03-08 13:45:28 +1030456 try:
William A. Kennington IIIa2156732018-06-30 18:38:09 -0700457 cmd = make_parallel + [ 'check' ]
Andrew Jeffery468309d2018-03-08 13:46:33 +1030458 for i in range(0, args.repeat):
459 check_call_cmd(os.path.join(WORKSPACE, UNIT_TEST_PKG), *cmd)
Andrew Jefferya4e31c62018-03-08 13:45:28 +1030460 except CalledProcessError:
William A. Kennington III386e05c2018-06-20 00:13:11 -0700461 for root, _, files in os.walk(os.path.join(WORKSPACE, UNIT_TEST_PKG)):
462 if 'test-suite.log' not in files:
Andrew Jeffery88721792018-04-30 14:30:32 +0930463 continue
William A. Kennington III386e05c2018-06-20 00:13:11 -0700464 check_call_cmd(root, 'cat', os.path.join(root, 'test-suite.log'))
465 raise Exception('Unit tests failed')
William A. Kennington III32383352018-06-20 00:18:05 -0700466
467 with open(os.devnull, 'w') as devnull:
468 # Run unit tests through valgrind if it exists
469 top_dir = os.path.join(WORKSPACE, UNIT_TEST_PKG)
470 try:
471 cmd = [ 'make', '-n', 'check-valgrind' ]
472 check_call(cmd, stdout=devnull, stderr=devnull)
473 try:
William A. Kennington IIIa2156732018-06-30 18:38:09 -0700474 cmd = make_parallel + [ 'check-valgrind' ]
William A. Kennington III32383352018-06-20 00:18:05 -0700475 check_call_cmd(top_dir, *cmd)
476 except CalledProcessError:
477 for root, _, files in os.walk(top_dir):
478 for f in files:
479 if re.search('test-suite-[a-z]+.log', f) is None:
480 continue
481 check_call_cmd(root, 'cat', os.path.join(root, f))
482 raise Exception('Valgrind tests failed')
483 except CalledProcessError:
484 pass
485
486 # Run code coverage if possible
487 try:
488 cmd = [ 'make', '-n', 'check-code-coverage' ]
489 check_call(cmd, stdout=devnull, stderr=devnull)
490 try:
William A. Kennington IIIa2156732018-06-30 18:38:09 -0700491 cmd = make_parallel + [ 'check-code-coverage' ]
William A. Kennington III32383352018-06-20 00:18:05 -0700492 check_call_cmd(top_dir, *cmd)
493 except CalledProcessError:
494 raise Exception('Code coverage failed')
495 except CalledProcessError:
496 pass
497
Matthew Barthccb7f852016-11-23 17:43:02 -0600498 os.umask(prev_umask)