diff mbox series

[v3,2/4] Add support for mixing C and shell code

Message ID 20240827120237.25805-3-chrubis@suse.cz
State Accepted
Headers show
Series Shell test library v3 | expand

Commit Message

Cyril Hrubis Aug. 27, 2024, 12:02 p.m. UTC
This is a proof of a concept of a seamless C and shell integration. The
idea is that with this you can mix shell and C code as much as as you
wish to get the best of the two worlds.

Signed-off-by: Cyril Hrubis <chrubis@suse.cz>
Reviewed-by: Richard Palethorpe <io@richiejp.com>
---
 include/tst_test.h                           | 37 +++++++++++++
 lib/tst_test.c                               | 51 +++++++++++++++++
 testcases/lib/.gitignore                     |  1 +
 testcases/lib/Makefile                       |  4 +-
 testcases/lib/run_tests.sh                   | 11 ++++
 testcases/lib/tests/.gitignore               |  6 ++
 testcases/lib/tests/Makefile                 | 11 ++++
 testcases/lib/tests/shell_test01.c           | 17 ++++++
 testcases/lib/tests/shell_test02.c           | 18 ++++++
 testcases/lib/tests/shell_test03.c           | 25 +++++++++
 testcases/lib/tests/shell_test04.c           | 18 ++++++
 testcases/lib/tests/shell_test05.c           | 27 +++++++++
 testcases/lib/tests/shell_test06.c           | 16 ++++++
 testcases/lib/tests/shell_test_brk.sh        |  6 ++
 testcases/lib/tests/shell_test_check_argv.sh | 23 ++++++++
 testcases/lib/tests/shell_test_checkpoint.sh |  7 +++
 testcases/lib/tests/shell_test_pass.sh       |  6 ++
 testcases/lib/tst_env.sh                     | 21 +++++++
 testcases/lib/tst_res_.c                     | 58 ++++++++++++++++++++
 19 files changed, 361 insertions(+), 2 deletions(-)
 create mode 100755 testcases/lib/run_tests.sh
 create mode 100644 testcases/lib/tests/.gitignore
 create mode 100644 testcases/lib/tests/Makefile
 create mode 100644 testcases/lib/tests/shell_test01.c
 create mode 100644 testcases/lib/tests/shell_test02.c
 create mode 100644 testcases/lib/tests/shell_test03.c
 create mode 100644 testcases/lib/tests/shell_test04.c
 create mode 100644 testcases/lib/tests/shell_test05.c
 create mode 100644 testcases/lib/tests/shell_test06.c
 create mode 100755 testcases/lib/tests/shell_test_brk.sh
 create mode 100755 testcases/lib/tests/shell_test_check_argv.sh
 create mode 100755 testcases/lib/tests/shell_test_checkpoint.sh
 create mode 100755 testcases/lib/tests/shell_test_pass.sh
 create mode 100644 testcases/lib/tst_env.sh
 create mode 100644 testcases/lib/tst_res_.c

Comments

Andrea Cervesato Aug. 30, 2024, 12:40 p.m. UTC | #1
Hi!

I like this minimal approach.

Reviewed-by: Andrea Cervesato <andrea.cervesato@suse.com>

