]> git.ipfire.org Git - thirdparty/u-boot.git/blob - tools/buildman/func_test.py
b45eb95a1e6aafdf7da0e1f2e2ace36db794776d
[thirdparty/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 io
6 import os
7 from pathlib import Path
8 import re
9 import shutil
10 import sys
11 import tempfile
12 import time
13 import unittest
14
15 from buildman import board
16 from buildman import boards
17 from buildman import bsettings
18 from buildman import cmdline
19 from buildman import control
20 from buildman import toolchain
21 from u_boot_pylib import command
22 from u_boot_pylib import gitutil
23 from u_boot_pylib import terminal
24 from u_boot_pylib import test_util
25 from u_boot_pylib import tools
26
27 settings_data = '''
28 # Buildman settings file
29 [global]
30
31 [toolchain]
32
33 [toolchain-alias]
34
35 [make-flags]
36 src=/home/sjg/c/src
37 chroot=/home/sjg/c/chroot
38 vboot=VBOOT_DEBUG=1 MAKEFLAGS_VBOOT=DEBUG=1 CFLAGS_EXTRA_VBOOT=-DUNROLL_LOOPS VBOOT_SOURCE=${src}/platform/vboot_reference
39 chromeos_coreboot=VBOOT=${chroot}/build/link/usr ${vboot}
40 chromeos_daisy=VBOOT=${chroot}/build/daisy/usr ${vboot}
41 chromeos_peach=VBOOT=${chroot}/build/peach_pit/usr ${vboot}
42 '''
43
44 BOARDS = [
45 ['Active', 'arm', 'armv7', '', 'Tester', 'ARM Board 0', 'board0', ''],
46 ['Active', 'arm', 'armv7', '', 'Tester', 'ARM Board 1', 'board1', ''],
47 ['Active', 'powerpc', 'powerpc', '', 'Tester', 'PowerPC board 1', 'board2', ''],
48 ['Active', 'sandbox', 'sandbox', '', 'Tester', 'Sandbox board', 'board4', ''],
49 ]
50
51 commit_shortlog = """4aca821 patman: Avoid changing the order of tags
52 39403bb patman: Use --no-pager' to stop git from forking a pager
53 db6e6f2 patman: Remove the -a option
54 f2ccf03 patman: Correct unit tests to run correctly
55 1d097f9 patman: Fix indentation in terminal.py
56 d073747 patman: Support the 'reverse' option for 'git log
57 """
58
59 commit_log = ["""commit 7f6b8315d18f683c5181d0c3694818c1b2a20dcd
60 Author: Masahiro Yamada <yamada.m@jp.panasonic.com>
61 Date: Fri Aug 22 19:12:41 2014 +0900
62
63 buildman: refactor help message
64
65 "buildman [options]" is displayed by default.
66
67 Append the rest of help messages to parser.usage
68 instead of replacing it.
69
70 Besides, "-b <branch>" is not mandatory since commit fea5858e.
71 Drop it from the usage.
72
73 Signed-off-by: Masahiro Yamada <yamada.m@jp.panasonic.com>
74 """,
75 """commit d0737479be6baf4db5e2cdbee123e96bc5ed0ba8
76 Author: Simon Glass <sjg@chromium.org>
77 Date: Thu Aug 14 16:48:25 2014 -0600
78
79 patman: Support the 'reverse' option for 'git log'
80
81 This option is currently not supported, but needs to be, for buildman to
82 operate as expected.
83
84 Series-changes: 7
85 - Add new patch to fix the 'reverse' bug
86
87 Series-version: 8
88
89 Change-Id: I79078f792e8b390b8a1272a8023537821d45feda
90 Reported-by: York Sun <yorksun@freescale.com>
91 Signed-off-by: Simon Glass <sjg@chromium.org>
92
93 """,
94 """commit 1d097f9ab487c5019152fd47bda126839f3bf9fc
95 Author: Simon Glass <sjg@chromium.org>
96 Date: Sat Aug 9 11:44:32 2014 -0600
97
98 patman: Fix indentation in terminal.py
99
100 This code came from a different project with 2-character indentation. Fix
101 it for U-Boot.
102
103 Series-changes: 6
104 - Add new patch to fix indentation in teminal.py
105
106 Change-Id: I5a74d2ebbb3cc12a665f5c725064009ac96e8a34
107 Signed-off-by: Simon Glass <sjg@chromium.org>
108
109 """,
110 """commit f2ccf03869d1e152c836515a3ceb83cdfe04a105
111 Author: Simon Glass <sjg@chromium.org>
112 Date: Sat Aug 9 11:08:24 2014 -0600
113
114 patman: Correct unit tests to run correctly
115
116 It seems that doctest behaves differently now, and some of the unit tests
117 do not run. Adjust the tests to work correctly.
118
119 ./tools/patman/patman --test
120 <unittest.result.TestResult run=10 errors=0 failures=0>
121
122 Series-changes: 6
123 - Add new patch to fix patman unit tests
124
125 Change-Id: I3d2ca588f4933e1f9d6b1665a00e4ae58269ff3b
126
127 """,
128 """commit db6e6f2f9331c5a37647d6668768d4a40b8b0d1c
129 Author: Simon Glass <sjg@chromium.org>
130 Date: Sat Aug 9 12:06:02 2014 -0600
131
132 patman: Remove the -a option
133
134 It seems that this is no longer needed, since checkpatch.pl will catch
135 whitespace problems in patches. Also the option is not widely used, so
136 it seems safe to just remove it.
137
138 Series-changes: 6
139 - Add new patch to remove patman's -a option
140
141 Suggested-by: Masahiro Yamada <yamada.m@jp.panasonic.com>
142 Change-Id: I5821a1c75154e532c46513486ca40b808de7e2cc
143
144 """,
145 """commit 39403bb4f838153028a6f21ca30bf100f3791133
146 Author: Simon Glass <sjg@chromium.org>
147 Date: Thu Aug 14 21:50:52 2014 -0600
148
149 patman: Use --no-pager' to stop git from forking a pager
150
151 """,
152 """commit 4aca821e27e97925c039e69fd37375b09c6f129c
153 Author: Simon Glass <sjg@chromium.org>
154 Date: Fri Aug 22 15:57:39 2014 -0600
155
156 patman: Avoid changing the order of tags
157
158 patman collects tags that it sees in the commit and places them nicely
159 sorted at the end of the patch. However, this is not really necessary and
160 in fact is apparently not desirable.
161
162 Series-changes: 9
163 - Add new patch to avoid changing the order of tags
164
165 Series-version: 9
166
167 Suggested-by: Masahiro Yamada <yamada.m@jp.panasonic.com>
168 Change-Id: Ib1518588c1a189ad5c3198aae76f8654aed8d0db
169 """]
170
171 TEST_BRANCH = '__testbranch'
172
173 class TestFunctional(unittest.TestCase):
174 """Functional test for buildman.
175
176 This aims to test from just below the invocation of buildman (parsing
177 of arguments) to 'make' and 'git' invocation. It is not a true
178 emd-to-end test, as it mocks git, make and the tool chain. But this
179 makes it easier to detect when the builder is doing the wrong thing,
180 since in many cases this test code will fail. For example, only a
181 very limited subset of 'git' arguments is supported - anything
182 unexpected will fail.
183 """
184 def setUp(self):
185 self._base_dir = tempfile.mkdtemp()
186 self._output_dir = tempfile.mkdtemp()
187 self._git_dir = os.path.join(self._base_dir, 'src')
188 self._buildman_pathname = sys.argv[0]
189 self._buildman_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
190 command.TEST_RESULT = self._HandleCommand
191 bsettings.setup(None)
192 bsettings.add_file(settings_data)
193 self.setupToolchains()
194 self._toolchains.Add('arm-gcc', test=False)
195 self._toolchains.Add('powerpc-gcc', test=False)
196 self._boards = boards.Boards()
197 for brd in BOARDS:
198 self._boards.add_board(board.Board(*brd))
199
200 # Directories where the source been cloned
201 self._clone_dirs = []
202 self._commits = len(commit_shortlog.splitlines()) + 1
203 self._total_builds = self._commits * len(BOARDS)
204
205 # Number of calls to make
206 self._make_calls = 0
207
208 # Map of [board, commit] to error messages
209 self._error = {}
210
211 self._test_branch = TEST_BRANCH
212
213 # Set to True to report missing blobs
214 self._missing = False
215
216 self._buildman_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
217 self._test_dir = os.path.join(self._buildman_dir, 'test')
218
219 # Set up some fake source files
220 shutil.copytree(self._test_dir, self._git_dir)
221
222 # Avoid sending any output and clear all terminal output
223 terminal.set_print_test_mode()
224 terminal.get_print_test_lines()
225
226 def tearDown(self):
227 shutil.rmtree(self._base_dir)
228 shutil.rmtree(self._output_dir)
229
230 def setupToolchains(self):
231 self._toolchains = toolchain.Toolchains()
232 self._toolchains.Add('gcc', test=False)
233
234 def _RunBuildman(self, *args):
235 all_args = [self._buildman_pathname] + list(args)
236 return command.run_one(*all_args, capture=True, capture_stderr=True)
237
238 def _RunControl(self, *args, brds=False, clean_dir=False,
239 test_thread_exceptions=False, get_builder=True):
240 """Run buildman
241
242 Args:
243 args: List of arguments to pass
244 brds: Boards object, or False to pass self._boards, or None to pass
245 None
246 clean_dir: Used for tests only, indicates that the existing output_dir
247 should be removed before starting the build
248 test_thread_exceptions: Uses for tests only, True to make the threads
249 raise an exception instead of reporting their result. This simulates
250 a failure in the code somewhere
251 get_builder (bool): Set self._builder to the resulting builder
252
253 Returns:
254 result code from buildman
255 """
256 sys.argv = [sys.argv[0]] + list(args)
257 args = cmdline.parse_args()
258 if brds == False:
259 brds = self._boards
260 result = control.do_buildman(
261 args, toolchains=self._toolchains, make_func=self._HandleMake,
262 brds=brds, clean_dir=clean_dir,
263 test_thread_exceptions=test_thread_exceptions)
264 if get_builder:
265 self._builder = control.TEST_BUILDER
266 return result
267
268 def testFullHelp(self):
269 command.TEST_RESULT = None
270 result = self._RunBuildman('-H')
271 help_file = os.path.join(self._buildman_dir, 'README.rst')
272 # Remove possible extraneous strings
273 extra = '::::::::::::::\n' + help_file + '\n::::::::::::::\n'
274 gothelp = result.stdout.replace(extra, '')
275 self.assertEqual(len(gothelp), os.path.getsize(help_file))
276 self.assertEqual(0, len(result.stderr))
277 self.assertEqual(0, result.return_code)
278
279 def testHelp(self):
280 command.TEST_RESULT = None
281 result = self._RunBuildman('-h')
282 help_file = os.path.join(self._buildman_dir, 'README.rst')
283 self.assertTrue(len(result.stdout) > 1000)
284 self.assertEqual(0, len(result.stderr))
285 self.assertEqual(0, result.return_code)
286
287 def testGitSetup(self):
288 """Test gitutils.Setup(), from outside the module itself"""
289 command.TEST_RESULT = command.CommandResult(return_code=1)
290 gitutil.setup()
291 self.assertEqual(gitutil.USE_NO_DECORATE, False)
292
293 command.TEST_RESULT = command.CommandResult(return_code=0)
294 gitutil.setup()
295 self.assertEqual(gitutil.USE_NO_DECORATE, True)
296
297 def _HandleCommandGitLog(self, args):
298 if args[-1] == '--':
299 args = args[:-1]
300 if '-n0' in args:
301 return command.CommandResult(return_code=0)
302 elif args[-1] == 'upstream/master..%s' % self._test_branch:
303 return command.CommandResult(return_code=0, stdout=commit_shortlog)
304 elif args[:3] == ['--no-color', '--no-decorate', '--reverse']:
305 if args[-1] == self._test_branch:
306 count = int(args[3][2:])
307 return command.CommandResult(return_code=0,
308 stdout=''.join(commit_log[:count]))
309
310 # Not handled, so abort
311 print('git log', args)
312 sys.exit(1)
313
314 def _HandleCommandGitConfig(self, args):
315 config = args[0]
316 if config == 'sendemail.aliasesfile':
317 return command.CommandResult(return_code=0)
318 elif config.startswith('branch.badbranch'):
319 return command.CommandResult(return_code=1)
320 elif config == 'branch.%s.remote' % self._test_branch:
321 return command.CommandResult(return_code=0, stdout='upstream\n')
322 elif config == 'branch.%s.merge' % self._test_branch:
323 return command.CommandResult(return_code=0,
324 stdout='refs/heads/master\n')
325
326 # Not handled, so abort
327 print('git config', args)
328 sys.exit(1)
329
330 def _HandleCommandGit(self, in_args):
331 """Handle execution of a git command
332
333 This uses a hacked-up parser.
334
335 Args:
336 in_args: Arguments after 'git' from the command line
337 """
338 git_args = [] # Top-level arguments to git itself
339 sub_cmd = None # Git sub-command selected
340 args = [] # Arguments to the git sub-command
341 for arg in in_args:
342 if sub_cmd:
343 args.append(arg)
344 elif arg[0] == '-':
345 git_args.append(arg)
346 else:
347 if git_args and git_args[-1] in ['--git-dir', '--work-tree']:
348 git_args.append(arg)
349 else:
350 sub_cmd = arg
351 if sub_cmd == 'config':
352 return self._HandleCommandGitConfig(args)
353 elif sub_cmd == 'log':
354 return self._HandleCommandGitLog(args)
355 elif sub_cmd == 'clone':
356 return command.CommandResult(return_code=0)
357 elif sub_cmd == 'checkout':
358 return command.CommandResult(return_code=0)
359 elif sub_cmd == 'worktree':
360 return command.CommandResult(return_code=0)
361
362 # Not handled, so abort
363 print('git', git_args, sub_cmd, args)
364 sys.exit(1)
365
366 def _HandleCommandNm(self, args):
367 return command.CommandResult(return_code=0)
368
369 def _HandleCommandObjdump(self, args):
370 return command.CommandResult(return_code=0)
371
372 def _HandleCommandObjcopy(self, args):
373 return command.CommandResult(return_code=0)
374
375 def _HandleCommandSize(self, args):
376 return command.CommandResult(return_code=0)
377
378 def _HandleCommandCpp(self, args):
379 # args ['-nostdinc', '-P', '-I', '/tmp/tmp7f17xk_o/src', '-undef',
380 # '-x', 'assembler-with-cpp', fname]
381 fname = args[7]
382 buf = io.StringIO()
383 for line in tools.read_file(fname, False).splitlines():
384 if line.startswith('#include'):
385 # Example: #include <configs/renesas_rcar2.config>
386 m_incfname = re.match('#include <(.*)>', line)
387 data = tools.read_file(m_incfname.group(1), False)
388 for line in data.splitlines():
389 print(line, file=buf)
390 else:
391 print(line, file=buf)
392 return command.CommandResult(stdout=buf.getvalue(), return_code=0)
393
394 def _HandleCommand(self, **kwargs):
395 """Handle a command execution.
396
397 The command is in kwargs['pipe-list'], as a list of pipes, each a
398 list of commands. The command should be emulated as required for
399 testing purposes.
400
401 Returns:
402 A CommandResult object
403 """
404 pipe_list = kwargs['pipe_list']
405 wc = False
406 if len(pipe_list) != 1:
407 if pipe_list[1] == ['wc', '-l']:
408 wc = True
409 else:
410 print('invalid pipe', kwargs)
411 sys.exit(1)
412 cmd = pipe_list[0][0]
413 args = pipe_list[0][1:]
414 result = None
415 if cmd == 'git':
416 result = self._HandleCommandGit(args)
417 elif cmd == './scripts/show-gnu-make':
418 return command.CommandResult(return_code=0, stdout='make')
419 elif cmd.endswith('nm'):
420 return self._HandleCommandNm(args)
421 elif cmd.endswith('objdump'):
422 return self._HandleCommandObjdump(args)
423 elif cmd.endswith('objcopy'):
424 return self._HandleCommandObjcopy(args)
425 elif cmd.endswith( 'size'):
426 return self._HandleCommandSize(args)
427 elif cmd.endswith( 'cpp'):
428 return self._HandleCommandCpp(args)
429
430 if not result:
431 # Not handled, so abort
432 print('unknown command', kwargs)
433 sys.exit(1)
434
435 if wc:
436 result.stdout = len(result.stdout.splitlines())
437 return result
438
439 def _HandleMake(self, commit, brd, stage, cwd, *args, **kwargs):
440 """Handle execution of 'make'
441
442 Args:
443 commit: Commit object that is being built
444 brd: Board object that is being built
445 stage: Stage that we are at (mrproper, config, build)
446 cwd: Directory where make should be run
447 args: Arguments to pass to make
448 kwargs: Arguments to pass to command.run_one()
449 """
450 self._make_calls += 1
451 out_dir = ''
452 for arg in args:
453 if arg.startswith('O='):
454 out_dir = arg[2:]
455 if stage == 'mrproper':
456 return command.CommandResult(return_code=0)
457 elif stage == 'config':
458 fname = os.path.join(cwd or '', out_dir, '.config')
459 tools.write_file(fname, b'CONFIG_SOMETHING=1')
460 return command.CommandResult(return_code=0,
461 combined='Test configuration complete')
462 elif stage == 'oldconfig':
463 return command.CommandResult(return_code=0)
464 elif stage == 'build':
465 stderr = ''
466 fname = os.path.join(cwd or '', out_dir, 'u-boot')
467 tools.write_file(fname, b'U-Boot')
468
469 # Handle missing blobs
470 if self._missing:
471 if 'BINMAN_ALLOW_MISSING=1' in args:
472 stderr = '''+Image 'main-section' is missing external blobs and is non-functional: intel-descriptor intel-ifwi intel-fsp-m intel-fsp-s intel-vbt
473 Image 'main-section' has faked external blobs and is non-functional: descriptor.bin fsp_m.bin fsp_s.bin vbt.bin
474
475 Some images are invalid'''
476 else:
477 stderr = "binman: Filename 'fsp.bin' not found in input path"
478 elif type(commit) is not str:
479 stderr = self._error.get((brd.target, commit.sequence))
480
481 if stderr:
482 return command.CommandResult(return_code=2, stderr=stderr)
483 return command.CommandResult(return_code=0)
484
485 # Not handled, so abort
486 print('_HandleMake failure: make', stage)
487 sys.exit(1)
488
489 # Example function to print output lines
490 def print_lines(self, lines):
491 print(len(lines))
492 for line in lines:
493 print(line)
494 #self.print_lines(terminal.get_print_test_lines())
495
496 def testNoBoards(self):
497 """Test that buildman aborts when there are no boards"""
498 self._boards = boards.Boards()
499 with self.assertRaises(SystemExit):
500 self._RunControl()
501
502 def testCurrentSource(self):
503 """Very simple test to invoke buildman on the current source"""
504 self.setupToolchains();
505 self._RunControl('-o', self._output_dir)
506 lines = terminal.get_print_test_lines()
507 self.assertIn('Building current source for %d boards' % len(BOARDS),
508 lines[0].text)
509
510 def testBadBranch(self):
511 """Test that we can detect an invalid branch"""
512 with self.assertRaises(ValueError):
513 self._RunControl('-b', 'badbranch')
514
515 def testBadToolchain(self):
516 """Test that missing toolchains are detected"""
517 self.setupToolchains();
518 ret_code = self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir)
519 lines = terminal.get_print_test_lines()
520
521 # Buildman always builds the upstream commit as well
522 self.assertIn('Building %d commits for %d boards' %
523 (self._commits, len(BOARDS)), lines[0].text)
524 self.assertEqual(self._builder.count, self._total_builds)
525
526 # Only sandbox should succeed, the others don't have toolchains
527 self.assertEqual(self._builder.fail,
528 self._total_builds - self._commits)
529 self.assertEqual(ret_code, 100)
530
531 for commit in range(self._commits):
532 for brd in self._boards.get_list():
533 if brd.arch != 'sandbox':
534 errfile = self._builder.get_err_file(commit, brd.target)
535 fd = open(errfile)
536 self.assertEqual(
537 fd.readlines(),
538 [f'Tool chain error for {brd.arch}: '
539 f"No tool chain found for arch '{brd.arch}'"])
540 fd.close()
541
542 def testBranch(self):
543 """Test building a branch with all toolchains present"""
544 self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir)
545 self.assertEqual(self._builder.count, self._total_builds)
546 self.assertEqual(self._builder.fail, 0)
547
548 def testCount(self):
549 """Test building a specific number of commitst"""
550 self._RunControl('-b', TEST_BRANCH, '-c2', '-o', self._output_dir)
551 self.assertEqual(self._builder.count, 2 * len(BOARDS))
552 self.assertEqual(self._builder.fail, 0)
553 # Each board has a config, and then one make per commit
554 self.assertEqual(self._make_calls, len(BOARDS) * (1 + 2))
555
556 def testIncremental(self):
557 """Test building a branch twice - the second time should do nothing"""
558 self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir)
559
560 # Each board has a mrproper, config, and then one make per commit
561 self.assertEqual(self._make_calls, len(BOARDS) * (self._commits + 1))
562 self._make_calls = 0
563 self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir, clean_dir=False)
564 self.assertEqual(self._make_calls, 0)
565 self.assertEqual(self._builder.count, self._total_builds)
566 self.assertEqual(self._builder.fail, 0)
567
568 def testForceBuild(self):
569 """The -f flag should force a rebuild"""
570 self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir)
571 self._make_calls = 0
572 self._RunControl('-b', TEST_BRANCH, '-f', '-o', self._output_dir, clean_dir=False)
573 # Each board has a config and one make per commit
574 self.assertEqual(self._make_calls, len(BOARDS) * (self._commits + 1))
575
576 def testForceReconfigure(self):
577 """The -f flag should force a rebuild"""
578 self._RunControl('-b', TEST_BRANCH, '-C', '-o', self._output_dir)
579 # Each commit has a config and make
580 self.assertEqual(self._make_calls, len(BOARDS) * self._commits * 2)
581
582 def testMrproper(self):
583 """The -f flag should force a rebuild"""
584 self._RunControl('-b', TEST_BRANCH, '-m', '-o', self._output_dir)
585 # Each board has a mkproper, config and then one make per commit
586 self.assertEqual(self._make_calls, len(BOARDS) * (self._commits + 2))
587
588 def testErrors(self):
589 """Test handling of build errors"""
590 self._error['board2', 1] = 'fred\n'
591 self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir)
592 self.assertEqual(self._builder.count, self._total_builds)
593 self.assertEqual(self._builder.fail, 1)
594
595 # Remove the error. This should have no effect since the commit will
596 # not be rebuilt
597 del self._error['board2', 1]
598 self._make_calls = 0
599 self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir, clean_dir=False)
600 self.assertEqual(self._builder.count, self._total_builds)
601 self.assertEqual(self._make_calls, 0)
602 self.assertEqual(self._builder.fail, 1)
603
604 # Now use the -F flag to force rebuild of the bad commit
605 self._RunControl('-b', TEST_BRANCH, '-o', self._output_dir, '-F', clean_dir=False)
606 self.assertEqual(self._builder.count, self._total_builds)
607 self.assertEqual(self._builder.fail, 0)
608 self.assertEqual(self._make_calls, 2)
609
610 def testBranchWithSlash(self):
611 """Test building a branch with a '/' in the name"""
612 self._test_branch = '/__dev/__testbranch'
613 self._RunControl('-b', self._test_branch, '-o', self._output_dir,
614 clean_dir=False)
615 self.assertEqual(self._builder.count, self._total_builds)
616 self.assertEqual(self._builder.fail, 0)
617
618 def testEnvironment(self):
619 """Test that the done and environment files are written to out-env"""
620 self._RunControl('-o', self._output_dir)
621 board0_dir = os.path.join(self._output_dir, 'current', 'board0')
622 self.assertTrue(os.path.exists(os.path.join(board0_dir, 'done')))
623 self.assertTrue(os.path.exists(os.path.join(board0_dir, 'out-env')))
624
625 def testEnvironmentUnicode(self):
626 """Test there are no unicode errors when the env has non-ASCII chars"""
627 try:
628 varname = b'buildman_test_var'
629 os.environb[varname] = b'strange\x80chars'
630 self.assertEqual(0, self._RunControl('-o', self._output_dir))
631 board0_dir = os.path.join(self._output_dir, 'current', 'board0')
632 self.assertTrue(os.path.exists(os.path.join(board0_dir, 'done')))
633 self.assertTrue(os.path.exists(os.path.join(board0_dir, 'out-env')))
634 finally:
635 del os.environb[varname]
636
637 def testWorkInOutput(self):
638 """Test the -w option which should write directly to the output dir"""
639 board_list = boards.Boards()
640 board_list.add_board(board.Board(*BOARDS[0]))
641 self._RunControl('-o', self._output_dir, '-w', clean_dir=False,
642 brds=board_list)
643 self.assertTrue(
644 os.path.exists(os.path.join(self._output_dir, 'u-boot')))
645 self.assertTrue(
646 os.path.exists(os.path.join(self._output_dir, 'done')))
647 self.assertTrue(
648 os.path.exists(os.path.join(self._output_dir, 'out-env')))
649
650 def testWorkInOutputFail(self):
651 """Test the -w option failures"""
652 with self.assertRaises(SystemExit) as e:
653 self._RunControl('-o', self._output_dir, '-w', clean_dir=False)
654 self.assertIn("single board", str(e.exception))
655 self.assertFalse(
656 os.path.exists(os.path.join(self._output_dir, 'u-boot')))
657
658 board_list = boards.Boards()
659 board_list.add_board(board.Board(*BOARDS[0]))
660 with self.assertRaises(SystemExit) as e:
661 self._RunControl('-b', self._test_branch, '-o', self._output_dir,
662 '-w', clean_dir=False, brds=board_list)
663 self.assertIn("single commit", str(e.exception))
664
665 board_list = boards.Boards()
666 board_list.add_board(board.Board(*BOARDS[0]))
667 with self.assertRaises(SystemExit) as e:
668 self._RunControl('-w', clean_dir=False)
669 self.assertIn("specify -o", str(e.exception))
670
671 def testThreadExceptions(self):
672 """Test that exceptions in threads are reported"""
673 with test_util.capture_sys_output() as (stdout, stderr):
674 self.assertEqual(102, self._RunControl('-o', self._output_dir,
675 test_thread_exceptions=True))
676 self.assertIn(
677 'Thread exception (use -T0 to run without threads): test exception',
678 stdout.getvalue())
679
680 def testBlobs(self):
681 """Test handling of missing blobs"""
682 self._missing = True
683
684 board0_dir = os.path.join(self._output_dir, 'current', 'board0')
685 errfile = os.path.join(board0_dir, 'err')
686 logfile = os.path.join(board0_dir, 'log')
687
688 # We expect failure when there are missing blobs
689 result = self._RunControl('board0', '-o', self._output_dir)
690 self.assertEqual(100, result)
691 self.assertTrue(os.path.exists(os.path.join(board0_dir, 'done')))
692 self.assertTrue(os.path.exists(errfile))
693 self.assertIn(b"Filename 'fsp.bin' not found in input path",
694 tools.read_file(errfile))
695
696 def testBlobsAllowMissing(self):
697 """Allow missing blobs - still failure but a different exit code"""
698 self._missing = True
699 result = self._RunControl('board0', '-o', self._output_dir, '-M',
700 clean_dir=True)
701 self.assertEqual(101, result)
702 board0_dir = os.path.join(self._output_dir, 'current', 'board0')
703 errfile = os.path.join(board0_dir, 'err')
704 self.assertTrue(os.path.exists(errfile))
705 self.assertIn(b'Some images are invalid', tools.read_file(errfile))
706
707 def testBlobsWarning(self):
708 """Allow missing blobs and ignore warnings"""
709 self._missing = True
710 result = self._RunControl('board0', '-o', self._output_dir, '-MW')
711 self.assertEqual(0, result)
712 board0_dir = os.path.join(self._output_dir, 'current', 'board0')
713 errfile = os.path.join(board0_dir, 'err')
714 self.assertIn(b'Some images are invalid', tools.read_file(errfile))
715
716 def testBlobSettings(self):
717 """Test with no settings"""
718 self.assertEqual(False,
719 control.get_allow_missing(False, False, 1, False))
720 self.assertEqual(True,
721 control.get_allow_missing(True, False, 1, False))
722 self.assertEqual(False,
723 control.get_allow_missing(True, True, 1, False))
724
725 def testBlobSettingsAlways(self):
726 """Test the 'always' policy"""
727 bsettings.set_item('global', 'allow-missing', 'always')
728 self.assertEqual(True,
729 control.get_allow_missing(False, False, 1, False))
730 self.assertEqual(False,
731 control.get_allow_missing(False, True, 1, False))
732
733 def testBlobSettingsBranch(self):
734 """Test the 'branch' policy"""
735 bsettings.set_item('global', 'allow-missing', 'branch')
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(False,
741 control.get_allow_missing(False, True, 1, True))
742
743 def testBlobSettingsMultiple(self):
744 """Test the 'multiple' policy"""
745 bsettings.set_item('global', 'allow-missing', 'multiple')
746 self.assertEqual(False,
747 control.get_allow_missing(False, False, 1, False))
748 self.assertEqual(True,
749 control.get_allow_missing(False, False, 2, False))
750 self.assertEqual(False,
751 control.get_allow_missing(False, True, 2, False))
752
753 def testBlobSettingsBranchMultiple(self):
754 """Test the 'branch multiple' policy"""
755 bsettings.set_item('global', 'allow-missing', 'branch multiple')
756 self.assertEqual(False,
757 control.get_allow_missing(False, False, 1, False))
758 self.assertEqual(True,
759 control.get_allow_missing(False, False, 1, True))
760 self.assertEqual(True,
761 control.get_allow_missing(False, False, 2, False))
762 self.assertEqual(True,
763 control.get_allow_missing(False, False, 2, True))
764 self.assertEqual(False,
765 control.get_allow_missing(False, True, 2, True))
766
767 def check_command(self, *extra_args):
768 """Run a command with the extra arguments and return the commands used
769
770 Args:
771 extra_args (list of str): List of extra arguments
772
773 Returns:
774 list of str: Lines returned in the out-cmd file
775 """
776 self._RunControl('-o', self._output_dir, *extra_args)
777 board0_dir = os.path.join(self._output_dir, 'current', 'board0')
778 self.assertTrue(os.path.exists(os.path.join(board0_dir, 'done')))
779 cmd_fname = os.path.join(board0_dir, 'out-cmd')
780 self.assertTrue(os.path.exists(cmd_fname))
781 data = tools.read_file(cmd_fname)
782
783 config_fname = os.path.join(board0_dir, '.config')
784 self.assertTrue(os.path.exists(config_fname))
785 cfg_data = tools.read_file(config_fname)
786
787 return data.splitlines(), cfg_data
788
789 def testCmdFile(self):
790 """Test that the -cmd-out file is produced"""
791 lines = self.check_command()[0]
792 self.assertEqual(2, len(lines))
793 self.assertRegex(lines[0], b'make O=/.*board0_defconfig')
794 self.assertRegex(lines[0], b'make O=/.*-s.*')
795
796 def testNoLto(self):
797 """Test that the --no-lto flag works"""
798 lines = self.check_command('-L')[0]
799 self.assertIn(b'NO_LTO=1', lines[0])
800
801 def testReproducible(self):
802 """Test that the -r flag works"""
803 lines, cfg_data = self.check_command('-r')
804 self.assertIn(b'SOURCE_DATE_EPOCH=0', lines[0])
805
806 # We should see CONFIG_LOCALVERSION_AUTO unset
807 self.assertEqual(b'''CONFIG_SOMETHING=1
808 # CONFIG_LOCALVERSION_AUTO is not set
809 ''', cfg_data)
810
811 with test_util.capture_sys_output() as (stdout, stderr):
812 lines, cfg_data = self.check_command('-r', '-a', 'LOCALVERSION')
813 self.assertIn(b'SOURCE_DATE_EPOCH=0', lines[0])
814
815 # We should see CONFIG_LOCALVERSION_AUTO unset
816 self.assertEqual(b'''CONFIG_SOMETHING=1
817 CONFIG_LOCALVERSION=y
818 ''', cfg_data)
819 self.assertIn('Not dropping LOCALVERSION_AUTO', stdout.getvalue())
820
821 def test_scan_defconfigs(self):
822 """Test scanning the defconfigs to obtain all the boards"""
823 src = self._git_dir
824
825 # Scan the test directory which contains a Kconfig and some *_defconfig
826 # files
827 params, warnings = self._boards.scan_defconfigs(src, src)
828
829 # We should get two boards
830 self.assertEqual(2, len(params))
831 self.assertFalse(warnings)
832 first = 0 if params[0]['target'] == 'board0' else 1
833 board0 = params[first]
834 board2 = params[1 - first]
835
836 self.assertEqual('arm', board0['arch'])
837 self.assertEqual('armv7', board0['cpu'])
838 self.assertEqual('-', board0['soc'])
839 self.assertEqual('Tester', board0['vendor'])
840 self.assertEqual('ARM Board 0', board0['board'])
841 self.assertEqual('config0', board0['config'])
842 self.assertEqual('board0', board0['target'])
843
844 self.assertEqual('powerpc', board2['arch'])
845 self.assertEqual('ppc', board2['cpu'])
846 self.assertEqual('mpc85xx', board2['soc'])
847 self.assertEqual('Tester', board2['vendor'])
848 self.assertEqual('PowerPC board 1', board2['board'])
849 self.assertEqual('config2', board2['config'])
850 self.assertEqual('board2', board2['target'])
851
852 def test_output_is_new(self):
853 """Test detecting new changes to Kconfig"""
854 base = self._base_dir
855 src = self._git_dir
856 config_dir = os.path.join(src, 'configs')
857 delay = 0.02
858
859 # Create a boards.cfg file
860 boards_cfg = os.path.join(base, 'boards.cfg')
861 content = b'''#
862 # List of boards
863 # Automatically generated by buildman/boards.py: don't edit
864 #
865 # Status, Arch, CPU, SoC, Vendor, Board, Target, Config, Maintainers
866
867 Active aarch64 armv8 - armltd corstone1000 board0
868 Active aarch64 armv8 - armltd total_compute board2
869 '''
870 # Check missing file
871 self.assertFalse(boards.output_is_new(boards_cfg, config_dir, src))
872
873 # Check that the board.cfg file is newer
874 time.sleep(delay)
875 tools.write_file(boards_cfg, content)
876 self.assertTrue(boards.output_is_new(boards_cfg, config_dir, src))
877
878 # Touch the Kconfig files after a show delay to avoid a race
879 time.sleep(delay)
880 Path(os.path.join(src, 'Kconfig')).touch()
881 self.assertFalse(boards.output_is_new(boards_cfg, config_dir, src))
882 Path(boards_cfg).touch()
883 self.assertTrue(boards.output_is_new(boards_cfg, config_dir, src))
884
885 # Touch a different Kconfig file
886 time.sleep(delay)
887 Path(os.path.join(src, 'Kconfig.something')).touch()
888 self.assertFalse(boards.output_is_new(boards_cfg, config_dir, src))
889 Path(boards_cfg).touch()
890 self.assertTrue(boards.output_is_new(boards_cfg, config_dir, src))
891
892 # Touch a MAINTAINERS file
893 time.sleep(delay)
894 Path(os.path.join(src, 'MAINTAINERS')).touch()
895 self.assertFalse(boards.output_is_new(boards_cfg, config_dir, src))
896
897 Path(boards_cfg).touch()
898 self.assertTrue(boards.output_is_new(boards_cfg, config_dir, src))
899
900 # Touch a defconfig file
901 time.sleep(delay)
902 Path(os.path.join(config_dir, 'board0_defconfig')).touch()
903 self.assertFalse(boards.output_is_new(boards_cfg, config_dir, src))
904 Path(boards_cfg).touch()
905 self.assertTrue(boards.output_is_new(boards_cfg, config_dir, src))
906
907 # Remove a board and check that the board.cfg file is now older
908 Path(os.path.join(config_dir, 'board0_defconfig')).unlink()
909 self.assertFalse(boards.output_is_new(boards_cfg, config_dir, src))
910
911 def test_maintainers(self):
912 """Test detecting boards without a MAINTAINERS entry"""
913 src = self._git_dir
914 main = os.path.join(src, 'boards', 'board0', 'MAINTAINERS')
915 other = os.path.join(src, 'boards', 'board2', 'MAINTAINERS')
916 kc_file = os.path.join(src, 'Kconfig')
917 config_dir = os.path.join(src, 'configs')
918 params_list, warnings = self._boards.build_board_list(config_dir, src)
919
920 # There should be two boards no warnings
921 self.assertEqual(2, len(params_list))
922 self.assertFalse(warnings)
923
924 # Set an invalid status line in the file
925 orig_data = tools.read_file(main, binary=False)
926 lines = ['S: Other\n' if line.startswith('S:') else line
927 for line in orig_data.splitlines(keepends=True)]
928 tools.write_file(main, ''.join(lines), binary=False)
929 params_list, warnings = self._boards.build_board_list(config_dir, src)
930 self.assertEqual(2, len(params_list))
931 params = params_list[0]
932 if params['target'] == 'board2':
933 params = params_list[1]
934 self.assertEqual('-', params['status'])
935 self.assertEqual(["WARNING: Other: unknown status for 'board0'"],
936 warnings)
937
938 # Remove the status line (S:) from a file
939 lines = [line for line in orig_data.splitlines(keepends=True)
940 if not line.startswith('S:')]
941 tools.write_file(main, ''.join(lines), binary=False)
942 params_list, warnings = self._boards.build_board_list(config_dir, src)
943 self.assertEqual(2, len(params_list))
944 self.assertEqual(["WARNING: -: unknown status for 'board0'"], warnings)
945
946 # Remove the configs/ line (F:) from a file - this is the last line
947 data = ''.join(orig_data.splitlines(keepends=True)[:-1])
948 tools.write_file(main, data, binary=False)
949 params_list, warnings = self._boards.build_board_list(config_dir, src)
950 self.assertEqual(2, len(params_list))
951 self.assertEqual(["WARNING: no maintainers for 'board0'"], warnings)
952
953 # Mark a board as orphaned - this should give a warning
954 lines = ['S: Orphaned' if line.startswith('S') else line
955 for line in orig_data.splitlines(keepends=True)]
956 tools.write_file(main, ''.join(lines), binary=False)
957 params_list, warnings = self._boards.build_board_list(config_dir, src)
958 self.assertEqual(2, len(params_list))
959 self.assertEqual(["WARNING: no maintainers for 'board0'"], warnings)
960
961 # Change the maintainer to '-' - this should give a warning
962 lines = ['M: -' if line.startswith('M') else line
963 for line in orig_data.splitlines(keepends=True)]
964 tools.write_file(main, ''.join(lines), binary=False)
965 params_list, warnings = self._boards.build_board_list(config_dir, src)
966 self.assertEqual(2, len(params_list))
967 self.assertEqual(["WARNING: -: unknown status for 'board0'"], warnings)
968
969 # Remove the maintainer line (M:) from a file
970 lines = [line for line in orig_data.splitlines(keepends=True)
971 if not line.startswith('M:')]
972 tools.write_file(main, ''.join(lines), binary=False)
973 params_list, warnings = self._boards.build_board_list(config_dir, src)
974 self.assertEqual(2, len(params_list))
975 self.assertEqual(["WARNING: no maintainers for 'board0'"], warnings)
976
977 # Move the contents of the second file into this one, removing the
978 # second file, to check multiple records in a single file.
979 both_data = orig_data + tools.read_file(other, binary=False)
980 tools.write_file(main, both_data, binary=False)
981 os.remove(other)
982 params_list, warnings = self._boards.build_board_list(config_dir, src)
983 self.assertEqual(2, len(params_list))
984 self.assertFalse(warnings)
985
986 # Add another record, this should be ignored with a warning
987 extra = '\n\nAnother\nM: Fred\nF: configs/board9_defconfig\nS: other\n'
988 tools.write_file(main, both_data + extra, binary=False)
989 params_list, warnings = self._boards.build_board_list(config_dir, src)
990 self.assertEqual(2, len(params_list))
991 self.assertFalse(warnings)
992
993 # Add another TARGET to the Kconfig
994 tools.write_file(main, both_data, binary=False)
995 orig_kc_data = tools.read_file(kc_file)
996 extra = (b'''
997 if TARGET_BOARD2
998 config TARGET_OTHER
999 \tbool "other"
1000 \tdefault y
1001 endif
1002 ''')
1003 tools.write_file(kc_file, orig_kc_data + extra)
1004 params_list, warnings = self._boards.build_board_list(config_dir, src,
1005 warn_targets=True)
1006 self.assertEqual(2, len(params_list))
1007 self.assertEqual(
1008 ['WARNING: board2_defconfig: Duplicate TARGET_xxx: board2 and other'],
1009 warnings)
1010
1011 # Remove the TARGET_BOARD0 Kconfig option
1012 lines = [b'' if line == b'config TARGET_BOARD2\n' else line
1013 for line in orig_kc_data.splitlines(keepends=True)]
1014 tools.write_file(kc_file, b''.join(lines))
1015 params_list, warnings = self._boards.build_board_list(config_dir, src,
1016 warn_targets=True)
1017 self.assertEqual(2, len(params_list))
1018 self.assertEqual(
1019 ['WARNING: board2_defconfig: No TARGET_BOARD2 enabled'],
1020 warnings)
1021 tools.write_file(kc_file, orig_kc_data)
1022
1023 # Replace the last F: line of board 2 with an N: line
1024 data = ''.join(both_data.splitlines(keepends=True)[:-1])
1025 tools.write_file(main, data + 'N: oa.*2\n', binary=False)
1026 params_list, warnings = self._boards.build_board_list(config_dir, src)
1027 self.assertEqual(2, len(params_list))
1028 self.assertFalse(warnings)
1029
1030 def testRegenBoards(self):
1031 """Test that we can regenerate the boards.cfg file"""
1032 outfile = os.path.join(self._output_dir, 'test-boards.cfg')
1033 if os.path.exists(outfile):
1034 os.remove(outfile)
1035 with test_util.capture_sys_output() as (stdout, stderr):
1036 result = self._RunControl('-R', outfile, brds=None,
1037 get_builder=False)
1038 self.assertTrue(os.path.exists(outfile))
1039
1040 def test_print_prefix(self):
1041 """Test that we can print the toolchain prefix"""
1042 with test_util.capture_sys_output() as (stdout, stderr):
1043 result = self._RunControl('-A', 'board0')
1044 self.assertEqual('arm-\n', stdout.getvalue())
1045 self.assertEqual('', stderr.getvalue())
1046
1047 def test_exclude_one(self):
1048 """Test excluding a single board from an arch"""
1049 self._RunControl('arm', '-x', 'board1', '-o', self._output_dir)
1050 self.assertEqual(['board0'],
1051 [b.target for b in self._boards.get_selected()])
1052
1053 def test_exclude_arch(self):
1054 """Test excluding an arch"""
1055 self._RunControl('-x', 'arm', '-o', self._output_dir)
1056 self.assertEqual(['board2', 'board4'],
1057 [b.target for b in self._boards.get_selected()])
1058
1059 def test_exclude_comma(self):
1060 """Test excluding a comma-separated list of things"""
1061 self._RunControl('-x', 'arm,powerpc', '-o', self._output_dir)
1062 self.assertEqual(['board4'],
1063 [b.target for b in self._boards.get_selected()])
1064
1065 def test_exclude_list(self):
1066 """Test excluding a list of things"""
1067 self._RunControl('-x', 'board2', '-x' 'board4', '-o', self._output_dir)
1068 self.assertEqual(['board0', 'board1'],
1069 [b.target for b in self._boards.get_selected()])
1070
1071 def test_single_boards(self):
1072 """Test building single boards"""
1073 self._RunControl('--boards', 'board1', '-o', self._output_dir)
1074 self.assertEqual(1, self._builder.count)
1075
1076 self._RunControl('--boards', 'board1', '--boards', 'board2',
1077 '-o', self._output_dir)
1078 self.assertEqual(2, self._builder.count)
1079
1080 self._RunControl('--boards', 'board1,board2', '--boards', 'board4',
1081 '-o', self._output_dir)
1082 self.assertEqual(3, self._builder.count)
1083
1084 def test_print_arch(self):
1085 """Test that we can print the board architecture"""
1086 with test_util.capture_sys_output() as (stdout, stderr):
1087 result = self._RunControl('--print-arch', 'board0')
1088 self.assertEqual('arm\n', stdout.getvalue())
1089 self.assertEqual('', stderr.getvalue())
1090
1091 def test_kconfig_scanner(self):
1092 """Test using the kconfig scanner to determine important values
1093
1094 Note that there is already a test_scan_defconfigs() which checks the
1095 higher-level scan_defconfigs() function. This test checks just the
1096 scanner itself
1097 """
1098 src = self._git_dir
1099 scanner = boards.KconfigScanner(src)
1100
1101 # First do a simple sanity check
1102 norm = os.path.join(src, 'board0_defconfig')
1103 tools.write_file(norm, 'CONFIG_TARGET_BOARD0=y', False)
1104 res = scanner.scan(norm, True)
1105 self.assertEqual(({
1106 'arch': 'arm',
1107 'cpu': 'armv7',
1108 'soc': '-',
1109 'vendor': 'Tester',
1110 'board': 'ARM Board 0',
1111 'config': 'config0',
1112 'target': 'board0'}, []), res)
1113
1114 # Check that the SoC cannot be changed and the filename does not affect
1115 # the resulting board
1116 tools.write_file(norm, '''CONFIG_TARGET_BOARD2=y
1117 CONFIG_SOC="fred"
1118 ''', False)
1119 res = scanner.scan(norm, True)
1120 self.assertEqual(({
1121 'arch': 'powerpc',
1122 'cpu': 'ppc',
1123 'soc': 'mpc85xx',
1124 'vendor': 'Tester',
1125 'board': 'PowerPC board 1',
1126 'config': 'config2',
1127 'target': 'board0'}, []), res)
1128
1129 # Check handling of missing information
1130 tools.write_file(norm, '', False)
1131 res = scanner.scan(norm, True)
1132 self.assertEqual(({
1133 'arch': '-',
1134 'cpu': '-',
1135 'soc': '-',
1136 'vendor': '-',
1137 'board': '-',
1138 'config': '-',
1139 'target': 'board0'},
1140 ['WARNING: board0_defconfig: No TARGET_BOARD0 enabled']), res)
1141
1142 # check handling of #include files; see _HandleCommandCpp()
1143 inc = os.path.join(src, 'common')
1144 tools.write_file(inc, b'CONFIG_TARGET_BOARD0=y\n')
1145 tools.write_file(norm, f'#include <{inc}>', False)
1146 res = scanner.scan(norm, True)
1147 self.assertEqual(({
1148 'arch': 'arm',
1149 'cpu': 'armv7',
1150 'soc': '-',
1151 'vendor': 'Tester',
1152 'board': 'ARM Board 0',
1153 'config': 'config0',
1154 'target': 'board0'}, []), res)