yams/tests/unit/downloader/downloader_catch2_test.cpp
Trevon 0dd3985db4
refactor: simplify runtime paths and harden retrieval (#61)
* refactor(tuning): prune dormant runtime knobs

Remove unused tuning controls and adaptive search budget state. Extract testable ONNX slot reconciliation and search telemetry hydration seams, and correct the Apple Silicon sysctl value width.

* refactor(vector): retire dormant backends

Remove unused FAISS, model-infrastructure, VectorIndexManager, and TurboQuant paths while keeping Simeon PQ and float-vector storage.

Migrate unused V2.2 quantized schemas transactionally to V2.3. Databases containing non-default quantized formats or packed-code blobs fail closed with recovery guidance and no mutation.

* refactor(plugins): remove legacy SDK tooling

Remove the Ghidra and SDK submodules, their bespoke tests, and stale scan exclusions. Retain generic PluginProcess coverage against the repository mock fixture.

* build(vector): remove stale FAISS overrides

* refactor(ipc): remove dormant FSM snapshots

* refactor(ingest): unify bounded KG enqueue

* fix(plugins): harden allocation contracts

Keep ABI outputs null or fully initialized when allocations fail, and represent empty object payloads without relying on malloc(0).

* fix(storage): define empty object reads

Return a null buffer with zero length for successful empty-object reads instead of treating malloc(0) as an allocation contract.

* refactor(storage): trim through string views

* test: prune dormant harness helpers

Remove unused readiness and fixture helpers, and keep benchmark client-count doubling in a wide integer domain.

* chore(cpp): initialize analyzer-facing locals

* build(simeon): advance analyzer cleanup

* refactor(config): drop dead daemon client keys

* refactor(ipc): inject daemon client transport

Socket and in-process execution now share DaemonClient request dispatch, so CLI fallbacks do not need alternate service paths.

* refactor(retrieval): route CLI through daemon

Search and grep now use the same daemon handlers over socket or in-process transport. Live grep remains an explicit freshness inspection.

* refactor(graph): move CLI scope into daemon

Graph CLI requests now share daemon handlers across socket and in-process transports. CWD filtering occurs before pagination so scoped totals and pages stay correct.

* build: isolate test-only runtime controls

* refactor(metadata): decouple lock telemetry

* fix(services): return on request timeout

* fix(retrieval): enforce workspace scope

* fix(graph): scope nodes before pagination

* fix(grep): drop binary result lines

* fix(daemon): bound checkpoint wait

* fix(repair): use KG health checks

* fix(storage): always verify compressed CRC

* perf(vector): skip unchanged PQ snapshots

* build(zyp): exclude bench from default build

* build: protect Meson files from formatter

* refactor: remove unreachable production APIs

* refactor(daemon): remove dormant runtime state

* fix(vector): harden daemon provider calls

Timed-out coroutines must own their client and requests. Provider discovery must also fail instead of returning synthetic embeddings in production.

* refactor(storage): remove unusable adapters

* fix(cli): unify in-process add requests

* fix(tsan): preserve async worker progress

* test(cli): isolate daemon-dependent suites

* fix(repair): expose queue backoff progress
2026-07-31 14:16:45 -04:00

1216 lines
44 KiB
C++

// Catch2 migration of test_downloader_basic.cpp with expanded test coverage
// Epic: yams-3s4 | Migration: downloader tests
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <catch2/matchers/catch_matchers_vector.hpp>
#include <nlohmann/json.hpp>
#include <yams/downloader/downloader.hpp>
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cstddef>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <memory>
#include <optional>
#include <random>
#include <string>
#include <string_view>
#include <vector>
namespace fs = std::filesystem;
using json = nlohmann::json;
using namespace yams::downloader;
namespace yams::downloader {
std::unique_ptr<IDiskWriter> makeDiskWriter();
std::unique_ptr<IIntegrityVerifier> makeIntegrityVerifierSha256Only();
std::unique_ptr<IResumeStore> makeInMemoryResumeStore();
std::unique_ptr<IRateLimiter> makeRateLimiter();
} // namespace yams::downloader
namespace {
// Catch2 assertion macros decompose comparisons for diagnostics, which triggers
// clang-tidy chained-comparison false positives. This migrated test also has a
// few harmless integer-size fixture constants.
// NOLINTBEGIN(bugprone-chained-comparison, bugprone-implicit-widening-of-multiplication-result)
// Helper to generate a unique temporary directory for tests
fs::path make_temp_dir(const std::string& prefix = "yams-dl-test-") {
auto base = fs::temp_directory_path();
std::random_device rd;
std::mt19937_64 gen(rd());
std::uniform_int_distribution<unsigned long long> dist;
fs::path dir;
for (int i = 0; i < 5; ++i) {
dir = base / (prefix + std::to_string(dist(gen)));
if (!fs::exists(dir)) {
fs::create_directories(dir);
break;
}
}
return dir;
}
// HTTP metadata parser helper
struct ParsedHttpMeta {
std::optional<std::string> etag;
std::optional<std::string> lastModified;
};
static inline std::string trim(std::string_view sv) {
size_t b = 0, e = sv.size();
while (b < e && std::isspace(static_cast<unsigned char>(sv[b])))
++b;
while (e > b && std::isspace(static_cast<unsigned char>(sv[e - 1])))
--e;
return std::string(sv.substr(b, e - b));
}
ParsedHttpMeta parse_http_headers_for_meta(const std::vector<std::string>& headers) {
ParsedHttpMeta out;
for (const auto& raw : headers) {
auto pos = raw.find(':');
if (pos == std::string::npos)
continue;
std::string key = trim(std::string_view(raw.data(), pos));
std::string val = trim(std::string_view(raw.data() + pos + 1, raw.size() - pos - 1));
std::transform(key.begin(), key.end(), key.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (key == "etag" && !out.etag.has_value()) {
if (val.size() >= 2 && ((val.front() == '"' && val.back() == '"') ||
(val.front() == '\'' && val.back() == '\''))) {
out.etag = val.substr(1, val.size() - 2);
} else {
out.etag = val;
}
} else if (key == "last-modified" && !out.lastModified.has_value()) {
out.lastModified = val;
}
}
return out;
}
// Persistent JSON-based ResumeStore (test implementation)
class PersistentResumeStore final : public yams::downloader::IResumeStore {
public:
explicit PersistentResumeStore(fs::path json_path) : path_(std::move(json_path)) {
(void)loadFile();
}
yams::downloader::Expected<std::optional<State>> load(std::string_view url) override {
auto j = loadFile();
if (!j.ok())
return j.error();
auto& root = j.value();
const auto it = root.find(std::string(url));
if (it == root.end())
return std::optional<State>{std::nullopt};
State st;
const auto& entry = it.value();
if (entry.contains("etag") && entry["etag"].is_string()) {
st.etag = entry["etag"].get<std::string>();
}
if (entry.contains("last_modified") && entry["last_modified"].is_string()) {
st.lastModified = entry["last_modified"].get<std::string>();
}
if (entry.contains("total_bytes") && entry["total_bytes"].is_number_unsigned()) {
st.totalBytes = entry["total_bytes"].get<std::uint64_t>();
}
if (entry.contains("completed_ranges") && entry["completed_ranges"].is_array()) {
for (const auto& r : entry["completed_ranges"]) {
if (r.is_array() && r.size() == 2 && r[0].is_number_unsigned() &&
r[1].is_number_unsigned()) {
st.completedRanges.emplace_back(r[0].get<std::uint64_t>(),
r[1].get<std::uint64_t>());
}
}
}
return std::optional<State>{st};
}
yams::downloader::Expected<void> save(std::string_view url, const State& state) override {
auto j = loadFile();
if (!j.ok())
return j.error();
auto& root = j.value();
json entry;
if (state.etag)
entry["etag"] = *state.etag;
if (state.lastModified)
entry["last_modified"] = *state.lastModified;
entry["total_bytes"] = state.totalBytes;
entry["completed_ranges"] = json::array();
for (const auto& [off, len] : state.completedRanges) {
entry["completed_ranges"].push_back(json::array({off, len}));
}
root[std::string(url)] = entry;
return writeFile(root);
}
void remove(std::string_view url) noexcept override {
auto j = loadFile();
if (!j.ok())
return;
auto& root = j.value();
root.erase(std::string(url));
(void)writeFile(root);
}
private:
yams::downloader::Expected<json> loadFile() const {
if (!fs::exists(path_)) {
return json::object();
}
std::ifstream in(path_);
if (!in) {
return yams::downloader::Error{yams::downloader::ErrorCode::IoError,
"Failed to open resume JSON for read"};
}
try {
json root;
in >> root;
if (!root.is_object())
root = json::object();
return root;
} catch (const std::exception&) {
return json::object();
}
}
yams::downloader::Expected<void> writeFile(const json& root) const {
std::ofstream out(path_, std::ios::trunc);
if (!out) {
return yams::downloader::Error{yams::downloader::ErrorCode::IoError,
"Failed to open resume JSON for write"};
}
out << root.dump(2);
return {};
}
fs::path path_;
};
// Tracking resume store for test verification
class TrackingResumeStore final : public IResumeStore {
public:
std::optional<State> initialState;
std::vector<State> savedStates;
std::vector<std::string> savedUrls;
std::vector<std::string> loadUrls;
std::optional<std::string> removedUrl;
Expected<std::optional<State>> load(std::string_view url) override {
loadUrls.emplace_back(url);
return initialState;
}
Expected<void> save(std::string_view url, const State& state) override {
savedUrls.emplace_back(url);
savedStates.push_back(state);
initialState = state;
return {};
}
void remove(std::string_view url) noexcept override {
removedUrl = std::string(url);
initialState.reset();
}
};
// Test HTTP adapter
class TestHttpAdapter final : public IHttpAdapter {
public:
bool resumeSupported{true};
std::optional<std::uint64_t> contentLength{};
std::optional<std::string> etag{};
std::optional<std::string> lastModified{};
std::vector<std::uint64_t> requestedOffsets;
std::vector<std::uint64_t> requestedSizes;
std::vector<std::byte> payload;
bool checkCancelBeforeSink{false};
bool cancelObserved{false};
bool lastProbeFollowRedirects{true};
int probeCallCount{0};
int fetchCallCount{0};
Expected<void> probe(std::string_view, const std::vector<Header>&, bool& resumeSupportedOut,
std::optional<std::uint64_t>& contentLengthOut,
std::optional<std::string>& etagOut,
std::optional<std::string>& lastModifiedOut,
std::optional<std::string>& contentTypeOut,
std::optional<std::string>& suggestedFilenameOut, const TlsConfig&,
const std::optional<std::string>&, bool followRedirects,
std::chrono::milliseconds) override {
probeCallCount++;
lastProbeFollowRedirects = followRedirects;
resumeSupportedOut = resumeSupported;
contentLengthOut = contentLength;
etagOut = etag;
lastModifiedOut = lastModified;
contentTypeOut.reset();
suggestedFilenameOut.reset();
return {};
}
Expected<void> fetchRange(std::string_view, const std::vector<Header>&, std::uint64_t offset,
std::uint64_t size, const TlsConfig&,
const std::optional<std::string>&, std::chrono::milliseconds,
const std::function<Expected<void>(std::span<const std::byte>)>& sink,
const ShouldCancel& shouldCancel,
const ProgressCallback& onProgress) override {
fetchCallCount++;
requestedOffsets.push_back(offset);
requestedSizes.push_back(size);
if (onProgress) {
ProgressEvent ev;
ev.downloadedBytes = 0;
ev.stage = ProgressStage::Downloading;
onProgress(ev);
}
if (checkCancelBeforeSink && shouldCancel && shouldCancel()) {
cancelObserved = true;
return Error{ErrorCode::OperationCancelled, "Operation cancelled"};
}
if (!payload.empty()) {
auto res = sink(std::span<const std::byte>(payload.data(), payload.size()));
if (!res.ok())
return res;
}
return {};
}
};
// Fake disk writer for testing
class FakeDiskWriter final : public IDiskWriter {
public:
explicit FakeDiskWriter(StorageConfig storage) : storage_(std::move(storage)) {}
void setInitialData(const std::vector<std::byte>& data) { initialData_ = data; }
const std::vector<std::byte>& stagedData() const { return data_; }
const fs::path& lastFinalPath() const { return lastFinalPath_; }
const std::string& lastSessionId() const { return lastSessionId_; }
Expected<fs::path> createStagingFile(const StorageConfig&, std::string_view sessionId,
std::string_view tempExtension) override {
lastSessionId_ = std::string(sessionId);
currentPath_ = stagingPath(sessionId, tempExtension);
data_.clear();
writeToFile();
return currentPath_;
}
Expected<fs::path> createOrOpenStagingFile(const StorageConfig&, std::string_view sessionId,
std::string_view tempExtension,
std::optional<std::uint64_t> expectedSize,
std::uint64_t& currentSize) override {
lastSessionId_ = std::string(sessionId);
currentPath_ = stagingPath(sessionId, tempExtension);
data_ = initialData_;
currentSize = data_.size();
writeToFile();
if (expectedSize && currentSize == 0) {
std::error_code ec;
std::filesystem::resize_file(currentPath_, static_cast<std::uint64_t>(*expectedSize),
ec);
if (!ec) {
data_.resize(static_cast<std::size_t>(*expectedSize));
writeToFile();
}
}
return currentPath_;
}
Expected<void> writeAt(const fs::path&, std::uint64_t offset,
std::span<const std::byte> data) override {
const auto end = offset + static_cast<std::uint64_t>(data.size());
if (end > data_.size()) {
data_.resize(static_cast<std::size_t>(end));
}
std::memcpy(data_.data() + offset, data.data(), data.size());
writeToFile();
return {};
}
Expected<void> sync(const fs::path&, const StorageConfig&) override { return {}; }
Expected<fs::path> finalizeToCas(const fs::path&, std::string_view hashHex,
const StorageConfig&) override {
lastFinalPath_ = casPathForSha256(storage_.objectsDir, hashHex);
std::error_code ec;
std::filesystem::create_directories(lastFinalPath_.parent_path(), ec);
std::ofstream out(lastFinalPath_, std::ios::binary | std::ios::trunc);
if (!out) {
return Error{ErrorCode::IoError,
"Failed to write CAS object: " + lastFinalPath_.string()};
}
if (!data_.empty()) {
out.write(reinterpret_cast<const char*>(data_.data()),
static_cast<std::streamsize>(data_.size()));
}
return lastFinalPath_;
}
void cleanup(const fs::path& stagingFile) noexcept override {
std::error_code ec;
std::filesystem::remove(stagingFile, ec);
}
private:
fs::path stagingPath(std::string_view sessionId, std::string_view ext) const {
fs::path dir = storage_.stagingDir / "downloader";
std::filesystem::create_directories(dir);
std::string fn(sessionId);
if (!ext.empty()) {
if (ext.front() == '.')
fn.append(ext);
else {
fn.push_back('.');
fn.append(ext);
}
} else {
fn.append(".part");
}
return dir / fn;
}
void writeToFile() {
if (currentPath_.empty())
return;
std::error_code ec;
std::filesystem::create_directories(currentPath_.parent_path(), ec);
std::ofstream out(currentPath_, std::ios::binary | std::ios::trunc);
if (!out)
return;
if (!data_.empty()) {
out.write(reinterpret_cast<const char*>(data_.data()),
static_cast<std::streamsize>(data_.size()));
}
}
StorageConfig storage_;
std::vector<std::byte> initialData_{};
std::vector<std::byte> data_{};
fs::path currentPath_{};
fs::path lastFinalPath_{};
std::string lastSessionId_{};
};
std::vector<std::byte> to_bytes(std::string_view s) {
std::vector<std::byte> out(s.size());
std::memcpy(out.data(), s.data(), s.size());
return out;
}
constexpr const char kExportMetaSuffix[] = ".yams.meta.json";
fs::path meta_path_for(const fs::path& exportPath) {
return fs::path(exportPath.string() + kExportMetaSuffix);
}
} // namespace
// =============================================================================
// HTTP Header Parsing Tests
// =============================================================================
TEST_CASE("HTTP Header Parsing: Basic extraction", "[downloader][http][headers]") {
SECTION("Extracts ETag and Last-Modified") {
std::vector<std::string> headers = {"Date: Tue, 19 Aug 2025 10:11:12 GMT",
"Server: test-http",
"ETag: \"abc123-xyz\"",
"Last-Modified: Tue, 19 Aug 2025 09:00:00 GMT",
"Accept-Ranges: bytes",
"Content-Length: 1048576"};
auto parsed = parse_http_headers_for_meta(headers);
REQUIRE(parsed.etag.has_value());
REQUIRE(parsed.lastModified.has_value());
CHECK((*parsed.etag == "abc123-xyz"));
CHECK((*parsed.lastModified == "Tue, 19 Aug 2025 09:00:00 GMT"));
}
SECTION("Handles missing values") {
std::vector<std::string> headers = {"date: Tue, 19 Aug 2025 10:11:12 GMT",
"server: test-http", "content-length: 512"};
auto parsed = parse_http_headers_for_meta(headers);
CHECK_FALSE(parsed.etag.has_value());
CHECK_FALSE(parsed.lastModified.has_value());
}
SECTION("Case insensitive header names") {
std::vector<std::string> headers = {"eTaG: 'W/\"weak-etag-value\"'",
"LAST-MODIFIED: Wed, 20 Aug 2025 00:00:00 GMT "};
auto parsed = parse_http_headers_for_meta(headers);
REQUIRE(parsed.etag.has_value());
REQUIRE(parsed.lastModified.has_value());
CHECK((*parsed.etag == "W/\"weak-etag-value\""));
CHECK((*parsed.lastModified == "Wed, 20 Aug 2025 00:00:00 GMT"));
}
SECTION("First occurrence wins") {
std::vector<std::string> headers = {"ETag: first-etag", "ETag: second-etag"};
auto parsed = parse_http_headers_for_meta(headers);
REQUIRE(parsed.etag.has_value());
CHECK((*parsed.etag == "first-etag"));
}
SECTION("Strips quotes from ETag") {
std::vector<std::string> headers = {"ETag: \"quoted-etag\""};
auto parsed = parse_http_headers_for_meta(headers);
REQUIRE(parsed.etag.has_value());
CHECK((*parsed.etag == "quoted-etag"));
}
SECTION("Handles unquoted ETag") {
std::vector<std::string> headers = {"ETag: unquoted-etag"};
auto parsed = parse_http_headers_for_meta(headers);
REQUIRE(parsed.etag.has_value());
CHECK((*parsed.etag == "unquoted-etag"));
}
}
// =============================================================================
// Persistent Resume Store Tests
// =============================================================================
TEST_CASE("PersistentResumeStore: Round-trip", "[downloader][resume]") {
auto dir = make_temp_dir();
auto jsonPath = dir / "resume.json";
SECTION("Save and load single URL") {
PersistentResumeStore store(jsonPath);
yams::downloader::IResumeStore::State state;
state.etag = std::string("abc123etag");
state.lastModified = std::string("Tue, 19 Aug 2025 09:00:00 GMT");
state.totalBytes = 1024ull * 1024ull * 5ull;
state.completedRanges = {{0, 256ull * 1024ull}, {256ull * 1024ull, 512ull * 1024ull}};
auto saveRes = store.save("https://example.com/file.bin", state);
REQUIRE(saveRes.ok());
PersistentResumeStore store2(jsonPath);
auto loadRes = store2.load("https://example.com/file.bin");
REQUIRE(loadRes.ok());
REQUIRE(loadRes.value().has_value());
const auto& got = *loadRes.value();
REQUIRE(got.etag.has_value());
REQUIRE(got.lastModified.has_value());
CHECK((*got.etag == *state.etag));
CHECK((*got.lastModified == *state.lastModified));
CHECK((got.totalBytes == state.totalBytes));
REQUIRE((got.completedRanges.size() == state.completedRanges.size()));
}
SECTION("Remove entry") {
PersistentResumeStore store(jsonPath);
yams::downloader::IResumeStore::State state;
state.etag = std::string("to-be-removed");
state.totalBytes = 42;
REQUIRE(store.save("http://example.com/a", state).ok());
REQUIRE(store.save("http://example.com/b", state).ok());
store.remove("http://example.com/a");
PersistentResumeStore store2(jsonPath);
auto la = store2.load("http://example.com/a");
auto lb = store2.load("http://example.com/b");
REQUIRE(la.ok());
REQUIRE(lb.ok());
CHECK_FALSE(la.value().has_value());
REQUIRE(lb.value().has_value());
CHECK(lb.value()->etag.has_value());
CHECK((*lb.value()->etag == "to-be-removed"));
}
SECTION("Multiple URLs") {
PersistentResumeStore store(jsonPath);
for (int i = 0; i < 5; ++i) {
yams::downloader::IResumeStore::State state;
state.etag = "etag-" + std::to_string(i);
state.totalBytes = 1000L * (i + 1);
REQUIRE(store.save("http://example.com/file" + std::to_string(i), state).ok());
}
PersistentResumeStore store2(jsonPath);
for (int i = 0; i < 5; ++i) {
auto result = store2.load("http://example.com/file" + std::to_string(i));
REQUIRE(result.ok());
REQUIRE(result.value().has_value());
CHECK((*result.value()->etag == "etag-" + std::to_string(i)));
}
}
SECTION("Load non-existent URL") {
PersistentResumeStore store(jsonPath);
auto result = store.load("http://nonexistent.com/file");
REQUIRE(result.ok());
CHECK_FALSE(result.value().has_value());
}
// Cleanup
std::error_code ec;
fs::remove_all(dir, ec);
}
// =============================================================================
// CAS Helpers Tests
// =============================================================================
TEST_CASE("CAS Helpers: Path and ID formatting", "[downloader][cas]") {
fs::path objectsDir = "/tmp/yams-objects";
const std::string digest = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
SECTION("CAS path includes shard directories") {
auto path = yams::downloader::casPathForSha256(objectsDir, digest);
CHECK((path.generic_string().find("sha256/01/23/0123456789abcdef") != std::string::npos));
}
SECTION("SHA256 ID format") {
auto id = yams::downloader::makeSha256Id(digest);
CHECK((id == "sha256:" + digest));
}
SECTION("Empty digest") {
auto id = yams::downloader::makeSha256Id("");
CHECK((id == "sha256:"));
}
}
// =============================================================================
// Download Manager Resume Tests
// =============================================================================
TEST_CASE("DownloadManager: Resume functionality", "[downloader][resume]") {
auto tempDir = make_temp_dir();
StorageConfig storage{tempDir / "objects", tempDir / "staging"};
DownloaderConfig cfg;
cfg.defaultChunkSizeBytes = 4;
SECTION("Persists and cleans ranges") {
auto http = std::make_unique<TestHttpAdapter>();
auto* httpPtr = http.get();
httpPtr->resumeSupported = true;
httpPtr->contentLength = 10;
httpPtr->etag = "etag-123";
httpPtr->payload = to_bytes("WORLD");
auto disk = std::make_unique<FakeDiskWriter>(storage);
auto* diskPtr = disk.get();
diskPtr->setInitialData(to_bytes("HELLO"));
auto resume = std::make_unique<TrackingResumeStore>();
auto* resumePtr = resume.get();
IResumeStore::State initial;
initial.totalBytes = 10;
initial.completedRanges.emplace_back(0, 5);
initial.etag = "etag-123";
resumePtr->initialState = initial;
auto dm = makeDownloadManagerWithDependencies(
storage, cfg, std::move(http), std::move(disk), nullptr, std::move(resume), nullptr);
DownloadRequest req;
req.url = "https://example.com/file.bin";
req.resume = true;
req.chunkSizeBytes = 5;
req.checksum = Checksum{HashAlgo::Sha256,
"0b21b7db59cd154904fac6336fa7d2be1bab38d632794f281549584068cdcb74"};
auto result = dm->download(req);
REQUIRE(result.ok());
auto final = result.value();
REQUIRE((httpPtr->requestedOffsets.size() == 1));
CHECK((httpPtr->requestedOffsets.front() == 5));
REQUIRE_FALSE(resumePtr->savedStates.empty());
const auto& lastState = resumePtr->savedStates.back();
REQUIRE((lastState.completedRanges.size() == 1));
CHECK((lastState.completedRanges.front().first == 0));
CHECK((lastState.completedRanges.front().second == 10));
REQUIRE(resumePtr->removedUrl.has_value());
CHECK((*resumePtr->removedUrl == req.url));
REQUIRE_FALSE(diskPtr->lastFinalPath().empty());
std::ifstream finalFile(diskPtr->lastFinalPath(), std::ios::binary);
REQUIRE(finalFile.good());
std::string contents((std::istreambuf_iterator<char>(finalFile)),
std::istreambuf_iterator<char>());
CHECK((contents == "HELLOWORLD"));
CHECK(final.hash ==
"sha256:0b21b7db59cd154904fac6336fa7d2be1bab38d632794f281549584068cdcb74");
CHECK((final.sizeBytes == 10));
}
SECTION("Uses a stable resume session id for the same URL") {
auto makeResumingManager = [&](FakeDiskWriter*& diskPtr) {
auto http = std::make_unique<TestHttpAdapter>();
auto* httpPtr = http.get();
httpPtr->resumeSupported = true;
httpPtr->contentLength = 10;
httpPtr->etag = "etag-123";
httpPtr->payload = to_bytes("WORLD");
auto disk = std::make_unique<FakeDiskWriter>(storage);
diskPtr = disk.get();
diskPtr->setInitialData(to_bytes("HELLO"));
auto resume = std::make_unique<TrackingResumeStore>();
IResumeStore::State initial;
initial.totalBytes = 10;
initial.completedRanges.emplace_back(0, 5);
initial.etag = "etag-123";
resume->initialState = initial;
return makeDownloadManagerWithDependencies(storage, cfg, std::move(http),
std::move(disk), nullptr, std::move(resume),
nullptr);
};
FakeDiskWriter* firstDisk = nullptr;
auto firstManager = makeResumingManager(firstDisk);
DownloadRequest req;
req.url = "https://example.com/file.bin";
req.resume = true;
auto firstResult = firstManager->download(req);
REQUIRE(firstResult.ok());
REQUIRE_FALSE(firstDisk->lastSessionId().empty());
FakeDiskWriter* secondDisk = nullptr;
auto secondManager = makeResumingManager(secondDisk);
auto secondResult = secondManager->download(req);
REQUIRE(secondResult.ok());
REQUIRE_FALSE(secondDisk->lastSessionId().empty());
CHECK((secondDisk->lastSessionId() == firstDisk->lastSessionId()));
}
// Cleanup
std::error_code ec;
fs::remove_all(tempDir, ec);
}
TEST_CASE("DownloadManager: probe forwards follow_redirects policy", "[downloader][redirect]") {
auto tempDir = make_temp_dir();
StorageConfig storage{tempDir / "objects", tempDir / "staging"};
DownloaderConfig cfg;
auto http = std::make_unique<TestHttpAdapter>();
auto* httpPtr = http.get();
httpPtr->resumeSupported = false;
httpPtr->contentLength = 5;
httpPtr->payload = to_bytes("ABCDE");
auto disk = std::make_unique<FakeDiskWriter>(storage);
auto dm = makeDownloadManagerWithDependencies(storage, cfg, std::move(http), std::move(disk),
nullptr, nullptr, nullptr);
DownloadRequest req;
req.url = "https://example.com/no-redirects.bin";
req.followRedirects = false;
auto result = dm->download(req);
REQUIRE(result.ok());
CHECK_FALSE(httpPtr->lastProbeFollowRedirects);
std::error_code ec;
fs::remove_all(tempDir, ec);
}
TEST_CASE("DownloadManager: verifies configured checksum algorithms",
"[downloader][checksum][algorithms]") {
const auto sha256 =
std::string{"f0393febe8baaa55e32f7be2a7cc180bf34e52137d99e056c817a9c07b8f239a"};
const auto sha512 =
std::string{"9989a8fcbc29044b5883a0a36c146fe7415b1439e995b4d806ea0af7da9ca4390eb92a604b3ecf"
"a3d75f9911c768fbe2aecc59eff1e48dcaeca1957bdde01dfb"};
const auto md5 = std::string{"2ecdde3959051d913f61b14579ea136d"};
auto runCase = [&](HashAlgo algo, const std::string& expectedHex) {
auto tempDir = make_temp_dir();
StorageConfig storage{tempDir / "objects", tempDir / "staging"};
DownloaderConfig cfg;
auto http = std::make_unique<TestHttpAdapter>();
http->resumeSupported = false;
http->contentLength = 5;
http->payload = to_bytes("ABCDE");
auto disk = std::make_unique<FakeDiskWriter>(storage);
auto dm = makeDownloadManagerWithDependencies(storage, cfg, std::move(http),
std::move(disk), nullptr, nullptr, nullptr);
DownloadRequest req;
req.url = "https://example.com/checksum.bin";
req.checksum = Checksum{algo, expectedHex};
auto result = dm->download(req);
REQUIRE(result.ok());
CHECK(result.value().success);
std::error_code ec;
fs::remove_all(tempDir, ec);
};
SECTION("sha256") {
runCase(HashAlgo::Sha256, sha256);
}
SECTION("sha512") {
runCase(HashAlgo::Sha512, sha512);
}
SECTION("md5") {
runCase(HashAlgo::Md5, md5);
}
}
TEST_CASE("DownloadManager: Export with ETag matching", "[downloader][export]") {
auto tempDir = make_temp_dir();
StorageConfig storage{tempDir / "objects", tempDir / "staging"};
DownloaderConfig cfg;
SECTION("Skips copy when ETag matches") {
auto http = std::make_unique<TestHttpAdapter>();
auto* httpPtr = http.get();
httpPtr->resumeSupported = false;
httpPtr->contentLength = 5;
httpPtr->etag = "tag-777";
httpPtr->payload = to_bytes("ABCDE");
auto disk = std::make_unique<FakeDiskWriter>(storage);
auto* diskPtr = disk.get();
auto resume = std::make_unique<TrackingResumeStore>();
auto* resumePtr = resume.get();
auto dm = makeDownloadManagerWithDependencies(
storage, cfg, std::move(http), std::move(disk), nullptr, std::move(resume), nullptr);
fs::path exportDir = tempDir / "exports";
fs::create_directories(exportDir);
fs::path exportPath = exportDir / "file.bin";
{
std::ofstream out(exportPath, std::ios::binary | std::ios::trunc);
out << "OLD";
}
auto metaPath = meta_path_for(exportPath);
{
json meta;
meta["etag"] = "tag-777";
meta["hash"] = "placeholder";
std::ofstream out(metaPath, std::ios::binary | std::ios::trunc);
out << meta.dump(2);
}
DownloadRequest req;
req.url = "https://example.com/export.bin";
req.exportPath = exportPath;
req.overwrite = OverwritePolicy::IfDifferentEtag;
auto result = dm->download(req);
REQUIRE(result.ok());
auto final = result.value();
std::string exported;
{
std::ifstream in(exportPath, std::ios::binary);
exported.assign((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
}
CHECK((exported == "OLD"));
json metaUpdated;
{
std::ifstream in(metaPath);
REQUIRE(in.good());
in >> metaUpdated;
}
REQUIRE(metaUpdated.contains("hash"));
REQUIRE(metaUpdated.contains("etag"));
CHECK((metaUpdated["etag"].get<std::string>() == "tag-777"));
REQUIRE_FALSE(diskPtr->lastFinalPath().empty());
std::ifstream casFile(diskPtr->lastFinalPath(), std::ios::binary);
std::string casData((std::istreambuf_iterator<char>(casFile)),
std::istreambuf_iterator<char>());
CHECK((casData == "ABCDE"));
CHECK(resumePtr->savedStates.empty());
CHECK((httpPtr->requestedOffsets.size() == 1));
}
// Cleanup
std::error_code ec;
fs::remove_all(tempDir, ec);
}
TEST_CASE("DownloadManager: Cooperative cancellation stops fetch", "[downloader][cancel][catch2]") {
auto tempDir = make_temp_dir();
StorageConfig storage{tempDir / "objects", tempDir / "staging"};
DownloaderConfig cfg;
auto http = std::make_unique<TestHttpAdapter>();
auto* httpPtr = http.get();
httpPtr->resumeSupported = false;
httpPtr->contentLength = 5;
httpPtr->payload = to_bytes("ABCDE");
httpPtr->checkCancelBeforeSink = true;
auto disk = std::make_unique<FakeDiskWriter>(storage);
auto dm = makeDownloadManagerWithDependencies(storage, cfg, std::move(http), std::move(disk),
nullptr, nullptr, nullptr);
DownloadRequest req;
req.url = "https://example.com/cancel.bin";
bool canceled = true;
auto result = dm->download(req, {}, [&]() { return canceled; });
REQUIRE_FALSE(result.ok());
CHECK((result.error().code == ErrorCode::OperationCancelled));
CHECK(httpPtr->cancelObserved);
std::error_code ec;
fs::remove_all(tempDir, ec);
}
// =============================================================================
// Test HTTP Adapter Tests
// =============================================================================
TEST_CASE("TestHttpAdapter: Behavior verification", "[downloader][adapter]") {
SECTION("Tracks probe calls") {
TestHttpAdapter adapter;
adapter.contentLength = 1000;
bool resumeSupported = false;
std::optional<std::uint64_t> contentLength;
std::optional<std::string> etag, lastModified, contentType, filename;
adapter.probe("http://test.com", {}, resumeSupported, contentLength, etag, lastModified,
contentType, filename, TlsConfig{}, std::nullopt, true,
std::chrono::milliseconds{1000});
CHECK((adapter.probeCallCount == 1));
CHECK((resumeSupported == true));
CHECK(contentLength.has_value());
CHECK((*contentLength == 1000));
}
SECTION("Tracks fetch calls") {
TestHttpAdapter adapter;
adapter.payload = to_bytes("test data");
std::vector<std::byte> received;
auto sink = [&received](std::span<const std::byte> data) -> Expected<void> {
received.insert(received.end(), data.begin(), data.end());
return {};
};
adapter.fetchRange("http://test.com", {}, 0, 9, TlsConfig{}, std::nullopt,
std::chrono::milliseconds{1000}, sink, nullptr, nullptr);
CHECK((adapter.fetchCallCount == 1));
CHECK((adapter.requestedOffsets.size() == 1));
CHECK((adapter.requestedOffsets[0] == 0));
CHECK((adapter.requestedSizes[0] == 9));
CHECK((received.size() == 9));
}
}
// =============================================================================
// Tracking Resume Store Tests
// =============================================================================
TEST_CASE("TrackingResumeStore: Tracking verification", "[downloader][resume][tracking]") {
TrackingResumeStore store;
SECTION("Tracks load calls") {
store.load("http://url1.com");
store.load("http://url2.com");
store.load("http://url1.com");
CHECK((store.loadUrls.size() == 3));
CHECK((store.loadUrls[0] == "http://url1.com"));
CHECK((store.loadUrls[1] == "http://url2.com"));
}
SECTION("Tracks save calls") {
IResumeStore::State state;
state.totalBytes = 100;
store.save("http://url1.com", state);
state.totalBytes = 200;
store.save("http://url2.com", state);
CHECK((store.savedUrls.size() == 2));
CHECK((store.savedStates.size() == 2));
CHECK((store.savedStates[0].totalBytes == 100));
CHECK((store.savedStates[1].totalBytes == 200));
}
SECTION("Tracks remove calls") {
store.remove("http://removed.com");
REQUIRE(store.removedUrl.has_value());
CHECK((*store.removedUrl == "http://removed.com"));
}
SECTION("Initial state returned on load") {
IResumeStore::State initial;
initial.totalBytes = 500;
initial.etag = "initial-etag";
store.initialState = initial;
auto result = store.load("http://any.com");
REQUIRE(result.ok());
REQUIRE(result.value().has_value());
CHECK((result.value()->totalBytes == 500));
CHECK((*result.value()->etag == "initial-etag"));
}
}
// =============================================================================
// Concrete downloader component tests
// =============================================================================
TEST_CASE("Concrete downloader components cover storage resume integrity and rate limits",
"[downloader][concrete]") {
SECTION("InMemoryResumeStore rejects empty keys and round-trips state") {
auto store = makeInMemoryResumeStore();
REQUIRE((store != nullptr));
auto emptyLoad = store->load("");
CHECK_FALSE(emptyLoad.ok());
CHECK((emptyLoad.error().code == ErrorCode::InvalidArgument));
IResumeStore::State state;
state.etag = "etag-1";
state.lastModified = "Mon, 01 Jan 2024 00:00:00 GMT";
state.totalBytes = 42;
state.completedRanges = {{0, 10}, {10, 32}};
REQUIRE(store->save("https://example.test/file", state).ok());
auto loaded = store->load("https://example.test/file");
REQUIRE(loaded.ok());
REQUIRE(loaded.value().has_value());
CHECK((loaded.value()->etag == state.etag));
CHECK((loaded.value()->lastModified == state.lastModified));
CHECK((loaded.value()->totalBytes == 42));
CHECK((loaded.value()->completedRanges == state.completedRanges));
store->remove("https://example.test/file");
auto removed = store->load("https://example.test/file");
REQUIRE(removed.ok());
CHECK_FALSE(removed.value().has_value());
}
SECTION("OpenSSL integrity verifier hashes bytes and resets after finalize") {
auto verifier = makeIntegrityVerifierSha256Only();
REQUIRE((verifier != nullptr));
verifier->reset(HashAlgo::Sha256);
auto data = to_bytes("abc");
verifier->update(data);
auto sum = verifier->finalize();
CHECK((sum.algo == HashAlgo::Sha256));
CHECK((sum.hex == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"));
verifier->update(data);
auto sum2 = verifier->finalize();
CHECK((sum2.hex == sum.hex));
}
SECTION("RateLimiter supports unlimited and cooperative cancellation fast paths") {
auto limiter = makeRateLimiter();
REQUIRE((limiter != nullptr));
limiter->setLimits(RateLimit{});
const auto start = std::chrono::steady_clock::now();
limiter->acquire(1024, [] { return false; });
CHECK((std::chrono::steady_clock::now() - start < std::chrono::milliseconds(100)));
RateLimit limited;
limited.globalBps = 1;
limiter->setLimits(limited);
bool cancelled = false;
limiter->acquire(1024, [&] {
cancelled = true;
return true;
});
CHECK(cancelled);
}
SECTION("DiskWriter stages writes and finalizes into SHA-256 CAS layout") {
auto tempDir = make_temp_dir("yams-dl-concrete-");
StorageConfig storage{tempDir / "objects", tempDir / "staging"};
auto writer = makeDiskWriter();
REQUIRE((writer != nullptr));
auto staging = writer->createStagingFile(storage, "session-abc", "part");
REQUIRE(staging.ok());
CHECK((staging.value().extension() == ".part"));
CHECK(fs::exists(staging.value()));
auto data = to_bytes("abc");
REQUIRE(writer->writeAt(staging.value(), 0, data).ok());
REQUIRE(writer->sync(staging.value(), storage).ok());
const std::string digest =
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
auto finalPath = writer->finalizeToCas(staging.value(), digest, storage);
REQUIRE(finalPath.ok());
CHECK((finalPath.value() == casPathForSha256(storage.objectsDir, digest)));
CHECK(fs::exists(finalPath.value()));
CHECK_FALSE(fs::exists(staging.value()));
std::ifstream in(finalPath.value(), std::ios::binary);
std::string contents((std::istreambuf_iterator<char>(in)),
std::istreambuf_iterator<char>());
CHECK((contents == "abc"));
std::uint64_t currentSize = 0;
auto resumed =
writer->createOrOpenStagingFile(storage, "session-resume", ".tmp", 4096, currentSize);
REQUIRE(resumed.ok());
CHECK((currentSize == 0));
REQUIRE(writer->writeAt(resumed.value(), 4, data).ok());
auto reopened = writer->createOrOpenStagingFile(storage, "session-resume", ".tmp",
std::nullopt, currentSize);
REQUIRE(reopened.ok());
CHECK((reopened.value() == resumed.value()));
CHECK((currentSize >= 7));
writer->cleanup(reopened.value());
CHECK_FALSE(fs::exists(reopened.value()));
std::error_code ec;
fs::remove_all(tempDir, ec);
}
}
// =============================================================================
// FakeDiskWriter Tests
// =============================================================================
TEST_CASE("FakeDiskWriter: Operations", "[downloader][disk]") {
auto tempDir = make_temp_dir();
StorageConfig storage{tempDir / "objects", tempDir / "staging"};
FakeDiskWriter writer(storage);
SECTION("Creates staging file") {
auto result = writer.createStagingFile(storage, "session-123", ".part");
REQUIRE(result.ok());
CHECK(fs::exists(result.value()));
}
SECTION("Writes at offset") {
auto staging = writer.createStagingFile(storage, "session-456", ".part");
REQUIRE(staging.ok());
auto data = to_bytes("hello");
auto writeResult = writer.writeAt(staging.value(), 0, data);
REQUIRE(writeResult.ok());
auto data2 = to_bytes("world");
writeResult = writer.writeAt(staging.value(), 5, data2);
REQUIRE(writeResult.ok());
CHECK((writer.stagedData().size() == 10));
}
SECTION("Handles initial data") {
writer.setInitialData(to_bytes("PREFIX"));
std::uint64_t currentSize = 0;
auto result =
writer.createOrOpenStagingFile(storage, "session-789", ".part", 100, currentSize);
REQUIRE(result.ok());
CHECK((currentSize == 6));
}
// Cleanup
std::error_code ec;
fs::remove_all(tempDir, ec);
}
// =============================================================================
// Edge Cases and Error Handling
// =============================================================================
TEST_CASE("Downloader: Edge cases", "[downloader][edge]") {
SECTION("Empty URL handling") {
TrackingResumeStore store;
auto result = store.load("");
CHECK(result.ok());
}
SECTION("Very long URL") {
std::string longUrl = "http://example.com/" + std::string(1000, 'x');
TrackingResumeStore store;
auto result = store.load(longUrl);
CHECK(result.ok());
CHECK((store.loadUrls[0] == longUrl));
}
SECTION("Unicode in URL") {
std::string unicodeUrl = "http://example.com/文件/файл.bin";
TrackingResumeStore store;
IResumeStore::State state;
state.totalBytes = 100;
auto saveResult = store.save(unicodeUrl, state);
CHECK(saveResult.ok());
CHECK((store.savedUrls[0] == unicodeUrl));
}
}
TEST_CASE("HTTP header parsing edge cases", "[downloader][http][edge]") {
SECTION("Empty headers list") {
std::vector<std::string> headers;
auto parsed = parse_http_headers_for_meta(headers);
CHECK_FALSE(parsed.etag.has_value());
CHECK_FALSE(parsed.lastModified.has_value());
}
SECTION("Malformed headers (no colon)") {
std::vector<std::string> headers = {"InvalidHeaderNoColon", "ETag: valid-etag"};
auto parsed = parse_http_headers_for_meta(headers);
REQUIRE(parsed.etag.has_value());
CHECK((*parsed.etag == "valid-etag"));
}
SECTION("Empty header value") {
std::vector<std::string> headers = {"ETag: ", "Last-Modified: "};
auto parsed = parse_http_headers_for_meta(headers);
// Empty values should still be captured
REQUIRE(parsed.etag.has_value());
CHECK(parsed.etag->empty());
}
SECTION("Whitespace only value") {
std::vector<std::string> headers = {"ETag: "};
auto parsed = parse_http_headers_for_meta(headers);
REQUIRE(parsed.etag.has_value());
CHECK(parsed.etag->empty());
}
}