/* Настоящие Qt-карточка и модель; очередь ядра задерживает ответы для проверки смены выбора. */ #include "../src/accountlist.h" #include "../src/memberlistmodel.h" #include "../transport/gui_bridge.h" #include #include #include 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 requests; static gui_member_detail_fn detailCallback; static bool ready = true; 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(arg)); u_free(arg); } 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(bytes.data())); return bytes; } static QByteArray reply(const chat_member_detail_req& req, const QList& hops = {}, int result = 0) { QByteArray bytes(CHAT_MEMBER_DETAIL_HEADER_SIZE + hops.size() * 8, 0); auto* data = reinterpret_cast(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(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(bytes.constData()), bytes.size()); } class MemberDetailTest : public QObject { Q_OBJECT static QString body(AccountList& p) { return p.findChild("memberDetailText")->toPlainText(); } static QString header(AccountList& p) { return p.findChild("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(); ready = true; debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); } 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(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(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(reordered.constData()), reordered.size()); QVERIFY(header(panel).contains("peer7")); QCOMPARE(panel.findChild()->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(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"