On 8/27/24 14:02, Cyril Hrubis wrote:
> This is a proof of a concept of a seamless C and shell integration. The
> idea is that with this you can mix shell and C code as much as as you
> wish to get the best of the two worlds.
>
> Signed-off-by: Cyril Hrubis <chrubis@suse.cz>
> Reviewed-by: Richard Palethorpe <io@richiejp.com>
> ---
>   include/tst_test.h                           | 37 +++++++++++++
>   lib/tst_test.c                               | 51 +++++++++++++++++
>   testcases/lib/.gitignore                     |  1 +
>   testcases/lib/Makefile                       |  4 +-
>   testcases/lib/run_tests.sh                   | 11 ++++
>   testcases/lib/tests/.gitignore               |  6 ++
>   testcases/lib/tests/Makefile                 | 11 ++++
>   testcases/lib/tests/shell_test01.c           | 17 ++++++
>   testcases/lib/tests/shell_test02.c           | 18 ++++++
>   testcases/lib/tests/shell_test03.c           | 25 +++++++++
>   testcases/lib/tests/shell_test04.c           | 18 ++++++
>   testcases/lib/tests/shell_test05.c           | 27 +++++++++
>   testcases/lib/tests/shell_test06.c           | 16 ++++++
>   testcases/lib/tests/shell_test_brk.sh        |  6 ++
>   testcases/lib/tests/shell_test_check_argv.sh | 23 ++++++++
>   testcases/lib/tests/shell_test_checkpoint.sh |  7 +++
>   testcases/lib/tests/shell_test_pass.sh       |  6 ++
>   testcases/lib/tst_env.sh                     | 21 +++++++
>   testcases/lib/tst_res_.c                     | 58 ++++++++++++++++++++
>   19 files changed, 361 insertions(+), 2 deletions(-)
>   create mode 100755 testcases/lib/run_tests.sh
>   create mode 100644 testcases/lib/tests/.gitignore
>   create mode 100644 testcases/lib/tests/Makefile
>   create mode 100644 testcases/lib/tests/shell_test01.c
>   create mode 100644 testcases/lib/tests/shell_test02.c
>   create mode 100644 testcases/lib/tests/shell_test03.c
>   create mode 100644 testcases/lib/tests/shell_test04.c
>   create mode 100644 testcases/lib/tests/shell_test05.c
>   create mode 100644 testcases/lib/tests/shell_test06.c
>   create mode 100755 testcases/lib/tests/shell_test_brk.sh
>   create mode 100755 testcases/lib/tests/shell_test_check_argv.sh
>   create mode 100755 testcases/lib/tests/shell_test_checkpoint.sh
>   create mode 100755 testcases/lib/tests/shell_test_pass.sh
>   create mode 100644 testcases/lib/tst_env.sh
>   create mode 100644 testcases/lib/tst_res_.c
>
> diff --git a/include/tst_test.h b/include/tst_test.h
> index afc6a5714..9871676a5 100644
> --- a/include/tst_test.h
> +++ b/include/tst_test.h
> @@ -331,6 +331,8 @@ struct tst_fs {
>    * @child_needs_reinit: Has to be set if the test needs to call tst_reinit()
>    *                      from a process started by exec().
>    *
> + * @runs_script: Implies child_needs_reinit and forks_child at the moment.
> + *
>    * @needs_devfs: If set the devfs is mounted at tst_test.mntpoint. This is
>    *               needed for tests that need to create device files since tmpfs
>    *               at /tmp is usually mounted with 'nodev' option.
> @@ -521,6 +523,7 @@ struct tst_fs {
>   	unsigned int mount_device:1;
>   	unsigned int needs_rofs:1;
>   	unsigned int child_needs_reinit:1;
> +	unsigned int runs_script:1;
>   	unsigned int needs_devfs:1;
>   	unsigned int restore_wallclock:1;
>   
> @@ -529,6 +532,8 @@ struct tst_fs {
>   	unsigned int skip_in_lockdown:1;
>   	unsigned int skip_in_secureboot:1;
>   	unsigned int skip_in_compat:1;
> +
> +
>   	int needs_abi_bits;
>   
>   	unsigned int needs_hugetlbfs:1;
> @@ -616,6 +621,38 @@ void tst_run_tcases(int argc, char *argv[], struct tst_test *self)
>    */
>   void tst_reinit(void);
>   
> +/**
> + * tst_run_script() - Prepare the environment and execute a (shell) script.
> + *
> + * @script_name: A filename of the script.
> + * @params: A NULL terminated array of (shell) script parameters, pass NULL if
> + *          none are needed. This what is passed starting from argv[1].
> + *
> + * The (shell) script is executed with LTP_IPC_PATH in environment so that the
> + * binary helpers such as tst_res_ or tst_checkpoint work properly when executed
> + * from the script. This also means that the tst_test.runs_script flag needs to
> + * be set.
> + *
> + * A shell script has to source the tst_env.sh shell script at the start and
> + * after that it's free to use tst_res in the same way C code would use.
> + *
> + * Example shell script that reports success::
> + *
> + *   #!/bin/sh
> + *   . tst_env.sh
> + *
> + *   tst_res TPASS "Example test works"
> + *
> + * The call returns a pid in a case that you want to examine the return value
> + * of the script yourself. If you do not need to check the return value
> + * yourself you can use tst_reap_children() to wait for the completion. Or let
> + * the test library collect the child automatically, just be wary that the
> + * script and the test both runs concurently at the same time in this case.
> + *
> + * Return: A pid of the (shell) script process.
> + */
> +int tst_run_script(const char *script_name, char *const params[]);
> +
>   unsigned int tst_multiply_timeout(unsigned int timeout);
>   
>   /*
> diff --git a/lib/tst_test.c b/lib/tst_test.c
> index 201b81e14..918bee2a1 100644
> --- a/lib/tst_test.c
> +++ b/lib/tst_test.c
> @@ -4,6 +4,8 @@
>    * Copyright (c) Linux Test Project, 2016-2024
>    */
>   
> +#define _GNU_SOURCE
> +
>   #include <limits.h>
>   #include <stdio.h>
>   #include <stdarg.h>
> @@ -174,6 +176,50 @@ void tst_reinit(void)
>   	SAFE_CLOSE(fd);
>   }
>   
> +extern char **environ;
> +
> +static unsigned int params_array_len(char *const array[])
> +{
> +	unsigned int ret = 0;
> +
> +	if (!array)
> +		return 0;
> +
> +	while (*(array++))
> +		ret++;
> +
> +	return ret;
> +}
> +
> +int tst_run_script(const char *script_name, char *const params[])
> +{
> +	int pid;
> +	unsigned int i, params_len = params_array_len(params);
> +	char *argv[params_len + 2];
> +
> +	if (!tst_test->runs_script)
> +		tst_brk(TBROK, "runs_script flag must be set!");
> +
> +	argv[0] = (char*)script_name;
> +
> +	if (params) {
> +		for (i = 0; i < params_len; i++)
> +			argv[i+1] = params[i];
> +	}
> +
> +	argv[params_len+1] = NULL;
> +
> +	pid = SAFE_FORK();
> +	if (pid)
> +		return pid;
> +
> +	execvpe(script_name, argv, environ);
> +
> +	tst_brk(TBROK | TERRNO, "execvpe(%s, ...) failed!", script_name);
> +
> +	return -1;
> +}
> +
>   static void update_results(int ttype)
>   {
>   	if (!results)
> @@ -1226,6 +1272,11 @@ static void do_setup(int argc, char *argv[])
>   		tdebug = 1;
>   	}
>   
> +	if (tst_test->runs_script) {
> +		tst_test->child_needs_reinit = 1;
> +		tst_test->forks_child = 1;
> +	}
> +
>   	if (tst_test->needs_kconfigs && tst_kconfig_check(tst_test->needs_kconfigs))
>   		tst_brk(TCONF, "Aborting due to unsuitable kernel config, see above!");
>   
> diff --git a/testcases/lib/.gitignore b/testcases/lib/.gitignore
> index e8afd06f3..d0dacf62a 100644
> --- a/testcases/lib/.gitignore
> +++ b/testcases/lib/.gitignore
> @@ -23,3 +23,4 @@
>   /tst_sleep
>   /tst_supported_fs
>   /tst_timeout_kill
> +/tst_res_
> diff --git a/testcases/lib/Makefile b/testcases/lib/Makefile
> index 990b46089..928d76d62 100644
> --- a/testcases/lib/Makefile
> +++ b/testcases/lib/Makefile
> @@ -13,6 +13,6 @@ 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_ns_ifmove tst_lockdown_enabled tst_secureboot_enabled tst_res_
>   
> -include $(top_srcdir)/include/mk/generic_leaf_target.mk
> +include $(top_srcdir)/include/mk/generic_trunk_target.mk
> diff --git a/testcases/lib/run_tests.sh b/testcases/lib/run_tests.sh
> new file mode 100755
> index 000000000..60e7d1bcf
> --- /dev/null
> +++ b/testcases/lib/run_tests.sh
> @@ -0,0 +1,11 @@
> +#!/bin/sh
> +
> +testdir=$(realpath $(dirname $0))
> +export PATH=$PATH:$testdir:$testdir/tests/
> +
> +for i in `seq -w 01 06`; do
> +	echo
> +	echo "*** Running shell_test$i ***"
> +	echo
> +	./tests/shell_test$i
> +done
> diff --git a/testcases/lib/tests/.gitignore b/testcases/lib/tests/.gitignore
> new file mode 100644
> index 000000000..da967c4d6
> --- /dev/null
> +++ b/testcases/lib/tests/.gitignore
> @@ -0,0 +1,6 @@
> +shell_test01
> +shell_test02
> +shell_test03
> +shell_test04
> +shell_test05
> +shell_test06
> diff --git a/testcases/lib/tests/Makefile b/testcases/lib/tests/Makefile
> new file mode 100644
> index 000000000..5a5cf5310
> --- /dev/null
> +++ b/testcases/lib/tests/Makefile
> @@ -0,0 +1,11 @@
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +# Copyright (C) 2009, Cisco Systems Inc.
> +# Ngie Cooper, August 2009
> +
> +top_srcdir		?= ../../..
> +
> +include $(top_srcdir)/include/mk/testcases.mk
> +
> +INSTALL_TARGETS=
> +
> +include $(top_srcdir)/include/mk/generic_leaf_target.mk
> diff --git a/testcases/lib/tests/shell_test01.c b/testcases/lib/tests/shell_test01.c
> new file mode 100644
> index 000000000..b9f07308e
> --- /dev/null
> +++ b/testcases/lib/tests/shell_test01.c
> @@ -0,0 +1,17 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Shell test example.
> + */
> +
> +#include "tst_test.h"
> +
> +static void run_test(void)
> +{
> +	tst_run_script("shell_test_pass.sh", NULL);
> +	tst_res(TINFO, "C test exits now");
> +}
> +
> +static struct tst_test test = {
> +	.runs_script = 1,
> +	.test_all = run_test,
> +};
> diff --git a/testcases/lib/tests/shell_test02.c b/testcases/lib/tests/shell_test02.c
> new file mode 100644
> index 000000000..087055794
> --- /dev/null
> +++ b/testcases/lib/tests/shell_test02.c
> @@ -0,0 +1,18 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Shell test example.
> + */
> +
> +#include "tst_test.h"
> +
> +static void run_test(void)
> +{
> +	tst_run_script("shell_test_pass.sh", NULL);
> +	tst_reap_children();
> +	tst_res(TINFO, "Shell test has finished at this point!");
> +}
> +
> +static struct tst_test test = {
> +	.runs_script = 1,
> +	.test_all = run_test,
> +};
> diff --git a/testcases/lib/tests/shell_test03.c b/testcases/lib/tests/shell_test03.c
> new file mode 100644
> index 000000000..61436891e
> --- /dev/null
> +++ b/testcases/lib/tests/shell_test03.c
> @@ -0,0 +1,25 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Shell test example.
> + */
> +
> +#include <sys/wait.h>
> +#include "tst_test.h"
> +
> +static void run_test(void)
> +{
> +	int pid, status;
> +
> +	pid = tst_run_script("shell_test_pass.sh", NULL);
> +
> +	tst_res(TINFO, "Waiting for the pid %i", pid);
> +
> +	waitpid(pid, &status, 0);
> +
> +	tst_res(TINFO, "Shell test has %s", tst_strstatus(status));
> +}
> +
> +static struct tst_test test = {
> +	.runs_script = 1,
> +	.test_all = run_test,
> +};
> diff --git a/testcases/lib/tests/shell_test04.c b/testcases/lib/tests/shell_test04.c
> new file mode 100644
> index 000000000..a32dd1e9f
> --- /dev/null
> +++ b/testcases/lib/tests/shell_test04.c
> @@ -0,0 +1,18 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Shell test example.
> + */
> +
> +#include "tst_test.h"
> +
> +static void run_test(void)
> +{
> +	char *const params[] = {"param1", "param2", NULL};
> +
> +	tst_run_script("shell_test_check_argv.sh", params);
> +}
> +
> +static struct tst_test test = {
> +	.runs_script = 1,
> +	.test_all = run_test,
> +};
> diff --git a/testcases/lib/tests/shell_test05.c b/testcases/lib/tests/shell_test05.c
> new file mode 100644
> index 000000000..771af8fc3
> --- /dev/null
> +++ b/testcases/lib/tests/shell_test05.c
> @@ -0,0 +1,27 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Shell test example.
> + */
> +
> +#include "tst_test.h"
> +
> +static void run_test(void)
> +{
> +	int pid;
> +
> +	pid = tst_run_script("shell_test_checkpoint.sh", NULL);
> +
> +	tst_res(TINFO, "Waiting for shell to sleep on checkpoint!");
> +
> +	TST_PROCESS_STATE_WAIT(pid, 'S', 10000);
> +
> +	tst_res(TINFO, "Waking shell child!");
> +
> +	TST_CHECKPOINT_WAKE(0);
> +}
> +
> +static struct tst_test test = {
> +	.runs_script = 1,
> +	.needs_checkpoints = 1,
> +	.test_all = run_test,
> +};
> diff --git a/testcases/lib/tests/shell_test06.c b/testcases/lib/tests/shell_test06.c
> new file mode 100644
> index 000000000..89d66bab0
> --- /dev/null
> +++ b/testcases/lib/tests/shell_test06.c
> @@ -0,0 +1,16 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Shell test example.
> + */
> +
> +#include "tst_test.h"
> +
> +static void run_test(void)
> +{
> +	tst_run_script("shell_test_brk.sh", NULL);
> +}
> +
> +static struct tst_test test = {
> +	.runs_script = 1,
> +	.test_all = run_test,
> +};
> diff --git a/testcases/lib/tests/shell_test_brk.sh b/testcases/lib/tests/shell_test_brk.sh
> new file mode 100755
> index 000000000..f266dc3fe
> --- /dev/null
> +++ b/testcases/lib/tests/shell_test_brk.sh
> @@ -0,0 +1,6 @@
> +#!/bin/sh
> +
> +. tst_env.sh
> +
> +tst_brk TCONF "This exits test and the next message should not be reached"
> +tst_res TFAIL "If you see this the test failed"
> diff --git a/testcases/lib/tests/shell_test_check_argv.sh b/testcases/lib/tests/shell_test_check_argv.sh
> new file mode 100755
> index 000000000..ce357027d
> --- /dev/null
> +++ b/testcases/lib/tests/shell_test_check_argv.sh
> @@ -0,0 +1,23 @@
> +#!/bin/sh
> +
> +. tst_env.sh
> +
> +tst_res TINFO "argv = $@"
> +
> +if [ $# -ne 2 ]; then
> +	tst_res TFAIL "Wrong number of parameters got $# expected 2"
> +else
> +	tst_res TPASS "Got 2 parameters"
> +fi
> +
> +if [ "$1" != "param1" ]; then
> +	tst_res TFAIL "First parameter is $1 expected param1"
> +else
> +	tst_res TPASS "First parameter is $1"
> +fi
> +
> +if [ "$2" != "param2" ]; then
> +	tst_res TFAIL "Second parameter is $2 expected param2"
> +else
> +	tst_res TPASS "Second parameter is $2"
> +fi
> diff --git a/testcases/lib/tests/shell_test_checkpoint.sh b/testcases/lib/tests/shell_test_checkpoint.sh
> new file mode 100755
> index 000000000..0ceb7cf66
> --- /dev/null
> +++ b/testcases/lib/tests/shell_test_checkpoint.sh
> @@ -0,0 +1,7 @@
> +#!/bin/sh
> +
> +. tst_env.sh
> +
> +tst_res TINFO "Waiting for a checkpoint 0"
> +tst_checkpoint wait 10000 0
> +tst_res TPASS "Continuing after checkpoint"
> diff --git a/testcases/lib/tests/shell_test_pass.sh b/testcases/lib/tests/shell_test_pass.sh
> new file mode 100755
> index 000000000..fd0684eb2
> --- /dev/null
> +++ b/testcases/lib/tests/shell_test_pass.sh
> @@ -0,0 +1,6 @@
> +#!/bin/sh
> +
> +. tst_env.sh
> +
> +tst_res TPASS "This is called from the shell script!"
> +tst_sleep 100ms
> diff --git a/testcases/lib/tst_env.sh b/testcases/lib/tst_env.sh
> new file mode 100644
> index 000000000..948bc5024
> --- /dev/null
> +++ b/testcases/lib/tst_env.sh
> @@ -0,0 +1,21 @@
> +#!/bin/sh
> +
> +tst_script_name=$(basename $0)
> +
> +if [ -z "$LTP_IPC_PATH" ]; then
> +	echo "This script has to be executed from a LTP loader!"
> +	exit 1
> +fi
> +
> +tst_brk_()
> +{
> +	tst_res_ "$@"
> +
> +	case "$3" in
> +		"TBROK") exit 2;;
> +		*) exit 0;;
> +	esac
> +}
> +
> +alias tst_res="tst_res_ $tst_script_name \$LINENO"
> +alias tst_brk="tst_brk_ $tst_script_name \$LINENO"
> diff --git a/testcases/lib/tst_res_.c b/testcases/lib/tst_res_.c
> new file mode 100644
> index 000000000..a43920f36
> --- /dev/null
> +++ b/testcases/lib/tst_res_.c
> @@ -0,0 +1,58 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Copyright (c) 2024 Cyril Hrubis <chrubis@suse.cz>
> + */
> +
> +#define TST_NO_DEFAULT_MAIN
> +#include "tst_test.h"
> +
> +static void print_help(void)
> +{
> +	printf("Usage: tst_res_ filename lineno [TPASS|TFAIL|TCONF|TINFO|TDEBUG] 'A short description'\n");
> +}
> +
> +int main(int argc, char *argv[])
> +{
> +	int type, i;
> +
> +	if (argc < 5)
> +		goto help;
> +
> +	if (!strcmp(argv[3], "TPASS"))
> +		type = TPASS;
> +	else if (!strcmp(argv[3], "TFAIL"))
> +		type = TFAIL;
> +	else if (!strcmp(argv[3], "TCONF"))
> +		type = TCONF;
> +	else if (!strcmp(argv[3], "TINFO"))
> +		type = TINFO;
> +	else if (!strcmp(argv[3], "TDEBUG"))
> +		type = TDEBUG;
> +	else
> +		goto help;
> +
> +	size_t len = 0;
> +
> +	for (i = 4; i < argc; i++)
> +		len += strlen(argv[i]) + 1;
> +
> +	char *msg = SAFE_MALLOC(len);
> +	char *msgp = msg;
> +
> +	for (i = 4; i < argc; i++) {
> +		msgp = strcpy(msgp, argv[i]);
> +		msgp += strlen(argv[i]);
> +		*(msgp++) = ' ';
> +	}
> +
> +	*(msgp - 1) = 0;
> +
> +	tst_reinit();
> +
> +	tst_res_(argv[1], atoi(argv[2]), type, "%s", msg);
> +
> +	return 0;
> +help:
> +	print_help();
> +	return 1;
> +}
Li Wang Sept. 3, 2024, 7:29 a.m. UTC | #2
On Tue, Aug 27, 2024 at 8:04 PM Cyril Hrubis <chrubis@suse.cz> wrote:
>
> This is a proof of a concept of a seamless C and shell integration. The
> idea is that with this you can mix shell and C code as much as as you
> wish to get the best of the two worlds.
>
> Signed-off-by: Cyril Hrubis <chrubis@suse.cz>
> Reviewed-by: Richard Palethorpe <io@richiejp.com>

Reviewed-by: Li Wang <liwang@redhat.com>



--
Regards,
Li Wang
Petr Vorel Sept. 3, 2024, 8:24 a.m. UTC | #3
Hi Cyril,

...
> diff --git a/testcases/lib/run_tests.sh b/testcases/lib/run_tests.sh
I hoped testcases/lib/run_tests.sh could be run in CI (and manually).

We have in the toplevel Makefile other test targets, could we add
testcases/lib/run_tests.sh to test-shell target?

1) Add test target to testcases/lib/tests/Makefile (similar to
metadata/tests/Makefile).

