buildman: Add a flag for reproducible builds
[platform/kernel/u-boot.git] / tools / buildman / func_test.py
1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2014 Google, Inc
3 #
4
5 import os
6 import shutil
7 import sys
8 import tempfile
9 import unittest
10
11 from buildman import board
12 from buildman import boards
13 from buildman import bsettings
14 from buildman import cmdline
15 from buildman import control
16 from buildman import toolchain
17 from patman import command
18 from patman import gitutil
19 from patman import terminal
20 from patman import test_util
21 from patman import tools
22
23 settings_data = '''
24 # Buildman settings file
25 [global]
26
27 [toolchain]
28
29 [toolchain-alias]
30
31 [make-flags]
32 src=/home/sjg/c/src
33 chroot=/home/sjg/c/chroot
34 vboot=VBOOT_DEBUG=1 MAKEFLAGS_VBOOT=DEBUG=1 CFLAGS_EXTRA_VBOOT=-DUNROLL_LOOPS VBOOT_SOURCE=${src}/platform/vboot_reference
35 chromeos_coreboot=VBOOT=${chroot}/build/link/usr ${vboot}
36 chromeos_daisy=VBOOT=${chroot}/build/daisy/usr ${vboot}
37 chromeos_peach=VBOOT=${chroot}/build/peach_pit/usr ${vboot}
38 '''
39
40 BOARDS = [
41     ['Active', 'arm', 'armv7', '', 'Tester', 'ARM Board 1', 'board0',  ''],
42     ['Active', 'arm', 'armv7', '', 'Tester', 'ARM Board 2', 'board1', ''],
43     ['Active', 'powerpc', 'powerpc', '', 'Tester', 'PowerPC board 1', 'board2', ''],
44     ['Active', 'sandbox', 'sandbox', '', 'Tester', 'Sandbox board', 'board4', ''],
45 ]
46
47 commit_shortlog = """4aca821 patman: Avoid changing the order of tags
48 39403bb patman: Use --no-pager' to stop git from forking a pager
49 db6e6f2 patman: Remove the -a option
50 f2ccf03 patman: Correct unit tests to run correctly
51 1d097f9 patman: Fix indentation in terminal.py
52 d073747 patman: Support the 'reverse' option for 'git log
53 """
54
55 commit_log = ["""commit 7f6b8315d18f683c5181d0c3694818c1b2a20dcd
56 Author: Masahiro Yamada <yamada.m@jp.panasonic.com>
57 Date:   Fri Aug 22 19:12:41 2014 +0900
58
59     buildman: refactor help message
60
61     "buildman [options]" is displayed by default.
62
63     Append the rest of help messages to parser.usage
64     instead of replacing it.
65
66     Besides, "-b <branch>" is not mandatory since commit fea5858e.
67     Drop it from the usage.
68
69     Signed-off-by: Masahiro Yamada <yamada.m@jp.panasonic.com>
70 """,
71 """commit d0737479be6baf4db5e2cdbee123e96bc5ed0ba8
72 Author: Simon Glass <sjg@chromium.org>
73 Date:   Thu Aug 14 16:48:25 2014 -0600
74
75     patman: Support the 'reverse' option for 'git log'
76
77     This option is currently not supported, but needs to be, for buildman to
78     operate as expected.
79
80     Series-changes: 7
81     - Add new patch to fix the 'reverse' bug
82
83     Series-version: 8
84
85     Change-Id: I79078f792e8b390b8a1272a8023537821d45feda
86     Reported-by: York Sun <yorksun@freescale.com>
87     Signed-off-by: Simon Glass <sjg@chromium.org>
88
89 """,
90 """commit 1d097f9ab487c5019152fd47bda126839f3bf9fc
91 Author: Simon Glass <sjg@chromium.org>
92 Date:   Sat Aug 9 11:44:32 2014 -0600
93
94     patman: Fix indentation in terminal.py
95
96     This code came from a different project with 2-character indentation. Fix
97     it for U-Boot.
98
99     Series-changes: 6
100     - Add new patch to fix indentation in teminal.py
101
102     Change-Id: I5a74d2ebbb3cc12a665f5c725064009ac96e8a34
103     Signed-off-by: Simon Glass <sjg@chromium.org>
104
105 """,
106 """commit f2ccf03869d1e152c836515a3ceb83cdfe04a105
107 Author: Simon Glass <sjg@chromium.org>
108 Date:   Sat Aug 9 11:08:24 2014 -0600
109
110     patman: Correct unit tests to run correctly
111
112     It seems that doctest behaves differently now, and some of the unit tests
113     do not run. Adjust the tests to work correctly.
114
115      ./tools/patman/patman --test
116     <unittest.result.TestResult run=10 errors=0 failures=0>
117
118     Series-changes: 6
119     - Add new patch to fix patman unit tests
120
121     Change-Id: I3d2ca588f4933e1f9d6b1665a00e4ae58269ff3b
122
123 """,
124 """commit db6e6f2f9331c5a37647d6668768d4a40b8b0d1c
125 Author: Simon Glass <sjg@chromium.org>
126 Date:   Sat Aug 9 12:06:02 2014 -0600
127
128     patman: Remove the -a option
129
130     It seems that this is no longer needed, since checkpatch.pl will catch
131     whitespace problems in patches. Also the option is not widely used, so
132     it seems safe to just remove it.
133
134     Series-changes: 6
135     - Add new patch to remove patman's -a option
136
137     Suggested-by: Masahiro Yamada <yamada.m@jp.panasonic.com>
138     Change-Id: I5821a1c75154e532c46513486ca40b808de7e2cc
139
140 """,
141 """commit 39403bb4f838153028a6f21ca30bf100f3791133
142 Author: Simon Glass <sjg@chromium.org>
143 Date:   Thu Aug 14 21:50:52 2014 -0600
144
145     patman: Use --no-pager' to stop git from forking a pager
146
147 """,
148 """commit 4aca821e27e97925c039e69fd37375b09c6f129c
149 Author: Simon Glass <sjg@chromium.org>
150 Date:   Fri Aug 22 15:57:39 2014 -0600
151
152     patman: Avoid changing the order of tags
153
154     patman collects tags that it sees in the commit and places them nicely
155     sorted at the end of the patch. However, this is not really necessary and
156     in fact is apparently not desirable.
157
158     Series-changes: 9
159     - Add new patch to avoid changing the order of tags
160
161     Series-version: 9
162
163     Suggested-by: Masahiro Yamada <yamada.m@jp.panasonic.com>
164     Change-Id: Ib1518588c1a189ad5c3198aae76f8654aed8d0db
165 """]
166
167 TEST_BRANCH = '__testbranch'
168
169 class TestFunctional(unittest.TestCase):
170     """Functional test for buildman.
171
172     This aims to test from just below the invocation of buildman (parsing
173     of arguments) to 'make' and 'git' invocation. It is not a true
174     emd-to-end test, as it mocks git, make and the tool chain. But this
175     makes it easier to detect when the builder is doing the wrong thing,
176     since in many cases this test code will fail. For example, only a
177     very limited subset of 'git' arguments is supported - anything
178     unexpected will fail.
179     """
180     def setUp(self):
181         self._base_dir = tempfile.mkdtemp()
182         self._output_dir = tempfile.mkdtemp()
183         self._git_dir = os.path.join(self._base_dir, 'src')
184         self._buildman_pathname = sys.argv[0]
185         self._buildman_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
186         command.test_result = self._HandleCommand
187         bsettings.Setup(None)
188         bsettings.AddFile(settings_data)
189         self.setupToolchains()
190         self._toolchains.Add('arm-gcc', test=False)
191         self._toolchains.Add('powerpc-gcc', test=False)
192         self._boards = boards.Boards()
193         for brd in BOARDS:
194             self._boards.add_board(board.Board(*brd))
195
196         # Directories where the source been cloned
197         self._clone_dirs = []
198         self._commits = len(commit_shortlog.splitlines()) + 1
199         self._total_builds = self._commits * len(BOARDS)
200
201         # Number of calls to make
202         self._make_calls = 0
203
204         # Map of [board, commit] to error messages
205         self._error = {}
206
207         self._test_branch = TEST_BRANCH
208
209         # Set to True to report missing blobs
210         self._missing = False
211
212         # Avoid sending any output and clear all terminal output
213         terminal.set_print_test_mode()
214         terminal.get_print_test_lines()
215
216     def tearDown(self):
217         shutil.rmtree(self._base_dir)
218         shutil.rmtree(self._output_dir)
219
220     def setupToolchains(self):
221         self._toolchains = toolchain.Toolchains()
222         self._toolchains.Add('gcc', test=False)
223
224     def _RunBuildman(self, *args):
225         return command.run_pipe([[self._buildman_pathname] + list(args)],
226                 capture=True, capture_stderr=True)
227
228     def _RunControl(self, *args, brds=None, clean_dir=False,
229                     test_thread_exceptions=False):
230         """Run buildman
231
232         Args:
233             args: List of arguments to pass
234             brds: Boards object
235             clean_dir: Used for tests only, indicates that the existing output_dir
236                 should be removed before starting the build
237             test_thread_exceptions: Uses for tests only, True to make the threads
238                 raise an exception instead of reporting their result. This simulates
239                 a failure in the code somewhere
240
241         Returns:
242             result code from buildman
243         """
244         sys.argv = [sys.argv[0]] + list(args)
245         options, args = cmdline.ParseArgs()
246         result = control.DoBuildman(options, args, toolchains=self._toolchains,
247                 make_func=self._HandleMake, brds=brds or self._boards,
248                 clean_dir=clean_dir,
249                 test_thread_exceptions=test_thread_exceptions)
250         self._builder = control.builder
251         return result
252
253     def testFullHelp(self):
254         command.test_result = None
255         result = self._RunBuildman('-H')
256         help_file = os.path.join(self._buildman_dir, 'README.rst')
257         # Remove possible extraneous strings
258         extra = '::::::::::::::\n' + help_file + '\n::::::::::::::\n'
259         gothelp = result.stdout.replace(extra, '')
260         self.assertEqual(len(gothelp), os.path.getsize(help_file))
261         self.assertEqual(0, len(result.stderr))
262         self.assertEqual(0, result.return_code)
263
264     def testHelp(self):
265         command.test_result = None
266         result = self._RunBuildman('-h')
267         help_file = os.path.join(self._buildman_dir, 'README.rst')
268         self.assertTrue(len(result.stdout) > 1000)
269         self.assertEqual(0, len(result.stderr))
270         self.assertEqual(0, result.return_code)
271
272     def testGitSetup(self):
273         """Test gitutils.Setup(), from outside the module itself"""
274         command.test_result = command.CommandResult(return_code=1)
275         gitutil.setup()
276         self.assertEqual(gitutil.use_no_decorate, False)
277
278         command.test_result = command.CommandResult(return_code=0)
279         gitutil.setup()
280         self.assertEqual(gitutil.use_no_decorate, True)
281
282     def _HandleCommandGitLog(self, args):
283         if args[-1] == '--':
284             args = args[:-1]
285         if '-n0' in args:
286             return command.CommandResult(return_code=0)
287         elif args[-1] == 'upstream/master..%s' % self._test_branch:
288             return command.CommandResult(return_code=0, stdout=commit_shortlog)
289         elif args[:3] == ['--no-color', '--no-decorate', '--reverse']:
290             if args[-1] == self._test_branch:
291                 count = int(args[3][2:])
292                 return command.CommandResult(return_code=0,
293                                             stdout=''.join(commit_log[:count]))
294
295         # Not handled, so abort
296         print('git log', args)
297         sys.exit(1)
298
299     def _HandleCommandGitConfig(self, args):
300         config = args[0]
301         if config == 'sendemail.aliasesfile':
302             return command.CommandResult(return_code=0)
303         elif config.startswith('branch.badbranch'):
304             return command.CommandResult(return_code=1)
305         elif config == 'branch.%s.remote' % self._test_branch:
306             return command.CommandResult(return_code=0, stdout='upstream\n')
307         elif config == 'branch.%s.merge' % self._test_branch:
308             return command.CommandResult(return_code=0,
309                                          stdout='refs/heads/master\n')
310
311         # Not handled, so abort
312         print('git config', args)
313         sys.exit(1)
314
315     def _HandleCommandGit(self, in_args):
316         """Handle execution of a git command
317
318         This uses a hacked-up parser.
319
320         Args:
321             in_args: Arguments after 'git' from the command line
322         """
323         git_args = []           # Top-level arguments to git itself
324         sub_cmd = None          # Git sub-command selected
325         args = []               # Arguments to the git sub-command
326         for arg in in_args:
327             if sub_cmd:
328                 args.append(arg)
329             elif arg[0] == '-':
330                 git_args.append(arg)
331             else:
332                 if git_args and git_args[-1] in ['--git-dir', '--work-tree']:
333                     git_args.append(arg)
334                 else:
335                     sub_cmd = arg
336         if sub_cmd == 'config':
337             return self._HandleCommandGitConfig(args)
338         elif sub_cmd == 'log':
339             return self._HandleCommandGitLog(args)
340         elif sub_cmd == 'clone':
341             return command.CommandResult(return_code=0)
342         elif sub_cmd == 'checkout':
343             return command.CommandResult(return_code=0)
344         elif sub_cmd == 'worktree':
345             return command.CommandResult(return_code=0)
346
347         # Not handled, so abort
348         print('git', git_args, sub_cmd, args)
349         sys.exit(1)
350
351     def _HandleCommandNm(self, args):
352         return command.CommandResult(return_code=0)
353
354     def _HandleCommandObjdump(self, args):
355         return command.CommandResult(return_code=0)
356
357     def _HandleCommandObjcopy(self, args):
358         return command.CommandResult(return_code=0)
359
360     def _HandleCommandSize(self, args):
361         return command.CommandResult(return_code=0)
362
363     def _HandleCommand(self, **kwargs):
364         """Handle a command execution.
365
366         The command is in kwargs['pipe-list'], as a list of pipes, each a
367         list of commands. The command should be emulated as required for
368         testing purposes.
369
370         Returns:
371             A CommandResult object
372         """
373         pipe_list = kwargs['pipe_list']
374         wc = False
375         if len(pipe_list) != 1:
376             if pipe_list[1] == ['wc', '-l']:
377                 wc = True
378             else:
379                 print('invalid pipe', kwargs)
380                 sys.exit(1)
381         cmd = pipe_list[0][0]
382         args = pipe_list[0][1:]
383         result = None
384         if cmd == 'git':
385             result = self._HandleCommandGit(args)
386         elif cmd == './scripts/show-gnu-make':
387             return command.CommandResult(return_code=0, stdout='make')
388         elif cmd.endswith('nm'):
389             return self._HandleCommandNm(args)
390         elif cmd.endswith('objdump'):
391             return self._HandleCommandObjdump(args)
392         elif cmd.endswith('objcopy'):
393             return self._HandleCommandObjcopy(args)
394         elif cmd.endswith( 'size'):
395             return self._HandleCommandSize(args)
396
397         if not result:
398             # Not handled, so abort
399             print('unknown command', kwargs)
400             sys.exit(1)
401
402         if wc:
403             result.stdout = len(result.stdout.splitlines())
404         return result
405
406     def _HandleMake(self, commit, brd, stage, cwd, *args, **kwargs):
407         """Handle execution of 'make'
408
409         Args:
410             commit: Commit object that is being built
411             brd: Board object that is being built
412             stage: Stage that we are at (mrproper, config, build)
413             cwd: Directory where make should be run
414             args: Arguments to pass to make
415             kwargs: Arguments to pass to command.run_pipe()
416         """
417         self._make_calls += 1
418         out_dir = ''
419         for arg in args:
420             if arg.startswith('O='):
421                 out_dir = arg[2:]
422         if stage == 'mrproper':
423             return command.CommandResult(return_code=0)
424         elif stage == 'config':
425             fname = os.path.join(cwd or '', out_dir, '.config')
426             tools.write_file(fname, b'CONFIG_SOMETHING=1')
427             return command.CommandResult(return_code=0,
428                     combined='Test configuration complete')
429         elif stage == 'build':
430             stderr = ''
431             fname = os.path.join(cwd or '', out_dir, 'u-boot')
432             tools.write_file(fname, b'U-Boot')
433
434             # Handle missing blobs
435             if self._missing:
436                 if 'BINMAN_ALLOW_MISSING=1' in args:
437                     stderr = '''+Image 'main-section' is missing external blobs and is non-functional: intel-descriptor intel-ifwi intel-fsp-m intel-fsp-s intel-vbt
438 Image 'main-section' has faked external blobs and is non-functional: descriptor.bin fsp_m.bin fsp_s.bin vbt.bin
439
440 Some images are invalid'''
441                 else:
442                     stderr = "binman: Filename 'fsp.bin' not found in input path"
443             elif type(commit) is not str:
444                 stderr = self._error.get((brd.target, commit.sequence))
445
446             if stderr:
447                 return command.CommandResult(return_code=2, stderr=stderr)
448             return command.CommandResult(return_code=0)
449
450         # Not handled, so abort
451         print('make', stage)
452         sys.exit(1)
453
454     # Example function to print output lines
455     def print_lines(self, lines):
456         print(len(lines))
457         for line in lines:
458             print(line)
459         #self.print_lines(terminal.get_print_test_lines())
460
461     def testNoBoards(self):
462         """Test that buildman aborts when there are no boards"""
463         self._boards = boards.Boards()
464         with self.assertRaises(SystemExit):
465             self._RunControl()
466
467     def testCurrentSource(self):
468         """Very simple test to invoke buildman on the current source"""
469         self.setupToolchains();
470         self._RunControl('-o', self._output_dir)
471         lines = terminal.get_print_test_lines()
472         self.assertIn('Building current source for %d boards' % len(BOARDS),
473                       lines[0].text)
474
475     def testBadBranch(self):
476         """Test that we can detect an invalid branch"""
477         with self.assertRaises(ValueError):
478             self._RunControl('-b', 'badbranch')
479
480     def testBadToolchain(self):
481         """Test that missing toolchains are detected"""
482         self.setupToolchains();
483         ret_code = self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir)
484         lines = terminal.get_print_test_lines()
485
486         # Buildman always builds the upstream commit as well
487         self.assertIn('Building %d commits for %d boards' %
488                 (self._commits, len(BOARDS)), lines[0].text)
489         self.assertEqual(self._builder.count, self._total_builds)
490
491         # Only sandbox should succeed, the others don't have toolchains
492         self.assertEqual(self._builder.fail,
493                          self._total_builds - self._commits)
494         self.assertEqual(ret_code, 100)
495
496         for commit in range(self._commits):
497             for brd in self._boards.get_list():
498                 if brd.arch != 'sandbox':
499                   errfile = self._builder.GetErrFile(commit, brd.target)
500                   fd = open(errfile)
501                   self.assertEqual(fd.readlines(),
502                           ['No tool chain for %s\n' % brd.arch])
503                   fd.close()
504
505     def testBranch(self):
506         """Test building a branch with all toolchains present"""
507         self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir)
508         self.assertEqual(self._builder.count, self._total_builds)
509         self.assertEqual(self._builder.fail, 0)
510
511     def testCount(self):
512         """Test building a specific number of commitst"""
513         self._RunControl('-b', TEST_BRANCH, '-c2', '-o', self._output_dir)
514         self.assertEqual(self._builder.count, 2 * len(BOARDS))
515         self.assertEqual(self._builder.fail, 0)
516         # Each board has a config, and then one make per commit
517         self.assertEqual(self._make_calls, len(BOARDS) * (1 + 2))
518
519     def testIncremental(self):
520         """Test building a branch twice - the second time should do nothing"""
521         self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir)
522
523         # Each board has a mrproper, config, and then one make per commit
524         self.assertEqual(self._make_calls, len(BOARDS) * (self._commits + 1))
525         self._make_calls = 0
526         self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir, clean_dir=False)
527         self.assertEqual(self._make_calls, 0)
528         self.assertEqual(self._builder.count, self._total_builds)
529         self.assertEqual(self._builder.fail, 0)
530
531     def testForceBuild(self):
532         """The -f flag should force a rebuild"""
533         self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir)
534         self._make_calls = 0
535         self._RunControl('-b', TEST_BRANCH, '-f', '-o', self._output_dir, clean_dir=False)
536         # Each board has a config and one make per commit
537         self.assertEqual(self._make_calls, len(BOARDS) * (self._commits + 1))
538
539     def testForceReconfigure(self):
540         """The -f flag should force a rebuild"""
541         self._RunControl('-b', TEST_BRANCH, '-C', '-o', self._output_dir)
542         # Each commit has a config and make
543         self.assertEqual(self._make_calls, len(BOARDS) * self._commits * 2)
544
545     def testMrproper(self):
546         """The -f flag should force a rebuild"""
547         self._RunControl('-b', TEST_BRANCH, '-m', '-o', self._output_dir)
548         # Each board has a mkproper, config and then one make per commit
549         self.assertEqual(self._make_calls, len(BOARDS) * (self._commits + 2))
550
551     def testErrors(self):
552         """Test handling of build errors"""
553         self._error['board2', 1] = 'fred\n'
554         self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir)
555         self.assertEqual(self._builder.count, self._total_builds)
556         self.assertEqual(self._builder.fail, 1)
557
558         # Remove the error. This should have no effect since the commit will
559         # not be rebuilt
560         del self._error['board2', 1]
561         self._make_calls = 0
562         self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir, clean_dir=False)
563         self.assertEqual(self._builder.count, self._total_builds)
564         self.assertEqual(self._make_calls, 0)
565         self.assertEqual(self._builder.fail, 1)
566
567         # Now use the -F flag to force rebuild of the bad commit
568         self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir, '-F', clean_dir=False)
569         self.assertEqual(self._builder.count, self._total_builds)
570         self.assertEqual(self._builder.fail, 0)
571         self.assertEqual(self._make_calls, 2)
572
573     def testBranchWithSlash(self):
574         """Test building a branch with a '/' in the name"""
575         self._test_branch = '/__dev/__testbranch'
576         self._RunControl('-b', self._test_branch, clean_dir=False)
577         self.assertEqual(self._builder.count, self._total_builds)
578         self.assertEqual(self._builder.fail, 0)
579
580     def testEnvironment(self):
581         """Test that the done and environment files are written to out-env"""
582         self._RunControl('-o', self._output_dir)
583         board0_dir = os.path.join(self._output_dir, 'current', 'board0')
584         self.assertTrue(os.path.exists(os.path.join(board0_dir, 'done')))
585         self.assertTrue(os.path.exists(os.path.join(board0_dir, 'out-env')))
586
587     def testEnvironmentUnicode(self):
588         """Test there are no unicode errors when the env has non-ASCII chars"""
589         try:
590             varname = b'buildman_test_var'
591             os.environb[varname] = b'strange\x80chars'
592             self.assertEqual(0, self._RunControl('-o', self._output_dir))
593             board0_dir = os.path.join(self._output_dir, 'current', 'board0')
594             self.assertTrue(os.path.exists(os.path.join(board0_dir, 'done')))
595             self.assertTrue(os.path.exists(os.path.join(board0_dir, 'out-env')))
596         finally:
597             del os.environb[varname]
598
599     def testWorkInOutput(self):
600         """Test the -w option which should write directly to the output dir"""
601         board_list = boards.Boards()
602         board_list.add_board(board.Board(*BOARDS[0]))
603         self._RunControl('-o', self._output_dir, '-w', clean_dir=False,
604                          brds=board_list)
605         self.assertTrue(
606             os.path.exists(os.path.join(self._output_dir, 'u-boot')))
607         self.assertTrue(
608             os.path.exists(os.path.join(self._output_dir, 'done')))
609         self.assertTrue(
610             os.path.exists(os.path.join(self._output_dir, 'out-env')))
611
612     def testWorkInOutputFail(self):
613         """Test the -w option failures"""
614         with self.assertRaises(SystemExit) as e:
615             self._RunControl('-o', self._output_dir, '-w', clean_dir=False)
616         self.assertIn("single board", str(e.exception))
617         self.assertFalse(
618             os.path.exists(os.path.join(self._output_dir, 'u-boot')))
619
620         board_list = boards.Boards()
621         board_list.add_board(board.Board(*BOARDS[0]))
622         with self.assertRaises(SystemExit) as e:
623             self._RunControl('-b', self._test_branch, '-o', self._output_dir,
624                              '-w', clean_dir=False, brds=board_list)
625         self.assertIn("single commit", str(e.exception))
626
627         board_list = boards.Boards()
628         board_list.add_board(board.Board(*BOARDS[0]))
629         with self.assertRaises(SystemExit) as e:
630             self._RunControl('-w', clean_dir=False)
631         self.assertIn("specify -o", str(e.exception))
632
633     def testThreadExceptions(self):
634         """Test that exceptions in threads are reported"""
635         with test_util.capture_sys_output() as (stdout, stderr):
636             self.assertEqual(102, self._RunControl('-o', self._output_dir,
637                                                    test_thread_exceptions=True))
638         self.assertIn(
639             'Thread exception (use -T0 to run without threads): test exception',
640             stdout.getvalue())
641
642     def testBlobs(self):
643         """Test handling of missing blobs"""
644         self._missing = True
645
646         board0_dir = os.path.join(self._output_dir, 'current', 'board0')
647         errfile = os.path.join(board0_dir, 'err')
648         logfile = os.path.join(board0_dir, 'log')
649
650         # We expect failure when there are missing blobs
651         result = self._RunControl('board0', '-o', self._output_dir)
652         self.assertEqual(100, result)
653         self.assertTrue(os.path.exists(os.path.join(board0_dir, 'done')))
654         self.assertTrue(os.path.exists(errfile))
655         self.assertIn(b"Filename 'fsp.bin' not found in input path",
656                       tools.read_file(errfile))
657
658     def testBlobsAllowMissing(self):
659         """Allow missing blobs - still failure but a different exit code"""
660         self._missing = True
661         result = self._RunControl('board0', '-o', self._output_dir, '-M',
662                                   clean_dir=True)
663         self.assertEqual(101, result)
664         board0_dir = os.path.join(self._output_dir, 'current', 'board0')
665         errfile = os.path.join(board0_dir, 'err')
666         self.assertTrue(os.path.exists(errfile))
667         self.assertIn(b'Some images are invalid', tools.read_file(errfile))
668
669     def testBlobsWarning(self):
670         """Allow missing blobs and ignore warnings"""
671         self._missing = True
672         result = self._RunControl('board0', '-o', self._output_dir, '-MW')
673         self.assertEqual(0, result)
674         board0_dir = os.path.join(self._output_dir, 'current', 'board0')
675         errfile = os.path.join(board0_dir, 'err')
676         self.assertIn(b'Some images are invalid', tools.read_file(errfile))
677
678     def testBlobSettings(self):
679         """Test with no settings"""
680         self.assertEqual(False,
681                          control.get_allow_missing(False, False, 1, False))
682         self.assertEqual(True,
683                          control.get_allow_missing(True, False, 1, False))
684         self.assertEqual(False,
685                          control.get_allow_missing(True, True, 1, False))
686
687     def testBlobSettingsAlways(self):
688         """Test the 'always' policy"""
689         bsettings.SetItem('global', 'allow-missing', 'always')
690         self.assertEqual(True,
691                          control.get_allow_missing(False, False, 1, False))
692         self.assertEqual(False,
693                          control.get_allow_missing(False, True, 1, False))
694
695     def testBlobSettingsBranch(self):
696         """Test the 'branch' policy"""
697         bsettings.SetItem('global', 'allow-missing', 'branch')
698         self.assertEqual(False,
699                          control.get_allow_missing(False, False, 1, False))
700         self.assertEqual(True,
701                          control.get_allow_missing(False, False, 1, True))
702         self.assertEqual(False,
703                          control.get_allow_missing(False, True, 1, True))
704
705     def testBlobSettingsMultiple(self):
706         """Test the 'multiple' policy"""
707         bsettings.SetItem('global', 'allow-missing', 'multiple')
708         self.assertEqual(False,
709                          control.get_allow_missing(False, False, 1, False))
710         self.assertEqual(True,
711                          control.get_allow_missing(False, False, 2, False))
712         self.assertEqual(False,
713                          control.get_allow_missing(False, True, 2, False))
714
715     def testBlobSettingsBranchMultiple(self):
716         """Test the 'branch multiple' policy"""
717         bsettings.SetItem('global', 'allow-missing', 'branch multiple')
718         self.assertEqual(False,
719                          control.get_allow_missing(False, False, 1, False))
720         self.assertEqual(True,
721                          control.get_allow_missing(False, False, 1, True))
722         self.assertEqual(True,
723                          control.get_allow_missing(False, False, 2, False))
724         self.assertEqual(True,
725                          control.get_allow_missing(False, False, 2, True))
726         self.assertEqual(False,
727                          control.get_allow_missing(False, True, 2, True))
728
729     def check_command(self, *extra_args):
730         """Run a command with the extra arguments and return the commands used
731
732         Args:
733             extra_args (list of str): List of extra arguments
734
735         Returns:
736             list of str: Lines returned in the out-cmd file
737         """
738         self._RunControl('-o', self._output_dir, *extra_args)
739         board0_dir = os.path.join(self._output_dir, 'current', 'board0')
740         self.assertTrue(os.path.exists(os.path.join(board0_dir, 'done')))
741         cmd_fname = os.path.join(board0_dir, 'out-cmd')
742         self.assertTrue(os.path.exists(cmd_fname))
743         data = tools.read_file(cmd_fname)
744
745         config_fname = os.path.join(board0_dir, '.config')
746         self.assertTrue(os.path.exists(config_fname))
747         cfg_data = tools.read_file(config_fname)
748
749         return data.splitlines(), cfg_data
750
751     def testCmdFile(self):
752         """Test that the -cmd-out file is produced"""
753         lines = self.check_command()[0]
754         self.assertEqual(2, len(lines))
755         self.assertRegex(lines[0], b'make O=/.*board0_defconfig')
756         self.assertRegex(lines[0], b'make O=/.*-s.*')
757
758     def testNoLto(self):
759         """Test that the --no-lto flag works"""
760         lines = self.check_command('-L')[0]
761         self.assertIn(b'NO_LTO=1', lines[0])
762
763     def testReproducible(self):
764         """Test that the -r flag works"""
765         lines, cfg_data = self.check_command('-r')
766         self.assertIn(b'SOURCE_DATE_EPOCH=0', lines[0])
767
768         # We should see CONFIG_LOCALVERSION_AUTO unset
769         self.assertEqual(b'''CONFIG_SOMETHING=1
770 # CONFIG_LOCALVERSION_AUTO is not set
771 ''', cfg_data)
772
773         with test_util.capture_sys_output() as (stdout, stderr):
774             lines, cfg_data = self.check_command('-r', '-a', 'LOCALVERSION')
775         self.assertIn(b'SOURCE_DATE_EPOCH=0', lines[0])
776
777         # We should see CONFIG_LOCALVERSION_AUTO unset
778         self.assertEqual(b'''CONFIG_SOMETHING=1
779 CONFIG_LOCALVERSION=y
780 ''', cfg_data)
781         self.assertIn('Not dropping LOCALVERSION_AUTO', stdout.getvalue())