Add regression suite testing JaySynth's TODO.md findings via testhost

tools/testhost/tests/run_tests.sh dynamically exercises the tracked
hardening findings using testhost itself - no GUI, sub-second wall
time:

- oversized_blocksize (Critical #5): --blockSize 16384 under a
  timeout, catching either the overflow or the deadlock the original
  bug could cause.
- nrpn_out_of_range/nrpn_in_range (Critical #1): JSON scenarios
  sending the 5-message MIDI NRPN CC sequence, once targeting the
  max 14-bit ID (16383) and once a legitimate in-range ID (500), so
  a regression that made the bounds check too aggressive would also
  show up.
- patch_save_load_roundtrip / bank_save_load_roundtrip: save a
  distinctive continuous parameter value, reload in a fresh process,
  check it round-trips within tolerance - covers the
  setCurrentProgramStateInformation no-op bug and the
  patchDecodeXml/patchImportXml fixes.
- stability_sweep: loads every bundled .fxp under extras/sounds/ and
  renders a held note through each, as a broad crash/NaN net.

Added --printParams to main.cpp (prints every parameter's index and
value) to make the round-trip tests possible - this was also already
a noted nice-to-have in testhost/TODO.md.

While building the oversized_blocksize test, found and fixed (on the
hardening branch, commit d713ca5) a second, previously-unknown bug:
VCF_CalcCoeff_LPF/_HPF/_BPF advance the coefficient buffer pointer
once per (sample, filter-section) pair, but the buffer was only ever
allocated for bufsize samples, not bufsize*sections - a 4th-order
filter overflowed it for any block between 4097 and 8192 samples.
Found via bisecting the crashing block size then confirming with an
ASan build; see tests/README.md's debugging-notes section for exactly
how, including the ASan symbolizer hang encountered along the way and
the workaround.

Also discovered (documented in tools/testhost/TODO.md's Investigate
section, not fixed - out of scope): JaySynth's compiled VST2 wrapper
never advertises chunk support to the host, so real .fxp/.fxb sample
files have no effect when loaded through the generic AudioProcessor
state API. The round-trip tests work around this by using testhost's
own save output as the fixture rather than an external sample file.

All 10 checks pass against the current hardening-branch build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dhtwRLARk4eiPngcQykLJ
This commit is contained in:
2026-07-27 20:06:44 +02:00
co-authored by Claude Sonnet 5
parent ab29ad2820
commit f54689f071
8 changed files with 515 additions and 1 deletions
+12
View File
@@ -39,6 +39,9 @@ namespace
" --sampleRate <hz> default 44100\n"
" --blockSize <n> default 512\n"
" --wav <file> record the plugin's output to a WAV file\n"
" --printParams print every parameter's index and current value\n"
" (after any --patch/--bank/--param has been applied,\n"
" before rendering) as \"PARAM <index> <value>\" lines\n"
"\n"
"Patch/bank state is exactly what JUCE's AudioPluginInstance::get/setCurrentProgram-\n"
"StateInformation and get/setStateInformation read and write for a hosted VST2\n"
@@ -66,6 +69,7 @@ namespace
double inputFrequencyHz = 440.0;
String perfCsvFile;
bool printParams = false;
};
bool parseArgs (int argc, char *argv[], Args &args)
@@ -96,6 +100,8 @@ namespace
args.blockSize = String (argv[++i]).getIntValue();
else if (arg == "--perfCsv" && i + 1 < argc)
args.perfCsvFile = argv[++i];
else if (arg == "--printParams")
args.printParams = true;
else if (arg == "--note" && i + 2 < argc)
{
args.hasNote = true;
@@ -294,6 +300,12 @@ int main (int argc, char *argv[])
if (scenario.initialLoadPatchFile.isNotEmpty() && ! loadStateFile (scenario.initialLoadPatchFile, false, host))
return 1;
if (args.printParams)
{
for (int i = 0; i < host.getNumParameters(); ++i)
printf ("PARAM %d %.6f\n", i, host.getParameter (i));
}
AudioInputSource inputSource;
inputSource.prepare (scenario.sampleRate);
switch (scenario.inputType)