@@ -442,6 +442,7 @@ _DEFAULT_STOP_ON_FAILURE="false"
442442_DEFAULT_SHOW_EXECUTION_TIME=" true"
443443_DEFAULT_VERBOSE=" false"
444444_DEFAULT_BENCH_MODE=" false"
445+ _DEFAULT_NO_OUTPUT=" false"
445446
446447: " ${BASHUNIT_PARALLEL_RUN:= ${PARALLEL_RUN:= $_DEFAULT_PARALLEL_RUN } } "
447448: " ${BASHUNIT_SHOW_HEADER:= ${SHOW_HEADER:= $_DEFAULT_SHOW_HEADER } } "
@@ -451,6 +452,7 @@ _DEFAULT_BENCH_MODE="false"
451452: " ${BASHUNIT_SHOW_EXECUTION_TIME:= ${SHOW_EXECUTION_TIME:= $_DEFAULT_SHOW_EXECUTION_TIME } } "
452453: " ${BASHUNIT_VERBOSE:= ${VERBOSE:= $_DEFAULT_VERBOSE } } "
453454: " ${BASHUNIT_BENCH_MODE:= ${BENCH_MODE:= $_DEFAULT_BENCH_MODE } } "
455+ : " ${BASHUNIT_NO_OUTPUT:= ${NO_OUTPUT:= $_DEFAULT_NO_OUTPUT } } "
454456
455457function env::is_parallel_run_enabled() {
456458 [[ " $BASHUNIT_PARALLEL_RUN " == " true" ]]
@@ -488,6 +490,10 @@ function env::is_bench_mode_enabled() {
488490 [[ " $BASHUNIT_BENCH_MODE " == " true" ]]
489491}
490492
493+ function env::is_no_output_enabled() {
494+ [[ " $BASHUNIT_NO_OUTPUT " == " true" ]]
495+ }
496+
491497function env::active_internet_connection() {
492498 if ping -c 1 -W 3 google.com & > /dev/null; then
493499 return 0
@@ -499,10 +505,11 @@ function env::active_internet_connection() {
499505function env::find_terminal_width() {
500506 local cols=" "
501507
502- if [[ -z " $cols " ]] && command -v stty > /dev/null; then
508+ if [[ -z " $cols " ]] && command -v tput > /dev/null; then
503509 cols=$( tput cols 2> /dev/null)
504510 fi
505- if [[ -n " $TERM " ]] && command -v tput > /dev/null; then
511+
512+ if [[ -z " $cols " ]] && command -v stty > /dev/null; then
506513 cols=$( stty size 2> /dev/null | cut -d' ' -f2)
507514 fi
508515
@@ -550,69 +557,107 @@ CAT="$(which cat)"
550557
551558# clock.sh
552559
553- function clock::now() {
560+ _CLOCK_NOW_IMPL=" "
561+
562+ function clock::_choose_impl() {
554563 local shell_time
555564 local attempts=()
556565
557- # 1. Try using native shell EPOCHREALTIME (if available)
558- attempts+=(" EPOCHREALTIME" )
559- if shell_time=" $( clock::shell_time) " ; then
560- local seconds=" ${shell_time%% .* } "
561- local microseconds=" ${shell_time#* .} "
562- math::calculate " ($seconds * 1000000000) + ($microseconds * 1000)"
563- return 0
564- fi
565-
566- # 2. Try Perl with Time::HiRes
566+ # 1. Try Perl with Time::HiRes
567567 attempts+=(" Perl" )
568568 if dependencies::has_perl && perl -MTime::HiRes -e " " & > /dev/null; then
569- perl -MTime::HiRes -e ' printf("%.0f\n", Time::HiRes::time() * 1000000000)' && return 0
569+ _CLOCK_NOW_IMPL=" perl"
570+ return 0
570571 fi
571572
572- # 3 . Try Python 3 with time module
573+ # 2 . Try Python 3 with time module
573574 attempts+=(" Python" )
574575 if dependencies::has_python; then
575- python - << 'EOF '
576- import time, sys
577- sys.stdout.write(str(int(time.time() * 1_000_000_000)))
578- EOF
576+ _CLOCK_NOW_IMPL=" python"
579577 return 0
580578 fi
581579
582- # 4 . Try Node.js
580+ # 3 . Try Node.js
583581 attempts+=(" Node" )
584582 if dependencies::has_node; then
585- node -e ' process.stdout.write((BigInt(Date.now()) * 1000000n).toString())' && return 0
583+ _CLOCK_NOW_IMPL=" node"
584+ return 0
586585 fi
587- # 5 . Windows fallback with PowerShell
586+ # 4 . Windows fallback with PowerShell
588587 attempts+=(" PowerShell" )
589588 if check_os::is_windows && dependencies::has_powershell; then
590- powershell -Command "
591- \$ unixEpoch = [DateTime]'1970-01-01 00:00:00';
592- \$ now = [DateTime]::UtcNow;
593- \$ ticksSinceEpoch = (\$ now - \$ unixEpoch).Ticks;
594- \$ nanosecondsSinceEpoch = \$ ticksSinceEpoch * 100;
595- Write-Output \$ nanosecondsSinceEpoch
596- " && return 0
589+ _CLOCK_NOW_IMPL=" powershell"
590+ return 0
597591 fi
598592
599- # 6 . Unix fallback using `date +%s%N` (if not macOS or Alpine)
593+ # 5 . Unix fallback using `date +%s%N` (if not macOS or Alpine)
600594 attempts+=(" date" )
601595 if ! check_os::is_macos && ! check_os::is_alpine; then
602596 local result
603597 result=$( date +%s%N 2> /dev/null)
604598 if [[ " $result " != * N && " $result " =~ ^[0-9]+$ ]]; then
605- echo " $result "
599+ _CLOCK_NOW_IMPL= " date "
606600 return 0
607601 fi
608602 fi
609603
604+ # 6. Try using native shell EPOCHREALTIME (if available)
605+ attempts+=(" EPOCHREALTIME" )
606+ if shell_time=" $( clock::shell_time) " ; then
607+ _CLOCK_NOW_IMPL=" shell"
608+ return 0
609+ fi
610+
610611 # 7. All methods failed
611612 printf " clock::now implementations tried: %s\n" " ${attempts[*]} " >&2
612613 echo " "
613614 return 1
614615}
615616
617+ function clock::now() {
618+ if [[ -z " $_CLOCK_NOW_IMPL " ]]; then
619+ clock::_choose_impl || return 1
620+ fi
621+
622+ case " $_CLOCK_NOW_IMPL " in
623+ perl)
624+ perl -MTime::HiRes -e ' printf("%.0f\n", Time::HiRes::time() * 1000000000)'
625+ ;;
626+ python)
627+ python - << 'EOF '
628+ import time, sys
629+ sys.stdout.write(str(int(time.time() * 1_000_000_000)))
630+ EOF
631+ ;;
632+ node)
633+ node -e ' process.stdout.write((BigInt(Date.now()) * 1000000n).toString())'
634+ ;;
635+ powershell)
636+ powershell -Command " \
637+ \$ unixEpoch = [DateTime]'1970-01-01 00:00:00';\
638+ \$ now = [DateTime]::UtcNow;\
639+ \$ ticksSinceEpoch = (\$ now - \$ unixEpoch).Ticks;\
640+ \$ nanosecondsSinceEpoch = \$ ticksSinceEpoch * 100;\
641+ Write-Output \$ nanosecondsSinceEpoch\
642+ "
643+ ;;
644+ date)
645+ date +%s%N
646+ ;;
647+ shell)
648+ # shellcheck disable=SC2155
649+ local shell_time=" $( clock::shell_time) "
650+ local seconds=" ${shell_time%% .* } "
651+ local microseconds=" ${shell_time#* .} "
652+ math::calculate " ($seconds * 1000000000) + ($microseconds * 1000)"
653+ ;;
654+ * )
655+ clock::_choose_impl || return 1
656+ clock::now
657+ ;;
658+ esac
659+ }
660+
616661function clock::shell_time() {
617662 # Get time directly from the shell variable EPOCHREALTIME (Bash 5+)
618663 [[ -n ${EPOCHREALTIME+x} && -n " $EPOCHREALTIME " ]] && LC_ALL=C echo " $EPOCHREALTIME "
@@ -954,55 +999,63 @@ EOF
954999
9551000function console_header::print_help() {
9561001 cat << EOF
957- bashunit [arguments] [options]
1002+ Usage:
1003+ bashunit [PATH] [OPTIONS]
9581004
9591005Arguments:
960- Specifies the directory or file containing the tests to run.
961- If a directory is specified, it will execute the tests within files ending with test.sh.
962- If you use wildcards, bashunit will run any tests it finds.
1006+ PATH File or directory containing tests.
1007+ - Directories: runs all '*test.sh' files.
1008+ - Wildcards: supported to match multiple test files.
1009+ - Default search path is 'tests'
9631010
9641011Options:
965- -a, --assert <function ... args>
966- Run a core assert function standalone without a test context.
1012+ -a, --assert <function args>
1013+ Run a core assert function standalone (outside test context) .
9671014
968- -e, --env, --boot <file-path>
969- Load a custom file, overriding the existing .env variables or loading a file with global functions.
1015+ -b, --bench [file]
1016+ Run benchmark functions from file or '*.bench.sh' under
1017+ BASHUNIT_DEFAULT_PATH when no file is provided.
9701018
971- -f, --filter <filter>
972- Filters the tests to run based on the test name .
1019+ --debug [file]
1020+ Enable shell debug mode. Logs to file if provided .
9731021
974- -l , --log-junit <out.xml >
975- Create a report JUnit XML file that contains information about the test results .
1022+ -e , --env, --boot <file >
1023+ Load a custom env/bootstrap file to override .env or define globals .
9761024
977- -p , --parallel || --no-parallel [default]
978- Run each test in child process, randomizing the tests execution order .
1025+ -f , --filter <name>
1026+ Only run tests matching the given name .
9791027
980- -r , --report-html <out.html>
981- Create a report HTML file that contains information about the test results .
1028+ -h , --help
1029+ Show this help message .
9821030
983- -s, --simple || --detailed [default ]
984- Enables simple or detailed output to the console .
1031+ --init [dir ]
1032+ Generate a sample test suite in current or specified directory .
9851033
986- -S , --stop-on-failure
987- Force to stop the runner right after encountering one failing test.
1034+ -l , --log-junit <file>
1035+ Write test results as JUnit XML report .
9881036
989- --debug <?file-path>
990- Print all executed shell commands to the terminal.
991- If a file-path is passed, it will redirect the output to that file.
1037+ -p, --parallel | --no-parallel
1038+ Run tests in parallel (default: enabled). Random execution order.
9921039
993- -vvv , --verbose
994- Display internal details for each test.
1040+ -r , --report-html <file>
1041+ Write test results as an HTML report .
9951042
996- --version
997- Displays the current version of bashunit.
1043+ -s, --simple | --detailed
1044+ Choose console output style (default: detailed).
1045+
1046+ -S, --stop-on-failure
1047+ Stop execution immediately on the first failing test.
9981048
9991049 --upgrade
1000- Upgrade to latest version of bashunit .
1050+ Upgrade bashunit to the latest version.
10011051
1002- -h , --help
1003- This message .
1052+ -vvv , --verbose
1053+ Show internal execution details per test .
10041054
1005- See more: https://bashunit.typeddevs.com/command-line
1055+ --version
1056+ Display the current version of bashunit.
1057+
1058+ More info: https://bashunit.typeddevs.com/command-line
10061059EOF
10071060}
10081061
@@ -3313,6 +3366,54 @@ function runner::clean_set_up_and_tear_down_after_script() {
33133366 helper::unset_if_exists ' tear_down_after_script'
33143367}
33153368
3369+ # init.sh
3370+
3371+ function init::project() {
3372+ local tests_dir=" ${1:- $BASHUNIT_DEFAULT_PATH } "
3373+ mkdir -p " $tests_dir "
3374+
3375+ local bootstrap_file=" $tests_dir /bootstrap.sh"
3376+ if [[ ! -f " $bootstrap_file " ]]; then
3377+ cat > " $bootstrap_file " << 'SH '
3378+ #!/usr/bin/env bash
3379+ set -euo pipefail
3380+ # Place your common test setup here
3381+ SH
3382+ chmod +x " $bootstrap_file "
3383+ echo " > Created $bootstrap_file "
3384+ fi
3385+
3386+ local example_test=" $tests_dir /example_test.sh"
3387+ if [[ ! -f " $example_test " ]]; then
3388+ cat > " $example_test " << 'SH '
3389+ #!/usr/bin/env bash
3390+
3391+ function test_bashunit_is_installed() {
3392+ assert_same "bashunit is installed" "bashunit is installed"
3393+ }
3394+ SH
3395+ chmod +x " $example_test "
3396+ echo " > Created $example_test "
3397+ fi
3398+
3399+ local env_file=" .env"
3400+ local env_line=" BASHUNIT_BOOTSTRAP=$bootstrap_file "
3401+ if [[ -f " $env_file " ]]; then
3402+ if grep -q " ^BASHUNIT_BOOTSTRAP=" " $env_file " ; then
3403+ if check_os::is_macos; then
3404+ sed -i ' ' -e " s/^BASHUNIT_BOOTSTRAP=/#&/" " $env_file "
3405+ else
3406+ sed -i -e " s/^BASHUNIT_BOOTSTRAP=/#&/" " $env_file "
3407+ fi
3408+ fi
3409+ echo " $env_line " >> " $env_file "
3410+ else
3411+ echo " $env_line " > " $env_file "
3412+ fi
3413+
3414+ echo " > bashunit initialized in $tests_dir "
3415+ }
3416+
33163417# bashunit.sh
33173418
33183419# This file provides a facade to developers who wants
@@ -3529,7 +3630,7 @@ function main::handle_assert_exit_code() {
35293630set -euo pipefail
35303631
35313632# shellcheck disable=SC2034
3532- declare -r BASHUNIT_VERSION=" 0.21 .0"
3633+ declare -r BASHUNIT_VERSION=" 0.22 .0"
35333634
35343635# shellcheck disable=SC2155
35353636declare -r BASHUNIT_ROOT_DIR=" $( dirname " ${BASH_SOURCE[0]} " ) "
@@ -3596,6 +3697,9 @@ while [[ $# -gt 0 ]]; do
35963697 export BASHUNIT_REPORT_HTML=" $2 "
35973698 shift
35983699 ;;
3700+ --no-output)
3701+ export BASHUNIT_NO_OUTPUT=true
3702+ ;;
35993703 -vvv|--verbose)
36003704 export BASHUNIT_VERBOSE=true
36013705 ;;
@@ -3607,6 +3711,15 @@ while [[ $# -gt 0 ]]; do
36073711 upgrade::upgrade
36083712 trap ' ' EXIT && exit 0
36093713 ;;
3714+ --init)
3715+ if [[ -n ${2:- } && ${2: 0: 1} != " -" ]]; then
3716+ init::project " $2 "
3717+ shift
3718+ else
3719+ init::project
3720+ fi
3721+ trap ' ' EXIT && exit 0
3722+ ;;
36103723 -h|--help)
36113724 console_header::print_help
36123725 trap ' ' EXIT && exit 0
@@ -3633,6 +3746,10 @@ fi
36333746# shellcheck disable=SC1090
36343747[[ -f " ${BASHUNIT_BOOTSTRAP:- } " ]] && source " $BASHUNIT_BOOTSTRAP "
36353748
3749+ if [[ " ${BASHUNIT_NO_OUTPUT:- false} " == true ]]; then
3750+ exec > /dev/null 2>&1
3751+ fi
3752+
36363753set +eu
36373754
36383755# ################
0 commit comments