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:
2026-07-26 19:00:00 -04:00
parent 83cd040f26
commit d23a3f9bb8
5 changed files with 740 additions and 50 deletions
+162
View File
@@ -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;
}