1# 2# Copyright (c) 2014 Google, Inc 3# 4# SPDX-License-Identifier: GPL-2.0+ 5# 6 7import os 8import shutil 9import sys 10import tempfile 11import unittest 12 13import board 14import bsettings 15import cmdline 16import command 17import control 18import gitutil 19import terminal 20import toolchain 21 22settings_data = ''' 23# Buildman settings file 24 25[toolchain] 26 27[toolchain-alias] 28 29[make-flags] 30src=/home/sjg/c/src 31chroot=/home/sjg/c/chroot 32vboot=USE_STDINT=1 VBOOT_DEBUG=1 MAKEFLAGS_VBOOT=DEBUG=1 CFLAGS_EXTRA_VBOOT=-DUNROLL_LOOPS VBOOT_SOURCE=${src}/platform/vboot_reference 33chromeos_coreboot=VBOOT=${chroot}/build/link/usr ${vboot} 34chromeos_daisy=VBOOT=${chroot}/build/daisy/usr ${vboot} 35chromeos_peach=VBOOT=${chroot}/build/peach_pit/usr ${vboot} 36''' 37 38boards = [ 39 ['Active', 'arm', 'armv7', '', 'Tester', 'ARM Board 1', 'board0', ''], 40 ['Active', 'arm', 'armv7', '', 'Tester', 'ARM Board 2', 'board1', ''], 41 ['Active', 'powerpc', 'powerpc', '', 'Tester', 'PowerPC board 1', 'board2', ''], 42 ['Active', 'sandbox', 'sandbox', '', 'Tester', 'Sandbox board', 'board4', ''], 43] 44 45commit_shortlog = """4aca821 patman: Avoid changing the order of tags 4639403bb patman: Use --no-pager' to stop git from forking a pager 47db6e6f2 patman: Remove the -a option 48f2ccf03 patman: Correct unit tests to run correctly 491d097f9 patman: Fix indentation in terminal.py 50d073747 patman: Support the 'reverse' option for 'git log 51""" 52 53commit_log = ["""commit 7f6b8315d18f683c5181d0c3694818c1b2a20dcd 54Author: Masahiro Yamada <yamada.m@jp.panasonic.com> 55Date: Fri Aug 22 19:12:41 2014 +0900 56 57 buildman: refactor help message 58 59 "buildman [options]" is displayed by default. 60 61 Append the rest of help messages to parser.usage 62 instead of replacing it. 63 64 Besides, "-b <branch>" is not mandatory since commit fea5858e. 65 Drop it from the usage. 66 67 Signed-off-by: Masahiro Yamada <yamada.m@jp.panasonic.com> 68""", 69"""commit d0737479be6baf4db5e2cdbee123e96bc5ed0ba8 70Author: Simon Glass <sjg@chromium.org> 71Date: Thu Aug 14 16:48:25 2014 -0600 72 73 patman: Support the 'reverse' option for 'git log' 74 75 This option is currently not supported, but needs to be, for buildman to 76 operate as expected. 77 78 Series-changes: 7 79 - Add new patch to fix the 'reverse' bug 80 81 Series-version: 8 82 83 Change-Id: I79078f792e8b390b8a1272a8023537821d45feda 84 Reported-by: York Sun <yorksun@freescale.com> 85 Signed-off-by: Simon Glass <sjg@chromium.org> 86 87""", 88"""commit 1d097f9ab487c5019152fd47bda126839f3bf9fc 89Author: Simon Glass <sjg@chromium.org> 90Date: Sat Aug 9 11:44:32 2014 -0600 91 92 patman: Fix indentation in terminal.py 93 94 This code came from a different project with 2-character indentation. Fix 95 it for U-Boot. 96 97 Series-changes: 6 98 - Add new patch to fix indentation in teminal.py 99 100 Change-Id: I5a74d2ebbb3cc12a665f5c725064009ac96e8a34 101 Signed-off-by: Simon Glass <sjg@chromium.org> 102 103""", 104"""commit f2ccf03869d1e152c836515a3ceb83cdfe04a105 105Author: Simon Glass <sjg@chromium.org> 106Date: Sat Aug 9 11:08:24 2014 -0600 107 108 patman: Correct unit tests to run correctly 109 110 It seems that doctest behaves differently now, and some of the unit tests 111 do not run. Adjust the tests to work correctly. 112 113 ./tools/patman/patman --test 114 <unittest.result.TestResult run=10 errors=0 failures=0> 115 116 Series-changes: 6 117 - Add new patch to fix patman unit tests 118 119 Change-Id: I3d2ca588f4933e1f9d6b1665a00e4ae58269ff3b 120 121""", 122"""commit db6e6f2f9331c5a37647d6668768d4a40b8b0d1c 123Author: Simon Glass <sjg@chromium.org> 124Date: Sat Aug 9 12:06:02 2014 -0600 125 126 patman: Remove the -a option 127 128 It seems that this is no longer needed, since checkpatch.pl will catch 129 whitespace problems in patches. Also the option is not widely used, so 130 it seems safe to just remove it. 131 132 Series-changes: 6 133 - Add new patch to remove patman's -a option 134 135 Suggested-by: Masahiro Yamada <yamada.m@jp.panasonic.com> 136 Change-Id: I5821a1c75154e532c46513486ca40b808de7e2cc 137 138""", 139"""commit 39403bb4f838153028a6f21ca30bf100f3791133 140Author: Simon Glass <sjg@chromium.org> 141Date: Thu Aug 14 21:50:52 2014 -0600 142 143 patman: Use --no-pager' to stop git from forking a pager 144 145""", 146"""commit 4aca821e27e97925c039e69fd37375b09c6f129c 147Author: Simon Glass <sjg@chromium.org> 148Date: Fri Aug 22 15:57:39 2014 -0600 149 150 patman: Avoid changing the order of tags 151 152 patman collects tags that it sees in the commit and places them nicely 153 sorted at the end of the patch. However, this is not really necessary and 154 in fact is apparently not desirable. 155 156 Series-changes: 9 157 - Add new patch to avoid changing the order of tags 158 159 Series-version: 9 160 161 Suggested-by: Masahiro Yamada <yamada.m@jp.panasonic.com> 162 Change-Id: Ib1518588c1a189ad5c3198aae76f8654aed8d0db 163"""] 164 165TEST_BRANCH = '__testbranch' 166 167class TestFunctional(unittest.TestCase): 168 """Functional test for buildman. 169 170 This aims to test from just below the invocation of buildman (parsing 171 of arguments) to 'make' and 'git' invocation. It is not a true 172 emd-to-end test, as it mocks git, make and the tool chain. But this 173 makes it easier to detect when the builder is doing the wrong thing, 174 since in many cases this test code will fail. For example, only a 175 very limited subset of 'git' arguments is supported - anything 176 unexpected will fail. 177 """ 178 def setUp(self): 179 self._base_dir = tempfile.mkdtemp() 180 self._git_dir = os.path.join(self._base_dir, 'src') 181 self._buildman_pathname = sys.argv[0] 182 self._buildman_dir = os.path.dirname(os.path.realpath(sys.argv[0])) 183 command.test_result = self._HandleCommand 184 self.setupToolchains() 185 self._toolchains.Add('arm-gcc', test=False) 186 self._toolchains.Add('powerpc-gcc', test=False) 187 bsettings.Setup(None) 188 bsettings.AddFile(settings_data) 189 self._boards = board.Boards() 190 for brd in boards: 191 self._boards.AddBoard(board.Board(*brd)) 192 193 # Directories where the source been cloned 194 self._clone_dirs = [] 195 self._commits = len(commit_shortlog.splitlines()) + 1 196 self._total_builds = self._commits * len(boards) 197 198 # Number of calls to make 199 self._make_calls = 0 200 201 # Map of [board, commit] to error messages 202 self._error = {} 203 204 self._test_branch = TEST_BRANCH 205 206 # Avoid sending any output and clear all terminal output 207 terminal.SetPrintTestMode() 208 terminal.GetPrintTestLines() 209 210 def tearDown(self): 211 shutil.rmtree(self._base_dir) 212 213 def setupToolchains(self): 214 self._toolchains = toolchain.Toolchains() 215 self._toolchains.Add('gcc', test=False) 216 217 def _RunBuildman(self, *args): 218 return command.RunPipe([[self._buildman_pathname] + list(args)], 219 capture=True, capture_stderr=True) 220 221 def _RunControl(self, *args, **kwargs): 222 sys.argv = [sys.argv[0]] + list(args) 223 options, args = cmdline.ParseArgs() 224 result = control.DoBuildman(options, args, toolchains=self._toolchains, 225 make_func=self._HandleMake, boards=self._boards, 226 clean_dir=kwargs.get('clean_dir', True)) 227 self._builder = control.builder 228 return result 229 230 def testFullHelp(self): 231 command.test_result = None 232 result = self._RunBuildman('-H') 233 help_file = os.path.join(self._buildman_dir, 'README') 234 # Remove possible extraneous strings 235 extra = '::::::::::::::\n' + help_file + '\n::::::::::::::\n' 236 gothelp = result.stdout.replace(extra, '') 237 self.assertEqual(len(gothelp), os.path.getsize(help_file)) 238 self.assertEqual(0, len(result.stderr)) 239 self.assertEqual(0, result.return_code) 240 241 def testHelp(self): 242 command.test_result = None 243 result = self._RunBuildman('-h') 244 help_file = os.path.join(self._buildman_dir, 'README') 245 self.assertTrue(len(result.stdout) > 1000) 246 self.assertEqual(0, len(result.stderr)) 247 self.assertEqual(0, result.return_code) 248 249 def testGitSetup(self): 250 """Test gitutils.Setup(), from outside the module itself""" 251 command.test_result = command.CommandResult(return_code=1) 252 gitutil.Setup() 253 self.assertEqual(gitutil.use_no_decorate, False) 254 255 command.test_result = command.CommandResult(return_code=0) 256 gitutil.Setup() 257 self.assertEqual(gitutil.use_no_decorate, True) 258 259 def _HandleCommandGitLog(self, args): 260 if args[-1] == '--': 261 args = args[:-1] 262 if '-n0' in args: 263 return command.CommandResult(return_code=0) 264 elif args[-1] == 'upstream/master..%s' % self._test_branch: 265 return command.CommandResult(return_code=0, stdout=commit_shortlog) 266 elif args[:3] == ['--no-color', '--no-decorate', '--reverse']: 267 if args[-1] == self._test_branch: 268 count = int(args[3][2:]) 269 return command.CommandResult(return_code=0, 270 stdout=''.join(commit_log[:count])) 271 272 # Not handled, so abort 273 print 'git log', args 274 sys.exit(1) 275 276 def _HandleCommandGitConfig(self, args): 277 config = args[0] 278 if config == 'sendemail.aliasesfile': 279 return command.CommandResult(return_code=0) 280 elif config.startswith('branch.badbranch'): 281 return command.CommandResult(return_code=1) 282 elif config == 'branch.%s.remote' % self._test_branch: 283 return command.CommandResult(return_code=0, stdout='upstream\n') 284 elif config == 'branch.%s.merge' % self._test_branch: 285 return command.CommandResult(return_code=0, 286 stdout='refs/heads/master\n') 287 288 # Not handled, so abort 289 print 'git config', args 290 sys.exit(1) 291 292 def _HandleCommandGit(self, in_args): 293 """Handle execution of a git command 294 295 This uses a hacked-up parser. 296 297 Args: 298 in_args: Arguments after 'git' from the command line 299 """ 300 git_args = [] # Top-level arguments to git itself 301 sub_cmd = None # Git sub-command selected 302 args = [] # Arguments to the git sub-command 303 for arg in in_args: 304 if sub_cmd: 305 args.append(arg) 306 elif arg[0] == '-': 307 git_args.append(arg) 308 else: 309 if git_args and git_args[-1] in ['--git-dir', '--work-tree']: 310 git_args.append(arg) 311 else: 312 sub_cmd = arg 313 if sub_cmd == 'config': 314 return self._HandleCommandGitConfig(args) 315 elif sub_cmd == 'log': 316 return self._HandleCommandGitLog(args) 317 elif sub_cmd == 'clone': 318 return command.CommandResult(return_code=0) 319 elif sub_cmd == 'checkout': 320 return command.CommandResult(return_code=0) 321 322 # Not handled, so abort 323 print 'git', git_args, sub_cmd, args 324 sys.exit(1) 325 326 def _HandleCommandNm(self, args): 327 return command.CommandResult(return_code=0) 328 329 def _HandleCommandObjdump(self, args): 330 return command.CommandResult(return_code=0) 331 332 def _HandleCommandSize(self, args): 333 return command.CommandResult(return_code=0) 334 335 def _HandleCommand(self, **kwargs): 336 """Handle a command execution. 337 338 The command is in kwargs['pipe-list'], as a list of pipes, each a 339 list of commands. The command should be emulated as required for 340 testing purposes. 341 342 Returns: 343 A CommandResult object 344 """ 345 pipe_list = kwargs['pipe_list'] 346 wc = False 347 if len(pipe_list) != 1: 348 if pipe_list[1] == ['wc', '-l']: 349 wc = True 350 else: 351 print 'invalid pipe', kwargs 352 sys.exit(1) 353 cmd = pipe_list[0][0] 354 args = pipe_list[0][1:] 355 result = None 356 if cmd == 'git': 357 result = self._HandleCommandGit(args) 358 elif cmd == './scripts/show-gnu-make': 359 return command.CommandResult(return_code=0, stdout='make') 360 elif cmd.endswith('nm'): 361 return self._HandleCommandNm(args) 362 elif cmd.endswith('objdump'): 363 return self._HandleCommandObjdump(args) 364 elif cmd.endswith( 'size'): 365 return self._HandleCommandSize(args) 366 367 if not result: 368 # Not handled, so abort 369 print 'unknown command', kwargs 370 sys.exit(1) 371 372 if wc: 373 result.stdout = len(result.stdout.splitlines()) 374 return result 375 376 def _HandleMake(self, commit, brd, stage, cwd, *args, **kwargs): 377 """Handle execution of 'make' 378 379 Args: 380 commit: Commit object that is being built 381 brd: Board object that is being built 382 stage: Stage that we are at (mrproper, config, build) 383 cwd: Directory where make should be run 384 args: Arguments to pass to make 385 kwargs: Arguments to pass to command.RunPipe() 386 """ 387 self._make_calls += 1 388 if stage == 'mrproper': 389 return command.CommandResult(return_code=0) 390 elif stage == 'config': 391 return command.CommandResult(return_code=0, 392 combined='Test configuration complete') 393 elif stage == 'build': 394 stderr = '' 395 if type(commit) is not str: 396 stderr = self._error.get((brd.target, commit.sequence)) 397 if stderr: 398 return command.CommandResult(return_code=1, stderr=stderr) 399 return command.CommandResult(return_code=0) 400 401 # Not handled, so abort 402 print 'make', stage 403 sys.exit(1) 404 405 # Example function to print output lines 406 def print_lines(self, lines): 407 print len(lines) 408 for line in lines: 409 print line 410 #self.print_lines(terminal.GetPrintTestLines()) 411 412 def testNoBoards(self): 413 """Test that buildman aborts when there are no boards""" 414 self._boards = board.Boards() 415 with self.assertRaises(SystemExit): 416 self._RunControl() 417 418 def testCurrentSource(self): 419 """Very simple test to invoke buildman on the current source""" 420 self.setupToolchains(); 421 self._RunControl() 422 lines = terminal.GetPrintTestLines() 423 self.assertIn('Building current source for %d boards' % len(boards), 424 lines[0].text) 425 426 def testBadBranch(self): 427 """Test that we can detect an invalid branch""" 428 with self.assertRaises(ValueError): 429 self._RunControl('-b', 'badbranch') 430 431 def testBadToolchain(self): 432 """Test that missing toolchains are detected""" 433 self.setupToolchains(); 434 ret_code = self._RunControl('-b', TEST_BRANCH) 435 lines = terminal.GetPrintTestLines() 436 437 # Buildman always builds the upstream commit as well 438 self.assertIn('Building %d commits for %d boards' % 439 (self._commits, len(boards)), lines[0].text) 440 self.assertEqual(self._builder.count, self._total_builds) 441 442 # Only sandbox should succeed, the others don't have toolchains 443 self.assertEqual(self._builder.fail, 444 self._total_builds - self._commits) 445 self.assertEqual(ret_code, 128) 446 447 for commit in range(self._commits): 448 for board in self._boards.GetList(): 449 if board.arch != 'sandbox': 450 errfile = self._builder.GetErrFile(commit, board.target) 451 fd = open(errfile) 452 self.assertEqual(fd.readlines(), 453 ['No tool chain for %s\n' % board.arch]) 454 fd.close() 455 456 def testBranch(self): 457 """Test building a branch with all toolchains present""" 458 self._RunControl('-b', TEST_BRANCH) 459 self.assertEqual(self._builder.count, self._total_builds) 460 self.assertEqual(self._builder.fail, 0) 461 462 def testCount(self): 463 """Test building a specific number of commitst""" 464 self._RunControl('-b', TEST_BRANCH, '-c2') 465 self.assertEqual(self._builder.count, 2 * len(boards)) 466 self.assertEqual(self._builder.fail, 0) 467 # Each board has a mrproper, config, and then one make per commit 468 self.assertEqual(self._make_calls, len(boards) * (2 + 2)) 469 470 def testIncremental(self): 471 """Test building a branch twice - the second time should do nothing""" 472 self._RunControl('-b', TEST_BRANCH) 473 474 # Each board has a mrproper, config, and then one make per commit 475 self.assertEqual(self._make_calls, len(boards) * (self._commits + 2)) 476 self._make_calls = 0 477 self._RunControl('-b', TEST_BRANCH, clean_dir=False) 478 self.assertEqual(self._make_calls, 0) 479 self.assertEqual(self._builder.count, self._total_builds) 480 self.assertEqual(self._builder.fail, 0) 481 482 def testForceBuild(self): 483 """The -f flag should force a rebuild""" 484 self._RunControl('-b', TEST_BRANCH) 485 self._make_calls = 0 486 self._RunControl('-b', TEST_BRANCH, '-f', clean_dir=False) 487 # Each board has a mrproper, config, and then one make per commit 488 self.assertEqual(self._make_calls, len(boards) * (self._commits + 2)) 489 490 def testForceReconfigure(self): 491 """The -f flag should force a rebuild""" 492 self._RunControl('-b', TEST_BRANCH, '-C') 493 # Each commit has a mrproper, config and make 494 self.assertEqual(self._make_calls, len(boards) * self._commits * 3) 495 496 def testErrors(self): 497 """Test handling of build errors""" 498 self._error['board2', 1] = 'fred\n' 499 self._RunControl('-b', TEST_BRANCH) 500 self.assertEqual(self._builder.count, self._total_builds) 501 self.assertEqual(self._builder.fail, 1) 502 503 # Remove the error. This should have no effect since the commit will 504 # not be rebuilt 505 del self._error['board2', 1] 506 self._make_calls = 0 507 self._RunControl('-b', TEST_BRANCH, clean_dir=False) 508 self.assertEqual(self._builder.count, self._total_builds) 509 self.assertEqual(self._make_calls, 0) 510 self.assertEqual(self._builder.fail, 1) 511 512 # Now use the -F flag to force rebuild of the bad commit 513 self._RunControl('-b', TEST_BRANCH, '-F', clean_dir=False) 514 self.assertEqual(self._builder.count, self._total_builds) 515 self.assertEqual(self._builder.fail, 0) 516 self.assertEqual(self._make_calls, 3) 517 518 def testBranchWithSlash(self): 519 """Test building a branch with a '/' in the name""" 520 self._test_branch = '/__dev/__testbranch' 521 self._RunControl('-b', self._test_branch, clean_dir=False) 522 self.assertEqual(self._builder.count, self._total_builds) 523 self.assertEqual(self._builder.fail, 0) 524