// Standalone tests for shell/package/package_rollback — no REAPER, no framework. // Pins both halves of the deletion discriminator: the structural half (only an // exclusively-created path is recorded, and the record is absolute so a CWD change // cannot re-aim it) and the contract half (markIndexCommitted disarms rollback). #include "../src/shell/package/package_rollback.h" #include "../src/shell/package/package_path.h" #include #include #include #include #include #include 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) // The journal records absolute paths, so every expectation is built the same way. static std::string scratch(const std::string& name) { return pathToUtf8(fs::current_path() / utf8Path(name)); } static std::vector patternBytes(std::size_t n, std::uint8_t seed) { std::vector v(n); for (std::size_t i = 0; i < n; ++i) v[i] = static_cast(seed + i * 7u); return v; } static void writeScratchFile(const std::string& path, const std::vector& bytes) { std::ofstream f(utf8Path(path), std::ios::binary | std::ios::trunc); f.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); } static std::vector readAll(const std::string& path) { std::ifstream f(utf8Path(path), std::ios::binary); return std::vector(std::istreambuf_iterator(f), std::istreambuf_iterator()); } static bool exists(const std::string& path) { return fs::exists(utf8Path(path)); } static void removeQuietly(const std::string& path) { std::error_code ec; fs::remove(utf8Path(path), ec); } static void testLandRecordsOnSuccessOnly() { LandedFileJournal journal; const std::string path = scratch("rb_land.bin"); const std::vector bytes = patternBytes(32, 1); CHECK(journal.writeLandedFile(path, PayloadBuffer(bytes))); CHECK(readAll(path) == bytes); CHECK(journal.landedPaths().size() == 1); CHECK(!exists(path + ".rsbanktmp")); // the land is a direct exclusive create journal.rollback(); CHECK(!exists(path)); // the recorded path denoted the file we asked for } static void testLandNonAsciiPathRoundTripsAsUtf8() { // The fs::absolute -> u8string round trip at writeLandedFile is otherwise // untested with a non-ASCII path. LandedFileJournal journal; const std::string path = scratch("rb_caf\xC3\xA9.bin"); const std::vector bytes = patternBytes(16, 2); CHECK(journal.writeLandedFile(path, PayloadBuffer(bytes))); CHECK(readAll(path) == bytes); journal.rollback(); CHECK(!exists(path)); } static void testRelativeInputIsRecordedAbsolute() { // The hazard: a bare name recorded verbatim, then a CWD change, and rollback // unlinks whatever now sits at that name in the new directory. LandedFileJournal journal; CHECK(journal.writeLandedFile("rb_relative.bin", PayloadBuffer(patternBytes(8, 4)))); CHECK(journal.landedPaths().size() == 1); const std::string recorded = journal.landedPaths().front(); CHECK(utf8Path(recorded).is_absolute()); std::error_code ec; // Absolute AND still the same file — a spelling check alone would not prove that. CHECK(fs::equivalent(utf8Path(recorded), utf8Path(scratch("rb_relative.bin")), ec)); CHECK(!ec); journal.rollback(); CHECK(!exists(scratch("rb_relative.bin"))); } static void testExistingDestinationRefusedUntouched() { LandedFileJournal journal; const std::string path = scratch("rb_existing.bin"); const std::vector original = patternBytes(16, 0x60); writeScratchFile(path, original); CHECK(!journal.writeLandedFile(path, PayloadBuffer(patternBytes(8, 1)))); CHECK(readAll(path) == original); // never overwritten CHECK(journal.empty()); // a refused write is not recorded const RollbackResult result = journal.rollback(); CHECK(result.deletedCount == 0); CHECK(exists(path)); // rollback cannot touch a file it did not write removeQuietly(path); } static void testEmptyPayloadRefused() { LandedFileJournal journal; const std::string path = scratch("rb_empty.bin"); CHECK(!journal.writeLandedFile(path, PayloadBuffer{})); CHECK(!exists(path)); CHECK(journal.empty()); } static void testRollbackDeletesExactlyTheRecordedSet() { LandedFileJournal journal; const std::string a = scratch("rb_a.bin"); const std::string c = scratch("rb_c.bin"); const std::string bystander = scratch("rb_bystander.bin"); CHECK(journal.writeLandedFile(a, PayloadBuffer(patternBytes(8, 1)))); CHECK(journal.writeLandedFile(c, PayloadBuffer(patternBytes(8, 2)))); writeScratchFile(bystander, patternBytes(8, 3)); // not journal-written const RollbackResult result = journal.rollback(); CHECK(result.deletedCount == 2); CHECK(result.alreadyAbsentCount == 0); CHECK(result.failedCount == 0); CHECK(!result.refused); CHECK(!exists(a)); CHECK(!exists(c)); CHECK(exists(bystander)); // exactly the given files, nothing else CHECK(journal.empty()); const RollbackResult second = journal.rollback(); // cleared: a no-op CHECK(second.deletedCount == 0); CHECK(exists(bystander)); removeQuietly(bystander); } static void testVanishedFileIsToleratedNotFailed() { LandedFileJournal journal; const std::string path = scratch("rb_gone.bin"); CHECK(journal.writeLandedFile(path, PayloadBuffer(patternBytes(8, 1)))); removeQuietly(path); // vanished between land and rollback CHECK(!exists(path)); const RollbackResult result = journal.rollback(); CHECK(result.deletedCount == 0); CHECK(result.alreadyAbsentCount == 1); CHECK(result.failedCount == 0); } static void testIndexCommitDisarmsRollback() { // Once the index references these files the carve-out no longer covers them, so a // late failure in the verb must not be able to delete indexed bytes. LandedFileJournal journal; const std::string path = scratch("rb_committed.bin"); const std::vector bytes = patternBytes(8, 1); CHECK(journal.writeLandedFile(path, PayloadBuffer(bytes))); journal.markIndexCommitted(); CHECK(journal.indexCommitted()); const RollbackResult result = journal.rollback(); CHECK(result.refused); CHECK(result.deletedCount == 0); CHECK(readAll(path) == bytes); // untouched CHECK(!journal.empty()); // the record survives the refusal // Landing more files after the commit would produce unrollbackable state. const std::string late = scratch("rb_late.bin"); CHECK(!journal.writeLandedFile(late, PayloadBuffer(patternBytes(8, 2)))); CHECK(!exists(late)); removeQuietly(path); } int main() { testLandRecordsOnSuccessOnly(); testLandNonAsciiPathRoundTripsAsUtf8(); testRelativeInputIsRecordedAbsolute(); testExistingDestinationRefusedUntouched(); testEmptyPayloadRefused(); testRollbackDeletesExactlyTheRecordedSet(); testVanishedFileIsToleratedNotFailed(); testIndexCommitDisarmsRollback(); if (g_fail == 0) std::printf("package_rollback: all tests passed\n"); else std::printf("package_rollback: %d CHECK(s) FAILED\n", g_fail); return g_fail == 0 ? 0 : 1; }