diff mbox series

[3/3] testcaes/lib: Add shell loader

Message ID 20240731092017.8267-4-chrubis@suse.cz
State Superseded
Headers show
Series Shell test library v3 | expand

Commit Message

Cyril Hrubis July 31, 2024, 9:20 a.m. UTC
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>
---
 include/tst_test.h                            |   2 +-
 testcases/lib/.gitignore                      |   1 +
 testcases/lib/Makefile                        |   6 +-
 testcases/lib/run_tests.sh                    |   9 +
 testcases/lib/tests/shell_loader.sh           |  15 +
 .../lib/tests/shell_loader_all_filesystems.sh |  24 ++
 .../lib/tests/shell_loader_filesystems.sh     |  30 ++
 .../tests/shell_loader_invalid_metadata.sh    |  12 +
 .../lib/tests/shell_loader_no_metadata.sh     |   8 +
 .../lib/tests/shell_loader_supported_archs.sh |   9 +
 .../lib/tests/shell_loader_wrong_metadata.sh  |  12 +
 testcases/lib/tst_env.sh                      |   4 +
 testcases/lib/tst_loader.sh                   |  11 +
 testcases/lib/tst_run_shell.c                 | 378 ++++++++++++++++++
 14 files changed, 519 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_metadata.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_wrong_metadata.sh
 create mode 100644 testcases/lib/tst_loader.sh
 create mode 100644 testcases/lib/tst_run_shell.c

Comments

Richard Palethorpe Aug. 13, 2024, 4:15 p.m. UTC | #1
Hello Cyril Hrubis <chrubis@suse.cz> writes:

> 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>
> ---
>  include/tst_test.h                            |   2 +-
>  testcases/lib/.gitignore                      |   1 +
>  testcases/lib/Makefile                        |   6 +-
>  testcases/lib/run_tests.sh                    |   9 +
>  testcases/lib/tests/shell_loader.sh           |  15 +
>  .../lib/tests/shell_loader_all_filesystems.sh |  24 ++
>  .../lib/tests/shell_loader_filesystems.sh     |  30 ++
>  .../tests/shell_loader_invalid_metadata.sh    |  12 +
>  .../lib/tests/shell_loader_no_metadata.sh     |   8 +
>  .../lib/tests/shell_loader_supported_archs.sh |   9 +
>  .../lib/tests/shell_loader_wrong_metadata.sh  |  12 +
>  testcases/lib/tst_env.sh                      |   4 +
>  testcases/lib/tst_loader.sh                   |  11 +
>  testcases/lib/tst_run_shell.c                 | 378 ++++++++++++++++++
>  14 files changed, 519 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_metadata.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_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 a334195ac..c04498937 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..9857f5f82 100755
> --- a/testcases/lib/run_tests.sh
> +++ b/testcases/lib/run_tests.sh
> @@ -9,3 +9,12 @@ 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; do
> +	echo
> +	echo "*** Running $i ***"
> +	echo
> +	$i
> +done
> diff --git a/testcases/lib/tests/shell_loader.sh b/testcases/lib/tests/shell_loader.sh
> new file mode 100755
> index 000000000..642ffe97b
> --- /dev/null
> +++ b/testcases/lib/tests/shell_loader.sh
> @@ -0,0 +1,15 @@
> +#!/bin/sh
> +#
> +# TEST = {
> +#  "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..8432b4b3d
> --- /dev/null
> +++ b/testcases/lib/tests/shell_loader_all_filesystems.sh
> @@ -0,0 +1,24 @@
> +#!/bin/sh
> +#
> +# TEST = {
> +#  "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..ede1a8fe9
> --- /dev/null
> +++ b/testcases/lib/tests/shell_loader_filesystems.sh
> @@ -0,0 +1,30 @@
> +#!/bin/sh
> +#
> +# TEST = {
> +#  "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_metadata.sh b/testcases/lib/tests/shell_loader_invalid_metadata.sh
> new file mode 100755
> index 000000000..265be6f36
> --- /dev/null
> +++ b/testcases/lib/tests/shell_loader_invalid_metadata.sh
> @@ -0,0 +1,12 @@
> +#!/bin/sh
> +#
> +# This test has wrong metadata and should not be run
> +#
> +# TEST = {
> +#  {"needs_tmpdir": 42,
> +# }
> +#
> +
> +. tst_loader.sh
> +
> +tst_res TFAIL "Shell loader should TBROK the test"
> 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..d5c6c648b
> --- /dev/null
> +++ b/testcases/lib/tests/shell_loader_supported_archs.sh
> @@ -0,0 +1,9 @@
> +#!/bin/sh
> +#
> +# TEST = {
> +#  "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_wrong_metadata.sh b/testcases/lib/tests/shell_loader_wrong_metadata.sh
> new file mode 100755
> index 000000000..f29b9308f
> --- /dev/null
> +++ b/testcases/lib/tests/shell_loader_wrong_metadata.sh
> @@ -0,0 +1,12 @@
> +#!/bin/sh
> +#
> +# This test has wrong metadata and should not be run
> +#
> +# TEST = {
> +#  "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..5ac095e44
> --- /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..96827e1cc
> --- /dev/null
> +++ b/testcases/lib/tst_run_shell.c
> @@ -0,0 +1,378 @@
> +// 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_shell(shell_filename, NULL);
> +}
> +
> +struct tst_test test = {
> +	.test_all = run_shell,
> +	.runs_script = 1,
> +};
> +
> +static void print_help(void)
> +{
> +	printf("Usage: tst_shell_loader ltp_shell_test.sh ...");
> +}
> +
> +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 += 128;

