#!/usr/bin/env bash # "${test}" invokes test_ functions # shellcheck disable=SC2329 set -eu bash_basename() { local tmp path="${1:-}" suffix="${2:-''}" tmp=${path%"${path##*[!/]}"} tmp=${tmp##*/} tmp=${tmp%"${suffix/"$tmp"/}"} printf '%s\n' "${tmp:-/}" } bash_dirname() { # Usage: dirname "path" local tmp=${1:-.} [[ $tmp != *[!/]* ]] && { printf '/\n' return } tmp=${tmp%%"${tmp##*[!/]}"} [[ $tmp != */* ]] && { printf '.\n' return } tmp=${tmp%/*} tmp=${tmp%%"${tmp##*[!/]}"} printf '%s\n' "${tmp:-/}" } readonly PROGPATH="$(readlink -f "$0")" readonly PROGDIR="$(bash_dirname "${PROGPATH}")" readonly PROGNAME="$(bash_basename "${PROGPATH}")" # to normalize paths cd "${PROGDIR}" readonly TEST_DIR="test-dir" # lyrics exist for this song readonly GOOD_SONG="${TEST_DIR}/back burner.opus" readonly GOOD_LYRICS="${GOOD_SONG//.opus/.lrc}" # lyrics do not exist for this song readonly BAD_SONG="${TEST_DIR}/subdir/avalanche.opus" readonly BAD_LYRICS="${BAD_SONG//.opus/.lrc}" readonly PROG="add-synced-lyrics.sh" # coverage readonly COVERAGE_DIR="coverage" readonly COVERAGE_FILE="${COVERAGE_DIR}/coverage" run_test_cmd() { local expectedRetval="${1:-}" shift 1 local cmd=("$@") # check for empty args for setup tests to run [[ -z "${expectedRetval}" && -z "${cmd[*]}" ]] && return # check a value was given if [[ ! "${expectedRetval}" =~ [0-9] ]]; then echo "expectedRetval not given" exit 1 fi local output output="$("${cmd[@]}")" local actualRetval=$? declare -g CMD_OUTPUT="${output}" declare -g CMD_RETURN="${actualRetval}" if [[ ${expectedRetval} -ne ${actualRetval} ]]; then echo "${expectedRetval} != ${actualRetval} for ${cmd[*]}" return 1 else return 0 fi } test_bad_input() { # incomplete args run_test_cmd \ 1 \ add-synced-lyrics run_test_cmd \ 1 \ add-synced-lyrics --file run_test_cmd \ 1 \ add-synced-lyrics --dir run_test_cmd \ 1 \ add-synced-lyrics --recurse # incorrect mixmatch run_test_cmd \ 1 \ add-synced-lyrics --file "${PROGPATH}" --recurse run_test_cmd \ 1 \ add-synced-lyrics --file "${PROGPATH}" --dir "${PROGDIR}" run_test_cmd \ 1 \ add-synced-lyrics --file "${PROGPATH}" --dir "${PROGDIR}" --recurse return 0 } test_determine_inputs() ( eval "$(add-synced-lyrics --eval)" set_default_options DIR="${TEST_DIR}" _determine_inputs_bash bashInputs=("${INPUTS[@]}") _determine_inputs_fd fdInputs=("${INPUTS[@]}") _determine_inputs_find findInputs=("${INPUTS[@]}") echo "comparing bash vs fd" diff \ <(printf '%s\n' "${bashInputs[@]}") \ <(printf '%s\n' "${fdInputs[@]}") echo "comparing fd vs find" diff \ <(printf '%s\n' "${fdInputs[@]}") \ <(printf '%s\n' "${findInputs[@]}") return 0 ) test_determine_inputs_recurse() { RECURSE=true test_determine_inputs } test_file_dry() { add-synced-lyrics --file "${GOOD_SONG}" --dry-run } test_directory_dry() { add-synced-lyrics --dir "${TEST_DIR}" --dry-run } test_directory_recurse_dry() { add-synced-lyrics --dir "${TEST_DIR}" --recurse --dry-run } test_good_file() { add-synced-lyrics --file "${GOOD_SONG}" test -f "${GOOD_LYRICS}" } test_bad_file() { run_test_cmd \ 1 \ add-synced-lyrics --file "${BAD_SONG}" run_test_cmd \ 1 \ test -f "${BAD_LYRICS}" } test_directory_recurse() { setup_test_data # this test should fail since it includes the bad file run_test_cmd \ 1 \ add-synced-lyrics --dir "${TEST_DIR}" --recurse } test_directory_recurse_ignore() { setup_test_data run_test_cmd \ 1 \ add-synced-lyrics --dir "${TEST_DIR}" --recurse --ignore test -f "${GOOD_LYRICS}" run_test_cmd \ 1 \ test -f "${BAD_LYRICS}" return 0 } setup_test_data() { test -d "${TEST_DIR}" && rm -rf "${TEST_DIR}" mkdir -p "${TEST_DIR}/subdir" ffmpegCmd=( ffmpeg -hide_banner -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=48000 -c:a libopus -t 5 ) "${ffmpegCmd[@]}" \ -metadata ISRC=USTN10700139 \ "${GOOD_SONG}" "${ffmpegCmd[@]}" \ -metadata ISRC=US5261822978 \ "${BAD_SONG}" } TESTS=( test_determine_inputs test_determine_inputs_recurse test_bad_input test_directory_dry test_directory_recurse_dry test_file_dry test_good_file test_bad_file test_directory_recurse test_directory_recurse_ignore ) # make sure no test is accidentally missed while read -r line; do IFS=' ' read -r _ _ function <<<"${line}" if [[ "${TESTS[*]}" != *"${function}"* && "${function}" == 'test_'* ]]; then echo "missing ${function} from \$TESTS" exit 1 fi done <<<"$(declare -F)" # generate the coverage function for use with eval # to not redefine constants gen_coverage_function() { # shellcheck disable=SC2016 echo 'coverage_function () { local source line source="${BASH_SOURCE[1]}" '" source=\"\${source//'${PROGDIR}/'/}\" "' line="${BASH_LINENO[0]}" if [[ "${source}" == *".sh" && "${source}" != *"'"${PROGNAME}"'" ]]; then { # minus one because shebang is not included echo "${source} $((line - 1))"' ">>${COVERAGE_FILE}" ' } fi }' } setup_coverage() { test -d "${COVERAGE_DIR}" && rm -rf "${COVERAGE_DIR}" mkdir "${COVERAGE_DIR}" : >"${COVERAGE_FILE}" eval "$(gen_coverage_function)" } add-synced-lyrics() { export -f coverage_function PS4='$(coverage_function)' bash -x "${PROG}" "$@" } line_is_covered() { local line="$1" local lineNum="$2" # empty lines are covered [[ "${#line}" -eq 0 ]] && return 0 # commented lines are covered read -r firstToken _ <<<"${line}" [[ "${firstToken}" == '#'* ]] && return 0 # line is explicitly in coverage file is covered grep -q "${coverageFile} ${lineNum}" "${COVERAGE_FILE}" && return 0 return 1 } analyze_coverage() { sort -u "${COVERAGE_FILE}" >"${COVERAGE_FILE}.tmp" mv "${COVERAGE_FILE}.tmp" "${COVERAGE_FILE}" mapfile -t coverageFiles < <(cut -d ' ' -f1 "${COVERAGE_FILE}" | sort -u) local RED='\e[0;31m' GREEN='\e[0;32m' NC='\e[0m' local fileDir fileLen color line linesCovered for coverageFile in "${coverageFiles[@]}"; do fileDir="${COVERAGE_DIR}/$(bash_dirname "${coverageFile}")" test -d "${fileDir}" || mkdir -p "${fileDir}" mapfile -t fileContents <"${coverageFile}" fileLen="${#fileContents[@]}" linesCovered=0 { for ((line = 0; line < fileLen; line++)); do if line_is_covered "${fileContents[${line}]}" "${line}"; then color="${GREEN}" linesCovered=$((linesCovered + 1)) else color="${RED}" fi printf '%b%s%b\n' \ "${color}" \ "${fileContents[${line}]}" \ "${NC}" done } >"${COVERAGE_DIR}/${coverageFile}" echo "${coverageFile} lines covered: $((linesCovered * 100 / fileLen))%" done } if [[ $# -eq 0 ]]; then setup_coverage for test in "${TESTS[@]}"; do if ! testOutput="$( ( set -x "${test}" ) 2>&1 )"; then echo echo "!! ${test} failed !!" echo "${testOutput}" exit 1 else echo "${test} passed" fi done analyze_coverage else "$@" fi