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