This seems like a very small amount to bother allocating.

> +		metadata = SAFE_REALLOC(metadata, metadata_size);
> +	}
> +
> +	strcpy(metadata + metadata_used, line);
> +	metadata_used += linelen;
> +}
> +
> +static ujson_obj_attr test_attrs[] = {
> +	UJSON_OBJ_ATTR("all_filesystems", UJSON_BOOL),
> +	UJSON_OBJ_ATTR("dev_min_size", UJSON_INT),
> +	UJSON_OBJ_ATTR("filesystems", UJSON_ARR),
> +	UJSON_OBJ_ATTR("format_device", UJSON_BOOL),
> +	UJSON_OBJ_ATTR("min_cpus", UJSON_INT),
> +	UJSON_OBJ_ATTR("min_mem_avail", UJSON_INT),
> +	UJSON_OBJ_ATTR("min_kver", UJSON_STR),
> +	UJSON_OBJ_ATTR("min_swap_avail", UJSON_INT),
> +	UJSON_OBJ_ATTR("mntpoint", UJSON_STR),
> +	UJSON_OBJ_ATTR("mount_device", UJSON_BOOL),
> +	UJSON_OBJ_ATTR("needs_abi_bits", UJSON_INT),
> +	UJSON_OBJ_ATTR("needs_devfs", UJSON_BOOL),
> +	UJSON_OBJ_ATTR("needs_device", UJSON_BOOL),
> +	UJSON_OBJ_ATTR("needs_hugetlbfs", UJSON_BOOL),
> +	UJSON_OBJ_ATTR("needs_rofs", UJSON_BOOL),
> +	UJSON_OBJ_ATTR("needs_root", UJSON_BOOL),
> +	UJSON_OBJ_ATTR("needs_tmpdir", UJSON_BOOL),
> +	UJSON_OBJ_ATTR("restore_wallclock", UJSON_BOOL),
> +	UJSON_OBJ_ATTR("skip_filesystems", UJSON_ARR),
> +	UJSON_OBJ_ATTR("skip_in_compat", UJSON_BOOL),
> +	UJSON_OBJ_ATTR("skip_in_lockdown", UJSON_BOOL),
> +	UJSON_OBJ_ATTR("skip_in_secureboot", UJSON_BOOL),
> +	UJSON_OBJ_ATTR("supported_archs", UJSON_ARR),
> +};
> +
> +static ujson_obj test_obj = {
> +	.attrs = test_attrs,
> +	.attr_cnt = UJSON_ARRAY_SIZE(test_attrs),
> +};
> +
> +/* Must match the order of test_attrs. */

