note: close every value type's domain at construction, so resolveNote is finite for every constructible input
Division and OffsetAmount get single normalizing doors and private constructors; fromBpm validates by running the conversions rather than their reciprocal. Readers drop their re-clamps and default labels.
This commit is contained in:
+45
-3
@@ -2,9 +2,10 @@
|
||||
// framework. Same fast assert loop as the sibling pure tests.
|
||||
//
|
||||
// Covers: BPM validation (the only rejection point, which is what makes the conversions
|
||||
// total); seconds-per-beat at several tempos; beats<->seconds and beats<->ms round-trips
|
||||
// across tempos and signs; the proportionality between two tempos, asserted as a ratio
|
||||
// rather than against any fixed seconds value.
|
||||
// total) including the tempos whose reciprocal is finite but whose conversions overflow;
|
||||
// seconds-per-beat at several tempos; beats<->seconds and beats<->ms round-trips across
|
||||
// tempos and signs; the proportionality between two tempos, asserted as a ratio rather than
|
||||
// against any fixed seconds value.
|
||||
|
||||
#include "../src/core/instrument/note/tempo.h"
|
||||
|
||||
@@ -46,6 +47,44 @@ static void testUnusableBpmIsRejected() {
|
||||
CHECK(!Tempo::fromBpm(1e-310).has_value());
|
||||
}
|
||||
|
||||
static void testBpmWhoseReciprocalIsFineButWhoseConversionsOverflowIsRejected() {
|
||||
// The gap a guard on 60/bpm alone leaves open: the reciprocal is an ordinary finite
|
||||
// double, and the multiply that follows it is what blows up.
|
||||
CHECK(std::isfinite(60.0 / 1e-306));
|
||||
CHECK(!Tempo::fromBpm(1e-306).has_value());
|
||||
// The fast end fails in the other direction — the divide, not the multiply.
|
||||
CHECK(!Tempo::fromBpm(1e308).has_value());
|
||||
}
|
||||
|
||||
static void testTheGuardAdmitsEveryRealTempoAndFarBeyond() {
|
||||
// The guard is structural, not musical, so it must not have narrowed onto the range of
|
||||
// tempos anyone would type. The extremes here are orders of magnitude past that.
|
||||
for (double bpm : {1e-200, 1e-6, 0.001, 1.0, 20.0, 120.0, 240.0, 960.0, 1e6, 1e100}) {
|
||||
CHECK(Tempo::fromBpm(bpm).has_value());
|
||||
}
|
||||
}
|
||||
|
||||
static void testEveryAcceptedTempoConvertsTheWholeDomainFinitely() {
|
||||
// What the guard is FOR: past it, no conversion of a magnitude the module admits can
|
||||
// reach inf or NaN, in either unit or either direction.
|
||||
int accepted = 0, rejected = 0;
|
||||
for (double bpm : {1e-320, 1e-306, 1e-300, 1e-100, 1e-6, 0.5, 120.0, 1e6, 1e100, 1e250,
|
||||
1e308}) {
|
||||
const std::optional<Tempo> t = Tempo::fromBpm(bpm);
|
||||
if (!t) { ++rejected; continue; }
|
||||
++accepted;
|
||||
for (double m : {-kMaxConvertibleMagnitude, -1.0, 0.0, 1.0, kMaxConvertibleMagnitude}) {
|
||||
CHECK(std::isfinite(t->beatsToSeconds(m)));
|
||||
CHECK(std::isfinite(t->beatsToMs(m)));
|
||||
CHECK(std::isfinite(t->msToBeats(m)));
|
||||
CHECK(std::isfinite(t->secondsToBeats(msToSeconds(m))));
|
||||
}
|
||||
}
|
||||
// Neither half of the sweep may be empty, or the loop above proves nothing.
|
||||
CHECK(accepted > 0);
|
||||
CHECK(rejected > 0);
|
||||
}
|
||||
|
||||
// --- Conversions ---------------------------------------------------------------
|
||||
|
||||
static void testSecondsPerBeatFollowsBpm() {
|
||||
@@ -121,6 +160,9 @@ static void testMillisecondsAreTempoFree() {
|
||||
int main() {
|
||||
testUsableBpmIsAccepted();
|
||||
testUnusableBpmIsRejected();
|
||||
testBpmWhoseReciprocalIsFineButWhoseConversionsOverflowIsRejected();
|
||||
testTheGuardAdmitsEveryRealTempoAndFarBeyond();
|
||||
testEveryAcceptedTempoConvertsTheWholeDomainFinitely();
|
||||
|
||||
testSecondsPerBeatFollowsBpm();
|
||||
testBeatsToSecondsAtAKnownTempo();
|
||||
|
||||
Reference in New Issue
Block a user