275 lines
12 KiB
C++
275 lines
12 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
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;
|
|
}
|