Test call command ownership, allocation failures and core rebinding
This commit is contained in:
1 parent
f3d18a57b5
commit
e23a7e27eb
2 files changed
+235
No files matched your search
@@ -532,6 +532,19 @@ endif()
|
||||
add_test(NAME test_message_events COMMAND test_message_events)
|
||||
set_tests_properties(test_message_events PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 15)
|
||||
|
||||
add_executable(test_call_commands tests/test_call_commands.cpp src/callwindow.cpp src/call_audio_engine.cpp src/sound_manager.cpp
|
||||
src/voice_audio_io.cpp src/audio_device.cpp src/connmonitorwindow.cpp transport/gui_bridge_impl.cpp
|
||||
transport/miniaudio_impl.c transport/audio_diagnostics.cpp)
|
||||
target_link_libraries(test_call_commands PRIVATE ${QT_LIBS} ${QT_TEST_LIB} chatgui_stream_publish utun_voice utun pthread ${CMAKE_DL_LIBS})
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
target_compile_definitions(test_call_commands PRIVATE UTUN_TEST_CALL_HOOKS)
|
||||
foreach(symbol u_calloc_impl call_start_trampoline call_accept_trampoline call_decline_trampoline call_hangup_trampoline call_continue_trampoline)
|
||||
target_link_options(test_call_commands PRIVATE "-Wl,--wrap=${symbol}")
|
||||
endforeach()
|
||||
endif()
|
||||
add_test(NAME test_call_commands COMMAND test_call_commands)
|
||||
set_tests_properties(test_call_commands PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 30)
|
||||
|
||||
add_executable(test_member_dialogs tests/test_member_dialogs.cpp src/renamedialog.cpp src/memberpropsdialog.cpp
|
||||
db/db_manager.cpp transport/gui_bridge_impl.cpp src/connmonitorwindow.cpp)
|
||||
target_link_libraries(test_member_dialogs PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun utun_voice pthread ${CMAKE_DL_LIBS})
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
/* Настоящие CallWindow/bridge/uasync: владение call-командами при отказе, отмене и исполнении.
|
||||
* Linux hooks наблюдают границу native trampoline и затем вызывают его без подмены освобождения payload. */
|
||||
#include "../src/callwindow.h"
|
||||
#include "../src/call_camera_capture.h"
|
||||
#include "../transport/gui_bridge.h"
|
||||
#include <QtTest>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <cstring>
|
||||
extern "C" {
|
||||
#include "utun_instance.h"
|
||||
#include "call/call.h"
|
||||
#include "mem.h"
|
||||
#include "debug_config.h"
|
||||
}
|
||||
|
||||
struct Observation {
|
||||
UTUN_INSTANCE* instance;
|
||||
uint64_t id, peer;
|
||||
int action;
|
||||
QByteArray channel;
|
||||
};
|
||||
static QList<Observation> observed;
|
||||
static UTUN_INSTANCE* executingCore = nullptr;
|
||||
static bool bindingValid = true;
|
||||
|
||||
#ifdef UTUN_TEST_CALL_HOOKS
|
||||
static std::atomic<int> failAllocation{0};
|
||||
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 == 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "[INJECT] call command allocation failure at %s", where); return nullptr;
|
||||
}
|
||||
return __real_u_calloc_impl(count, size, where);
|
||||
}
|
||||
extern "C" void __real_call_start_trampoline(void*);
|
||||
extern "C" void __wrap_call_start_trampoline(void* arg) {
|
||||
auto* request = static_cast<call_start_arg*>(arg);
|
||||
bindingValid &= request->inst == executingCore;
|
||||
observed.append({request->inst, request->call_id, request->peer_node_id, -1, request->ch_id});
|
||||
__real_call_start_trampoline(arg);
|
||||
}
|
||||
static void observeAction(void* arg, int action, void (*native)(void*)) {
|
||||
auto* request = static_cast<call_id_arg*>(arg);
|
||||
bindingValid &= request->inst == executingCore;
|
||||
observed.append({request->inst, request->call_id, 0, action, {}}); native(arg);
|
||||
}
|
||||
extern "C" void __real_call_accept_trampoline(void*);
|
||||
extern "C" void __wrap_call_accept_trampoline(void* arg) { observeAction(arg, GUI_CALL_ACCEPT, __real_call_accept_trampoline); }
|
||||
extern "C" void __real_call_decline_trampoline(void*);
|
||||
extern "C" void __wrap_call_decline_trampoline(void* arg) { observeAction(arg, GUI_CALL_DECLINE, __real_call_decline_trampoline); }
|
||||
extern "C" void __real_call_hangup_trampoline(void*);
|
||||
extern "C" void __wrap_call_hangup_trampoline(void* arg) { observeAction(arg, GUI_CALL_HANGUP, __real_call_hangup_trampoline); }
|
||||
extern "C" void __real_call_continue_trampoline(void*);
|
||||
extern "C" void __wrap_call_continue_trampoline(void* arg) { observeAction(arg, GUI_CALL_CONTINUE, __real_call_continue_trampoline); }
|
||||
#endif
|
||||
|
||||
struct CommandCore {
|
||||
UASYNC* ua = uasync_create();
|
||||
UTUN_INSTANCE instance{};
|
||||
~CommandCore() {
|
||||
gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr);
|
||||
if (ua) { uasync_mark_stopped(ua); uasync_destroy(ua, 0); }
|
||||
}
|
||||
void attach() {
|
||||
instance.ua = ua; uasync_mark_running(ua); gui_bridge_set_uasync(ua); gui_bridge_set_inst(&instance);
|
||||
}
|
||||
void poll() { executingCore = &instance; uasync_poll(ua, 0); executingCore = nullptr; }
|
||||
};
|
||||
static int pending(UASYNC* ua) {
|
||||
int count = 0;
|
||||
for (auto* task = ua->posted_tasks_head; task; task = task->next) ++count;
|
||||
return count;
|
||||
}
|
||||
|
||||
class CallCommandsTest : public QObject {
|
||||
Q_OBJECT
|
||||
static void scenarios() {
|
||||
QTest::addColumn<QString>("scenario");
|
||||
for (const char* value : {"unavailable", "stopped", "cancelled", "executed"}) QTest::newRow(value) << QString(value);
|
||||
#ifdef UTUN_TEST_CALL_HOOKS
|
||||
QTest::newRow("request-allocation") << QString("request-allocation");
|
||||
QTest::newRow("post-allocation") << QString("post-allocation");
|
||||
#endif
|
||||
}
|
||||
static void prepare(CommandCore& core, const QString& scenario) {
|
||||
core.attach();
|
||||
if (scenario == "unavailable") gui_bridge_set_inst(nullptr);
|
||||
if (scenario == "stopped") uasync_mark_stopped(core.ua);
|
||||
#ifdef UTUN_TEST_CALL_HOOKS
|
||||
failAllocation = scenario == "request-allocation" ? 1 : scenario == "post-allocation" ? 2 : 0;
|
||||
#endif
|
||||
}
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
|
||||
debug_set_category_level(DEBUG_CATEGORY_CALL, DEBUG_LEVEL_DEBUG);
|
||||
debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_DEBUG); gui_bridge_init();
|
||||
{ CallWindow warmup; } // Singleton audio engine создаётся до подсчёта командных аллокаций.
|
||||
}
|
||||
void cleanup() {
|
||||
#ifdef UTUN_TEST_CALL_HOOKS
|
||||
failAllocation = 0;
|
||||
#endif
|
||||
gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr);
|
||||
observed.clear(); bindingValid = true;
|
||||
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
|
||||
}
|
||||
void windowAction_data() {
|
||||
QTest::addColumn<QString>("scenario"); QTest::addColumn<QString>("action");
|
||||
QStringList failures{"unavailable", "stopped", "cancelled", "executed"};
|
||||
#ifdef UTUN_TEST_CALL_HOOKS
|
||||
failures << "request-allocation" << "post-allocation";
|
||||
#endif
|
||||
for (const auto& failure : failures) for (const char* action : {"onAccept", "onDecline", "onHangup", "continue", "close"})
|
||||
QTest::newRow(qPrintable(failure + '-' + action)) << failure << QString(action);
|
||||
}
|
||||
void windowAction() {
|
||||
QFETCH(QString, scenario); QFETCH(QString, action);
|
||||
const size_t before = u_get_allocated_count();
|
||||
{
|
||||
CommandCore core; QVERIFY(core.ua);
|
||||
CallWindow window; window.setCallId(0x123456789);
|
||||
const auto state = action == "onAccept" || action == "onDecline" ? CallWindow::Incoming :
|
||||
action == "continue" ? CallWindow::WaitDecision : CallWindow::Outgoing;
|
||||
window.setState(state); prepare(core, scenario);
|
||||
const size_t ready = u_get_allocated_count();
|
||||
if (action == "continue") {
|
||||
QPushButton* button = nullptr;
|
||||
for (auto* candidate : window.findChildren<QPushButton*>())
|
||||
if (candidate->text() == QCoreApplication::translate("ChatGui", "Call anyway")) button = candidate;
|
||||
QVERIFY(button); button->click();
|
||||
} else if (action == "close") { window.show(); window.close(); }
|
||||
else QVERIFY(QMetaObject::invokeMethod(&window, qPrintable(action), Qt::DirectConnection));
|
||||
const bool accepted = scenario == "executed" || scenario == "cancelled";
|
||||
QCOMPARE(pending(core.ua), accepted ? 1 : 0);
|
||||
if (!accepted && action != "close") {
|
||||
QCOMPARE(window.state(), state);
|
||||
bool message = false;
|
||||
for (auto* label : window.findChildren<QLabel*>())
|
||||
message |= label->text() == QCoreApplication::translate("ChatGui", "Could not send the call command");
|
||||
QVERIFY(message);
|
||||
}
|
||||
if (scenario == "executed") core.poll();
|
||||
else gui_bridge_set_inst(nullptr);
|
||||
QCOMPARE(pending(core.ua), 0); QCOMPARE(u_get_allocated_count(), ready);
|
||||
#ifdef UTUN_TEST_CALL_HOOKS
|
||||
QCOMPARE(failAllocation.load(), 0); QCOMPARE(observed.size(), scenario == "executed" ? 1 : 0);
|
||||
if (!observed.isEmpty()) {
|
||||
const int expected = action == "onAccept" ? GUI_CALL_ACCEPT : action == "onDecline" ? GUI_CALL_DECLINE :
|
||||
action == "continue" ? GUI_CALL_CONTINUE : GUI_CALL_HANGUP;
|
||||
QCOMPARE(observed.front().action, expected); QCOMPARE(observed.front().id, uint64_t(0x123456789));
|
||||
}
|
||||
#endif
|
||||
QVERIFY(bindingValid);
|
||||
}
|
||||
QCOMPARE(u_get_allocated_count(), before);
|
||||
}
|
||||
void startLifecycle_data() { scenarios(); }
|
||||
void startLifecycle() {
|
||||
QFETCH(QString, scenario);
|
||||
const size_t before = u_get_allocated_count();
|
||||
{
|
||||
CommandCore core; QVERIFY(core.ua); prepare(core, scenario);
|
||||
const size_t ready = u_get_allocated_count();
|
||||
char channel[] = "dm:123456789abcdef0";
|
||||
const bool accepted = scenario == "executed" || scenario == "cancelled";
|
||||
QCOMPARE(gui_bridge_call_start(channel, 42, 73), accepted ? 0 : -1);
|
||||
memset(channel, 'x', sizeof(channel) - 1); // После возврата caller может менять/освобождать строку.
|
||||
if (scenario == "executed") core.poll();
|
||||
else gui_bridge_set_inst(nullptr);
|
||||
QCOMPARE(pending(core.ua), 0); QCOMPARE(u_get_allocated_count(), ready);
|
||||
#ifdef UTUN_TEST_CALL_HOOKS
|
||||
QCOMPARE(failAllocation.load(), 0); QCOMPARE(observed.size(), scenario == "executed" ? 1 : 0);
|
||||
if (!observed.isEmpty()) {
|
||||
QCOMPARE(observed.front().channel, QByteArray("dm:123456789abcdef0"));
|
||||
QCOMPARE(observed.front().peer, uint64_t(42)); QCOMPARE(observed.front().id, uint64_t(73));
|
||||
}
|
||||
#endif
|
||||
QVERIFY(bindingValid);
|
||||
}
|
||||
QCOMPARE(u_get_allocated_count(), before);
|
||||
}
|
||||
void invalidArguments() {
|
||||
CommandCore core; QVERIFY(core.ua); core.attach(); const size_t before = u_get_allocated_count();
|
||||
char channel[64]; memset(channel, '1', sizeof(channel));
|
||||
QCOMPARE(gui_bridge_call_start(nullptr, 1, 1), -1); QCOMPARE(gui_bridge_call_start("", 1, 1), -1);
|
||||
QCOMPARE(gui_bridge_call_start(channel, 1, 1), -1); QCOMPARE(gui_bridge_call_start("42", 0, 1), -1);
|
||||
QCOMPARE(gui_bridge_call_start("42", 1, 0), -1);
|
||||
QCOMPARE(gui_bridge_call_action(static_cast<gui_call_action>(99), 1), -1);
|
||||
QCOMPARE(gui_bridge_call_action(GUI_CALL_ACCEPT, 0), -1);
|
||||
QCOMPARE(pending(core.ua), 0); QCOMPARE(u_get_allocated_count(), before);
|
||||
}
|
||||
void concurrentRebind() {
|
||||
const size_t before = u_get_allocated_count();
|
||||
{
|
||||
CommandCore first, second; QVERIFY(first.ua); QVERIFY(second.ua); first.attach();
|
||||
std::thread producer([] {
|
||||
for (uint64_t id = 1; id <= 64; ++id) gui_bridge_call_action(GUI_CALL_HANGUP, id);
|
||||
});
|
||||
for (unsigned i = 0; i < 32; ++i) {
|
||||
gui_bridge_set_inst(nullptr);
|
||||
auto& core = i % 2 ? first : second; core.attach(); core.poll();
|
||||
}
|
||||
producer.join(); gui_bridge_set_inst(nullptr); first.attach();
|
||||
QCOMPARE(gui_bridge_call_action(GUI_CALL_HANGUP, 1000), 0); first.poll();
|
||||
QVERIFY(bindingValid); QCOMPARE(pending(first.ua), 0); QCOMPARE(pending(second.ua), 0);
|
||||
#ifdef UTUN_TEST_CALL_HOOKS
|
||||
QVERIFY(!observed.isEmpty()); QCOMPARE(observed.back().instance, &first.instance);
|
||||
#endif
|
||||
}
|
||||
QCOMPARE(u_get_allocated_count(), before);
|
||||
}
|
||||
};
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc >= 2 && !strcmp(argv[1], "--call-camera-worker")) return call_camera_worker_main(argc, argv);
|
||||
QApplication application(argc, argv); CallCommandsTest test; return QTest::qExec(&test, argc, argv);
|
||||
}
|
||||
#include "test_call_commands.moc"
|
||||
Reference in new issue
Block a user