test:
	@echo == Run C - shell integration tests ===
	@./run_tests.sh

2) Add test target to testcases/lib/Makefile which would call test target in the
test/ directory (similar to metadata/Makefile).

test:
	$(MAKE) -C $(abs_srcdir)/tests/ test

3) Add testing to the toplevel test-shell target + build dependency to testcases/lib/.

test-shell: lib-all
ifneq ($(build),$(host))
	$(error running tests on cross-compile build not supported)
endif
	$(call _test,-s)
	$(MAKE) -C $(abs_top_builddir)/testcases/lib
	$(MAKE) -C $(abs_top_srcdir)/testcases/lib test

> --- /dev/null
> +++ b/testcases/lib/run_tests.sh
> @@ -0,0 +1,11 @@
> +#!/bin/sh
> +
> +testdir=$(realpath $(dirname $0))
Not sure if there should be some quotes.

> +export PATH=$PATH:$testdir:$testdir/tests/
Also quotes here.

...
> diff --git a/testcases/lib/tests/Makefile b/testcases/lib/tests/Makefile
> new file mode 100644
> index 000000000..5a5cf5310
> --- /dev/null
> +++ b/testcases/lib/tests/Makefile
> @@ -0,0 +1,11 @@
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +# Copyright (C) 2009, Cisco Systems Inc.
nit: Maybe LTP copyright? (copy paste error)
> +# Ngie Cooper, August 2009
nit: and you, or just replace Ngie?
> +
> +top_srcdir		?= ../../..
> +
> +include $(top_srcdir)/include/mk/testcases.mk
> +
> +INSTALL_TARGETS=
> +
> +include $(top_srcdir)/include/mk/generic_leaf_target.mk

