#!/usr/bin/env bash # CONSTANTS TMP_DIRECTORY="/tmp/$(basename "$0")-$UID" TMP_OUT="$TMP_DIRECTORY/_stdout" TMP_ERR="$TMP_DIRECTORY/_stderr" TMP_MIAOU="$TMP_DIRECTORY/_miaou" readonly TMP_DIRECTORY TMP_OUT TMP_ERR TMP_MIAOU source "$MIAOU_BASH_DIR"/lib/ansi.bash # ANSI associative array # functions function ansi_code { local code for code in "$@"; do if [[ -v ANSI[$code] ]]; then echo -ne "\e[${ANSI[$code]}m" else >&2 echo "unknown ansi key: <$code>" break fi done } function print_passed { ansi_code FG_GREEN echo -en '.' ansi_code RESET } function print_bypassed { ansi_code FG_YELLOW echo -en 'b' ansi_code RESET } function print_failed { ansi_code FG_RED echo -en 'F' ansi_code RESET } function assert_tty { ((test_count++)) if [[ -w /dev/tty ]]; then print_passed return fi print_failed failures+=("test -w /dev/tty") return 1 } function assert { ((test_count++)) cmd="$MIAOU_BASH_DIR/test/stub/scenario.bash $1" if $cmd >/dev/null 2>/dev/null 3>"$TMP_MIAOU"; then if [[ -s "$TMP_MIAOU" ]]; then print_passed return fi fi print_failed failures+=("$cmd") return 1 } # assert performing '$1:script' successfully + output must contain '$2:content' # in case of extra args (4th position or more) must be provided after -- (3rd position) # 1: script # 2: content # 3: -- # 4..: extra args function assert_grep { ((test_count++)) local script="$1" content="$2" extra_args=() [[ $# -gt 3 ]] && shift 3 && extra_args=("$@") cmd="$MIAOU_BASH_DIR/test/stub/scenario.bash $script" if $cmd "${extra_args[@]}" >"$TMP_OUT" 2>/dev/null 3>/dev/null; then if grep -q "$content" "$TMP_OUT"; then print_passed return fi fi print_failed #TODO: quote extra_args for i in "${extra_args[@]}"; do safe_args+=$(printf '%q ' "$i"); done failures+=("$cmd $safe_args") return 1 } function fail_type { ((test_count++)) local success regex hint_message cmd lang_options success=false hint_message="${3:-}" lang_options=() cmd="$MIAOU_BASH_DIR/test/stub/scenario.bash $1" if [[ -v TEST_LANG ]]; then local locale_found=false for gen_locale in "${GEN_LOCALES[@]}"; do if [[ "$gen_locale" == "$TEST_LANG" ]]; then locale_found=true break fi done if ! $locale_found; then ((test_bypassed++)) BYPASSED_LOCALES["$TEST_LANG"]=true print_bypassed return fi fi eval "${lang_options[*]} $cmd" >/dev/null 2>"$TMP_ERR" 3>"$TMP_MIAOU" mapfile -t stderr <"$TMP_ERR" last_error="${stderr[-1]}" regex="^(.*): line ([0-9]+): miaou_error: $2 ([0-9]+)" [[ "${last_error}" =~ $regex ]] && success=true if [[ $success == true && -n "${hint_message}" ]]; then grep -q "${hint_message}" "$TMP_MIAOU" || success=false fi if [[ $success == true ]]; then print_passed return fi print_failed failures+=("${lang_options[*]} $cmd") return 1 } function fail_stderr_grep { ((test_count++)) cmd="$MIAOU_BASH_DIR/test/stub/scenario.bash $1" stderr_line="$2" if ! $cmd 2>"$TMP_ERR" && grep -q "$stderr_line" "$TMP_ERR"; then print_passed return fi print_failed failures+=("$cmd 2>&1 | grep -q \"$stderr_line\"") return 1 } function fail_stderr_count { ((test_count++)) local regex count="$2" cmd="$MIAOU_BASH_DIR/test/stub/scenario.bash $1" if ! $cmd >/dev/null 2>"$TMP_ERR" 3>/dev/null; then mapfile -t stderr <"$TMP_ERR" if [[ "${#stderr[@]}" == "$count" ]]; then print_passed return fi fi print_failed failures+=("$cmd 2>$TMP_ERR") return 1 } function fail_miaou_count { ((test_count++)) local regex count="$2" cmd="$MIAOU_BASH_DIR/test/stub/scenario.bash $1" if ! $cmd >/dev/null 2>/dev/null 3>"$TMP_MIAOU"; then mapfile -t miaou <"$TMP_MIAOU" if [[ "${#miaou[@]}" == "$count" ]]; then print_passed return fi fi print_failed failures+=("$cmd 3>$TMP_MIAOU") return 1 } function assert_locales_equally_populated { ((test_count++)) local locales_dir="$MIAOU_BASH_DIR"/locales local main_locale_file="$locales_dir"/"$1".sh if [[ ! -f $main_locale_file ]]; then print_failed failures+=("missing locale file: $main_locale_file for locale: $1") return 1 fi shift if [[ $# -gt 0 ]]; then local extra_locale_file difference variable_name for extra in "$@"; do extra_locale_file="$locales_dir"/"$extra".sh if [[ ! -f $extra_locale_file ]]; then print_failed failures+=("missing locale file: $extra_locale_file for locale: $extra") return 1 fi mapfile -t difference < <( diff <(grep -oP '^\s*[A-Za-z_][A-Za-z0-9_]*' "$main_locale_file" | sort) \ <(grep -oP '^\s*[A-Za-z_][A-Za-z0-9_]*' "$extra_locale_file" | sort) \ 2>&1 | head -n2 ) if [[ ${#difference[@]} -gt 0 ]]; then variable_name=$(echo "${difference[1]}" | cut -d' ' -f2) change_type="${difference[0]}" print_failed if [[ "$change_type" =~ d ]]; then failures+=("locale=$extra (in $extra_locale_file) misses variable (to add): ${variable_name}") else failures+=("locale=$extra (in $extra_locale_file) useless variable (to remove): ${variable_name}") fi return 1 fi done fi print_passed } # main set -uo pipefail [[ ! -v MIAOU_BASH_DIR ]] && >&2 echo MIAOU_BASH_DIR missing && builtin exit 1 test_count=0 test_bypassed=0 failures=() declare -A BYPASSED_LOCALES=() mapfile -t GEN_LOCALES < <(locale -a) mkdir -p "$TMP_DIRECTORY" TIMEFORMAT=$'\nelapsed: %Rs' time { assert_tty assert success assert exit_0 assert return_conditional assert_grep success_with_args 'count args: 2' -- 'A B' C assert heredoc assert pipe1 assert pipe2 TEST_LANG=en_US.utf8 fail_type unbound_variable UNBOUND_VARIABLE TEST_LANG=es_ES.utf8 fail_type unbound_variable UNBOUND_VARIABLE TEST_LANG=fr_FR.utf8 fail_type unbound_variable UNBOUND_VARIABLE TEST_LANG=en_US.utf8 fail_type unbound_associative UNBOUND_VARIABLE TEST_LANG=es_ES.utf8 fail_type unbound_associative UNBOUND_VARIABLE TEST_LANG=fr_FR.utf8 fail_type unbound_associative UNBOUND_VARIABLE TEST_LANG=en_US.utf8 fail_type command_not_found COMMAND_NOT_FOUND TEST_LANG=es_ES.utf8 fail_type command_not_found COMMAND_NOT_FOUND TEST_LANG=fr_FR.utf8 fail_type command_not_found COMMAND_NOT_FOUND TEST_LANG=en_US.utf8 fail_type source_not_found SOURCE_NOT_FOUND TEST_LANG=es_ES.utf8 fail_type source_not_found SOURCE_NOT_FOUND TEST_LANG=fr_FR.utf8 fail_type source_not_found SOURCE_NOT_FOUND TEST_LANG=en_US.utf8 fail_type file_not_found FILE_NOT_FOUND TEST_LANG=es_ES.utf8 fail_type file_not_found FILE_NOT_FOUND TEST_LANG=fr_FR.utf8 fail_type file_not_found FILE_NOT_FOUND fail_type command_error COMMAND_ERROR fail_type subshell_term SUBSHELL_ERROR fail_type false FALSE fail_type last_false COMMAND_ERROR fail_type subshell_term SUBSHELL_ERROR fail_type exit EXIT fail_type child_error CHILD_ERROR 'prefer source than child process' fail_stderr_count unbound_variable 1 fail_stderr_count child_error 4 fail_miaou_count command_error_multiline 7 fail_miaou_count unknown 1 fail_stderr_grep two_options 'ERROR: too many args 2, either single option or SCRIPT + args accepted!' fail_stderr_grep script_not_found "ERROR: script 'doesnt_exist' not found!" fail_stderr_grep no_script 'ERROR: script expected!' assert_locales_equally_populated en es fr } failed=${#failures[@]} success=$((test_count - failed)) optional_bypassed=() if [[ ${#BYPASSED_LOCALES[@]} -gt 0 ]]; then optional_bypassed=(", $test_bypassed bypassed (missing locales: ${!BYPASSED_LOCALES[*]})") fi if [[ $failed -gt 0 ]]; then ratio=$((100 * success / test_count)) echo "${ratio}% passed, ${test_count} tests, ${failed} failed!${optional_bypassed[*]}" echo failures: for failure in "${failures[@]}"; do echo -e "\t- $failure" done exit 1 else echo "100% passed, ${test_count} tests${optional_bypassed[*]}" fi