Files
abcmidi/tests/run_test.cmake
Ronan Keryell a6fa0d6b8a abc2midi: fix -PMAR perturbing note timing and end-of-track time (#26)
Emitting a Marker meta-event from the PART case wrote the event with
delta_time_track0 (correct, since that's the conductor-track accumulator)
but did not also reset delta_time. On track 0 in multi-track mode,
delta_time also accumulates via timestep() and is what writetrack()
returns to the MIDI library as the end-of-track delta — so each marker
left a stale delta_time that pushed the end-of-track event past the end
of the music. In single-track mode (ntracks == 1), markers were written
with delta_time_track0 too, but delta_time is the only relevant counter
in that mode, so notes following each marker were shifted later.

Fix: mirror the TEMPO handler — on ntracks == 1 use delta_time and reset
it; on ntracks != 1 keep using delta_time_track0 for the event delta but
also zero delta_time so the end-of-track is not inflated.

Reported by James Allwright with the partdemo.abc test case (now in
samples/), which exercises the parts != -1 path (header P:BACDBAC plus
body P: labels A/B/C/D). James independently fixed the same bug in his
abc2midiu fork in r32 (commit 34b7c32, "Add -PMAR option for part
markers"), where the equivalent reset is on the single delta_time
counter — his fork having retired delta_time_track0 in an earlier
refactor. Verified that mftext output of the generated MIDI is now
byte-identical to the non-PMAR output except for the added Marker
events, on both partdemo.abc (single-track) and samples/demo.abc tune 5
with P:(AB)3 (multi-track).

Add a CMake/CTest regression test (abc2midi_pmar_partdemo) that locks in
the post-fix mftext output. To support it, add_golden_test() gains an
optional NAME (to register multiple tests against the same TYPE+SAMPLE
pair) and an optional ABC2MIDI_ARGS (forwarded to the abc2midi
invocation in run_via_mid). Reverting the genmidi.c fix makes the new
test fail; reapplying it makes it pass.
2026-05-01 09:09:46 -04:00

185 lines
6.4 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>").
#
# ABC2MIDI_ARGS (set by the caller, possibly empty) is forwarded as a CMake
# list to the abc2midi invocation, so callers can opt into flags like -PMAR.
function(run_via_mid outfile bin)
run_or_die("${ABC2MIDI}" "${SAMPLE}" ${ABC2MIDI_ARGS} -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. Two date forms are emitted across the
# binaries: "5.02 February 16 2025 abc2midi" (with day) and "5.03 April
# 2026 abc2midi" (month + year only) — the day number is optional.
# The month token must be capitalized to avoid matching PostScript lines
# like "0.8 setlinewidth 0 setlinecap" in yaps output.
string(REGEX REPLACE "[0-9]+\\.[0-9]+ +[A-Z][a-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()