Files
utun2/tools/chatgui/tests/test_member_files_lifecycle.cpp
T

423 lines
25 KiB
C++

/* Реальные batch/browser и bridge: отказ очереди, detach до trampoline и итог без утечек.
* Минимальное ядро без файлового сервиса даёт штатный отказ при исполнении команды. */
#include "../src/member_files/browser.h"
#include "../transport/gui_bridge.h"
#include "../db/db_manager.h"
#include <QtTest>
#include <QLabel>
#include <QDir>
#include <QPointer>
#include <QPushButton>
#include <QTreeWidget>
#include <QFileDialog>
#include <QFile>
#include <QTemporaryDir>
#include <QScopeGuard>
#include <cstring>
extern "C" {
#include "utun_instance.h"
#include "chat/chat_core.h"
#include "member_files/member_files.h"
#include "debug_config.h"
#include "mem.h"
}
#ifdef UTUN_TEST_FILE_ALLOC_FAILURES
static int failAllocation;
extern "C" void* __real_u_calloc_impl(uint32_t, uint32_t, const char*);
extern "C" void* __wrap_u_calloc_impl(uint32_t count, uint32_t size, const char* where) {
if (failAllocation && strstr(where, "gui_bridge_impl.cpp") && !--failAllocation) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "[INJECT] file bridge allocation failure at %s", where); return nullptr;
}
return __real_u_calloc_impl(count, size, where);
}
#endif
struct FileCore {
UASYNC* ua = uasync_create();
UTUN_INSTANCE instance{};
void attach() {
instance.ua = ua;
chat_event_set_handler(&instance, [](UTUN_INSTANCE*, int event, const uint8_t* data, int size) {
gui_bridge_post(event, data, size);
});
uasync_mark_running(ua); gui_bridge_set_uasync(ua); gui_bridge_set_inst(&instance);
}
~FileCore() {
gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr);
if (ua) { uasync_mark_stopped(ua); uasync_destroy(ua, 0); }
}
};
static QPointer<MemberFileBatch> activeBatch;
static QPointer<MemberFileBrowser> activeBrowser;
static QList<QByteArray> responses, transfers;
static int pending(UASYNC* ua) {
int count = 0;
for (auto* task = ua->posted_tasks_head; task; task = task->next) count++;
return count;
}
class FileLifecycleTest : public QObject {
Q_OBJECT
private slots:
void initTestCase() {
QCoreApplication::setAttribute(Qt::AA_DontUseNativeDialogs);
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
debug_set_category_level(DEBUG_CATEGORY_MEDIA, DEBUG_LEVEL_TRACE);
debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_DEBUG);
// Тест намеренно приостанавливает poll для detach/диалогов; предупреждения о stall здесь ожидаемы.
debug_set_category_level(DEBUG_CATEGORY_SYS, DEBUG_LEVEL_ERROR);
gui_bridge_init();
gui_bridge_set_member_files_response_cb([](const uint8_t* data, int length) {
QVERIFY(QThread::currentThread() == qApp->thread()); responses.append(QByteArray((const char*)data, length));
if (activeBrowser) activeBrowser->onResponse(data, length);
mf_message message{};
if (activeBatch && length >= 16 && !mf_decode(data + 16, length - 16, &message)) activeBatch->response(message);
});
gui_bridge_set_member_files_transfer_cb([](const uint8_t* data, int length) {
QVERIFY(QThread::currentThread() == qApp->thread()); transfers.append(QByteArray((const char*)data, length));
if (activeBrowser) activeBrowser->onTransfer(data, length);
if (activeBatch) activeBatch->transfer(data, length);
});
}
void cleanupTestCase() {
gui_bridge_set_member_files_response_cb(nullptr); gui_bridge_set_member_files_transfer_cb(nullptr);
}
void cleanup() {
activeBatch = nullptr; activeBrowser = nullptr;
QCoreApplication::processEvents(); responses.clear(); transfers.clear();
#ifdef UTUN_TEST_FILE_ALLOC_FAILURES
failAllocation = 0;
#endif
}
void batchRequest_data() {
QTest::addColumn<QString>("scenario");
for (const char* scenario : {"detach", "unavailable", "stopped", "executed-error"}) QTest::newRow(scenario) << QString(scenario);
#ifdef UTUN_TEST_FILE_ALLOC_FAILURES
QTest::newRow("request-allocation") << QString("request-allocation");
QTest::newRow("post-allocation") << QString("post-allocation");
#endif
}
void batchRequest() {
QFETCH(QString, scenario);
const size_t before = u_get_allocated_count();
{
FileCore core; QVERIFY(core.ua); core.attach();
if (scenario == "unavailable") gui_bridge_set_inst(nullptr);
if (scenario == "stopped") uasync_mark_stopped(core.ua);
MemberFileBatch batch(42, 33, 22, MemberFileBatch::Remove, nullptr); activeBatch = &batch;
int completed = 0; bool success = true; QString summary;
batch.progress = [](const QString&, int) {};
batch.finished = [&](bool ok, const QString& message, const QStringList& roots) {
completed++; success = ok; summary = message; QVERIFY(roots.isEmpty());
};
#ifdef UTUN_TEST_FILE_ALLOC_FAILURES
if (scenario == "request-allocation") failAllocation = 1;
if (scenario == "post-allocation") failAllocation = 2;
#endif
batch.start({{"selected.txt", {}, QByteArray(16, '\7'), false}});
if (scenario == "detach") gui_bridge_set_inst(nullptr);
if (scenario == "executed-error") uasync_poll(core.ua, 0);
QTRY_COMPARE_WITH_TIMEOUT(completed, 1, 1000);
QVERIFY(!success); QVERIFY(!summary.isEmpty()); QCOMPARE(pending(core.ua), 0);
#ifdef UTUN_TEST_FILE_ALLOC_FAILURES
QCOMPARE(failAllocation, 0);
#endif
if (scenario == "detach" || scenario == "executed-error") {
QCOMPARE(responses.size(), 1); mf_message message{};
QCOMPARE(mf_decode((const uint8_t*)responses[0].constData() + 16, responses[0].size() - 16, &message), 0);
QCOMPARE(message.status, uint16_t(scenario == "detach" ? MF_CANCELLED : MF_ACCESS_DENIED));
QCOMPARE(message.command, uint8_t(MF_STAT)); QCOMPARE(QString::fromUtf8(message.path), QString("selected.txt"));
} else QVERIFY(responses.isEmpty());
batch.cancel(); QCoreApplication::processEvents(); QCOMPARE(completed, 1);
}
QCOMPARE(u_get_allocated_count(), before);
}
void batchDownload_data() { batchRequest_data(); }
void batchDownload() {
QFETCH(QString, scenario);
const size_t before = u_get_allocated_count();
{
FileCore core; QVERIFY(core.ua); core.attach(); QTemporaryDir root; QVERIFY(root.isValid());
if (scenario == "unavailable") gui_bridge_set_inst(nullptr);
if (scenario == "stopped") uasync_mark_stopped(core.ua);
MemberFileBatch batch(42, 33, 22, MemberFileBatch::Download, nullptr); activeBatch = &batch;
int completed = 0; bool success = true;
batch.progress = [](const QString&, int) {};
batch.finished = [&](bool ok, const QString&, const QStringList& roots) {
completed++; success = ok; QVERIFY(roots.isEmpty());
};
#ifdef UTUN_TEST_FILE_ALLOC_FAILURES
if (scenario == "request-allocation") failAllocation = 1;
if (scenario == "post-allocation") failAllocation = 2;
#endif
batch.start({{"selected.txt", {}, QByteArray(16, '\7'), false}}, root.path(), true);
if (scenario == "detach" || scenario == "executed-error") {
QTRY_COMPARE_WITH_TIMEOUT(pending(core.ua), 1, 1000);
if (scenario == "detach") gui_bridge_set_inst(nullptr);
else uasync_poll(core.ua, 0);
}
QTRY_COMPARE_WITH_TIMEOUT(completed, 1, 1000); QVERIFY(!success); QCOMPARE(pending(core.ua), 0);
QCOMPARE(QDir(root.path()).entryList(QDir::AllEntries | QDir::NoDotAndDotDot).size(), 0);
#ifdef UTUN_TEST_FILE_ALLOC_FAILURES
QCOMPARE(failAllocation, 0);
#endif
if (scenario == "detach" || scenario == "executed-error") {
QCOMPARE(transfers.size(), 1); const auto* data = (const uint8_t*)transfers[0].constData();
QCOMPARE(transfers[0].size(), 51); QCOMPARE(mf_get64(data + 8), uint64_t(42));
QCOMPARE(mf_get64(data + 16), uint64_t(33)); QCOMPARE(mf_get64(data + 24), uint64_t(22));
QCOMPARE(unsigned(data[48]) | unsigned(data[49]) << 8, unsigned(scenario == "detach" ? MF_CANCELLED : MF_BUSY));
QCOMPARE(data[50], uint8_t(2));
} else QVERIFY(transfers.isEmpty());
batch.cancel(); QCoreApplication::processEvents(); QCOMPARE(completed, 1);
}
QCOMPARE(u_get_allocated_count(), before);
}
void snapshotsSurviveCallerChangesAndDoNotUseSuppliedInstance() {
const size_t before = u_get_allocated_count();
{
FileCore core; QVERIFY(core.ua); core.attach();
mf_message message{}; message.kind = MF_REQUEST; message.command = MF_STAT;
message.request_id = UINT64_C(0x0102030405060708); message.owner = 22; strcpy(message.path, "original.txt");
QCOMPARE(gui_bridge_member_files_request(42, 33, &message), 0);
message = {};
mf_transfer_request transfer{}; transfer.id = 123; transfer.group = 42; transfer.server = 33; transfer.owner = 22;
transfer.inst = reinterpret_cast<UTUN_INSTANCE*>(quintptr(1)); // Bridge обязан заменить этот указатель.
strcpy(transfer.remote, "original.txt"); strcpy(transfer.local, "/unused/local.txt");
QCOMPARE(gui_bridge_member_files_transfer(&transfer), 0); transfer = {};
gui_bridge_set_inst(nullptr); QCoreApplication::processEvents();
QCOMPARE(responses.size(), 1); QCOMPARE(transfers.size(), 1); mf_message result{};
const auto& bytes = responses[0];
QCOMPARE(mf_get64((const uint8_t*)bytes.constData()), uint64_t(42));
QCOMPARE(mf_get64((const uint8_t*)bytes.constData() + 8), uint64_t(33));
QCOMPARE(mf_decode((const uint8_t*)bytes.constData() + 16, bytes.size() - 16, &result), 0);
QCOMPARE(result.request_id, UINT64_C(0x0102030405060708)); QCOMPARE(result.owner, uint64_t(22));
QCOMPARE(result.status, uint16_t(MF_CANCELLED)); QCOMPARE(QString::fromUtf8(result.path), QString("original.txt"));
QCOMPARE(mf_get64((const uint8_t*)transfers[0].constData()), uint64_t(123));
responses.clear(); transfers.clear(); core.attach();
transfer.id = 124; transfer.group = 42; transfer.server = 33; transfer.owner = 22; transfer.upload = 1;
transfer.inst = reinterpret_cast<UTUN_INSTANCE*>(quintptr(1));
strcpy(transfer.remote, "original.txt"); strcpy(transfer.local, "/unused/local.txt");
QCOMPARE(gui_bridge_member_files_transfer(&transfer), 0);
uasync_poll(core.ua, 0); QCoreApplication::processEvents();
QCOMPARE(transfers.size(), 1); QCOMPARE(mf_get64((const uint8_t*)transfers[0].constData()), uint64_t(124));
QCOMPARE(transfers[0][50], char(2));
QCOMPARE(transfer.inst, reinterpret_cast<UTUN_INSTANCE*>(quintptr(1)));
}
QCOMPARE(u_get_allocated_count(), before);
}
void cancelCommandsReleaseMemoryOnExecutionDetachAndRejection() {
const size_t before = u_get_allocated_count();
{
FileCore core; QVERIFY(core.ua); core.attach(); const size_t baseline = u_get_allocated_count();
QCOMPARE(gui_bridge_member_files_cancel(99), 0); uasync_poll(core.ua, 0);
QCOMPARE(u_get_allocated_count(), baseline);
QCOMPARE(gui_bridge_member_files_cancel(100), 0); gui_bridge_set_inst(nullptr);
QCOMPARE(u_get_allocated_count(), baseline); QVERIFY(gui_bridge_member_files_cancel(101) < 0);
core.attach(); QCOMPARE(gui_bridge_member_files_cancel(0), 0);
#ifdef UTUN_TEST_FILE_ALLOC_FAILURES
for (int point : {1, 2}) {
failAllocation = point; QVERIFY(gui_bridge_member_files_cancel(102) < 0); QCOMPARE(failAllocation, 0);
QCOMPARE(u_get_allocated_count(), baseline); QCOMPARE(pending(core.ua), 0);
}
#endif
QCoreApplication::processEvents(); QVERIFY(responses.isEmpty()); QVERIFY(transfers.isEmpty());
}
QCOMPARE(u_get_allocated_count(), before);
}
void browserRejectsOrCancelsListAndCanRetry_data() { batchRequest_data(); }
void browserRejectsOrCancelsListAndCanRetry() {
QFETCH(QString, scenario);
const size_t before = u_get_allocated_count();
{
FileCore core; QVERIFY(core.ua); core.attach(); DbManager db; sqlite3* sql = nullptr;
QCOMPARE(sqlite3_open(":memory:", &sql), SQLITE_OK);
const auto close = qScopeGuard([&] { db.setDb(nullptr); sqlite3_close(sql); });
QCOMPARE(sqlite3_exec(sql, "CREATE TABLE accounts(node_id INTEGER,display_name TEXT);"
"INSERT INTO accounts VALUES(22,'Me'),(33,'Storage');"
"CREATE TABLE peers_42(node_id INTEGER,adm_tags TEXT,adm_tags_sig BLOB,deleted INTEGER,source INTEGER);"
"INSERT INTO peers_42 VALUES(22,'{}',NULL,0,0),(33,'{\"storage\":\"yes\"}',zeroblob(64),0,0);",
nullptr, nullptr, nullptr), SQLITE_OK);
db.setDb(sql); db.setMyNodeId(22);
MemberFileBrowser browser(&db); activeBrowser = &browser;
if (scenario == "unavailable") gui_bridge_set_inst(nullptr);
if (scenario == "stopped") uasync_mark_stopped(core.ua);
#ifdef UTUN_TEST_FILE_ALLOC_FAILURES
if (scenario == "request-allocation") failAllocation = 1;
if (scenario == "post-allocation") failAllocation = 2;
#endif
browser.setChannel("42");
if (scenario == "detach") gui_bridge_set_inst(nullptr);
if (scenario == "executed-error") uasync_poll(core.ua, 0);
QCoreApplication::processEvents(); QCOMPARE(pending(core.ua), 0);
QStringList labels; for (auto* label : browser.findChildren<QLabel*>()) labels.append(label->text());
const QString expected = scenario == "detach" ? QString::fromUtf8(mf_status_text(MF_CANCELLED)) :
scenario == "executed-error" ? QString::fromUtf8(mf_status_text(MF_ACCESS_DENIED)) : "Unable to request directory";
QVERIFY2(labels.contains(expected), qPrintable(labels.join('\n')));
#ifdef UTUN_TEST_FILE_ALLOC_FAILURES
QCOMPARE(failAllocation, 0);
#endif
core.attach();
QPushButton* refresh = nullptr;
for (auto* button : browser.findChildren<QPushButton*>()) if (button->text() == "Refresh") refresh = button;
QVERIFY(refresh); refresh->click(); QCOMPARE(pending(core.ua), 1);
uasync_poll(core.ua, 0); QCoreApplication::processEvents(); QCOMPARE(pending(core.ua), 0);
QVERIFY(browser.findChild<QTreeWidget*>()->isEnabled());
}
QCOMPARE(u_get_allocated_count(), before);
}
void browserSingleDownload_data() {
QTest::addColumn<int>("failure");
QTest::newRow("detach") << 0;
#ifdef UTUN_TEST_FILE_ALLOC_FAILURES
QTest::newRow("request-allocation") << 1;
QTest::newRow("post-allocation") << 2;
#endif
}
void browserSingleDownload() {
QFETCH(int, failure);
const size_t before = u_get_allocated_count();
{
FileCore core; QVERIFY(core.ua); core.attach(); QTemporaryDir root; QVERIFY(root.isValid());
DbManager db; sqlite3* sql = nullptr; QCOMPARE(sqlite3_open(":memory:", &sql), SQLITE_OK);
const auto close = qScopeGuard([&] { db.setDb(nullptr); sqlite3_close(sql); });
QCOMPARE(sqlite3_exec(sql, "CREATE TABLE accounts(node_id INTEGER,display_name TEXT);"
"INSERT INTO accounts VALUES(22,'Me'),(33,'Storage');"
"CREATE TABLE peers_42(node_id INTEGER,adm_tags TEXT,adm_tags_sig BLOB,deleted INTEGER,source INTEGER);"
"INSERT INTO peers_42 VALUES(22,'{}',NULL,0,0),(33,'{\"storage\":\"yes\"}',zeroblob(64),0,0);",
nullptr, nullptr, nullptr), SQLITE_OK);
db.setDb(sql); db.setMyNodeId(22);
MemberFileBrowser browser(&db); activeBrowser = &browser; browser.setChannel("42", 22);
uasync_poll(core.ua, 0); QCoreApplication::processEvents();
auto* tree = browser.findChild<QTreeWidget*>(); QVERIFY(tree);
auto* item = new QTreeWidgetItem(tree, {"selected.txt"}); item->setData(0, Qt::UserRole, "selected.txt");
item->setData(0, Qt::UserRole + 1, MF_FILE); item->setData(0, Qt::UserRole + 2, QByteArray(16, '\7'));
tree->setCurrentItem(item); browser.show();
QPushButton* download = nullptr; QPushButton* cancel = nullptr;
for (auto* button : browser.findChildren<QPushButton*>()) {
if (button->text() == "Download…") download = button;
if (button->text() == "Cancel transfer") cancel = button;
}
QVERIFY(download); QVERIFY(cancel); QVERIFY(download->isEnabled());
bool accepted = false;
const QString destination = root.filePath("download.txt");
QTimer::singleShot(0, [&] {
auto* dialog = qobject_cast<QFileDialog*>(QApplication::activeModalWidget());
if (!dialog) return;
dialog->selectFile(destination); accepted = QMetaObject::invokeMethod(dialog, "accept");
#ifdef UTUN_TEST_FILE_ALLOC_FAILURES
// Включить отказ после выбора пути, чтобы QUOTA-таймер внутри диалога не перехватил инъекцию.
failAllocation = failure;
#endif
});
download->click(); QVERIFY(accepted);
if (!failure) {
QVERIFY(cancel->isEnabled()); QVERIFY(!tree->isEnabled());
gui_bridge_set_inst(nullptr); QCoreApplication::processEvents();
}
QVERIFY(!cancel->isEnabled()); QVERIFY(tree->isEnabled()); QVERIFY(!QFile::exists(destination));
QCOMPARE(pending(core.ua), 0);
#ifdef UTUN_TEST_FILE_ALLOC_FAILURES
QCOMPARE(failAllocation, 0);
#endif
core.attach(); item->setSelected(false); item->setSelected(true); QVERIFY(download->isEnabled());
QTimer::singleShot(0, [&] {
auto* dialog = qobject_cast<QFileDialog*>(QApplication::activeModalWidget());
if (dialog) { dialog->selectFile(destination); QMetaObject::invokeMethod(dialog, "accept"); }
});
download->click(); QVERIFY(cancel->isEnabled());
uasync_poll(core.ua, 0); QCoreApplication::processEvents();
QVERIFY(!cancel->isEnabled()); QVERIFY(tree->isEnabled()); QVERIFY(!QFile::exists(destination));
}
QCOMPARE(u_get_allocated_count(), before);
}
void realStorageRoundTrip() {
const size_t before = u_get_allocated_count();
{
QTemporaryDir root; QVERIFY(root.isValid());
UASYNC* ua = uasync_create(); QVERIFY(ua); UTUN_INSTANCE* instance = nullptr; QTimer poll;
const auto cleanup = qScopeGuard([&] {
activeBatch = nullptr; poll.stop(); gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr);
uasync_mark_stopped(ua);
if (instance) utun_instance_destroy(instance);
uasync_destroy(ua, 0); QCoreApplication::processEvents();
});
const QByteArray config = "[global]\ndb_path=" + root.path().toUtf8() + "\nauto_sockets=no\ntun_enabled=no\n"
"my_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n"
"my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n"
"[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\nmember_files_enabled=1\n"
"local_invites_enabled=0\nlocal_broadcasts_enabled=0\nmember_files_path=" + root.path().toUtf8() +
"/storage\n[ntp]\nenabled=no\n";
instance = utun_instance_create_from_str(ua, config.constData()); QVERIFY(instance);
QCOMPARE(utun_core_start(instance), 0); QCOMPARE(chat_service_start(instance), 0);
const uint64_t group = chat_core_create_channel_auto(instance, "Files"); QVERIFY(group);
const QByteArray channel = QByteArray::number(group); const uint64_t node = instance->node_id;
auto* tags = chat_member_tags_load(instance, channel.constData(), node); QVERIFY(tags);
const int set = chat_member_tags_set(tags, "storage", "yes");
const int commit = set ? set : chat_member_tags_commit(instance, tags); chat_member_tags_free(tags); QCOMPARE(commit, 0);
chat_event_set_handler(instance, [](UTUN_INSTANCE*, int event, const uint8_t* data, int size) {
gui_bridge_post(event, data, size);
});
uasync_mark_running(ua); gui_bridge_set_uasync(ua); gui_bridge_set_inst(instance);
connect(&poll, &QTimer::timeout, this, [ua] { uasync_poll(ua, 0); }); poll.start(1);
// Дождаться ответа готового storage, не подменяя асинхронный старт его worker.
bool ready = false;
for (int attempt = 0; attempt < 20 && !ready; attempt++) {
mf_message q{}; q.kind = MF_REQUEST; q.command = MF_QUOTA; q.request_id = 100 + attempt; q.owner = node;
const auto count = responses.size(); QCOMPARE(gui_bridge_member_files_request(group, node, &q), 0);
QTRY_COMPARE_WITH_TIMEOUT(responses.size(), count + 1, 3000); mf_message r{};
const auto& bytes = responses.last();
QCOMPARE(mf_decode((const uint8_t*)bytes.constData() + 16, bytes.size() - 16, &r), 0);
QVERIFY(r.status == MF_OK || r.status == MF_BUSY); ready = r.status == MF_OK;
}
QVERIFY(ready);
const QByteArray payload(20000, 'f'); const QString source = root.filePath("source.txt");
{ QFile file(source); QVERIFY(file.open(QIODevice::WriteOnly)); QCOMPARE(file.write(payload), qint64(payload.size())); }
int completed = 0; bool success = false; QString summary;
auto finished = [&](bool ok, const QString& text, const QStringList&) { completed++; success = ok; summary = text; };
const QString stored = root.filePath("storage/" + QString::number(group) + '/' + QString::number(node) + "/source.txt");
{
MemberFileBatch batch(group, node, node, MemberFileBatch::Upload, nullptr); activeBatch = &batch;
batch.progress = [](const QString&, int) {}; batch.finished = finished;
batch.upload({source}, {}); QTRY_COMPARE_WITH_TIMEOUT(completed, 1, 5000); QVERIFY2(success, qPrintable(summary));
}
{ QFile file(stored); QVERIFY(file.open(QIODevice::ReadOnly)); QCOMPARE(file.readAll(), payload); }
mf_message q{}; q.kind = MF_REQUEST; q.command = MF_STAT; q.request_id = 1000; q.owner = node; strcpy(q.path, "source.txt");
const auto count = responses.size(); QCOMPARE(gui_bridge_member_files_request(group, node, &q), 0);
QTRY_COMPARE_WITH_TIMEOUT(responses.size(), count + 1, 3000); mf_message info{};
const auto bytes = responses.last();
QCOMPARE(mf_decode((const uint8_t*)bytes.constData() + 16, bytes.size() - 16, &info), 0); QCOMPARE(info.status, uint16_t(MF_OK));
const QString destination = root.filePath("download.txt");
const QByteArray version((const char*)info.version, 16); completed = 0; success = false;
{
MemberFileBatch batch(group, node, node, MemberFileBatch::Download, nullptr); activeBatch = &batch;
batch.progress = [](const QString&, int) {}; batch.finished = finished;
batch.start({{"source.txt", destination, version, false}});
QTRY_COMPARE_WITH_TIMEOUT(completed, 1, 5000); QVERIFY2(success, qPrintable(summary));
}
{ QFile file(destination); QVERIFY(file.open(QIODevice::ReadOnly)); QCOMPARE(file.readAll(), payload); }
completed = 0; success = false;
{
MemberFileBatch batch(group, node, node, MemberFileBatch::Remove, nullptr); activeBatch = &batch;
batch.progress = [](const QString&, int) {}; batch.finished = finished;
batch.start({{"source.txt", {}, version, false}});
QTRY_COMPARE_WITH_TIMEOUT(completed, 1, 5000); QVERIFY2(success, qPrintable(summary));
}
QVERIFY(!QFile::exists(stored)); QVERIFY(QFile::exists(source)); QVERIFY(QFile::exists(destination));
}
QCOMPARE(u_get_allocated_count(), before);
}
};
QTEST_MAIN(FileLifecycleTest)
#include "test_member_files_lifecycle.moc"