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