mirror of
https://github.com/sshlien/abcmidi.git
synced 2026-05-30 20:09:29 +00:00
Implement basic testing infrastructure (#19)
* Add configuration for CMake build system alongside autoconf - Add a modern CMake build system (`CMakeLists.txt`, `CMakePresets.json`) that coexists with the legacy autoconf/Makefile build - Shared source files (`midifile.c`, `parseabc.c`, `music_utils.c`, `parser2.c`) are compiled once via OBJECT libraries and linked into the 8 binaries - Three presets: `default` (Release), `debug`, `sanitize` (ASan + UBSan) - Generates `compile_commands.json` for clangd/LSP editor support - Install rules match the legacy Makefile (binaries, doc files, man pages) - Pinned to `-std=gnu89` because the codebase mixes K&R `()` and ANSI typed prototypes — in C23/gnu23 (GCC 15+ default), `()` means `(void)`, making these a hard error. Note: **the existing autoconf build is also broken with GCC 15** for the same reason ```sh cmake --preset debug cmake --build --preset debug cmake --install build/debug --prefix /usr/local Documentation - README.md: added Building section with both autoconf and CMake instructions - doc/readme.txt: added build instructions in the existing preamble - doc/CHANGES: added changelog entry Test plan - All 3 presets configure and build with GCC 15 - Smoke test: abc2midi samples/coleraine.abc produces valid MIDI through mftext - Sanitizer build (--preset sanitize) runs clean on sample files - Install layout verified: 8 binaries, 10 doc files, 8 man pages in correct paths - Build on macOS (untested, should work with AppleClang) * Implement basic testing infrastructure The CMake build includes a test suite covering all 8 programs: - **Smoke tests** verify each binary runs cleanly with `-ver`. - **Golden-file tests** run each program on a sample input and compare the (normalized) output to a checked-in reference. Binary MIDI outputs are piped through `mftext` to produce diffable text. Volatile lines (version banners, dates, temporary paths) are stripped before comparison. ```sh ctest --preset debug ctest --preset debug -L golden ctest --preset debug -L smoke ``` To regenerate the golden files after an intentional behavioural change, review the diff, then commit: ```sh cmake --build build/debug --target update-golden git diff tests/golden/ ``` * Factorize more the test CMake code
This commit is contained in:
177
tests/run_test.cmake
Normal file
177
tests/run_test.cmake
Normal file
@@ -0,0 +1,177 @@
|
||||
# Generic test runner for abcmidi golden-file tests.
|
||||
#
|
||||
# Required variables (passed via -D on the cmake command line):
|
||||
# TYPE - one of: abc2midi, abc2abc, midi2abc, midistats, mftext,
|
||||
# yaps, midicopy, abcmatch
|
||||
# SAMPLE - path to the input ABC sample file
|
||||
# GOLDEN - path to the golden reference file
|
||||
# TMPDIR - working directory for temporary outputs
|
||||
#
|
||||
# ABC2MIDI, ABC2ABC, MIDI2ABC, MIDISTATS, MFTEXT, YAPS, MIDICOPY, ABCMATCH
|
||||
# - absolute paths to the binaries (only those needed for TYPE)
|
||||
#
|
||||
# Behaviour:
|
||||
# - Runs the requested binary (and any pipeline steps required to make the
|
||||
# output diffable, e.g. mftext for binary MIDI output).
|
||||
# - Strips non-deterministic lines (version banners, dates) from the output.
|
||||
# - If the environment variable ABCMIDI_UPDATE_GOLDEN is set, the filtered
|
||||
# output is written to GOLDEN (used to regenerate references after an
|
||||
# intentional behavioural change).
|
||||
# - Otherwise, the filtered output is compared with GOLDEN; on mismatch, a
|
||||
# unified diff is printed and the test fails.
|
||||
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
|
||||
if(NOT TYPE OR NOT SAMPLE OR NOT GOLDEN OR NOT TMPDIR)
|
||||
message(FATAL_ERROR "run_test.cmake: TYPE, SAMPLE, GOLDEN, TMPDIR are required")
|
||||
endif()
|
||||
|
||||
file(MAKE_DIRECTORY "${TMPDIR}")
|
||||
get_filename_component(stem "${SAMPLE}" NAME_WE)
|
||||
|
||||
set(raw "${TMPDIR}/${TYPE}_${stem}.raw")
|
||||
set(out "${TMPDIR}/${TYPE}_${stem}.out")
|
||||
set(midfile "${TMPDIR}/${TYPE}_${stem}.mid")
|
||||
|
||||
# --- Command helpers ---------------------------------------------------------
|
||||
|
||||
# Run a command; abort with a detailed message on non-zero exit.
|
||||
function(run_or_die)
|
||||
execute_process(
|
||||
COMMAND ${ARGN}
|
||||
RESULT_VARIABLE rc
|
||||
OUTPUT_VARIABLE captured_out
|
||||
ERROR_VARIABLE captured_err
|
||||
)
|
||||
if(NOT rc EQUAL 0)
|
||||
message(FATAL_ERROR
|
||||
"Command failed (rc=${rc}):\n ${ARGN}\n"
|
||||
"--- stdout ---\n${captured_out}\n"
|
||||
"--- stderr ---\n${captured_err}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Run a command and capture its stdout into a file.
|
||||
function(run_to_file outfile)
|
||||
execute_process(
|
||||
COMMAND ${ARGN}
|
||||
OUTPUT_FILE "${outfile}"
|
||||
RESULT_VARIABLE rc
|
||||
ERROR_VARIABLE captured_err
|
||||
)
|
||||
if(NOT rc EQUAL 0)
|
||||
message(FATAL_ERROR
|
||||
"Command failed (rc=${rc}):\n ${ARGN}\n--- stderr ---\n${captured_err}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Convert ${SAMPLE} to MIDI, then run ${bin} on the resulting MIDI file and
|
||||
# capture its stdout. Any extra arguments are inserted BEFORE the MIDI path
|
||||
# (needed e.g. for "midi2abc -f <file>").
|
||||
function(run_via_mid outfile bin)
|
||||
run_or_die("${ABC2MIDI}" "${SAMPLE}" -o "${midfile}" -quiet -silent)
|
||||
run_to_file("${outfile}" "${bin}" ${ARGN} "${midfile}")
|
||||
endfunction()
|
||||
|
||||
# Run ${bin} directly on ${SAMPLE} and capture its stdout. Extra arguments
|
||||
# are appended after the sample path.
|
||||
function(run_on_sample outfile bin)
|
||||
run_to_file("${outfile}" "${bin}" "${SAMPLE}" ${ARGN})
|
||||
endfunction()
|
||||
|
||||
# --- Dispatch: populate ${raw} based on TYPE ---------------------------------
|
||||
#
|
||||
# The binary path is derived from TYPE via TOUPPER + double-dereference:
|
||||
# for TYPE=midistats, ${bin} resolves to ${MIDISTATS}. The only exception is
|
||||
# abc2midi, where we render the MIDI output through mftext for diffing — it
|
||||
# is therefore an alias for the mftext test.
|
||||
|
||||
string(TOUPPER "${TYPE}" TYPE_UPPER)
|
||||
if(TYPE STREQUAL "abc2midi")
|
||||
set(bin "${MFTEXT}")
|
||||
else()
|
||||
set(bin "${${TYPE_UPPER}}")
|
||||
endif()
|
||||
|
||||
if(TYPE MATCHES "^(abc2midi|mftext|midistats)$")
|
||||
run_via_mid("${raw}" "${bin}")
|
||||
|
||||
elseif(TYPE STREQUAL "midi2abc")
|
||||
run_via_mid("${raw}" "${bin}" -f)
|
||||
|
||||
elseif(TYPE STREQUAL "abc2abc")
|
||||
run_on_sample("${raw}" "${bin}")
|
||||
|
||||
elseif(TYPE STREQUAL "abcmatch")
|
||||
# -pitch_hist gives a short, deterministic, useful summary of the tune.
|
||||
run_on_sample("${raw}" "${bin}" -pitch_hist)
|
||||
|
||||
elseif(TYPE STREQUAL "yaps")
|
||||
set(psfile "${TMPDIR}/yaps_${stem}.ps")
|
||||
run_or_die("${bin}" "${SAMPLE}" -o "${psfile}")
|
||||
configure_file("${psfile}" "${raw}" COPYONLY)
|
||||
|
||||
elseif(TYPE STREQUAL "midicopy")
|
||||
# ABC -> MIDI -> midicopy -> mftext
|
||||
set(copied "${TMPDIR}/midicopy_${stem}_out.mid")
|
||||
run_or_die("${ABC2MIDI}" "${SAMPLE}" -o "${midfile}" -quiet -silent)
|
||||
run_or_die("${bin}" "${midfile}" "${copied}")
|
||||
run_to_file("${raw}" "${MFTEXT}" "${copied}")
|
||||
|
||||
else()
|
||||
message(FATAL_ERROR "Unknown TYPE: ${TYPE}")
|
||||
endif()
|
||||
|
||||
# --- Normalize: strip non-deterministic lines --------------------------------
|
||||
#
|
||||
# Each program prints a version line as the first line of stdout. yaps
|
||||
# PostScript output contains a CreationDate. Filenames embedded in output
|
||||
# (e.g. yaps "%%Title: /tmp/...") are also volatile and stripped.
|
||||
|
||||
file(READ "${raw}" content)
|
||||
|
||||
# Strip program version banners (e.g. "5.02 February 16 2025 abc2midi")
|
||||
string(REGEX REPLACE "[0-9]+\\.[0-9]+ +[A-Za-z]+ +[0-9]+ +[0-9]+ +[a-z2]+\n" "" content "${content}")
|
||||
|
||||
# Strip yaps PostScript volatile headers
|
||||
string(REGEX REPLACE "%%Title:[^\n]*\n" "%%Title: <stripped>\n" content "${content}")
|
||||
string(REGEX REPLACE "%%CreationDate:[^\n]*\n" "%%CreationDate: <stripped>\n" content "${content}")
|
||||
|
||||
# Strip absolute paths to TMPDIR (filenames embedded in some outputs)
|
||||
string(REPLACE "${TMPDIR}/" "" content "${content}")
|
||||
|
||||
file(WRITE "${out}" "${content}")
|
||||
|
||||
# --- Update or compare -------------------------------------------------------
|
||||
|
||||
if(DEFINED ENV{ABCMIDI_UPDATE_GOLDEN})
|
||||
get_filename_component(golden_dir "${GOLDEN}" DIRECTORY)
|
||||
file(MAKE_DIRECTORY "${golden_dir}")
|
||||
file(WRITE "${GOLDEN}" "${content}")
|
||||
message(STATUS "Updated golden: ${GOLDEN}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(NOT EXISTS "${GOLDEN}")
|
||||
message(FATAL_ERROR
|
||||
"Golden file does not exist: ${GOLDEN}\n"
|
||||
"Run 'ABCMIDI_UPDATE_GOLDEN=1 ctest ...' to generate it.")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND "${CMAKE_COMMAND}" -E compare_files "${out}" "${GOLDEN}"
|
||||
RESULT_VARIABLE diff_rc
|
||||
OUTPUT_QUIET
|
||||
ERROR_QUIET
|
||||
)
|
||||
|
||||
if(NOT diff_rc EQUAL 0)
|
||||
execute_process(
|
||||
COMMAND diff -u "${GOLDEN}" "${out}"
|
||||
OUTPUT_VARIABLE diff_text
|
||||
)
|
||||
message(FATAL_ERROR
|
||||
"Output differs from golden ${GOLDEN}\n"
|
||||
"To accept the new output: ABCMIDI_UPDATE_GOLDEN=1 ctest ...\n"
|
||||
"${diff_text}")
|
||||
endif()
|
||||
Reference in New Issue
Block a user