229 lines
8.4 KiB
C++
229 lines
8.4 KiB
C++
// Standalone tests for shell/package/package_io — no REAPER, no framework. Pins the
|
|
// two properties the seam exists for: an interrupted or failed write leaves the
|
|
// destination absent or holding its prior contents (failure injected at the writer
|
|
// seam — abandonment, open failure, rename failure), and a multi-entry round trip
|
|
// holds at most one entry's payload, asserted against PayloadBuffer::alive() — the
|
|
// seam counter — rather than a memory measurement.
|
|
|
|
#include "../src/shell/package/package_io.h"
|
|
|
|
#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 void writeScratchFile(const std::string& path,
|
|
const std::vector<std::uint8_t>& bytes) {
|
|
std::ofstream f(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(path, std::ios::binary);
|
|
return std::vector<std::uint8_t>(std::istreambuf_iterator<char>(f),
|
|
std::istreambuf_iterator<char>());
|
|
}
|
|
|
|
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(p.bytes() == 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(!fs::exists(dest + ".rsbanktmp"));
|
|
CHECK(fs::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(p.bytes() == entries[i]);
|
|
}
|
|
CHECK(PayloadBuffer::alive() == 0);
|
|
}
|
|
|
|
std::error_code ec;
|
|
for (const std::string& s : srcs) fs::remove(s, ec);
|
|
fs::remove(dest, ec);
|
|
}
|
|
|
|
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(!fs::exists(dest));
|
|
CHECK(!fs::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(!fs::exists(dest + ".rsbanktmp"));
|
|
std::error_code ec;
|
|
fs::remove(dest, ec);
|
|
}
|
|
|
|
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(!fs::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(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(dest)); // prior state intact
|
|
CHECK(!fs::exists(dest + ".rsbanktmp"));
|
|
fs::remove(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
|
|
const PayloadBuffer slice = reader.readRange(2, 3);
|
|
CHECK(slice.bytes() ==
|
|
std::vector<std::uint8_t>(bytes.begin() + 2, bytes.begin() + 5));
|
|
}
|
|
std::error_code ec;
|
|
fs::remove(path, ec);
|
|
}
|
|
|
|
static void testListFolderFileNames() {
|
|
const std::string dir = "pkg_io_listdir";
|
|
std::error_code ec;
|
|
fs::create_directory(dir, ec);
|
|
writeScratchFile(dir + "/b.bin", patternBytes(2, 1));
|
|
writeScratchFile(dir + "/a.bin", patternBytes(2, 2));
|
|
fs::create_directory(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(dir, ec);
|
|
}
|
|
|
|
int main() {
|
|
testPayloadCounterTracksMovesNotCopies();
|
|
testStreamingRoundTripHoldsOnePayload();
|
|
testAbandonedWriteLeavesNoDestination();
|
|
testAbortPreservesPriorContents();
|
|
testOpenFailureIsInert();
|
|
testCommitRenameFailureSelfCleans();
|
|
testReaderEdges();
|
|
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;
|
|
}
|