Files
reasampler/tests/test_capture_name.cpp
T

316 lines
14 KiB
C++

// Standalone tests for reasampler::capture_name — no REAPER, no framework. Covers the
// name SHAPE (label vs file-stem base, multi-source marker, batch ordinal, discriminator)
// and the awkward source names: empty, all-punctuation, non-ASCII, over-long, duplicate.
//
// The stem base is asserted through sanitizeStem here as well as raw, because the stem's
// real contract is "survives the sanitizer as something filesystem-legal", not "equals
// this string" — sanitizeStem is the function that has to hold, and it is capture_paths'.
#include "../src/core/capture/capture_name.h"
#include "../src/core/capture/capture_paths.h"
#include <cstdio>
#include <string>
#include <utility>
#include <vector>
using namespace reasampler;
using namespace reasampler::capture;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
// A fixed stamp so every expectation below is a literal, not a re-derivation.
static CaptureStamp stamp() { return CaptureStamp{8, 1, 14, 32}; }
static CaptureNameInputs inputsFor(std::vector<std::string> names,
int ordinal = 0,
const std::string& fallback = "item") {
CaptureNameInputs in;
in.sourceNames = std::move(names);
in.stamp = stamp();
in.ordinal = ordinal;
in.fallback = fallback;
return in;
}
// --- the discriminator -------------------------------------------------------
static void testStampIsZeroPaddedMonthDayHourMinute() {
CHECK(formatCaptureStamp(CaptureStamp{8, 1, 14, 32}) == "08-01 1432");
CHECK(formatCaptureStamp(CaptureStamp{12, 25, 0, 5}) == "12-25 0005");
}
static void testUnsetStampProducesNoDiscriminator() {
// A failed clock read leaves the stamp zeroed; the label must degrade to the bare
// name rather than render "00-00 0000".
CHECK(formatCaptureStamp(CaptureStamp{}) == "");
const CaptureNameInputs in{{"Bass"}, CaptureStamp{}, 0, "item"};
CHECK(composeCaptureName(in).label == "Bass");
}
// --- ordinary derivation ------------------------------------------------------
static void testOrdinaryNameLabelsAndFilesAfterTheTrack() {
const CaptureName n = composeCaptureName(inputsFor({"Bass"}));
CHECK(n.label == "Bass 08-01 1432");
CHECK(n.stemBase == "Bass");
CHECK(sanitizeStem(n.stemBase) == "Bass");
}
static void testTwoCapturesMinutesApartAreDistinguishable() {
CaptureNameInputs a = inputsFor({"Bass"});
CaptureNameInputs b = inputsFor({"Bass"});
b.stamp.minute = 47;
CHECK(composeCaptureName(a).label != composeCaptureName(b).label);
CHECK(composeCaptureName(a).label == "Bass 08-01 1432");
CHECK(composeCaptureName(b).label == "Bass 08-01 1447");
}
static void testNameWithSpacesKeepsThemInTheLabelAndSanitizesInTheStem() {
const CaptureName n = composeCaptureName(inputsFor({"Lead Vox"}));
CHECK(n.label == "Lead Vox 08-01 1432");
CHECK(sanitizeStem(n.stemBase) == "Lead_Vox");
}
static void testSurroundingWhitespaceIsTrimmed() {
// Untrimmed, this would file as "__Bass__" and read ragged on the card.
const CaptureName n = composeCaptureName(inputsFor({" Bass "}));
CHECK(n.label == "Bass 08-01 1432");
CHECK(sanitizeStem(n.stemBase) == "Bass");
}
// --- awkward names ------------------------------------------------------------
static void testEmptyNameFallsBackToTheScopeLiteral() {
// Unreachable in the DAW (GetTrackName answers "Track N" for an unnamed track), so
// this pins the defensive path: the scope literal, never an empty label.
const CaptureName n = composeCaptureName(inputsFor({""}, 0, "item"));
CHECK(n.label == "item 08-01 1432");
CHECK(sanitizeStem(n.stemBase) == "item");
}
static void testNoSourceAtAllFallsBackToTheScopeLiteral() {
const CaptureName n = composeCaptureName(inputsFor({}, 0, "track"));
CHECK(n.label == "track 08-01 1432");
CHECK(sanitizeStem(n.stemBase) == "track");
}
static void testEmptyNameAndEmptyFallbackStillYieldALegalStem() {
const CaptureName n = composeCaptureName(inputsFor({""}, 0, ""));
CHECK(n.label == "capture 08-01 1432");
CHECK(sanitizeStem(n.stemBase) == "capture");
}
static void testAllWhitespaceNameFallsBackToTheScopeLiteral() {
// Distinct path from an empty name: trimmed() is what empties it, before truncation
// ever runs.
const CaptureName n = composeCaptureName(inputsFor({" "}, 0, "item"));
CHECK(n.label == "item 08-01 1432");
CHECK(sanitizeStem(n.stemBase) == "item");
}
static void testNameThatTruncatesToNothingFallsBackToCapture() {
// Over-length and made entirely of a UTF-8 continuation byte (0x80-0xBF): not empty
// pre-truncation, so it survives trimmed()/the fallback chain as a real name — but
// truncateUtf8 backs off over continuation bytes and walks all the way to 0 (every byte
// in the first kMaxSourceNameBytes is a continuation byte), which used to leave an
// empty label.
const std::string allContinuation(kMaxSourceNameBytes + 10, '\x80');
const CaptureName n = composeCaptureName(inputsFor({allContinuation}));
CHECK(n.label == "capture 08-01 1432");
CHECK(sanitizeStem(n.stemBase) == "capture");
}
static void testUnnamedTrackUsesReaperTrackNConvention() {
// What GetTrackName actually hands back for an unnamed track — the deterministic
// fallback rides in as an ordinary name, no special case in the composer.
const CaptureName n = composeCaptureName(inputsFor({"Track 3"}));
CHECK(n.label == "Track 3 08-01 1432");
CHECK(sanitizeStem(n.stemBase) == "Track_3");
}
static void testAllPunctuationNameKeepsTheLabelAndCollapsesTheStem() {
const CaptureName n = composeCaptureName(inputsFor({"***"}));
CHECK(n.label == "*** 08-01 1432"); // the label is display-only; punctuation is fine
CHECK(n.stemBase == "***");
CHECK(sanitizeStem(n.stemBase) == "capture"); // nothing alnum survives
}
static void testNonAsciiNameKeepsTheLabelAndCollapsesTheStem() {
const std::string kana = "\xE3\x83\x99\xE3\x83\xBC\xE3\x82\xB9"; // UTF-8 "ベース"
const CaptureName n = composeCaptureName(inputsFor({kana}));
CHECK(n.label == kana + " 08-01 1432");
CHECK(sanitizeStem(n.stemBase) == "capture");
}
static void testMixedAsciiAndNonAsciiKeepsTheAsciiPartInTheStem() {
const std::string mixed = "Bass\xC3\xA9"; // "Bassé"
const CaptureName n = composeCaptureName(inputsFor({mixed}));
const std::string stem = sanitizeStem(n.stemBase);
CHECK(stem.rfind("Bass", 0) == 0); // recognizable
CHECK(stem != "capture"); // did not collapse
}
static void testOverLongNameIsBoundedInBothLabelAndStem() {
const std::string huge(400, 'x');
const CaptureName n = composeCaptureName(inputsFor({huge}));
CHECK(n.stemBase.size() == kMaxSourceNameBytes);
CHECK(sanitizeStem(n.stemBase).size() == kMaxSourceNameBytes);
// Label = bounded name + " MM-DD HHMM".
CHECK(n.label.size() == kMaxSourceNameBytes + 11);
}
static void testOverLongNonAsciiNameIsNotCutMidCharacter() {
// 3-byte characters do not tile the 64-byte bound evenly, so a naive cut would leave
// a truncated sequence in a label that goes on to be persisted as JSON.
std::string kana;
for (int i = 0; i < 60; ++i) kana += "\xE3\x83\x99"; // 180 bytes of "ベ"
const CaptureName n = composeCaptureName(inputsFor({kana}));
CHECK(n.stemBase.size() % 3 == 0);
CHECK(n.stemBase.size() <= kMaxSourceNameBytes);
CHECK(n.stemBase.size() > kMaxSourceNameBytes - 3); // took as much as fits
}
static void testTwoTracksWithTheSameNameComposeIdentically() {
// Deliberate: displayName is explicitly NOT unique, and stem uniqueness is
// makeUniqueTag's job, not the composer's.
const CaptureName a = composeCaptureName(inputsFor({"Bass"}));
const CaptureName b = composeCaptureName(inputsFor({"Bass"}));
CHECK(a.label == b.label);
CHECK(a.stemBase == b.stemBase);
}
// --- multi-source -------------------------------------------------------------
static void testMultiTrackSourceMarksTheExtraCount() {
const CaptureName n = composeCaptureName(inputsFor({"Bass", "Drums", "Keys"}));
CHECK(n.label == "Bass +2 08-01 1432");
CHECK(n.stemBase == "Bass+2");
CHECK(sanitizeStem(n.stemBase) == "Bass_2");
}
static void testMultiTrackSourceIgnoresUnnamedEntriesInTheCount() {
const CaptureName n = composeCaptureName(inputsFor({"Bass", ""}));
CHECK(n.label == "Bass 08-01 1432"); // one real source, no marker
}
static void testMultiTrackSourceNamesAfterTheFirstNamedTrack() {
const CaptureName n = composeCaptureName(inputsFor({"", "Drums", "Keys"}));
CHECK(n.label == "Drums +1 08-01 1432");
}
// --- batch ordinals -----------------------------------------------------------
static void testBatchOrdinalDistinguishesUnitsFromOneTrack() {
const CaptureName a = composeCaptureName(inputsFor({"Bass"}, 1));
const CaptureName b = composeCaptureName(inputsFor({"Bass"}, 2));
CHECK(a.label == "Bass #1 08-01 1432");
CHECK(b.label == "Bass #2 08-01 1432");
CHECK(sanitizeStem(a.stemBase) == "Bass-1");
CHECK(sanitizeStem(b.stemBase) == "Bass-2");
}
static void testOrdinalZeroAddsNothing() {
CHECK(composeCaptureName(inputsFor({"Bass"}, 0)).stemBase == "Bass");
}
static void testOrdinalAndMultiSourceCompose() {
const CaptureName n = composeCaptureName(inputsFor({"Bass", "Drums"}, 3));
CHECK(n.label == "Bass +1 #3 08-01 1432");
CHECK(sanitizeStem(n.stemBase) == "Bass_1-3");
}
// --- stem legality across every awkward input ---------------------------------
static void testEveryAwkwardStemStaysFilesystemLegal() {
const std::string kana = "\xE3\x83\x99\xE3\x83\xBC\xE3\x82\xB9";
const std::vector<std::string> names = {
"Bass", "", "***", kana, std::string(400, 'x'), "Lead Vox", "Track 3",
"a/b\\c:d*e?f\"g<h>i|j",
};
for (const std::string& raw : names) {
const std::string stem = sanitizeStem(composeCaptureName(inputsFor({raw})).stemBase);
CHECK(!stem.empty());
for (unsigned char c : stem) {
const bool legal = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-';
CHECK(legal);
}
}
}
// --- captureTrackName -------------------------------------------------------
static void testCaptureTrackNamePrefixesAPlainSourceName() {
CHECK(captureTrackName("MONEY") == "Capture MONEY");
CHECK(captureTrackName("bass di") == "Capture bass di");
}
static void testCaptureTrackNameIsIdempotent() {
// The whole point: a second render over a result track must not stack the prefix.
CHECK(captureTrackName("Capture MONEY") == "Capture MONEY");
CHECK(captureTrackName(captureTrackName("MONEY")) == "Capture MONEY");
// A fixed point on its own output for EVERY input, degenerate ones included.
for (const char* src : {"MONEY", "", "Capture", "Capture ", "Captured drums"}) {
const std::string once = captureTrackName(src);
CHECK(captureTrackName(once) == once);
}
}
static void testCaptureTrackNameEmptySourceHasNoTrailingSpace() {
// Unreachable from trackName (GetTrackName always answers "Track N"), so this is
// the defensive case — a bare word rather than a name ending in a space.
CHECK(captureTrackName("") == "Capture");
}
static void testCaptureTrackNameUnnamedSourceReadsAsCaptureTrackN() {
// trackName's GetTrackName fallback rides in as an ordinary name.
CHECK(captureTrackName("Track 7") == "Capture Track 7");
}
static void testCaptureTrackNameDoesNotMatchAMerePrefixOfTheWord() {
// "Captured" begins with "Capture" but not with "Capture " — it is a different
// name and must be prefixed like any other.
CHECK(captureTrackName("Captured drums") == "Capture Captured drums");
}
int main() {
testStampIsZeroPaddedMonthDayHourMinute();
testUnsetStampProducesNoDiscriminator();
testOrdinaryNameLabelsAndFilesAfterTheTrack();
testTwoCapturesMinutesApartAreDistinguishable();
testNameWithSpacesKeepsThemInTheLabelAndSanitizesInTheStem();
testSurroundingWhitespaceIsTrimmed();
testEmptyNameFallsBackToTheScopeLiteral();
testNoSourceAtAllFallsBackToTheScopeLiteral();
testEmptyNameAndEmptyFallbackStillYieldALegalStem();
testAllWhitespaceNameFallsBackToTheScopeLiteral();
testNameThatTruncatesToNothingFallsBackToCapture();
testUnnamedTrackUsesReaperTrackNConvention();
testAllPunctuationNameKeepsTheLabelAndCollapsesTheStem();
testNonAsciiNameKeepsTheLabelAndCollapsesTheStem();
testMixedAsciiAndNonAsciiKeepsTheAsciiPartInTheStem();
testOverLongNameIsBoundedInBothLabelAndStem();
testOverLongNonAsciiNameIsNotCutMidCharacter();
testTwoTracksWithTheSameNameComposeIdentically();
testMultiTrackSourceMarksTheExtraCount();
testMultiTrackSourceIgnoresUnnamedEntriesInTheCount();
testMultiTrackSourceNamesAfterTheFirstNamedTrack();
testBatchOrdinalDistinguishesUnitsFromOneTrack();
testOrdinalZeroAddsNothing();
testOrdinalAndMultiSourceCompose();
testEveryAwkwardStemStaysFilesystemLegal();
testCaptureTrackNamePrefixesAPlainSourceName();
testCaptureTrackNameIsIdempotent();
testCaptureTrackNameEmptySourceHasNoTrailingSpace();
testCaptureTrackNameUnnamedSourceReadsAsCaptureTrackN();
testCaptureTrackNameDoesNotMatchAMerePrefixOfTheWord();
if (g_fail == 0) std::printf("capture_name: all tests passed\n");
else std::printf("capture_name: %d CHECK(s) FAILED\n", g_fail);
return g_fail ? 1 : 0;
}