Message ID | 20240827120237.25805-5-chrubis@suse.cz |
---|---|
State | Accepted |
Headers | show |
Series | Shell test library v3 | expand |
Hi! On 8/27/24 14:02, Cyril Hrubis wrote: > This commit implements a shell loader so that we don't have to write a C > loader for each LTP shell test. The idea is simple, the loader parses > the shell test and prepares the tst_test structure accordingly, then > runs the actual shell test. > > The format for the metadata in the shell test was choosen to be JSON > because: > > - I didn't want to invent an adhoc format and JSON is perfect for > serializing data structures > - The metadata parser for shell test will be trivial, it will just pick > the JSON from the comment, no parsing will be required > > Signed-off-by: Cyril Hrubis <chrubis@suse.cz> > Reviewed-by: Richard Palethorpe <io@richiejp.com> > --- > include/tst_test.h | 2 +- > testcases/lib/.gitignore | 1 + > testcases/lib/Makefile | 6 +- > testcases/lib/run_tests.sh | 21 + > testcases/lib/tests/shell_loader.sh | 26 + > .../lib/tests/shell_loader_all_filesystems.sh | 27 + > .../lib/tests/shell_loader_filesystems.sh | 33 ++ > .../lib/tests/shell_loader_invalid_block.sh | 26 + > .../tests/shell_loader_invalid_metadata.sh | 15 + > testcases/lib/tests/shell_loader_kconfigs.sh | 12 + > .../lib/tests/shell_loader_no_metadata.sh | 8 + > .../lib/tests/shell_loader_supported_archs.sh | 12 + > testcases/lib/tests/shell_loader_tags.sh | 15 + > testcases/lib/tests/shell_loader_tcnt.sh | 15 + > .../lib/tests/shell_loader_wrong_metadata.sh | 15 + > testcases/lib/tst_env.sh | 4 + > testcases/lib/tst_loader.sh | 11 + > testcases/lib/tst_run_shell.c | 491 ++++++++++++++++++ > 18 files changed, 738 insertions(+), 2 deletions(-) > create mode 100755 testcases/lib/tests/shell_loader.sh > create mode 100755 testcases/lib/tests/shell_loader_all_filesystems.sh > create mode 100755 testcases/lib/tests/shell_loader_filesystems.sh > create mode 100755 testcases/lib/tests/shell_loader_invalid_block.sh > create mode 100755 testcases/lib/tests/shell_loader_invalid_metadata.sh > create mode 100755 testcases/lib/tests/shell_loader_kconfigs.sh > create mode 100755 testcases/lib/tests/shell_loader_no_metadata.sh > create mode 100755 testcases/lib/tests/shell_loader_supported_archs.sh > create mode 100755 testcases/lib/tests/shell_loader_tags.sh > create mode 100755 testcases/lib/tests/shell_loader_tcnt.sh > create mode 100755 testcases/lib/tests/shell_loader_wrong_metadata.sh > create mode 100644 testcases/lib/tst_loader.sh > create mode 100644 testcases/lib/tst_run_shell.c > > diff --git a/include/tst_test.h b/include/tst_test.h > index 9871676a5..d0fa84a71 100644 > --- a/include/tst_test.h > +++ b/include/tst_test.h > @@ -274,7 +274,7 @@ struct tst_fs { > const char *const *mkfs_opts; > const char *mkfs_size_opt; > > - const unsigned int mnt_flags; > + unsigned int mnt_flags; > const void *mnt_data; > }; > > diff --git a/testcases/lib/.gitignore b/testcases/lib/.gitignore > index d0dacf62a..385f3c3ca 100644 > --- a/testcases/lib/.gitignore > +++ b/testcases/lib/.gitignore > @@ -24,3 +24,4 @@ > /tst_supported_fs > /tst_timeout_kill > /tst_res_ > +/tst_run_shell > diff --git a/testcases/lib/Makefile b/testcases/lib/Makefile > index 928d76d62..b3a9181c1 100644 > --- a/testcases/lib/Makefile > +++ b/testcases/lib/Makefile > @@ -4,6 +4,9 @@ > > top_srcdir ?= ../.. > > +LTPLIBS = ujson > +tst_run_shell: LTPLDLIBS = -lujson > + > include $(top_srcdir)/include/mk/testcases.mk > > INSTALL_TARGETS := *.sh > @@ -13,6 +16,7 @@ MAKE_TARGETS := tst_sleep tst_random tst_checkpoint tst_rod tst_kvcmp\ > tst_getconf tst_supported_fs tst_check_drivers tst_get_unused_port\ > tst_get_median tst_hexdump tst_get_free_pids tst_timeout_kill\ > tst_check_kconfigs tst_cgctl tst_fsfreeze tst_ns_create tst_ns_exec\ > - tst_ns_ifmove tst_lockdown_enabled tst_secureboot_enabled tst_res_ > + tst_ns_ifmove tst_lockdown_enabled tst_secureboot_enabled tst_res_\ > + tst_run_shell > > include $(top_srcdir)/include/mk/generic_trunk_target.mk > diff --git a/testcases/lib/run_tests.sh b/testcases/lib/run_tests.sh > index 60e7d1bcf..e30065f1d 100755 > --- a/testcases/lib/run_tests.sh > +++ b/testcases/lib/run_tests.sh > @@ -9,3 +9,24 @@ for i in `seq -w 01 06`; do > echo > ./tests/shell_test$i > done > + > +for i in shell_loader.sh shell_loader_all_filesystems.sh shell_loader_no_metadata.sh \ > + shell_loader_wrong_metadata.sh shell_loader_invalid_metadata.sh\ > + shell_loader_supported_archs.sh shell_loader_filesystems.sh\ > + shell_loader_tcnt.sh shell_loader_kconfigs.sh shell_loader_tags.sh \ > + shell_loader_invalid_block.sh; do > + echo > + echo "*** Running $i ***" > + echo > + $i > +done > + > +echo > +echo "*** Testing LTP test -h option ***" > +echo > +shell_loader.sh -h > + > +echo > +echo "*** Testing LTP test -i option ***" > +echo > +shell_loader.sh -i 2 > diff --git a/testcases/lib/tests/shell_loader.sh b/testcases/lib/tests/shell_loader.sh > new file mode 100755 > index 000000000..df7f0c0af > --- /dev/null > +++ b/testcases/lib/tests/shell_loader.sh > @@ -0,0 +1,26 @@ > +#!/bin/sh > +# > +# --- > +# doc > +# > +# [Description] > +# > +# This is a simple shell test loader example. > +# --- > +# > +# --- > +# env > +# { > +# "needs_tmpdir": true > +# } > +# --- > + > +. tst_loader.sh > + > +tst_res TPASS "Shell loader works fine!" > +case "$PWD" in > + /tmp/*) > + tst_res TPASS "We are running in temp directory in $PWD";; > + *) > + tst_res TFAIL "We are not running in temp directory but $PWD";; > +esac > diff --git a/testcases/lib/tests/shell_loader_all_filesystems.sh b/testcases/lib/tests/shell_loader_all_filesystems.sh > new file mode 100755 > index 000000000..d5943c335 > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_all_filesystems.sh > @@ -0,0 +1,27 @@ > +#!/bin/sh > +# > +# --- > +# env > +# { > +# "needs_root": true, > +# "mount_device": true, > +# "all_filesystems": true, > +# "mntpoint": "ltp_mntpoint" > +# } > +# --- > + > +. tst_loader.sh > + > +tst_res TINFO "In shell" > + > +mntpath=$(realpath ltp_mntpoint) > +mounted=$(grep $mntpath /proc/mounts) > + > +if [ -n "$mounted" ]; then > + device=$(echo $mounted |cut -d' ' -f 1) > + path=$(echo $mounted |cut -d' ' -f 2) > + > + tst_res TPASS "$device mounted at $path" > +else > + tst_res TFAIL "Device not mounted!" > +fi > diff --git a/testcases/lib/tests/shell_loader_filesystems.sh b/testcases/lib/tests/shell_loader_filesystems.sh > new file mode 100755 > index 000000000..5d8aa9808 > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_filesystems.sh > @@ -0,0 +1,33 @@ > +#!/bin/sh > +# > +# --- > +# env > +# { > +# "mount_device": true, > +# "mntpoint": "ltp_mntpoint", > +# "filesystems": [ > +# { > +# "type": "btrfs" > +# }, > +# { > +# "type": "xfs", > +# "mkfs_opts": ["-m", "reflink=1"] > +# } > +# ] > +# } > +# --- > + > +. tst_loader.sh > + > +tst_res TINFO "In shell" > + > +mntpoint=$(realpath ltp_mntpoint) > +mounted=$(grep $mntpoint /proc/mounts) > + > +if [ -n "$mounted" ]; then > + fs=$(echo $mounted |cut -d' ' -f 3) > + > + tst_res TPASS "Mounted device formatted with $fs" > +else > + tst_res TFAIL "Device not mounted!" > +fi > diff --git a/testcases/lib/tests/shell_loader_invalid_block.sh b/testcases/lib/tests/shell_loader_invalid_block.sh > new file mode 100755 > index 000000000..f41de04fd > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_invalid_block.sh > @@ -0,0 +1,26 @@ > +#!/bin/sh > +# > +# --- > +# doc > +# > +# [Description] > +# > +# This is a simple shell test loader example. > +# --- > +# > +# --- > +# env > +# { > +# "needs_tmpdir": true > +# } > +# --- > +# > +# --- > +# inv > +# > +# This is an invalid block that breaks the test. > +# --- > + > +. tst_loader.sh > + > +tst_res TPASS "This should pass!" > diff --git a/testcases/lib/tests/shell_loader_invalid_metadata.sh b/testcases/lib/tests/shell_loader_invalid_metadata.sh > new file mode 100755 > index 000000000..c10b00f1b > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_invalid_metadata.sh > @@ -0,0 +1,15 @@ > +#!/bin/sh > +# > +# This test has wrong metadata and should not be run > +# > +# --- > +# env > +# { > +# {"needs_tmpdir": 42, > +# } > +# --- > +# > + > +. tst_loader.sh > + > +tst_res TFAIL "Shell loader should TBROK the test" > diff --git a/testcases/lib/tests/shell_loader_kconfigs.sh b/testcases/lib/tests/shell_loader_kconfigs.sh > new file mode 100755 > index 000000000..7e9a1dce7 > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_kconfigs.sh > @@ -0,0 +1,12 @@ > +#!/bin/sh > +# > +# --- > +# env > +# { > +# "needs_kconfigs": ["CONFIG_NUMA=y"] > +# } > +# --- > + > +. tst_loader.sh > + > +tst_res TPASS "Shell loader works fine!" > diff --git a/testcases/lib/tests/shell_loader_no_metadata.sh b/testcases/lib/tests/shell_loader_no_metadata.sh > new file mode 100755 > index 000000000..60ba8b889 > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_no_metadata.sh > @@ -0,0 +1,8 @@ > +#!/bin/sh > +# > +# This test has no metadata and should not be executed > +# > + > +. tst_loader.sh > + > +tst_res TFAIL "Shell loader should TBROK the test" > diff --git a/testcases/lib/tests/shell_loader_supported_archs.sh b/testcases/lib/tests/shell_loader_supported_archs.sh > new file mode 100755 > index 000000000..45213f840 > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_supported_archs.sh > @@ -0,0 +1,12 @@ > +#!/bin/sh > +# > +# --- > +# env > +# { > +# "supported_archs": ["x86", "ppc64", "x86_64"] > +# } > +# --- > + > +. tst_loader.sh > + > +tst_res TPASS "We are running on supported architecture" > diff --git a/testcases/lib/tests/shell_loader_tags.sh b/testcases/lib/tests/shell_loader_tags.sh > new file mode 100755 > index 000000000..a6278c37d > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_tags.sh > @@ -0,0 +1,15 @@ > +#!/bin/sh > +# > +# --- > +# env > +# { > +# "tags": [ > +# ["linux-git", "832478cd342ab"], > +# ["CVE", "2099-999"] > +# ] > +# } > +# --- > + > +. tst_loader.sh > + > +tst_res TFAIL "Fails the test so that tags are shown." > diff --git a/testcases/lib/tests/shell_loader_tcnt.sh b/testcases/lib/tests/shell_loader_tcnt.sh > new file mode 100755 > index 000000000..81fc08179 > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_tcnt.sh > @@ -0,0 +1,15 @@ > +#!/bin/sh > +# > +# The script should be executed tcnt times and the iteration number should be in $1 > +# > +# --- > +# env > +# { > +# "tcnt": 2 > +# } > +# --- > +# > + > +. tst_loader.sh > + > +tst_res TPASS "Iteration $1" > diff --git a/testcases/lib/tests/shell_loader_wrong_metadata.sh b/testcases/lib/tests/shell_loader_wrong_metadata.sh > new file mode 100755 > index 000000000..752e25eea > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_wrong_metadata.sh > @@ -0,0 +1,15 @@ > +#!/bin/sh > +# > +# This test has wrong metadata and should not be run > +# > +# --- > +# env > +# { > +# "needs_tmpdir": 42, > +# } > +# --- > +# > + > +. tst_loader.sh > + > +tst_res TFAIL "Shell loader should TBROK the test" > diff --git a/testcases/lib/tst_env.sh b/testcases/lib/tst_env.sh > index 948bc5024..67ba80744 100644 > --- a/testcases/lib/tst_env.sh > +++ b/testcases/lib/tst_env.sh > @@ -1,4 +1,8 @@ > #!/bin/sh > +# > +# This is a minimal test environment for a shell scripts executed from C by > +# tst_run_shell() function. Shell tests must use the tst_loader.sh instead! > +# > > tst_script_name=$(basename $0) > > diff --git a/testcases/lib/tst_loader.sh b/testcases/lib/tst_loader.sh > new file mode 100644 > index 000000000..ed04d0340 > --- /dev/null > +++ b/testcases/lib/tst_loader.sh > @@ -0,0 +1,11 @@ > +#!/bin/sh > +# > +# This is a loader for shell tests that use the C test library. > +# > + > +if [ -z "$LTP_IPC_PATH" ]; then > + tst_run_shell $(basename "$0") "$@" > + exit $? > +else > + . tst_env.sh > +fi > diff --git a/testcases/lib/tst_run_shell.c b/testcases/lib/tst_run_shell.c > new file mode 100644 > index 000000000..8ed0f21b6 > --- /dev/null > +++ b/testcases/lib/tst_run_shell.c > @@ -0,0 +1,491 @@ > +// SPDX-License-Identifier: GPL-2.0-or-later > +/* > + * Copyright (c) 2024 Cyril Hrubis <chrubis@suse.cz> > + */ > +#include <sys/mount.h> > + > +#define TST_NO_DEFAULT_MAIN > +#include "tst_test.h" > +#include "tst_safe_stdio.h" > +#include "ujson.h" > + > +static char *shell_filename; > + > +static void run_shell(void) > +{ > + tst_run_script(shell_filename, NULL); > +} > + > +static void run_shell_tcnt(unsigned int n) > +{ > + char buf[128]; > + char *const params[] = {buf, NULL}; > + > + snprintf(buf, sizeof(buf), "%u", n); > + > + tst_run_script(shell_filename, params); > +} > + > +struct tst_test test = { > + .runs_script = 1, > +}; > + > +static void print_help(void) > +{ > + printf("Usage: tst_shell_loader ltp_shell_test.sh ...\n"); > +} > + > +static char *metadata; > +static size_t metadata_size; > +static size_t metadata_used; > + > +static void metadata_append(const char *line) > +{ > + size_t linelen = strlen(line); > + > + if (metadata_size - metadata_used < linelen + 1) { > + metadata_size += 4096; > + metadata = SAFE_REALLOC(metadata, metadata_size); > + } > + > + strcpy(metadata + metadata_used, line); > + metadata_used += linelen; > +} > + > +enum test_attr_ids { > + ALL_FILESYSTEMS, > + DEV_MIN_SIZE, > + FILESYSTEMS, > + FORMAT_DEVICE, > + MIN_CPUS, > + MIN_MEM_AVAIL, > + MIN_KVER, > + MIN_SWAP_AVAIL, > + MNTPOINT, > + MOUNT_DEVICE, > + NEEDS_ABI_BITS, > + NEEDS_CMDS, > + NEEDS_DEVFS, > + NEEDS_DEVICE, > + NEEDS_DRIVERS, > + NEEDS_HUGETLBFS, > + NEEDS_KCONFIGS, > + NEEDS_ROFS, > + NEEDS_ROOT, > + NEEDS_TMPDIR, > + RESTORE_WALLCLOCK, > + SKIP_FILESYSTEMS, > + SKIP_IN_COMPAT, > + SKIP_IN_LOCKDOWN, > + SKIP_IN_SECUREBOOT, > + SUPPORTED_ARCHS, > + TAGS, > + TAINT_CHECK, > + TCNT, > +}; > + > +static ujson_obj_attr test_attrs[] = { > + UJSON_OBJ_ATTR_IDX(ALL_FILESYSTEMS, "all_filesystems", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(DEV_MIN_SIZE, "dev_min_size", UJSON_INT), > + UJSON_OBJ_ATTR_IDX(FILESYSTEMS, "filesystems", UJSON_ARR), > + UJSON_OBJ_ATTR_IDX(FORMAT_DEVICE, "format_device", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(MIN_CPUS, "min_cpus", UJSON_INT), > + UJSON_OBJ_ATTR_IDX(MIN_MEM_AVAIL, "min_mem_avail", UJSON_INT), > + UJSON_OBJ_ATTR_IDX(MIN_KVER, "min_kver", UJSON_STR), > + UJSON_OBJ_ATTR_IDX(MIN_SWAP_AVAIL, "min_swap_avail", UJSON_INT), > + UJSON_OBJ_ATTR_IDX(MNTPOINT, "mntpoint", UJSON_STR), > + UJSON_OBJ_ATTR_IDX(MOUNT_DEVICE, "mount_device", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(NEEDS_ABI_BITS, "needs_abi_bits", UJSON_INT), > + UJSON_OBJ_ATTR_IDX(NEEDS_CMDS, "needs_cmds", UJSON_ARR), > + UJSON_OBJ_ATTR_IDX(NEEDS_DEVFS, "needs_devfs", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(NEEDS_DEVICE, "needs_device", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(NEEDS_DRIVERS, "needs_drivers", UJSON_ARR), > + UJSON_OBJ_ATTR_IDX(NEEDS_HUGETLBFS, "needs_hugetlbfs", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(NEEDS_KCONFIGS, "needs_kconfigs", UJSON_ARR), > + UJSON_OBJ_ATTR_IDX(NEEDS_ROFS, "needs_rofs", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(NEEDS_ROOT, "needs_root", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(NEEDS_TMPDIR, "needs_tmpdir", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(RESTORE_WALLCLOCK, "restore_wallclock", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(SKIP_FILESYSTEMS, "skip_filesystems", UJSON_ARR), > + UJSON_OBJ_ATTR_IDX(SKIP_IN_COMPAT, "skip_in_compat", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(SKIP_IN_LOCKDOWN, "skip_in_lockdown", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(SKIP_IN_SECUREBOOT, "skip_in_secureboot", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(SUPPORTED_ARCHS, "supported_archs", UJSON_ARR), > + UJSON_OBJ_ATTR_IDX(TAGS, "tags", UJSON_ARR), > + UJSON_OBJ_ATTR_IDX(TAINT_CHECK, "taint_check", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(TCNT, "tcnt", UJSON_INT) > +}; > + > +static ujson_obj test_obj = { > + .attrs = test_attrs, > + .attr_cnt = UJSON_ARRAY_SIZE(test_attrs), > +}; > + > +static const char *const *parse_strarr(ujson_reader *reader, ujson_val *val) > +{ > + unsigned int cnt = 0, i = 0; > + char **ret; > + > + ujson_reader_state state = ujson_reader_state_save(reader); > + > + UJSON_ARR_FOREACH(reader, val) { > + if (val->type != UJSON_STR) { > + ujson_err(reader, "Expected string!"); > + return NULL; > + } > + > + cnt++; > + } > + > + ujson_reader_state_load(reader, state); > + > + ret = SAFE_MALLOC(sizeof(char*) * (cnt + 1)); > + > + UJSON_ARR_FOREACH(reader, val) { > + ret[i++] = strdup(val->val_str); > + } > + > + ret[i] = NULL; > + > + return (const char *const *)ret; > +} > + > +enum fs_ids { > + MKFS_OPTS, > + MKFS_SIZE_OPT, > + MNT_FLAGS, > + TYPE, > +}; > + > +static ujson_obj_attr fs_attrs[] = { > + UJSON_OBJ_ATTR_IDX(MKFS_OPTS, "mkfs_opts", UJSON_ARR), > + UJSON_OBJ_ATTR_IDX(MKFS_SIZE_OPT, "mkfs_size_opt", UJSON_STR), > + UJSON_OBJ_ATTR_IDX(MNT_FLAGS, "mnt_flags", UJSON_ARR), > + UJSON_OBJ_ATTR_IDX(TYPE, "type", UJSON_STR), > +}; > + > +static ujson_obj fs_obj = { > + .attrs = fs_attrs, > + .attr_cnt = UJSON_ARRAY_SIZE(fs_attrs), > +}; > + > +static int parse_mnt_flags(ujson_reader *reader, ujson_val *val) > +{ > + int ret = 0; > + > + UJSON_ARR_FOREACH(reader, val) { > + if (val->type != UJSON_STR) { > + ujson_err(reader, "Expected string!"); > + return ret; > + } > + > + if (!strcmp(val->val_str, "RDONLY")) > + ret |= MS_RDONLY; > + else if (!strcmp(val->val_str, "NOATIME")) > + ret |= MS_NOATIME; > + else if (!strcmp(val->val_str, "NOEXEC")) > + ret |= MS_NOEXEC; > + else if (!strcmp(val->val_str, "NOSUID")) > + ret |= MS_NOSUID; > + else > + ujson_err(reader, "Invalid mount flag"); > + } > + > + return ret; > +} > + > +static struct tst_fs *parse_filesystems(ujson_reader *reader, ujson_val *val) > +{ > + unsigned int i = 0, cnt = 0; > + struct tst_fs *ret; > + > + ujson_reader_state state = ujson_reader_state_save(reader); > + > + UJSON_ARR_FOREACH(reader, val) { > + if (val->type != UJSON_OBJ) { > + ujson_err(reader, "Expected object!"); > + return NULL; > + } > + ujson_obj_skip(reader); > + cnt++; > + } > + > + ujson_reader_state_load(reader, state); > + > + ret = SAFE_MALLOC(sizeof(struct tst_fs) * (cnt + 1)); > + memset(ret, 0, sizeof(*ret) * (cnt+1)); > + > + UJSON_ARR_FOREACH(reader, val) { > + UJSON_OBJ_FOREACH_FILTER(reader, val, &fs_obj, ujson_empty_obj) { > + switch ((enum fs_ids)val->idx) { > + case MKFS_OPTS: > + ret[i].mkfs_opts = parse_strarr(reader, val); > + break; > + case MKFS_SIZE_OPT: > + ret[i].mkfs_size_opt = strdup(val->val_str); > + break; > + case MNT_FLAGS: > + ret[i].mnt_flags = parse_mnt_flags(reader, val); > + break; > + case TYPE: > + ret[i].type = strdup(val->val_str); > + break; > + } > + > + } > + > + i++; > + } > + > + return ret; > +} > + > +static struct tst_tag *parse_tags(ujson_reader *reader, ujson_val *val) > +{ > + unsigned int i = 0, cnt = 0; > + struct tst_tag *ret; > + > + ujson_reader_state state = ujson_reader_state_save(reader); > + > + UJSON_ARR_FOREACH(reader, val) { > + if (val->type != UJSON_ARR) { > + ujson_err(reader, "Expected array!"); > + return NULL; > + } > + ujson_arr_skip(reader); > + cnt++; > + } > + > + ujson_reader_state_load(reader, state); > + > + ret = SAFE_MALLOC(sizeof(struct tst_tag) * (cnt + 1)); > + memset(&ret[cnt], 0, sizeof(ret[cnt])); > + > + UJSON_ARR_FOREACH(reader, val) { > + char *name = NULL; > + char *value = NULL; > + > + UJSON_ARR_FOREACH(reader, val) { > + if (val->type != UJSON_STR) { > + ujson_err(reader, "Expected string!"); > + return NULL; > + } > + > + if (!name) { > + name = strdup(val->val_str); > + } else if (!value) { > + value = strdup(val->val_str); > + } else { > + ujson_err(reader, "Expected only two members!"); > + return NULL; > + } > + } > + > + ret[i].name = name; > + ret[i].value = value; > + i++; > + } > + > + return ret; > +} > + > +static void parse_metadata(void) > +{ > + ujson_reader reader = UJSON_READER_INIT(metadata, metadata_used, UJSON_READER_STRICT); > + char str_buf[128]; > + ujson_val val = UJSON_VAL_INIT(str_buf, sizeof(str_buf)); > + > + UJSON_OBJ_FOREACH_FILTER(&reader, &val, &test_obj, ujson_empty_obj) { > + switch ((enum test_attr_ids)val.idx) { > + case ALL_FILESYSTEMS: > + test.all_filesystems = val.val_bool; > + break; > + case DEV_MIN_SIZE: > + if (val.val_int <= 0) > + ujson_err(&reader, "Device size must be > 0"); > + else > + test.dev_min_size = val.val_int; > + break; > + case FILESYSTEMS: > + test.filesystems = parse_filesystems(&reader, &val); > + break; > + case FORMAT_DEVICE: > + test.format_device = val.val_bool; > + break; > + case MIN_CPUS: > + if (val.val_int <= 0) > + ujson_err(&reader, "Minimal number of cpus must be > 0"); > + else > + test.min_cpus = val.val_int; > + break; > + case MIN_MEM_AVAIL: > + if (val.val_int <= 0) > + ujson_err(&reader, "Minimal available memory size must be > 0"); > + else > + test.min_mem_avail = val.val_int; > + break; > + case MIN_KVER: > + test.min_kver = strdup(val.val_str); > + break; > + case MIN_SWAP_AVAIL: > + if (val.val_int <= 0) > + ujson_err(&reader, "Minimal available swap size must be > 0"); > + else > + test.min_swap_avail = val.val_int; > + break; > + case MNTPOINT: > + test.mntpoint = strdup(val.val_str); > + break; > + case MOUNT_DEVICE: > + test.mount_device = val.val_bool; > + break; > + case NEEDS_ABI_BITS: > + if (val.val_int == 32 || val.val_int == 64) > + test.needs_abi_bits = val.val_int; > + else > + ujson_err(&reader, "ABI bits must be 32 or 64"); > + break; > + case NEEDS_CMDS: > + test.needs_cmds = parse_strarr(&reader, &val); > + break; > + case NEEDS_DEVFS: > + test.needs_devfs = val.val_bool; > + break; > + case NEEDS_DEVICE: > + test.needs_device = val.val_bool; > + break; > + case NEEDS_DRIVERS: > + test.needs_drivers = parse_strarr(&reader, &val); > + break; > + case NEEDS_HUGETLBFS: > + test.needs_hugetlbfs = val.val_bool; > + break; > + case NEEDS_KCONFIGS: > + test.needs_kconfigs = parse_strarr(&reader, &val); > + break; > + case NEEDS_ROFS: > + test.needs_rofs = val.val_bool; > + break; > + case NEEDS_ROOT: > + test.needs_root = val.val_bool; > + break; > + case NEEDS_TMPDIR: > + test.needs_tmpdir = val.val_bool; > + break; > + case RESTORE_WALLCLOCK: > + test.restore_wallclock = val.val_bool; > + break; > + case SKIP_FILESYSTEMS: > + test.skip_filesystems = parse_strarr(&reader, &val); > + break; > + case SKIP_IN_COMPAT: > + test.skip_in_compat = val.val_bool; > + break; > + case SKIP_IN_LOCKDOWN: > + test.skip_in_lockdown = val.val_bool; > + break; > + case SKIP_IN_SECUREBOOT: > + test.skip_in_secureboot = val.val_bool; > + break; > + case SUPPORTED_ARCHS: > + test.supported_archs = parse_strarr(&reader, &val); > + break; > + case TAGS: > + test.tags = parse_tags(&reader, &val); > + break; > + case TAINT_CHECK: > + test.taint_check = val.val_bool; > + break; > + case TCNT: > + if (val.val_int <= 0) > + ujson_err(&reader, "Number of tests must be > 0"); > + else > + test.tcnt = val.val_int; > + break; > + } > + } > + > + ujson_reader_finish(&reader); > + > + if (ujson_reader_err(&reader)) > + tst_brk(TBROK, "Invalid metadata"); > +} > + > +enum parser_state { > + PAR_NONE, > + PAR_ESC, > + PAR_DOC, > + PAR_ENV, > +}; > + > +static void extract_metadata(void) > +{ > + FILE *f; > + char line[4096]; > + char path[4096]; > + enum parser_state state = PAR_NONE; > + > + if (tst_get_path(shell_filename, path, sizeof(path)) == -1) > + tst_brk(TBROK, "Failed to find %s in $PATH", shell_filename); > + > + f = SAFE_FOPEN(path, "r"); > + > + while (fgets(line, sizeof(line), f)) { > + switch (state) { > + case PAR_NONE: > + if (!strcmp(line, "# ---\n")) What if user defines "#---" or "# ---" ? IMHO it would be better to parse it following shell comments standards. In particular, "^#\s+---" using a *scan function. > + state = PAR_ESC; > + break; > + case PAR_ESC: > + if (!strcmp(line, "# env\n")) Same apply here and to the others. > + state = PAR_ENV; > + else if (!strcmp(line, "# doc\n")) > + state = PAR_DOC; > + else > + tst_brk(TBROK, "Unknown comment block %s", line); > + break; > + case PAR_ENV: > + if (!strcmp(line, "# ---\n")) > + state = PAR_NONE; > + else > + metadata_append(line + 2); > + break; > + case PAR_DOC: > + if (!strcmp(line, "# ---\n")) > + state = PAR_NONE; > + break; > + } > + } > + > + fclose(f); > +} > + > +static void prepare_test_struct(void) > +{ > + extract_metadata(); > + > + if (metadata) > + parse_metadata(); > + else > + tst_brk(TBROK, "No metadata found!"); > +} > + > +int main(int argc, char *argv[]) > +{ > + if (argc < 2) > + goto help; > + > + shell_filename = argv[1]; > + > + prepare_test_struct(); > + > + if (test.tcnt) > + test.test = run_shell_tcnt; > + else > + test.test_all = run_shell; > + > + tst_run_tcases(argc - 1, argv + 1, &test); > +help: > + print_help(); > + return 1; > +} Andrea
Also is there a reason why we are adding tests to testcases/lib/tests ? On 8/27/24 14:02, Cyril Hrubis wrote: > This commit implements a shell loader so that we don't have to write a C > loader for each LTP shell test. The idea is simple, the loader parses > the shell test and prepares the tst_test structure accordingly, then > runs the actual shell test. > > The format for the metadata in the shell test was choosen to be JSON > because: > > - I didn't want to invent an adhoc format and JSON is perfect for > serializing data structures > - The metadata parser for shell test will be trivial, it will just pick > the JSON from the comment, no parsing will be required > > Signed-off-by: Cyril Hrubis <chrubis@suse.cz> > Reviewed-by: Richard Palethorpe <io@richiejp.com> > --- > include/tst_test.h | 2 +- > testcases/lib/.gitignore | 1 + > testcases/lib/Makefile | 6 +- > testcases/lib/run_tests.sh | 21 + > testcases/lib/tests/shell_loader.sh | 26 + > .../lib/tests/shell_loader_all_filesystems.sh | 27 + > .../lib/tests/shell_loader_filesystems.sh | 33 ++ > .../lib/tests/shell_loader_invalid_block.sh | 26 + > .../tests/shell_loader_invalid_metadata.sh | 15 + > testcases/lib/tests/shell_loader_kconfigs.sh | 12 + > .../lib/tests/shell_loader_no_metadata.sh | 8 + > .../lib/tests/shell_loader_supported_archs.sh | 12 + > testcases/lib/tests/shell_loader_tags.sh | 15 + > testcases/lib/tests/shell_loader_tcnt.sh | 15 + > .../lib/tests/shell_loader_wrong_metadata.sh | 15 + > testcases/lib/tst_env.sh | 4 + > testcases/lib/tst_loader.sh | 11 + > testcases/lib/tst_run_shell.c | 491 ++++++++++++++++++ > 18 files changed, 738 insertions(+), 2 deletions(-) > create mode 100755 testcases/lib/tests/shell_loader.sh > create mode 100755 testcases/lib/tests/shell_loader_all_filesystems.sh > create mode 100755 testcases/lib/tests/shell_loader_filesystems.sh > create mode 100755 testcases/lib/tests/shell_loader_invalid_block.sh > create mode 100755 testcases/lib/tests/shell_loader_invalid_metadata.sh > create mode 100755 testcases/lib/tests/shell_loader_kconfigs.sh > create mode 100755 testcases/lib/tests/shell_loader_no_metadata.sh > create mode 100755 testcases/lib/tests/shell_loader_supported_archs.sh > create mode 100755 testcases/lib/tests/shell_loader_tags.sh > create mode 100755 testcases/lib/tests/shell_loader_tcnt.sh > create mode 100755 testcases/lib/tests/shell_loader_wrong_metadata.sh > create mode 100644 testcases/lib/tst_loader.sh > create mode 100644 testcases/lib/tst_run_shell.c > > diff --git a/include/tst_test.h b/include/tst_test.h > index 9871676a5..d0fa84a71 100644 > --- a/include/tst_test.h > +++ b/include/tst_test.h > @@ -274,7 +274,7 @@ struct tst_fs { > const char *const *mkfs_opts; > const char *mkfs_size_opt; > > - const unsigned int mnt_flags; > + unsigned int mnt_flags; > const void *mnt_data; > }; > > diff --git a/testcases/lib/.gitignore b/testcases/lib/.gitignore > index d0dacf62a..385f3c3ca 100644 > --- a/testcases/lib/.gitignore > +++ b/testcases/lib/.gitignore > @@ -24,3 +24,4 @@ > /tst_supported_fs > /tst_timeout_kill > /tst_res_ > +/tst_run_shell > diff --git a/testcases/lib/Makefile b/testcases/lib/Makefile > index 928d76d62..b3a9181c1 100644 > --- a/testcases/lib/Makefile > +++ b/testcases/lib/Makefile > @@ -4,6 +4,9 @@ > > top_srcdir ?= ../.. > > +LTPLIBS = ujson > +tst_run_shell: LTPLDLIBS = -lujson > + > include $(top_srcdir)/include/mk/testcases.mk > > INSTALL_TARGETS := *.sh > @@ -13,6 +16,7 @@ MAKE_TARGETS := tst_sleep tst_random tst_checkpoint tst_rod tst_kvcmp\ > tst_getconf tst_supported_fs tst_check_drivers tst_get_unused_port\ > tst_get_median tst_hexdump tst_get_free_pids tst_timeout_kill\ > tst_check_kconfigs tst_cgctl tst_fsfreeze tst_ns_create tst_ns_exec\ > - tst_ns_ifmove tst_lockdown_enabled tst_secureboot_enabled tst_res_ > + tst_ns_ifmove tst_lockdown_enabled tst_secureboot_enabled tst_res_\ > + tst_run_shell > > include $(top_srcdir)/include/mk/generic_trunk_target.mk > diff --git a/testcases/lib/run_tests.sh b/testcases/lib/run_tests.sh > index 60e7d1bcf..e30065f1d 100755 > --- a/testcases/lib/run_tests.sh > +++ b/testcases/lib/run_tests.sh > @@ -9,3 +9,24 @@ for i in `seq -w 01 06`; do > echo > ./tests/shell_test$i > done > + > +for i in shell_loader.sh shell_loader_all_filesystems.sh shell_loader_no_metadata.sh \ > + shell_loader_wrong_metadata.sh shell_loader_invalid_metadata.sh\ > + shell_loader_supported_archs.sh shell_loader_filesystems.sh\ > + shell_loader_tcnt.sh shell_loader_kconfigs.sh shell_loader_tags.sh \ > + shell_loader_invalid_block.sh; do > + echo > + echo "*** Running $i ***" > + echo > + $i > +done > + > +echo > +echo "*** Testing LTP test -h option ***" > +echo > +shell_loader.sh -h > + > +echo > +echo "*** Testing LTP test -i option ***" > +echo > +shell_loader.sh -i 2 > diff --git a/testcases/lib/tests/shell_loader.sh b/testcases/lib/tests/shell_loader.sh > new file mode 100755 > index 000000000..df7f0c0af > --- /dev/null > +++ b/testcases/lib/tests/shell_loader.sh > @@ -0,0 +1,26 @@ > +#!/bin/sh > +# > +# --- > +# doc > +# > +# [Description] > +# > +# This is a simple shell test loader example. > +# --- > +# > +# --- > +# env > +# { > +# "needs_tmpdir": true > +# } > +# --- > + > +. tst_loader.sh > + > +tst_res TPASS "Shell loader works fine!" > +case "$PWD" in > + /tmp/*) > + tst_res TPASS "We are running in temp directory in $PWD";; > + *) > + tst_res TFAIL "We are not running in temp directory but $PWD";; > +esac > diff --git a/testcases/lib/tests/shell_loader_all_filesystems.sh b/testcases/lib/tests/shell_loader_all_filesystems.sh > new file mode 100755 > index 000000000..d5943c335 > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_all_filesystems.sh > @@ -0,0 +1,27 @@ > +#!/bin/sh > +# > +# --- > +# env > +# { > +# "needs_root": true, > +# "mount_device": true, > +# "all_filesystems": true, > +# "mntpoint": "ltp_mntpoint" > +# } > +# --- > + > +. tst_loader.sh > + > +tst_res TINFO "In shell" > + > +mntpath=$(realpath ltp_mntpoint) > +mounted=$(grep $mntpath /proc/mounts) > + > +if [ -n "$mounted" ]; then > + device=$(echo $mounted |cut -d' ' -f 1) > + path=$(echo $mounted |cut -d' ' -f 2) > + > + tst_res TPASS "$device mounted at $path" > +else > + tst_res TFAIL "Device not mounted!" > +fi > diff --git a/testcases/lib/tests/shell_loader_filesystems.sh b/testcases/lib/tests/shell_loader_filesystems.sh > new file mode 100755 > index 000000000..5d8aa9808 > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_filesystems.sh > @@ -0,0 +1,33 @@ > +#!/bin/sh > +# > +# --- > +# env > +# { > +# "mount_device": true, > +# "mntpoint": "ltp_mntpoint", > +# "filesystems": [ > +# { > +# "type": "btrfs" > +# }, > +# { > +# "type": "xfs", > +# "mkfs_opts": ["-m", "reflink=1"] > +# } > +# ] > +# } > +# --- > + > +. tst_loader.sh > + > +tst_res TINFO "In shell" > + > +mntpoint=$(realpath ltp_mntpoint) > +mounted=$(grep $mntpoint /proc/mounts) > + > +if [ -n "$mounted" ]; then > + fs=$(echo $mounted |cut -d' ' -f 3) > + > + tst_res TPASS "Mounted device formatted with $fs" > +else > + tst_res TFAIL "Device not mounted!" > +fi > diff --git a/testcases/lib/tests/shell_loader_invalid_block.sh b/testcases/lib/tests/shell_loader_invalid_block.sh > new file mode 100755 > index 000000000..f41de04fd > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_invalid_block.sh > @@ -0,0 +1,26 @@ > +#!/bin/sh > +# > +# --- > +# doc > +# > +# [Description] > +# > +# This is a simple shell test loader example. > +# --- > +# > +# --- > +# env > +# { > +# "needs_tmpdir": true > +# } > +# --- > +# > +# --- > +# inv > +# > +# This is an invalid block that breaks the test. > +# --- > + > +. tst_loader.sh > + > +tst_res TPASS "This should pass!" > diff --git a/testcases/lib/tests/shell_loader_invalid_metadata.sh b/testcases/lib/tests/shell_loader_invalid_metadata.sh > new file mode 100755 > index 000000000..c10b00f1b > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_invalid_metadata.sh > @@ -0,0 +1,15 @@ > +#!/bin/sh > +# > +# This test has wrong metadata and should not be run > +# > +# --- > +# env > +# { > +# {"needs_tmpdir": 42, > +# } > +# --- > +# > + > +. tst_loader.sh > + > +tst_res TFAIL "Shell loader should TBROK the test" > diff --git a/testcases/lib/tests/shell_loader_kconfigs.sh b/testcases/lib/tests/shell_loader_kconfigs.sh > new file mode 100755 > index 000000000..7e9a1dce7 > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_kconfigs.sh > @@ -0,0 +1,12 @@ > +#!/bin/sh > +# > +# --- > +# env > +# { > +# "needs_kconfigs": ["CONFIG_NUMA=y"] > +# } > +# --- > + > +. tst_loader.sh > + > +tst_res TPASS "Shell loader works fine!" > diff --git a/testcases/lib/tests/shell_loader_no_metadata.sh b/testcases/lib/tests/shell_loader_no_metadata.sh > new file mode 100755 > index 000000000..60ba8b889 > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_no_metadata.sh > @@ -0,0 +1,8 @@ > +#!/bin/sh > +# > +# This test has no metadata and should not be executed > +# > + > +. tst_loader.sh > + > +tst_res TFAIL "Shell loader should TBROK the test" > diff --git a/testcases/lib/tests/shell_loader_supported_archs.sh b/testcases/lib/tests/shell_loader_supported_archs.sh > new file mode 100755 > index 000000000..45213f840 > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_supported_archs.sh > @@ -0,0 +1,12 @@ > +#!/bin/sh > +# > +# --- > +# env > +# { > +# "supported_archs": ["x86", "ppc64", "x86_64"] > +# } > +# --- > + > +. tst_loader.sh > + > +tst_res TPASS "We are running on supported architecture" > diff --git a/testcases/lib/tests/shell_loader_tags.sh b/testcases/lib/tests/shell_loader_tags.sh > new file mode 100755 > index 000000000..a6278c37d > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_tags.sh > @@ -0,0 +1,15 @@ > +#!/bin/sh > +# > +# --- > +# env > +# { > +# "tags": [ > +# ["linux-git", "832478cd342ab"], > +# ["CVE", "2099-999"] > +# ] > +# } > +# --- > + > +. tst_loader.sh > + > +tst_res TFAIL "Fails the test so that tags are shown." > diff --git a/testcases/lib/tests/shell_loader_tcnt.sh b/testcases/lib/tests/shell_loader_tcnt.sh > new file mode 100755 > index 000000000..81fc08179 > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_tcnt.sh > @@ -0,0 +1,15 @@ > +#!/bin/sh > +# > +# The script should be executed tcnt times and the iteration number should be in $1 > +# > +# --- > +# env > +# { > +# "tcnt": 2 > +# } > +# --- > +# > + > +. tst_loader.sh > + > +tst_res TPASS "Iteration $1" > diff --git a/testcases/lib/tests/shell_loader_wrong_metadata.sh b/testcases/lib/tests/shell_loader_wrong_metadata.sh > new file mode 100755 > index 000000000..752e25eea > --- /dev/null > +++ b/testcases/lib/tests/shell_loader_wrong_metadata.sh > @@ -0,0 +1,15 @@ > +#!/bin/sh > +# > +# This test has wrong metadata and should not be run > +# > +# --- > +# env > +# { > +# "needs_tmpdir": 42, > +# } > +# --- > +# > + > +. tst_loader.sh > + > +tst_res TFAIL "Shell loader should TBROK the test" > diff --git a/testcases/lib/tst_env.sh b/testcases/lib/tst_env.sh > index 948bc5024..67ba80744 100644 > --- a/testcases/lib/tst_env.sh > +++ b/testcases/lib/tst_env.sh > @@ -1,4 +1,8 @@ > #!/bin/sh > +# > +# This is a minimal test environment for a shell scripts executed from C by > +# tst_run_shell() function. Shell tests must use the tst_loader.sh instead! > +# > > tst_script_name=$(basename $0) > > diff --git a/testcases/lib/tst_loader.sh b/testcases/lib/tst_loader.sh > new file mode 100644 > index 000000000..ed04d0340 > --- /dev/null > +++ b/testcases/lib/tst_loader.sh > @@ -0,0 +1,11 @@ > +#!/bin/sh > +# > +# This is a loader for shell tests that use the C test library. > +# > + > +if [ -z "$LTP_IPC_PATH" ]; then > + tst_run_shell $(basename "$0") "$@" > + exit $? > +else > + . tst_env.sh > +fi > diff --git a/testcases/lib/tst_run_shell.c b/testcases/lib/tst_run_shell.c > new file mode 100644 > index 000000000..8ed0f21b6 > --- /dev/null > +++ b/testcases/lib/tst_run_shell.c > @@ -0,0 +1,491 @@ > +// SPDX-License-Identifier: GPL-2.0-or-later > +/* > + * Copyright (c) 2024 Cyril Hrubis <chrubis@suse.cz> > + */ > +#include <sys/mount.h> > + > +#define TST_NO_DEFAULT_MAIN > +#include "tst_test.h" > +#include "tst_safe_stdio.h" > +#include "ujson.h" > + > +static char *shell_filename; > + > +static void run_shell(void) > +{ > + tst_run_script(shell_filename, NULL); > +} > + > +static void run_shell_tcnt(unsigned int n) > +{ > + char buf[128]; > + char *const params[] = {buf, NULL}; > + > + snprintf(buf, sizeof(buf), "%u", n); > + > + tst_run_script(shell_filename, params); > +} > + > +struct tst_test test = { > + .runs_script = 1, > +}; > + > +static void print_help(void) > +{ > + printf("Usage: tst_shell_loader ltp_shell_test.sh ...\n"); > +} > + > +static char *metadata; > +static size_t metadata_size; > +static size_t metadata_used; > + > +static void metadata_append(const char *line) > +{ > + size_t linelen = strlen(line); > + > + if (metadata_size - metadata_used < linelen + 1) { > + metadata_size += 4096; > + metadata = SAFE_REALLOC(metadata, metadata_size); > + } > + > + strcpy(metadata + metadata_used, line); > + metadata_used += linelen; > +} > + > +enum test_attr_ids { > + ALL_FILESYSTEMS, > + DEV_MIN_SIZE, > + FILESYSTEMS, > + FORMAT_DEVICE, > + MIN_CPUS, > + MIN_MEM_AVAIL, > + MIN_KVER, > + MIN_SWAP_AVAIL, > + MNTPOINT, > + MOUNT_DEVICE, > + NEEDS_ABI_BITS, > + NEEDS_CMDS, > + NEEDS_DEVFS, > + NEEDS_DEVICE, > + NEEDS_DRIVERS, > + NEEDS_HUGETLBFS, > + NEEDS_KCONFIGS, > + NEEDS_ROFS, > + NEEDS_ROOT, > + NEEDS_TMPDIR, > + RESTORE_WALLCLOCK, > + SKIP_FILESYSTEMS, > + SKIP_IN_COMPAT, > + SKIP_IN_LOCKDOWN, > + SKIP_IN_SECUREBOOT, > + SUPPORTED_ARCHS, > + TAGS, > + TAINT_CHECK, > + TCNT, > +}; > + > +static ujson_obj_attr test_attrs[] = { > + UJSON_OBJ_ATTR_IDX(ALL_FILESYSTEMS, "all_filesystems", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(DEV_MIN_SIZE, "dev_min_size", UJSON_INT), > + UJSON_OBJ_ATTR_IDX(FILESYSTEMS, "filesystems", UJSON_ARR), > + UJSON_OBJ_ATTR_IDX(FORMAT_DEVICE, "format_device", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(MIN_CPUS, "min_cpus", UJSON_INT), > + UJSON_OBJ_ATTR_IDX(MIN_MEM_AVAIL, "min_mem_avail", UJSON_INT), > + UJSON_OBJ_ATTR_IDX(MIN_KVER, "min_kver", UJSON_STR), > + UJSON_OBJ_ATTR_IDX(MIN_SWAP_AVAIL, "min_swap_avail", UJSON_INT), > + UJSON_OBJ_ATTR_IDX(MNTPOINT, "mntpoint", UJSON_STR), > + UJSON_OBJ_ATTR_IDX(MOUNT_DEVICE, "mount_device", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(NEEDS_ABI_BITS, "needs_abi_bits", UJSON_INT), > + UJSON_OBJ_ATTR_IDX(NEEDS_CMDS, "needs_cmds", UJSON_ARR), > + UJSON_OBJ_ATTR_IDX(NEEDS_DEVFS, "needs_devfs", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(NEEDS_DEVICE, "needs_device", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(NEEDS_DRIVERS, "needs_drivers", UJSON_ARR), > + UJSON_OBJ_ATTR_IDX(NEEDS_HUGETLBFS, "needs_hugetlbfs", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(NEEDS_KCONFIGS, "needs_kconfigs", UJSON_ARR), > + UJSON_OBJ_ATTR_IDX(NEEDS_ROFS, "needs_rofs", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(NEEDS_ROOT, "needs_root", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(NEEDS_TMPDIR, "needs_tmpdir", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(RESTORE_WALLCLOCK, "restore_wallclock", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(SKIP_FILESYSTEMS, "skip_filesystems", UJSON_ARR), > + UJSON_OBJ_ATTR_IDX(SKIP_IN_COMPAT, "skip_in_compat", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(SKIP_IN_LOCKDOWN, "skip_in_lockdown", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(SKIP_IN_SECUREBOOT, "skip_in_secureboot", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(SUPPORTED_ARCHS, "supported_archs", UJSON_ARR), > + UJSON_OBJ_ATTR_IDX(TAGS, "tags", UJSON_ARR), > + UJSON_OBJ_ATTR_IDX(TAINT_CHECK, "taint_check", UJSON_BOOL), > + UJSON_OBJ_ATTR_IDX(TCNT, "tcnt", UJSON_INT) > +}; > + > +static ujson_obj test_obj = { > + .attrs = test_attrs, > + .attr_cnt = UJSON_ARRAY_SIZE(test_attrs), > +}; > + > +static const char *const *parse_strarr(ujson_reader *reader, ujson_val *val) > +{ > + unsigned int cnt = 0, i = 0; > + char **ret; > + > + ujson_reader_state state = ujson_reader_state_save(reader); > + > + UJSON_ARR_FOREACH(reader, val) { > + if (val->type != UJSON_STR) { > + ujson_err(reader, "Expected string!"); > + return NULL; > + } > + > + cnt++; > + } > + > + ujson_reader_state_load(reader, state); > + > + ret = SAFE_MALLOC(sizeof(char*) * (cnt + 1)); > + > + UJSON_ARR_FOREACH(reader, val) { > + ret[i++] = strdup(val->val_str); > + } > + > + ret[i] = NULL; > + > + return (const char *const *)ret; > +} > + > +enum fs_ids { > + MKFS_OPTS, > + MKFS_SIZE_OPT, > + MNT_FLAGS, > + TYPE, > +}; > + > +static ujson_obj_attr fs_attrs[] = { > + UJSON_OBJ_ATTR_IDX(MKFS_OPTS, "mkfs_opts", UJSON_ARR), > + UJSON_OBJ_ATTR_IDX(MKFS_SIZE_OPT, "mkfs_size_opt", UJSON_STR), > + UJSON_OBJ_ATTR_IDX(MNT_FLAGS, "mnt_flags", UJSON_ARR), > + UJSON_OBJ_ATTR_IDX(TYPE, "type", UJSON_STR), > +}; > + > +static ujson_obj fs_obj = { > + .attrs = fs_attrs, > + .attr_cnt = UJSON_ARRAY_SIZE(fs_attrs), > +}; > + > +static int parse_mnt_flags(ujson_reader *reader, ujson_val *val) > +{ > + int ret = 0; > + > + UJSON_ARR_FOREACH(reader, val) { > + if (val->type != UJSON_STR) { > + ujson_err(reader, "Expected string!"); > + return ret; > + } > + > + if (!strcmp(val->val_str, "RDONLY")) > + ret |= MS_RDONLY; > + else if (!strcmp(val->val_str, "NOATIME")) > + ret |= MS_NOATIME; > + else if (!strcmp(val->val_str, "NOEXEC")) > + ret |= MS_NOEXEC; > + else if (!strcmp(val->val_str, "NOSUID")) > + ret |= MS_NOSUID; > + else > + ujson_err(reader, "Invalid mount flag"); > + } > + > + return ret; > +} > + > +static struct tst_fs *parse_filesystems(ujson_reader *reader, ujson_val *val) > +{ > + unsigned int i = 0, cnt = 0; > + struct tst_fs *ret; > + > + ujson_reader_state state = ujson_reader_state_save(reader); > + > + UJSON_ARR_FOREACH(reader, val) { > + if (val->type != UJSON_OBJ) { > + ujson_err(reader, "Expected object!"); > + return NULL; > + } > + ujson_obj_skip(reader); > + cnt++; > + } > + > + ujson_reader_state_load(reader, state); > + > + ret = SAFE_MALLOC(sizeof(struct tst_fs) * (cnt + 1)); > + memset(ret, 0, sizeof(*ret) * (cnt+1)); > + > + UJSON_ARR_FOREACH(reader, val) { > + UJSON_OBJ_FOREACH_FILTER(reader, val, &fs_obj, ujson_empty_obj) { > + switch ((enum fs_ids)val->idx) { > + case MKFS_OPTS: > + ret[i].mkfs_opts = parse_strarr(reader, val); > + break; > + case MKFS_SIZE_OPT: > + ret[i].mkfs_size_opt = strdup(val->val_str); > + break; > + case MNT_FLAGS: > + ret[i].mnt_flags = parse_mnt_flags(reader, val); > + break; > + case TYPE: > + ret[i].type = strdup(val->val_str); > + break; > + } > + > + } > + > + i++; > + } > + > + return ret; > +} > + > +static struct tst_tag *parse_tags(ujson_reader *reader, ujson_val *val) > +{ > + unsigned int i = 0, cnt = 0; > + struct tst_tag *ret; > + > + ujson_reader_state state = ujson_reader_state_save(reader); > + > + UJSON_ARR_FOREACH(reader, val) { > + if (val->type != UJSON_ARR) { > + ujson_err(reader, "Expected array!"); > + return NULL; > + } > + ujson_arr_skip(reader); > + cnt++; > + } > + > + ujson_reader_state_load(reader, state); > + > + ret = SAFE_MALLOC(sizeof(struct tst_tag) * (cnt + 1)); > + memset(&ret[cnt], 0, sizeof(ret[cnt])); > + > + UJSON_ARR_FOREACH(reader, val) { > + char *name = NULL; > + char *value = NULL; > + > + UJSON_ARR_FOREACH(reader, val) { > + if (val->type != UJSON_STR) { > + ujson_err(reader, "Expected string!"); > + return NULL; > + } > + > + if (!name) { > + name = strdup(val->val_str); > + } else if (!value) { > + value = strdup(val->val_str); > + } else { > + ujson_err(reader, "Expected only two members!"); > + return NULL; > + } > + } > + > + ret[i].name = name; > + ret[i].value = value; > + i++; > + } > + > + return ret; > +} > + > +static void parse_metadata(void) > +{ > + ujson_reader reader = UJSON_READER_INIT(metadata, metadata_used, UJSON_READER_STRICT); > + char str_buf[128]; > + ujson_val val = UJSON_VAL_INIT(str_buf, sizeof(str_buf)); > + > + UJSON_OBJ_FOREACH_FILTER(&reader, &val, &test_obj, ujson_empty_obj) { > + switch ((enum test_attr_ids)val.idx) { > + case ALL_FILESYSTEMS: > + test.all_filesystems = val.val_bool; > + break; > + case DEV_MIN_SIZE: > + if (val.val_int <= 0) > + ujson_err(&reader, "Device size must be > 0"); > + else > + test.dev_min_size = val.val_int; > + break; > + case FILESYSTEMS: > + test.filesystems = parse_filesystems(&reader, &val); > + break; > + case FORMAT_DEVICE: > + test.format_device = val.val_bool; > + break; > + case MIN_CPUS: > + if (val.val_int <= 0) > + ujson_err(&reader, "Minimal number of cpus must be > 0"); > + else > + test.min_cpus = val.val_int; > + break; > + case MIN_MEM_AVAIL: > + if (val.val_int <= 0) > + ujson_err(&reader, "Minimal available memory size must be > 0"); > + else > + test.min_mem_avail = val.val_int; > + break; > + case MIN_KVER: > + test.min_kver = strdup(val.val_str); > + break; > + case MIN_SWAP_AVAIL: > + if (val.val_int <= 0) > + ujson_err(&reader, "Minimal available swap size must be > 0"); > + else > + test.min_swap_avail = val.val_int; > + break; > + case MNTPOINT: > + test.mntpoint = strdup(val.val_str); > + break; > + case MOUNT_DEVICE: > + test.mount_device = val.val_bool; > + break; > + case NEEDS_ABI_BITS: > + if (val.val_int == 32 || val.val_int == 64) > + test.needs_abi_bits = val.val_int; > + else > + ujson_err(&reader, "ABI bits must be 32 or 64"); > + break; > + case NEEDS_CMDS: > + test.needs_cmds = parse_strarr(&reader, &val); > + break; > + case NEEDS_DEVFS: > + test.needs_devfs = val.val_bool; > + break; > + case NEEDS_DEVICE: > + test.needs_device = val.val_bool; > + break; > + case NEEDS_DRIVERS: > + test.needs_drivers = parse_strarr(&reader, &val); > + break; > + case NEEDS_HUGETLBFS: > + test.needs_hugetlbfs = val.val_bool; > + break; > + case NEEDS_KCONFIGS: > + test.needs_kconfigs = parse_strarr(&reader, &val); > + break; > + case NEEDS_ROFS: > + test.needs_rofs = val.val_bool; > + break; > + case NEEDS_ROOT: > + test.needs_root = val.val_bool; > + break; > + case NEEDS_TMPDIR: > + test.needs_tmpdir = val.val_bool; > + break; > + case RESTORE_WALLCLOCK: > + test.restore_wallclock = val.val_bool; > + break; > + case SKIP_FILESYSTEMS: > + test.skip_filesystems = parse_strarr(&reader, &val); > + break; > + case SKIP_IN_COMPAT: > + test.skip_in_compat = val.val_bool; > + break; > + case SKIP_IN_LOCKDOWN: > + test.skip_in_lockdown = val.val_bool; > + break; > + case SKIP_IN_SECUREBOOT: > + test.skip_in_secureboot = val.val_bool; > + break; > + case SUPPORTED_ARCHS: > + test.supported_archs = parse_strarr(&reader, &val); > + break; > + case TAGS: > + test.tags = parse_tags(&reader, &val); > + break; > + case TAINT_CHECK: > + test.taint_check = val.val_bool; > + break; > + case TCNT: > + if (val.val_int <= 0) > + ujson_err(&reader, "Number of tests must be > 0"); > + else > + test.tcnt = val.val_int; > + break; > + } > + } > + > + ujson_reader_finish(&reader); > + > + if (ujson_reader_err(&reader)) > + tst_brk(TBROK, "Invalid metadata"); > +} > + > +enum parser_state { > + PAR_NONE, > + PAR_ESC, > + PAR_DOC, > + PAR_ENV, > +}; > + > +static void extract_metadata(void) > +{ > + FILE *f; > + char line[4096]; > + char path[4096]; > + enum parser_state state = PAR_NONE; > + > + if (tst_get_path(shell_filename, path, sizeof(path)) == -1) > + tst_brk(TBROK, "Failed to find %s in $PATH", shell_filename); > + > + f = SAFE_FOPEN(path, "r"); > + > + while (fgets(line, sizeof(line), f)) { > + switch (state) { > + case PAR_NONE: > + if (!strcmp(line, "# ---\n")) > + state = PAR_ESC; > + break; > + case PAR_ESC: > + if (!strcmp(line, "# env\n")) > + state = PAR_ENV; > + else if (!strcmp(line, "# doc\n")) > + state = PAR_DOC; > + else > + tst_brk(TBROK, "Unknown comment block %s", line); > + break; > + case PAR_ENV: > + if (!strcmp(line, "# ---\n")) > + state = PAR_NONE; > + else > + metadata_append(line + 2); > + break; > + case PAR_DOC: > + if (!strcmp(line, "# ---\n")) > + state = PAR_NONE; > + break; > + } > + } > + > + fclose(f); > +} > + > +static void prepare_test_struct(void) > +{ > + extract_metadata(); > + > + if (metadata) > + parse_metadata(); > + else > + tst_brk(TBROK, "No metadata found!"); > +} > + > +int main(int argc, char *argv[]) > +{ > + if (argc < 2) > + goto help; > + > + shell_filename = argv[1]; > + > + prepare_test_struct(); > + > + if (test.tcnt) > + test.test = run_shell_tcnt; > + else > + test.test_all = run_shell; > + > + tst_run_tcases(argc - 1, argv + 1, &test); > +help: > + print_help(); > + return 1; > +}
On Tue, Aug 27, 2024 at 8:05 PM Cyril Hrubis <chrubis@suse.cz> wrote: > This commit implements a shell loader so that we don't have to write a C > loader for each LTP shell test. The idea is simple, the loader parses > the shell test and prepares the tst_test structure accordingly, then > runs the actual shell test. > > The format for the metadata in the shell test was choosen to be JSON > because: > > - I didn't want to invent an adhoc format and JSON is perfect for > serializing data structures > - The metadata parser for shell test will be trivial, it will just pick > the JSON from the comment, no parsing will be required > > Signed-off-by: Cyril Hrubis <chrubis@suse.cz> > Reviewed-by: Richard Palethorpe <io@richiejp.com> > Reviewed-by: Li Wang <liwang@redhat.com>
Hi!
> Also is there a reason why we are adding tests to testcases/lib/tests ?
That is because historically all the libraries for shell tests are in
testcases/lib/. We should probably do a big cleanup and reorganization
of the codebase, but that is something to be done later on.
Hi! > > +static void extract_metadata(void) > > +{ > > + FILE *f; > > + char line[4096]; > > + char path[4096]; > > + enum parser_state state = PAR_NONE; > > + > > + if (tst_get_path(shell_filename, path, sizeof(path)) == -1) > > + tst_brk(TBROK, "Failed to find %s in $PATH", shell_filename); > > + > > + f = SAFE_FOPEN(path, "r"); > > + > > + while (fgets(line, sizeof(line), f)) { > > + switch (state) { > > + case PAR_NONE: > > + if (!strcmp(line, "# ---\n")) > What if user defines "#---" or "# ---" ? IMHO it would be better to > parse it following shell comments standards. In particular, "^#\s+---" > using a *scan function. If user sets the line any differently the test will end up with pretty clear error message that metadata are missing, so I do not see this as an issue. > > + state = PAR_ESC; > > + break; > > + case PAR_ESC: > > + if (!strcmp(line, "# env\n")) > Same apply here and to the others. Same here as well.
diff --git a/include/tst_test.h b/include/tst_test.h index 9871676a5..d0fa84a71 100644 --- a/include/tst_test.h +++ b/include/tst_test.h @@ -274,7 +274,7 @@ struct tst_fs { const char *const *mkfs_opts; const char *mkfs_size_opt; - const unsigned int mnt_flags; + unsigned int mnt_flags; const void *mnt_data; }; diff --git a/testcases/lib/.gitignore b/testcases/lib/.gitignore index d0dacf62a..385f3c3ca 100644 --- a/testcases/lib/.gitignore +++ b/testcases/lib/.gitignore @@ -24,3 +24,4 @@ /tst_supported_fs /tst_timeout_kill /tst_res_ +/tst_run_shell diff --git a/testcases/lib/Makefile b/testcases/lib/Makefile index 928d76d62..b3a9181c1 100644 --- a/testcases/lib/Makefile +++ b/testcases/lib/Makefile @@ -4,6 +4,9 @@ top_srcdir ?= ../.. +LTPLIBS = ujson +tst_run_shell: LTPLDLIBS = -lujson + include $(top_srcdir)/include/mk/testcases.mk INSTALL_TARGETS := *.sh @@ -13,6 +16,7 @@ MAKE_TARGETS := tst_sleep tst_random tst_checkpoint tst_rod tst_kvcmp\ tst_getconf tst_supported_fs tst_check_drivers tst_get_unused_port\ tst_get_median tst_hexdump tst_get_free_pids tst_timeout_kill\ tst_check_kconfigs tst_cgctl tst_fsfreeze tst_ns_create tst_ns_exec\ - tst_ns_ifmove tst_lockdown_enabled tst_secureboot_enabled tst_res_ + tst_ns_ifmove tst_lockdown_enabled tst_secureboot_enabled tst_res_\ + tst_run_shell include $(top_srcdir)/include/mk/generic_trunk_target.mk diff --git a/testcases/lib/run_tests.sh b/testcases/lib/run_tests.sh index 60e7d1bcf..e30065f1d 100755 --- a/testcases/lib/run_tests.sh +++ b/testcases/lib/run_tests.sh @@ -9,3 +9,24 @@ for i in `seq -w 01 06`; do echo ./tests/shell_test$i done + +for i in shell_loader.sh shell_loader_all_filesystems.sh shell_loader_no_metadata.sh \ + shell_loader_wrong_metadata.sh shell_loader_invalid_metadata.sh\ + shell_loader_supported_archs.sh shell_loader_filesystems.sh\ + shell_loader_tcnt.sh shell_loader_kconfigs.sh shell_loader_tags.sh \ + shell_loader_invalid_block.sh; do + echo + echo "*** Running $i ***" + echo + $i +done + +echo +echo "*** Testing LTP test -h option ***" +echo +shell_loader.sh -h + +echo +echo "*** Testing LTP test -i option ***" +echo +shell_loader.sh -i 2 diff --git a/testcases/lib/tests/shell_loader.sh b/testcases/lib/tests/shell_loader.sh new file mode 100755 index 000000000..df7f0c0af --- /dev/null +++ b/testcases/lib/tests/shell_loader.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# +# --- +# doc +# +# [Description] +# +# This is a simple shell test loader example. +# --- +# +# --- +# env +# { +# "needs_tmpdir": true +# } +# --- + +. tst_loader.sh + +tst_res TPASS "Shell loader works fine!" +case "$PWD" in + /tmp/*) + tst_res TPASS "We are running in temp directory in $PWD";; + *) + tst_res TFAIL "We are not running in temp directory but $PWD";; +esac diff --git a/testcases/lib/tests/shell_loader_all_filesystems.sh b/testcases/lib/tests/shell_loader_all_filesystems.sh new file mode 100755 index 000000000..d5943c335 --- /dev/null +++ b/testcases/lib/tests/shell_loader_all_filesystems.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# +# --- +# env +# { +# "needs_root": true, +# "mount_device": true, +# "all_filesystems": true, +# "mntpoint": "ltp_mntpoint" +# } +# --- + +. tst_loader.sh + +tst_res TINFO "In shell" + +mntpath=$(realpath ltp_mntpoint) +mounted=$(grep $mntpath /proc/mounts) + +if [ -n "$mounted" ]; then + device=$(echo $mounted |cut -d' ' -f 1) + path=$(echo $mounted |cut -d' ' -f 2) + + tst_res TPASS "$device mounted at $path" +else + tst_res TFAIL "Device not mounted!" +fi diff --git a/testcases/lib/tests/shell_loader_filesystems.sh b/testcases/lib/tests/shell_loader_filesystems.sh new file mode 100755 index 000000000..5d8aa9808 --- /dev/null +++ b/testcases/lib/tests/shell_loader_filesystems.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# +# --- +# env +# { +# "mount_device": true, +# "mntpoint": "ltp_mntpoint", +# "filesystems": [ +# { +# "type": "btrfs" +# }, +# { +# "type": "xfs", +# "mkfs_opts": ["-m", "reflink=1"] +# } +# ] +# } +# --- + +. tst_loader.sh + +tst_res TINFO "In shell" + +mntpoint=$(realpath ltp_mntpoint) +mounted=$(grep $mntpoint /proc/mounts) + +if [ -n "$mounted" ]; then + fs=$(echo $mounted |cut -d' ' -f 3) + + tst_res TPASS "Mounted device formatted with $fs" +else + tst_res TFAIL "Device not mounted!" +fi diff --git a/testcases/lib/tests/shell_loader_invalid_block.sh b/testcases/lib/tests/shell_loader_invalid_block.sh new file mode 100755 index 000000000..f41de04fd --- /dev/null +++ b/testcases/lib/tests/shell_loader_invalid_block.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# +# --- +# doc +# +# [Description] +# +# This is a simple shell test loader example. +# --- +# +# --- +# env +# { +# "needs_tmpdir": true +# } +# --- +# +# --- +# inv +# +# This is an invalid block that breaks the test. +# --- + +. tst_loader.sh + +tst_res TPASS "This should pass!" diff --git a/testcases/lib/tests/shell_loader_invalid_metadata.sh b/testcases/lib/tests/shell_loader_invalid_metadata.sh new file mode 100755 index 000000000..c10b00f1b --- /dev/null +++ b/testcases/lib/tests/shell_loader_invalid_metadata.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# +# This test has wrong metadata and should not be run +# +# --- +# env +# { +# {"needs_tmpdir": 42, +# } +# --- +# + +. tst_loader.sh + +tst_res TFAIL "Shell loader should TBROK the test" diff --git a/testcases/lib/tests/shell_loader_kconfigs.sh b/testcases/lib/tests/shell_loader_kconfigs.sh new file mode 100755 index 000000000..7e9a1dce7 --- /dev/null +++ b/testcases/lib/tests/shell_loader_kconfigs.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# +# --- +# env +# { +# "needs_kconfigs": ["CONFIG_NUMA=y"] +# } +# --- + +. tst_loader.sh + +tst_res TPASS "Shell loader works fine!" diff --git a/testcases/lib/tests/shell_loader_no_metadata.sh b/testcases/lib/tests/shell_loader_no_metadata.sh new file mode 100755 index 000000000..60ba8b889 --- /dev/null +++ b/testcases/lib/tests/shell_loader_no_metadata.sh @@ -0,0 +1,8 @@ +#!/bin/sh +# +# This test has no metadata and should not be executed +# + +. tst_loader.sh + +tst_res TFAIL "Shell loader should TBROK the test" diff --git a/testcases/lib/tests/shell_loader_supported_archs.sh b/testcases/lib/tests/shell_loader_supported_archs.sh new file mode 100755 index 000000000..45213f840 --- /dev/null +++ b/testcases/lib/tests/shell_loader_supported_archs.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# +# --- +# env +# { +# "supported_archs": ["x86", "ppc64", "x86_64"] +# } +# --- + +. tst_loader.sh + +tst_res TPASS "We are running on supported architecture" diff --git a/testcases/lib/tests/shell_loader_tags.sh b/testcases/lib/tests/shell_loader_tags.sh new file mode 100755 index 000000000..a6278c37d --- /dev/null +++ b/testcases/lib/tests/shell_loader_tags.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# +# --- +# env +# { +# "tags": [ +# ["linux-git", "832478cd342ab"], +# ["CVE", "2099-999"] +# ] +# } +# --- + +. tst_loader.sh + +tst_res TFAIL "Fails the test so that tags are shown." diff --git a/testcases/lib/tests/shell_loader_tcnt.sh b/testcases/lib/tests/shell_loader_tcnt.sh new file mode 100755 index 000000000..81fc08179 --- /dev/null +++ b/testcases/lib/tests/shell_loader_tcnt.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# +# The script should be executed tcnt times and the iteration number should be in $1 +# +# --- +# env +# { +# "tcnt": 2 +# } +# --- +# + +. tst_loader.sh + +tst_res TPASS "Iteration $1" diff --git a/testcases/lib/tests/shell_loader_wrong_metadata.sh b/testcases/lib/tests/shell_loader_wrong_metadata.sh new file mode 100755 index 000000000..752e25eea --- /dev/null +++ b/testcases/lib/tests/shell_loader_wrong_metadata.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# +# This test has wrong metadata and should not be run +# +# --- +# env +# { +# "needs_tmpdir": 42, +# } +# --- +# + +. tst_loader.sh + +tst_res TFAIL "Shell loader should TBROK the test" diff --git a/testcases/lib/tst_env.sh b/testcases/lib/tst_env.sh index 948bc5024..67ba80744 100644 --- a/testcases/lib/tst_env.sh +++ b/testcases/lib/tst_env.sh @@ -1,4 +1,8 @@ #!/bin/sh +# +# This is a minimal test environment for a shell scripts executed from C by +# tst_run_shell() function. Shell tests must use the tst_loader.sh instead! +# tst_script_name=$(basename $0) diff --git a/testcases/lib/tst_loader.sh b/testcases/lib/tst_loader.sh new file mode 100644 index 000000000..ed04d0340 --- /dev/null +++ b/testcases/lib/tst_loader.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# +# This is a loader for shell tests that use the C test library. +# + +if [ -z "$LTP_IPC_PATH" ]; then + tst_run_shell $(basename "$0") "$@" + exit $? +else + . tst_env.sh +fi diff --git a/testcases/lib/tst_run_shell.c b/testcases/lib/tst_run_shell.c new file mode 100644 index 000000000..8ed0f21b6 --- /dev/null +++ b/testcases/lib/tst_run_shell.c @@ -0,0 +1,491 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (c) 2024 Cyril Hrubis <chrubis@suse.cz> + */ +#include <sys/mount.h> + +#define TST_NO_DEFAULT_MAIN +#include "tst_test.h" +#include "tst_safe_stdio.h" +#include "ujson.h" + +static char *shell_filename; + +static void run_shell(void) +{ + tst_run_script(shell_filename, NULL); +} + +static void run_shell_tcnt(unsigned int n) +{ + char buf[128]; + char *const params[] = {buf, NULL}; + + snprintf(buf, sizeof(buf), "%u", n); + + tst_run_script(shell_filename, params); +} + +struct tst_test test = { + .runs_script = 1, +}; + +static void print_help(void) +{ + printf("Usage: tst_shell_loader ltp_shell_test.sh ...\n"); +} + +static char *metadata; +static size_t metadata_size; +static size_t metadata_used; + +static void metadata_append(const char *line) +{ + size_t linelen = strlen(line); + + if (metadata_size - metadata_used < linelen + 1) { + metadata_size += 4096; + metadata = SAFE_REALLOC(metadata, metadata_size); + } + + strcpy(metadata + metadata_used, line); + metadata_used += linelen; +} + +enum test_attr_ids { + ALL_FILESYSTEMS, + DEV_MIN_SIZE, + FILESYSTEMS, + FORMAT_DEVICE, + MIN_CPUS, + MIN_MEM_AVAIL, + MIN_KVER, + MIN_SWAP_AVAIL, + MNTPOINT, + MOUNT_DEVICE, + NEEDS_ABI_BITS, + NEEDS_CMDS, + NEEDS_DEVFS, + NEEDS_DEVICE, + NEEDS_DRIVERS, + NEEDS_HUGETLBFS, + NEEDS_KCONFIGS, + NEEDS_ROFS, + NEEDS_ROOT, + NEEDS_TMPDIR, + RESTORE_WALLCLOCK, + SKIP_FILESYSTEMS, + SKIP_IN_COMPAT, + SKIP_IN_LOCKDOWN, + SKIP_IN_SECUREBOOT, + SUPPORTED_ARCHS, + TAGS, + TAINT_CHECK, + TCNT, +}; + +static ujson_obj_attr test_attrs[] = { + UJSON_OBJ_ATTR_IDX(ALL_FILESYSTEMS, "all_filesystems", UJSON_BOOL), + UJSON_OBJ_ATTR_IDX(DEV_MIN_SIZE, "dev_min_size", UJSON_INT), + UJSON_OBJ_ATTR_IDX(FILESYSTEMS, "filesystems", UJSON_ARR), + UJSON_OBJ_ATTR_IDX(FORMAT_DEVICE, "format_device", UJSON_BOOL), + UJSON_OBJ_ATTR_IDX(MIN_CPUS, "min_cpus", UJSON_INT), + UJSON_OBJ_ATTR_IDX(MIN_MEM_AVAIL, "min_mem_avail", UJSON_INT), + UJSON_OBJ_ATTR_IDX(MIN_KVER, "min_kver", UJSON_STR), + UJSON_OBJ_ATTR_IDX(MIN_SWAP_AVAIL, "min_swap_avail", UJSON_INT), + UJSON_OBJ_ATTR_IDX(MNTPOINT, "mntpoint", UJSON_STR), + UJSON_OBJ_ATTR_IDX(MOUNT_DEVICE, "mount_device", UJSON_BOOL), + UJSON_OBJ_ATTR_IDX(NEEDS_ABI_BITS, "needs_abi_bits", UJSON_INT), + UJSON_OBJ_ATTR_IDX(NEEDS_CMDS, "needs_cmds", UJSON_ARR), + UJSON_OBJ_ATTR_IDX(NEEDS_DEVFS, "needs_devfs", UJSON_BOOL), + UJSON_OBJ_ATTR_IDX(NEEDS_DEVICE, "needs_device", UJSON_BOOL), + UJSON_OBJ_ATTR_IDX(NEEDS_DRIVERS, "needs_drivers", UJSON_ARR), + UJSON_OBJ_ATTR_IDX(NEEDS_HUGETLBFS, "needs_hugetlbfs", UJSON_BOOL), + UJSON_OBJ_ATTR_IDX(NEEDS_KCONFIGS, "needs_kconfigs", UJSON_ARR), + UJSON_OBJ_ATTR_IDX(NEEDS_ROFS, "needs_rofs", UJSON_BOOL), + UJSON_OBJ_ATTR_IDX(NEEDS_ROOT, "needs_root", UJSON_BOOL), + UJSON_OBJ_ATTR_IDX(NEEDS_TMPDIR, "needs_tmpdir", UJSON_BOOL), + UJSON_OBJ_ATTR_IDX(RESTORE_WALLCLOCK, "restore_wallclock", UJSON_BOOL), + UJSON_OBJ_ATTR_IDX(SKIP_FILESYSTEMS, "skip_filesystems", UJSON_ARR), + UJSON_OBJ_ATTR_IDX(SKIP_IN_COMPAT, "skip_in_compat", UJSON_BOOL), + UJSON_OBJ_ATTR_IDX(SKIP_IN_LOCKDOWN, "skip_in_lockdown", UJSON_BOOL), + UJSON_OBJ_ATTR_IDX(SKIP_IN_SECUREBOOT, "skip_in_secureboot", UJSON_BOOL), + UJSON_OBJ_ATTR_IDX(SUPPORTED_ARCHS, "supported_archs", UJSON_ARR), + UJSON_OBJ_ATTR_IDX(TAGS, "tags", UJSON_ARR), + UJSON_OBJ_ATTR_IDX(TAINT_CHECK, "taint_check", UJSON_BOOL), + UJSON_OBJ_ATTR_IDX(TCNT, "tcnt", UJSON_INT) +}; + +static ujson_obj test_obj = { + .attrs = test_attrs, + .attr_cnt = UJSON_ARRAY_SIZE(test_attrs), +}; + +static const char *const *parse_strarr(ujson_reader *reader, ujson_val *val) +{ + unsigned int cnt = 0, i = 0; + char **ret; + + ujson_reader_state state = ujson_reader_state_save(reader); + + UJSON_ARR_FOREACH(reader, val) { + if (val->type != UJSON_STR) { + ujson_err(reader, "Expected string!"); + return NULL; + } + + cnt++; + } + + ujson_reader_state_load(reader, state); + + ret = SAFE_MALLOC(sizeof(char*) * (cnt + 1)); + + UJSON_ARR_FOREACH(reader, val) { + ret[i++] = strdup(val->val_str); + } + + ret[i] = NULL; + + return (const char *const *)ret; +} + +enum fs_ids { + MKFS_OPTS, + MKFS_SIZE_OPT, + MNT_FLAGS, + TYPE, +}; + +static ujson_obj_attr fs_attrs[] = { + UJSON_OBJ_ATTR_IDX(MKFS_OPTS, "mkfs_opts", UJSON_ARR), + UJSON_OBJ_ATTR_IDX(MKFS_SIZE_OPT, "mkfs_size_opt", UJSON_STR), + UJSON_OBJ_ATTR_IDX(MNT_FLAGS, "mnt_flags", UJSON_ARR), + UJSON_OBJ_ATTR_IDX(TYPE, "type", UJSON_STR), +}; + +static ujson_obj fs_obj = { + .attrs = fs_attrs, + .attr_cnt = UJSON_ARRAY_SIZE(fs_attrs), +}; + +static int parse_mnt_flags(ujson_reader *reader, ujson_val *val) +{ + int ret = 0; + + UJSON_ARR_FOREACH(reader, val) { + if (val->type != UJSON_STR) { + ujson_err(reader, "Expected string!"); + return ret; + } + + if (!strcmp(val->val_str, "RDONLY")) + ret |= MS_RDONLY; + else if (!strcmp(val->val_str, "NOATIME")) + ret |= MS_NOATIME; + else if (!strcmp(val->val_str, "NOEXEC")) + ret |= MS_NOEXEC; + else if (!strcmp(val->val_str, "NOSUID")) + ret |= MS_NOSUID; + else + ujson_err(reader, "Invalid mount flag"); + } + + return ret; +} + +static struct tst_fs *parse_filesystems(ujson_reader *reader, ujson_val *val) +{ + unsigned int i = 0, cnt = 0; + struct tst_fs *ret; + + ujson_reader_state state = ujson_reader_state_save(reader); + + UJSON_ARR_FOREACH(reader, val) { + if (val->type != UJSON_OBJ) { + ujson_err(reader, "Expected object!"); + return NULL; + } + ujson_obj_skip(reader); + cnt++; + } + + ujson_reader_state_load(reader, state); + + ret = SAFE_MALLOC(sizeof(struct tst_fs) * (cnt + 1)); + memset(ret, 0, sizeof(*ret) * (cnt+1)); + + UJSON_ARR_FOREACH(reader, val) { + UJSON_OBJ_FOREACH_FILTER(reader, val, &fs_obj, ujson_empty_obj) { + switch ((enum fs_ids)val->idx) { + case MKFS_OPTS: + ret[i].mkfs_opts = parse_strarr(reader, val); + break; + case MKFS_SIZE_OPT: + ret[i].mkfs_size_opt = strdup(val->val_str); + break; + case MNT_FLAGS: + ret[i].mnt_flags = parse_mnt_flags(reader, val); + break; + case TYPE: + ret[i].type = strdup(val->val_str); + break; + } + + } + + i++; + } + + return ret; +} + +static struct tst_tag *parse_tags(ujson_reader *reader, ujson_val *val) +{ + unsigned int i = 0, cnt = 0; + struct tst_tag *ret; + + ujson_reader_state state = ujson_reader_state_save(reader); + + UJSON_ARR_FOREACH(reader, val) { + if (val->type != UJSON_ARR) { + ujson_err(reader, "Expected array!"); + return NULL; + } + ujson_arr_skip(reader); + cnt++; + } + + ujson_reader_state_load(reader, state); + + ret = SAFE_MALLOC(sizeof(struct tst_tag) * (cnt + 1)); + memset(&ret[cnt], 0, sizeof(ret[cnt])); + + UJSON_ARR_FOREACH(reader, val) { + char *name = NULL; + char *value = NULL; + + UJSON_ARR_FOREACH(reader, val) { + if (val->type != UJSON_STR) { + ujson_err(reader, "Expected string!"); + return NULL; + } + + if (!name) { + name = strdup(val->val_str); + } else if (!value) { + value = strdup(val->val_str); + } else { + ujson_err(reader, "Expected only two members!"); + return NULL; + } + } + + ret[i].name = name; + ret[i].value = value; + i++; + } + + return ret; +} + +static void parse_metadata(void) +{ + ujson_reader reader = UJSON_READER_INIT(metadata, metadata_used, UJSON_READER_STRICT); + char str_buf[128]; + ujson_val val = UJSON_VAL_INIT(str_buf, sizeof(str_buf)); + + UJSON_OBJ_FOREACH_FILTER(&reader, &val, &test_obj, ujson_empty_obj) { + switch ((enum test_attr_ids)val.idx) { + case ALL_FILESYSTEMS: + test.all_filesystems = val.val_bool; + break; + case DEV_MIN_SIZE: + if (val.val_int <= 0) + ujson_err(&reader, "Device size must be > 0"); + else + test.dev_min_size = val.val_int; + break; + case FILESYSTEMS: + test.filesystems = parse_filesystems(&reader, &val); + break; + case FORMAT_DEVICE: + test.format_device = val.val_bool; + break; + case MIN_CPUS: + if (val.val_int <= 0) + ujson_err(&reader, "Minimal number of cpus must be > 0"); + else + test.min_cpus = val.val_int; + break; + case MIN_MEM_AVAIL: + if (val.val_int <= 0) + ujson_err(&reader, "Minimal available memory size must be > 0"); + else + test.min_mem_avail = val.val_int; + break; + case MIN_KVER: + test.min_kver = strdup(val.val_str); + break; + case MIN_SWAP_AVAIL: + if (val.val_int <= 0) + ujson_err(&reader, "Minimal available swap size must be > 0"); + else + test.min_swap_avail = val.val_int; + break; + case MNTPOINT: + test.mntpoint = strdup(val.val_str); + break; + case MOUNT_DEVICE: + test.mount_device = val.val_bool; + break; + case NEEDS_ABI_BITS: + if (val.val_int == 32 || val.val_int == 64) + test.needs_abi_bits = val.val_int; + else + ujson_err(&reader, "ABI bits must be 32 or 64"); + break; + case NEEDS_CMDS: + test.needs_cmds = parse_strarr(&reader, &val); + break; + case NEEDS_DEVFS: + test.needs_devfs = val.val_bool; + break; + case NEEDS_DEVICE: + test.needs_device = val.val_bool; + break; + case NEEDS_DRIVERS: + test.needs_drivers = parse_strarr(&reader, &val); + break; + case NEEDS_HUGETLBFS: + test.needs_hugetlbfs = val.val_bool; + break; + case NEEDS_KCONFIGS: + test.needs_kconfigs = parse_strarr(&reader, &val); + break; + case NEEDS_ROFS: + test.needs_rofs = val.val_bool; + break; + case NEEDS_ROOT: + test.needs_root = val.val_bool; + break; + case NEEDS_TMPDIR: + test.needs_tmpdir = val.val_bool; + break; + case RESTORE_WALLCLOCK: + test.restore_wallclock = val.val_bool; + break; + case SKIP_FILESYSTEMS: + test.skip_filesystems = parse_strarr(&reader, &val); + break; + case SKIP_IN_COMPAT: + test.skip_in_compat = val.val_bool; + break; + case SKIP_IN_LOCKDOWN: + test.skip_in_lockdown = val.val_bool; + break; + case SKIP_IN_SECUREBOOT: + test.skip_in_secureboot = val.val_bool; + break; + case SUPPORTED_ARCHS: + test.supported_archs = parse_strarr(&reader, &val); + break; + case TAGS: + test.tags = parse_tags(&reader, &val); + break; + case TAINT_CHECK: + test.taint_check = val.val_bool; + break; + case TCNT: + if (val.val_int <= 0) + ujson_err(&reader, "Number of tests must be > 0"); + else + test.tcnt = val.val_int; + break; + } + } + + ujson_reader_finish(&reader); + + if (ujson_reader_err(&reader)) + tst_brk(TBROK, "Invalid metadata"); +} + +enum parser_state { + PAR_NONE, + PAR_ESC, + PAR_DOC, + PAR_ENV, +}; + +static void extract_metadata(void) +{ + FILE *f; + char line[4096]; + char path[4096]; + enum parser_state state = PAR_NONE; + + if (tst_get_path(shell_filename, path, sizeof(path)) == -1) + tst_brk(TBROK, "Failed to find %s in $PATH", shell_filename); + + f = SAFE_FOPEN(path, "r"); + + while (fgets(line, sizeof(line), f)) { + switch (state) { + case PAR_NONE: + if (!strcmp(line, "# ---\n")) + state = PAR_ESC; + break; + case PAR_ESC: + if (!strcmp(line, "# env\n")) + state = PAR_ENV; + else if (!strcmp(line, "# doc\n")) + state = PAR_DOC; + else + tst_brk(TBROK, "Unknown comment block %s", line); + break; + case PAR_ENV: + if (!strcmp(line, "# ---\n")) + state = PAR_NONE; + else + metadata_append(line + 2); + break; + case PAR_DOC: + if (!strcmp(line, "# ---\n")) + state = PAR_NONE; + break; + } + } + + fclose(f); +} + +static void prepare_test_struct(void) +{ + extract_metadata(); + + if (metadata) + parse_metadata(); + else + tst_brk(TBROK, "No metadata found!"); +} + +int main(int argc, char *argv[]) +{ + if (argc < 2) + goto help; + + shell_filename = argv[1]; + + prepare_test_struct(); + + if (test.tcnt) + test.test = run_shell_tcnt; + else + test.test_all = run_shell; + + tst_run_tcases(argc - 1, argv + 1, &test); +help: + print_help(); + return 1; +}