1# SPDX-License-Identifier: GPL-2.0+ 2# Copyright (c) 2014 Google, Inc 3# 4 5import os 6from pathlib import Path 7import shutil 8import sys 9import tempfile 10import time 11import unittest 12 13from buildman import board 14from buildman import boards 15from buildman import bsettings 16from buildman import cmdline 17from buildman import control 18from buildman import toolchain 19from patman import gitutil 20from u_boot_pylib import command 21from u_boot_pylib import terminal 22from u_boot_pylib import test_util 23from u_boot_pylib import tools 24 25settings_data = ''' 26# Buildman settings file 27[global] 28 29[toolchain] 30 31[toolchain-alias] 32 33[make-flags] 34src=/home/sjg/c/src 35chroot=/home/sjg/c/chroot 36vboot=VBOOT_DEBUG=1 MAKEFLAGS_VBOOT=DEBUG=1 CFLAGS_EXTRA_VBOOT=-DUNROLL_LOOPS VBOOT_SOURCE=${src}/platform/vboot_reference 37chromeos_coreboot=VBOOT=${chroot}/build/link/usr ${vboot} 38chromeos_daisy=VBOOT=${chroot}/build/daisy/usr ${vboot} 39chromeos_peach=VBOOT=${chroot}/build/peach_pit/usr ${vboot} 40''' 41 42BOARDS = [ 43 ['Active', 'arm', 'armv7', '', 'Tester', 'ARM Board 0', 'board0', ''], 44 ['Active', 'arm', 'armv7', '', 'Tester', 'ARM Board 1', 'board1', ''], 45 ['Active', 'powerpc', 'powerpc', '', 'Tester', 'PowerPC board 1', 'board2', ''], 46 ['Active', 'sandbox', 'sandbox', '', 'Tester', 'Sandbox board', 'board4', ''], 47] 48 49commit_shortlog = """4aca821 patman: Avoid changing the order of tags 5039403bb patman: Use --no-pager' to stop git from forking a pager 51db6e6f2 patman: Remove the -a option 52f2ccf03 patman: Correct unit tests to run correctly 531d097f9 patman: Fix indentation in terminal.py 54d073747 patman: Support the 'reverse' option for 'git log 55""" 56 57commit_log = ["""commit 7f6b8315d18f683c5181d0c3694818c1b2a20dcd 58Author: Masahiro Yamada <yamada.m@jp.panasonic.com> 59Date: Fri Aug 22 19:12:41 2014 +0900 60 61 buildman: refactor help message 62 63 "buildman [options]" is displayed by default. 64 65 Append the rest of help messages to parser.usage 66 instead of replacing it. 67 68 Besides, "-b <branch>" is not mandatory since commit fea5858e. 69 Drop it from the usage. 70 71 Signed-off-by: Masahiro Yamada <yamada.m@jp.panasonic.com> 72""", 73"""commit d0737479be6baf4db5e2cdbee123e96bc5ed0ba8 74Author: Simon Glass <sjg@chromium.org> 75Date: Thu Aug 14 16:48:25 2014 -0600 76 77 patman: Support the 'reverse' option for 'git log' 78 79 This option is currently not supported, but needs to be, for buildman to 80 operate as expected. 81 82 Series-changes: 7 83 - Add new patch to fix the 'reverse' bug 84 85 Series-version: 8 86 87 Change-Id: I79078f792e8b390b8a1272a8023537821d45feda 88 Reported-by: York Sun <yorksun@freescale.com> 89 Signed-off-by: Simon Glass <sjg@chromium.org> 90 91""", 92"""commit 1d097f9ab487c5019152fd47bda126839f3bf9fc 93Author: Simon Glass <sjg@chromium.org> 94Date: Sat Aug 9 11:44:32 2014 -0600 95 96 patman: Fix indentation in terminal.py 97 98 This code came from a different project with 2-character indentation. Fix 99 it for U-Boot. 100 101 Series-changes: 6 102 - Add new patch to fix indentation in teminal.py 103 104 Change-Id: I5a74d2ebbb3cc12a665f5c725064009ac96e8a34 105 Signed-off-by: Simon Glass <sjg@chromium.org> 106 107""", 108"""commit f2ccf03869d1e152c836515a3ceb83cdfe04a105 109Author: Simon Glass <sjg@chromium.org> 110Date: Sat Aug 9 11:08:24 2014 -0600 111 112 patman: Correct unit tests to run correctly 113 114 It seems that doctest behaves differently now, and some of the unit tests 115 do not run. Adjust the tests to work correctly. 116 117 ./tools/patman/patman --test 118 <unittest.result.TestResult run=10 errors=0 failures=0> 119 120 Series-changes: 6 121 - Add new patch to fix patman unit tests 122 123 Change-Id: I3d2ca588f4933e1f9d6b1665a00e4ae58269ff3b 124 125""", 126"""commit db6e6f2f9331c5a37647d6668768d4a40b8b0d1c 127Author: Simon Glass <sjg@chromium.org> 128Date: Sat Aug 9 12:06:02 2014 -0600 129 130 patman: Remove the -a option 131 132 It seems that this is no longer needed, since checkpatch.pl will catch 133 whitespace problems in patches. Also the option is not widely used, so 134 it seems safe to just remove it. 135 136 Series-changes: 6 137 - Add new patch to remove patman's -a option 138 139 Suggested-by: Masahiro Yamada <yamada.m@jp.panasonic.com> 140 Change-Id: I5821a1c75154e532c46513486ca40b808de7e2cc 141 142""", 143"""commit 39403bb4f838153028a6f21ca30bf100f3791133 144Author: Simon Glass <sjg@chromium.org> 145Date: Thu Aug 14 21:50:52 2014 -0600 146 147 patman: Use --no-pager' to stop git from forking a pager 148 149""", 150"""commit 4aca821e27e97925c039e69fd37375b09c6f129c 151Author: Simon Glass <sjg@chromium.org> 152Date: Fri Aug 22 15:57:39 2014 -0600 153 154 patman: Avoid changing the order of tags 155 156 patman collects tags that it sees in the commit and places them nicely 157 sorted at the end of the patch. However, this is not really necessary and 158 in fact is apparently not desirable. 159 160 Series-changes: 9 161 - Add new patch to avoid changing the order of tags 162 163 Series-version: 9 164 165 Suggested-by: Masahiro Yamada <yamada.m@jp.panasonic.com> 166 Change-Id: Ib1518588c1a189ad5c3198aae76f8654aed8d0db 167"""] 168 169TEST_BRANCH = '__testbranch' 170 171class TestFunctional(unittest.TestCase): 172 """Functional test for buildman. 173 174 This aims to test from just below the invocation of buildman (parsing 175 of arguments) to 'make' and 'git' invocation. It is not a true 176 emd-to-end test, as it mocks git, make and the tool chain. But this 177 makes it easier to detect when the builder is doing the wrong thing, 178 since in many cases this test code will fail. For example, only a 179 very limited subset of 'git' arguments is supported - anything 180 unexpected will fail. 181 """ 182 def setUp(self): 183 self._base_dir = tempfile.mkdtemp() 184 self._output_dir = tempfile.mkdtemp() 185 self._git_dir = os.path.join(self._base_dir, 'src') 186 self._buildman_pathname = sys.argv[0] 187 self._buildman_dir = os.path.dirname(os.path.realpath(sys.argv[0])) 188 command.test_result = self._HandleCommand 189 bsettings.setup(None) 190 bsettings.add_file(settings_data) 191 self.setupToolchains() 192 self._toolchains.Add('arm-gcc', test=False) 193 self._toolchains.Add('powerpc-gcc', test=False) 194 self._boards = boards.Boards() 195 for brd in BOARDS: 196 self._boards.add_board(board.Board(*brd)) 197 198 # Directories where the source been cloned 199 self._clone_dirs = [] 200 self._commits = len(commit_shortlog.splitlines()) + 1 201 self._total_builds = self._commits * len(BOARDS) 202 203 # Number of calls to make 204 self._make_calls = 0 205 206 # Map of [board, commit] to error messages 207 self._error = {} 208 209 self._test_branch = TEST_BRANCH 210 211 # Set to True to report missing blobs 212 self._missing = False 213 214 self._buildman_dir = os.path.dirname(os.path.realpath(sys.argv[0])) 215 self._test_dir = os.path.join(self._buildman_dir, 'test') 216 217 # Set up some fake source files 218 shutil.copytree(self._test_dir, self._git_dir) 219 220 # Avoid sending any output and clear all terminal output 221 terminal.set_print_test_mode() 222 terminal.get_print_test_lines() 223 224 def tearDown(self): 225 shutil.rmtree(self._base_dir) 226 shutil.rmtree(self._output_dir) 227 228 def setupToolchains(self): 229 self._toolchains = toolchain.Toolchains() 230 self._toolchains.Add('gcc', test=False) 231 232 def _RunBuildman(self, *args): 233 return command.run_pipe([[self._buildman_pathname] + list(args)], 234 capture=True, capture_stderr=True) 235 236 def _RunControl(self, *args, brds=False, clean_dir=False, 237 test_thread_exceptions=False, get_builder=True): 238 """Run buildman 239 240 Args: 241 args: List of arguments to pass 242 brds: Boards object, or False to pass self._boards, or None to pass 243 None 244 clean_dir: Used for tests only, indicates that the existing output_dir 245 should be removed before starting the build 246 test_thread_exceptions: Uses for tests only, True to make the threads 247 raise an exception instead of reporting their result. This simulates 248 a failure in the code somewhere 249 get_builder (bool): Set self._builder to the resulting builder 250 251 Returns: 252 result code from buildman 253 """ 254 sys.argv = [sys.argv[0]] + list(args) 255 args = cmdline.parse_args() 256 if brds == False: 257 brds = self._boards 258 result = control.do_buildman( 259 args, toolchains=self._toolchains, make_func=self._HandleMake, 260 brds=brds, clean_dir=clean_dir, 261 test_thread_exceptions=test_thread_exceptions) 262 if get_builder: 263 self._builder = control.TEST_BUILDER 264 return result 265 266 def testFullHelp(self): 267 command.test_result = None 268 result = self._RunBuildman('-H') 269 help_file = os.path.join(self._buildman_dir, 'README.rst') 270 # Remove possible extraneous strings 271 extra = '::::::::::::::\n' + help_file + '\n::::::::::::::\n' 272 gothelp = result.stdout.replace(extra, '') 273 self.assertEqual(len(gothelp), os.path.getsize(help_file)) 274 self.assertEqual(0, len(result.stderr)) 275 self.assertEqual(0, result.return_code) 276 277 def testHelp(self): 278 command.test_result = None 279 result = self._RunBuildman('-h') 280 help_file = os.path.join(self._buildman_dir, 'README.rst') 281 self.assertTrue(len(result.stdout) > 1000) 282 self.assertEqual(0, len(result.stderr)) 283 self.assertEqual(0, result.return_code) 284 285 def testGitSetup(self): 286 """Test gitutils.Setup(), from outside the module itself""" 287 command.test_result = command.CommandResult(return_code=1) 288 gitutil.setup() 289 self.assertEqual(gitutil.use_no_decorate, False) 290 291 command.test_result = command.CommandResult(return_code=0) 292 gitutil.setup() 293 self.assertEqual(gitutil.use_no_decorate, True) 294 295 def _HandleCommandGitLog(self, args): 296 if args[-1] == '--': 297 args = args[:-1] 298 if '-n0' in args: 299 return command.CommandResult(return_code=0) 300 elif args[-1] == 'upstream/master..%s' % self._test_branch: 301 return command.CommandResult(return_code=0, stdout=commit_shortlog) 302 elif args[:3] == ['--no-color', '--no-decorate', '--reverse']: 303 if args[-1] == self._test_branch: 304 count = int(args[3][2:]) 305 return command.CommandResult(return_code=0, 306 stdout=''.join(commit_log[:count])) 307 308 # Not handled, so abort 309 print('git log', args) 310 sys.exit(1) 311 312 def _HandleCommandGitConfig(self, args): 313 config = args[0] 314 if config == 'sendemail.aliasesfile': 315 return command.CommandResult(return_code=0) 316 elif config.startswith('branch.badbranch'): 317 return command.CommandResult(return_code=1) 318 elif config == 'branch.%s.remote' % self._test_branch: 319 return command.CommandResult(return_code=0, stdout='upstream\n') 320 elif config == 'branch.%s.merge' % self._test_branch: 321 return command.CommandResult(return_code=0, 322 stdout='refs/heads/master\n') 323 324 # Not handled, so abort 325 print('git config', args) 326 sys.exit(1) 327 328 def _HandleCommandGit(self, in_args): 329 """Handle execution of a git command 330 331 This uses a hacked-up parser. 332 333 Args: 334 in_args: Arguments after 'git' from the command line 335 """ 336 git_args = [] # Top-level arguments to git itself 337 sub_cmd = None # Git sub-command selected 338 args = [] # Arguments to the git sub-command 339 for arg in in_args: 340 if sub_cmd: 341 args.append(arg) 342 elif arg[0] == '-': 343 git_args.append(arg) 344 else: 345 if git_args and git_args[-1] in ['--git-dir', '--work-tree']: 346 git_args.append(arg) 347 else: 348 sub_cmd = arg 349 if sub_cmd == 'config': 350 return self._HandleCommandGitConfig(args) 351 elif sub_cmd == 'log': 352 return self._HandleCommandGitLog(args) 353 elif sub_cmd == 'clone': 354 return command.CommandResult(return_code=0) 355 elif sub_cmd == 'checkout': 356 return command.CommandResult(return_code=0) 357 elif sub_cmd == 'worktree': 358 return command.CommandResult(return_code=0) 359 360 # Not handled, so abort 361 print('git', git_args, sub_cmd, args) 362 sys.exit(1) 363 364 def _HandleCommandNm(self, args): 365 return command.CommandResult(return_code=0) 366 367 def _HandleCommandObjdump(self, args): 368 return command.CommandResult(return_code=0) 369 370 def _HandleCommandObjcopy(self, args): 371 return command.CommandResult(return_code=0) 372 373 def _HandleCommandSize(self, args): 374 return command.CommandResult(return_code=0) 375 376 def _HandleCommand(self, **kwargs): 377 """Handle a command execution. 378 379 The command is in kwargs['pipe-list'], as a list of pipes, each a 380 list of commands. The command should be emulated as required for 381 testing purposes. 382 383 Returns: 384 A CommandResult object 385 """ 386 pipe_list = kwargs['pipe_list'] 387 wc = False 388 if len(pipe_list) != 1: 389 if pipe_list[1] == ['wc', '-l']: 390 wc = True 391 else: 392 print('invalid pipe', kwargs) 393 sys.exit(1) 394 cmd = pipe_list[0][0] 395 args = pipe_list[0][1:] 396 result = None 397 if cmd == 'git': 398 result = self._HandleCommandGit(args) 399 elif cmd == './scripts/show-gnu-make': 400 return command.CommandResult(return_code=0, stdout='make') 401 elif cmd.endswith('nm'): 402 return self._HandleCommandNm(args) 403 elif cmd.endswith('objdump'): 404 return self._HandleCommandObjdump(args) 405 elif cmd.endswith('objcopy'): 406 return self._HandleCommandObjcopy(args) 407 elif cmd.endswith( 'size'): 408 return self._HandleCommandSize(args) 409 410 if not result: 411 # Not handled, so abort 412 print('unknown command', kwargs) 413 sys.exit(1) 414 415 if wc: 416 result.stdout = len(result.stdout.splitlines()) 417 return result 418 419 def _HandleMake(self, commit, brd, stage, cwd, *args, **kwargs): 420 """Handle execution of 'make' 421 422 Args: 423 commit: Commit object that is being built 424 brd: Board object that is being built 425 stage: Stage that we are at (mrproper, config, build) 426 cwd: Directory where make should be run 427 args: Arguments to pass to make 428 kwargs: Arguments to pass to command.run_pipe() 429 """ 430 self._make_calls += 1 431 out_dir = '' 432 for arg in args: 433 if arg.startswith('O='): 434 out_dir = arg[2:] 435 if stage == 'mrproper': 436 return command.CommandResult(return_code=0) 437 elif stage == 'config': 438 fname = os.path.join(cwd or '', out_dir, '.config') 439 tools.write_file(fname, b'CONFIG_SOMETHING=1') 440 return command.CommandResult(return_code=0, 441 combined='Test configuration complete') 442 elif stage == 'oldconfig': 443 return command.CommandResult(return_code=0) 444 elif stage == 'build': 445 stderr = '' 446 fname = os.path.join(cwd or '', out_dir, 'u-boot') 447 tools.write_file(fname, b'U-Boot') 448 449 # Handle missing blobs 450 if self._missing: 451 if 'BINMAN_ALLOW_MISSING=1' in args: 452 stderr = '''+Image 'main-section' is missing external blobs and is non-functional: intel-descriptor intel-ifwi intel-fsp-m intel-fsp-s intel-vbt 453Image 'main-section' has faked external blobs and is non-functional: descriptor.bin fsp_m.bin fsp_s.bin vbt.bin 454 455Some images are invalid''' 456 else: 457 stderr = "binman: Filename 'fsp.bin' not found in input path" 458 elif type(commit) is not str: 459 stderr = self._error.get((brd.target, commit.sequence)) 460 461 if stderr: 462 return command.CommandResult(return_code=2, stderr=stderr) 463 return command.CommandResult(return_code=0) 464 465 # Not handled, so abort 466 print('_HandleMake failure: make', stage) 467 sys.exit(1) 468 469 # Example function to print output lines 470 def print_lines(self, lines): 471 print(len(lines)) 472 for line in lines: 473 print(line) 474 #self.print_lines(terminal.get_print_test_lines()) 475 476 def testNoBoards(self): 477 """Test that buildman aborts when there are no boards""" 478 self._boards = boards.Boards() 479 with self.assertRaises(SystemExit): 480 self._RunControl() 481 482 def testCurrentSource(self): 483 """Very simple test to invoke buildman on the current source""" 484 self.setupToolchains(); 485 self._RunControl('-o', self._output_dir) 486 lines = terminal.get_print_test_lines() 487 self.assertIn('Building current source for %d boards' % len(BOARDS), 488 lines[0].text) 489 490 def testBadBranch(self): 491 """Test that we can detect an invalid branch""" 492 with self.assertRaises(ValueError): 493 self._RunControl('-b', 'badbranch') 494 495 def testBadToolchain(self): 496 """Test that missing toolchains are detected""" 497 self.setupToolchains(); 498 ret_code = self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir) 499 lines = terminal.get_print_test_lines() 500 501 # Buildman always builds the upstream commit as well 502 self.assertIn('Building %d commits for %d boards' % 503 (self._commits, len(BOARDS)), lines[0].text) 504 self.assertEqual(self._builder.count, self._total_builds) 505 506 # Only sandbox should succeed, the others don't have toolchains 507 self.assertEqual(self._builder.fail, 508 self._total_builds - self._commits) 509 self.assertEqual(ret_code, 100) 510 511 for commit in range(self._commits): 512 for brd in self._boards.get_list(): 513 if brd.arch != 'sandbox': 514 errfile = self._builder.get_err_file(commit, brd.target) 515 fd = open(errfile) 516 self.assertEqual( 517 fd.readlines(), 518 [f'Tool chain error for {brd.arch}: ' 519 f"No tool chain found for arch '{brd.arch}'"]) 520 fd.close() 521 522 def testBranch(self): 523 """Test building a branch with all toolchains present""" 524 self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir) 525 self.assertEqual(self._builder.count, self._total_builds) 526 self.assertEqual(self._builder.fail, 0) 527 528 def testCount(self): 529 """Test building a specific number of commitst""" 530 self._RunControl('-b', TEST_BRANCH, '-c2', '-o', self._output_dir) 531 self.assertEqual(self._builder.count, 2 * len(BOARDS)) 532 self.assertEqual(self._builder.fail, 0) 533 # Each board has a config, and then one make per commit 534 self.assertEqual(self._make_calls, len(BOARDS) * (1 + 2)) 535 536 def testIncremental(self): 537 """Test building a branch twice - the second time should do nothing""" 538 self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir) 539 540 # Each board has a mrproper, config, and then one make per commit 541 self.assertEqual(self._make_calls, len(BOARDS) * (self._commits + 1)) 542 self._make_calls = 0 543 self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir, clean_dir=False) 544 self.assertEqual(self._make_calls, 0) 545 self.assertEqual(self._builder.count, self._total_builds) 546 self.assertEqual(self._builder.fail, 0) 547 548 def testForceBuild(self): 549 """The -f flag should force a rebuild""" 550 self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir) 551 self._make_calls = 0 552 self._RunControl('-b', TEST_BRANCH, '-f', '-o', self._output_dir, clean_dir=False) 553 # Each board has a config and one make per commit 554 self.assertEqual(self._make_calls, len(BOARDS) * (self._commits + 1)) 555 556 def testForceReconfigure(self): 557 """The -f flag should force a rebuild""" 558 self._RunControl('-b', TEST_BRANCH, '-C', '-o', self._output_dir) 559 # Each commit has a config and make 560 self.assertEqual(self._make_calls, len(BOARDS) * self._commits * 2) 561 562 def testMrproper(self): 563 """The -f flag should force a rebuild""" 564 self._RunControl('-b', TEST_BRANCH, '-m', '-o', self._output_dir) 565 # Each board has a mkproper, config and then one make per commit 566 self.assertEqual(self._make_calls, len(BOARDS) * (self._commits + 2)) 567 568 def testErrors(self): 569 """Test handling of build errors""" 570 self._error['board2', 1] = 'fred\n' 571 self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir) 572 self.assertEqual(self._builder.count, self._total_builds) 573 self.assertEqual(self._builder.fail, 1) 574 575 # Remove the error. This should have no effect since the commit will 576 # not be rebuilt 577 del self._error['board2', 1] 578 self._make_calls = 0 579 self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir, clean_dir=False) 580 self.assertEqual(self._builder.count, self._total_builds) 581 self.assertEqual(self._make_calls, 0) 582 self.assertEqual(self._builder.fail, 1) 583 584 # Now use the -F flag to force rebuild of the bad commit 585 self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir, '-F', clean_dir=False) 586 self.assertEqual(self._builder.count, self._total_builds) 587 self.assertEqual(self._builder.fail, 0) 588 self.assertEqual(self._make_calls, 2) 589 590 def testBranchWithSlash(self): 591 """Test building a branch with a '/' in the name""" 592 self._test_branch = '/__dev/__testbranch' 593 self._RunControl('-b', self._test_branch, '-o', self._output_dir, 594 clean_dir=False) 595 self.assertEqual(self._builder.count, self._total_builds) 596 self.assertEqual(self._builder.fail, 0) 597 598 def testEnvironment(self): 599 """Test that the done and environment files are written to out-env""" 600 self._RunControl('-o', self._output_dir) 601 board0_dir = os.path.join(self._output_dir, 'current', 'board0') 602 self.assertTrue(os.path.exists(os.path.join(board0_dir, 'done'))) 603 self.assertTrue(os.path.exists(os.path.join(board0_dir, 'out-env'))) 604 605 def testEnvironmentUnicode(self): 606 """Test there are no unicode errors when the env has non-ASCII chars""" 607 try: 608 varname = b'buildman_test_var' 609 os.environb[varname] = b'strange\x80chars' 610 self.assertEqual(0, self._RunControl('-o', self._output_dir)) 611 board0_dir = os.path.join(self._output_dir, 'current', 'board0') 612 self.assertTrue(os.path.exists(os.path.join(board0_dir, 'done'))) 613 self.assertTrue(os.path.exists(os.path.join(board0_dir, 'out-env'))) 614 finally: 615 del os.environb[varname] 616 617 def testWorkInOutput(self): 618 """Test the -w option which should write directly to the output dir""" 619 board_list = boards.Boards() 620 board_list.add_board(board.Board(*BOARDS[0])) 621 self._RunControl('-o', self._output_dir, '-w', clean_dir=False, 622 brds=board_list) 623 self.assertTrue( 624 os.path.exists(os.path.join(self._output_dir, 'u-boot'))) 625 self.assertTrue( 626 os.path.exists(os.path.join(self._output_dir, 'done'))) 627 self.assertTrue( 628 os.path.exists(os.path.join(self._output_dir, 'out-env'))) 629 630 def testWorkInOutputFail(self): 631 """Test the -w option failures""" 632 with self.assertRaises(SystemExit) as e: 633 self._RunControl('-o', self._output_dir, '-w', clean_dir=False) 634 self.assertIn("single board", str(e.exception)) 635 self.assertFalse( 636 os.path.exists(os.path.join(self._output_dir, 'u-boot'))) 637 638 board_list = boards.Boards() 639 board_list.add_board(board.Board(*BOARDS[0])) 640 with self.assertRaises(SystemExit) as e: 641 self._RunControl('-b', self._test_branch, '-o', self._output_dir, 642 '-w', clean_dir=False, brds=board_list) 643 self.assertIn("single commit", str(e.exception)) 644 645 board_list = boards.Boards() 646 board_list.add_board(board.Board(*BOARDS[0])) 647 with self.assertRaises(SystemExit) as e: 648 self._RunControl('-w', clean_dir=False) 649 self.assertIn("specify -o", str(e.exception)) 650 651 def testThreadExceptions(self): 652 """Test that exceptions in threads are reported""" 653 with test_util.capture_sys_output() as (stdout, stderr): 654 self.assertEqual(102, self._RunControl('-o', self._output_dir, 655 test_thread_exceptions=True)) 656 self.assertIn( 657 'Thread exception (use -T0 to run without threads): test exception', 658 stdout.getvalue()) 659 660 def testBlobs(self): 661 """Test handling of missing blobs""" 662 self._missing = True 663 664 board0_dir = os.path.join(self._output_dir, 'current', 'board0') 665 errfile = os.path.join(board0_dir, 'err') 666 logfile = os.path.join(board0_dir, 'log') 667 668 # We expect failure when there are missing blobs 669 result = self._RunControl('board0', '-o', self._output_dir) 670 self.assertEqual(100, result) 671 self.assertTrue(os.path.exists(os.path.join(board0_dir, 'done'))) 672 self.assertTrue(os.path.exists(errfile)) 673 self.assertIn(b"Filename 'fsp.bin' not found in input path", 674 tools.read_file(errfile)) 675 676 def testBlobsAllowMissing(self): 677 """Allow missing blobs - still failure but a different exit code""" 678 self._missing = True 679 result = self._RunControl('board0', '-o', self._output_dir, '-M', 680 clean_dir=True) 681 self.assertEqual(101, result) 682 board0_dir = os.path.join(self._output_dir, 'current', 'board0') 683 errfile = os.path.join(board0_dir, 'err') 684 self.assertTrue(os.path.exists(errfile)) 685 self.assertIn(b'Some images are invalid', tools.read_file(errfile)) 686 687 def testBlobsWarning(self): 688 """Allow missing blobs and ignore warnings""" 689 self._missing = True 690 result = self._RunControl('board0', '-o', self._output_dir, '-MW') 691 self.assertEqual(0, result) 692 board0_dir = os.path.join(self._output_dir, 'current', 'board0') 693 errfile = os.path.join(board0_dir, 'err') 694 self.assertIn(b'Some images are invalid', tools.read_file(errfile)) 695 696 def testBlobSettings(self): 697 """Test with no settings""" 698 self.assertEqual(False, 699 control.get_allow_missing(False, False, 1, False)) 700 self.assertEqual(True, 701 control.get_allow_missing(True, False, 1, False)) 702 self.assertEqual(False, 703 control.get_allow_missing(True, True, 1, False)) 704 705 def testBlobSettingsAlways(self): 706 """Test the 'always' policy""" 707 bsettings.set_item('global', 'allow-missing', 'always') 708 self.assertEqual(True, 709 control.get_allow_missing(False, False, 1, False)) 710 self.assertEqual(False, 711 control.get_allow_missing(False, True, 1, False)) 712 713 def testBlobSettingsBranch(self): 714 """Test the 'branch' policy""" 715 bsettings.set_item('global', 'allow-missing', 'branch') 716 self.assertEqual(False, 717 control.get_allow_missing(False, False, 1, False)) 718 self.assertEqual(True, 719 control.get_allow_missing(False, False, 1, True)) 720 self.assertEqual(False, 721 control.get_allow_missing(False, True, 1, True)) 722 723 def testBlobSettingsMultiple(self): 724 """Test the 'multiple' policy""" 725 bsettings.set_item('global', 'allow-missing', 'multiple') 726 self.assertEqual(False, 727 control.get_allow_missing(False, False, 1, False)) 728 self.assertEqual(True, 729 control.get_allow_missing(False, False, 2, False)) 730 self.assertEqual(False, 731 control.get_allow_missing(False, True, 2, False)) 732 733 def testBlobSettingsBranchMultiple(self): 734 """Test the 'branch multiple' policy""" 735 bsettings.set_item('global', 'allow-missing', 'branch multiple') 736 self.assertEqual(False, 737 control.get_allow_missing(False, False, 1, False)) 738 self.assertEqual(True, 739 control.get_allow_missing(False, False, 1, True)) 740 self.assertEqual(True, 741 control.get_allow_missing(False, False, 2, False)) 742 self.assertEqual(True, 743 control.get_allow_missing(False, False, 2, True)) 744 self.assertEqual(False, 745 control.get_allow_missing(False, True, 2, True)) 746 747 def check_command(self, *extra_args): 748 """Run a command with the extra arguments and return the commands used 749 750 Args: 751 extra_args (list of str): List of extra arguments 752 753 Returns: 754 list of str: Lines returned in the out-cmd file 755 """ 756 self._RunControl('-o', self._output_dir, *extra_args) 757 board0_dir = os.path.join(self._output_dir, 'current', 'board0') 758 self.assertTrue(os.path.exists(os.path.join(board0_dir, 'done'))) 759 cmd_fname = os.path.join(board0_dir, 'out-cmd') 760 self.assertTrue(os.path.exists(cmd_fname)) 761 data = tools.read_file(cmd_fname) 762 763 config_fname = os.path.join(board0_dir, '.config') 764 self.assertTrue(os.path.exists(config_fname)) 765 cfg_data = tools.read_file(config_fname) 766 767 return data.splitlines(), cfg_data 768 769 def testCmdFile(self): 770 """Test that the -cmd-out file is produced""" 771 lines = self.check_command()[0] 772 self.assertEqual(2, len(lines)) 773 self.assertRegex(lines[0], b'make O=/.*board0_defconfig') 774 self.assertRegex(lines[0], b'make O=/.*-s.*') 775 776 def testNoLto(self): 777 """Test that the --no-lto flag works""" 778 lines = self.check_command('-L')[0] 779 self.assertIn(b'NO_LTO=1', lines[0]) 780 781 def testReproducible(self): 782 """Test that the -r flag works""" 783 lines, cfg_data = self.check_command('-r') 784 self.assertIn(b'SOURCE_DATE_EPOCH=0', lines[0]) 785 786 # We should see CONFIG_LOCALVERSION_AUTO unset 787 self.assertEqual(b'''CONFIG_SOMETHING=1 788# CONFIG_LOCALVERSION_AUTO is not set 789''', cfg_data) 790 791 with test_util.capture_sys_output() as (stdout, stderr): 792 lines, cfg_data = self.check_command('-r', '-a', 'LOCALVERSION') 793 self.assertIn(b'SOURCE_DATE_EPOCH=0', lines[0]) 794 795 # We should see CONFIG_LOCALVERSION_AUTO unset 796 self.assertEqual(b'''CONFIG_SOMETHING=1 797CONFIG_LOCALVERSION=y 798''', cfg_data) 799 self.assertIn('Not dropping LOCALVERSION_AUTO', stdout.getvalue()) 800 801 def test_scan_defconfigs(self): 802 """Test scanning the defconfigs to obtain all the boards""" 803 src = self._git_dir 804 805 # Scan the test directory which contains a Kconfig and some *_defconfig 806 # files 807 params, warnings = self._boards.scan_defconfigs(src, src) 808 809 # We should get two boards 810 self.assertEquals(2, len(params)) 811 self.assertFalse(warnings) 812 first = 0 if params[0]['target'] == 'board0' else 1 813 board0 = params[first] 814 board2 = params[1 - first] 815 816 self.assertEquals('arm', board0['arch']) 817 self.assertEquals('armv7', board0['cpu']) 818 self.assertEquals('-', board0['soc']) 819 self.assertEquals('Tester', board0['vendor']) 820 self.assertEquals('ARM Board 0', board0['board']) 821 self.assertEquals('config0', board0['config']) 822 self.assertEquals('board0', board0['target']) 823 824 self.assertEquals('powerpc', board2['arch']) 825 self.assertEquals('ppc', board2['cpu']) 826 self.assertEquals('mpc85xx', board2['soc']) 827 self.assertEquals('Tester', board2['vendor']) 828 self.assertEquals('PowerPC board 1', board2['board']) 829 self.assertEquals('config2', board2['config']) 830 self.assertEquals('board2', board2['target']) 831 832 def test_output_is_new(self): 833 """Test detecting new changes to Kconfig""" 834 base = self._base_dir 835 src = self._git_dir 836 config_dir = os.path.join(src, 'configs') 837 delay = 0.02 838 839 # Create a boards.cfg file 840 boards_cfg = os.path.join(base, 'boards.cfg') 841 content = b'''# 842# List of boards 843# Automatically generated by buildman/boards.py: don't edit 844# 845# Status, Arch, CPU, SoC, Vendor, Board, Target, Config, Maintainers 846 847Active aarch64 armv8 - armltd corstone1000 board0 848Active aarch64 armv8 - armltd total_compute board2 849''' 850 # Check missing file 851 self.assertFalse(boards.output_is_new(boards_cfg, config_dir, src)) 852 853 # Check that the board.cfg file is newer 854 time.sleep(delay) 855 tools.write_file(boards_cfg, content) 856 self.assertTrue(boards.output_is_new(boards_cfg, config_dir, src)) 857 858 # Touch the Kconfig files after a show delay to avoid a race 859 time.sleep(delay) 860 Path(os.path.join(src, 'Kconfig')).touch() 861 self.assertFalse(boards.output_is_new(boards_cfg, config_dir, src)) 862 Path(boards_cfg).touch() 863 self.assertTrue(boards.output_is_new(boards_cfg, config_dir, src)) 864 865 # Touch a different Kconfig file 866 time.sleep(delay) 867 Path(os.path.join(src, 'Kconfig.something')).touch() 868 self.assertFalse(boards.output_is_new(boards_cfg, config_dir, src)) 869 Path(boards_cfg).touch() 870 self.assertTrue(boards.output_is_new(boards_cfg, config_dir, src)) 871 872 # Touch a MAINTAINERS file 873 time.sleep(delay) 874 Path(os.path.join(src, 'MAINTAINERS')).touch() 875 self.assertFalse(boards.output_is_new(boards_cfg, config_dir, src)) 876 877 Path(boards_cfg).touch() 878 self.assertTrue(boards.output_is_new(boards_cfg, config_dir, src)) 879 880 # Touch a defconfig file 881 time.sleep(delay) 882 Path(os.path.join(config_dir, 'board0_defconfig')).touch() 883 self.assertFalse(boards.output_is_new(boards_cfg, config_dir, src)) 884 Path(boards_cfg).touch() 885 self.assertTrue(boards.output_is_new(boards_cfg, config_dir, src)) 886 887 # Remove a board and check that the board.cfg file is now older 888 Path(os.path.join(config_dir, 'board0_defconfig')).unlink() 889 self.assertFalse(boards.output_is_new(boards_cfg, config_dir, src)) 890 891 def test_maintainers(self): 892 """Test detecting boards without a MAINTAINERS entry""" 893 src = self._git_dir 894 main = os.path.join(src, 'boards', 'board0', 'MAINTAINERS') 895 other = os.path.join(src, 'boards', 'board2', 'MAINTAINERS') 896 kc_file = os.path.join(src, 'Kconfig') 897 config_dir = os.path.join(src, 'configs') 898 params_list, warnings = self._boards.build_board_list(config_dir, src) 899 900 # There should be two boards no warnings 901 self.assertEquals(2, len(params_list)) 902 self.assertFalse(warnings) 903 904 # Set an invalid status line in the file 905 orig_data = tools.read_file(main, binary=False) 906 lines = ['S: Other\n' if line.startswith('S:') else line 907 for line in orig_data.splitlines(keepends=True)] 908 tools.write_file(main, ''.join(lines), binary=False) 909 params_list, warnings = self._boards.build_board_list(config_dir, src) 910 self.assertEquals(2, len(params_list)) 911 params = params_list[0] 912 if params['target'] == 'board2': 913 params = params_list[1] 914 self.assertEquals('-', params['status']) 915 self.assertEquals(["WARNING: Other: unknown status for 'board0'"], 916 warnings) 917 918 # Remove the status line (S:) from a file 919 lines = [line for line in orig_data.splitlines(keepends=True) 920 if not line.startswith('S:')] 921 tools.write_file(main, ''.join(lines), binary=False) 922 params_list, warnings = self._boards.build_board_list(config_dir, src) 923 self.assertEquals(2, len(params_list)) 924 self.assertEquals(["WARNING: -: unknown status for 'board0'"], warnings) 925 926 # Remove the configs/ line (F:) from a file - this is the last line 927 data = ''.join(orig_data.splitlines(keepends=True)[:-1]) 928 tools.write_file(main, data, binary=False) 929 params_list, warnings = self._boards.build_board_list(config_dir, src) 930 self.assertEquals(2, len(params_list)) 931 self.assertEquals(["WARNING: no maintainers for 'board0'"], warnings) 932 933 # Mark a board as orphaned - this should give a warning 934 lines = ['S: Orphaned' if line.startswith('S') else line 935 for line in orig_data.splitlines(keepends=True)] 936 tools.write_file(main, ''.join(lines), binary=False) 937 params_list, warnings = self._boards.build_board_list(config_dir, src) 938 self.assertEquals(2, len(params_list)) 939 self.assertEquals(["WARNING: no maintainers for 'board0'"], warnings) 940 941 # Change the maintainer to '-' - this should give a warning 942 lines = ['M: -' if line.startswith('M') else line 943 for line in orig_data.splitlines(keepends=True)] 944 tools.write_file(main, ''.join(lines), binary=False) 945 params_list, warnings = self._boards.build_board_list(config_dir, src) 946 self.assertEquals(2, len(params_list)) 947 self.assertEquals(["WARNING: -: unknown status for 'board0'"], warnings) 948 949 # Remove the maintainer line (M:) from a file 950 lines = [line for line in orig_data.splitlines(keepends=True) 951 if not line.startswith('M:')] 952 tools.write_file(main, ''.join(lines), binary=False) 953 params_list, warnings = self._boards.build_board_list(config_dir, src) 954 self.assertEquals(2, len(params_list)) 955 self.assertEquals(["WARNING: no maintainers for 'board0'"], warnings) 956 957 # Move the contents of the second file into this one, removing the 958 # second file, to check multiple records in a single file. 959 both_data = orig_data + tools.read_file(other, binary=False) 960 tools.write_file(main, both_data, binary=False) 961 os.remove(other) 962 params_list, warnings = self._boards.build_board_list(config_dir, src) 963 self.assertEquals(2, len(params_list)) 964 self.assertFalse(warnings) 965 966 # Add another record, this should be ignored with a warning 967 extra = '\n\nAnother\nM: Fred\nF: configs/board9_defconfig\nS: other\n' 968 tools.write_file(main, both_data + extra, binary=False) 969 params_list, warnings = self._boards.build_board_list(config_dir, src) 970 self.assertEquals(2, len(params_list)) 971 self.assertFalse(warnings) 972 973 # Add another TARGET to the Kconfig 974 tools.write_file(main, both_data, binary=False) 975 orig_kc_data = tools.read_file(kc_file) 976 extra = (b''' 977if TARGET_BOARD2 978config TARGET_OTHER 979\tbool "other" 980\tdefault y 981endif 982''') 983 tools.write_file(kc_file, orig_kc_data + extra) 984 params_list, warnings = self._boards.build_board_list(config_dir, src, 985 warn_targets=True) 986 self.assertEquals(2, len(params_list)) 987 self.assertEquals( 988 ['WARNING: board2_defconfig: Duplicate TARGET_xxx: board2 and other'], 989 warnings) 990 991 # Remove the TARGET_BOARD0 Kconfig option 992 lines = [b'' if line == b'config TARGET_BOARD2\n' else line 993 for line in orig_kc_data.splitlines(keepends=True)] 994 tools.write_file(kc_file, b''.join(lines)) 995 params_list, warnings = self._boards.build_board_list(config_dir, src, 996 warn_targets=True) 997 self.assertEquals(2, len(params_list)) 998 self.assertEquals( 999 ['WARNING: board2_defconfig: No TARGET_BOARD2 enabled'], 1000 warnings) 1001 tools.write_file(kc_file, orig_kc_data) 1002 1003 # Replace the last F: line of board 2 with an N: line 1004 data = ''.join(both_data.splitlines(keepends=True)[:-1]) 1005 tools.write_file(main, data + 'N: oa.*2\n', binary=False) 1006 params_list, warnings = self._boards.build_board_list(config_dir, src) 1007 self.assertEquals(2, len(params_list)) 1008 self.assertFalse(warnings) 1009 1010 def testRegenBoards(self): 1011 """Test that we can regenerate the boards.cfg file""" 1012 outfile = os.path.join(self._output_dir, 'test-boards.cfg') 1013 if os.path.exists(outfile): 1014 os.remove(outfile) 1015 with test_util.capture_sys_output() as (stdout, stderr): 1016 result = self._RunControl('-R', outfile, brds=None, 1017 get_builder=False) 1018 self.assertTrue(os.path.exists(outfile)) 1019 1020 def test_print_prefix(self): 1021 """Test that we can print the toolchain prefix""" 1022 with test_util.capture_sys_output() as (stdout, stderr): 1023 result = self._RunControl('-A', 'board0') 1024 self.assertEqual('arm-\n', stdout.getvalue()) 1025 self.assertEqual('', stderr.getvalue()) 1026 1027 def test_exclude_one(self): 1028 """Test excluding a single board from an arch""" 1029 self._RunControl('arm', '-x', 'board1', '-o', self._output_dir) 1030 self.assertEqual(['board0'], 1031 [b.target for b in self._boards.get_selected()]) 1032 1033 def test_exclude_arch(self): 1034 """Test excluding an arch""" 1035 self._RunControl('-x', 'arm', '-o', self._output_dir) 1036 self.assertEqual(['board2', 'board4'], 1037 [b.target for b in self._boards.get_selected()]) 1038 1039 def test_exclude_comma(self): 1040 """Test excluding a comma-separated list of things""" 1041 self._RunControl('-x', 'arm,powerpc', '-o', self._output_dir) 1042 self.assertEqual(['board4'], 1043 [b.target for b in self._boards.get_selected()]) 1044 1045 def test_exclude_list(self): 1046 """Test excluding a list of things""" 1047 self._RunControl('-x', 'board2', '-x' 'board4', '-o', self._output_dir) 1048 self.assertEqual(['board0', 'board1'], 1049 [b.target for b in self._boards.get_selected()]) 1050 1051 def test_single_boards(self): 1052 """Test building single boards""" 1053 self._RunControl('--boards', 'board1', '-o', self._output_dir) 1054 self.assertEqual(1, self._builder.count) 1055 1056 self._RunControl('--boards', 'board1', '--boards', 'board2', 1057 '-o', self._output_dir) 1058 self.assertEqual(2, self._builder.count) 1059 1060 self._RunControl('--boards', 'board1,board2', '--boards', 'board4', 1061 '-o', self._output_dir) 1062 self.assertEqual(3, self._builder.count) 1063 1064 def test_print_arch(self): 1065 """Test that we can print the board architecture""" 1066 with test_util.capture_sys_output() as (stdout, stderr): 1067 result = self._RunControl('--print-arch', 'board0') 1068 self.assertEqual('arm\n', stdout.getvalue()) 1069 self.assertEqual('', stderr.getvalue()) 1070