add coverage

This commit is contained in:
2026-04-28 18:38:20 -05:00
parent 5b25505990
commit 3fec74f772
2 changed files with 234 additions and 51 deletions
+1
View File
@@ -1,2 +1,3 @@
test-dir* test-dir*
*.json *.json
coverage*
+218 -36
View File
@@ -3,35 +3,122 @@
# "${test}" invokes test_ functions # "${test}" invokes test_ functions
# shellcheck disable=SC2329 # shellcheck disable=SC2329
set -u set -eu
readonly THIS_DIR="$(dirname "$(readlink -f "$0")")" bash_basename() {
readonly TEST_DIR="${THIS_DIR}/test-dir" 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 # lyrics exist for this song
readonly GOOD_SONG="${TEST_DIR}/back burner.opus" readonly GOOD_SONG="${TEST_DIR}/back burner.opus"
readonly GOOD_LYRICS="${GOOD_SONG//.opus/.lrc}" readonly GOOD_LYRICS="${GOOD_SONG//.opus/.lrc}"
# lyrics do not exist for this song # lyrics do not exist for this song
readonly BAD_SONG="${TEST_DIR}/subdir/avalanche.opus" readonly BAD_SONG="${TEST_DIR}/subdir/avalanche.opus"
readonly BAD_LYRICS="${BAD_SONG//.opus/.lrc}" readonly BAD_LYRICS="${BAD_SONG//.opus/.lrc}"
readonly PROG="${THIS_DIR}/add-synced-lyrics.sh" 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() { test_bad_input() {
# incomplete args # incomplete args
"${PROG}" && return 1 run_test_cmd \
"${PROG}" --file && return 1 1 \
"${PROG}" --dir && return 1 add-synced-lyrics
"${PROG}" --recurse && return 1 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 # incorrect mixmatch
"${PROG}" --file "${PROG}" --recurse && return 1 run_test_cmd \
"${PROG}" --file "${PROG}" --dir "${THIS_DIR}" && return 1 1 \
"${PROG}" --file "${PROG}" --dir "${THIS_DIR}" --recurse && return 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 return 0
} }
test_determine_inputs() ( test_determine_inputs() (
eval "$("${PROG}" --eval)" eval "$(add-synced-lyrics --eval)"
set_default_options set_default_options
DIR="${TEST_DIR}" DIR="${TEST_DIR}"
@@ -48,12 +135,12 @@ test_determine_inputs() (
echo "comparing bash vs fd" echo "comparing bash vs fd"
diff \ diff \
<(printf '%s\n' "${bashInputs[@]}") \ <(printf '%s\n' "${bashInputs[@]}") \
<(printf '%s\n' "${fdInputs[@]}") || return 1 <(printf '%s\n' "${fdInputs[@]}")
echo "comparing fd vs find" echo "comparing fd vs find"
diff \ diff \
<(printf '%s\n' "${fdInputs[@]}") \ <(printf '%s\n' "${fdInputs[@]}") \
<(printf '%s\n' "${findInputs[@]}") || return 1 <(printf '%s\n' "${findInputs[@]}")
return 0 return 0
) )
@@ -63,47 +150,55 @@ test_determine_inputs_recurse() {
} }
test_file_dry() { test_file_dry() {
"${PROG}" --file "${GOOD_SONG}" --dry-run add-synced-lyrics --file "${GOOD_SONG}" --dry-run
} }
test_directory_dry() { test_directory_dry() {
"${PROG}" --dir "${TEST_DIR}" --dry-run add-synced-lyrics --dir "${TEST_DIR}" --dry-run
} }
test_directory_recurse_dry() { test_directory_recurse_dry() {
"${PROG}" --dir "${TEST_DIR}" --recurse --dry-run add-synced-lyrics --dir "${TEST_DIR}" --recurse --dry-run
} }
test_good_file() { test_good_file() {
"${PROG}" --file "${GOOD_SONG}" || return 1 add-synced-lyrics --file "${GOOD_SONG}"
test -f "${GOOD_LYRICS}" test -f "${GOOD_LYRICS}"
} }
test_bad_file() { test_bad_file() {
"${PROG}" --file "${BAD_SONG}" && return 1 run_test_cmd \
! test -f "${BAD_LYRICS}" 1 \
add-synced-lyrics --file "${BAD_SONG}"
run_test_cmd \
1 \
test -f "${BAD_LYRICS}"
} }
test_directory_recurse() { test_directory_recurse() {
setup_test_data || return 1 setup_test_data
# this test should fail since it includes the bad file # this test should fail since it includes the bad file
"${PROG}" --dir "${TEST_DIR}" --recurse && return 1 run_test_cmd \
1 \
return 0 add-synced-lyrics --dir "${TEST_DIR}" --recurse
} }
test_directory_recurse_ignore() { test_directory_recurse_ignore() {
setup_test_data || return 1 setup_test_data
"${PROG}" --dir "${TEST_DIR}" --recurse --ignore && return 1 run_test_cmd \
test -f "${GOOD_LYRICS}" || return 1 1 \
test -f "${BAD_LYRICS}" && return 1 add-synced-lyrics --dir "${TEST_DIR}" --recurse --ignore
test -f "${GOOD_LYRICS}"
run_test_cmd \
1 \
test -f "${BAD_LYRICS}"
return 0 return 0
} }
setup_test_data() { setup_test_data() {
test -d "${TEST_DIR}" && rm -rf "${TEST_DIR}" test -d "${TEST_DIR}" && rm -rf "${TEST_DIR}"
mkdir -p "${TEST_DIR}/subdir" || return 1 mkdir -p "${TEST_DIR}/subdir"
ffmpegCmd=( ffmpegCmd=(
ffmpeg ffmpeg
-hide_banner -hide_banner
@@ -114,14 +209,13 @@ setup_test_data() {
) )
"${ffmpegCmd[@]}" \ "${ffmpegCmd[@]}" \
-metadata ISRC=USTN10700139 \ -metadata ISRC=USTN10700139 \
"${GOOD_SONG}" || return 1 "${GOOD_SONG}"
"${ffmpegCmd[@]}" \ "${ffmpegCmd[@]}" \
-metadata ISRC=US5261822978 \ -metadata ISRC=US5261822978 \
"${BAD_SONG}" || return 1 "${BAD_SONG}"
} }
TESTS=( TESTS=(
setup_test_data
test_determine_inputs test_determine_inputs
test_determine_inputs_recurse test_determine_inputs_recurse
test_bad_input test_bad_input
@@ -136,13 +230,98 @@ TESTS=(
# make sure no test is accidentally missed # make sure no test is accidentally missed
while read -r line; do while read -r line; do
IFS=' ' read -r _ _ test <<<"${line}" IFS=' ' read -r _ _ function <<<"${line}"
if [[ "${TESTS[*]}" != *"${test}"* ]]; then if [[ "${TESTS[*]}" != *"${function}"* &&
echo "missing ${test} from \$TESTS" "${function}" == 'test_'* ]]; then
echo "missing ${function} from \$TESTS"
exit 1 exit 1
fi fi
done <<<"$(declare -F)" 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 for test in "${TESTS[@]}"; do
if ! testOutput="$( if ! testOutput="$(
( (
@@ -159,4 +338,7 @@ for test in "${TESTS[@]}"; do
fi fi
done done
exit 0 analyze_coverage
else
"$@"
fi