From 1113be584b46e7e037a2bc4ece9b3b938969ec75 Mon Sep 17 00:00:00 2001 From: Egor Tensin Date: Wed, 14 Oct 2026 22:50:19 +0500 Subject: shuffle files around, add the top-level symlink --- README.md | 33 ++++++++++---- harness | 1 + lib/log.sh | 139 ---------------------------------------------------------- lib/test.sh | 23 ---------- main.sh | 141 ----------------------------------------------------------- src/log.sh | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.sh | 143 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/test.sh | 23 ++++++++++ 8 files changed, 331 insertions(+), 311 deletions(-) create mode 120000 harness delete mode 100644 lib/log.sh delete mode 100644 lib/test.sh delete mode 100755 main.sh create mode 100644 src/log.sh create mode 100755 src/main.sh create mode 100644 src/test.sh diff --git a/README.md b/README.md index df58098..015b544 100644 --- a/README.md +++ b/README.md @@ -8,22 +8,39 @@ Used in a bunch of projects of mine, for example: * [tag-release](https://github.com/egor-tensin/tag-release/tree/master/test) * [config-links](https://github.com/egor-tensin/config-links/tree/master/test) +Integration +----------- + +* Can be added as a submodule like so: + + > cd my/project/test + > git submodule add https://github.com/egor-tensin/test-harness.git runner + + * Run tests using the `harness` script: + + > ./test/runner/harness + + * This will pick up all test files in "project/test/". + +* Alternatively, check out this repo somewhere and use the `-d` parameter to +specify the path to the tests directory. + Usage ----- -Supposed to be added as a submodule like so: +* By default, all tests in the parent directory are run: - > cd my/project/test - > git submodule add https://github.com/egor-tensin/test-harness.git runner + ./test/runner/harness # Runs all tests in the test/ directory -This will add it as a submodule in `test/runner/` in your project's directory. +* You can filter tests to run using the `-f`/`--filter` parameter (uses +fnmatch(3)): -Run tests using the main.sh script: + ./test/runner/harness -f '*' # Runs all tests still + ./test/runner/harness -f '*label*' # Collects all test files containing "label" - > ./test/runner/main.sh +* Specify a directory to collect tests from using the `-d`/`--dir` parameter: -The script will discover all your tests in the parent directory, run them and -print a short summary. + ./harness -d path/to/tests/ API --- diff --git a/harness b/harness new file mode 120000 index 0000000..2dfaae6 --- /dev/null +++ b/harness @@ -0,0 +1 @@ +src/main.sh \ No newline at end of file diff --git a/lib/log.sh b/lib/log.sh deleted file mode 100644 index db2c498..0000000 --- a/lib/log.sh +++ /dev/null @@ -1,139 +0,0 @@ -# Copyright (c) 2026 Egor Tensin -# This file is part of the "test-harness" project. -# For details, see https://github.com/egor-tensin/test-harness -# Distributed under the MIT License. - -log() { - local msg - for msg; do - echo -e "$test_file | $msg" >&2 - done -} - -log_run() { - if [ "$#" -lt 1 ]; then - log "usage: ${FUNCNAME[0]} ARGV0 [ARG...]" - return 1 - fi - - local msg='Running:' - msg="$msg$( printf -- ' %q' "$@" )" - log "$msg" -} - -fail() { - local msg - for msg; do - log "FAIL: $msg" - done -} - -fail_details() { - local msg - for msg; do - fail " $msg" - done -} - -_log_numof_tests_ok=0 -_log_numof_tests_failed=0 -declare -a _log_tests_failed=() -declare -A _log_tests_should_fail=() - -_log_test_start() { - if [ "$#" -ne 1 ]; then - echo "usage: ${FUNCNAME[0]} TEST_FILE" >&2 - return 1 - fi - - local test_file="$1" - - if [ -n "${test_should_fail:-}" ]; then - _log_tests_should_fail["$test_file"]=1 - fi - - cat <&2 - -====================================================================== -TEST: $test_file -====================================================================== -EOF - - log "Should fail: ${test_should_fail:-No}" -} - -_log_test_ok() { - if [ "$#" -ne 1 ]; then - echo "usage: ${FUNCNAME[0]} TEST_FILE" >&2 - return 1 - fi - - _log_numof_tests_ok=$((_log_numof_tests_ok + 1)) - - cat <&2 ----------------------------------------------------------------------- -OK: $1 ----------------------------------------------------------------------- -EOF -} - -_log_test_fail() { - if [ "$#" -ne 1 ]; then - echo "usage: ${FUNCNAME[0]} TEST_FILE" >&2 - return 1 - fi - - _log_numof_tests_failed=$((_log_numof_tests_failed + 1)) - _log_tests_failed+=("$1") - - cat <&2 ----------------------------------------------------------------------- -FAIL: $1 ----------------------------------------------------------------------- -EOF -} - -_log_test_finish() { - if [ "$#" -ne 2 ]; then - echo "usage: ${FUNCNAME[0]} TEST_FILE EXIT_CODE" >&2 - return 1 - fi - - local test_file="$1" - local ret="$2" - local test_should_fail - test_should_fail="${_log_tests_should_fail["$test_file"]:+x}" - - if [ "$ret" -eq 0 ] || [ -n "$test_should_fail" ]; then - _log_test_ok "$test_file" - else - _log_test_fail "$test_file" - fi -} - -_log_tests_summary() { - cat <&2 - -====================================================================== -SUMMARY -====================================================================== -OK: $_log_numof_tests_ok -FAIL: $_log_numof_tests_failed -EOF - - local test_file - for test_file in ${_log_tests_failed[@]+"${_log_tests_failed[@]}"}; do - cat <&2 -FAIL: $test_file -EOF - done - - cat <&2 ----------------------------------------------------------------------- -EOF - - local ret=0 - [ "$_log_numof_tests_failed" -gt 0 ] && ret=1 - - return "$ret" -} diff --git a/lib/test.sh b/lib/test.sh deleted file mode 100644 index 7c5844b..0000000 --- a/lib/test.sh +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) 2026 Egor Tensin -# This file is part of the "test-harness" project. -# For details, see https://github.com/egor-tensin/test-harness -# Distributed under the MIT License. - -test_should_fail= -test_root_dir= - -_test_setup_global() { - test_root_dir="$( mktemp -d )" - # mktemp returns /var/..., which is actually in /private/var/... on macOS. - test_root_dir="$( readlink -e -- "$test_root_dir" )" -} - -_test_cleanup_global() { - if [ "$( type -t test_cleanup )" == function ]; then - test_cleanup || true - fi - if [ -n "$test_root_dir" ]; then - log "Removing test's root directory: $test_root_dir" - rm -rf -- "$test_root_dir" - fi -} diff --git a/main.sh b/main.sh deleted file mode 100755 index a6de982..0000000 --- a/main.sh +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2026 Egor Tensin -# This file is part of the "test-harness" project. -# For details, see https://github.com/egor-tensin/test-harness -# Distributed under the MIT License. - -set -o errexit -o nounset -o pipefail -shopt -s inherit_errexit 2> /dev/null || true -shopt -s lastpipe - -_script_dir="$( dirname -- "${BASH_SOURCE[0]}" )" -_script_dir="$( cd -- "$_script_dir" && pwd )" -readonly _script_dir -_script_name="$( basename -- "${BASH_SOURCE[0]}" )" -readonly _script_name - -source "$_script_dir/lib/log.sh" - -_test_run_file() { - local test_file - for test_file; do - source "$_script_dir/lib/test.sh" - - local lib_file - find "$tests_dir/lib" \ - -mindepth 1 \ - -maxdepth 1 \ - -type f \ - -name '*.sh' \ - -print0 \ - | sort -z \ - | \ - while IFS= read -d '' -r lib_file; do - source "$lib_file" - done - - source "$tests_dir/$test_file" - - _log_test_start "$test_file" - - set +e - ( - set -o errexit -o nounset -o pipefail - shopt -s inherit_errexit 2> /dev/null || true - shopt -s lastpipe - - trap _test_cleanup_global EXIT - _test_setup_global - test_run - ) - local ret="$?" - set -e - - _log_test_finish "$test_file" "$ret" - done -} - -_test_usage() { - local msg - for msg; do - echo "$_script_name: $msg" - done - - echo "usage: $_script_name [-h|--help] [-d|--dir DIR] [-f|--filter NAME_PATTERN] - -h,--help show this message and exit - -d,--dir directory with tests - -f,--filter only run tests with names matching the pattern (uses fnmatch(3))" -} - -_test_parse_args() { - tests_dir="$_script_dir/.." - _test_pattern='*' - - while [ "$#" -gt 0 ]; do - local key="$1" - shift - - case "$key" in - -h|--help) - _test_usage - exit 0 - ;; - -d|--dir|-f|--filter) - ;; - *) - _test_usage "unrecognized parameter: $key" >&2 - return 1 - ;; - esac - - if [ "$#" -eq 0 ]; then - _test_usage "missing argument for parameter: $key" >&2 - return 1 - fi - - local value="$1" - shift - - case "$key" in - -d|--dir) - tests_dir="$value" - ;; - -f|--filter) - _test_pattern="$value" - ;; - *) - _test_usage "unrecognized parameter: $key" >&2 - return 1 - ;; - esac - done - - tests_dir="$( realpath -e -- "$tests_dir" )" -} - -_test_main() { - _test_parse_args "$@" - - local -a test_files=() - local test_file - - find "$tests_dir" \ - -mindepth 1 \ - -maxdepth 1 \ - -type f \ - -regex '.*/tst-.*\.sh$' \ - -regextype posix-basic \ - -name "$_test_pattern" \ - -printf '%P\0' \ - | sort -z \ - | \ - while IFS= read -d '' -r test_file; do - test_files+=("$test_file") - done - - _test_run_file ${test_files[@]+"${test_files[@]}"} - _log_tests_summary -} - -_test_main "$@" diff --git a/src/log.sh b/src/log.sh new file mode 100644 index 0000000..db2c498 --- /dev/null +++ b/src/log.sh @@ -0,0 +1,139 @@ +# Copyright (c) 2026 Egor Tensin +# This file is part of the "test-harness" project. +# For details, see https://github.com/egor-tensin/test-harness +# Distributed under the MIT License. + +log() { + local msg + for msg; do + echo -e "$test_file | $msg" >&2 + done +} + +log_run() { + if [ "$#" -lt 1 ]; then + log "usage: ${FUNCNAME[0]} ARGV0 [ARG...]" + return 1 + fi + + local msg='Running:' + msg="$msg$( printf -- ' %q' "$@" )" + log "$msg" +} + +fail() { + local msg + for msg; do + log "FAIL: $msg" + done +} + +fail_details() { + local msg + for msg; do + fail " $msg" + done +} + +_log_numof_tests_ok=0 +_log_numof_tests_failed=0 +declare -a _log_tests_failed=() +declare -A _log_tests_should_fail=() + +_log_test_start() { + if [ "$#" -ne 1 ]; then + echo "usage: ${FUNCNAME[0]} TEST_FILE" >&2 + return 1 + fi + + local test_file="$1" + + if [ -n "${test_should_fail:-}" ]; then + _log_tests_should_fail["$test_file"]=1 + fi + + cat <&2 + +====================================================================== +TEST: $test_file +====================================================================== +EOF + + log "Should fail: ${test_should_fail:-No}" +} + +_log_test_ok() { + if [ "$#" -ne 1 ]; then + echo "usage: ${FUNCNAME[0]} TEST_FILE" >&2 + return 1 + fi + + _log_numof_tests_ok=$((_log_numof_tests_ok + 1)) + + cat <&2 +---------------------------------------------------------------------- +OK: $1 +---------------------------------------------------------------------- +EOF +} + +_log_test_fail() { + if [ "$#" -ne 1 ]; then + echo "usage: ${FUNCNAME[0]} TEST_FILE" >&2 + return 1 + fi + + _log_numof_tests_failed=$((_log_numof_tests_failed + 1)) + _log_tests_failed+=("$1") + + cat <&2 +---------------------------------------------------------------------- +FAIL: $1 +---------------------------------------------------------------------- +EOF +} + +_log_test_finish() { + if [ "$#" -ne 2 ]; then + echo "usage: ${FUNCNAME[0]} TEST_FILE EXIT_CODE" >&2 + return 1 + fi + + local test_file="$1" + local ret="$2" + local test_should_fail + test_should_fail="${_log_tests_should_fail["$test_file"]:+x}" + + if [ "$ret" -eq 0 ] || [ -n "$test_should_fail" ]; then + _log_test_ok "$test_file" + else + _log_test_fail "$test_file" + fi +} + +_log_tests_summary() { + cat <&2 + +====================================================================== +SUMMARY +====================================================================== +OK: $_log_numof_tests_ok +FAIL: $_log_numof_tests_failed +EOF + + local test_file + for test_file in ${_log_tests_failed[@]+"${_log_tests_failed[@]}"}; do + cat <&2 +FAIL: $test_file +EOF + done + + cat <&2 +---------------------------------------------------------------------- +EOF + + local ret=0 + [ "$_log_numof_tests_failed" -gt 0 ] && ret=1 + + return "$ret" +} diff --git a/src/main.sh b/src/main.sh new file mode 100755 index 0000000..ff5aeac --- /dev/null +++ b/src/main.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash + +# Copyright (c) 2026 Egor Tensin +# This file is part of the "test-harness" project. +# For details, see https://github.com/egor-tensin/test-harness +# Distributed under the MIT License. + +set -o errexit -o nounset -o pipefail +shopt -s inherit_errexit 2> /dev/null || true +shopt -s lastpipe + +_script_name="$( basename -- "${BASH_SOURCE[0]}" )" +readonly _script_name +_script_path="$( realpath -e -- "${BASH_SOURCE[0]}" )" +readonly _script_path +_script_dir="$( dirname -- "$_script_path" )" +_script_dir="$( cd -- "$_script_dir" && pwd )" +readonly _script_dir + +source "$_script_dir/log.sh" + +_test_run_file() { + local test_file + for test_file; do + source "$_script_dir/test.sh" + + local lib_file + find "$tests_dir/lib" \ + -mindepth 1 \ + -maxdepth 1 \ + -type f \ + -name '*.sh' \ + -print0 \ + | sort -z \ + | \ + while IFS= read -d '' -r lib_file; do + source "$lib_file" + done + + source "$tests_dir/$test_file" + + _log_test_start "$test_file" + + set +e + ( + set -o errexit -o nounset -o pipefail + shopt -s inherit_errexit 2> /dev/null || true + shopt -s lastpipe + + trap _test_cleanup_global EXIT + _test_setup_global + test_run + ) + local ret="$?" + set -e + + _log_test_finish "$test_file" "$ret" + done +} + +_test_usage() { + local msg + for msg; do + echo "$_script_name: $msg" + done + + echo "usage: $_script_name [-h|--help] [-d|--dir DIR] [-f|--filter NAME_PATTERN] + -h,--help show this message and exit + -d,--dir directory with tests + -f,--filter only run tests with names matching the pattern (uses fnmatch(3))" +} + +_test_parse_args() { + tests_dir="$_script_dir/../.." + _test_pattern='*' + + while [ "$#" -gt 0 ]; do + local key="$1" + shift + + case "$key" in + -h|--help) + _test_usage + exit 0 + ;; + -d|--dir|-f|--filter) + ;; + *) + _test_usage "unrecognized parameter: $key" >&2 + return 1 + ;; + esac + + if [ "$#" -eq 0 ]; then + _test_usage "missing argument for parameter: $key" >&2 + return 1 + fi + + local value="$1" + shift + + case "$key" in + -d|--dir) + tests_dir="$value" + ;; + -f|--filter) + _test_pattern="$value" + ;; + *) + _test_usage "unrecognized parameter: $key" >&2 + return 1 + ;; + esac + done + + tests_dir="$( realpath -e -- "$tests_dir" )" +} + +_test_main() { + _test_parse_args "$@" + + local -a test_files=() + local test_file + + find "$tests_dir" \ + -mindepth 1 \ + -maxdepth 1 \ + -type f \ + -regex '.*/tst-.*\.sh$' \ + -regextype posix-basic \ + -name "$_test_pattern" \ + -printf '%P\0' \ + | sort -z \ + | \ + while IFS= read -d '' -r test_file; do + test_files+=("$test_file") + done + + _test_run_file ${test_files[@]+"${test_files[@]}"} + _log_tests_summary +} + +_test_main "$@" diff --git a/src/test.sh b/src/test.sh new file mode 100644 index 0000000..7c5844b --- /dev/null +++ b/src/test.sh @@ -0,0 +1,23 @@ +# Copyright (c) 2026 Egor Tensin +# This file is part of the "test-harness" project. +# For details, see https://github.com/egor-tensin/test-harness +# Distributed under the MIT License. + +test_should_fail= +test_root_dir= + +_test_setup_global() { + test_root_dir="$( mktemp -d )" + # mktemp returns /var/..., which is actually in /private/var/... on macOS. + test_root_dir="$( readlink -e -- "$test_root_dir" )" +} + +_test_cleanup_global() { + if [ "$( type -t test_cleanup )" == function ]; then + test_cleanup || true + fi + if [ -n "$test_root_dir" ]; then + log "Removing test's root directory: $test_root_dir" + rm -rf -- "$test_root_dir" + fi +} -- cgit v1.3