feat(capture): M11 batch capture - one bank sample per selected item / razor area
Pure batch_capture module (plan + mixed-result aggregation, unit-tested); CAPTURE_BATCH_ITEMS/RAZOR actions with transient per-unit selection restored on all paths, per-unit invariants + provenance, single persist. Conform-on-insert already shipped.
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
// Standalone tests for reasampler::batch_capture — no REAPER, no test framework.
|
||||
// Same fast loop as the sibling pure tests (render_settings et al.): assert the batch
|
||||
// planning (selected-set -> ordered capture units) and result aggregation directly.
|
||||
//
|
||||
// Covers (M11 brief §test cases):
|
||||
// * selected-set -> capture-request-list planning: items; razor areas; empty set;
|
||||
// multiple razor areas across tracks (order preserved, ordinals contiguous);
|
||||
// invalid (empty/inverted) ranges dropped.
|
||||
// * batch result aggregation: all-success; partial-failure ORDERING (failed
|
||||
// ordinals reported in unit order); all-failed; single-unit noun singular.
|
||||
|
||||
#include "../src/batch_capture.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace reasampler;
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
// --- planCaptureUnits: items -------------------------------------------------
|
||||
|
||||
// Three selected items -> three units, order preserved, ordinals 1,2,3, bounds exact.
|
||||
static void testPlanItemsPreservesOrderAndBounds() {
|
||||
std::vector<BatchRange> in = {{0.0, 1.5}, {2.0, 2.25}, {10.0, 12.0}};
|
||||
auto units = planCaptureUnits(in);
|
||||
CHECK(units.size() == 3);
|
||||
CHECK(units[0].ordinal == 1 && units[0].startSeconds == 0.0 && units[0].endSeconds == 1.5);
|
||||
CHECK(units[1].ordinal == 2 && units[1].startSeconds == 2.0 && units[1].endSeconds == 2.25);
|
||||
CHECK(units[2].ordinal == 3 && units[2].startSeconds == 10.0 && units[2].endSeconds == 12.0);
|
||||
}
|
||||
|
||||
// --- planCaptureUnits: empty set ---------------------------------------------
|
||||
|
||||
// No selected item / no razor area -> empty plan (shell reports nothing-to-batch).
|
||||
static void testPlanEmptySet() {
|
||||
CHECK(planCaptureUnits({}).empty());
|
||||
}
|
||||
|
||||
// --- planCaptureUnits: invalid ranges dropped, ordinals stay contiguous ------
|
||||
|
||||
// An empty (end==start) and an inverted (end<start) range are DROPPED; the kept
|
||||
// units are renumbered 1..k with no gap, so a dropped neighbor never leaves a hole
|
||||
// in the reported ordinals.
|
||||
static void testPlanDropsInvalidAndRenumbers() {
|
||||
std::vector<BatchRange> in = {
|
||||
{0.0, 1.0}, // kept -> #1
|
||||
{5.0, 5.0}, // empty -> dropped
|
||||
{3.0, 2.0}, // inverted -> dropped
|
||||
{8.0, 9.0}, // kept -> #2
|
||||
};
|
||||
auto units = planCaptureUnits(in);
|
||||
CHECK(units.size() == 2);
|
||||
CHECK(units[0].ordinal == 1 && units[0].startSeconds == 0.0 && units[0].endSeconds == 1.0);
|
||||
CHECK(units[1].ordinal == 2 && units[1].startSeconds == 8.0 && units[1].endSeconds == 9.0);
|
||||
}
|
||||
|
||||
// --- planCaptureUnits: multiple razor areas across tracks --------------------
|
||||
|
||||
// The shell concatenates every track's parsed razor areas in track order; planning
|
||||
// preserves that concatenation order and assigns contiguous ordinals. Two areas on
|
||||
// track A followed by one on track B -> ordinals 1,2,3 in that exact order.
|
||||
static void testPlanRazorAreasAcrossTracksOrdered() {
|
||||
std::vector<BatchRange> in = {
|
||||
{0.0, 0.5}, // track A, area 1
|
||||
{1.0, 1.5}, // track A, area 2
|
||||
{4.0, 6.0}, // track B, area 1
|
||||
};
|
||||
auto units = planCaptureUnits(in);
|
||||
CHECK(units.size() == 3);
|
||||
CHECK(units[0].startSeconds == 0.0 && units[0].endSeconds == 0.5);
|
||||
CHECK(units[1].startSeconds == 1.0 && units[1].endSeconds == 1.5);
|
||||
CHECK(units[2].startSeconds == 4.0 && units[2].endSeconds == 6.0);
|
||||
CHECK(units[0].ordinal == 1 && units[1].ordinal == 2 && units[2].ordinal == 3);
|
||||
}
|
||||
|
||||
// --- BatchOutcome: all-success -----------------------------------------------
|
||||
|
||||
static void testOutcomeAllSuccess() {
|
||||
BatchOutcome o;
|
||||
o.record(1, true);
|
||||
o.record(2, true);
|
||||
o.record(3, true);
|
||||
CHECK(o.total() == 3);
|
||||
CHECK(o.succeeded() == 3);
|
||||
CHECK(o.failed() == 0);
|
||||
CHECK(o.failures().empty());
|
||||
CHECK(o.summaryLine("item") == "ReaSampler batch capture: 3 items captured.");
|
||||
}
|
||||
|
||||
// --- BatchOutcome: partial-failure ordering ----------------------------------
|
||||
|
||||
// Failures are reported in UNIT ORDER (#2, #4), not record-count order or sorted —
|
||||
// the summary must let the user retry exactly the right units. This assertion fails
|
||||
// if the aggregation reordered, deduped, or dropped a failure.
|
||||
static void testOutcomePartialFailureOrdering() {
|
||||
BatchOutcome o;
|
||||
o.record(1, true);
|
||||
o.record(2, false, "render produced no output file");
|
||||
o.record(3, true);
|
||||
o.record(4, false, "capture range is empty");
|
||||
o.record(5, true);
|
||||
CHECK(o.total() == 5);
|
||||
CHECK(o.succeeded() == 3);
|
||||
CHECK(o.failed() == 2);
|
||||
|
||||
auto fails = o.failures();
|
||||
CHECK(fails.size() == 2);
|
||||
CHECK(fails[0].ordinal == 2 && !fails[0].ok);
|
||||
CHECK(fails[0].detail == "render produced no output file");
|
||||
CHECK(fails[1].ordinal == 4);
|
||||
|
||||
CHECK(o.summaryLine("item") ==
|
||||
"ReaSampler batch capture: 3 of 5 items captured (2 failed: #2, #4).");
|
||||
}
|
||||
|
||||
// --- BatchOutcome: all-failed ------------------------------------------------
|
||||
|
||||
static void testOutcomeAllFailed() {
|
||||
BatchOutcome o;
|
||||
o.record(1, false, "x");
|
||||
o.record(2, false, "y");
|
||||
CHECK(o.succeeded() == 0);
|
||||
CHECK(o.failed() == 2);
|
||||
CHECK(o.summaryLine("razor area") ==
|
||||
"ReaSampler batch capture: 0 of 2 razor areas captured (2 failed: #1, #2).");
|
||||
}
|
||||
|
||||
// --- BatchOutcome: single unit uses singular noun ----------------------------
|
||||
|
||||
static void testOutcomeSingularNoun() {
|
||||
BatchOutcome o;
|
||||
o.record(1, true);
|
||||
CHECK(o.summaryLine("item") == "ReaSampler batch capture: 1 item captured.");
|
||||
}
|
||||
|
||||
// --- BatchOutcome: empty (no units planned) ----------------------------------
|
||||
|
||||
static void testOutcomeEmpty() {
|
||||
BatchOutcome o;
|
||||
CHECK(o.total() == 0);
|
||||
CHECK(o.summaryLine("item") == "ReaSampler batch capture: nothing to capture.");
|
||||
}
|
||||
|
||||
int main() {
|
||||
testPlanItemsPreservesOrderAndBounds();
|
||||
testPlanEmptySet();
|
||||
testPlanDropsInvalidAndRenumbers();
|
||||
testPlanRazorAreasAcrossTracksOrdered();
|
||||
|
||||
testOutcomeAllSuccess();
|
||||
testOutcomePartialFailureOrdering();
|
||||
testOutcomeAllFailed();
|
||||
testOutcomeSingularNoun();
|
||||
testOutcomeEmpty();
|
||||
|
||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user