Files
add-synced-lyrics/test-add-synced-lyrics.sh
T
2026-04-29 17:42:01 -05:00

413 lines
9.2 KiB
Bash
Executable File

#!/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[@]}" 2>&1)"
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_help_option() {
add-synced-lyrics --help
}
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
# non-existent flag
run_test_cmd \
1 \
add-synced-lyrics --this-flag-does-not-exist
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_determine_inputs_find() {
local utils=(
base64
cat
ffprobe
jq
librelyrics
spotify
)
local path=()
for util in "${utils[@]}"; do
path+=("$(bash_dirname "$(command -v "${util}")")")
done
IFS=':' moddedPath="${path[*]}"
PATH="${moddedPath}" \
add-synced-lyrics \
--dir "${TEST_DIR}" \
--dry-run "$@"
}
test_determine_inputs_find_recurse() {
test_determine_inputs_find --recurse
}
test_file_dry() {
setup_test_data
add-synced-lyrics --file "${GOOD_SONG}" --dry-run
}
test_directory_dry() {
setup_test_data
add-synced-lyrics --dir "${TEST_DIR}" --dry-run
}
test_directory_recurse_dry() {
setup_test_data
add-synced-lyrics --dir "${TEST_DIR}" --recurse --dry-run
}
test_good_file() {
setup_test_data
add-synced-lyrics --file "${GOOD_SONG}" --debug
test -f "${GOOD_LYRICS}"
}
test_good_file_embed() {
test_good_file
# rm "${GOOD_LYRICS}"
add-synced-lyrics --file "${GOOD_SONG}" --embed
run_test_cmd \
1 \
test -f "${GOOD_LYRICS}"
# do it again to test that embedded is picked up and valid
run_test_cmd \
0 \
add-synced-lyrics --file "${GOOD_SONG}" --embed
[[ "${CMD_OUTPUT}" == *'already has lyrics, skipping'* ]]
}
test_bad_file() {
setup_test_data
run_test_cmd \
1 \
add-synced-lyrics \
--file "${BAD_SONG}" \
--debug
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
}
test_missing_utils() {
PATH="$(bash_dirname "$(command -v bash)")" run_test_cmd \
1 \
add-synced-lyrics
}
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_help_option
test_determine_inputs
test_determine_inputs_recurse
test_determine_inputs_find
test_determine_inputs_find_recurse
test_bad_input
test_directory_dry
test_directory_recurse_dry
test_file_dry
test_good_file
test_good_file_embed
test_bad_file
test_directory_recurse
test_directory_recurse_ignore
test_missing_utils
)
# 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
echo
analyze_coverage
else
setup_coverage
"$@"
fi