3fd3214ff8
Freeze *.rsbank as binary via .gitattributes; add a truncated additive_forward fixture proving the exact-size proof beats TooNew; enumerate the fixture dir to catch orphaned files; make fixture-size checks fatal instead of just logged; pin fixture version asserts as literals, not build-relative.
276 lines
11 KiB
C++
276 lines
11 KiB
C++
// The corpus driven through both verbs — no REAPER, no framework. Where
|
|
// test_package_compat asserts what the frozen bytes DECODE to, this file asserts what
|
|
// the import and export verbs DO with them: the payload bytes survive a full
|
|
// export -> import -> export, and every refusal in the corpus refuses before a planner
|
|
// or a filesystem write is reached.
|
|
|
|
#include "../src/shell/package/import_landing.h"
|
|
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "../src/core/package/bank_package.h"
|
|
#include "../src/shell/package/export_bank.h"
|
|
#include "../src/shell/package/package_path.h"
|
|
#include "../src/shell/persist/session.h"
|
|
#include "package_fixtures.h"
|
|
|
|
using namespace reasampler;
|
|
namespace fs = std::filesystem;
|
|
|
|
static int g_fail = 0;
|
|
#define CHECK(cond) do { if(!(cond)) { \
|
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
|
|
|
static const char* kTag = "1754000000";
|
|
static const char* kImportedBankId = "bank-imported";
|
|
|
|
// --- scratch project ---------------------------------------------------------
|
|
|
|
// One project directory per suite, torn down after, so no suite observes another's
|
|
// bank folder.
|
|
class Scratch {
|
|
public:
|
|
explicit Scratch(const std::string& name) {
|
|
std::error_code ec;
|
|
dir_ = pathToUtf8(fs::temp_directory_path(ec) /
|
|
utf8Path("reasampler_compat_" + name));
|
|
fs::remove_all(utf8Path(dir_), ec);
|
|
fs::create_directories(utf8Path(dir_), ec);
|
|
}
|
|
~Scratch() {
|
|
std::error_code ec;
|
|
fs::remove_all(utf8Path(dir_), ec);
|
|
}
|
|
const std::string& projectDir() const { return dir_; }
|
|
std::string bankDir() const { return package::bankFolderDir(dir_); }
|
|
std::string importPath() const { return dir_ + "/in.rsbank"; }
|
|
std::string exportPath() const { return dir_ + "/out.rsbank"; }
|
|
|
|
private:
|
|
std::string dir_;
|
|
};
|
|
|
|
static std::vector<std::uint8_t> readBytes(const std::string& path) {
|
|
std::ifstream f(utf8Path(path), std::ios::binary);
|
|
return std::vector<std::uint8_t>(std::istreambuf_iterator<char>(f),
|
|
std::istreambuf_iterator<char>());
|
|
}
|
|
|
|
static void writeBytes(const std::string& path, const std::vector<std::uint8_t>& bytes) {
|
|
std::ofstream f(utf8Path(path), std::ios::binary | std::ios::trunc);
|
|
f.write(reinterpret_cast<const char*>(bytes.data()),
|
|
static_cast<std::streamsize>(bytes.size()));
|
|
}
|
|
|
|
// Copies a committed fixture to the path the verb will be pointed at. Fails loudly on a
|
|
// size mismatch (empty included): an unreadable OR mangled corpus would otherwise let
|
|
// every "must refuse" suite pass, since a garbled fixture still refuses, just not for
|
|
// the reason under test.
|
|
static bool stageFixture(const Scratch& scratch, const char* fixture,
|
|
std::size_t expectedSize) {
|
|
const std::vector<std::uint8_t> bytes = packageFixtureBytes(fixture);
|
|
if (bytes.size() != expectedSize) {
|
|
std::printf("FAIL: fixture %s is %zu bytes, expected %zu (path: %s)\n", fixture,
|
|
bytes.size(), expectedSize, packageFixturePath(fixture).c_str());
|
|
++g_fail;
|
|
return false;
|
|
}
|
|
writeBytes(scratch.importPath(), bytes);
|
|
return true;
|
|
}
|
|
|
|
// Each entry's payload bytes, sliced out of a whole package file by its own layout.
|
|
static std::vector<std::vector<std::uint8_t>> payloadsOf(const std::vector<std::uint8_t>& file) {
|
|
std::vector<std::vector<std::uint8_t>> out;
|
|
const package::DecodedPackage dec = package::decodePackage(file, file.size());
|
|
if (dec.status != package::PackageReadability::Readable) return out;
|
|
for (const package::PackageEntrySpan& span : dec.layout) {
|
|
const auto begin = file.begin() + static_cast<std::ptrdiff_t>(span.offset);
|
|
out.emplace_back(begin, begin + static_cast<std::ptrdiff_t>(span.length));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// --- the sequence the import verb runs, minus REAPER -------------------------
|
|
|
|
static ImportLanding runImport(const Scratch& scratch, ReaSamplerSession& session,
|
|
int* outBirths = nullptr) {
|
|
LandedFileJournal journal;
|
|
ImportLanding landing = landPackage(scratch.importPath(), scratch.projectDir(),
|
|
session.book(), kTag, journal);
|
|
if (landing.outcome != ImportOutcome::Landed) return landing;
|
|
int births = 0;
|
|
const bool applied = applyImportedBank(session.book(), kImportedBankId, landing.plan,
|
|
[&births](const model::Sample&) { ++births; });
|
|
CHECK(applied);
|
|
if (applied) journal.markIndexCommitted();
|
|
if (outBirths) *outBirths = births;
|
|
return landing;
|
|
}
|
|
|
|
// --- the round-trip anchor ---------------------------------------------------
|
|
|
|
// The fixture IS the first export (written by the shipping build's own export verb), so
|
|
// importing and re-exporting it closes export -> import -> export over frozen bytes.
|
|
static void testV1FixtureReExportsByteIdenticalPayloads() {
|
|
Scratch scratch("roundtrip");
|
|
if (!stageFixture(scratch, "v1_shipping.rsbank", 1207)) return;
|
|
|
|
ReaSamplerSession session;
|
|
int births = 0;
|
|
const ImportLanding landing = runImport(scratch, session, &births);
|
|
CHECK(landing.outcome == ImportOutcome::Landed);
|
|
CHECK(births == 1);
|
|
CHECK(landing.plan.landCount == 1);
|
|
if (landing.plan.entries.size() != 1) { CHECK(false); return; }
|
|
|
|
const std::vector<std::vector<std::uint8_t>> sourcePayloads =
|
|
payloadsOf(packageFixtureBytes("v1_shipping.rsbank"));
|
|
if (sourcePayloads.size() != 1) { CHECK(false); return; }
|
|
|
|
// The landed file is the package's payload verbatim — the first half of the claim.
|
|
const std::string landed = scratch.bankDir() + "/" + landing.plan.entries[0].destFileName;
|
|
CHECK(readBytes(landed) == sourcePayloads[0]);
|
|
|
|
ExportRequest req;
|
|
req.projectDir = scratch.projectDir();
|
|
req.bankId = kImportedBankId;
|
|
req.destAbsPath = scratch.exportPath();
|
|
req.exportTimestamp = 1754200000;
|
|
const ExportOutcome out = exportBank(session, req);
|
|
CHECK(out.status == ExportStatus::Written);
|
|
CHECK(out.entriesWritten == 1);
|
|
|
|
// The second half: the re-export's payloads, byte for byte. Entry NAMES may legally
|
|
// differ across the trip — the importer re-spells a bank file and the exporter mints
|
|
// its own transport name (src/core/package/CLAUDE.md) — the payload bytes may not.
|
|
CHECK(payloadsOf(readBytes(scratch.exportPath())) == sourcePayloads);
|
|
}
|
|
|
|
// --- the refusals, at the verb rather than the codec -------------------------
|
|
|
|
static void testRefuseFixtureRefusesTheWholeImportAndNamesTheWriter() {
|
|
Scratch scratch("refuse");
|
|
if (!stageFixture(scratch, "refuse_structural.rsbank", 1207)) return;
|
|
|
|
ReaSamplerSession session;
|
|
const BankBook before = session.book();
|
|
LandedFileJournal journal;
|
|
const ImportLanding landing = landPackage(scratch.importPath(), scratch.projectDir(),
|
|
session.book(), kTag, journal);
|
|
|
|
CHECK(landing.outcome == ImportOutcome::TooNew);
|
|
// Literal, not kPackageFormatVersion-relative: this is the FIXTURE's frozen pair
|
|
// (2/2), not this build's (see test_package_compat.cpp's file header note).
|
|
CHECK(landing.header.formatVersion == 2);
|
|
CHECK(landing.header.minReaderVersion == 2);
|
|
CHECK(landing.header.writerVersion == "1.9.0");
|
|
// Nothing planned, nothing on disk, nothing in the index.
|
|
CHECK(landing.plan.entries.empty());
|
|
CHECK(!fs::exists(utf8Path(scratch.bankDir())));
|
|
CHECK(session.book() == before);
|
|
}
|
|
|
|
// The same eight cuts test_package_compat classifies, driven through the verb's
|
|
// incremental prefix reader — the one caller that can ask requiredPrefixSize for more
|
|
// bytes than the file holds. Sizes match test_package_compat.cpp's kTruncations.
|
|
struct TruncationFixture {
|
|
const char* file;
|
|
std::size_t size;
|
|
};
|
|
|
|
static const TruncationFixture kTruncationFixtures[] = {
|
|
{"trunc_magic.rsbank", 2},
|
|
{"trunc_version_pair.rsbank", 10},
|
|
{"trunc_writer_semver.rsbank", 18},
|
|
{"trunc_manifest_length.rsbank", 23},
|
|
{"trunc_manifest_body.rsbank", 466},
|
|
{"trunc_payload_start.rsbank", 907},
|
|
{"trunc_payload_middle.rsbank", 1057},
|
|
{"trunc_one_short.rsbank", 1206},
|
|
};
|
|
|
|
static void testEveryTruncationRefusesTheImportAsMalformed() {
|
|
for (const TruncationFixture& fixture : kTruncationFixtures) {
|
|
Scratch scratch(std::string("trunc_") + fixture.file);
|
|
if (!stageFixture(scratch, fixture.file, fixture.size)) continue;
|
|
|
|
ReaSamplerSession session;
|
|
const BankBook before = session.book();
|
|
LandedFileJournal journal;
|
|
const ImportLanding landing = landPackage(scratch.importPath(), scratch.projectDir(),
|
|
session.book(), kTag, journal);
|
|
if (landing.outcome != ImportOutcome::Malformed) {
|
|
std::printf("FAIL: %s imported as outcome %d, expected Malformed\n", fixture.file,
|
|
static_cast<int>(landing.outcome));
|
|
++g_fail;
|
|
}
|
|
CHECK(landing.plan.entries.empty());
|
|
CHECK(!fs::exists(utf8Path(scratch.bankDir())));
|
|
CHECK(session.book() == before);
|
|
}
|
|
}
|
|
|
|
// Sizes match test_package_compat.cpp's kHostiles.
|
|
struct HostileFixtureFile {
|
|
const char* file;
|
|
std::size_t size;
|
|
};
|
|
|
|
static const HostileFixtureFile kHostileFixtures[] = {
|
|
{"hostile_name_dotdot.rsbank", 624},
|
|
{"hostile_name_parent_slash.rsbank", 633},
|
|
{"hostile_name_parent_backslash.rsbank", 634},
|
|
{"hostile_name_subdir_slash.rsbank", 634},
|
|
{"hostile_name_drive_absolute.rsbank", 643},
|
|
{"hostile_name_unc_absolute.rsbank", 646},
|
|
{"hostile_path_dotdot_slash.rsbank", 641},
|
|
{"hostile_path_dotdot_backslash.rsbank", 640},
|
|
{"hostile_path_rooted.rsbank", 635},
|
|
{"hostile_path_drive_absolute.rsbank", 643},
|
|
{"hostile_path_unc_absolute.rsbank", 646},
|
|
};
|
|
|
|
static void testEveryHostileNameIsRefusedBeforeThePlannerRuns() {
|
|
for (const HostileFixtureFile& fixture : kHostileFixtures) {
|
|
Scratch scratch(std::string("hostile_") + fixture.file);
|
|
if (!stageFixture(scratch, fixture.file, fixture.size)) continue;
|
|
|
|
ReaSamplerSession session;
|
|
const BankBook before = session.book();
|
|
LandedFileJournal journal;
|
|
const ImportLanding landing = landPackage(scratch.importPath(), scratch.projectDir(),
|
|
session.book(), kTag, journal);
|
|
if (landing.outcome != ImportOutcome::Malformed) {
|
|
std::printf("FAIL: %s imported as outcome %d, expected Malformed\n", fixture.file,
|
|
static_cast<int>(landing.outcome));
|
|
++g_fail;
|
|
}
|
|
// planImport is the ONLY producer of a non-empty plan and it runs after the
|
|
// decode — an empty one is how "refused before any planner" is observed here.
|
|
CHECK(landing.plan.entries.empty());
|
|
CHECK(landing.plan.bankDisplayName.empty());
|
|
CHECK(!fs::exists(utf8Path(scratch.bankDir())));
|
|
CHECK(session.book() == before);
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
testV1FixtureReExportsByteIdenticalPayloads();
|
|
testRefuseFixtureRefusesTheWholeImportAndNamesTheWriter();
|
|
testEveryTruncationRefusesTheImportAsMalformed();
|
|
testEveryHostileNameIsRefusedBeforeThePlannerRuns();
|
|
|
|
if (g_fail == 0) {
|
|
std::printf("package_round_trip_tests: all passed\n");
|
|
return 0;
|
|
}
|
|
std::printf("package_round_trip_tests: %d failure(s)\n", g_fail);
|
|
return 1;
|
|
}
|