xref: /rk3399_rockchip-uboot/tools/binman/func_test.py (revision 4f44304b0bd881f79252c7b7d2fb796e31ca3b0a)
1#
2# Copyright (c) 2016 Google, Inc
3# Written by Simon Glass <sjg@chromium.org>
4#
5# SPDX-License-Identifier:      GPL-2.0+
6#
7# To run a single test, change to this directory, and:
8#
9#    python -m unittest func_test.TestFunctional.testHelp
10
11from optparse import OptionParser
12import os
13import shutil
14import struct
15import sys
16import tempfile
17import unittest
18
19import binman
20import cmdline
21import command
22import control
23import entry
24import fdt_select
25import fdt_util
26import tools
27import tout
28
29# Contents of test files, corresponding to different entry types
30U_BOOT_DATA         = '1234'
31U_BOOT_IMG_DATA     = 'img'
32U_BOOT_SPL_DATA     = '567'
33BLOB_DATA           = '89'
34ME_DATA             = '0abcd'
35VGA_DATA            = 'vga'
36U_BOOT_DTB_DATA     = 'udtb'
37X86_START16_DATA    = 'start16'
38U_BOOT_NODTB_DATA   = 'nodtb with microcode pointer somewhere in here'
39
40class TestFunctional(unittest.TestCase):
41    """Functional tests for binman
42
43    Most of these use a sample .dts file to build an image and then check
44    that it looks correct. The sample files are in the test/ subdirectory
45    and are numbered.
46
47    For each entry type a very small test file is created using fixed
48    string contents. This makes it easy to test that things look right, and
49    debug problems.
50
51    In some cases a 'real' file must be used - these are also supplied in
52    the test/ diurectory.
53    """
54    @classmethod
55    def setUpClass(self):
56        # Handle the case where argv[0] is 'python'
57        self._binman_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
58        self._binman_pathname = os.path.join(self._binman_dir, 'binman')
59
60        # Create a temporary directory for input files
61        self._indir = tempfile.mkdtemp(prefix='binmant.')
62
63        # Create some test files
64        TestFunctional._MakeInputFile('u-boot.bin', U_BOOT_DATA)
65        TestFunctional._MakeInputFile('u-boot.img', U_BOOT_IMG_DATA)
66        TestFunctional._MakeInputFile('spl/u-boot-spl.bin', U_BOOT_SPL_DATA)
67        TestFunctional._MakeInputFile('blobfile', BLOB_DATA)
68        TestFunctional._MakeInputFile('u-boot.dtb', U_BOOT_DTB_DATA)
69        TestFunctional._MakeInputFile('u-boot-nodtb.bin', U_BOOT_NODTB_DATA)
70        self._output_setup = False
71
72    @classmethod
73    def tearDownClass(self):
74        """Remove the temporary input directory and its contents"""
75        if self._indir:
76            shutil.rmtree(self._indir)
77        self._indir = None
78
79    def setUp(self):
80        # Enable this to turn on debugging output
81        # tout.Init(tout.DEBUG)
82        command.test_result = None
83
84    def tearDown(self):
85        """Remove the temporary output directory"""
86        tools._FinaliseForTest()
87
88    def _RunBinman(self, *args, **kwargs):
89        """Run binman using the command line
90
91        Args:
92            Arguments to pass, as a list of strings
93            kwargs: Arguments to pass to Command.RunPipe()
94        """
95        result = command.RunPipe([[self._binman_pathname] + list(args)],
96                capture=True, capture_stderr=True, raise_on_error=False)
97        if result.return_code and kwargs.get('raise_on_error', True):
98            raise Exception("Error running '%s': %s" % (' '.join(args),
99                            result.stdout + result.stderr))
100        return result
101
102    def _DoBinman(self, *args):
103        """Run binman using directly (in the same process)
104
105        Args:
106            Arguments to pass, as a list of strings
107        Returns:
108            Return value (0 for success)
109        """
110        (options, args) = cmdline.ParseArgs(list(args))
111        options.pager = 'binman-invalid-pager'
112        options.build_dir = self._indir
113
114        # For testing, you can force an increase in verbosity here
115        # options.verbosity = tout.DEBUG
116        return control.Binman(options, args)
117
118    def _DoTestFile(self, fname):
119        """Run binman with a given test file
120
121        Args:
122            fname: Device tree source filename to use (e.g. 05_simple.dts)
123        """
124        return self._DoBinman('-p', '-I', self._indir,
125                              '-d', self.TestFile(fname))
126
127    def _SetupDtb(self, fname, outfile='u-boot.dtb'):
128        if not self._output_setup:
129            tools.PrepareOutputDir(self._indir, True)
130            self._output_setup = True
131        dtb = fdt_util.EnsureCompiled(self.TestFile(fname))
132        with open(dtb) as fd:
133            data = fd.read()
134            TestFunctional._MakeInputFile(outfile, data)
135
136    def _DoReadFile(self, fname, use_real_dtb=False):
137        """Run binman and return the resulting image
138
139        This runs binman with a given test file and then reads the resulting
140        output file. It is a shortcut function since most tests need to do
141        these steps.
142
143        Raises an assertion failure if binman returns a non-zero exit code.
144
145        Args:
146            fname: Device tree source filename to use (e.g. 05_simple.dts)
147            use_real_dtb: True to use the test file as the contents of
148                the u-boot-dtb entry. Normally this is not needed and the
149                test contents (the U_BOOT_DTB_DATA string) can be used.
150                But in some test we need the real contents.
151        """
152        # Use the compiled test file as the u-boot-dtb input
153        if use_real_dtb:
154            self._SetupDtb(fname)
155
156        try:
157            retcode = self._DoTestFile(fname)
158            self.assertEqual(0, retcode)
159
160            # Find the (only) image, read it and return its contents
161            image = control.images['image']
162            fname = tools.GetOutputFilename('image.bin')
163            self.assertTrue(os.path.exists(fname))
164            with open(fname) as fd:
165                return fd.read()
166        finally:
167            # Put the test file back
168            if use_real_dtb:
169                TestFunctional._MakeInputFile('u-boot.dtb', U_BOOT_DTB_DATA)
170
171    @classmethod
172    def _MakeInputFile(self, fname, contents):
173        """Create a new test input file, creating directories as needed
174
175        Args:
176            fname: Filenaem to create
177            contents: File contents to write in to the file
178        Returns:
179            Full pathname of file created
180        """
181        pathname = os.path.join(self._indir, fname)
182        dirname = os.path.dirname(pathname)
183        if dirname and not os.path.exists(dirname):
184            os.makedirs(dirname)
185        with open(pathname, 'wb') as fd:
186            fd.write(contents)
187        return pathname
188
189    @classmethod
190    def TestFile(self, fname):
191        return os.path.join(self._binman_dir, 'test', fname)
192
193    def AssertInList(self, grep_list, target):
194        """Assert that at least one of a list of things is in a target
195
196        Args:
197            grep_list: List of strings to check
198            target: Target string
199        """
200        for grep in grep_list:
201            if grep in target:
202                return
203        self.fail("Error: '%' not found in '%s'" % (grep_list, target))
204
205    def CheckNoGaps(self, entries):
206        """Check that all entries fit together without gaps
207
208        Args:
209            entries: List of entries to check
210        """
211        pos = 0
212        for entry in entries.values():
213            self.assertEqual(pos, entry.pos)
214            pos += entry.size
215
216    def testRun(self):
217        """Test a basic run with valid args"""
218        result = self._RunBinman('-h')
219
220    def testFullHelp(self):
221        """Test that the full help is displayed with -H"""
222        result = self._RunBinman('-H')
223        help_file = os.path.join(self._binman_dir, 'README')
224        self.assertEqual(len(result.stdout), os.path.getsize(help_file))
225        self.assertEqual(0, len(result.stderr))
226        self.assertEqual(0, result.return_code)
227
228    def testFullHelpInternal(self):
229        """Test that the full help is displayed with -H"""
230        try:
231            command.test_result = command.CommandResult()
232            result = self._DoBinman('-H')
233            help_file = os.path.join(self._binman_dir, 'README')
234        finally:
235            command.test_result = None
236
237    def testHelp(self):
238        """Test that the basic help is displayed with -h"""
239        result = self._RunBinman('-h')
240        self.assertTrue(len(result.stdout) > 200)
241        self.assertEqual(0, len(result.stderr))
242        self.assertEqual(0, result.return_code)
243
244    # Not yet available.
245    def testBoard(self):
246        """Test that we can run it with a specific board"""
247        self._SetupDtb('05_simple.dts', 'sandbox/u-boot.dtb')
248        TestFunctional._MakeInputFile('sandbox/u-boot.bin', U_BOOT_DATA)
249        result = self._DoBinman('-b', 'sandbox')
250        self.assertEqual(0, result)
251
252    def testNeedBoard(self):
253        """Test that we get an error when no board ius supplied"""
254        with self.assertRaises(ValueError) as e:
255            result = self._DoBinman()
256        self.assertIn("Must provide a board to process (use -b <board>)",
257                str(e.exception))
258
259    def testMissingDt(self):
260        """Test that an invalid device tree file generates an error"""
261        with self.assertRaises(Exception) as e:
262            self._RunBinman('-d', 'missing_file')
263        # We get one error from libfdt, and a different one from fdtget.
264        self.AssertInList(["Couldn't open blob from 'missing_file'",
265                           'No such file or directory'], str(e.exception))
266
267    def testBrokenDt(self):
268        """Test that an invalid device tree source file generates an error
269
270        Since this is a source file it should be compiled and the error
271        will come from the device-tree compiler (dtc).
272        """
273        with self.assertRaises(Exception) as e:
274            self._RunBinman('-d', self.TestFile('01_invalid.dts'))
275        self.assertIn("FATAL ERROR: Unable to parse input tree",
276                str(e.exception))
277
278    def testMissingNode(self):
279        """Test that a device tree without a 'binman' node generates an error"""
280        with self.assertRaises(Exception) as e:
281            self._DoBinman('-d', self.TestFile('02_missing_node.dts'))
282        self.assertIn("does not have a 'binman' node", str(e.exception))
283
284    def testEmpty(self):
285        """Test that an empty binman node works OK (i.e. does nothing)"""
286        result = self._RunBinman('-d', self.TestFile('03_empty.dts'))
287        self.assertEqual(0, len(result.stderr))
288        self.assertEqual(0, result.return_code)
289
290    def testInvalidEntry(self):
291        """Test that an invalid entry is flagged"""
292        with self.assertRaises(Exception) as e:
293            result = self._RunBinman('-d',
294                                     self.TestFile('04_invalid_entry.dts'))
295        #print e.exception
296        self.assertIn("Unknown entry type 'not-a-valid-type' in node "
297                "'/binman/not-a-valid-type'", str(e.exception))
298
299    def testSimple(self):
300        """Test a simple binman with a single file"""
301        data = self._DoReadFile('05_simple.dts')
302        self.assertEqual(U_BOOT_DATA, data)
303
304    def testDual(self):
305        """Test that we can handle creating two images
306
307        This also tests image padding.
308        """
309        retcode = self._DoTestFile('06_dual_image.dts')
310        self.assertEqual(0, retcode)
311
312        image = control.images['image1']
313        self.assertEqual(len(U_BOOT_DATA), image._size)
314        fname = tools.GetOutputFilename('image1.bin')
315        self.assertTrue(os.path.exists(fname))
316        with open(fname) as fd:
317            data = fd.read()
318            self.assertEqual(U_BOOT_DATA, data)
319
320        image = control.images['image2']
321        self.assertEqual(3 + len(U_BOOT_DATA) + 5, image._size)
322        fname = tools.GetOutputFilename('image2.bin')
323        self.assertTrue(os.path.exists(fname))
324        with open(fname) as fd:
325            data = fd.read()
326            self.assertEqual(U_BOOT_DATA, data[3:7])
327            self.assertEqual(chr(0) * 3, data[:3])
328            self.assertEqual(chr(0) * 5, data[7:])
329
330    def testBadAlign(self):
331        """Test that an invalid alignment value is detected"""
332        with self.assertRaises(ValueError) as e:
333            self._DoTestFile('07_bad_align.dts')
334        self.assertIn("Node '/binman/u-boot': Alignment 23 must be a power "
335                      "of two", str(e.exception))
336
337    def testPackSimple(self):
338        """Test that packing works as expected"""
339        retcode = self._DoTestFile('08_pack.dts')
340        self.assertEqual(0, retcode)
341        self.assertIn('image', control.images)
342        image = control.images['image']
343        entries = image._entries
344        self.assertEqual(5, len(entries))
345
346        # First u-boot
347        self.assertIn('u-boot', entries)
348        entry = entries['u-boot']
349        self.assertEqual(0, entry.pos)
350        self.assertEqual(len(U_BOOT_DATA), entry.size)
351
352        # Second u-boot, aligned to 16-byte boundary
353        self.assertIn('u-boot-align', entries)
354        entry = entries['u-boot-align']
355        self.assertEqual(16, entry.pos)
356        self.assertEqual(len(U_BOOT_DATA), entry.size)
357
358        # Third u-boot, size 23 bytes
359        self.assertIn('u-boot-size', entries)
360        entry = entries['u-boot-size']
361        self.assertEqual(20, entry.pos)
362        self.assertEqual(len(U_BOOT_DATA), entry.contents_size)
363        self.assertEqual(23, entry.size)
364
365        # Fourth u-boot, placed immediate after the above
366        self.assertIn('u-boot-next', entries)
367        entry = entries['u-boot-next']
368        self.assertEqual(43, entry.pos)
369        self.assertEqual(len(U_BOOT_DATA), entry.size)
370
371        # Fifth u-boot, placed at a fixed position
372        self.assertIn('u-boot-fixed', entries)
373        entry = entries['u-boot-fixed']
374        self.assertEqual(61, entry.pos)
375        self.assertEqual(len(U_BOOT_DATA), entry.size)
376
377        self.assertEqual(65, image._size)
378
379    def testPackExtra(self):
380        """Test that extra packing feature works as expected"""
381        retcode = self._DoTestFile('09_pack_extra.dts')
382
383        self.assertEqual(0, retcode)
384        self.assertIn('image', control.images)
385        image = control.images['image']
386        entries = image._entries
387        self.assertEqual(5, len(entries))
388
389        # First u-boot with padding before and after
390        self.assertIn('u-boot', entries)
391        entry = entries['u-boot']
392        self.assertEqual(0, entry.pos)
393        self.assertEqual(3, entry.pad_before)
394        self.assertEqual(3 + 5 + len(U_BOOT_DATA), entry.size)
395
396        # Second u-boot has an aligned size, but it has no effect
397        self.assertIn('u-boot-align-size-nop', entries)
398        entry = entries['u-boot-align-size-nop']
399        self.assertEqual(12, entry.pos)
400        self.assertEqual(4, entry.size)
401
402        # Third u-boot has an aligned size too
403        self.assertIn('u-boot-align-size', entries)
404        entry = entries['u-boot-align-size']
405        self.assertEqual(16, entry.pos)
406        self.assertEqual(32, entry.size)
407
408        # Fourth u-boot has an aligned end
409        self.assertIn('u-boot-align-end', entries)
410        entry = entries['u-boot-align-end']
411        self.assertEqual(48, entry.pos)
412        self.assertEqual(16, entry.size)
413
414        # Fifth u-boot immediately afterwards
415        self.assertIn('u-boot-align-both', entries)
416        entry = entries['u-boot-align-both']
417        self.assertEqual(64, entry.pos)
418        self.assertEqual(64, entry.size)
419
420        self.CheckNoGaps(entries)
421        self.assertEqual(128, image._size)
422
423    def testPackAlignPowerOf2(self):
424        """Test that invalid entry alignment is detected"""
425        with self.assertRaises(ValueError) as e:
426            self._DoTestFile('10_pack_align_power2.dts')
427        self.assertIn("Node '/binman/u-boot': Alignment 5 must be a power "
428                      "of two", str(e.exception))
429
430    def testPackAlignSizePowerOf2(self):
431        """Test that invalid entry size alignment is detected"""
432        with self.assertRaises(ValueError) as e:
433            self._DoTestFile('11_pack_align_size_power2.dts')
434        self.assertIn("Node '/binman/u-boot': Alignment size 55 must be a "
435                      "power of two", str(e.exception))
436
437    def testPackInvalidAlign(self):
438        """Test detection of an position that does not match its alignment"""
439        with self.assertRaises(ValueError) as e:
440            self._DoTestFile('12_pack_inv_align.dts')
441        self.assertIn("Node '/binman/u-boot': Position 0x5 (5) does not match "
442                      "align 0x4 (4)", str(e.exception))
443
444    def testPackInvalidSizeAlign(self):
445        """Test that invalid entry size alignment is detected"""
446        with self.assertRaises(ValueError) as e:
447            self._DoTestFile('13_pack_inv_size_align.dts')
448        self.assertIn("Node '/binman/u-boot': Size 0x5 (5) does not match "
449                      "align-size 0x4 (4)", str(e.exception))
450
451    def testPackOverlap(self):
452        """Test that overlapping regions are detected"""
453        with self.assertRaises(ValueError) as e:
454            self._DoTestFile('14_pack_overlap.dts')
455        self.assertIn("Node '/binman/u-boot-align': Position 0x3 (3) overlaps "
456                      "with previous entry '/binman/u-boot' ending at 0x4 (4)",
457                      str(e.exception))
458
459    def testPackEntryOverflow(self):
460        """Test that entries that overflow their size are detected"""
461        with self.assertRaises(ValueError) as e:
462            self._DoTestFile('15_pack_overflow.dts')
463        self.assertIn("Node '/binman/u-boot': Entry contents size is 0x4 (4) "
464                      "but entry size is 0x3 (3)", str(e.exception))
465
466    def testPackImageOverflow(self):
467        """Test that entries which overflow the image size are detected"""
468        with self.assertRaises(ValueError) as e:
469            self._DoTestFile('16_pack_image_overflow.dts')
470        self.assertIn("Image '/binman': contents size 0x4 (4) exceeds image "
471                      "size 0x3 (3)", str(e.exception))
472
473    def testPackImageSize(self):
474        """Test that the image size can be set"""
475        retcode = self._DoTestFile('17_pack_image_size.dts')
476        self.assertEqual(0, retcode)
477        self.assertIn('image', control.images)
478        image = control.images['image']
479        self.assertEqual(7, image._size)
480
481    def testPackImageSizeAlign(self):
482        """Test that image size alignemnt works as expected"""
483        retcode = self._DoTestFile('18_pack_image_align.dts')
484        self.assertEqual(0, retcode)
485        self.assertIn('image', control.images)
486        image = control.images['image']
487        self.assertEqual(16, image._size)
488
489    def testPackInvalidImageAlign(self):
490        """Test that invalid image alignment is detected"""
491        with self.assertRaises(ValueError) as e:
492            self._DoTestFile('19_pack_inv_image_align.dts')
493        self.assertIn("Image '/binman': Size 0x7 (7) does not match "
494                      "align-size 0x8 (8)", str(e.exception))
495
496    def testPackAlignPowerOf2(self):
497        """Test that invalid image alignment is detected"""
498        with self.assertRaises(ValueError) as e:
499            self._DoTestFile('20_pack_inv_image_align_power2.dts')
500        self.assertIn("Image '/binman': Alignment size 131 must be a power of "
501                      "two", str(e.exception))
502
503    def testImagePadByte(self):
504        """Test that the image pad byte can be specified"""
505        data = self._DoReadFile('21_image_pad.dts')
506        self.assertEqual(U_BOOT_SPL_DATA + (chr(0xff) * 9) + U_BOOT_DATA, data)
507
508    def testImageName(self):
509        """Test that image files can be named"""
510        retcode = self._DoTestFile('22_image_name.dts')
511        self.assertEqual(0, retcode)
512        image = control.images['image1']
513        fname = tools.GetOutputFilename('test-name')
514        self.assertTrue(os.path.exists(fname))
515
516        image = control.images['image2']
517        fname = tools.GetOutputFilename('test-name.xx')
518        self.assertTrue(os.path.exists(fname))
519
520    def testBlobFilename(self):
521        """Test that generic blobs can be provided by filename"""
522        data = self._DoReadFile('23_blob.dts')
523        self.assertEqual(BLOB_DATA, data)
524
525    def testPackSorted(self):
526        """Test that entries can be sorted"""
527        data = self._DoReadFile('24_sorted.dts')
528        self.assertEqual(chr(0) * 5 + U_BOOT_SPL_DATA + chr(0) * 2 +
529                         U_BOOT_DATA, data)
530
531    def testPackZeroPosition(self):
532        """Test that an entry at position 0 is not given a new position"""
533        with self.assertRaises(ValueError) as e:
534            self._DoTestFile('25_pack_zero_size.dts')
535        self.assertIn("Node '/binman/u-boot-spl': Position 0x0 (0) overlaps "
536                      "with previous entry '/binman/u-boot' ending at 0x4 (4)",
537                      str(e.exception))
538
539    def testPackUbootDtb(self):
540        """Test that a device tree can be added to U-Boot"""
541        data = self._DoReadFile('26_pack_u_boot_dtb.dts')
542        self.assertEqual(U_BOOT_NODTB_DATA + U_BOOT_DTB_DATA, data)
543