The rest LGTM, but I'll have a deeper look today or tomorrow and try to play
with it.

Kind regards,
Petr
Li Wang Sept. 6, 2024, 7:34 a.m. UTC | #4
Cyril Hrubis <chrubis@suse.cz> wrote:


> diff --git a/testcases/lib/tst_env.sh b/testcases/lib/tst_env.sh
> new file mode 100644
> index 000000000..948bc5024
> --- /dev/null
> +++ b/testcases/lib/tst_env.sh
> @@ -0,0 +1,21 @@
> +#!/bin/sh
> +
> +tst_script_name=$(basename $0)
> +
> +if [ -z "$LTP_IPC_PATH" ]; then
> +       echo "This script has to be executed from a LTP loader!"
> +       exit 1
> +fi
> +
> +tst_brk_()
> +{
> +       tst_res_ "$@"
> +
> +       case "$3" in
> +               "TBROK") exit 2;;
> +               *) exit 0;;
> +       esac
> +}
> +
> +alias tst_res="tst_res_ $tst_script_name \$LINENO"
> +alias tst_brk="tst_brk_ $tst_script_name \$LINENO"
>


*** Running shell_test01 ***

tst_test.c:1860: TINFO: LTP version: 20240524
tst_test.c:1864: TINFO: Tested kernel: 6.8.0-1010-raspi #11-Ubuntu SMP
PREEMPT_DYNAMIC Thu Aug  8 23:22:41 UTC 2024 aarch64
tst_test.c:1703: TINFO: Timeout per run is 0h 00m 30s
shell_test01.c:11: TINFO: C test exits now
Usage: tst_res_ filename lineno [TPASS|TFAIL|TCONF|TINFO|TDEBUG] 'A short
description'
tst_test.c:1535: TBROK: Test haven't reported results!

Summary:
passed   0
failed   0
broken   1
skipped  0
warnings 0


Here I got test failures on my RPi4 (bash-5.2.21) that the $LINEON can't
be parsed correctly.

The reason probably is some shells or specific versions might not handle
$LINENO correctly within aliases, especially when the line number needs
to be dynamically determined.

So I suggest using a function instead of the alias.


--- a/testcases/lib/tst_env.sh
+++ b/testcases/lib/tst_env.sh
@@ -21,5 +21,12 @@ tst_brk_()
        esac
 }

-alias tst_res="tst_res_ $tst_script_name \$LINENO"
-alias tst_brk="tst_brk_ $tst_script_name \$LINENO"
+tst_res()
+{
+    tst_res_ "$tst_script_name" "$LINENO" "$@"
+}
+
+tst_brk()
+{
+    tst_brk_ "$tst_script_name" "$LINENO" "$@"
+}
Cyril Hrubis Sept. 6, 2024, 9:53 a.m. UTC | #5
Hi!
> tst_test.c:1860: TINFO: LTP version: 20240524
> tst_test.c:1864: TINFO: Tested kernel: 6.8.0-1010-raspi #11-Ubuntu SMP
> PREEMPT_DYNAMIC Thu Aug  8 23:22:41 UTC 2024 aarch64
> tst_test.c:1703: TINFO: Timeout per run is 0h 00m 30s
> shell_test01.c:11: TINFO: C test exits now
> Usage: tst_res_ filename lineno [TPASS|TFAIL|TCONF|TINFO|TDEBUG] 'A short
> description'
> tst_test.c:1535: TBROK: Test haven't reported results!
> 
> Summary:
> passed   0
> failed   0
> broken   1
> skipped  0
> warnings 0
> 
> 
> Here I got test failures on my RPi4 (bash-5.2.21) that the $LINEON can't
> be parsed correctly.

Are you sure that this is due to bash? My guess is that on RPi the
default shell is dash because it's debian based.

I will try to reproduce.

> The reason probably is some shells or specific versions might not handle
> $LINENO correctly within aliases, especially when the line number needs
> to be dynamically determined.
> 
> So I suggest using a function instead of the alias.
> 
> 
> --- a/testcases/lib/tst_env.sh
> +++ b/testcases/lib/tst_env.sh
> @@ -21,5 +21,12 @@ tst_brk_()
>         esac
>  }
> 
> -alias tst_res="tst_res_ $tst_script_name \$LINENO"
> -alias tst_brk="tst_brk_ $tst_script_name \$LINENO"
> +tst_res()
> +{
> +    tst_res_ "$tst_script_name" "$LINENO" "$@"
> +}
> +
> +tst_brk()
> +{
> +    tst_brk_ "$tst_script_name" "$LINENO" "$@"
> +}

That actually does not work because unlike the alias the $LINENO is
expanded in the wrong place and the line is incorrect.

The whole reason for this to be alias is that it's expanded on the
correct line in the test source.
Cyril Hrubis Sept. 6, 2024, 10:03 a.m. UTC | #6
Hi!
> Are you sure that this is due to bash? My guess is that on RPi the
> default shell is dash because it's debian based.
> 
> I will try to reproduce.

