fix: harden note-program model against corrupt tempo/division/denomination records
Reject subnormal BPM that overflows to NaN, normalize Division equality, pin a single out-of-range-denomination interpretation across all readers, flag collapsed capture windows, add offset off-view editors and structural static_asserts, collapse duplicated CLAUDE.md facts.
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
// Standalone tests for reasampler::instrument::note::note_program — no VST3, no REAPER, no
|
||||
// framework. Same fast assert loop as the sibling pure tests.
|
||||
//
|
||||
// Covers: velocity clamping; the ms/beats denomination seam and its round-trip; anchoring
|
||||
// (start to note-on, end to note-off); the resolved window against hand-computed values;
|
||||
// every division resolving to its duration in seconds; proportionality across two tempos;
|
||||
// record equality and copy round-trip.
|
||||
// Covers: velocity clamping; the ms/beats denomination seam and its round-trip, including a
|
||||
// corrupt denomination byte; anchoring (start to note-on, end to note-off); the resolved
|
||||
// window against hand-computed values and its windowCollapsed flag; every division resolving
|
||||
// to its duration in seconds; proportionality across two tempos; record equality and copy
|
||||
// round-trip; editing an offset via its non-stored view (withMsView/withBeatsView).
|
||||
|
||||
#include "../src/core/instrument/note/note_program.h"
|
||||
|
||||
@@ -323,7 +324,67 @@ static void testDefaultRecordIsAQuarterNoteWithNoOffsets() {
|
||||
CHECK(almostEqual(r.noteOffSeconds, 0.5));
|
||||
CHECK(almostEqual(r.captureStartSeconds, 0.0));
|
||||
CHECK(almostEqual(r.captureEndSeconds, 0.5));
|
||||
CHECK(r.velocity >= Velocity::kMin && r.velocity <= Velocity::kMax);
|
||||
CHECK(r.velocity == 100); // NoteProgram{}'s default Velocity, documented in note_program.h
|
||||
}
|
||||
|
||||
// --- Corrupt denomination byte --------------------------------------------------
|
||||
|
||||
static void testOutOfRangeDenominationReadsAsMillisecondsEverywhere() {
|
||||
// A denomination byte outside {Milliseconds, Beats} is well-defined but unnamed; all
|
||||
// three readers must default it to the same interpretation or a popup and a bake can
|
||||
// report different instants for one record.
|
||||
OffsetAmount corrupt;
|
||||
corrupt.magnitude = 250.0;
|
||||
corrupt.denomination = static_cast<Denomination>(7);
|
||||
const Tempo t = at(120.0);
|
||||
CHECK(almostEqual(offsetMs(corrupt, t), 250.0));
|
||||
CHECK(almostEqual(offsetSeconds(corrupt, t), 0.25));
|
||||
CHECK(almostEqual(offsetBeats(corrupt, t), t.msToBeats(250.0)));
|
||||
}
|
||||
|
||||
// --- windowCollapsed -------------------------------------------------------------
|
||||
|
||||
static void testWindowCollapsedFlagsAnInvertedWindow() {
|
||||
const Tempo t = at(120.0);
|
||||
const ResolvedNote inverted = resolveNote(
|
||||
program(makeDivision(0, DivisionModifier::Straight), offsetFromMs(0.0),
|
||||
offsetFromMs(-5000.0), 100),
|
||||
t);
|
||||
CHECK(inverted.windowCollapsed);
|
||||
|
||||
const ResolvedNote normal = resolveNote(
|
||||
program(makeDivision(0, DivisionModifier::Straight), offsetFromMs(-20.0),
|
||||
offsetFromMs(500.0), 100),
|
||||
t);
|
||||
CHECK(!normal.windowCollapsed);
|
||||
}
|
||||
|
||||
// --- Editing via the non-stored view ---------------------------------------------
|
||||
|
||||
static void testWithMsViewPreservesTheStoredDenomination() {
|
||||
const Tempo t = at(120.0); // one beat is 500 ms
|
||||
const OffsetAmount msOffset = offsetFromMs(10.0);
|
||||
const OffsetAmount editedMs = withMsView(msOffset, 40.0, t);
|
||||
CHECK(editedMs.denomination == Denomination::Milliseconds);
|
||||
CHECK(almostEqual(editedMs.magnitude, 40.0));
|
||||
|
||||
const OffsetAmount beatsOffset = offsetFromBeats(1.0);
|
||||
const OffsetAmount editedBeats = withMsView(beatsOffset, 250.0, t);
|
||||
CHECK(editedBeats.denomination == Denomination::Beats); // stays beats-denominated
|
||||
CHECK(almostEqual(offsetMs(editedBeats, t), 250.0, 1e-6)); // but reads back as 250 ms
|
||||
}
|
||||
|
||||
static void testWithBeatsViewPreservesTheStoredDenomination() {
|
||||
const Tempo t = at(120.0); // one beat is 500 ms
|
||||
const OffsetAmount beatsOffset = offsetFromBeats(0.5);
|
||||
const OffsetAmount editedBeats = withBeatsView(beatsOffset, 2.0, t);
|
||||
CHECK(editedBeats.denomination == Denomination::Beats);
|
||||
CHECK(almostEqual(editedBeats.magnitude, 2.0));
|
||||
|
||||
const OffsetAmount msOffset = offsetFromMs(100.0);
|
||||
const OffsetAmount editedMs = withBeatsView(msOffset, 1.0, t);
|
||||
CHECK(editedMs.denomination == Denomination::Milliseconds); // stays ms-denominated
|
||||
CHECK(almostEqual(offsetBeats(editedMs, t), 1.0)); // but reads back as 1 beat
|
||||
}
|
||||
|
||||
int main() {
|
||||
@@ -353,6 +414,13 @@ int main() {
|
||||
testRedenominatedRecordDescribesTheSameWindow();
|
||||
testDefaultRecordIsAQuarterNoteWithNoOffsets();
|
||||
|
||||
testOutOfRangeDenominationReadsAsMillisecondsEverywhere();
|
||||
|
||||
testWindowCollapsedFlagsAnInvertedWindow();
|
||||
|
||||
testWithMsViewPreservesTheStoredDenomination();
|
||||
testWithBeatsViewPreservesTheStoredDenomination();
|
||||
|
||||
if (g_fail == 0) std::printf("note_program: all tests passed\n");
|
||||
else std::printf("note_program: %d FAILED\n", g_fail);
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
|
||||
Reference in New Issue
Block a user