227 lines
12 KiB
C++
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"
|