From: Sheng Yong <shengyo...@xiaomi.com> This patch addes helper scripts for auto testcases, and testcases of fsck will be submitted in the following commits.
The basic idea of these testcases are: 1. create f2fs image 2. corrupt the image by inject specific fields 3. fsck fixes the corrupted image 4. verify fsck output with expected message The helper scripts include: * test_config.in: is used to generate the basic configurations of all testcases. * runtests.in: is used to generate `runtests'. * filter.sed: is used to remove unnecessary messages and cleanup arbitrary values. * helpers: provides helper functions The usage of runtests: * run all testcases: runtests * run one testcase: runtests <testcase directory path> * cleanup previous results: runtests clean Each testcase should have a sub-directory, where three files should be included: * README: describe information of the testcase * script: testcase itself * expect.in: expected output message New files are generated in the testcase directory after test: * log: output in detail * expect: derived from expect.in * out: output that will be compared with expect * PASS: testcase is passed * FAIL: testcase is failed * SKIP: testcase is skipped Signed-off-by: Sheng Yong <shengyo...@xiaomi.com> --- .gitignore | 14 +++ Makefile.am | 2 +- configure.ac | 1 + tests/Makefile.am | 26 +++++ tests/filter.sed | 69 +++++++++++ tests/helpers | 269 +++++++++++++++++++++++++++++++++++++++++++ tests/runtests.in | 48 ++++++++ tests/test_config.in | 53 +++++++++ 8 files changed, 481 insertions(+), 1 deletion(-) create mode 100644 tests/Makefile.am create mode 100644 tests/filter.sed create mode 100644 tests/helpers create mode 100644 tests/runtests.in create mode 100644 tests/test_config.in diff --git a/.gitignore b/.gitignore index 49809446793d..68dbe1c407c5 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,8 @@ stamp-h1 /mkfs/mkfs.f2fs /fsck/fsck.f2fs +/fsck/dump.f2fs +/fsck/inject.f2fs /tools/fibmap.f2fs /tools/parse.f2fs /tools/f2fscrypt @@ -54,3 +56,15 @@ stamp-h1 # cscope files cscope.* ncscope.* + +# testcase files +tests/*/log +tests/*/out +tests/*/PASS +tests/*/FAIL +tests/*/SKIP +tests/*/expect +tests/meta.img +tests/data.img +tests/runtests +tests/test_config diff --git a/Makefile.am b/Makefile.am index d2921d626e48..0c9ec66d02b6 100644 --- a/Makefile.am +++ b/Makefile.am @@ -2,4 +2,4 @@ ACLOCAL_AMFLAGS = -I m4 -SUBDIRS = man lib mkfs fsck tools +SUBDIRS = man lib mkfs fsck tools tests diff --git a/configure.ac b/configure.ac index ddfc3b0f30e2..a819dde93ed1 100644 --- a/configure.ac +++ b/configure.ac @@ -265,6 +265,7 @@ AC_CONFIG_FILES([ fsck/Makefile tools/Makefile tools/f2fs_io/Makefile + tests/Makefile ]) AC_CHECK_MEMBER([struct blk_zone.capacity], diff --git a/tests/Makefile.am b/tests/Makefile.am new file mode 100644 index 000000000000..1b4dfdc70e44 --- /dev/null +++ b/tests/Makefile.am @@ -0,0 +1,26 @@ +## Makefile.am + +noinst_SCRIPTS = test_config runtests +EXTRA_DIST = test_config.in runtests.in + +test_config: $(srcdir)/test_config.in + @echo "Creating test_config script..." + @[ -f test_config ] && chmod u+w test_config || true + @echo "#!/bin/bash" > test_config + @echo "" >> test_config + @echo "TOPDIR=@top_srcdir@" >> test_config + @cat $(srcdir)/test_config.in >> test_config + @chmod +x-w test_config + +runtests: $(srcdir)/runtests.in + @echo "Creating runtests script..." + @[ -f runtests ] && chmod u+w runtests || true + @echo "#!/bin/bash" > runtests + @echo "" >> runtests + @echo "SRCDIR=@srcdir@" >> runtests + @cat $(srcdir)/runtests.in >> runtests + @chmod +x-w runtests + +clean-local: + @[ -f runtests ] && ${SHELL} runtests clean || true + @rm -f $(noinst_SCRIPTS) diff --git a/tests/filter.sed b/tests/filter.sed new file mode 100644 index 000000000000..b2161a55d454 --- /dev/null +++ b/tests/filter.sed @@ -0,0 +1,69 @@ +# output messages that could be removed or ignored +# +# Note that some sed only supports POSIX Basic Regular Expression (BRE) +# syntax. In BRE, metacharacters, for example '|', '\s', '\+', are not +# supported. So BRE is recommended in this file. + +# remove messages which start with "^Info: ", expect for: +# - Info: Automatic fix mode enabled +# indicate that fsck -a is used. +# - Info: Force to fix corruption +# indicate that fsck -f is used. +# - Info: No error was reported +# indicate that fsck does not detect any errors. +# - Info: checkpoint stop reason +# - Info: fs errors +# indicate that fsck detects error recorded in super block. +/^Info: MKFS version/,+1d +/^Info: FSCK version/,+2d +/^Info: Host-managed zoned block device:/,+3d +/^Info: /{ + /Info: Automatic fix mode enabled/b out; + /Info: Force to fix corruption/b out; + /Info: No error was reported/b out; + /Info: checkpoint stop reason/b out; + /Info: fs errors/b out; + d +} +# e.g [update_block: 904] Info: Done to update superblock +/^\[[a-zA-Z_][0-9a-zA-Z_]*:[[:space:]]*[0-9]\{1,\}\] Info: /d +# e.g [INFO] (fsck_update_sb_flags:2389) --> Casefold: linear_lookup [enable] +/^\[INFO\]/d + +# remove fsck statistics messages +# e.g [FSCK] valid_block_count matching with CP [Ok..][0xc] +/^\[FSCK\]/d + +# remove trivial messages +/be careful to overwrite a mounted loopback file/d +/^Done: [0-9\.]\{1,\} secs/d +/^update nat(nid:.*) blkaddr \[.*\] in journal/d + +# some messages contain arbitrary numeric, ignore them +# e.g [ASSERT] (is_valid_ssa_data_blk: 340) => [ASSERT] (is_valid_ssa_data_blk: x) +s/\(\[ASSERT\] ([a-zA-Z_][0-9a-zA-Z_]*:[[:space:]]*\)[0-9]\{1,\})/\1x)/g +# e.g [FIX] (nullify_nat_entry:3274) => [FIX] (nullify_nat_entry:x) +s/\(\[FIX\] ([a-zA-Z_][0-9a-zA-Z_]*:[[:space:]]*\)[0-9]\{1,\})/\1x)/g +# e.g [fsck_chk_quota_files:2242] => [fsck_chk_quota_files:x] +s/^\(\[[a-zA-Z_][0-9a-zA-Z_]*:[[:space:]]*\)[0-9]\{1,\}\]/\1x\]/g + +# exceptions +# the message is only shown on zoned device, but I don't find a better way +# to keep this message in expect.in for both zoned and legacy UFS. So remove +# it for now. +/Reset write pointer of zone at segment/d + +# cut dump messages +# e.g nid: 7 ino: 7 ver: 0 offset: 0 blkaddr: 28642878 pack:1 cp_ver:0xe42573115ffc7cfc => +# nid: 7 ino: 7 ver: 0 offset: 0 +s/\(^nid:.*\) blkaddr:[0-9]\{1,\} pack:[12] cp_ver:0x[0-9a-fA-F]\{1,\}/\1/g +# e.g segno: 55968 vblocks:512 seg_type:1 mtime:0 sit_pack:2 => +# segno: 55968 vblocks:512 seg_type:1 mtime:0 +s/\(^segno:.*\) sit_pack:[12]/\1/g + +# keep the following at the end of the file +:out +# remove empty lines +/^$/d +# remove tail spaces +s/[[:space:]]\{1,\}$//g diff --git a/tests/helpers b/tests/helpers new file mode 100644 index 000000000000..03853f803598 --- /dev/null +++ b/tests/helpers @@ -0,0 +1,269 @@ +#!/bin/bash + +OUT=$TESTDIR/out +EXP=$TESTDIR/expect +LOG=$TESTDIR/log + +# $1: string to be trimed +trim_extra_space() { + local line="" + while IFS= read -r line; do + # trim multiple spaces as one + line=`echo "$line" | sed "s/[[:space:]]\{1,\}/ /g"` + # delete space in hex values + line=`echo "$line" | sed "s/0x /0x/g"` + # delete space before and after ":" + echo "$line" | sed "s/ *: */:/g" + done +} + +# clean up temporary files +cleanup() { + [ -f $TESTDIR/PASS ] && rm $TESTDIR/PASS || true + [ -f $TESTDIR/FAIL ] && rm $TESTDIR/FAIL || true + [ -f $TESTDIR/SKIP ] && rm $TESTDIR/SKIP || true + [ -f $TESTDIR/expect ] && rm $TESTDIR/expect || true + [ -f $OUT ] && rm $OUT || true + [ -f $LOG ] && rm $LOG || true +} + +# check test result +check_result() { + local NAME=`basename $TESTDIR` + + if [ -e $TESTDIR/SKIP ]; then + echo "$NAME: $DESC: skip" + return + fi + + if [ ! -e $EXP ]; then + cp "$EXP".in $EXP + fi + sed -f $FILTER -i $OUT + cmp -s $OUT $EXP + if [ x"$?" = x"0" ]; then + echo "$NAME: $DESC: pass" + touch $TESTDIR/PASS + else + echo "$NAME: $DESC: fail" + echo "diff $EXP $OUT" + diff $EXP $OUT > $TESTDIR/FAIL + exit + fi +} + +# $1: dev path +is_block_dev() { + if [ ! -e $1 ]; then + return 0 + fi + local dev_real_path=`realpath $1` + if [ -b $dev_real_path ]; then + # dev is block device + return 1 + fi + # dev is not block device + return 0 +} + +# $1: path of image file +# $2: size in MB +create_img_file() { + is_block_dev $1 + if [ $? -eq 1 ]; then + # dev is block device, return directly + return + fi + truncate -s $2"M" $1 +} + +make_f2fs() { + local metasize=10 + local datasize=118 + local opts="$MKFS_OPTS" + + if [ $SEGS_PER_SEC -gt 1 ]; then + metasize=$((3 * (SEGS_PER_SEC * 2))) + datasize=$((11 * (SEGS_PER_SEC * 2))) + opts="$MKFS_OPTS -s $SEGS_PER_SEC" + fi + if [ $MULTIDEV -eq 1 ]; then + create_img_file $META $metasize + create_img_file $DATA $datasize + opts = "$MKFS_OPTS -c $DATA" + else + create_img_file $META $((metasize + datasize)) + fi + + $MKFS $opts $META + if [ $? -ne 0 ]; then + echo "failed to format f2fs" >&2 + exit + fi + $DUMP -d 1 $META +} + +safe_mount() { + if [ $MULTIDEV -eq 1 ]; then + is_block_dev $DATA + if [ $? -eq 0 ]; then + # If multiple devices are used and DATA is not + # a block device, DATA should be associated to + # a loop device in advance. + # dump/fsck will check the path of the DATA + # device, to make them happy, replace devs[1].path + # with the loop device before mount, and restore + # it after umount. + local dev=`losetup -f` + losetup $dev $DATA + if [ $? -ne 0 ]; then + echo "cannot setup loop dev: losetup $dev $DATA" >&2 + exit + fi + $INJECT --sb 1 --mb devs.path --idx 1 --str $dev $META + fi + fi + + mount -t f2fs $* + if [ $? -ne 0 ]; then + echo "cannot mount f2fs image" >&2 + exit + fi +} + +# $1: mntpoint +safe_umount() { + local max_retry=10 + local dev="" + local i=0 + + umount $1 + while [ $? -ne 0 ]; do + i=$((i + 1)) + if [ $i -gt $max_retry ]; then + echo "cannot umount f2fs image" >&2 + exit + fi + echo "cannot umount f2fs image, retry: $i" + sleep 1 + umount $1 + done + + if [ ! -e $DATA ]; then + return + fi + dev=`losetup -j $DATA | cut -d ":" -f 1` + if [ x"$dev" != x"" ]; then + losetup -d $dev + # restore devs[1].path from $dev to $DATA + $INJECT --sb 1 --mb devs.path --idx 1 --str $DATA $META + fi +} + +# $1: subject: cp, sb or node +# $2: member of subject +# $3: the number of output lines if there are multiple results, default is 1 +get_mb() { + if [ x"$3" = x"" ]; then + local nth=1 + else + local nth=$3 + fi + echo "$1" | grep "^$2" | awk "NR==$nth" | trim_extra_space +} + +# $1: subject: cp, sb or node +# $2: member of subject +# $3: the number of output lines if there are multiple results, default is 1 +get_mb_val() { + if [ x"$3" = x"" ]; then + local nth=1 + else + local nth=$3 + fi + local val=`echo "$1" | grep "^$2" | awk "NR==$nth" | \ + sed "s/^$2[[:space:]]*\[\(.*\)\]$/\1/g"` + echo "$val" | grep ":" > /dev/null + if [ $? -eq 0 ]; then + val=`echo $val | sed "s/.*:[[:space:]]*\([0-9]\{1,\}\)$/\1/g"` + fi + echo $val +} + +# $1: device +get_sb() { + local sb_cp="`$DUMP $DUMP_OPTS -d 1 $1`" + local cut_here=`echo "$sb_cp" | awk '/^\| Checkpoint/ {print NR}'` + local sb="`echo "$sb_cp" | head -n $((cut_here - 2))`" + echo "$sb" +} + +# $1: device +get_cp() { + local sb_cp="`$DUMP $DUMP_OPTS -d 1 $1`" + local cut_here=`echo "$sb_cp" | awk '/^\| Checkpoint/ {print NR}'` + local cp="`echo "$sb_cp" | tail -n +$((cut_here - 2))`" + echo "$cp" +} + +# $1: blkaddr +get_segno() { + local sb=`get_sb $META` + local main_blkaddr=`get_mb_val "$sb" main_blkaddr` + local log_blks_per_seg=`get_mb_val "$sb" log_blocks_per_seg` + echo $((($1 - main_blkaddr) >> log_blks_per_seg)) +} + +# $1: segno +start_block() { + local sb=`get_sb $META` + local main_blkaddr=`get_mb_val "$sb" main_blkaddr` + local log_blks_per_seg=`get_mb_val "$sb" log_blocks_per_seg` + echo $((main_blkaddr + ($1 << log_blks_per_seg))) +} + +# $1: blkaddr +offset_in_seg() { + local sb=`get_sb $META` + local main_blkaddr=`get_mb_val "$sb" main_blkaddr` + local log_blks_per_seg=`get_mb_val "$sb" log_blocks_per_seg` + echo $((($1 - main_blkaddr) % (1 << log_blks_per_seg))) +} + +# reference INIT_FEATURE_TABLE +# $1: feature +test_sb_feature() { + local sb=`get_sb $META` + local feats=`get_mb_val "$sb" feature` + case $1 in + "encrypt") + return $((!!(feats & 0x1)));; + "blkzoned") + return $((!!(feats & 0x2)));; + "extra_attr") + return $((!!(feats & 0x8)));; + "project_quota") + return $((!!(feats & 0x10)));; + "inode_checksum") + return $((!!(feats & 0x20)));; + "flexible_inline_xattr") + return $((!!(feats & 0x40)));; + "quota") + return $((!!(feats & 0x80)));; + "inode_crtime") + return $((!!(feats & 0x100)));; + "lost_found") + return $((!!(feats & 0x200)));; + "verity") + return $((!!(feats & 0x400)));; + "sb_checksum") + return $((!!(feats & 0x800)));; + "casefold") + return $((!!(feats & 0x1000)));; + "compression") + return $((!!(feats & 0x2000)));; + "ro") + return $((!!(feats & 0x4000)));; + esac + return 0 +} diff --git a/tests/runtests.in b/tests/runtests.in new file mode 100644 index 000000000000..29db79a4a95c --- /dev/null +++ b/tests/runtests.in @@ -0,0 +1,48 @@ +#!/bin/bash +# +# run testcases +# +# run all testcases: +# runtests +# +# run one testcase: +# runtests <testcase directory path> +# +# cleanup: +# runtests clean +# + +# clean temporary files +if [ x"$1" = x"clean" ]; then + find $SRCDIR \( -name FAIL -o -name PASS -o -name SKIP -o \ + -name expect -o -name log -o -name out -o \ + -name meta.img -o -name data.img \) -exec rm -f {} \; + exit +fi + +TEST_CONFIG=$SRCDIR/test_config +. $TEST_CONFIG + +# $1: path of testcase path +run_one_test() { + local subdir=`realpath $1` + local name=`basename $subdir` + echo "Run testcase: $name ..." + TESTDIR=$subdir + . $subdir/script + echo "" +} + +# run one testcase +# $1: testcase path +if [ x"$1" != x"" ]; then + run_one_test $1 + exit +fi + +# run all testcases +TESTS=`find ./ -type f -name script` +for testcase in ${TESTS[@]}; do + subdir=`dirname $testcase` + run_one_test $subdir +done diff --git a/tests/test_config.in b/tests/test_config.in new file mode 100644 index 000000000000..41ce50052197 --- /dev/null +++ b/tests/test_config.in @@ -0,0 +1,53 @@ +#!/bin/bash +# +# Test Configuration +# + +check_executable() { + file $1 | grep ELF > /dev/null + [ $? -ne 0 ] && { echo "ERROR: $1 not ELF"; exit; } +} + +check_diff() { + diff $1 $2 > /dev/null + [ $? -ne 0 ] && { echo "WARNING: $1 and $2 differ"; } +} + +# path of devices +# If multiple devices are used, META is the first device and DATA is the +# second device. If there is only one device, META is used only. +META=`realpath $TOPDIR/tests/meta.img` +DATA=`realpath $TOPDIR/tests/data.img` + +# path of tools +MKFS="$TOPDIR/mkfs/mkfs.f2fs" +FSCK="$TOPDIR/fsck/fsck.f2fs" +DUMP="$TOPDIR/fsck/dump.f2fs" +INJECT="$TOPDIR/fsck/inject.f2fs" +F2FS_IO="$TOPDIR/tools/f2fs_io/f2fs_io" +FILTER="$TOPDIR/tests/filter.sed" +[ ! -e $MKFS ] && { echo "$MKFS not exist"; exit; } +[ ! -e $FSCK ] && { echo "$FSCK not exist"; exit; } +check_executable $FSCK +[ ! -e $DUMP ] && { cp $FSCK $DUMP; } +check_executable $DUMP +check_diff $FSCK $DUMP +[ ! -e $F2FS_IO ] && { echo "$F2FS_IO not exist"; exit; } +[ ! -e $INJECT ] && { cp $FSCK $INJECT; } +check_executable $INJECT +check_diff $FSCK $INJECT + +# config of mkfs +F2FS_FEATURES="encrypt,quota,project_quota,extra_attr,verity,compression" +MKFS_OPTS="-O $F2FS_FEATURES -f" +MULTIDEV=0 +SEGS_PER_SEC=1 + +# config of fsck +FSCK_OPTS="--no-kernel-check -N" + +# config of dump +DUMP_OPTS="-N" + +# config of mount +MNT_OPTS="" -- 2.43.0 _______________________________________________ Linux-f2fs-devel mailing list Linux-f2fs-devel@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/linux-f2fs-devel