Files
abcmidi/tests/run_test.cmake
Ronan Keryell 1d766a85d2 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
2026-04-22 07:42:29 -04:00

178 lines
6.0 KiB
CMake

# 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()