buildman: Move to absolute imports
[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 bsettings
13 from buildman import cmdline
14 from buildman import control
15 from buildman import toolchain
16 import command
17 import gitutil
18 import terminal
19 import toolchain
20 import tools
21
22 settings_data = '''
23 # Buildman settings file
24
25 [toolchain]
26
27 [toolchain-alias]
28
29 [make-flags]
30 src=/home/sjg/c/src
31 chroot=/home/sjg/c/chroot
32 vboot=VBOOT_DEBUG=1 MAKEFLAGS_VBOOT=DEBUG=1 CFLAGS_EXTRA_VBOOT=-DUNROLL_LOOPS VBOOT_SOURCE=${src}/platform/vboot_reference
33 chromeos_coreboot=VBOOT=${chroot}/build/link/usr ${vboot}
34 chromeos_daisy=VBOOT=${chroot}/build/daisy/usr ${vboot}
35 chromeos_peach=VBOOT=${chroot}/build/peach_pit/usr ${vboot}
36 '''
37
38 boards = [
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
45 commit_shortlog = """4aca821 patman: Avoid changing the order of tags
46 39403bb patman: Use --no-pager' to stop git from forking a pager
47 db6e6f2 patman: Remove the -a option
48 f2ccf03 patman: Correct unit tests to run correctly
49 1d097f9 patman: Fix indentation in terminal.py
50 d073747 patman: Support the 'reverse' option for 'git log
51 """
52
53 commit_log = ["""commit 7f6b8315d18f683c5181d0c3694818c1b2a20dcd
54 Author: Masahiro Yamada <yamada.m@jp.panasonic.com>
55 Date:   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
70 Author: Simon Glass <sjg@chromium.org>
71 Date:   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
89 Author: Simon Glass <sjg@chromium.org>
90 Date:   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
105 Author: Simon Glass <sjg@chromium.org>
106 Date:   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
123 Author: Simon Glass <sjg@chromium.org>
124 Date:   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
140 Author: Simon Glass <sjg@chromium.org>
141 Date:   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
147 Author: Simon Glass <sjg@chromium.org>
148 Date:   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
165 TEST_BRANCH = '__testbranch'
166
167 class 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._output_dir = tempfile.mkdtemp()
181         self._git_dir = os.path.join(self._base_dir, 'src')
182         self._buildman_pathname = sys.argv[0]
183         self._buildman_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
184         command.test_result = self._HandleCommand
185         self.setupToolchains()
186         self._toolchains.Add('arm-gcc', test=False)
187         self._toolchains.Add('powerpc-gcc', test=False)
188         bsettings.Setup(None)
189         bsettings.AddFile(settings_data)
190         self._boards = board.Boards()
191         for brd in boards:
192             self._boards.AddBoard(board.Board(*brd))
193
194         # Directories where the source been cloned
195         self._clone_dirs = []
196         self._commits = len(commit_shortlog.splitlines()) + 1
197         self._total_builds = self._commits * len(boards)
198
199         # Number of calls to make
200         self._make_calls = 0
201
202         # Map of [board, commit] to error messages
203         self._error = {}
204
205         self._test_branch = TEST_BRANCH
206
207         # Avoid sending any output and clear all terminal output
208         terminal.SetPrintTestMode()
209         terminal.GetPrintTestLines()
210
211     def tearDown(self):
212         shutil.rmtree(self._base_dir)
213         #shutil.rmtree(self._output_dir)
214
215     def setupToolchains(self):
216         self._toolchains = toolchain.Toolchains()
217         self._toolchains.Add('gcc', test=False)
218
219     def _RunBuildman(self, *args):
220         return command.RunPipe([[self._buildman_pathname] + list(args)],
221                 capture=True, capture_stderr=True)
222
223     def _RunControl(self, *args, clean_dir=False, boards=None):
224         sys.argv = [sys.argv[0]] + list(args)
225         options, args = cmdline.ParseArgs()
226         result = control.DoBuildman(options, args, toolchains=self._toolchains,
227                 make_func=self._HandleMake, boards=boards or self._boards,
228                 clean_dir=clean_dir)
229         self._builder = control.builder
230         return result
231
232     def testFullHelp(self):
233         command.test_result = None
234         result = self._RunBuildman('-H')
235         help_file = os.path.join(self._buildman_dir, 'README')
236         # Remove possible extraneous strings
237         extra = '::::::::::::::\n' + help_file + '\n::::::::::::::\n'
238         gothelp = result.stdout.replace(extra, '')
239         self.assertEqual(len(gothelp), os.path.getsize(help_file))
240         self.assertEqual(0, len(result.stderr))
241         self.assertEqual(0, result.return_code)
242
243     def testHelp(self):
244         command.test_result = None
245         result = self._RunBuildman('-h')
246         help_file = os.path.join(self._buildman_dir, 'README')
247         self.assertTrue(len(result.stdout) > 1000)
248         self.assertEqual(0, len(result.stderr))
249         self.assertEqual(0, result.return_code)
250
251     def testGitSetup(self):
252         """Test gitutils.Setup(), from outside the module itself"""
253         command.test_result = command.CommandResult(return_code=1)
254         gitutil.Setup()
255         self.assertEqual(gitutil.use_no_decorate, False)
256
257         command.test_result = command.CommandResult(return_code=0)
258         gitutil.Setup()
259         self.assertEqual(gitutil.use_no_decorate, True)
260
261     def _HandleCommandGitLog(self, args):
262         if args[-1] == '--':
263             args = args[:-1]
264         if '-n0' in args:
265             return command.CommandResult(return_code=0)
266         elif args[-1] == 'upstream/master..%s' % self._test_branch:
267             return command.CommandResult(return_code=0, stdout=commit_shortlog)
268         elif args[:3] == ['--no-color', '--no-decorate', '--reverse']:
269             if args[-1] == self._test_branch:
270                 count = int(args[3][2:])
271                 return command.CommandResult(return_code=0,
272                                             stdout=''.join(commit_log[:count]))
273
274         # Not handled, so abort
275         print('git log', args)
276         sys.exit(1)
277
278     def _HandleCommandGitConfig(self, args):
279         config = args[0]
280         if config == 'sendemail.aliasesfile':
281             return command.CommandResult(return_code=0)
282         elif config.startswith('branch.badbranch'):
283             return command.CommandResult(return_code=1)
284         elif config == 'branch.%s.remote' % self._test_branch:
285             return command.CommandResult(return_code=0, stdout='upstream\n')
286         elif config == 'branch.%s.merge' % self._test_branch:
287             return command.CommandResult(return_code=0,
288                                          stdout='refs/heads/master\n')
289
290         # Not handled, so abort
291         print('git config', args)
292         sys.exit(1)
293
294     def _HandleCommandGit(self, in_args):
295         """Handle execution of a git command
296
297         This uses a hacked-up parser.
298
299         Args:
300             in_args: Arguments after 'git' from the command line
301         """
302         git_args = []           # Top-level arguments to git itself
303         sub_cmd = None          # Git sub-command selected
304         args = []               # Arguments to the git sub-command
305         for arg in in_args:
306             if sub_cmd:
307                 args.append(arg)
308             elif arg[0] == '-':
309                 git_args.append(arg)
310             else:
311                 if git_args and git_args[-1] in ['--git-dir', '--work-tree']:
312                     git_args.append(arg)
313                 else:
314                     sub_cmd = arg
315         if sub_cmd == 'config':
316             return self._HandleCommandGitConfig(args)
317         elif sub_cmd == 'log':
318             return self._HandleCommandGitLog(args)
319         elif sub_cmd == 'clone':
320             return command.CommandResult(return_code=0)
321         elif sub_cmd == 'checkout':
322             return command.CommandResult(return_code=0)
323
324         # Not handled, so abort
325         print('git', git_args, sub_cmd, args)
326         sys.exit(1)
327
328     def _HandleCommandNm(self, args):
329         return command.CommandResult(return_code=0)
330
331     def _HandleCommandObjdump(self, args):
332         return command.CommandResult(return_code=0)
333
334     def _HandleCommandObjcopy(self, args):
335         return command.CommandResult(return_code=0)
336
337     def _HandleCommandSize(self, args):
338         return command.CommandResult(return_code=0)
339
340     def _HandleCommand(self, **kwargs):
341         """Handle a command execution.
342
343         The command is in kwargs['pipe-list'], as a list of pipes, each a
344         list of commands. The command should be emulated as required for
345         testing purposes.
346
347         Returns:
348             A CommandResult object
349         """
350         pipe_list = kwargs['pipe_list']
351         wc = False
352         if len(pipe_list) != 1:
353             if pipe_list[1] == ['wc', '-l']:
354                 wc = True
355             else:
356                 print('invalid pipe', kwargs)
357                 sys.exit(1)
358         cmd = pipe_list[0][0]
359         args = pipe_list[0][1:]
360         result = None
361         if cmd == 'git':
362             result = self._HandleCommandGit(args)
363         elif cmd == './scripts/show-gnu-make':
364             return command.CommandResult(return_code=0, stdout='make')
365         elif cmd.endswith('nm'):
366             return self._HandleCommandNm(args)
367         elif cmd.endswith('objdump'):
368             return self._HandleCommandObjdump(args)
369         elif cmd.endswith('objcopy'):
370             return self._HandleCommandObjcopy(args)
371         elif cmd.endswith( 'size'):
372             return self._HandleCommandSize(args)
373
374         if not result:
375             # Not handled, so abort
376             print('unknown command', kwargs)
377             sys.exit(1)
378
379         if wc:
380             result.stdout = len(result.stdout.splitlines())
381         return result
382
383     def _HandleMake(self, commit, brd, stage, cwd, *args, **kwargs):
384         """Handle execution of 'make'
385
386         Args:
387             commit: Commit object that is being built
388             brd: Board object that is being built
389             stage: Stage that we are at (mrproper, config, build)
390             cwd: Directory where make should be run
391             args: Arguments to pass to make
392             kwargs: Arguments to pass to command.RunPipe()
393         """
394         self._make_calls += 1
395         if stage == 'mrproper':
396             return command.CommandResult(return_code=0)
397         elif stage == 'config':
398             return command.CommandResult(return_code=0,
399                     combined='Test configuration complete')
400         elif stage == 'build':
401             stderr = ''
402             out_dir = ''
403             for arg in args:
404                 if arg.startswith('O='):
405                     out_dir = arg[2:]
406             fname = os.path.join(cwd or '', out_dir, 'u-boot')
407             tools.WriteFile(fname, b'U-Boot')
408             if type(commit) is not str:
409                 stderr = self._error.get((brd.target, commit.sequence))
410             if stderr:
411                 return command.CommandResult(return_code=1, stderr=stderr)
412             return command.CommandResult(return_code=0)
413
414         # Not handled, so abort
415         print('make', stage)
416         sys.exit(1)
417
418     # Example function to print output lines
419     def print_lines(self, lines):
420         print(len(lines))
421         for line in lines:
422             print(line)
423         #self.print_lines(terminal.GetPrintTestLines())
424
425     def testNoBoards(self):
426         """Test that buildman aborts when there are no boards"""
427         self._boards = board.Boards()
428         with self.assertRaises(SystemExit):
429             self._RunControl()
430
431     def testCurrentSource(self):
432         """Very simple test to invoke buildman on the current source"""
433         self.setupToolchains();
434         self._RunControl('-o', self._output_dir)
435         lines = terminal.GetPrintTestLines()
436         self.assertIn('Building current source for %d boards' % len(boards),
437                       lines[0].text)
438
439     def testBadBranch(self):
440         """Test that we can detect an invalid branch"""
441         with self.assertRaises(ValueError):
442             self._RunControl('-b', 'badbranch')
443
444     def testBadToolchain(self):
445         """Test that missing toolchains are detected"""
446         self.setupToolchains();
447         ret_code = self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir)
448         lines = terminal.GetPrintTestLines()
449
450         # Buildman always builds the upstream commit as well
451         self.assertIn('Building %d commits for %d boards' %
452                 (self._commits, len(boards)), lines[0].text)
453         self.assertEqual(self._builder.count, self._total_builds)
454
455         # Only sandbox should succeed, the others don't have toolchains
456         self.assertEqual(self._builder.fail,
457                          self._total_builds - self._commits)
458         self.assertEqual(ret_code, 100)
459
460         for commit in range(self._commits):
461             for board in self._boards.GetList():
462                 if board.arch != 'sandbox':
463                   errfile = self._builder.GetErrFile(commit, board.target)
464                   fd = open(errfile)
465                   self.assertEqual(fd.readlines(),
466                           ['No tool chain for %s\n' % board.arch])
467                   fd.close()
468
469     def testBranch(self):
470         """Test building a branch with all toolchains present"""
471         self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir)
472         self.assertEqual(self._builder.count, self._total_builds)
473         self.assertEqual(self._builder.fail, 0)
474
475     def testCount(self):
476         """Test building a specific number of commitst"""
477         self._RunControl('-b', TEST_BRANCH, '-c2', '-o', self._output_dir)
478         self.assertEqual(self._builder.count, 2 * len(boards))
479         self.assertEqual(self._builder.fail, 0)
480         # Each board has a config, and then one make per commit
481         self.assertEqual(self._make_calls, len(boards) * (1 + 2))
482
483     def testIncremental(self):
484         """Test building a branch twice - the second time should do nothing"""
485         self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir)
486
487         # Each board has a mrproper, config, and then one make per commit
488         self.assertEqual(self._make_calls, len(boards) * (self._commits + 1))
489         self._make_calls = 0
490         self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir, clean_dir=False)
491         self.assertEqual(self._make_calls, 0)
492         self.assertEqual(self._builder.count, self._total_builds)
493         self.assertEqual(self._builder.fail, 0)
494
495     def testForceBuild(self):
496         """The -f flag should force a rebuild"""
497         self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir)
498         self._make_calls = 0
499         self._RunControl('-b', TEST_BRANCH, '-f', '-o', self._output_dir, clean_dir=False)
500         # Each board has a config and one make per commit
501         self.assertEqual(self._make_calls, len(boards) * (self._commits + 1))
502
503     def testForceReconfigure(self):
504         """The -f flag should force a rebuild"""
505         self._RunControl('-b', TEST_BRANCH, '-C', '-o', self._output_dir)
506         # Each commit has a config and make
507         self.assertEqual(self._make_calls, len(boards) * self._commits * 2)
508
509     def testForceReconfigure(self):
510         """The -f flag should force a rebuild"""
511         self._RunControl('-b', TEST_BRANCH, '-C', '-o', self._output_dir)
512         # Each commit has a config and make
513         self.assertEqual(self._make_calls, len(boards) * self._commits * 2)
514
515     def testMrproper(self):
516         """The -f flag should force a rebuild"""
517         self._RunControl('-b', TEST_BRANCH, '-m', '-o', self._output_dir)
518         # Each board has a mkproper, config and then one make per commit
519         self.assertEqual(self._make_calls, len(boards) * (self._commits + 2))
520
521     def testErrors(self):
522         """Test handling of build errors"""
523         self._error['board2', 1] = 'fred\n'
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, 1)
527
528         # Remove the error. This should have no effect since the commit will
529         # not be rebuilt
530         del self._error['board2', 1]
531         self._make_calls = 0
532         self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir, clean_dir=False)
533         self.assertEqual(self._builder.count, self._total_builds)
534         self.assertEqual(self._make_calls, 0)
535         self.assertEqual(self._builder.fail, 1)
536
537         # Now use the -F flag to force rebuild of the bad commit
538         self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir, '-F', clean_dir=False)
539         self.assertEqual(self._builder.count, self._total_builds)
540         self.assertEqual(self._builder.fail, 0)
541         self.assertEqual(self._make_calls, 2)
542
543     def testBranchWithSlash(self):
544         """Test building a branch with a '/' in the name"""
545         self._test_branch = '/__dev/__testbranch'
546         self._RunControl('-b', self._test_branch, clean_dir=False)
547         self.assertEqual(self._builder.count, self._total_builds)
548         self.assertEqual(self._builder.fail, 0)
549
550     def testEnvironment(self):
551         """Test that the done and environment files are written to out-env"""
552         self._RunControl('-o', self._output_dir)
553         board0_dir = os.path.join(self._output_dir, 'current', 'board0')
554         self.assertTrue(os.path.exists(os.path.join(board0_dir, 'done')))
555         self.assertTrue(os.path.exists(os.path.join(board0_dir, 'out-env')))
556
557     def testWorkInOutput(self):
558         """Test the -w option which should write directly to the output dir"""
559         board_list = board.Boards()
560         board_list.AddBoard(board.Board(*boards[0]))
561         self._RunControl('-o', self._output_dir, '-w', clean_dir=False,
562                          boards=board_list)
563         self.assertTrue(
564             os.path.exists(os.path.join(self._output_dir, 'u-boot')))
565         self.assertTrue(
566             os.path.exists(os.path.join(self._output_dir, 'done')))
567         self.assertTrue(
568             os.path.exists(os.path.join(self._output_dir, 'out-env')))
569
570     def testWorkInOutputFail(self):
571         """Test the -w option failures"""
572         with self.assertRaises(SystemExit) as e:
573             self._RunControl('-o', self._output_dir, '-w', clean_dir=False)
574         self.assertIn("single board", str(e.exception))
575         self.assertFalse(
576             os.path.exists(os.path.join(self._output_dir, 'u-boot')))
577
578         board_list = board.Boards()
579         board_list.AddBoard(board.Board(*boards[0]))
580         with self.assertRaises(SystemExit) as e:
581             self._RunControl('-b', self._test_branch, '-o', self._output_dir,
582                              '-w', clean_dir=False, boards=board_list)
583         self.assertIn("single commit", str(e.exception))
584
585         board_list = board.Boards()
586         board_list.AddBoard(board.Board(*boards[0]))
587         with self.assertRaises(SystemExit) as e:
588             self._RunControl('-w', clean_dir=False)
589         self.assertIn("specify -o", str(e.exception))