diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/os.sh | 19 | ||||
-rw-r--r-- | lib/test.sh | 179 | ||||
-rw-r--r-- | lib/test/asserts.sh | 68 | ||||
-rw-r--r-- | lib/test/output.sh | 130 | ||||
-rw-r--r-- | lib/vars.sh | 5 |
5 files changed, 396 insertions, 5 deletions
@@ -5,8 +5,8 @@ export JSON_OS= if [ -f /etc/os-release ]; then . /etc/os-release - _os_type="${NAME}" - _os_name="${ID}" + _os_type="${ID}" + _os_name="${NAME}" _os_pretty_name="${PRETTY_NAME}" _os_version="${VERSION}" _os_version_id="${VERSION_ID}" @@ -15,9 +15,18 @@ else uname=$(uname | tr '[:upper:]' '[:lower:]') case "${uname}" in darwin) - _os_type="macos" - >&2 echo "${0}: error: macos not done yet!" - exit 1;; + _os_type="darwin" + _vers=$(sw_vers | awk '{print $2}') + if [ "$?" = "0" ]; then + _os_name=$(echo "${_vers}" | tail -n +1 | head -n 1) + _os_version=$(echo "${_vers}" | tail -n +2 | head -n 1) + _os_pretty_name="${_os_name} ${_os_version}" + _os_version_id="${_os_version}" + _os_supported=1 + else + >&2 echo "cloyster/os_detect.sh: error: unknown darwin system: '${uname}'" + fi + ;; *) _os_type="${uname}" >&2 echo "cloyster/os_detect.sh: error: unknown operating system: '${uname}'" diff --git a/lib/test.sh b/lib/test.sh new file mode 100644 index 0000000..1da470b --- /dev/null +++ b/lib/test.sh @@ -0,0 +1,179 @@ +. "${CLOYSTER_SCRIPT_ROOT}/lib/test/output.sh" + +cloyster_test_varname() { + if [ -n "${1}" ]; then echo "_cloyster_test_${1}"; else echo "_cloyster_test"; fi +} + +cloyster_test_init() { + export _cloyster_test_tests_counter_count=0 + export _cloyster_test_tests_counter_success=0 + export _cloyster_test_tests_counter_skipped=0 + export _cloyster_test_tests_counter_failed=0 + export _cloyster_test_tests_counter_ignored=0 + export _cloyster_test_tests_failed= + export _cloyster_test_tests_skipped= + export _cloyster_test_tests_success= + export _cloyster_test_tests_ignored= + export _cloyster_test_tests= + export _cloyster_test_groups= + export _cloyster_test_init=y +} + +cloyster_test_add_shellcheck() { + ignore= + if ! command -v shellcheck >/dev/null; then ign=".ignore:SHELLCHECK_REQUIRED"; fi + _cloyster__test__shellcheck() { + shellcheck -e SC1008,SC2239 "${CLOYSTER_COMMAND_FILE}" + } + cloyster_test_add -a shellcheck shellcheck "_cloyster__test__shellcheck" "${ignore}" +} + +cloyster_test_add() { + append= + while getopts :a _opt; do case "${_opt}" in + a) append=y;; + esac; done; shift $((OPTIND - 1)) + id=$1 + name=$2 + fun=$3 + args=$4 + if [ -z "${id}" ]; then return 1; fi + if [ -z "${name}" ]; then return 1; fi + list=$(cloyster_test_varname "tests") + var=$(cloyster_test_varname "tests_${id}") + if [ -n "$(readvar "$var")" ]; then + echo "ERROR: A test '${id}' has already been defined" + return 1 + fi + if is_true "${append}"; then _cloyster_test_tests="${name} ${_cloyster_test_tests}"; else _cloyster_test_tests="${_cloyster_test_tests} ${name}"; fi + setvar "${var}" "${var}" + setvar "${var}__name" "${name}" + setvar "${var}__fun" "${fun}" + setvar "${var}__args" "${args}" +} + +cloyster_test_run() { + cloyster_test_log "${_green}${_bold}⚡⚡⚡ Running cloyster tests for ${_bold}${_yellow}${CLOYSTER_COMMAND}${_rst} ⚡⚡⚡" + cloyster_test_log "${_darkgray} (did you knew that #!/bin/sh is twenty years older than emojis ?)${_rst}\n" + cloyster_test_add_shellcheck #|| s=$?; _error "Failed to lol $s"; exit 1 + + incr() { + var="_cloyster_test_tests_counter_${1}" + val=$(($(readvar "$var") + 1)) + setvar "$var" "$val" + } + + _echo_block_init + all_start_us=$(posix_time_microseconds) + for id in ${_cloyster_test_tests}; do + incr "count" + name=$(readvar "_cloyster_test_tests_${id}__name") + args=$(readvar "_cloyster_test_tests_${id}__args") + line=$(readvar "_cloyster_test_tests_${id}__line") + fun=$(readvar "_cloyster_test_tests_${id}__fun") + fun="${fun:-"_test_${id}"}" + testlog="${_tmp_dir}/test_${id}.log" + _debug "Running test '${name}' line=${line} fun=${fun} args=${args}" + refute=; skip=; tags=; status=; precond_status=; ignore=; fatal=; + skip_reason=; ignore_reason= fatal_reason= + for arg in $args; do + tag=$(echo "$arg" | cut -d ":" -f 1) + targ=$(echo "$arg" | sed "s/^${tag}://g") + case $tag in + .refute) refute=y;; + .skip) skip=y; skip_reason=${targ:-.skip};; + .ignore) ignore=y; ignore_reason=${targ:-.ignore};; + .fatal) fatal=y; fatal_reason=${targ:-.fatal};; + .tag) if [ -n "${targ}" ]; then tags="${tags} ${targ}"; fi;; + *) ;; + esac + done + +## -- Preconditions +if [ ! "$skip" = "y" ]; then + ( + preconds=$( + for arg in $args; do + cond=$(echo "$arg" | cut -d ":" -f 2,100) + case $arg in + .if:*) if $($cond); then miss=y; break; fi;; + .not:*) if ! $($cond); then miss=y; break; fi;; + *) ;; + esac + done + if [ "${miss}" = "y" ]; then + echo "$(tput AF 105)Precondition failed: ${arg}" >> "${testlog}" + exit 2 + fi + ) + precond_status=$? + exit $precond_status + ) 2>> "${testlog}" >> "${testlog}" + precond_status=$? +else + echo "$(tput AF 105)${skip_reason}" >> "${testlog}" +fi + +# -- TEST SUBSHELL +if [ "${precond_status}" = "0" ] && [ ! "${skip}" = "y" ]; then +( +. "${CLOYSTER_SCRIPT_ROOT}/lib/test/asserts.sh" +_assert_reset_count +start_us=$(posix_time_microseconds) +( + "$fun" + s=$? + asserts=$(_assert_count) + if [ ! "${asserts}" = "0" ]; then echo "☀️ Passed ${asserts} assertions."; fi + exit $s +) +status=$? +end_us=$(posix_time_microseconds) +duration=$(bc -e "$end_us - $start_us") +if is_true "$refute" && [ "${status}" = "0" ]; then echo "⚠️ Exited with code ${_bold}${status}${_rst} (refuting, expected >0)"; fi +if is_true "$refute" && [ ! "${status}" = "0" ]; then echo "🌗 Exited with code ${status} (refuting)"; fi +if ! is_true "$refute" && [ ! "${status}" = "0" ]; then echo "⚠️ Exited with code ${_bold}${status}${_rst} (expected 0)"; fi +echo "$(tput AF 59)Ran in ${duration}ms${_rst}" +exit $status +) 2>> "${testlog}" >> "${testlog}" +status=$? +fi + +## -- TEST FINISHED + + cmpstatus="$status" + if is_true "$refute"; then + if [ "$status" = "0" ]; then cmpstatus=1; else cmpstatus=0; fi + fi + if [ "$precond_status" = "0" ] && [ "$cmpstatus" = "0" ]; then + _echo_block success + _cloyster_test_tests_success="${_cloyster_test_tests_success}${id} " + incr "success" + elif [ "$precond_status" = "2" ] || [ "$skip" = "y" ]; then + _echo_block skipped + _cloyster_test_tests_skipped="${_cloyster_test_tests_skipped} ${id}" + incr "skipped" + else + if [ ! "$precond_status" = "0" ]; then + err="precondition_error:${precond_status}" + else + err="${status}" + fi + _action=failed + if is_true "${ignore}"; then _action="ignored"; fi + _echo_block "${_action}" + setvar "_cloyster_test_tests_${id}__status" "$err" + setvar "_cloyster_test_tests_${id}__ignore" "$ignore" + setvar "_cloyster_test_tests_${_action}" "$(readvar "_cloyster_test_tests_${_action}") ${id}" + incr "${_action}" + fi + + done + _echo_block_flush + all_end_us=$(posix_time_microseconds) + duration=$(bc -e "$all_end_us - $all_start_us") + + _test_output_results + echo "" + _test_output_overview +} diff --git a/lib/test/asserts.sh b/lib/test/asserts.sh new file mode 100644 index 0000000..b1f538e --- /dev/null +++ b/lib/test/asserts.sh @@ -0,0 +1,68 @@ +#!/bin/sh + +fail() { + message=${1:-"fail called without message"} + echo -e " ${red}fail:${_rst} ${reason}" + exit 1 +} + +_assert_warn= + +assert_warn() { + if ! is_true "${_assert_warn}"; then _assert_warn=y; fi +} + +assert_exited() { + status=${1} + expected=${2:-0} + message=${3:-"exit status"} + assert_compare "=" "$status" "$expected" "$message" +} + +assert_compare() { + expression=$1 + left=$2 + right=$3 + message=$4 + #cloyster_test_log "assert_compare 'test \"$left\" $expression \"$right\" && echo \"OK\" || echo \"$right\"'" + assert "test \"$left\" $expression \"$right\" && echo \"\$?\" || echo \"\$?\"" "0" "${message}" +} + +assert() { + assertion=$1 + expected=$2 + message=${3:-assertion} + status= + assertion_eval="$(eval $(echo "$1"))" && status=0 || status=$?; true + cloyster_test_log "assert: assertion={$(echo "$1" | head -c 30 | sed "s:\\n: :g")} expected=${expected} evaluated=${assertion_eval} status=${status}" + if [ ! "$status" = "0" ]; then + _assert_failed "exited: ${status}" + _assert_exit + elif [ ! "$assertion_eval" = "$expected" ]; then + _assert_failed "${message}" + echo -e "${_bold}Expected:${_rst} ${expected}\n${_bold}Got :${_rst} ${assertion_eval}" + _assert_exit + fi + _test_asserts_counter=$(( ${_test_asserts_counter:-0} + 1 )) + _assert_warn= +} + +_assert_count() { + echo "$_test_asserts_counter" +} +_assert_reset_count() { + _test_asserts_counter=0 +} +_assert_failed() { + name=$1 + reason=$2; if [ -n "${reason}" ]; then reason=" ${reason}"; fi + line=; if [ -n "${_assert_line}" ]; then line=" at line ${_bold}${_assert_line}"; fi + emoji="💥"; text= + if is_true "$_assert_warn"; then emoji="🔥"; text=" (WARNING)"; fi + echo -e "${emoji} $(tput AF 214)Assertion ${_test_asserts_counter} failed${text}${line}: ${_bold}$name${_rst}${reason}" +} + +_assert_exit() { + if ! is_true "$_assert_warn"; then exit 99; fi + _assert_warn= +} diff --git a/lib/test/output.sh b/lib/test/output.sh new file mode 100644 index 0000000..4b09685 --- /dev/null +++ b/lib/test/output.sh @@ -0,0 +1,130 @@ +#!/bin/sh + + _bold=$(tput md) + _rst=$(tput me) + _red=$(tput AF 202) + _redb=$(tput AF 202) + _green=$(tput AF 2) + _yellow=$(tput AF 3) + _blue=$(tput AF 111) + _darkgray=$(tput AF 8) + _gray=$(tput AF 102) + _orange=$(tput AF 214) + +cloyster_test_log() { +# >&2 echo -e "${_ctx}[${_logcontext}]${_rst} $@${_rst}" + >&2 echo -e "$@${_rst}" + } + cloyster_test_debug() { + if is_true "${DEBUG}"; then cloyster_test_log "🔵 ${_blue}${@}"; fi + } + cloyster_test_info() { + cloyster_test_log "👍 ${@}" + } + cloyster_test_warn() { + cloyster_test_log "⚠️ ${_bold}${_yellow}${@}" + } + cloyster_test_error() { + cloyster_test_log "❌ ${_redb}${@}" + } + + +_echo_block= +_echo_block_per_line=25 +_echo_block_count= +_echo_block_init() { + _echo_block=y + _echo_block_count=1 +} +_echo_block() { + if is_true "${_echo_block}"; then + _echo_block_count=$((${_echo_block_count} + 1)) + case $1 in + success) e="🟩";; + skipped) e="🟦";; + failed) e="🟥";; + ignored) e="🟧";; + *) e="⬛";; + esac + if [ "${_echo_block_count}" -le "${_echo_block_per_line}" ]; then + echo -n "${e}" + else + _echo_block_count=1 + echo "${e}" + fi + fi +} +_echo_block_flush() { + if is_true "${_echo_block}"; then + _echo_block= + _echo_block_count= + echo "" + fi +} + +_test_output_overview() { + if [ -n "${_cloyster_test_tests_failed}" ]; then + color="${_redb}" + accent="${_bold}${_redb}" + emoji="❌" + title="FAILED" + else + color="${_green}" + accent="${_bold}${_green}" + emoji="✅" + title="PASSED" + fi + + cloyster_test_log "${emoji} ${_bold}${_yellow}${CLOYSTER_COMMAND}${_rst} ${color}tests${_rst} ${accent}${title}${_rst}" + _greenb=$(tput AF 154) + _gray=$(tput AF 8) + echo -n "${emoji} ${_bold}${_cloyster_test_tests_counter_count} tests${_rst}, ${_bold}${_greenb}${_cloyster_test_tests_counter_success} ok${_rst}" + if [ ! "${_cloyster_test_tests_counter_failed}" = "0" ]; then + echo -n ", ${_bold}${_redb}${_cloyster_test_tests_counter_failed} failed${_rst}" + fi + if [ ! "${_cloyster_test_tests_counter_ignored}" = "0" ]; then + echo -n ", ${_bold}${_orange}${_cloyster_test_tests_counter_ignored} ignored${_rst}" + fi + if [ ! "${_cloyster_test_tests_counter_skipped}" = "0" ]; then + echo -n ", ${_bold}${_blue}${_cloyster_test_tests_counter_skipped} skipped${_rst}" + fi + echo -n " ${_gray}in ${duration}ms${_rst}" + echo -e "${_rst}" +} + +_test_output_results() { + list=${1:-"success skipped ignored failed"} + for t in ${list}; do _test_output_results_one "${t}"; done +} + +_test_output_results_one() { + key=$1 + cnt=0 + count=$(readvar "_cloyster_test_tests_counter_${key}") + + case $key in + success) color="${_green}"; type="SUCCESS"; emoji="👍"; logcolor=$(tput AF 64);; + skipped) color="${_blue}"; type="SKIPPED"; emoji="🙈"; logcolor=$(tput AF 111);; + failed) color="${_red}"; type="FAILED"; emoji="😡"; logcolor=$(tput AF 181);; + ignored) color="${_orange}"; type="IGNORED"; emoji="🥲"; logcolor=$(tput 130);; + *) color="${_yellow}"; type="TEST"; emoji="💩"; logcolor=${_yellow};; + esac + + for id in $(readvar "_cloyster_test_tests_${key}"); do + cnt=$(( $cnt + 1 )) + name=$(readvar "_cloyster_test_tests_${id}__name") + args=$(readvar "_cloyster_test_tests_${id}__args") + lineno=$(readvar "_cloyster_test_tests_${id}__line") + status=$(readvar "_cloyster_test_tests_${id}__status") + args_s=; lineno_s=; status_s=; line= + args_s=" [${args}]" + if [ -n "${lineno}" ] && [ ! "${lineno}" = "0" ]; then lineno_s=" L#${lineno}"; fi + if [ -n "${status}" ]; then status_s=" !${status}"; fi + line="\n${emoji} ${_bold}${color}${type} ${cnt}/${count}:${_rst} ${_bold}${name}${_rst}" + line="${line}${_gray}${args_s}${_darkgray}${lineno_s}${status_s}${_rst}" + echo -e "${line}" + cat "${_tmp_dir}/test_${id}.log" | while read -r line; do + echo -e " ${logcolor}> ${line}${_rst}" + done + done +} diff --git a/lib/vars.sh b/lib/vars.sh new file mode 100644 index 0000000..4ae2ee7 --- /dev/null +++ b/lib/vars.sh @@ -0,0 +1,5 @@ +readvar() { + local var=$1; + if [ ! "$var" ]; then _debug "readvar: missing arg: var"; exit 1; fi + eval "echo \$$var" +} |