Merge Ε-W1-T2: the package filesystem shell, pickers, and rollback journal
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
// Standalone tests for shell/package/package_io — no REAPER, no framework. Failure is
|
||||
// injected at the writer seam (abandonment, open failure, rename failure) rather than
|
||||
// simulated, and the streaming claim is asserted against PayloadBuffer::alive().
|
||||
|
||||
#include "../src/shell/package/package_io.h"
|
||||
#include "../src/shell/package/package_path.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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 std::vector<std::uint8_t> patternBytes(std::size_t n, std::uint8_t seed) {
|
||||
std::vector<std::uint8_t> v(n);
|
||||
for (std::size_t i = 0; i < n; ++i)
|
||||
v[i] = static_cast<std::uint8_t>(seed + i * 7u);
|
||||
return v;
|
||||
}
|
||||
|
||||
static bool sameBytes(const PayloadBuffer& p, const std::vector<std::uint8_t>& want) {
|
||||
return p.size() == want.size() &&
|
||||
(want.empty() || std::equal(want.begin(), want.end(), p.data()));
|
||||
}
|
||||
|
||||
static void writeScratchFile(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()));
|
||||
}
|
||||
|
||||
static std::vector<std::uint8_t> readAll(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 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 testPayloadCounterTracksMovesNotCopies() {
|
||||
CHECK(PayloadBuffer::alive() == 0);
|
||||
{
|
||||
PayloadBuffer a(patternBytes(4, 1));
|
||||
CHECK(PayloadBuffer::alive() == 1);
|
||||
PayloadBuffer b = std::move(a);
|
||||
CHECK(PayloadBuffer::alive() == 1); // the count moved with the bytes
|
||||
PayloadBuffer c;
|
||||
c = std::move(b);
|
||||
CHECK(PayloadBuffer::alive() == 1);
|
||||
CHECK(c.size() == 4);
|
||||
}
|
||||
CHECK(PayloadBuffer::alive() == 0);
|
||||
{
|
||||
PayloadBuffer empty;
|
||||
CHECK(PayloadBuffer::alive() == 0); // holding nothing counts as nothing
|
||||
}
|
||||
}
|
||||
|
||||
static void testStreamingRoundTripHoldsOnePayload() {
|
||||
const std::string dest = "pkg_io_scratch.rsbank";
|
||||
const std::vector<std::uint8_t> header = patternBytes(16, 0xA0);
|
||||
const std::vector<std::vector<std::uint8_t>> entries = {
|
||||
patternBytes(1000, 1), patternBytes(500, 2), patternBytes(1, 3)};
|
||||
|
||||
std::vector<std::string> srcs;
|
||||
for (std::size_t i = 0; i < entries.size(); ++i) {
|
||||
srcs.push_back("pkg_io_src" + std::to_string(i) + ".bin");
|
||||
writeScratchFile(srcs[i], entries[i]);
|
||||
}
|
||||
|
||||
CHECK(PayloadBuffer::alive() == 0);
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> layout; // offset, length
|
||||
{
|
||||
PackageFileWriter writer(dest);
|
||||
CHECK(writer.ok());
|
||||
CHECK(writer.appendRaw(header.data(), header.size()));
|
||||
std::uint64_t offset = header.size();
|
||||
for (std::size_t i = 0; i < entries.size(); ++i) {
|
||||
PayloadBuffer p = readFilePayload(srcs[i]);
|
||||
CHECK(sameBytes(p, entries[i]));
|
||||
CHECK(PayloadBuffer::alive() == 1); // exactly one entry in memory
|
||||
CHECK(writer.appendPayload(p));
|
||||
layout.emplace_back(offset, p.size());
|
||||
offset += p.size();
|
||||
}
|
||||
CHECK(PayloadBuffer::alive() == 0); // each released before the next
|
||||
CHECK(writer.commit());
|
||||
CHECK(!writer.commit()); // a second commit is refused
|
||||
}
|
||||
CHECK(!exists(dest + ".rsbanktmp"));
|
||||
CHECK(exists(dest));
|
||||
|
||||
{
|
||||
// Scoped: the reader holds the file open, and Windows refuses to delete an
|
||||
// open file — cleanup below needs it closed first.
|
||||
PackageFileReader reader(dest);
|
||||
CHECK(reader.ok());
|
||||
CHECK(reader.fileSize() == header.size() + 1000 + 500 + 1);
|
||||
for (std::size_t i = 0; i < entries.size(); ++i) {
|
||||
PayloadBuffer p = reader.readRange(layout[i].first, layout[i].second);
|
||||
CHECK(PayloadBuffer::alive() == 1); // one entry per readRange, no more
|
||||
CHECK(sameBytes(p, entries[i]));
|
||||
}
|
||||
CHECK(PayloadBuffer::alive() == 0);
|
||||
}
|
||||
|
||||
for (const std::string& s : srcs) removeQuietly(s);
|
||||
removeQuietly(dest);
|
||||
}
|
||||
|
||||
static void testEmptyPayloadIsRefusedAndPoisonsTheWriter() {
|
||||
// The failure this guards: an unreadable source yields an empty payload, and a
|
||||
// verb that trusted a `true` here would commit framing claiming bytes nobody wrote.
|
||||
const std::string dest = "pkg_io_emptypayload.rsbank";
|
||||
{
|
||||
PackageFileWriter writer(dest);
|
||||
const std::vector<std::uint8_t> head = patternBytes(4, 1);
|
||||
CHECK(writer.appendRaw(head.data(), head.size()));
|
||||
CHECK(!writer.appendPayload(PayloadBuffer{}));
|
||||
CHECK(!writer.ok());
|
||||
CHECK(!writer.commit()); // the short stream can never reach the destination
|
||||
}
|
||||
CHECK(!exists(dest));
|
||||
CHECK(!exists(dest + ".rsbanktmp"));
|
||||
// A zero-length appendRaw stays tolerated — framing has legitimate empty edges.
|
||||
{
|
||||
PackageFileWriter writer(dest);
|
||||
CHECK(writer.appendRaw(nullptr, 0));
|
||||
CHECK(writer.ok());
|
||||
writer.abort();
|
||||
}
|
||||
}
|
||||
|
||||
static void testAbandonedWriteLeavesNoDestination() {
|
||||
const std::string dest = "pkg_io_abandon.rsbank";
|
||||
{
|
||||
PackageFileWriter writer(dest);
|
||||
const std::vector<std::uint8_t> some = patternBytes(64, 9);
|
||||
CHECK(writer.appendRaw(some.data(), some.size()));
|
||||
// no commit — destruction is the injected interruption
|
||||
}
|
||||
CHECK(!exists(dest));
|
||||
CHECK(!exists(dest + ".rsbanktmp"));
|
||||
}
|
||||
|
||||
static void testAbortPreservesPriorContents() {
|
||||
const std::string dest = "pkg_io_prior.rsbank";
|
||||
const std::vector<std::uint8_t> prior = patternBytes(32, 0x40);
|
||||
writeScratchFile(dest, prior);
|
||||
{
|
||||
PackageFileWriter writer(dest);
|
||||
const std::vector<std::uint8_t> some = patternBytes(64, 9);
|
||||
CHECK(writer.appendRaw(some.data(), some.size()));
|
||||
writer.abort();
|
||||
CHECK(!writer.appendRaw(some.data(), some.size())); // dead after abort
|
||||
CHECK(!writer.commit());
|
||||
}
|
||||
CHECK(readAll(dest) == prior);
|
||||
CHECK(!exists(dest + ".rsbanktmp"));
|
||||
removeQuietly(dest);
|
||||
}
|
||||
|
||||
static void testCommitReplacesAnExistingFile() {
|
||||
// The module's one deliberate asymmetry against LandedFileJournal's never-overwrite
|
||||
// rule: the save dialog's overwrite confirm is the consent, so commit() replaces.
|
||||
const std::string dest = "pkg_io_replace.rsbank";
|
||||
const std::vector<std::uint8_t> prior = patternBytes(40, 0x11);
|
||||
const std::vector<std::uint8_t> fresh = patternBytes(7, 0x22);
|
||||
writeScratchFile(dest, prior);
|
||||
{
|
||||
PackageFileWriter writer(dest);
|
||||
CHECK(writer.ok());
|
||||
CHECK(writer.appendRaw(fresh.data(), fresh.size()));
|
||||
CHECK(writer.commit());
|
||||
}
|
||||
CHECK(readAll(dest) == fresh);
|
||||
CHECK(!exists(dest + ".rsbanktmp"));
|
||||
removeQuietly(dest);
|
||||
}
|
||||
|
||||
static void testOpenFailureIsInert() {
|
||||
const std::string dest = "pkg_io_no_such_dir/x.rsbank";
|
||||
PackageFileWriter writer(dest);
|
||||
CHECK(!writer.ok());
|
||||
const std::vector<std::uint8_t> some = patternBytes(8, 1);
|
||||
CHECK(!writer.appendRaw(some.data(), some.size()));
|
||||
CHECK(!writer.commit());
|
||||
CHECK(!exists("pkg_io_no_such_dir"));
|
||||
}
|
||||
|
||||
static void testCommitRenameFailureSelfCleans() {
|
||||
// A directory squatting on the destination makes the final rename fail — a real
|
||||
// injected commit failure, not a simulated one.
|
||||
const std::string dest = "pkg_io_dir.rsbank";
|
||||
std::error_code ec;
|
||||
fs::create_directory(utf8Path(dest), ec);
|
||||
CHECK(!ec);
|
||||
{
|
||||
PackageFileWriter writer(dest);
|
||||
CHECK(writer.ok());
|
||||
const std::vector<std::uint8_t> some = patternBytes(8, 1);
|
||||
CHECK(writer.appendRaw(some.data(), some.size()));
|
||||
CHECK(!writer.commit());
|
||||
}
|
||||
CHECK(fs::is_directory(utf8Path(dest))); // prior state intact
|
||||
CHECK(!exists(dest + ".rsbanktmp"));
|
||||
fs::remove(utf8Path(dest), ec);
|
||||
}
|
||||
|
||||
static void testReaderEdges() {
|
||||
PackageFileReader missing("pkg_io_no_such_file.rsbank");
|
||||
CHECK(!missing.ok());
|
||||
CHECK(missing.readRange(0, 1).empty());
|
||||
|
||||
const std::string path = "pkg_io_edges.bin";
|
||||
const std::vector<std::uint8_t> bytes = patternBytes(10, 5);
|
||||
writeScratchFile(path, bytes);
|
||||
{
|
||||
// Scoped: the reader must be closed before the cleanup remove below.
|
||||
PackageFileReader reader(path);
|
||||
CHECK(reader.ok());
|
||||
CHECK(reader.fileSize() == 10);
|
||||
CHECK(reader.readRange(5, 10).empty()); // past the end
|
||||
CHECK(reader.readRange(10, 1).empty()); // starts at the end
|
||||
CHECK(reader.readRange(0, 0).empty()); // zero length is failure, one branch
|
||||
CHECK(sameBytes(reader.readRange(2, 3),
|
||||
std::vector<std::uint8_t>(bytes.begin() + 2, bytes.begin() + 5)));
|
||||
}
|
||||
removeQuietly(path);
|
||||
}
|
||||
|
||||
static void testFileStatusSeparatesAbsentFromUnreadable() {
|
||||
CHECK(fileStatus("pkg_io_no_such_file.bin") == FileStatus::Absent);
|
||||
|
||||
const std::string path = "pkg_io_status.bin";
|
||||
writeScratchFile(path, patternBytes(4, 1));
|
||||
CHECK(fileStatus(path) == FileStatus::Present);
|
||||
removeQuietly(path);
|
||||
|
||||
// A directory in a file's place is not a readable file — the export message must
|
||||
// not report it as simply missing.
|
||||
const std::string dir = "pkg_io_status_dir";
|
||||
std::error_code ec;
|
||||
fs::create_directory(utf8Path(dir), ec);
|
||||
CHECK(fileStatus(dir) == FileStatus::Unreadable);
|
||||
fs::remove(utf8Path(dir), ec);
|
||||
}
|
||||
|
||||
static void testNonAsciiPathsRoundTripAsUtf8() {
|
||||
// "café" in UTF-8. Windows decodes a narrow std::filesystem path through the ANSI
|
||||
// code page, so an unconverted seam lands "café" or fails outright.
|
||||
const std::string dir = "pkg_io_caf\xC3\xA9_dir";
|
||||
const std::string name = "caf\xC3\xA9.rsbank";
|
||||
std::error_code ec;
|
||||
fs::create_directory(utf8Path(dir), ec);
|
||||
CHECK(!ec);
|
||||
|
||||
const std::string dest = dir + "/" + name;
|
||||
const std::vector<std::uint8_t> bytes = patternBytes(24, 0x5A);
|
||||
{
|
||||
PackageFileWriter writer(dest);
|
||||
CHECK(writer.ok());
|
||||
CHECK(writer.appendRaw(bytes.data(), bytes.size()));
|
||||
CHECK(writer.commit());
|
||||
}
|
||||
CHECK(exists(dest));
|
||||
CHECK(fileStatus(dest) == FileStatus::Present);
|
||||
CHECK(sameBytes(readFilePayload(dest), bytes));
|
||||
// The listing must hand the name back in the same encoding it was given.
|
||||
CHECK(listFolderFileNames(dir) == (std::vector<std::string>{name}));
|
||||
|
||||
fs::remove_all(utf8Path(dir), ec);
|
||||
}
|
||||
|
||||
static void testWriteFileExclusiveRefusesAnOccupiedPath() {
|
||||
const std::string path = "pkg_io_excl.bin";
|
||||
const std::vector<std::uint8_t> mine = patternBytes(12, 3);
|
||||
CHECK(writeFileExclusive(path, PayloadBuffer(mine)));
|
||||
CHECK(readAll(path) == mine);
|
||||
// The create IS the check: a second call cannot replace the first file's bytes.
|
||||
// (this pins the refusal, not O_EXCL's atomicity — a genuine race isn't portable)
|
||||
CHECK(!writeFileExclusive(path, PayloadBuffer(patternBytes(9, 8))));
|
||||
CHECK(readAll(path) == mine);
|
||||
CHECK(!writeFileExclusive("pkg_io_excl_empty.bin", PayloadBuffer{}));
|
||||
CHECK(!exists("pkg_io_excl_empty.bin"));
|
||||
removeQuietly(path);
|
||||
|
||||
// writeFileExclusive is the one call site using the wstring()/c_str() Windows
|
||||
// open form rather than the fstream(fs::path) overload every other test here
|
||||
// exercises — the only conversion whose reversion this file would otherwise miss.
|
||||
const std::string cafePath = "pkg_io_excl_caf\xC3\xA9.bin";
|
||||
const std::vector<std::uint8_t> cafeBytes = patternBytes(6, 4);
|
||||
CHECK(writeFileExclusive(cafePath, PayloadBuffer(cafeBytes)));
|
||||
CHECK(readAll(cafePath) == cafeBytes);
|
||||
CHECK(!writeFileExclusive(cafePath, PayloadBuffer(patternBytes(3, 9))));
|
||||
removeQuietly(cafePath);
|
||||
}
|
||||
|
||||
static void testListFolderFileNames() {
|
||||
const std::string dir = "pkg_io_listdir";
|
||||
std::error_code ec;
|
||||
fs::create_directory(utf8Path(dir), ec);
|
||||
writeScratchFile(dir + "/b.bin", patternBytes(2, 1));
|
||||
writeScratchFile(dir + "/a.bin", patternBytes(2, 2));
|
||||
fs::create_directory(utf8Path(dir + "/sub"), ec);
|
||||
writeScratchFile(dir + "/sub/c.bin", patternBytes(2, 3));
|
||||
|
||||
const std::vector<std::string> names = listFolderFileNames(dir);
|
||||
CHECK(names == (std::vector<std::string>{"a.bin", "b.bin"})); // sorted, bare, non-recursive
|
||||
CHECK(listFolderFileNames("pkg_io_no_such_dir").empty());
|
||||
|
||||
fs::remove_all(utf8Path(dir), ec);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testPayloadCounterTracksMovesNotCopies();
|
||||
testStreamingRoundTripHoldsOnePayload();
|
||||
testEmptyPayloadIsRefusedAndPoisonsTheWriter();
|
||||
testAbandonedWriteLeavesNoDestination();
|
||||
testAbortPreservesPriorContents();
|
||||
testCommitReplacesAnExistingFile();
|
||||
testOpenFailureIsInert();
|
||||
testCommitRenameFailureSelfCleans();
|
||||
testReaderEdges();
|
||||
testFileStatusSeparatesAbsentFromUnreadable();
|
||||
testNonAsciiPathsRoundTripAsUtf8();
|
||||
testWriteFileExclusiveRefusesAnOccupiedPath();
|
||||
testListFolderFileNames();
|
||||
|
||||
if (g_fail == 0) std::printf("package_io: all tests passed\n");
|
||||
else std::printf("package_io: %d CHECK(s) FAILED\n", g_fail);
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// 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 <algorithm>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<std::uint8_t> patternBytes(std::size_t n, std::uint8_t seed) {
|
||||
std::vector<std::uint8_t> v(n);
|
||||
for (std::size_t i = 0; i < n; ++i)
|
||||
v[i] = static_cast<std::uint8_t>(seed + i * 7u);
|
||||
return v;
|
||||
}
|
||||
|
||||
static void writeScratchFile(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()));
|
||||
}
|
||||
|
||||
static std::vector<std::uint8_t> readAll(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 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<std::uint8_t> 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<std::uint8_t> 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<std::uint8_t> 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<std::uint8_t> 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;
|
||||
}
|
||||
Reference in New Issue
Block a user