From 301e8038678a70762144c0e9de3513fca3a13cb8 Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Thu, 16 May 2013 13:53:28 +0000 Subject: [PATCH] sandbox: image: Create a test for loading FIT images The image code is fairly complex with various different options. It would be useful to have comprehensive tests for this. As a start, create a script which tries out loading a kernel/ramdisk/fdt from a FIT and checks that the images appear in the right place in memory. This uses sandbox which now supports bootm and related features. Signed-off-by: Simon Glass --- test/image/test-fit.py | 422 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 422 insertions(+) create mode 100755 test/image/test-fit.py diff --git a/test/image/test-fit.py b/test/image/test-fit.py new file mode 100755 index 0000000..c4e8211 --- /dev/null +++ b/test/image/test-fit.py @@ -0,0 +1,422 @@ +#!/usr/bin/python +# +# Copyright (c) 2013, Google Inc. +# +# Sanity check of the FIT handling in U-Boot +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307 USA +# +# To run this: +# +# make O=sandbox sandbox_config +# make O=sandbox +# ./test/image/test-fit.py -u sandbox/u-boot + +import doctest +from optparse import OptionParser +import os +import shutil +import struct +import sys +import tempfile + +# The 'command' library in patman is convenient for running commands +base_path = os.path.dirname(sys.argv[0]) +patman = os.path.join(base_path, '../../tools/patman') +sys.path.append(patman) + +import command + +# Define a base ITS which we can adjust using % and a dictionary +base_its = ''' +/dts-v1/; + +/ { + description = "Chrome OS kernel image with one or more FDT blobs"; + #address-cells = <1>; + + images { + kernel@1 { + data = /incbin/("%(kernel)s"); + type = "kernel"; + arch = "sandbox"; + os = "linux"; + compression = "none"; + load = <0x40000>; + entry = <0x8>; + }; + fdt@1 { + description = "snow"; + data = /incbin/("u-boot.dtb"); + type = "flat_dt"; + arch = "sandbox"; + %(fdt_load)s + compression = "none"; + signature@1 { + algo = "sha1,rsa2048"; + key-name-hint = "dev"; + }; + }; + ramdisk@1 { + description = "snow"; + data = /incbin/("%(ramdisk)s"); + type = "ramdisk"; + arch = "sandbox"; + os = "linux"; + %(ramdisk_load)s + compression = "none"; + }; + }; + configurations { + default = "conf@1"; + conf@1 { + kernel = "kernel@1"; + fdt = "fdt@1"; + %(ramdisk_config)s + }; + }; +}; +''' + +# Define a base FDT - currently we don't use anything in this +base_fdt = ''' +/dts-v1/; + +/ { + model = "Sandbox Verified Boot Test"; + compatible = "sandbox"; + +}; +''' + +# This is the U-Boot script that is run for each test. First load the fit, +# then do the 'bootm' command, then save out memory from the places where +# we expect 'bootm' to write things. Then quit. +base_script = ''' +sb load host 0 %(fit_addr)x %(fit)s +fdt addr %(fit_addr)x +bootm start %(fit_addr)x +bootm loados +sb save host 0 %(kernel_out)s %(kernel_addr)x %(kernel_size)x +sb save host 0 %(fdt_out)s %(fdt_addr)x %(fdt_size)x +sb save host 0 %(ramdisk_out)s %(ramdisk_addr)x %(ramdisk_size)x +reset +''' + +def make_fname(leaf): + """Make a temporary filename + + Args: + leaf: Leaf name of file to create (within temporary directory) + Return: + Temporary filename + """ + global base_dir + + return os.path.join(base_dir, leaf) + +def filesize(fname): + """Get the size of a file + + Args: + fname: Filename to check + Return: + Size of file in bytes + """ + return os.stat(fname).st_size + +def read_file(fname): + """Read the contents of a file + + Args: + fname: Filename to read + Returns: + Contents of file as a string + """ + with open(fname, 'r') as fd: + return fd.read() + +def make_dtb(): + """Make a sample .dts file and compile it to a .dtb + + Returns: + Filename of .dtb file created + """ + src = make_fname('u-boot.dts') + dtb = make_fname('u-boot.dtb') + with open(src, 'w') as fd: + print >>fd, base_fdt + command.Output('dtc', src, '-O', 'dtb', '-o', dtb) + return dtb + +def make_its(params): + """Make a sample .its file with parameters embedded + + Args: + params: Dictionary containing parameters to embed in the %() strings + Returns: + Filename of .its file created + """ + its = make_fname('test.its') + with open(its, 'w') as fd: + print >>fd, base_its % params + return its + +def make_fit(mkimage, params): + """Make a sample .fit file ready for loading + + This creates a .its script with the selected parameters and uses mkimage to + turn this into a .fit image. + + Args: + mkimage: Filename of 'mkimage' utility + params: Dictionary containing parameters to embed in the %() strings + Return: + Filename of .fit file created + """ + fit = make_fname('test.fit') + its = make_its(params) + command.Output(mkimage, '-f', its, fit) + with open(make_fname('u-boot.dts'), 'w') as fd: + print >>fd, base_fdt + return fit + +def make_kernel(): + """Make a sample kernel with test data + + Returns: + Filename of kernel created + """ + fname = make_fname('test-kernel.bin') + data = '' + for i in range(100): + data += 'this kernel %d is unlikely to boot\n' % i + with open(fname, 'w') as fd: + print >>fd, data + return fname + +def make_ramdisk(): + """Make a sample ramdisk with test data + + Returns: + Filename of ramdisk created + """ + fname = make_fname('test-ramdisk.bin') + data = '' + for i in range(100): + data += 'ramdisk %d was seldom used in the middle ages\n' % i + with open(fname, 'w') as fd: + print >>fd, data + return fname + +def find_matching(text, match): + """Find a match in a line of text, and return the unmatched line portion + + This is used to extract a part of a line from some text. The match string + is used to locate the line - we use the first line that contains that + match text. + + Once we find a match, we discard the match string itself from the line, + and return what remains. + + TODO: If this function becomes more generally useful, we could change it + to use regex and return groups. + + Args: + text: Text to check (each line separated by \n) + match: String to search for + Return: + String containing unmatched portion of line + Exceptions: + ValueError: If match is not found + + >>> find_matching('first line:10\\nsecond_line:20', 'first line:') + '10' + >>> find_matching('first line:10\\nsecond_line:20', 'second linex') + Traceback (most recent call last): + ... + ValueError: Test aborted + >>> find_matching('first line:10\\nsecond_line:20', 'second_line:') + '20' + """ + for line in text.splitlines(): + pos = line.find(match) + if pos != -1: + return line[:pos] + line[pos + len(match):] + + print "Expected '%s' but not found in output:" + print text + raise ValueError('Test aborted') + +def set_test(name): + """Set the name of the current test and print a message + + Args: + name: Name of test + """ + global test_name + + test_name = name + print name + +def fail(msg): + """Raise an error with a helpful failure message + + Args: + msg: Message to display + """ + raise ValueError("Test '%s' failed: %s" % (test_name, msg)) + +def run_fit_test(mkimage, u_boot): + """Basic sanity check of FIT loading in U-Boot + + TODO: Almost everything: + - hash algorithms - invalid hash/contents should be detected + - signature algorithms - invalid sig/contents should be detected + - compression + - checking that errors are detected like: + - image overwriting + - missing images + - invalid configurations + - incorrect os/arch/type fields + - empty data + - images too large/small + - invalid FDT (e.g. putting a random binary in instead) + - default configuration selection + - bootm command line parameters should have desired effect + - run code coverage to make sure we are testing all the code + """ + global test_name + + # Set up invariant files + control_dtb = make_dtb() + kernel = make_kernel() + ramdisk = make_ramdisk() + kernel_out = make_fname('kernel-out.bin') + fdt_out = make_fname('fdt-out.dtb') + ramdisk_out = make_fname('ramdisk-out.bin') + + # Set up basic parameters with default values + params = { + 'fit_addr' : 0x1000, + + 'kernel' : kernel, + 'kernel_out' : kernel_out, + 'kernel_addr' : 0x40000, + 'kernel_size' : filesize(kernel), + + 'fdt_out' : fdt_out, + 'fdt_addr' : 0x80000, + 'fdt_size' : filesize(control_dtb), + 'fdt_load' : '', + + 'ramdisk' : ramdisk, + 'ramdisk_out' : ramdisk_out, + 'ramdisk_addr' : 0xc0000, + 'ramdisk_size' : filesize(ramdisk), + 'ramdisk_load' : '', + 'ramdisk_config' : '', + } + + # Make a basic FIT and a script to load it + fit = make_fit(mkimage, params) + params['fit'] = fit + cmd = base_script % params + + # First check that we can load a kernel + # We could perhaps reduce duplication with some loss of readability + set_test('Kernel load') + stdout = command.Output(u_boot, '-d', control_dtb, '-c', cmd) + if read_file(kernel) != read_file(kernel_out): + fail('Kernel not loaded') + if read_file(control_dtb) == read_file(fdt_out): + fail('FDT loaded but should be ignored') + if read_file(ramdisk) == read_file(ramdisk_out): + fail('Ramdisk loaded but should not be') + + # Find out the offset in the FIT where U-Boot has found the FDT + line = find_matching(stdout, 'Booting using the fdt blob at ') + fit_offset = int(line, 16) - params['fit_addr'] + fdt_magic = struct.pack('>L', 0xd00dfeed) + data = read_file(fit) + + # Now find where it actually is in the FIT (skip the first word) + real_fit_offset = data.find(fdt_magic, 4) + if fit_offset != real_fit_offset: + fail('U-Boot loaded FDT from offset %#x, FDT is actually at %#x' % + (fit_offset, real_fit_offset)) + + # Now a kernel and an FDT + set_test('Kernel + FDT load') + params['fdt_load'] = 'load = <%#x>;' % params['fdt_addr'] + fit = make_fit(mkimage, params) + stdout = command.Output(u_boot, '-d', control_dtb, '-c', cmd) + if read_file(kernel) != read_file(kernel_out): + fail('Kernel not loaded') + if read_file(control_dtb) != read_file(fdt_out): + fail('FDT not loaded') + if read_file(ramdisk) == read_file(ramdisk_out): + fail('Ramdisk loaded but should not be') + + # Try a ramdisk + set_test('Kernel + FDT + Ramdisk load') + params['ramdisk_config'] = 'ramdisk = "ramdisk@1";' + params['ramdisk_load'] = 'load = <%#x>;' % params['ramdisk_addr'] + fit = make_fit(mkimage, params) + stdout = command.Output(u_boot, '-d', control_dtb, '-c', cmd) + if read_file(ramdisk) != read_file(ramdisk_out): + fail('Ramdisk not loaded') + +def run_tests(): + """Parse options, run the FIT tests and print the result""" + global base_path, base_dir + + # Work in a temporary directory + base_dir = tempfile.mkdtemp() + parser = OptionParser() + parser.add_option('-u', '--u-boot', + default=os.path.join(base_path, 'u-boot'), + help='Select U-Boot sandbox binary') + parser.add_option('-k', '--keep', action='store_true', + help="Don't delete temporary directory even when tests pass") + parser.add_option('-t', '--selftest', action='store_true', + help='Run internal self tests') + (options, args) = parser.parse_args() + + # Find the path to U-Boot, and assume mkimage is in its tools/mkimage dir + base_path = os.path.dirname(options.u_boot) + mkimage = os.path.join(base_path, 'tools/mkimage') + + # There are a few doctests - handle these here + if options.selftest: + doctest.testmod() + return + + title = 'FIT Tests' + print title, '\n', '=' * len(title) + + run_fit_test(mkimage, options.u_boot) + + print '\nTests passed' + print 'Caveat: this is only a sanity check - test coverage is poor' + + # Remove the tempoerary directory unless we are asked to keep it + if options.keep: + print "Output files are in '%s'" % base_dir + else: + shutil.rmtree(base_dir) + +run_tests()