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