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