And the whole reason seems to be that dash does not support $LINENO
which is actually required to be there by POSIX.

I quite close to declare dash unsupported because it does not even
support POSIX standard.
Li Wang Sept. 6, 2024, 10:09 a.m. UTC | #7
On Fri, Sep 6, 2024 at 5:54 PM Cyril Hrubis <chrubis@suse.cz> wrote:

> Hi!
> > tst_test.c:1860: TINFO: LTP version: 20240524
> > tst_test.c:1864: TINFO: Tested kernel: 6.8.0-1010-raspi #11-Ubuntu SMP
> > PREEMPT_DYNAMIC Thu Aug  8 23:22:41 UTC 2024 aarch64
> > tst_test.c:1703: TINFO: Timeout per run is 0h 00m 30s
> > shell_test01.c:11: TINFO: C test exits now
> > Usage: tst_res_ filename lineno [TPASS|TFAIL|TCONF|TINFO|TDEBUG] 'A short
> > description'
> > tst_test.c:1535: TBROK: Test haven't reported results!
> >
> > Summary:
> > passed   0
> > failed   0
> > broken   1
> > skipped  0
> > warnings 0
> >
> >
> > Here I got test failures on my RPi4 (bash-5.2.21) that the $LINEON can't
> > be parsed correctly.
>
> Are you sure that this is due to bash? My guess is that on RPi the
> default shell is dash because it's debian based.
>

Double checked that is indeed bash.

liwang@raspi4:~/ltp/testcases/lib$ echo $0
-bash

liwang@raspi4:~/ltp/testcases/lib$ echo $SHELL
/bin/bash

liwang@raspi4:~/ltp/testcases/lib$ ps -p $$
    PID TTY          TIME CMD
 174997 pts/0    00:00:04 bash

I even manually coded the !#/bin/bash in every test file, the test still
failed.

$ bash run_tests.sh
tst_test.c:1860: TINFO: LTP version: 20240524
tst_test.c:1864: TINFO: Tested kernel: 6.8.0-1010-raspi #11-Ubuntu SMP
PREEMPT_DYNAMIC Thu Aug  8 23:22:41 UTC 2024 aarch64
tst_test.c:1703: TINFO: Timeout per run is 0h 00m 30s
shell_test01.c:11: TINFO: C test exits now
/home/liwang/ltp/testcases/lib/tests//shell_test_pass.sh: line 5: tst_res:
command not found
/bin/bash
tst_test.c:1535: TBROK: Test haven't reported results!

Summary:
passed   0
failed   0
broken   1
skipped  0
warnings 0



>
> I will try to reproduce.
>
> > The reason probably is some shells or specific versions might not handle
> > $LINENO correctly within aliases, especially when the line number needs
> > to be dynamically determined.
> >
> > So I suggest using a function instead of the alias.
> >
> >
> > --- a/testcases/lib/tst_env.sh
> > +++ b/testcases/lib/tst_env.sh
> > @@ -21,5 +21,12 @@ tst_brk_()
> >         esac
> >  }
> >
> > -alias tst_res="tst_res_ $tst_script_name \$LINENO"
> > -alias tst_brk="tst_brk_ $tst_script_name \$LINENO"
> > +tst_res()
> > +{
> > +    tst_res_ "$tst_script_name" "$LINENO" "$@"
> > +}
> > +
> > +tst_brk()
> > +{
> > +    tst_brk_ "$tst_script_name" "$LINENO" "$@"
> > +}
>
> That actually does not work because unlike the alias the $LINENO is
> expanded in the wrong place and the line is incorrect.
>
> The whole reason for this to be alias is that it's expanded on the
> correct line in the test source.
>
> --
> Cyril Hrubis
> chrubis@suse.cz
>
>
Li Wang Sept. 6, 2024, 10:19 a.m. UTC | #8
On Fri, Sep 6, 2024 at 5:54 PM Cyril Hrubis <chrubis@suse.cz> wrote:

> Hi!
> > tst_test.c:1860: TINFO: LTP version: 20240524
> > tst_test.c:1864: TINFO: Tested kernel: 6.8.0-1010-raspi #11-Ubuntu SMP
> > PREEMPT_DYNAMIC Thu Aug  8 23:22:41 UTC 2024 aarch64
> > tst_test.c:1703: TINFO: Timeout per run is 0h 00m 30s
> > shell_test01.c:11: TINFO: C test exits now
> > Usage: tst_res_ filename lineno [TPASS|TFAIL|TCONF|TINFO|TDEBUG] 'A short
> > description'
> > tst_test.c:1535: TBROK: Test haven't reported results!
> >
> > Summary:
> > passed   0
> > failed   0
> > broken   1
> > skipped  0
> > warnings 0
> >
> >
> > Here I got test failures on my RPi4 (bash-5.2.21) that the $LINEON can't
> > be parsed correctly.
>
> Are you sure that this is due to bash? My guess is that on RPi the
> default shell is dash because it's debian based.
>
> I will try to reproduce.
>
> > The reason probably is some shells or specific versions might not handle
> > $LINENO correctly within aliases, especially when the line number needs
> > to be dynamically determined.
> >
> > So I suggest using a function instead of the alias.
> >
> >
> > --- a/testcases/lib/tst_env.sh
> > +++ b/testcases/lib/tst_env.sh
> > @@ -21,5 +21,12 @@ tst_brk_()
> >         esac
> >  }
> >
> > -alias tst_res="tst_res_ $tst_script_name \$LINENO"
> > -alias tst_brk="tst_brk_ $tst_script_name \$LINENO"
> > +tst_res()
> > +{
> > +    tst_res_ "$tst_script_name" "$LINENO" "$@"
> > +}
> > +
> > +tst_brk()
> > +{
> > +    tst_brk_ "$tst_script_name" "$LINENO" "$@"
> > +}
>
> That actually does not work because unlike the alias the $LINENO is
> expanded in the wrong place and the line is incorrect.
>
> The whole reason for this to be alias is that it's expanded on the
> correct line in the test source.
>

Ah, you're right, I wasn't aware of that.

Maybe we can use a trick with eval to delay the expansion
of $LINENO until the function is actually called?

e.g.

tst_res()
{
    eval "tst_res_ \"$tst_script_name\" \$LINENO \"\$@\""
}

tst_brk()
{
    eval "tst_brk_ \"$tst_script_name\" \$LINENO \"\$@\""
}
Li Wang Sept. 6, 2024, 11:06 a.m. UTC | #9
On Fri, Sep 6, 2024 at 6:19 PM Li Wang <liwang@redhat.com> wrote:

>
>
> On Fri, Sep 6, 2024 at 5:54 PM Cyril Hrubis <chrubis@suse.cz> wrote:
>
>> Hi!
>> > tst_test.c:1860: TINFO: LTP version: 20240524
>> > tst_test.c:1864: TINFO: Tested kernel: 6.8.0-1010-raspi #11-Ubuntu SMP
>> > PREEMPT_DYNAMIC Thu Aug  8 23:22:41 UTC 2024 aarch64
>> > tst_test.c:1703: TINFO: Timeout per run is 0h 00m 30s
>> > shell_test01.c:11: TINFO: C test exits now
>> > Usage: tst_res_ filename lineno [TPASS|TFAIL|TCONF|TINFO|TDEBUG] 'A
>> short
>> > description'
>> > tst_test.c:1535: TBROK: Test haven't reported results!
>> >
>> > Summary:
>> > passed   0
>> > failed   0
>> > broken   1
>> > skipped  0
>> > warnings 0
>> >
>> >
>> > Here I got test failures on my RPi4 (bash-5.2.21) that the $LINEON can't
>> > be parsed correctly.
>>
>> Are you sure that this is due to bash? My guess is that on RPi the
>> default shell is dash because it's debian based.
>>
>> I will try to reproduce.
>>
>> > The reason probably is some shells or specific versions might not handle
>> > $LINENO correctly within aliases, especially when the line number needs
>> > to be dynamically determined.
>> >
>> > So I suggest using a function instead of the alias.
>> >
>> >
>> > --- a/testcases/lib/tst_env.sh
>> > +++ b/testcases/lib/tst_env.sh
>> > @@ -21,5 +21,12 @@ tst_brk_()
>> >         esac
>> >  }
>> >
>> > -alias tst_res="tst_res_ $tst_script_name \$LINENO"
>> > -alias tst_brk="tst_brk_ $tst_script_name \$LINENO"
>> > +tst_res()
>> > +{
>> > +    tst_res_ "$tst_script_name" "$LINENO" "$@"
>> > +}
>> > +
>> > +tst_brk()
>> > +{
>> > +    tst_brk_ "$tst_script_name" "$LINENO" "$@"
>> > +}
>>
>> That actually does not work because unlike the alias the $LINENO is
>> expanded in the wrong place and the line is incorrect.
>>
>> The whole reason for this to be alias is that it's expanded on the
>> correct line in the test source.
>>
>
> Ah, you're right, I wasn't aware of that.
>
> Maybe we can use a trick with eval to delay the expansion
> of $LINENO until the function is actually called?
>

