commits
tags
#include <doctest/doctest.h>
#include <map>
#include "torlinkc/engine/bootguard.hpp"
#include "torlinkc/engine/queue.hpp"
#include "test_support.hpp"
using namespace torlinkc;
using torlinkc::test::StateDirSandbox;
namespace {
// A 40-hex-char-valid id/magnet pair libtorrent's magnet parser accepts, so
// engine_.add() succeeds and these stay in the state the queue's own
// bookkeeping put them in, rather than flipping to "failed" on a parse error.
AddInput mk(int n) {
char buf[41];
std::snprintf(buf, sizeof(buf), "%040d", n);
std::string id(buf);
AddInput in;
in.id = id;
in.name = "T" + std::to_string(n);
in.magnet = "magnet:?xt=urn:btih:" + id;
return in;
}
std::map<std::string, DownloadStatus> statuses(const DownloadQueue& q) {
std::map<std::string, DownloadStatus> out;
for (const auto& it : q.getItems()) out[it.id] = it.status;
return out;
}
QueueItem persistedItem(int n, DownloadStatus status, std::int64_t addedAt) {
auto input = mk(n);
QueueItem it;
it.id = input.id;
it.name = input.name;
it.magnet = input.magnet;
it.dir = "/d";
it.status = status;
it.addedAt = addedAt;
return it;
}
} // namespace
TEST_CASE("DownloadQueue concurrency cap: queues beyond the cap, promotes oldest queued on pause") {
StateDirSandbox sandbox;
DownloadQueue q(2);
q.add(mk(1), "/d");
q.add(mk(2), "/d");
q.add(mk(3), "/d");
CHECK(q.activeCount() == 2);
auto s = statuses(q);
CHECK(s[mk(1).id] == DownloadStatus::Downloading);
CHECK(s[mk(2).id] == DownloadStatus::Downloading);
CHECK(s[mk(3).id] == DownloadStatus::Queued);
// Pausing an active download frees a slot -> the queued one starts.
q.pause(mk(1).id);
CHECK(q.activeCount() == 2);
s = statuses(q);
CHECK(s[mk(1).id] == DownloadStatus::Paused);
CHECK(s[mk(3).id] == DownloadStatus::Downloading);
}
TEST_CASE("DownloadQueue defaults to unlimited (maxDownloads 0): everything downloads at once") {
StateDirSandbox sandbox;
DownloadQueue q(0);
q.add(mk(1), "/d");
q.add(mk(2), "/d");
q.add(mk(3), "/d");
CHECK(q.activeCount() == 3);
for (const auto& [id, status] : statuses(q)) CHECK(status == DownloadStatus::Downloading);
}
TEST_CASE("DownloadQueue respects the cap when restoring persisted downloads on boot") {
StateDirSandbox sandbox;
std::vector<QueueItem> persisted = {
persistedItem(1, DownloadStatus::Downloading, 1),
persistedItem(2, DownloadStatus::Downloading, 2),
persistedItem(3, DownloadStatus::Downloading, 3),
};
DownloadQueue q(2);
q.restore(persisted);
CHECK(q.activeCount() == 2);
CHECK(statuses(q)[mk(3).id] == DownloadStatus::Queued);
}
TEST_CASE("DownloadQueue starts persisted queued items on restore when the cap is unset") {
StateDirSandbox sandbox;
std::vector<QueueItem> persisted = {
persistedItem(1, DownloadStatus::Downloading, 1),
persistedItem(2, DownloadStatus::Queued, 2),
persistedItem(3, DownloadStatus::Queued, 3),
};
DownloadQueue q(0);
q.restore(persisted);
CHECK(q.activeCount() == 3);
for (const auto& [id, status] : statuses(q)) CHECK(status == DownloadStatus::Downloading);
}
TEST_CASE("DownloadQueue safe-mode restore brings active and queued items back paused") {
StateDirSandbox sandbox;
DownloadQueue q;
std::vector<QueueItem> persisted = {
persistedItem(1, DownloadStatus::Downloading, 1),
persistedItem(2, DownloadStatus::Queued, 2),
persistedItem(3, DownloadStatus::Failed, 3),
persistedItem(4, DownloadStatus::Paused, 4),
};
q.restore(persisted, RestoreOptions{true});
auto s = statuses(q);
CHECK(s[mk(1).id] == DownloadStatus::Paused);
CHECK(s[mk(2).id] == DownloadStatus::Paused);
CHECK(s[mk(3).id] == DownloadStatus::Failed);
CHECK(s[mk(4).id] == DownloadStatus::Paused);
}
TEST_CASE("DownloadQueue safe-mode restoreSeeds brings persisted seeders back paused") {
StateDirSandbox sandbox;
DownloadQueue q;
HistoryItem h1;
h1.id = "s1";
h1.name = "Seed One";
h1.magnet = "magnet:?xt=urn:btih:" + std::string(40, '1');
h1.dir = "/d";
HistoryItem h2 = h1;
h2.id = "s2";
h2.name = "Seed Two";
h2.magnet = "magnet:?xt=urn:btih:" + std::string(40, '2');
q.restoreHistory({h1, h2});
q.restoreSeeds({SeedRecord{"s1", SeedStatus::Seeding}, SeedRecord{"s2", SeedStatus::Paused}}, RestoreOptions{true});
REQUIRE(q.getSeed("s1").has_value());
REQUIRE(q.getSeed("s2").has_value());
CHECK(q.getSeed("s1")->status == SeedStatus::Paused);
CHECK(q.getSeed("s2")->status == SeedStatus::Paused);
CHECK(q.seedingCount() == 0);
}
TEST_CASE("DownloadQueue::add on an existing non-failed item is a no-op") {
StateDirSandbox sandbox;
DownloadQueue q;
auto input = mk(1);
q.add(input, "/d");
q.pause(input.id);
REQUIRE(statuses(q)[input.id] == DownloadStatus::Paused);
// Re-adding a paused (not failed) item must not restart it.
q.add(input, "/d");
CHECK(statuses(q)[input.id] == DownloadStatus::Paused);
}
TEST_CASE("DownloadQueue::cancel removes an active download entirely") {
StateDirSandbox sandbox;
DownloadQueue q;
auto input = mk(1);
q.add(input, "/d");
REQUIRE(q.has(input.id));
q.cancel(input.id);
CHECK_FALSE(q.has(input.id));
}
TEST_CASE("DownloadQueue::persistSync disarms the boot marker") {
StateDirSandbox sandbox;
armBootMarker();
REQUIRE(wasBootInterrupted());
DownloadQueue q;
q.persistSync();
CHECK_FALSE(wasBootInterrupted());
}