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

227 lines
12 KiB
C++

/* Настоящие Qt-карточка и модель; очередь ядра задерживает ответы для проверки смены выбора. */
#include "../src/accountlist.h"
#include "../src/memberlistmodel.h"
#include "../transport/gui_bridge.h"
#include <QtTest>
#include <QListView>
#include <QAbstractItemModelTester>
#include <vector>
extern "C" {
#include "utun_instance.h"
#include "chat/chat_member.h"
#include "member_files/protocol.h"
#include "debug_config.h"
#include "mem.h"
}
static UTUN_INSTANCE instance{};
static std::vector<chat_member_detail_req> requests;
static gui_member_detail_fn detailCallback;
static bool ready = true;
static bool rejectOwned = false;
static int ownedPosts = 0;
static std::vector<chat_member_list_req> memberRequests;
extern "C" UTUN_INSTANCE* gui_bridge_get_inst() { return &instance; }
extern "C" int gui_bridge_is_uasync_ready() { return ready; }
extern "C" void gui_bridge_set_member_detail_cb(gui_member_detail_fn cb) { detailCallback = cb; }
extern "C" void gui_bridge_set_radio_mute_result_cb(gui_dm_messages_fn) {}
extern "C" int gui_bridge_toggle_member_radio_mute(const char*, uint64_t) { return 0; }
extern "C" void gui_bridge_post_uasync_fn(void (*fn)(void*), void* arg) {
if (fn == chat_core_collect_member_detail_trampoline) requests.push_back(*static_cast<chat_member_detail_req*>(arg));
if (fn == chat_core_request_member_list_trampoline || fn == chat_core_request_member_rtt_trampoline)
memberRequests.push_back(*static_cast<chat_member_list_req*>(arg));
u_free(arg);
}
extern "C" int gui_bridge_post_uasync_owned_fn(void (*fn)(void*), void* arg) {
++ownedPosts;
if (rejectOwned) return -1;
gui_bridge_post_uasync_fn(fn, arg);
return 0;
}
static QByteArray row(quint64 node, bool online = true, int up = 5, int rtt = 30) {
chat_member_display member{};
member.node_id = node; member.online = online; member.conn_presence = 5; member.conn_up = up;
member.rtt = rtt; member.nat_type = 0xff; member.flags = CHAT_MEMBER_FLAG_PHONE_KNOWN;
snprintf(member.name, sizeof(member.name), "peer%llu", (unsigned long long)node);
QByteArray bytes(CHAT_MEMBER_DISPLAY_SIZE, 0);
chat_core_serialize_member(&member, reinterpret_cast<uint8_t*>(bytes.data()));
return bytes;
}
static QByteArray reply(const chat_member_detail_req& req, const QList<quint64>& hops = {}, int result = 0) {
QByteArray bytes(CHAT_MEMBER_DETAIL_HEADER_SIZE + hops.size() * 8, 0);
auto* data = reinterpret_cast<uint8_t*>(bytes.data());
mf_put64(data, req.request_id); mf_put64(data + 8, req.group_id); mf_put64(data + 16, req.node_id);
data[24] = result; data[25] = result ? 0 : 7; data[28] = hops.size();
for (int i = 0; i < hops.size(); i++) mf_put64(data + CHAT_MEMBER_DETAIL_HEADER_SIZE + i * 8, hops[i]);
return bytes;
}
static void deliver(const QByteArray& bytes) {
if (detailCallback) detailCallback(reinterpret_cast<const uint8_t*>(bytes.constData()), bytes.size());
}
static void fill(AccountList& panel, const QString& channel, const QByteArray& bytes) {
panel.setChannel(channel);
panel.onMemberList(channel, bytes.size() / CHAT_MEMBER_DISPLAY_SIZE,
reinterpret_cast<const uint8_t*>(bytes.constData()), bytes.size());
}
class MemberDetailTest : public QObject {
Q_OBJECT
static QString body(AccountList& p) { return p.findChild<QTextEdit*>("memberDetailText")->toPlainText(); }
static QString header(AccountList& p) { return p.findChild<QLabel*>("memberDetailHeader")->text(); }
static bool select(AccountList& p, int rowNumber) {
return QMetaObject::invokeMethod(&p, "onMemberClicked", Qt::DirectConnection, Q_ARG(QModelIndex, p.model()->index(rowNumber)));
}
private slots:
void init() {
requests.clear(); memberRequests.clear(); ownedPosts = 0; rejectOwned = false; ready = true;
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
}
void flatModelPreservesRowsSnapshotsAndPersistentIndexes() {
MemberListModel model;
QAbstractItemModelTester tester(&model, QAbstractItemModelTester::FailureReportingMode::QtTest);
model.setChannel("123", false);
auto bytes = row(7) + row(8) + row(9);
model.onMemberList("123", 3, reinterpret_cast<const uint8_t*>(bytes.constData()), bytes.size());
QCOMPARE(model.rowCount(model.index(0)), 0);
const QPersistentModelIndex last = model.index(2);
const auto snapshot = model.member(9); QVERIFY(snapshot);
model.onMemberRemoved("123", 7);
QCOMPARE(last.row(), 1); QCOMPARE(model.nodeIdAt(last.row()), quint64(9));
const auto changed = row(9, false, 0, 0xffff);
model.onMemberUpdated("123", reinterpret_cast<const uint8_t*>(changed.constData()));
QVERIFY(snapshot->online); QVERIFY(!model.member(9)->online);
const auto added = row(10);
model.onMemberUpdated("123", reinterpret_cast<const uint8_t*>(added.constData()));
QCOMPARE(model.rowCount(), 3); QCOMPARE(model.nodeIdAt(2), quint64(10));
QVERIFY(!model.member(7)); QCOMPARE(model.nodeIdAt(-1), quint64(0));
}
void snapshotChangedSeesNewChannel() {
MemberListModel model; model.setChannel("123", false);
memberRequests.clear();
connect(&model, &MemberListModel::snapshotChanged, &model, [&] { model.refresh(); });
model.setChannel("456", false);
QCOMPARE(memberRequests.size(), size_t(2));
for (const auto& request : memberRequests) QCOMPARE(QString::fromUtf8(request.ch_id), QString("456"));
}
void losingOwnerPolicyHidesDeletedRowsWithoutCore() {
MemberListModel model; model.setChannel("123", true);
auto bytes = row(7) + row(8);
bytes[CHAT_MEMBER_DISPLAY_SIZE + 10] = CHAT_MEMBER_FLAG_DELETED;
model.onMemberList("123", 2, reinterpret_cast<const uint8_t*>(bytes.constData()), bytes.size());
QCOMPARE(model.rowCount(), 2);
ready = false; model.setChannel("123", false);
QCOMPARE(model.rowCount(), 1); QVERIFY(!model.member(8));
}
void malformedUpdatesKeepExistingSnapshot() {
MemberListModel model; model.setChannel("123", false);
const auto bytes = row(7);
model.onMemberList("123", 1, reinterpret_cast<const uint8_t*>(bytes.constData()), bytes.size());
model.onMemberList("123", 2, reinterpret_cast<const uint8_t*>(bytes.constData()), bytes.size());
model.onMemberList("123", -1, nullptr, -1);
model.onMemberUpdated("123", nullptr);
QCOMPARE(model.rowCount(), 1); QCOMPARE(model.nodeIdAt(0), quint64(7));
}
void rejectedMemberRequestsReleaseArguments() {
const auto before = u_get_allocated_count();
rejectOwned = true;
MemberListModel model; model.setChannel("123", false);
model.refresh(); model.refreshRtt();
QCOMPARE(ownedPosts, 3);
QCOMPARE(u_get_allocated_count(), before);
}
void invalidChannelDoesNotReplaceVisibleSnapshotOrSendRequests() {
MemberListModel model; model.setChannel("123", false);
const auto bytes = row(7);
model.onMemberList("123", 1, reinterpret_cast<const uint8_t*>(bytes.constData()), bytes.size());
memberRequests.clear();
model.setChannel(QString(64, '1'), false);
model.setChannel(QString::fromUtf8("123\0suffix", 10), false);
QCOMPARE(model.rowCount(), 1); QCOMPARE(model.nodeIdAt(0), quint64(7));
QVERIFY(memberRequests.empty());
model.refresh(); QCOMPARE(memberRequests.size(), size_t(1));
QCOMPARE(QString::fromUtf8(memberRequests.back().ch_id), QString("123"));
}
void modelOwnsPresenceAndRttAndRefreshPreservesSelection() {
AccountList panel; panel.resize(600, 800); panel.show();
fill(panel, "123", row(7, false, 0, 0xffff) + row(8));
QVERIFY(select(panel, 0)); QCOMPARE(requests.size(), size_t(1));
deliver(reply(requests.back())); // Физический транспорт UP не делает участника онлайн.
QVERIFY(header(panel).contains("#9E9E9E")); QVERIFY(body(panel).contains("offline"));
const auto live = row(7, true, 4, 93);
panel.onMemberUpdated("123", reinterpret_cast<const uint8_t*>(live.constData()));
QVERIFY(header(panel).contains("#4CAF50")); QVERIFY(body(panel).contains("rtt=9.3ms"));
deliver(reply(requests.back(), {7, 9})); QVERIFY(body(panel).contains("0000000000000009"));
const auto other = row(7, false, 0, 0xffff);
panel.onMemberUpdated("456", reinterpret_cast<const uint8_t*>(other.constData()));
QVERIFY(header(panel).contains("#4CAF50"));
panel.refresh(); QCOMPARE(panel.model()->rowCount(), 2); QVERIFY(body(panel).contains("rtt=9.3ms"));
const auto reordered = row(8) + live;
panel.onMemberList("123", 2, reinterpret_cast<const uint8_t*>(reordered.constData()), reordered.size());
QVERIFY(header(panel).contains("peer7"));
QCOMPARE(panel.findChild<QListView*>()->currentIndex().row(), 1);
}
void aToBToADiscardsOldResponsesWithoutClearingNewRequest() {
AccountList panel; panel.show(); fill(panel, "123", row(7) + row(8));
QVERIFY(select(panel, 0)); const auto first = requests.back();
QVERIFY(select(panel, 1)); const auto second = requests.back();
QVERIFY(select(panel, 0)); const auto third = requests.back();
QVERIFY(first.request_id < second.request_id && second.request_id < third.request_id);
deliver(reply(first, {111})); deliver(reply(second, {222}));
QVERIFY(!body(panel).contains("BGP:"));
auto wrongGroup = third; wrongGroup.group_id = 456;
deliver(reply(wrongGroup, {333})); QVERIFY(!body(panel).contains("BGP:"));
deliver(reply(third, {7, 444})); QVERIFY(body(panel).contains("00000000000001bc"));
const auto pending = requests.size();
QVERIFY(QMetaObject::invokeMethod(&panel, "onDetailTimerTick")); QCOMPARE(requests.size(), pending + 1);
const auto oldChannel = requests.back();
fill(panel, "456", row(7)); QVERIFY(select(panel, 0)); const auto current = requests.back();
deliver(reply(oldChannel, {555})); QVERIFY(!body(panel).contains("BGP:"));
deliver(reply(current, {7, 666})); QVERIFY(body(panel).contains("000000000000029a"));
}
void newMemberEventInvalidatesPendingPathEvenWhenFieldsAreEqual() {
AccountList panel; panel.show(); const auto initial = row(7);
fill(panel, "123", initial); QVERIFY(select(panel, 0)); const auto first = requests.back();
panel.onMemberUpdated("123", reinterpret_cast<const uint8_t*>(initial.constData()));
QCOMPARE(requests.size(), size_t(1));
deliver(reply(first, {111})); QCOMPARE(requests.size(), size_t(2)); QVERIFY(!body(panel).contains("BGP:"));
const auto correct = reply(requests.back(), {7, 8});
deliver(correct.left(correct.size() - 1)); QVERIFY(!body(panel).contains("BGP:"));
deliver(correct); QVERIFY(body(panel).contains("0000000000000008"));
panel.onMemberRemoved("123", 7); QVERIFY(!header(panel).contains("peer7"));
deliver(correct); QVERIFY(!body(panel).contains("BGP:"));
}
void errorTimeoutAndHiddenPanelReleaseOnlyTheirOwnRequests() {
AccountList panel; panel.show(); fill(panel, "123", row(7)); QVERIFY(select(panel, 0));
deliver(reply(requests.back(), {}, CHAT_MEMBER_DETAIL_NOT_MEMBER));
QVERIFY(header(panel).contains("#4CAF50")); // Диагностическая ошибка не меняет live-снимок.
QVERIFY(QMetaObject::invokeMethod(&panel, "onDetailTimerTick")); const auto expired = requests.back();
QTRY_VERIFY_WITH_TIMEOUT(requests.back().request_id != expired.request_id, 6500);
const auto current = requests.back(); deliver(reply(expired, {111}));
deliver(reply(current, {7, 8})); QVERIFY(body(panel).contains("BGP:"));
QVERIFY(QMetaObject::invokeMethod(&panel, "onDetailTimerTick")); const auto hidden = requests.back();
panel.hide(); deliver(reply(hidden, {222})); panel.show();
QVERIFY(requests.back().request_id > hidden.request_id);
deliver(reply(hidden, {222})); QVERIFY(!body(panel).contains("00000000000000de"));
ready = false; panel.setChannel(QString()); deliver(reply(requests.back(), {333}));
QCOMPARE(panel.model()->rowCount(), 0); QVERIFY(!body(panel).contains("BGP:"));
}
};
QTEST_MAIN(MemberDetailTest)
#include "test_member_detail.moc"