Hmm, seems this is also not working as expected. Please ignore.

$ cat liwang.sh
#!/bin/bash

print_line() {
    eval "echo \"this is line: \$LINENO\""
}

echo "This is line $LINENO"
print_line

liwang@raspi4:~/ltp/testcases/lib$ ./liwang.sh
This is line 7
this is line: 4

>
Cyril Hrubis Sept. 6, 2024, 11:22 a.m. UTC | #10
Hi!
> > > Here I got test failures on my RPi4 (bash-5.2.21) that the $LINEON can't
> > > be parsed correctly.
> >
> > Are you sure that this is due to bash? My guess is that on RPi the
> > default shell is dash because it's debian based.
> >
> 
> Double checked that is indeed bash.
> 
> liwang@raspi4:~/ltp/testcases/lib$ echo $0
> -bash
> 
> liwang@raspi4:~/ltp/testcases/lib$ echo $SHELL
> /bin/bash
> 
> liwang@raspi4:~/ltp/testcases/lib$ ps -p $$
>     PID TTY          TIME CMD
>  174997 pts/0    00:00:04 bash
> 
> I even manually coded the !#/bin/bash in every test file, the test still
> failed.

The difference is that /bin/sh links to dash in debian. But as you
pointed out even if all /bin/sh is changed to /bin/bash in the scripts
the tests stil fails.

It looks like on RPi alias with variables does not work at all.
Cyril Hrubis Sept. 6, 2024, 1:28 p.m. UTC | #11
Hi!
> The difference is that /bin/sh links to dash in debian. But as you
> pointed out even if all /bin/sh is changed to /bin/bash in the scripts
> the tests stil fails.
> 
> It looks like on RPi alias with variables does not work at all.

So it looks like aliases may not be expanded on non-interactive shells
unless it's enabled explicitely.

This patch uses shopt to enable that and also works around dash that
does not support $LINENO:

diff --git a/testcases/lib/tst_env.sh b/testcases/lib/tst_env.sh
index 67ba80744..bb0c586d7 100644
--- a/testcases/lib/tst_env.sh
+++ b/testcases/lib/tst_env.sh
@@ -6,6 +6,16 @@
 
 tst_script_name=$(basename $0)
 
+# bash does not expand aliases in non-iteractive mode, enable it
+if [ -n "$BASH_VERSION" ]; then
+       shopt -s expand_aliases
+fi
+
+# dash does not support line numbers even though this is mandated by POSIX
+if [ -z "$LINENO" ]; then
+       LINENO=-1
+fi
+
 if [ -z "$LTP_IPC_PATH" ]; then
        echo "This script has to be executed from a LTP loader!"
        exit 1
Li Wang Sept. 7, 2024, 1:29 a.m. UTC | #12
On Fri, Sep 6, 2024 at 9:30 PM Cyril Hrubis <chrubis@suse.cz> wrote:

> Hi!
> > The difference is that /bin/sh links to dash in debian. But as you
> > pointed out even if all /bin/sh is changed to /bin/bash in the scripts
> > the tests stil fails.
> >
> > It looks like on RPi alias with variables does not work at all.
>
> So it looks like aliases may not be expanded on non-interactive shells
> unless it's enabled explicitely.
>
> This patch uses shopt to enable that and also works around dash that
> does not support $LINENO:
>
> diff --git a/testcases/lib/tst_env.sh b/testcases/lib/tst_env.sh
> index 67ba80744..bb0c586d7 100644
> --- a/testcases/lib/tst_env.sh
> +++ b/testcases/lib/tst_env.sh
> @@ -6,6 +6,16 @@
>
>  tst_script_name=$(basename $0)
>
> +# bash does not expand aliases in non-iteractive mode, enable it
> +if [ -n "$BASH_VERSION" ]; then
> +       shopt -s expand_aliases
> +fi
> +
> +# dash does not support line numbers even though this is mandated by POSIX
> +if [ -z "$LINENO" ]; then
> +       LINENO=-1
> +fi
> +
>  if [ -z "$LTP_IPC_PATH" ]; then
>         echo "This script has to be executed from a LTP loader!"
>         exit 1


LGTM.

This ensures that the script will work in both bash (with alias expansion
enabled)
and dash (with a fallback for LINENO). I confirmed that works correctly in
my RPi4.
Thanks!
Cyril Hrubis Sept. 16, 2024, 9:52 a.m. UTC | #13
Hi!
> I hoped testcases/lib/run_tests.sh could be run in CI (and manually).
> 
> We have in the toplevel Makefile other test targets, could we add
> testcases/lib/run_tests.sh to test-shell target?
> 
> 1) Add test target to testcases/lib/tests/Makefile (similar to
> metadata/tests/Makefile).
> 
> test:
> 	@echo == Run C - shell integration tests ===
> 	@./run_tests.sh
> 
> 2) Add test target to testcases/lib/Makefile which would call test target in the
> test/ directory (similar to metadata/Makefile).
> 
> test:
> 	$(MAKE) -C $(abs_srcdir)/tests/ test
> 
> 3) Add testing to the toplevel test-shell target + build dependency to testcases/lib/.
> 
> test-shell: lib-all
> ifneq ($(build),$(host))
> 	$(error running tests on cross-compile build not supported)
> endif
> 	$(call _test,-s)
> 	$(MAKE) -C $(abs_top_builddir)/testcases/lib
> 	$(MAKE) -C $(abs_top_srcdir)/testcases/lib test

Quite a few of these testcases fail in order to test failures, we need
to be able to record the test output and compare that with the expected
outcome first, in order to make these tests useful. And I guess that we
need that for the C library tests as well. So I would rather do this
later for both libraries instead.
diff mbox series

Patch

