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