#!/bin/sh # clitest - Tester for Unix command lines # # Author: Aurelio Jargas (http://aurelio.net) # Created: 2013-07-24 # License: MIT # GitHub: https://github.com/aureliojargas/clitest # # POSIX shell script: # This script is carefully coded to be compatible with POSIX shells. # It is currently tested in bash, dash, ksh, zsh and busybox's sh. # # Exit codes: # 0 All tests passed, or normal operation (--help, --list, ...) # 1 One or more tests have failed # 2 An error occurred (file not found, invalid range, ...) # # Test environment: # By default, the tests will run in the current working directory ($PWD). # You can change to another dir normally using 'cd' inside the test file. # All the tests are executed in the same shell session, using eval. Test # data such as variables and working directory will persist between tests. # # Namespace: # All variables and functions in this script are prefixed by 'tt_' to # avoid clashing with test's variables, functions, aliases and commands. tt_my_name="$(basename "$0")" tt_my_url='https://github.com/aureliojargas/clitest' tt_my_version='0.5.0' # Customization (if needed, edit here or use the command line options) tt_prefix='' tt_prompt='$ ' tt_inline_prefix='#=> ' tt_diff_options='-u' tt_color_mode='auto' # auto, always, never tt_progress='test' # test, number, dot, none # End of customization # --help message, keep it simple, short and informative tt_my_help="\ Usage: $tt_my_name [options] <file ...> Options: -1, --first Stop execution upon first failed test -l, --list List all the tests (no execution) -L, --list-run List all the tests with OK/FAIL status -t, --test RANGE Run specific tests, by number (1,2,4-7) -s, --skip RANGE Skip specific tests, by number (1,2,4-7) --pre-flight COMMAND Execute command before running the first test --post-flight COMMAND Execute command after running the last test -q, --quiet Quiet operation, no output shown -V, --version Show program version and exit Customization options: -P, --progress TYPE Set progress indicator: test, number, dot, none --color WHEN Set when to use colors: auto, always, never --diff-options OPTIONS Set diff command options (default: '$tt_diff_options') --inline-prefix PREFIX Set inline output prefix (default: '$tt_inline_prefix') --prefix PREFIX Set command line prefix (default: '$tt_prefix') --prompt STRING Set prompt string (default: '$tt_prompt') See also: $tt_my_url" # Flags (0=off, 1=on), most can be altered by command line options tt_debug=0 tt_use_colors=0 tt_stop_on_first_fail=0 tt_separator_line_shown=0 # The output mode values are mutually exclusive tt_output_mode='normal' # normal, quiet, list, list-run # Globals (all variables are globals, for better portability) tt_nr_files=0 tt_nr_total_tests=0 tt_nr_total_fails=0 tt_nr_total_skips=0 tt_nr_file_tests=0 tt_nr_file_fails=0 tt_nr_file_skips=0 tt_nr_file_ok=0 tt_files_stats= tt_original_dir=$(pwd) tt_pre_command= tt_post_command= tt_run_range= tt_run_range_data= tt_skip_range= tt_skip_range_data= tt_failed_range= tt_temp_dir= tt_test_file= tt_input_line= tt_line_number=0 tt_test_number=0 tt_test_line_number=0 tt_test_command= tt_test_inline= tt_test_mode= tt_test_status=2 tt_test_output= tt_test_exit_code= tt_test_diff= tt_test_ok_text= tt_missing_nl=0 # Special handy chars tt_tab=' ' tt_nl=' ' ### Utilities # shellcheck disable=SC2317 # Function is reachable (invoked by a trap) tt_clean_up() { test -n "$tt_temp_dir" && rm -rf "$tt_temp_dir" } tt_message() { test "$tt_output_mode" = 'quiet' && return 0 test $tt_missing_nl -eq 1 && echo printf '%s\n' "$*" tt_separator_line_shown=0 tt_missing_nl=0 } tt_message_part() { # no line break test "$tt_output_mode" = 'quiet' && return 0 printf '%s' "$*" tt_separator_line_shown=0 tt_missing_nl=1 } tt_error() { test $tt_missing_nl -eq 1 && echo printf '%s\n' "$tt_my_name: Error: $1" >&2 exit 2 } tt_debug() { # $1=id, $2=contents test $tt_debug -ne 1 && return 0 if test INPUT_LINE = "$1"; then # Original input line is all cyan and preceded by separator line printf -- "${tt_color_cyan}%s${tt_color_off}\n" "$(tt_separator_line)" printf -- "${tt_color_cyan}-- %10s[%s]${tt_color_off}\n" "$1" "$2" else # Highlight tabs and the (last) inline prefix printf -- "${tt_color_cyan}-- %10s[${tt_color_off}%s${tt_color_cyan}]${tt_color_off}\n" "$1" "$2" | sed "/LINE_CMD/ s/\(.*\)\($tt_inline_prefix\)/\1${tt_color_red}\2${tt_color_off}/" | sed "s/$tt_tab/${tt_color_green}<tab>${tt_color_off}/g" fi } tt_separator_line() { printf "%${COLUMNS}s" ' ' | tr ' ' - } tt_list_test() { # $1=normal|list|ok|fail # Show the test command in normal mode, --list and --list-run case "$1" in normal | list) # Normal line, no color, no stamp (--list) tt_message "#${tt_test_number}${tt_tab}${tt_test_command}" ;; ok) # Green line or OK stamp (--list-run) if test $tt_use_colors -eq 1; then tt_message "${tt_color_green}#${tt_test_number}${tt_tab}${tt_test_command}${tt_color_off}" else tt_message "#${tt_test_number}${tt_tab}OK${tt_tab}${tt_test_command}" fi ;; fail) # Red line or FAIL stamp (--list-run) if test $tt_use_colors -eq 1; then tt_message "${tt_color_red}#${tt_test_number}${tt_tab}${tt_test_command}${tt_color_off}" else tt_message "#${tt_test_number}${tt_tab}FAIL${tt_tab}${tt_test_command}" fi ;; esac } tt_parse_range() { # $1=range # Parse numeric ranges and output them in an expanded format # # Supported formats Expanded # ------------------------------------------------------ # Single: 1 :1: # List: 1,3,4,7 :1:3:4:7: # Range: 1-4 :1:2:3:4: # Mixed: 1,3,4-7,11,13-15 :1:3:4:5:6:7:11:13:14:15: # # Reverse ranges and repeated/unordered numbers are ok. # Later we will just grep for :number: in each test. case "$1" in 0 | '') # No range, nothing to do return 0 ;; *[!0-9,-]*) # Error: strange chars, not 0123456789,- return 1 ;; esac # OK, all valid chars in range, let's parse them tt_part= tt_n1= tt_n2= tt_swap= tt_range_data=':' # :1:2:4:7: # Loop each component: a number or a range for tt_part in $(echo "$1" | tr , ' '); do # If there's an hyphen, it's a range case "$tt_part" in *-*) # Error: Invalid range format, must be: number-number echo "$tt_part" | grep '^[0-9][0-9]*-[0-9][0-9]*$' > /dev/null || return 1 tt_n1=${tt_part%-*} tt_n2=${tt_part#*-} # Negative range, let's just reverse it (5-1 => 1-5) if test "$tt_n1" -gt "$tt_n2"; then tt_swap=$tt_n1 tt_n1=$tt_n2 tt_n2=$tt_swap fi # Expand the range (1-4 => 1:2:3:4) tt_part=$tt_n1: while test "$tt_n1" -ne "$tt_n2"; do tt_n1=$((tt_n1 + 1)) tt_part=$tt_part$tt_n1: done tt_part=${tt_part%:} ;; esac # Append the number or expanded range to the holder test "$tt_part" != 0 && tt_range_data=$tt_range_data$tt_part: done test "$tt_range_data" != ':' && echo "$tt_range_data" return 0 } tt_reset_test_data() { tt_test_command= tt_test_inline= tt_test_mode= tt_test_status=2 tt_test_output= tt_test_diff= tt_test_ok_text= } tt_run_test() { tt_test_number=$((tt_test_number + 1)) tt_nr_total_tests=$((tt_nr_total_tests + 1)) tt_nr_file_tests=$((tt_nr_file_tests + 1)) # Run range on: skip this test if it's not listed in $tt_run_range_data if test -n "$tt_run_range_data" && test "$tt_run_range_data" = "${tt_run_range_data#*:"$tt_test_number":}"; then tt_nr_total_skips=$((tt_nr_total_skips + 1)) tt_nr_file_skips=$((tt_nr_file_skips + 1)) tt_reset_test_data return 0 fi # Skip range on: skip this test if it's listed in $tt_skip_range_data # Note: --skip always wins over --test, regardless of order if test -n "$tt_skip_range_data" && test "$tt_skip_range_data" != "${tt_skip_range_data#*:"$tt_test_number":}"; then tt_nr_total_skips=$((tt_nr_total_skips + 1)) tt_nr_file_skips=$((tt_nr_file_skips + 1)) tt_reset_test_data return 0 fi case "$tt_output_mode" in normal) # Normal mode: show progress indicator case "$tt_progress" in test) tt_list_test normal ;; number) tt_message_part "$tt_test_number " ;; none) : ;; *) tt_message_part "$tt_progress" ;; esac ;; list) # List mode: just show the command and return (no execution) tt_list_test list tt_reset_test_data return 0 ;; esac tt_debug EVAL "$tt_test_command" # Execute the test command, saving output (STDOUT and STDERR) eval "$tt_test_command" > "$tt_test_output_file" 2>&1 < /dev/null tt_test_exit_code=$? tt_debug OUTPUT "$(cat "$tt_test_output_file")" # The command output matches the expected output? case $tt_test_mode in output) printf %s "$tt_test_ok_text" > "$tt_test_ok_file" # shellcheck disable=SC2086 # diff options should be split tt_test_diff=$(diff $tt_diff_options "$tt_test_ok_file" "$tt_test_output_file") tt_test_status=$? ;; text) # Inline OK text represents a full line, with \n printf '%s\n' "$tt_test_inline" > "$tt_test_ok_file" # shellcheck disable=SC2086 # diff options should be split tt_test_diff=$(diff $tt_diff_options "$tt_test_ok_file" "$tt_test_output_file") tt_test_status=$? ;; eval) eval "$tt_test_inline" > "$tt_test_ok_file" # shellcheck disable=SC2086 # diff options should be split tt_test_diff=$(diff $tt_diff_options "$tt_test_ok_file" "$tt_test_output_file") tt_test_status=$? ;; lines) tt_test_output=$(sed -n '$=' "$tt_test_output_file") test -z "$tt_test_output" && tt_test_output=0 test "$tt_test_output" -eq "$tt_test_inline" tt_test_status=$? tt_test_diff="Expected $tt_test_inline lines, got $tt_test_output." ;; file) # If path is relative, make it relative to the test file path, not $PWD if test "$tt_test_inline" = "${tt_test_inline#/}"; then tt_test_inline="$(dirname "$tt_test_file")/$tt_test_inline" fi # Abort when ok file not found/readable if test ! -f "$tt_test_inline" || test ! -r "$tt_test_inline"; then tt_error "cannot read inline output file '$tt_test_inline', from line $tt_line_number of $tt_test_file" fi # shellcheck disable=SC2086 # diff options should be split tt_test_diff=$(diff $tt_diff_options "$tt_test_inline" "$tt_test_output_file") tt_test_status=$? ;; egrep) grep -E "$tt_test_inline" "$tt_test_output_file" > /dev/null tt_test_status=$? # Test failed: the regex not matched if test $tt_test_status -eq 1; then tt_test_diff="egrep '$tt_test_inline' failed in:$tt_nl$(cat "$tt_test_output_file")" # Regex errors are common and user must take action to fix them elif test $tt_test_status -gt 1; then tt_error "check your inline egrep regex at line $tt_line_number of $tt_test_file" fi ;; perl | regex) # Escape regex delimiter (if any) inside the regex: ' => \' if test "$tt_test_inline" != "${tt_test_inline#*\'}"; then tt_test_inline=$(printf %s "$tt_test_inline" | sed "s/'/\\\\'/g") fi # Note: -0777 to get the full file contents as a single string perl -0777 -ne "exit(!m'$tt_test_inline')" "$tt_test_output_file" tt_test_status=$? case $tt_test_status in 0) # Test matched, nothing to do : ;; 1) # Test failed: the regex not matched tt_test_diff="Perl regex '$tt_test_inline' not matched in:$tt_nl$(cat "$tt_test_output_file")" ;; 127) # Perl not found :( tt_error "Perl not found. It's needed by --$tt_test_mode at line $tt_line_number of $tt_test_file" ;; 255) # Regex syntax errors are common and user must take action to fix them tt_error "check your inline Perl regex at line $tt_line_number of $tt_test_file" ;; *) tt_error "unknown error when running Perl for --$tt_test_mode at line $tt_line_number of $tt_test_file" ;; esac ;; exit) test "$tt_test_exit_code" -eq "$tt_test_inline" tt_test_status=$? tt_test_diff="Expected exit code $tt_test_inline, got $tt_test_exit_code" ;; *) tt_error "unknown test mode '$tt_test_mode'" ;; esac # Test failed :( if test $tt_test_status -ne 0; then tt_nr_file_fails=$((tt_nr_file_fails + 1)) tt_nr_total_fails=$((tt_nr_total_fails + 1)) tt_failed_range="$tt_failed_range$tt_test_number," # Decide the message format if test "$tt_output_mode" = 'list-run'; then # List mode tt_list_test fail else # Normal mode: show FAILED message and the diff if test $tt_separator_line_shown -eq 0; then # avoid dups tt_message "${tt_color_red}$(tt_separator_line)${tt_color_off}" fi tt_message "${tt_color_red}[FAILED #$tt_test_number, line $tt_test_line_number] $tt_test_command${tt_color_off}" tt_message "$tt_test_diff" | sed '1 { /^--- / { N; /\n+++ /d; }; }' # no ---/+++ headers tt_message "${tt_color_red}$(tt_separator_line)${tt_color_off}" tt_separator_line_shown=1 fi # Should I abort now? if test $tt_stop_on_first_fail -eq 1; then exit 1 fi # Test OK else test "$tt_output_mode" = 'list-run' && tt_list_test ok fi tt_reset_test_data } tt_process_test_file() { # Reset counters tt_nr_file_tests=0 tt_nr_file_fails=0 tt_nr_file_skips=0 tt_line_number=0 tt_test_line_number=0 # Loop for each line of input file # Note: changing IFS to avoid right-trimming of spaces/tabs # Note: read -r to preserve the backslashes while IFS='' read -r tt_input_line || test -n "$tt_input_line"; do tt_line_number=$((tt_line_number + 1)) tt_debug INPUT_LINE "$tt_input_line" case "$tt_input_line" in "$tt_prefix$tt_prompt" | "$tt_prefix${tt_prompt% }" | "$tt_prefix$tt_prompt ") # Prompt alone: closes previous command line (if any) tt_debug 'LINE_$' "$tt_input_line" # Run pending tests test -n "$tt_test_command" && tt_run_test ;; "$tt_prefix$tt_prompt"*) # This line is a command line to be tested tt_debug LINE_CMD "$tt_input_line" # Run pending tests test -n "$tt_test_command" && tt_run_test # Remove the prompt tt_test_command="${tt_input_line#"$tt_prefix$tt_prompt"}" # Save the test's line number for future messages tt_test_line_number=$tt_line_number # This is a special test with inline output? if printf '%s\n' "$tt_test_command" | grep "$tt_inline_prefix" > /dev/null; then # Separate command from inline output tt_test_command="${tt_test_command%"$tt_inline_prefix"*}" tt_test_inline="${tt_input_line##*"$tt_inline_prefix"}" tt_debug NEW_CMD "$tt_test_command" tt_debug OK_INLINE "$tt_test_inline" # Maybe the OK text has options? case "$tt_test_inline" in '--egrep '*) tt_test_inline=${tt_test_inline#--egrep } tt_test_mode='egrep' ;; '--regex '*) # alias to --perl tt_test_inline=${tt_test_inline#--regex } tt_test_mode='regex' ;; '--perl '*) tt_test_inline=${tt_test_inline#--perl } tt_test_mode='perl' ;; '--file '*) tt_test_inline=${tt_test_inline#--file } tt_test_mode='file' ;; '--lines '*) tt_test_inline=${tt_test_inline#--lines } tt_test_mode='lines' ;; '--exit '*) tt_test_inline=${tt_test_inline#--exit } tt_test_mode='exit' ;; '--eval '*) tt_test_inline=${tt_test_inline#--eval } tt_test_mode='eval' ;; '--text '*) tt_test_inline=${tt_test_inline#--text } tt_test_mode='text' ;; *) tt_test_mode='text' ;; esac tt_debug OK_TEXT "$tt_test_inline" # There must be a number in --lines and --exit if test "$tt_test_mode" = 'lines' || test "$tt_test_mode" = 'exit'; then case "$tt_test_inline" in '' | *[!0-9]*) tt_error "--$tt_test_mode requires a number. See line $tt_line_number of $tt_test_file" ;; esac fi # An empty inline parameter is an error user must see if test -z "$tt_test_inline" && test "$tt_test_mode" != 'text'; then tt_error "empty --$tt_test_mode at line $tt_line_number of $tt_test_file" fi # Since we already have the command and the output, run test tt_run_test else # It's a normal command line, output begins in next line tt_test_mode='output' tt_debug NEW_CMD "$tt_test_command" fi ;; *) # Test output, blank line or comment tt_debug 'LINE_MISC' "$tt_input_line" # Ignore this line if there's no pending test test -n "$tt_test_command" || continue # Required prefix is missing: we just left a command block if test -n "$tt_prefix" && test "${tt_input_line#"$tt_prefix"}" = "$tt_input_line"; then tt_debug BLOCK_OUT "$tt_input_line" # Run the pending test and we're done in this line tt_run_test continue fi # This line is a test output, save it (without prefix) tt_test_ok_text="$tt_test_ok_text${tt_input_line#"$tt_prefix"}$tt_nl" tt_debug OK_TEXT "${tt_input_line#"$tt_prefix"}" ;; esac done < "$tt_temp_file" tt_debug LOOP_OUT "\$tt_test_command=$tt_test_command" # Run pending tests test -n "$tt_test_command" && tt_run_test } tt_make_temp_dir() { # Create private temporary dir and sets global $tt_temp_dir. # http://mywiki.wooledge.org/BashFAQ/062 # Prefer mktemp when available tt_temp_dir=$(mktemp -d "${TMPDIR:-/tmp}/clitest.XXXXXX" 2> /dev/null) && return 0 # No mktemp, let's create the dir manually # shellcheck disable=SC2015 tt_temp_dir="${TMPDIR:-/tmp}/clitest.$(awk 'BEGIN { srand(); print rand() }').$$" && mkdir -m 700 "$tt_temp_dir" || tt_error "cannot create temporary dir: $tt_temp_dir" } ### Init process # Handle command line options while test "${1#-}" != "$1"; do case "$1" in -1 | --first) shift tt_stop_on_first_fail=1 ;; -l | --list) shift tt_output_mode='list' ;; -L | --list-run) shift tt_output_mode='list-run' ;; -q | --quiet) shift tt_output_mode='quiet' ;; -t | --test) shift tt_run_range="$1" shift ;; -s | --skip) shift tt_skip_range="$1" shift ;; --pre-flight) shift tt_pre_command="$1" shift ;; --post-flight) shift tt_post_command="$1" shift ;; -P | --progress) shift tt_progress="$1" tt_output_mode='normal' shift ;; --color | --colour) shift tt_color_mode="$1" shift ;; --diff-options) shift tt_diff_options="$1" shift ;; --inline-prefix) shift tt_inline_prefix="$1" shift ;; --prefix) shift tt_prefix="$1" shift ;; --prompt) shift tt_prompt="$1" shift ;; -h | --help) printf '%s\n' "$tt_my_help" exit 0 ;; -V | --version) printf '%s %s\n' "$tt_my_name" "$tt_my_version" exit 0 ;; --debug) # Undocumented dev-only option shift tt_debug=1 ;; --) # No more options to process shift break ;; -) # Argument - means "read test file from STDIN" break ;; *) tt_error "invalid option $1" ;; esac done # Command line options consumed, now it's just the files tt_nr_files=$# # No files? if test $tt_nr_files -eq 0; then tt_error 'no test file informed (try --help)' fi # Handy shortcuts for prefixes case "$tt_prefix" in tab) tt_prefix="$tt_tab" ;; 0) tt_prefix='' ;; [1-9] | [1-9][0-9]) # 1-99 # convert number to spaces: 2 => ' ' tt_prefix=$(printf "%${tt_prefix}s" ' ') ;; *\\*) tt_prefix="$(printf %b "$tt_prefix")" # expand \t and others ;; esac # Validate and normalize progress value if test "$tt_output_mode" = 'normal'; then case "$tt_progress" in test) : ;; number | n | [0-9]) tt_progress='number' ;; dot | .) tt_progress='.' ;; none | no) tt_progress='none' ;; ?) # Single char, use it as the progress : ;; *) tt_error "invalid value '$tt_progress' for --progress. Use: test, number, dot or none." ;; esac fi # Will we use colors in the output? case "$tt_color_mode" in always | yes | y) tt_use_colors=1 ;; never | no | n) tt_use_colors=0 ;; auto | a) # The auto mode will use colors if the output is a terminal # Note: test -t is in POSIX if test -t 1; then tt_use_colors=1 else tt_use_colors=0 fi ;; *) tt_error "invalid value '$tt_color_mode' for --color. Use: auto, always or never." ;; esac # Set colors # Remember: colors must be readable in dark and light backgrounds # Customization: tweak the numbers after [ to adjust the colors if test $tt_use_colors -eq 1; then tt_color_red=$( printf '\033[31m') # fail tt_color_green=$(printf '\033[32m') # ok tt_color_cyan=$( printf '\033[36m') # debug tt_color_off=$( printf '\033[m') fi # Find the terminal width # The COLUMNS env var is set by Bash (must be exported in ~/.bashrc). # In other shells, try to use 'tput cols' (not POSIX). # If not, defaults to 50 columns, a conservative amount. : ${COLUMNS:=$(tput cols 2> /dev/null)} : "${COLUMNS:=50}" # Parse and validate --test option value, if informed tt_run_range_data=$(tt_parse_range "$tt_run_range") if test $? -ne 0; then tt_error "invalid argument for -t or --test: $tt_run_range" fi # Parse and validate --skip option value, if informed tt_skip_range_data=$(tt_parse_range "$tt_skip_range") if test $? -ne 0; then tt_error "invalid argument for -s or --skip: $tt_skip_range" fi ### Real execution begins here trap tt_clean_up EXIT # Temporary files (using files because <(...) is not portable) tt_make_temp_dir # sets global $tt_temp_dir tt_temp_file="$tt_temp_dir/temp.txt" tt_stdin_file="$tt_temp_dir/stdin.txt" tt_test_ok_file="$tt_temp_dir/ok.txt" tt_test_output_file="$tt_temp_dir/output.txt" # Some preparing command to run before all the tests? if test -n "$tt_pre_command"; then eval "$tt_pre_command" || tt_error "pre-flight command failed with status=$?: $tt_pre_command" fi # For each input file in $@ for tt_test_file; do # Some tests may 'cd' to another dir, we need to get back # to preserve the relative paths of the input files cd "$tt_original_dir" || tt_error "cannot enter starting directory $tt_original_dir" # Support using '-' to read the test file from STDIN if test "$tt_test_file" = '-'; then tt_test_file="$tt_stdin_file" cat > "$tt_test_file" fi # Abort when test file is a directory if test -d "$tt_test_file"; then tt_error "input file is a directory: $tt_test_file" fi # Abort when test file not found/readable if test ! -r "$tt_test_file"; then tt_error "cannot read input file: $tt_test_file" fi # In multifile mode, identify the current file if test $tt_nr_files -gt 1; then case "$tt_output_mode" in normal) # Normal mode, show message with filename case "$tt_progress" in test | none) tt_message "Testing file $tt_test_file" ;; *) test $tt_missing_nl -eq 1 && echo tt_message_part "Testing file $tt_test_file " ;; esac ;; list | list-run) # List mode, show ------ and the filename tt_message "$(tt_separator_line | cut -c 1-40)" "$tt_test_file" ;; esac fi # Convert Windows files (CRLF) to the Unix format (LF) # Note: the temporary file is required, because doing "sed | while" opens # a subshell and global vars won't be updated outside the loop. sed "s/$(printf '\r')$//" "$tt_test_file" > "$tt_temp_file" # The magic happens here tt_process_test_file # Abort when no test found (and no active range with --test or --skip) if test $tt_nr_file_tests -eq 0 && test -z "$tt_run_range_data" && test -z "$tt_skip_range_data"; then tt_error "no test found in input file: $tt_test_file" fi # Save file stats tt_nr_file_ok=$((tt_nr_file_tests - tt_nr_file_fails - tt_nr_file_skips)) tt_files_stats="$tt_files_stats$tt_nr_file_ok $tt_nr_file_fails $tt_nr_file_skips$tt_nl" # Dots mode: any missing new line? # Note: had to force tt_missing_nl=0, even when it's done in tt_message :/ test $tt_missing_nl -eq 1 && tt_missing_nl=0 && tt_message done # Some clean up command to run after all the tests? if test -n "$tt_post_command"; then eval "$tt_post_command" || tt_error "post-flight command failed with status=$?: $tt_post_command" fi #----------------------------------------------------------------------- # From this point on, it's safe to use non-prefixed global vars #----------------------------------------------------------------------- # Range active, but no test matched :( if test $tt_nr_total_tests -eq $tt_nr_total_skips; then if test -n "$tt_run_range_data" && test -n "$tt_skip_range_data"; then tt_error "no test found. The combination of -t and -s resulted in no tests." elif test -n "$tt_run_range_data"; then tt_error "no test found for the specified number or range '$tt_run_range'" elif test -n "$tt_skip_range_data"; then tt_error "no test found. Maybe '--skip $tt_skip_range' was too much?" fi fi # List mode has no stats if test "$tt_output_mode" = 'list' || test "$tt_output_mode" = 'list-run'; then if test $tt_nr_total_fails -eq 0; then exit 0 else exit 1 fi fi # Show stats # Data: # $tt_files_stats -> "100 0 23 \n 12 34 0" # $@ -> foo.sh bar.sh # Output: # ok fail skip # 100 0 23 foo.sh # 12 34 0 bar.sh if test $tt_nr_files -gt 1 && test "$tt_output_mode" != 'quiet'; then echo printf ' %5s %5s %5s\n' ok fail skip printf %s "$tt_files_stats" | while IFS=' ' read -r ok fail skip; do printf ' %5s %5s %5s %s\n' "$ok" "$fail" "$skip" "$1" shift done | sed 's/ 0/ -/g' # hide zeros echo fi # The final message: OK or FAIL? # OK: 123 of 123 tests passed # OK: 100 of 123 tests passed (23 skipped) # FAIL: 123 of 123 tests failed # FAIL: 100 of 123 tests failed (23 skipped) skips= if test $tt_nr_total_skips -gt 0; then skips=" ($tt_nr_total_skips skipped)" fi if test $tt_nr_total_fails -eq 0; then stamp="${tt_color_green}OK:${tt_color_off}" stats="$((tt_nr_total_tests - tt_nr_total_skips)) of $tt_nr_total_tests tests passed" test $tt_nr_total_tests -eq 1 && stats=$(echo "$stats" | sed 's/tests /test /') tt_message "$stamp $stats$skips" exit 0 else test $tt_nr_files -eq 1 && tt_message # separate from previous FAILED message stamp="${tt_color_red}FAIL:${tt_color_off}" stats="$tt_nr_total_fails of $tt_nr_total_tests tests failed" test $tt_nr_total_tests -eq 1 && stats=$(echo "$stats" | sed 's/tests /test /') tt_message "$stamp $stats$skips" # test $tt_test_file = 'test.md' && tt_message "-t ${tt_failed_range%,}" # dev helper exit 1 fi