1# SPDX-License-Identifier:      GPL-2.0+
2# Copyright (c) 2018, Linaro Limited
3# Author: Takahiro Akashi <takahiro.akashi@linaro.org>
4
5import os
6import os.path
7import pytest
8import re
9from subprocess import call, check_call, check_output, CalledProcessError
10from fstest_defs import *
11
12supported_fs_basic = ['fat16', 'fat32', 'ext4']
13supported_fs_ext = ['fat16', 'fat32']
14supported_fs_mkdir = ['fat16', 'fat32']
15supported_fs_unlink = ['fat16', 'fat32']
16supported_fs_symlink = ['ext4']
17
18#
19# Filesystem test specific setup
20#
21def pytest_addoption(parser):
22    """Enable --fs-type option.
23
24    See pytest_configure() about how it works.
25
26    Args:
27        parser: Pytest command-line parser.
28
29    Returns:
30        Nothing.
31    """
32    parser.addoption('--fs-type', action='append', default=None,
33        help='Targeting Filesystem Types')
34
35def pytest_configure(config):
36    """Restrict a file system(s) to be tested.
37
38    A file system explicitly named with --fs-type option is selected
39    if it belongs to a default supported_fs_xxx list.
40    Multiple options can be specified.
41
42    Args:
43        config: Pytest configuration.
44
45    Returns:
46        Nothing.
47    """
48    global supported_fs_basic
49    global supported_fs_ext
50    global supported_fs_mkdir
51    global supported_fs_unlink
52    global supported_fs_symlink
53
54    def intersect(listA, listB):
55        return  [x for x in listA if x in listB]
56
57    supported_fs = config.getoption('fs_type')
58    if supported_fs:
59        print('*** FS TYPE modified: %s' % supported_fs)
60        supported_fs_basic =  intersect(supported_fs, supported_fs_basic)
61        supported_fs_ext =  intersect(supported_fs, supported_fs_ext)
62        supported_fs_mkdir =  intersect(supported_fs, supported_fs_mkdir)
63        supported_fs_unlink =  intersect(supported_fs, supported_fs_unlink)
64        supported_fs_symlink =  intersect(supported_fs, supported_fs_symlink)
65
66def pytest_generate_tests(metafunc):
67    """Parametrize fixtures, fs_obj_xxx
68
69    Each fixture will be parametrized with a corresponding support_fs_xxx
70    list.
71
72    Args:
73        metafunc: Pytest test function.
74
75    Returns:
76        Nothing.
77    """
78    if 'fs_obj_basic' in metafunc.fixturenames:
79        metafunc.parametrize('fs_obj_basic', supported_fs_basic,
80            indirect=True, scope='module')
81    if 'fs_obj_ext' in metafunc.fixturenames:
82        metafunc.parametrize('fs_obj_ext', supported_fs_ext,
83            indirect=True, scope='module')
84    if 'fs_obj_mkdir' in metafunc.fixturenames:
85        metafunc.parametrize('fs_obj_mkdir', supported_fs_mkdir,
86            indirect=True, scope='module')
87    if 'fs_obj_unlink' in metafunc.fixturenames:
88        metafunc.parametrize('fs_obj_unlink', supported_fs_unlink,
89            indirect=True, scope='module')
90    if 'fs_obj_symlink' in metafunc.fixturenames:
91        metafunc.parametrize('fs_obj_symlink', supported_fs_symlink,
92            indirect=True, scope='module')
93
94#
95# Helper functions
96#
97def fstype_to_ubname(fs_type):
98    """Convert a file system type to an U-boot specific string
99
100    A generated string can be used as part of file system related commands
101    or a config name in u-boot. Currently fat16 and fat32 are handled
102    specifically.
103
104    Args:
105        fs_type: File system type.
106
107    Return:
108        A corresponding string for file system type.
109    """
110    if re.match('fat', fs_type):
111        return 'fat'
112    else:
113        return fs_type
114
115def check_ubconfig(config, fs_type):
116    """Check whether a file system is enabled in u-boot configuration.
117
118    This function is assumed to be called in a fixture function so that
119    the whole test cases will be skipped if a given file system is not
120    enabled.
121
122    Args:
123        fs_type: File system type.
124
125    Return:
126        Nothing.
127    """
128    if not config.buildconfig.get('config_cmd_%s' % fs_type, None):
129        pytest.skip('.config feature "CMD_%s" not enabled' % fs_type.upper())
130    if not config.buildconfig.get('config_%s_write' % fs_type, None):
131        pytest.skip('.config feature "%s_WRITE" not enabled'
132        % fs_type.upper())
133
134def mk_fs(config, fs_type, size, id):
135    """Create a file system volume.
136
137    Args:
138        fs_type: File system type.
139        size: Size of file system in MiB.
140        id: Prefix string of volume's file name.
141
142    Return:
143        Nothing.
144    """
145    fs_img = '%s.%s.img' % (id, fs_type)
146    fs_img = config.persistent_data_dir + '/' + fs_img
147
148    if fs_type == 'fat16':
149        mkfs_opt = '-F 16'
150    elif fs_type == 'fat32':
151        mkfs_opt = '-F 32'
152    else:
153        mkfs_opt = ''
154
155    if re.match('fat', fs_type):
156        fs_lnxtype = 'vfat'
157    else:
158        fs_lnxtype = fs_type
159
160    count = (size + 1048576 - 1) / 1048576
161
162    try:
163        check_call('rm -f %s' % fs_img, shell=True)
164        check_call('dd if=/dev/zero of=%s bs=1M count=%d'
165            % (fs_img, count), shell=True)
166        check_call('mkfs.%s %s %s'
167            % (fs_lnxtype, mkfs_opt, fs_img), shell=True)
168        if fs_type == 'ext4':
169            sb_content = check_output('tune2fs -l %s' % fs_img, shell=True).decode()
170            if 'metadata_csum' in sb_content:
171                check_call('tune2fs -O ^metadata_csum %s' % fs_img, shell=True)
172        return fs_img
173    except CalledProcessError:
174        call('rm -f %s' % fs_img, shell=True)
175        raise
176
177# from test/py/conftest.py
178def tool_is_in_path(tool):
179    """Check whether a given command is available on host.
180
181    Args:
182        tool: Command name.
183
184    Return:
185        True if available, False if not.
186    """
187    for path in os.environ['PATH'].split(os.pathsep):
188        fn = os.path.join(path, tool)
189        if os.path.isfile(fn) and os.access(fn, os.X_OK):
190            return True
191    return False
192
193fuse_mounted = False
194
195def mount_fs(fs_type, device, mount_point):
196    """Mount a volume.
197
198    Args:
199        fs_type: File system type.
200        device: Volume's file name.
201        mount_point: Mount point.
202
203    Return:
204        Nothing.
205    """
206    global fuse_mounted
207
208    fuse_mounted = False
209    try:
210        if tool_is_in_path('guestmount'):
211            fuse_mounted = True
212            check_call('guestmount -a %s -m /dev/sda %s'
213                % (device, mount_point), shell=True)
214        else:
215            mount_opt = 'loop,rw'
216            if re.match('fat', fs_type):
217                mount_opt += ',umask=0000'
218
219            check_call('sudo mount -o %s %s %s'
220                % (mount_opt, device, mount_point), shell=True)
221
222            # may not be effective for some file systems
223            check_call('sudo chmod a+rw %s' % mount_point, shell=True)
224    except CalledProcessError:
225        raise
226
227def umount_fs(mount_point):
228    """Unmount a volume.
229
230    Args:
231        mount_point: Mount point.
232
233    Return:
234        Nothing.
235    """
236    if fuse_mounted:
237        call('sync')
238        call('guestunmount %s' % mount_point, shell=True)
239    else:
240        call('sudo umount %s' % mount_point, shell=True)
241
242#
243# Fixture for basic fs test
244#     derived from test/fs/fs-test.sh
245#
246@pytest.fixture()
247def fs_obj_basic(request, u_boot_config):
248    """Set up a file system to be used in basic fs test.
249
250    Args:
251        request: Pytest request object.
252	u_boot_config: U-boot configuration.
253
254    Return:
255        A fixture for basic fs test, i.e. a triplet of file system type,
256        volume file name and  a list of MD5 hashes.
257    """
258    fs_type = request.param
259    fs_img = ''
260
261    fs_ubtype = fstype_to_ubname(fs_type)
262    check_ubconfig(u_boot_config, fs_ubtype)
263
264    mount_dir = u_boot_config.persistent_data_dir + '/mnt'
265
266    small_file = mount_dir + '/' + SMALL_FILE
267    big_file = mount_dir + '/' + BIG_FILE
268
269    try:
270
271        # 3GiB volume
272        fs_img = mk_fs(u_boot_config, fs_type, 0xc0000000, '3GB')
273    except CalledProcessError as err:
274        pytest.skip('Creating failed for filesystem: ' + fs_type + '. {}'.format(err))
275        return
276
277    try:
278        check_call('mkdir -p %s' % mount_dir, shell=True)
279    except CalledProcessError as err:
280        pytest.skip('Preparing mount folder failed for filesystem: ' + fs_type + '. {}'.format(err))
281        return
282    finally:
283        call('rm -f %s' % fs_img, shell=True)
284
285    try:
286        # Mount the image so we can populate it.
287        mount_fs(fs_type, fs_img, mount_dir)
288
289        # Create a subdirectory.
290        check_call('mkdir %s/SUBDIR' % mount_dir, shell=True)
291
292        # Create big file in this image.
293        # Note that we work only on the start 1MB, couple MBs in the 2GB range
294        # and the last 1 MB of the huge 2.5GB file.
295        # So, just put random values only in those areas.
296        check_call('dd if=/dev/urandom of=%s bs=1M count=1'
297	    % big_file, shell=True)
298        check_call('dd if=/dev/urandom of=%s bs=1M count=2 seek=2047'
299            % big_file, shell=True)
300        check_call('dd if=/dev/urandom of=%s bs=1M count=1 seek=2499'
301            % big_file, shell=True)
302
303        # Create a small file in this image.
304        check_call('dd if=/dev/urandom of=%s bs=1M count=1'
305	    % small_file, shell=True)
306
307        # Delete the small file copies which possibly are written as part of a
308        # previous test.
309        # check_call('rm -f "%s.w"' % MB1, shell=True)
310        # check_call('rm -f "%s.w2"' % MB1, shell=True)
311
312        # Generate the md5sums of reads that we will test against small file
313        out = check_output(
314            'dd if=%s bs=1M skip=0 count=1 2> /dev/null | md5sum'
315	    % small_file, shell=True).decode()
316        md5val = [ out.split()[0] ]
317
318        # Generate the md5sums of reads that we will test against big file
319        # One from beginning of file.
320        out = check_output(
321            'dd if=%s bs=1M skip=0 count=1 2> /dev/null | md5sum'
322	    % big_file, shell=True).decode()
323        md5val.append(out.split()[0])
324
325        # One from end of file.
326        out = check_output(
327            'dd if=%s bs=1M skip=2499 count=1 2> /dev/null | md5sum'
328	    % big_file, shell=True).decode()
329        md5val.append(out.split()[0])
330
331        # One from the last 1MB chunk of 2GB
332        out = check_output(
333            'dd if=%s bs=1M skip=2047 count=1 2> /dev/null | md5sum'
334	    % big_file, shell=True).decode()
335        md5val.append(out.split()[0])
336
337        # One from the start 1MB chunk from 2GB
338        out = check_output(
339            'dd if=%s bs=1M skip=2048 count=1 2> /dev/null | md5sum'
340	    % big_file, shell=True).decode()
341        md5val.append(out.split()[0])
342
343        # One 1MB chunk crossing the 2GB boundary
344        out = check_output(
345            'dd if=%s bs=512K skip=4095 count=2 2> /dev/null | md5sum'
346	    % big_file, shell=True).decode()
347        md5val.append(out.split()[0])
348
349    except CalledProcessError as err:
350        pytest.skip('Setup failed for filesystem: ' + fs_type + '. {}'.format(err))
351        return
352    else:
353        yield [fs_ubtype, fs_img, md5val]
354    finally:
355        umount_fs(mount_dir)
356        call('rmdir %s' % mount_dir, shell=True)
357        call('rm -f %s' % fs_img, shell=True)
358
359#
360# Fixture for extended fs test
361#
362@pytest.fixture()
363def fs_obj_ext(request, u_boot_config):
364    """Set up a file system to be used in extended fs test.
365
366    Args:
367        request: Pytest request object.
368	u_boot_config: U-boot configuration.
369
370    Return:
371        A fixture for extended fs test, i.e. a triplet of file system type,
372        volume file name and  a list of MD5 hashes.
373    """
374    fs_type = request.param
375    fs_img = ''
376
377    fs_ubtype = fstype_to_ubname(fs_type)
378    check_ubconfig(u_boot_config, fs_ubtype)
379
380    mount_dir = u_boot_config.persistent_data_dir + '/mnt'
381
382    min_file = mount_dir + '/' + MIN_FILE
383    tmp_file = mount_dir + '/tmpfile'
384
385    try:
386
387        # 128MiB volume
388        fs_img = mk_fs(u_boot_config, fs_type, 0x8000000, '128MB')
389    except CalledProcessError as err:
390        pytest.skip('Creating failed for filesystem: ' + fs_type + '. {}'.format(err))
391        return
392
393    try:
394        check_call('mkdir -p %s' % mount_dir, shell=True)
395    except CalledProcessError as err:
396        pytest.skip('Preparing mount folder failed for filesystem: ' + fs_type + '. {}'.format(err))
397        return
398    finally:
399        call('rm -f %s' % fs_img, shell=True)
400
401    try:
402        # Mount the image so we can populate it.
403        mount_fs(fs_type, fs_img, mount_dir)
404
405        # Create a test directory
406        check_call('mkdir %s/dir1' % mount_dir, shell=True)
407
408        # Create a small file and calculate md5
409        check_call('dd if=/dev/urandom of=%s bs=1K count=20'
410            % min_file, shell=True)
411        out = check_output(
412            'dd if=%s bs=1K 2> /dev/null | md5sum'
413            % min_file, shell=True).decode()
414        md5val = [ out.split()[0] ]
415
416        # Calculate md5sum of Test Case 4
417        check_call('dd if=%s of=%s bs=1K count=20'
418            % (min_file, tmp_file), shell=True)
419        check_call('dd if=%s of=%s bs=1K seek=5 count=20'
420            % (min_file, tmp_file), shell=True)
421        out = check_output('dd if=%s bs=1K 2> /dev/null | md5sum'
422            % tmp_file, shell=True).decode()
423        md5val.append(out.split()[0])
424
425        # Calculate md5sum of Test Case 5
426        check_call('dd if=%s of=%s bs=1K count=20'
427            % (min_file, tmp_file), shell=True)
428        check_call('dd if=%s of=%s bs=1K seek=5 count=5'
429            % (min_file, tmp_file), shell=True)
430        out = check_output('dd if=%s bs=1K 2> /dev/null | md5sum'
431            % tmp_file, shell=True).decode()
432        md5val.append(out.split()[0])
433
434        # Calculate md5sum of Test Case 7
435        check_call('dd if=%s of=%s bs=1K count=20'
436            % (min_file, tmp_file), shell=True)
437        check_call('dd if=%s of=%s bs=1K seek=20 count=20'
438            % (min_file, tmp_file), shell=True)
439        out = check_output('dd if=%s bs=1K 2> /dev/null | md5sum'
440            % tmp_file, shell=True).decode()
441        md5val.append(out.split()[0])
442
443        check_call('rm %s' % tmp_file, shell=True)
444    except CalledProcessError:
445        pytest.skip('Setup failed for filesystem: ' + fs_type)
446        return
447    else:
448        yield [fs_ubtype, fs_img, md5val]
449    finally:
450        umount_fs(mount_dir)
451        call('rmdir %s' % mount_dir, shell=True)
452        call('rm -f %s' % fs_img, shell=True)
453
454#
455# Fixture for mkdir test
456#
457@pytest.fixture()
458def fs_obj_mkdir(request, u_boot_config):
459    """Set up a file system to be used in mkdir test.
460
461    Args:
462        request: Pytest request object.
463	u_boot_config: U-boot configuration.
464
465    Return:
466        A fixture for mkdir test, i.e. a duplet of file system type and
467        volume file name.
468    """
469    fs_type = request.param
470    fs_img = ''
471
472    fs_ubtype = fstype_to_ubname(fs_type)
473    check_ubconfig(u_boot_config, fs_ubtype)
474
475    try:
476        # 128MiB volume
477        fs_img = mk_fs(u_boot_config, fs_type, 0x8000000, '128MB')
478    except:
479        pytest.skip('Setup failed for filesystem: ' + fs_type)
480        return
481    else:
482        yield [fs_ubtype, fs_img]
483    call('rm -f %s' % fs_img, shell=True)
484
485#
486# Fixture for unlink test
487#
488@pytest.fixture()
489def fs_obj_unlink(request, u_boot_config):
490    """Set up a file system to be used in unlink test.
491
492    Args:
493        request: Pytest request object.
494	u_boot_config: U-boot configuration.
495
496    Return:
497        A fixture for unlink test, i.e. a duplet of file system type and
498        volume file name.
499    """
500    fs_type = request.param
501    fs_img = ''
502
503    fs_ubtype = fstype_to_ubname(fs_type)
504    check_ubconfig(u_boot_config, fs_ubtype)
505
506    mount_dir = u_boot_config.persistent_data_dir + '/mnt'
507
508    try:
509
510        # 128MiB volume
511        fs_img = mk_fs(u_boot_config, fs_type, 0x8000000, '128MB')
512    except CalledProcessError as err:
513        pytest.skip('Creating failed for filesystem: ' + fs_type + '. {}'.format(err))
514        return
515
516    try:
517        check_call('mkdir -p %s' % mount_dir, shell=True)
518    except CalledProcessError as err:
519        pytest.skip('Preparing mount folder failed for filesystem: ' + fs_type + '. {}'.format(err))
520        return
521    finally:
522        call('rm -f %s' % fs_img, shell=True)
523
524    try:
525        # Mount the image so we can populate it.
526        mount_fs(fs_type, fs_img, mount_dir)
527
528        # Test Case 1 & 3
529        check_call('mkdir %s/dir1' % mount_dir, shell=True)
530        check_call('dd if=/dev/urandom of=%s/dir1/file1 bs=1K count=1'
531                                    % mount_dir, shell=True)
532        check_call('dd if=/dev/urandom of=%s/dir1/file2 bs=1K count=1'
533                                    % mount_dir, shell=True)
534
535        # Test Case 2
536        check_call('mkdir %s/dir2' % mount_dir, shell=True)
537        for i in range(0, 20):
538            check_call('mkdir %s/dir2/0123456789abcdef%02x'
539                                    % (mount_dir, i), shell=True)
540
541        # Test Case 4
542        check_call('mkdir %s/dir4' % mount_dir, shell=True)
543
544        # Test Case 5, 6 & 7
545        check_call('mkdir %s/dir5' % mount_dir, shell=True)
546        check_call('dd if=/dev/urandom of=%s/dir5/file1 bs=1K count=1'
547                                    % mount_dir, shell=True)
548
549    except CalledProcessError:
550        pytest.skip('Setup failed for filesystem: ' + fs_type)
551        return
552    else:
553        yield [fs_ubtype, fs_img]
554    finally:
555        umount_fs(mount_dir)
556        call('rmdir %s' % mount_dir, shell=True)
557        call('rm -f %s' % fs_img, shell=True)
558
559#
560# Fixture for symlink fs test
561#
562@pytest.fixture()
563def fs_obj_symlink(request, u_boot_config):
564    """Set up a file system to be used in symlink fs test.
565
566    Args:
567        request: Pytest request object.
568        u_boot_config: U-boot configuration.
569
570    Return:
571        A fixture for basic fs test, i.e. a triplet of file system type,
572        volume file name and  a list of MD5 hashes.
573    """
574    fs_type = request.param
575    fs_img = ''
576
577    fs_ubtype = fstype_to_ubname(fs_type)
578    check_ubconfig(u_boot_config, fs_ubtype)
579
580    mount_dir = u_boot_config.persistent_data_dir + '/mnt'
581
582    small_file = mount_dir + '/' + SMALL_FILE
583    medium_file = mount_dir + '/' + MEDIUM_FILE
584
585    try:
586
587        # 1GiB volume
588        fs_img = mk_fs(u_boot_config, fs_type, 0x40000000, '1GB')
589    except CalledProcessError as err:
590        pytest.skip('Creating failed for filesystem: ' + fs_type + '. {}'.format(err))
591        return
592
593    try:
594        check_call('mkdir -p %s' % mount_dir, shell=True)
595    except CalledProcessError as err:
596        pytest.skip('Preparing mount folder failed for filesystem: ' + fs_type + '. {}'.format(err))
597        return
598    finally:
599        call('rm -f %s' % fs_img, shell=True)
600
601    try:
602        # Mount the image so we can populate it.
603        mount_fs(fs_type, fs_img, mount_dir)
604
605        # Create a subdirectory.
606        check_call('mkdir %s/SUBDIR' % mount_dir, shell=True)
607
608        # Create a small file in this image.
609        check_call('dd if=/dev/urandom of=%s bs=1M count=1'
610                   % small_file, shell=True)
611
612        # Create a medium file in this image.
613        check_call('dd if=/dev/urandom of=%s bs=10M count=1'
614                   % medium_file, shell=True)
615
616        # Generate the md5sums of reads that we will test against small file
617        out = check_output(
618            'dd if=%s bs=1M skip=0 count=1 2> /dev/null | md5sum'
619            % small_file, shell=True).decode()
620        md5val = [out.split()[0]]
621        out = check_output(
622            'dd if=%s bs=10M skip=0 count=1 2> /dev/null | md5sum'
623            % medium_file, shell=True).decode()
624        md5val.extend([out.split()[0]])
625
626    except CalledProcessError:
627        pytest.skip('Setup failed for filesystem: ' + fs_type)
628        return
629    else:
630        yield [fs_ubtype, fs_img, md5val]
631    finally:
632        umount_fs(mount_dir)
633        call('rmdir %s' % mount_dir, shell=True)
634        call('rm -f %s' % fs_img, shell=True)
635