diff --git a/include/tst_test.h b/include/tst_test.h
index afc6a5714..9871676a5 100644
--- a/include/tst_test.h
+++ b/include/tst_test.h
@@ -331,6 +331,8 @@  struct tst_fs {
  * @child_needs_reinit: Has to be set if the test needs to call tst_reinit()
  *                      from a process started by exec().
  *
+ * @runs_script: Implies child_needs_reinit and forks_child at the moment.
+ *
  * @needs_devfs: If set the devfs is mounted at tst_test.mntpoint. This is
  *               needed for tests that need to create device files since tmpfs
  *               at /tmp is usually mounted with 'nodev' option.
@@ -521,6 +523,7 @@  struct tst_fs {
 	unsigned int mount_device:1;
 	unsigned int needs_rofs:1;
 	unsigned int child_needs_reinit:1;
+	unsigned int runs_script:1;
 	unsigned int needs_devfs:1;
 	unsigned int restore_wallclock:1;
 
@@ -529,6 +532,8 @@  struct tst_fs {
 	unsigned int skip_in_lockdown:1;
 	unsigned int skip_in_secureboot:1;
 	unsigned int skip_in_compat:1;
+
+
 	int needs_abi_bits;
 
 	unsigned int needs_hugetlbfs:1;
@@ -616,6 +621,38 @@  void tst_run_tcases(int argc, char *argv[], struct tst_test *self)
  */
 void tst_reinit(void);
 
+/**
+ * tst_run_script() - Prepare the environment and execute a (shell) script.
+ *
+ * @script_name: A filename of the script.
+ * @params: A NULL terminated array of (shell) script parameters, pass NULL if
+ *          none are needed. This what is passed starting from argv[1].
+ *
+ * The (shell) script is executed with LTP_IPC_PATH in environment so that the
+ * binary helpers such as tst_res_ or tst_checkpoint work properly when executed
+ * from the script. This also means that the tst_test.runs_script flag needs to
+ * be set.
+ *
+ * A shell script has to source the tst_env.sh shell script at the start and
+ * after that it's free to use tst_res in the same way C code would use.
+ *
+ * Example shell script that reports success::
+ *
+ *   #!/bin/sh
+ *   . tst_env.sh
+ *
+ *   tst_res TPASS "Example test works"
+ *
+ * The call returns a pid in a case that you want to examine the return value
+ * of the script yourself. If you do not need to check the return value
+ * yourself you can use tst_reap_children() to wait for the completion. Or let
+ * the test library collect the child automatically, just be wary that the
+ * script and the test both runs concurently at the same time in this case.
+ *
+ * Return: A pid of the (shell) script process.
+ */
+int tst_run_script(const char *script_name, char *const params[]);
+
 unsigned int tst_multiply_timeout(unsigned int timeout);
 
 /*
diff --git a/lib/tst_test.c b/lib/tst_test.c
index 201b81e14..918bee2a1 100644
--- a/lib/tst_test.c
+++ b/lib/tst_test.c
@@ -4,6 +4,8 @@ 
  * Copyright (c) Linux Test Project, 2016-2024
  */
 
+#define _GNU_SOURCE
+
 #include <limits.h>
 #include <stdio.h>
 #include <stdarg.h>
@@ -174,6 +176,50 @@  void tst_reinit(void)
 	SAFE_CLOSE(fd);
 }
 
+extern char **environ;
+
+static unsigned int params_array_len(char *const array[])
+{
+	unsigned int ret = 0;
+
+	if (!array)
+		return 0;
+
+	while (*(array++))
+		ret++;
+
+	return ret;
+}
+
+int tst_run_script(const char *script_name, char *const params[])
+{
+	int pid;
+	unsigned int i, params_len = params_array_len(params);
+	char *argv[params_len + 2];
+
+	if (!tst_test->runs_script)
+		tst_brk(TBROK, "runs_script flag must be set!");
+
+	argv[0] = (char*)script_name;
+
+	if (params) {
+		for (i = 0; i < params_len; i++)
+			argv[i+1] = params[i];
+	}
+
+	argv[params_len+1] = NULL;
+
+	pid = SAFE_FORK();
+	if (pid)
+		return pid;
+
+	execvpe(script_name, argv, environ);
+
+	tst_brk(TBROK | TERRNO, "execvpe(%s, ...) failed!", script_name);
+
+	return -1;
+}
+
 static void update_results(int ttype)
 {
 	if (!results)
@@ -1226,6 +1272,11 @@  static void do_setup(int argc, char *argv[])
 		tdebug = 1;
 	}
 
+	if (tst_test->runs_script) {
+		tst_test->child_needs_reinit = 1;
+		tst_test->forks_child = 1;
+	}
+
 	if (tst_test->needs_kconfigs && tst_kconfig_check(tst_test->needs_kconfigs))
 		tst_brk(TCONF, "Aborting due to unsuitable kernel config, see above!");
 
diff --git a/testcases/lib/.gitignore b/testcases/lib/.gitignore
index e8afd06f3..d0dacf62a 100644
--- a/testcases/lib/.gitignore
+++ b/testcases/lib/.gitignore
@@ -23,3 +23,4 @@ 
 /tst_sleep
 /tst_supported_fs
 /tst_timeout_kill
+/tst_res_
diff --git a/testcases/lib/Makefile b/testcases/lib/Makefile
index 990b46089..928d76d62 100644
--- a/testcases/lib/Makefile
+++ b/testcases/lib/Makefile
@@ -13,6 +13,6 @@  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_ns_ifmove tst_lockdown_enabled tst_secureboot_enabled tst_res_
 
-include $(top_srcdir)/include/mk/generic_leaf_target.mk
+include $(top_srcdir)/include/mk/generic_trunk_target.mk
diff --git a/testcases/lib/run_tests.sh b/testcases/lib/run_tests.sh
new file mode 100755
index 000000000..60e7d1bcf
--- /dev/null
+++ b/testcases/lib/run_tests.sh
@@ -0,0 +1,11 @@ 
+#!/bin/sh
+
+testdir=$(realpath $(dirname $0))
+export PATH=$PATH:$testdir:$testdir/tests/
+
+for i in `seq -w 01 06`; do
+	echo
+	echo "*** Running shell_test$i ***"
+	echo
+	./tests/shell_test$i
+done
diff --git a/testcases/lib/tests/.gitignore b/testcases/lib/tests/.gitignore
new file mode 100644
index 000000000..da967c4d6
--- /dev/null
+++ b/testcases/lib/tests/.gitignore
@@ -0,0 +1,6 @@ 
+shell_test01
+shell_test02
+shell_test03
+shell_test04
+shell_test05
+shell_test06
diff --git a/testcases/lib/tests/Makefile b/testcases/lib/tests/Makefile
new file mode 100644
index 000000000..5a5cf5310
--- /dev/null
+++ b/testcases/lib/tests/Makefile
@@ -0,0 +1,11 @@ 
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (C) 2009, Cisco Systems Inc.
+# Ngie Cooper, August 2009
+
+top_srcdir		?= ../../..
+
+include $(top_srcdir)/include/mk/testcases.mk
+
+INSTALL_TARGETS=
+
+include $(top_srcdir)/include/mk/generic_leaf_target.mk
diff --git a/testcases/lib/tests/shell_test01.c b/testcases/lib/tests/shell_test01.c
new file mode 100644
index 000000000..b9f07308e
--- /dev/null
+++ b/testcases/lib/tests/shell_test01.c
@@ -0,0 +1,17 @@ 
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Shell test example.
+ */
+
+#include "tst_test.h"
+
+static void run_test(void)
+{
+	tst_run_script("shell_test_pass.sh", NULL);
+	tst_res(TINFO, "C test exits now");
+}
+
+static struct tst_test test = {
+	.runs_script = 1,
+	.test_all = run_test,
+};
diff --git a/testcases/lib/tests/shell_test02.c b/testcases/lib/tests/shell_test02.c
new file mode 100644
index 000000000..087055794
--- /dev/null
+++ b/testcases/lib/tests/shell_test02.c
@@ -0,0 +1,18 @@ 
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Shell test example.
+ */
+
+#include "tst_test.h"
+
+static void run_test(void)
+{
+	tst_run_script("shell_test_pass.sh", NULL);
+	tst_reap_children();
+	tst_res(TINFO, "Shell test has finished at this point!");
+}
+
+static struct tst_test test = {
+	.runs_script = 1,
+	.test_all = run_test,
+};
diff --git a/testcases/lib/tests/shell_test03.c b/testcases/lib/tests/shell_test03.c
new file mode 100644
index 000000000..61436891e
--- /dev/null
+++ b/testcases/lib/tests/shell_test03.c
@@ -0,0 +1,25 @@ 
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Shell test example.
+ */
+
+#include <sys/wait.h>
+#include "tst_test.h"
+
+static void run_test(void)
+{
+	int pid, status;
+
+	pid = tst_run_script("shell_test_pass.sh", NULL);
+
+	tst_res(TINFO, "Waiting for the pid %i", pid);
+
+	waitpid(pid, &status, 0);
+
+	tst_res(TINFO, "Shell test has %s", tst_strstatus(status));
+}
+
+static struct tst_test test = {
+	.runs_script = 1,
+	.test_all = run_test,
+};
diff --git a/testcases/lib/tests/shell_test04.c b/testcases/lib/tests/shell_test04.c
new file mode 100644
index 000000000..a32dd1e9f
--- /dev/null
+++ b/testcases/lib/tests/shell_test04.c
@@ -0,0 +1,18 @@ 
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Shell test example.
+ */
+
+#include "tst_test.h"
+
+static void run_test(void)
+{
+	char *const params[] = {"param1", "param2", NULL};
+
+	tst_run_script("shell_test_check_argv.sh", params);
+}
+
+static struct tst_test test = {
+	.runs_script = 1,
+	.test_all = run_test,
+};
diff --git a/testcases/lib/tests/shell_test05.c b/testcases/lib/tests/shell_test05.c
new file mode 100644
index 000000000..771af8fc3
--- /dev/null
+++ b/testcases/lib/tests/shell_test05.c
@@ -0,0 +1,27 @@ 
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Shell test example.
+ */
+
+#include "tst_test.h"
+
+static void run_test(void)
+{
+	int pid;
+
+	pid = tst_run_script("shell_test_checkpoint.sh", NULL);
+
+	tst_res(TINFO, "Waiting for shell to sleep on checkpoint!");
+
+	TST_PROCESS_STATE_WAIT(pid, 'S', 10000);
+
+	tst_res(TINFO, "Waking shell child!");
+
+	TST_CHECKPOINT_WAKE(0);
+}
+
+static struct tst_test test = {
+	.runs_script = 1,
+	.needs_checkpoints = 1,
+	.test_all = run_test,
+};
diff --git a/testcases/lib/tests/shell_test06.c b/testcases/lib/tests/shell_test06.c
new file mode 100644
index 000000000..89d66bab0
--- /dev/null
+++ b/testcases/lib/tests/shell_test06.c
@@ -0,0 +1,16 @@ 
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Shell test example.
+ */
+
+#include "tst_test.h"
+
+static void run_test(void)
+{
+	tst_run_script("shell_test_brk.sh", NULL);
+}
+
+static struct tst_test test = {
+	.runs_script = 1,
+	.test_all = run_test,
+};
diff --git a/testcases/lib/tests/shell_test_brk.sh b/testcases/lib/tests/shell_test_brk.sh
new file mode 100755
index 000000000..f266dc3fe
--- /dev/null
+++ b/testcases/lib/tests/shell_test_brk.sh
@@ -0,0 +1,6 @@ 
+#!/bin/sh
+
+. tst_env.sh
+
+tst_brk TCONF "This exits test and the next message should not be reached"
+tst_res TFAIL "If you see this the test failed"
diff --git a/testcases/lib/tests/shell_test_check_argv.sh b/testcases/lib/tests/shell_test_check_argv.sh
new file mode 100755
index 000000000..ce357027d
--- /dev/null
+++ b/testcases/lib/tests/shell_test_check_argv.sh
@@ -0,0 +1,23 @@ 
+#!/bin/sh
+
+. tst_env.sh
+
+tst_res TINFO "argv = $@"
+
+if [ $# -ne 2 ]; then
+	tst_res TFAIL "Wrong number of parameters got $# expected 2"
+else
+	tst_res TPASS "Got 2 parameters"
+fi
+
+if [ "$1" != "param1" ]; then
+	tst_res TFAIL "First parameter is $1 expected param1"
+else
+	tst_res TPASS "First parameter is $1"
+fi
+
+if [ "$2" != "param2" ]; then
+	tst_res TFAIL "Second parameter is $2 expected param2"
+else
+	tst_res TPASS "Second parameter is $2"
+fi
diff --git a/testcases/lib/tests/shell_test_checkpoint.sh b/testcases/lib/tests/shell_test_checkpoint.sh
new file mode 100755
index 000000000..0ceb7cf66
--- /dev/null
+++ b/testcases/lib/tests/shell_test_checkpoint.sh
@@ -0,0 +1,7 @@ 
+#!/bin/sh
+
+. tst_env.sh
+
+tst_res TINFO "Waiting for a checkpoint 0"
+tst_checkpoint wait 10000 0
+tst_res TPASS "Continuing after checkpoint"
diff --git a/testcases/lib/tests/shell_test_pass.sh b/testcases/lib/tests/shell_test_pass.sh
new file mode 100755
index 000000000..fd0684eb2
--- /dev/null
+++ b/testcases/lib/tests/shell_test_pass.sh
@@ -0,0 +1,6 @@ 
+#!/bin/sh
+
+. tst_env.sh
+
+tst_res TPASS "This is called from the shell script!"
+tst_sleep 100ms
diff --git a/testcases/lib/tst_env.sh b/testcases/lib/tst_env.sh
new file mode 100644
index 000000000..948bc5024
--- /dev/null
+++ b/testcases/lib/tst_env.sh
@@ -0,0 +1,21 @@ 
+#!/bin/sh
+
+tst_script_name=$(basename $0)
+
+if [ -z "$LTP_IPC_PATH" ]; then
+	echo "This script has to be executed from a LTP loader!"
+	exit 1
+fi
+
+tst_brk_()
+{
+	tst_res_ "$@"
+
+	case "$3" in
+		"TBROK") exit 2;;
+		*) exit 0;;
+	esac
+}
+
+alias tst_res="tst_res_ $tst_script_name \$LINENO"
+alias tst_brk="tst_brk_ $tst_script_name \$LINENO"
diff --git a/testcases/lib/tst_res_.c b/testcases/lib/tst_res_.c
new file mode 100644
index 000000000..a43920f36
--- /dev/null
+++ b/testcases/lib/tst_res_.c
@@ -0,0 +1,58 @@ 
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2024 Cyril Hrubis <chrubis@suse.cz>
+ */
+
+#define TST_NO_DEFAULT_MAIN
+#include "tst_test.h"
+
+static void print_help(void)
+{
+	printf("Usage: tst_res_ filename lineno [TPASS|TFAIL|TCONF|TINFO|TDEBUG] 'A short description'\n");
+}
+
+int main(int argc, char *argv[])
+{
+	int type, i;
+
+	if (argc < 5)
+		goto help;
+
+	if (!strcmp(argv[3], "TPASS"))
+		type = TPASS;
+	else if (!strcmp(argv[3], "TFAIL"))
+		type = TFAIL;
+	else if (!strcmp(argv[3], "TCONF"))
+		type = TCONF;
+	else if (!strcmp(argv[3], "TINFO"))
+		type = TINFO;
+	else if (!strcmp(argv[3], "TDEBUG"))
+		type = TDEBUG;
+	else
+		goto help;
+
+	size_t len = 0;
+
+	for (i = 4; i < argc; i++)
+		len += strlen(argv[i]) + 1;
+
+	char *msg = SAFE_MALLOC(len);
+	char *msgp = msg;
+
+	for (i = 4; i < argc; i++) {
+		msgp = strcpy(msgp, argv[i]);
+		msgp += strlen(argv[i]);
+		*(msgp++) = ' ';
+	}
+
+	*(msgp - 1) = 0;
+
+	tst_reinit();
+
+	tst_res_(argv[1], atoi(argv[2]), type, "%s", msg);
+
+	return 0;
+help:
+	print_help();
+	return 1;
+}