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