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