You could use the index syntax like [ALL_FILESYSTEMS] = UJASON_OBJ_ATTR...
IIRC.

Then the order can't be messed up

> +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_DEVFS,
> +	NEEDS_DEVICE,
> +	NEEDS_HUGETLBFS,
> +	NEEDS_ROFS,
> +	NEEDS_ROOT,
> +	NEEDS_TMPDIR,
> +	RESTORE_WALLCLOCK,
> +	SKIP_FILESYSTEMS,
> +	SKIP_IN_COMPAT,
> +	SKIP_IN_LOCKDOWN,
> +	SKIP_IN_SECUREBOOT,
> +	SUPPORTED_ARCHS,
> +};
> +
> +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;
> +}
> +
> +static ujson_obj_attr fs_attrs[] = {
> +	UJSON_OBJ_ATTR("mkfs_opts", UJSON_ARR),
> +	UJSON_OBJ_ATTR("mkfs_size_opt", UJSON_STR),
> +	UJSON_OBJ_ATTR("mnt_flags", UJSON_ARR),
> +	UJSON_OBJ_ATTR("type", UJSON_STR),
> +};
> +
> +static ujson_obj fs_obj = {
> +	.attrs = fs_attrs,
> +	.attr_cnt = UJSON_ARRAY_SIZE(fs_attrs),
> +};
> +
> +/* Must match the order of fs_attrs. */
> +enum fs_ids {
> +	MKFS_OPTS,
> +	MKFS_SIZE_OPT,
> +	MNT_FLAGS,
> +	TYPE,
> +};
> +
> +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;
> +}
> +
> +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 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_DEVFS:
> +			test.needs_devfs = val.val_bool;
> +		break;
> +		case NEEDS_DEVICE:
> +			test.needs_device = val.val_bool;
> +		break;
> +		case NEEDS_HUGETLBFS:
> +			test.needs_hugetlbfs = val.val_bool;
> +		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;
> +		}
> +	}
> +
> +	ujson_reader_finish(&reader);
> +
> +	if (ujson_reader_err(&reader))
> +		tst_brk(TBROK, "Invalid metadata");
> +}
> +
> +static void extract_metadata(void)
> +{
> +	FILE *f;
> +	char line[4096];
> +	char path[4096];
> +	int in_json = 0;
> +
> +	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)) {
> +		if (in_json)
> +			metadata_append(line + 2);
> +
> +		if (in_json) {
> +			if (!strcmp(line, "# }\n"))
> +				in_json = 0;
> +		} else {
> +			if (!strcmp(line, "# TEST = {\n")) {
> +				metadata_append("{\n");
> +				in_json = 1;
> +			}
> +		}

This is maybe a little bit too rigid, even if you want this exact
formatting to be the only valid one. People will get frustrated when
parsing fails due to whitespace and don't see a clear indication why
it failed.

Alternativey you could adopt the standard markdown meta-data format
where the meta-data is enclosed with three dashes like

---
"meta": "data"
---

at the top of the file. I suppose you could change it to have comments
if you want the script to run outside LTP's runner.

In any case using three dots removes any whitespace between tokens which
is harder to trim.

The whole patchset looks good though.

Reviewed-by: Richard Palethorpe <io@richiejp.com>
Cyril Hrubis Aug. 26, 2024, 3:10 p.m. UTC | #2
Hi!
> > +static void metadata_append(const char *line)
> > +{
> > +	size_t linelen = strlen(line);
> > +
> > +	if (metadata_size - metadata_used < linelen + 1) {
> > +		metadata_size += 128;
> 
> This seems like a very small amount to bother allocating.

That is what I have left there after the testing phase where I wanted to
trigger the reallocation. I will change this to 4k.

> > +		metadata = SAFE_REALLOC(metadata, metadata_size);
> > +	}
> > +
> > +	strcpy(metadata + metadata_used, line);
> > +	metadata_used += linelen;
> > +}
> > +
> > +static ujson_obj_attr test_attrs[] = {
> > +	UJSON_OBJ_ATTR("all_filesystems", UJSON_BOOL),
> > +	UJSON_OBJ_ATTR("dev_min_size", UJSON_INT),
> > +	UJSON_OBJ_ATTR("filesystems", UJSON_ARR),
> > +	UJSON_OBJ_ATTR("format_device", UJSON_BOOL),
> > +	UJSON_OBJ_ATTR("min_cpus", UJSON_INT),
> > +	UJSON_OBJ_ATTR("min_mem_avail", UJSON_INT),
> > +	UJSON_OBJ_ATTR("min_kver", UJSON_STR),
> > +	UJSON_OBJ_ATTR("min_swap_avail", UJSON_INT),
> > +	UJSON_OBJ_ATTR("mntpoint", UJSON_STR),
> > +	UJSON_OBJ_ATTR("mount_device", UJSON_BOOL),
> > +	UJSON_OBJ_ATTR("needs_abi_bits", UJSON_INT),
> > +	UJSON_OBJ_ATTR("needs_devfs", UJSON_BOOL),
> > +	UJSON_OBJ_ATTR("needs_device", UJSON_BOOL),
> > +	UJSON_OBJ_ATTR("needs_hugetlbfs", UJSON_BOOL),
> > +	UJSON_OBJ_ATTR("needs_rofs", UJSON_BOOL),
> > +	UJSON_OBJ_ATTR("needs_root", UJSON_BOOL),
> > +	UJSON_OBJ_ATTR("needs_tmpdir", UJSON_BOOL),
> > +	UJSON_OBJ_ATTR("restore_wallclock", UJSON_BOOL),
> > +	UJSON_OBJ_ATTR("skip_filesystems", UJSON_ARR),
> > +	UJSON_OBJ_ATTR("skip_in_compat", UJSON_BOOL),
> > +	UJSON_OBJ_ATTR("skip_in_lockdown", UJSON_BOOL),
> > +	UJSON_OBJ_ATTR("skip_in_secureboot", UJSON_BOOL),
> > +	UJSON_OBJ_ATTR("supported_archs", UJSON_ARR),
> > +};
> > +
> > +static ujson_obj test_obj = {
> > +	.attrs = test_attrs,
> > +	.attr_cnt = UJSON_ARRAY_SIZE(test_attrs),
> > +};
> > +
> > +/* Must match the order of test_attrs. */
> 
> You could use the index syntax like [ALL_FILESYSTEMS] = UJASON_OBJ_ATTR...
> IIRC.
> 
> Then the order can't be messed up

Good idea, will do. Maybe it makes sense to embedded this into the
UJSON_OBJ_ATTR() macro as a second parameter so that we have better
syntax too.

> > +	while (fgets(line, sizeof(line), f)) {
> > +		if (in_json)
> > +			metadata_append(line + 2);
> > +
> > +		if (in_json) {
> > +			if (!strcmp(line, "# }\n"))
> > +				in_json = 0;
> > +		} else {
> > +			if (!strcmp(line, "# TEST = {\n")) {
> > +				metadata_append("{\n");
> > +				in_json = 1;
> > +			}
> > +		}
> 
> This is maybe a little bit too rigid, even if you want this exact
> formatting to be the only valid one. People will get frustrated when
> parsing fails due to whitespace and don't see a clear indication why
> it failed.
> 
> Alternativey you could adopt the standard markdown meta-data format
> where the meta-data is enclosed with three dashes like
> 
> ---
> "meta": "data"
> ---

That may be better, but we would need some markers for the asciidoc
documentation as well, so we need some kind of identifier too.

So maybe:

# ---
# doc
#
# [Description]
#
# This test does bla bla.
# ---
#
# ---
# env
#
# {
#  "needs_root": 1,
# }
# ---

Getting the delimiters right when embedding data is
hard...

> at the top of the file. I suppose you could change it to have comments
> if you want the script to run outside LTP's runner.

We actually need it to be in the comments, because we run the script by
bash first, which bootstraps the enviroment from the shell script we
source first, then reexecutes the script in the right environment.

> In any case using three dots removes any whitespace between tokens which
> is harder to trim.
> 
> The whole patchset looks good though.

Thanks a lot for the review, I will work on the small bits and send v3.
diff mbox series

Patch

diff --git a/include/tst_test.h b/include/tst_test.h
index a334195ac..c04498937 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..9857f5f82 100755
--- a/testcases/lib/run_tests.sh
+++ b/testcases/lib/run_tests.sh
@@ -9,3 +9,12 @@  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; do
+	echo
+	echo "*** Running $i ***"
+	echo
+	$i
+done
diff --git a/testcases/lib/tests/shell_loader.sh b/testcases/lib/tests/shell_loader.sh
new file mode 100755
index 000000000..642ffe97b
--- /dev/null
+++ b/testcases/lib/tests/shell_loader.sh
@@ -0,0 +1,15 @@ 
+#!/bin/sh
+#
+# TEST = {
+#  "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..8432b4b3d
--- /dev/null
+++ b/testcases/lib/tests/shell_loader_all_filesystems.sh
@@ -0,0 +1,24 @@ 
+#!/bin/sh
+#
+# TEST = {
+#  "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..ede1a8fe9
--- /dev/null
+++ b/testcases/lib/tests/shell_loader_filesystems.sh
@@ -0,0 +1,30 @@ 
+#!/bin/sh
+#
+# TEST = {
+#  "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_metadata.sh b/testcases/lib/tests/shell_loader_invalid_metadata.sh
new file mode 100755
index 000000000..265be6f36
--- /dev/null
+++ b/testcases/lib/tests/shell_loader_invalid_metadata.sh
@@ -0,0 +1,12 @@ 
+#!/bin/sh
+#
+# This test has wrong metadata and should not be run
+#
+# TEST = {
+#  {"needs_tmpdir": 42,
+# }
+#
+
+. tst_loader.sh
+
+tst_res TFAIL "Shell loader should TBROK the test"
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..d5c6c648b
--- /dev/null
+++ b/testcases/lib/tests/shell_loader_supported_archs.sh
@@ -0,0 +1,9 @@ 
+#!/bin/sh
+#
+# TEST = {
+#  "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_wrong_metadata.sh b/testcases/lib/tests/shell_loader_wrong_metadata.sh
new file mode 100755
index 000000000..f29b9308f
--- /dev/null
+++ b/testcases/lib/tests/shell_loader_wrong_metadata.sh
@@ -0,0 +1,12 @@ 
+#!/bin/sh
+#
+# This test has wrong metadata and should not be run
+#
+# TEST = {
+#  "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..5ac095e44
--- /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..96827e1cc
--- /dev/null
+++ b/testcases/lib/tst_run_shell.c
@@ -0,0 +1,378 @@ 
+// 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_shell(shell_filename, NULL);
+}
+
+struct tst_test test = {
+	.test_all = run_shell,
+	.runs_script = 1,
+};
+
+static void print_help(void)
+{
+	printf("Usage: tst_shell_loader ltp_shell_test.sh ...");
+}
+
+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 += 128;
+		metadata = SAFE_REALLOC(metadata, metadata_size);
+	}
+
+	strcpy(metadata + metadata_used, line);
+	metadata_used += linelen;
+}
+
+static ujson_obj_attr test_attrs[] = {
+	UJSON_OBJ_ATTR("all_filesystems", UJSON_BOOL),
+	UJSON_OBJ_ATTR("dev_min_size", UJSON_INT),
+	UJSON_OBJ_ATTR("filesystems", UJSON_ARR),
+	UJSON_OBJ_ATTR("format_device", UJSON_BOOL),
+	UJSON_OBJ_ATTR("min_cpus", UJSON_INT),
+	UJSON_OBJ_ATTR("min_mem_avail", UJSON_INT),
+	UJSON_OBJ_ATTR("min_kver", UJSON_STR),
+	UJSON_OBJ_ATTR("min_swap_avail", UJSON_INT),
+	UJSON_OBJ_ATTR("mntpoint", UJSON_STR),
+	UJSON_OBJ_ATTR("mount_device", UJSON_BOOL),
+	UJSON_OBJ_ATTR("needs_abi_bits", UJSON_INT),
+	UJSON_OBJ_ATTR("needs_devfs", UJSON_BOOL),
+	UJSON_OBJ_ATTR("needs_device", UJSON_BOOL),
+	UJSON_OBJ_ATTR("needs_hugetlbfs", UJSON_BOOL),
+	UJSON_OBJ_ATTR("needs_rofs", UJSON_BOOL),
+	UJSON_OBJ_ATTR("needs_root", UJSON_BOOL),
+	UJSON_OBJ_ATTR("needs_tmpdir", UJSON_BOOL),
+	UJSON_OBJ_ATTR("restore_wallclock", UJSON_BOOL),
+	UJSON_OBJ_ATTR("skip_filesystems", UJSON_ARR),
+	UJSON_OBJ_ATTR("skip_in_compat", UJSON_BOOL),
+	UJSON_OBJ_ATTR("skip_in_lockdown", UJSON_BOOL),
+	UJSON_OBJ_ATTR("skip_in_secureboot", UJSON_BOOL),
+	UJSON_OBJ_ATTR("supported_archs", UJSON_ARR),
+};
+
+static ujson_obj test_obj = {
+	.attrs = test_attrs,
+	.attr_cnt = UJSON_ARRAY_SIZE(test_attrs),
+};
+
+/* Must match the order of test_attrs. */
+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_DEVFS,
+	NEEDS_DEVICE,
+	NEEDS_HUGETLBFS,
+	NEEDS_ROFS,
+	NEEDS_ROOT,
+	NEEDS_TMPDIR,
+	RESTORE_WALLCLOCK,
+	SKIP_FILESYSTEMS,
+	SKIP_IN_COMPAT,
+	SKIP_IN_LOCKDOWN,
+	SKIP_IN_SECUREBOOT,
+	SUPPORTED_ARCHS,
+};
+
+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;
+}
+
+static ujson_obj_attr fs_attrs[] = {
+	UJSON_OBJ_ATTR("mkfs_opts", UJSON_ARR),
+	UJSON_OBJ_ATTR("mkfs_size_opt", UJSON_STR),
+	UJSON_OBJ_ATTR("mnt_flags", UJSON_ARR),
+	UJSON_OBJ_ATTR("type", UJSON_STR),
+};
+
+static ujson_obj fs_obj = {
+	.attrs = fs_attrs,
+	.attr_cnt = UJSON_ARRAY_SIZE(fs_attrs),
+};
+
+/* Must match the order of fs_attrs. */
+enum fs_ids {
+	MKFS_OPTS,
+	MKFS_SIZE_OPT,
+	MNT_FLAGS,
+	TYPE,
+};
+
+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;
+}
+
+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 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_DEVFS:
+			test.needs_devfs = val.val_bool;
+		break;
+		case NEEDS_DEVICE:
+			test.needs_device = val.val_bool;
+		break;
+		case NEEDS_HUGETLBFS:
+			test.needs_hugetlbfs = val.val_bool;
+		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;
+		}
+	}
+
+	ujson_reader_finish(&reader);
+
+	if (ujson_reader_err(&reader))
+		tst_brk(TBROK, "Invalid metadata");
+}
+
+static void extract_metadata(void)
+{
+	FILE *f;
+	char line[4096];
+	char path[4096];
+	int in_json = 0;
+
+	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)) {
+		if (in_json)
+			metadata_append(line + 2);
+
+		if (in_json) {
+			if (!strcmp(line, "# }\n"))
+				in_json = 0;
+		} else {
+			if (!strcmp(line, "# TEST = {\n")) {
+				metadata_append("{\n");
+				in_json = 1;
+			}
+		}
+	}
+
+	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();
+
+	tst_run_tcases(argc - 1, argv + 1, &test);
+help:
+	print_help();
+	return 1;
+}