Refuse every multi-track selected-tracks render, both scopes; make a failed mono collapse observable
This commit is contained in:
@@ -243,40 +243,58 @@ static void testRangedItemScopeRendersTimeBounded() {
|
||||
CHECK(fxBypassPlanFor(CaptureScope::Item).bypassSelfFx);
|
||||
}
|
||||
|
||||
static void testMultiTrackRangedItemRenderIsNamedForRefusal() {
|
||||
// The one shape that cannot land: a ranged item capture (item scope re-sourced to
|
||||
// the selected-tracks render) whose items span more than one track. That source
|
||||
// is INFERRED to render one file per track with no single-file bit available
|
||||
// (unverified; see src/shell/capture/CLAUDE.md §Gotchas for what that inference
|
||||
// rests on), so N stems would collapse onto one render pattern and one track's
|
||||
// audio would land as a successful capture.
|
||||
CHECK(isMultiTrackRangedItemRender(CaptureScope::Item,
|
||||
SourceMode::SelectedTracks, 2));
|
||||
CHECK(isMultiTrackRangedItemRender(CaptureScope::Item,
|
||||
SourceMode::SelectedTracks, 7));
|
||||
static void testMultiTrackStemRenderIsNamedForRefusal() {
|
||||
// The one shape that cannot land: a selected-tracks render over more than one
|
||||
// track. That source is INFERRED to render one file per track with no single-file
|
||||
// bit available (unverified; see src/shell/capture/CLAUDE.md §Gotchas for what that
|
||||
// inference rests on), so N stems would collapse onto one render pattern and one
|
||||
// track's audio would land as a successful capture.
|
||||
CHECK(isMultiTrackStemRender(SourceMode::SelectedTracks, 2));
|
||||
CHECK(isMultiTrackStemRender(SourceMode::SelectedTracks, 7));
|
||||
|
||||
// One track is the whole point of the re-source — it must still render.
|
||||
CHECK(!isMultiTrackRangedItemRender(CaptureScope::Item,
|
||||
SourceMode::SelectedTracks, 1));
|
||||
CHECK(!isMultiTrackRangedItemRender(CaptureScope::Item,
|
||||
SourceMode::SelectedTracks, 0));
|
||||
// One track is the common case for BOTH scopes — it must still render. This is the
|
||||
// regression floor for the plain single-track track capture.
|
||||
CHECK(!isMultiTrackStemRender(SourceMode::SelectedTracks, 1));
|
||||
CHECK(!isMultiTrackStemRender(SourceMode::SelectedTracks, 0));
|
||||
|
||||
// A full-extent item capture keeps the selected-items source, whose single-file
|
||||
// bit already sums a multi-track item selection into one file.
|
||||
CHECK(!isMultiTrackRangedItemRender(CaptureScope::Item,
|
||||
SourceMode::SelectedItems, 3));
|
||||
CHECK(!isMultiTrackStemRender(SourceMode::SelectedItems, 3));
|
||||
// Razor's single-file bit does the same; master mix is one file by definition.
|
||||
CHECK(!isMultiTrackStemRender(SourceMode::RazorArea, 3));
|
||||
CHECK(!isMultiTrackStemRender(SourceMode::MasterMix, 3));
|
||||
// Realtime never reaches the offline render at all (sends sum into one temp track).
|
||||
CHECK(!isMultiTrackStemRender(SourceMode::Realtime, 3));
|
||||
|
||||
// Track scope renders through the same source and is deliberately untouched here
|
||||
// — that per-track output predates the item re-source and is filed in docs/TODO.md.
|
||||
CHECK(!isMultiTrackRangedItemRender(CaptureScope::Track,
|
||||
SourceMode::SelectedTracks, 3));
|
||||
// The predicate is reachable from the mappings it guards: BOTH the ranged item
|
||||
// capture's source mode and the track scope's resolve to the one it names, while a
|
||||
// full-extent item capture does not.
|
||||
CHECK(isMultiTrackStemRender(sourceModeForScope(CaptureScope::Item, false), 2));
|
||||
CHECK(isMultiTrackStemRender(sourceModeForScope(CaptureScope::Track, false), 2));
|
||||
CHECK(isMultiTrackStemRender(sourceModeForScope(CaptureScope::Track, true), 2));
|
||||
CHECK(!isMultiTrackStemRender(sourceModeForScope(CaptureScope::Item, true), 2));
|
||||
}
|
||||
|
||||
// The predicate is reachable from the mapping it guards: the ranged item capture's
|
||||
// own source mode is the one it names.
|
||||
CHECK(isMultiTrackRangedItemRender(CaptureScope::Item,
|
||||
sourceModeForScope(CaptureScope::Item, false), 2));
|
||||
CHECK(!isMultiTrackRangedItemRender(CaptureScope::Item,
|
||||
sourceModeForScope(CaptureScope::Item, true), 2));
|
||||
static void testRefusalMessagesAreSiblingsWithDistinctExits() {
|
||||
const std::string item = multiTrackRefusalMessage(CaptureScope::Item);
|
||||
const std::string track = multiTrackRefusalMessage(CaptureScope::Track);
|
||||
|
||||
// Both name the same reason — a shape that cannot land as one file — so a user who
|
||||
// hits the mistake in either scope reads one story, not two.
|
||||
CHECK(item.find("single file") != std::string::npos);
|
||||
CHECK(track.find("single file") != std::string::npos);
|
||||
|
||||
// And both name a way out. The shared one is "one track at a time"; each scope then
|
||||
// adds the exit only it has (widen the range / capture the folder).
|
||||
CHECK(item.find("one track's items at a time") != std::string::npos);
|
||||
CHECK(item.find("range match the items' extent") != std::string::npos);
|
||||
CHECK(track.find("one track at a time") != std::string::npos);
|
||||
CHECK(track.find("folder") != std::string::npos);
|
||||
|
||||
// Distinct texts: the track message must not be the item message's wording about
|
||||
// items and ranges, which would misdescribe what the user actually did.
|
||||
CHECK(item != track);
|
||||
CHECK(track.find("selected items") == std::string::npos);
|
||||
}
|
||||
|
||||
// --- inferRangeSource: razor-else-time (orthogonal to scope) -----------------
|
||||
@@ -376,7 +394,8 @@ int main() {
|
||||
testRazorUnionBounds();
|
||||
testScopeSourceModes();
|
||||
testRangedItemScopeRendersTimeBounded();
|
||||
testMultiTrackRangedItemRenderIsNamedForRefusal();
|
||||
testMultiTrackStemRenderIsNamedForRefusal();
|
||||
testRefusalMessagesAreSiblingsWithDistinctExits();
|
||||
testRangeInference();
|
||||
testItemScopeBypassesEverythingButTake();
|
||||
testTrackScopeKeepsSelfBypassesAncestorsAndMaster();
|
||||
|
||||
@@ -775,6 +775,24 @@ static void testCollapsePreservesQuietNaNBitPattern() {
|
||||
if (!pcm.empty()) CHECK(bitsFromFloat(pcm[0]) == kQuietNaNBits);
|
||||
}
|
||||
|
||||
// The file-side collapse has three outcomes, and a genuine I/O failure once reported
|
||||
// identically to "the channels differ" — a stereo file landing under a plain Ok. The
|
||||
// report is where they must part: Declined stays silent (a capture with nothing to
|
||||
// collapse reads as it always did), and the other two say different things.
|
||||
static void testCollapseOutcomesReportDistinctly() {
|
||||
const std::string declined = monoCollapseSuffix(MonoCollapseOutcome::Declined);
|
||||
const std::string collapsed = monoCollapseSuffix(MonoCollapseOutcome::Collapsed);
|
||||
const std::string failed = monoCollapseSuffix(MonoCollapseOutcome::Failed);
|
||||
|
||||
CHECK(declined.empty());
|
||||
CHECK(!collapsed.empty());
|
||||
CHECK(!failed.empty());
|
||||
CHECK(collapsed != failed);
|
||||
// The failure must read as a failure, not as a quieter success.
|
||||
CHECK(failed.find("failed") != std::string::npos);
|
||||
CHECK(collapsed.find("failed") == std::string::npos);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testParseCanonicalStereo();
|
||||
testParseMonoAndLeadingChunk();
|
||||
@@ -810,6 +828,7 @@ int main() {
|
||||
testCollapseDeclinesOnUnparseableBytes();
|
||||
testCollapseChangesContentHash();
|
||||
testCollapsePreservesQuietNaNBitPattern();
|
||||
testCollapseOutcomesReportDistinctly();
|
||||
|
||||
if (g_fail == 0) std::printf("wav_codec: all tests passed\n");
|
||||
else std::printf("wav_codec: %d CHECK(s) FAILED\n", g_fail);
|
||||
|
||||
Reference in New Issue
Block a user