From: Stefan Brüns <stefan.bru...@rwth-aachen.de> The following checks are currently implemented: 1. listing a directory 2. verifying size of a file 3. veryfying md5sum for a file region 4. reading the beginning of a file
Signed-off-by: Stefan Brüns <stefan.bru...@rwth-aachen.de> --- test/py/tests/test_fs.py | 357 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 357 insertions(+) create mode 100644 test/py/tests/test_fs.py diff --git a/test/py/tests/test_fs.py b/test/py/tests/test_fs.py new file mode 100644 index 0000000000..7aaf8debaf --- /dev/null +++ b/test/py/tests/test_fs.py @@ -0,0 +1,357 @@ +# Copyright (c) 2016, Stefan Bruens <stefan.bru...@rwth-aachen.de> +# +# SPDX-License-Identifier: GPL-2.0 + +# Test U-Boot's filesystem implementations +# +# The tests are currently covering the ext4 and fat filesystem implementations. +# +# Following functionality is currently checked: +# - Listing a directory and checking for a set of files +# - Verifying the size of a file +# - Reading sparse and non-sparse files +# - File content verification using md5sums + + +from distutils.spawn import find_executable +import hashlib +import pytest +import os +import random +import re +import u_boot_utils as util + +@pytest.fixture(scope='session') +def prereq_commands(): + """Detect required commands to run file system tests.""" + for command in ['mkfs', 'mount', 'umount']: + if find_executable(command) is None: + pytest.skip('Filesystem tests, "{0}" not in PATH'.format(command)) + +""" +Scenarios: + hostfs: access image contents through the sandbox hostfs + facility, using the filesytem implementation of + the sandbox host, e.g. Linux kernel + generic: test u-boots native filesystem implementations, + using the 'generic' command names, e.g. 'load' + TODO - + fscommands: test u-boots native filesystem implementations, + using the fs specific commands, e.g. 'ext4load' +""" +@pytest.fixture(scope='class', params=['generic', 'hostfs']) +def scenario(request): + request.cls.scenario = request.param + return request.param + + +""" +Dictionary of files to use during filesystem tests. The files +are keyed by the filenames. The value is an array of strides, each tuple +contains the the start offset (inclusive) and end offset (exclusive). +""" +files = { + 'empty.file' : [(0, 0)], + '1MB.file' : [(0, 1e6)], + '1MB.sparse.file' : [(1e6-1, 1e6)], + '32MB.sparse.file' : [(0, 1e6), (4e6, 5e6), (31e6, 32e6)], + # Creating a 2.5 GB file on FAT is exceptionally slow, disable it for now + # '2_5GB.sparse.file' : [(0, 1e6), (1e9, 1e9+1e6), (2.5e9-1e6, 2.5e9)], +} + +"""Options to pass to mkfs.""" +mkfs_opts = { + 'fat' :'-t vfat', + 'ext4' : '-t ext4 -F', +} + +class FsImage: + def __init__(self, fstype, imagepath, mountpath): + """Create a new filesystem image. + + Args: + fstype: filesystem type (string) + imagepath: full path to image file + mountpath: mountpoint directory + """ + self.fstype = fstype + self.imagepath = imagepath + self.mountpath = mountpath + self.md5s = {} + with open(self.imagepath, 'w') as fd: + fd.truncate(0) + fd.seek(3e9) + fd.write(bytes([0])) + + def mkfs(self, log): + mkfsopts = mkfs_opts.get(self.fstype) + util.run_and_log(log, + 'mkfs {0} {1}'.format(mkfsopts, self.imagepath)) + + def create_file(self, log, filename, strides): + """Create a single file in the filesystem. Each file + is defined by one or more strides, which is filled with + random data. For each stride, the md5sum is calculated + and stored. + """ + md5sums = [] + with open(self.mountpath + '/' + filename, 'w') as fd: + for stride in strides: + length = int(stride[1] - stride[0]) + data = bytearray(random.getrandbits(8) for _ in xrange(length)) + md5 = hashlib.md5(data).hexdigest() + md5sums.append(md5) + log.info('{0}: write {1} bytes @ {2} : {3}'.format( + filename, int(stride[1] - stride[0]), + int(stride[0]), md5)) + fd.seek(stride[0]) + fd.write(data); + self.md5s[filename] = md5sums + + def create_files(self, log): + with log.section('Create initial files'): + for filename in files: + self.create_file(log, filename, files[filename]) + log.info('Created test files in "{0}"'.format(self.mountpath)) + util.run_and_log(log, 'ls -la {0}'.format(self.mountpath)) + util.run_and_log(log, 'sync {0}'.format(self.mountpath)) + + def mount(self, log): + if not os.path.exists(self.mountpath): + os.mkdir(self.mountpath) + log.info('Mounting {0} at {1}'.format(self.imagepath, self.mountpath)) + if self.fstype == 'ext4': + cmd = 'sudo -n mount -o loop,rw {0} {1}'.format(self.imagepath, self.mountpath) + else: + cmd = 'sudo -n mount -o loop,rw,umask=000 {0} {1}'.format(self.imagepath, self.mountpath) + util.run_and_log(log, cmd) + if self.fstype == 'ext4': + cmd = 'sudo -n chmod og+rw {0}'.format(self.mountpath) + return util.run_and_log(log, cmd) + + def unmount(self, log): + log.info('Unmounting {0}'.format(self.imagepath)) + cmd = 'sudo -n umount -l {0}'.format(self.mountpath) + util.run_and_log(log, cmd, ignore_errors=True) + + +@pytest.fixture(scope='module', params=['fat', 'ext4']) +def fsimage(prereq_commands, u_boot_config, u_boot_log, request): + """Filesystem image instance.""" + datadir = u_boot_config.result_dir + '/' + fstype = request.param + imagepath = datadir + '3GB.' + fstype + '.img' + mountpath = datadir + 'mnt_' + fstype + + with u_boot_log.section('Create image "{0}"'.format(imagepath)): + fsimage = FsImage(fstype, imagepath, mountpath) + fsimage.mkfs(u_boot_log) + + yield fsimage + fsimage.unmount(u_boot_log) + +@pytest.fixture(scope='module') +def populated_image(fsimage, u_boot_log): + """Filesystem populated with files required for tests.""" + try: + fsimage.mount(u_boot_log) + except Exception as e: + pytest.skip('{0}: could not mount "{1}"'.format( + fsimage.fstype, fsimage.imagepath)) + yield None + + fsimage.create_files(u_boot_log) + fsimage.unmount(u_boot_log) + yield fsimage + +@pytest.fixture(scope='function') +def boundimage(populated_image, u_boot_console, request): + """Filesystem image instance which is accessible from inside + the running U-Boot instance.""" + image = populated_image + request.cls.image = image + if request.cls.scenario == 'hostfs': + image.mount(u_boot_console.log) + image.rootpath = image.mountpath + '/' + yield image + image.unmount(u_boot_console.log) + else: + output = u_boot_console.run_command_list( + ['host bind 0 {0}'.format(image.imagepath)]) + image.rootpath = '/' + yield image + output = u_boot_console.run_command_list(['host bind 0 ']) + + +def test_fs_prepare_image(u_boot_config, fsimage, request): + """Dummy test to create an image file with filesystem. + Useful to isolate fixture setup from actual tests.""" + if not fsimage: + pytest.fail('Failed to create image') + +def test_fs_populate_image(populated_image, request): + """Dummy test to create initial filesystem contents.""" + if not populated_image: + pytest.fail('Failed create initial image content') + +@pytest.mark.usefixtures('u_boot_console', 'scenario', 'boundimage') +class TestFilesystems: + ignore_cleanup_errors = True + filesize_regex = re.compile('^filesize=([A-Fa-f0-9]+)') + md5sum_regex = re.compile('^md5 for .* ==> ([A-Fa-f0-9]{32})') + dirlist_regex = re.compile('\s+(\d+)\s+(\S+.file\S*)') + + commands = { + 'fat' : { + 'listcmd' : 'ls', + 'readcmd' : 'load', + 'sizecmd' : 'size', + 'writecmd' : 'size', + }, + 'ext4' : { + 'listcmd' : 'ls', + 'readcmd' : 'load', + 'sizecmd' : 'size', + 'writecmd' : 'size', + }, + } + + cmd_parameters = { + 'hostfs' : { + 'prefix' : 'host ', + 'interface' : 'hostfs -', + }, + 'generic' : { + 'prefix' : '', + 'interface' : 'host 0:0', + }, + } + + def get_filesize(self, filename): + """Get the size of the given file.""" + strides = files[filename] + return int(strides[-1][1]) + + def check_dirlist(self, string, filenames): + """Check if the output string returned by a list command + contains all given filenames.""" + m = self.dirlist_regex.findall(string) + assert(m) + for i, e in enumerate(m): + m[i] = (int(e[0]), e[1].lower()) + for f in filenames: + e = (self.get_filesize(f), f.lower()) + assert(e in m) + + def check_filesize(self, string, size): + """Check if the output string returned by a size command + matches the expected file size.""" + m = self.filesize_regex.match(string) + assert(m) + assert(int(m.group(1), 16) == size) + + def check_md5sum(self, string, md5): + """Check if the output string returned by the md5sum + command matches the expected md5.""" + m = self.md5sum_regex.match(string) + assert(m) + assert(len(m.group(1)) == 32) + assert(m.group(1) == md5) + + def run_listcmd(self, dirname): + """Run the scenario and filesystem specific list command + for the given directory name.""" + cmd = '{0}{1} {2} {3}'.format( + self.fs_params.get('prefix'), + self.fs_commands.get('listcmd'), + self.fs_params.get('interface'), + self.image.rootpath + dirname) + with self.console.log.section('List "{0}"'.format(dirname)): + output = self.console.run_command_list([cmd]) + return output[0] + + def run_readcmd(self, filename, offset, length): + """Run the scenario and filesystem specific read command + for the given file.""" + cmd = '{0}{1} {2} {3} {4} 0x{5:x} 0x{6:x}'.format( + self.fs_params.get('prefix'), + self.fs_commands.get('readcmd'), + self.fs_params.get('interface'), + '0', # address + self.image.rootpath + filename, + length, offset) + with self.console.log.section('Read file "{0}"'.format(filename)): + output = self.console.run_command_list( + [cmd, 'env print filesize', + 'md5sum 0 $filesize', 'env set filesize']) + return output[1:3] + + def run_sizecmd(self, filename): + """Run the scenario and filesystem specific size command + for the given file.""" + cmd = '{0}{1} {2} {3}'.format( + self.fs_params.get('prefix'), + self.fs_commands.get('sizecmd'), + self.fs_params.get('interface'), + self.image.rootpath + filename) + with self.console.log.section('Get size of "{0}"'.format(filename)): + output = self.console.run_command_list( + [cmd, 'env print filesize', 'env set filesize']) + return output[1] + + def setup(self): + self.fs_params = self.cmd_parameters.get(self.scenario) + self.fs_commands = self.commands.get(self.image.fstype) + + @pytest.mark.parametrize('dirname', ['', './']) + def test_fs_ls(self, u_boot_console, dirname): + """Check the contents of the given directory.""" + if self.image.fstype == 'fat' and dirname == './': + pytest.skip("FAT has no '.' entry in the root directory") + self.console = u_boot_console + self.setup() + + output = self.run_listcmd(dirname) + self.check_dirlist(output, files.keys()) + + @pytest.mark.parametrize('filename', files.keys()) + def test_fs_filesize(self, u_boot_console, filename): + """Check the filesize of the given file.""" + self.console = u_boot_console + self.setup() + + filesize = self.get_filesize(filename) + + output = self.run_sizecmd(filename) + self.check_filesize(output, filesize) + + @pytest.mark.parametrize('filename', files.keys()) + def test_fs_read(self, u_boot_console, filename): + """Read all defined strides of the given file and checks + its contents.""" + self.console = u_boot_console + self.setup() + + md5s = self.image.md5s[filename] + + for i, stride in enumerate(files[filename]): + length = int(stride[1]) - int(stride[0]) + offset = int(stride[0]) + output = self.run_readcmd(filename, offset, length) + self.check_filesize(output[0], length) + self.console.log.info('md5: {0}'.format(md5s[i])) + self.check_md5sum(output[1], md5s[i]) + + @pytest.mark.parametrize('filename', files.keys()) + def test_fs_read_head(self, u_boot_console, filename): + """Check reading the head of the given file, up to the first + 4 Megabyte (or less for smaller files). Also reads sparse + regions of a file.""" + self.console = u_boot_console + self.setup() + + filesize = self.get_filesize(filename) + filesize = min(filesize, int(4e6)) + + output = self.run_readcmd(filename, 0, filesize) + self.check_filesize(output[0], filesize) -- 2.11.0 _______________________________________________ U-Boot mailing list U-Boot@lists.denx.de http://lists.denx.de/mailman/listinfo/u-boot