mirror of
https://github.com/logos-co/logos-test-modules.git
synced 2026-08-30 19:51:16 +00:00
* fix(unit-tests): retarget test harnesses to the qt-split SDK stack The unit-test CMakeLists linked logos-cpp-sdk::logos_sdk, which no longer exists after the Qt split: the Qt developer layer (LogosAPI, provider objects) moved to logos-qt-sdk and the transport/consumer core (types, api_client, token_manager, mock transports) moved into the logos-protocol library. Both harnesses (test-ipc-module, test-ipc-module-new-api) now resolve three roots (LOGOS_CPP_SDK_ROOT via logos_module_context.h, plus LOGOS_QT_SDK_ROOT and LOGOS_PROTOCOL_ROOT), take mock-transport headers from logos-protocol, link logos-qt-sdk::logos_qt_sdk (carries the protocol library transitively) plus logos-cpp-sdk::logos_headers, and in source layouts compile the qt-sdk sources while linking the protocol library instead of compiling the old SDK sources in. flake.nix threads the two new roots into both testBin derivations, reaching the packages through logos-liblogos's inputs like the existing cpp-sdk reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * lock: pin extraction-chain branch revs for standalone CI Temporary — drop when the chain PRs merge (re-lock against masters). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * lock: qt-split chain fully merged — pins advance to masters (liblogos 050f2d3, logoscore-cli 519e7d3, module-builder 24cec35) * test(thread-safety): update to liblogos simplified C API (#130) liblogos#130 merged the redundant C API functions: set_modules_dir → add_modules_dir; load_module gained a with_dependencies bool (folding in load_module_with_dependencies); unload_module gained a with_dependents bool. test_thread_safety.cpp still called the pre-#130 signatures, so it failed to compile against current liblogos. Mechanical update — load_module(x) → load_module(x,false), load_module_with_dependencies(x) → load_module(x,true), unload_module(x) → unload_module(x,false). Now compiles; 8/10 tests pass (the residual SIGKILL in ConcurrentLoadSamePlugin is the pre-existing heavy-concurrency runtime instability, not API-related). --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
515 lines
18 KiB
C++
515 lines
18 KiB
C++
#include <gtest/gtest.h>
|
|
#include "logos_core.h"
|
|
#include "dummy_module_generator.h"
|
|
#include <QTemporaryDir>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
#include <atomic>
|
|
#include <mutex>
|
|
#include <condition_variable>
|
|
#include <cstring>
|
|
|
|
// Reusable barrier so all threads start work at the same instant.
|
|
class Barrier {
|
|
public:
|
|
explicit Barrier(int count) : m_threshold(count), m_waiting(0), m_generation(0) {}
|
|
|
|
void wait() {
|
|
std::unique_lock<std::mutex> lock(m_mutex);
|
|
int gen = m_generation;
|
|
if (++m_waiting == m_threshold) {
|
|
m_generation++;
|
|
m_waiting = 0;
|
|
m_cv.notify_all();
|
|
} else {
|
|
m_cv.wait(lock, [&] { return gen != m_generation; });
|
|
}
|
|
}
|
|
|
|
private:
|
|
std::mutex m_mutex;
|
|
std::condition_variable m_cv;
|
|
int m_threshold;
|
|
int m_waiting;
|
|
int m_generation;
|
|
};
|
|
|
|
// Free a null-terminated array of strings allocated with new[].
|
|
static void freeStringArray(char** arr) {
|
|
if (!arr) return;
|
|
for (int i = 0; arr[i]; ++i)
|
|
delete[] arr[i];
|
|
delete[] arr;
|
|
}
|
|
|
|
// Count entries in a null-terminated string array.
|
|
static int stringArrayLen(char** arr) {
|
|
if (!arr) return 0;
|
|
int n = 0;
|
|
while (arr[n]) ++n;
|
|
return n;
|
|
}
|
|
|
|
// Return true if name appears in a null-terminated string array.
|
|
static bool stringArrayContains(char** arr, const char* name) {
|
|
if (!arr) return false;
|
|
for (int i = 0; arr[i]; ++i)
|
|
if (strcmp(arr[i], name) == 0) return true;
|
|
return false;
|
|
}
|
|
|
|
// Qt requires at least one argument (the program name) for QCoreApplication.
|
|
static int s_argc = 1;
|
|
static char s_name[] = "thread_safety_tests";
|
|
static char* s_argv[] = {s_name, nullptr};
|
|
|
|
static void initPluginState() {
|
|
logos_core_init(s_argc, s_argv);
|
|
logos_core_start();
|
|
}
|
|
|
|
static void initPluginState(const char* pluginsDir) {
|
|
logos_core_init(s_argc, s_argv);
|
|
logos_core_add_modules_dir(pluginsDir);
|
|
logos_core_start();
|
|
}
|
|
|
|
// =============================================================================
|
|
// Lightweight tests — no real plugin files needed. Exercise the C API with
|
|
// names that are unknown to the registry.
|
|
// =============================================================================
|
|
|
|
class PluginApiTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override { initPluginState(); }
|
|
void TearDown() override { logos_core_cleanup(); }
|
|
|
|
static constexpr int kThreads = 8;
|
|
static constexpr int kIterations = 200;
|
|
};
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Multiple threads all try to load unknown plugins concurrently.
|
|
// Every call must return 0 (failure) without crashing.
|
|
// -----------------------------------------------------------------------------
|
|
TEST_F(PluginApiTest, ConcurrentLoadUnknownPlugins) {
|
|
Barrier barrier(kThreads);
|
|
std::vector<std::thread> threads;
|
|
|
|
for (int t = 0; t < kThreads; ++t) {
|
|
threads.emplace_back([&barrier, t]() {
|
|
barrier.wait();
|
|
for (int i = 0; i < kIterations; ++i) {
|
|
std::string name = "unknown_" + std::to_string(t) + "_" + std::to_string(i);
|
|
int ok = logos_core_load_module(name.c_str(), false);
|
|
EXPECT_EQ(ok, 0);
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto& th : threads) th.join();
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// logos_core_load_module(…, true) on unknown plugins from many threads.
|
|
// Each call must return 0 (failure) without crashing.
|
|
// -----------------------------------------------------------------------------
|
|
TEST_F(PluginApiTest, ConcurrentLoadWithDepsUnknown) {
|
|
Barrier barrier(kThreads);
|
|
std::vector<std::thread> threads;
|
|
|
|
for (int t = 0; t < kThreads; ++t) {
|
|
threads.emplace_back([&barrier, t]() {
|
|
barrier.wait();
|
|
for (int i = 0; i < kIterations; ++i) {
|
|
std::string name = "nodeps_" + std::to_string(t) + "_" + std::to_string(i);
|
|
int rc = logos_core_load_module(name.c_str(), true);
|
|
EXPECT_EQ(rc, 0);
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto& th : threads) th.join();
|
|
}
|
|
|
|
// =============================================================================
|
|
// Real-plugin tests — use binary-patched copies of a real Qt plugin.
|
|
// All operations go through the public logos_core C API.
|
|
// =============================================================================
|
|
|
|
class RealPluginThreadSafetyTest : public ::testing::Test {
|
|
protected:
|
|
static constexpr int kThreads = 8;
|
|
static constexpr int kModuleCount = 100;
|
|
static constexpr int kIterations = 200;
|
|
|
|
QTemporaryDir tmpDir;
|
|
QVector<DummyModule> modules;
|
|
|
|
void SetUp() override {
|
|
ASSERT_TRUE(tmpDir.isValid());
|
|
modules = DummyModuleGenerator::generate(kModuleCount, tmpDir.path());
|
|
if (modules.isEmpty())
|
|
GTEST_SKIP() << "Dummy plugin template not found — skipping real-plugin tests";
|
|
ASSERT_EQ(modules.size(), kModuleCount) << "Partial plugin generation — expected "
|
|
<< kModuleCount << " but got " << modules.size();
|
|
std::string dir = tmpDir.path().toStdString();
|
|
initPluginState(dir.c_str());
|
|
}
|
|
|
|
void TearDown() override {
|
|
logos_core_cleanup();
|
|
}
|
|
};
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Each thread processes a disjoint slice of the generated plugins via
|
|
// logos_core_process_module. After joining, every plugin must appear in
|
|
// logos_core_get_known_modules().
|
|
// -----------------------------------------------------------------------------
|
|
TEST_F(RealPluginThreadSafetyTest, ConcurrentProcessPlugins) {
|
|
Barrier barrier(kThreads);
|
|
std::atomic<int> processed{0};
|
|
std::vector<std::thread> threads;
|
|
|
|
int perThread = kModuleCount / kThreads;
|
|
|
|
for (int t = 0; t < kThreads; ++t) {
|
|
int start = t * perThread;
|
|
int end = (t == kThreads - 1) ? kModuleCount : start + perThread;
|
|
|
|
threads.emplace_back([&, start, end]() {
|
|
barrier.wait();
|
|
for (int i = start; i < end; ++i) {
|
|
std::string path = modules[i].path.toStdString();
|
|
char* name = logos_core_process_module(path.c_str());
|
|
if (name) {
|
|
processed.fetch_add(1, std::memory_order_relaxed);
|
|
delete[] name;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto& th : threads) th.join();
|
|
|
|
EXPECT_EQ(processed.load(), kModuleCount);
|
|
|
|
char** known = logos_core_get_known_modules();
|
|
EXPECT_EQ(stringArrayLen(known), kModuleCount);
|
|
for (const DummyModule& m : modules) {
|
|
std::string name = m.name.toStdString();
|
|
EXPECT_TRUE(stringArrayContains(known, name.c_str())) << name;
|
|
}
|
|
freeStringArray(known);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// All threads try to process the SAME set of plugins — tests idempotent
|
|
// insertion under heavy write contention.
|
|
// -----------------------------------------------------------------------------
|
|
TEST_F(RealPluginThreadSafetyTest, ConcurrentProcessSamePlugins) {
|
|
Barrier barrier(kThreads);
|
|
std::vector<std::thread> threads;
|
|
|
|
for (int t = 0; t < kThreads; ++t) {
|
|
threads.emplace_back([&]() {
|
|
barrier.wait();
|
|
for (const DummyModule& m : modules) {
|
|
std::string path = m.path.toStdString();
|
|
char* name = logos_core_process_module(path.c_str());
|
|
delete[] name;
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto& th : threads) th.join();
|
|
|
|
char** known = logos_core_get_known_modules();
|
|
EXPECT_EQ(stringArrayLen(known), kModuleCount);
|
|
freeStringArray(known);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Half the threads process plugins while the other half continuously call
|
|
// logos_core_get_known_modules(). Tests reader safety during concurrent writes.
|
|
// -----------------------------------------------------------------------------
|
|
TEST_F(RealPluginThreadSafetyTest, ConcurrentProcessWhileQuerying) {
|
|
Barrier barrier(kThreads);
|
|
std::atomic<int> writersDone{0};
|
|
std::vector<std::thread> threads;
|
|
|
|
int writers = kThreads / 2;
|
|
|
|
for (int t = 0; t < writers; ++t) {
|
|
int start = t * (kModuleCount / writers);
|
|
int end = (t == writers - 1) ? kModuleCount : start + (kModuleCount / writers);
|
|
|
|
threads.emplace_back([&, start, end]() {
|
|
barrier.wait();
|
|
for (int i = start; i < end; ++i) {
|
|
std::string path = modules[i].path.toStdString();
|
|
char* name = logos_core_process_module(path.c_str());
|
|
delete[] name;
|
|
}
|
|
writersDone.fetch_add(1, std::memory_order_release);
|
|
});
|
|
}
|
|
|
|
for (int t = writers; t < kThreads; ++t) {
|
|
threads.emplace_back([&, writers]() {
|
|
barrier.wait();
|
|
while (writersDone.load(std::memory_order_acquire) < writers) {
|
|
char** known = logos_core_get_known_modules();
|
|
int len = stringArrayLen(known);
|
|
EXPECT_GE(len, 0);
|
|
freeStringArray(known);
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto& th : threads) th.join();
|
|
|
|
char** known = logos_core_get_known_modules();
|
|
EXPECT_EQ(stringArrayLen(known), kModuleCount);
|
|
freeStringArray(known);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// logos_core_get_known_modules() and logos_core_get_loaded_modules() are called
|
|
// concurrently while other threads repeatedly load and unload a small set of
|
|
// plugins. Tests that the list accessors are safe under concurrent state
|
|
// changes.
|
|
// -----------------------------------------------------------------------------
|
|
TEST_F(RealPluginThreadSafetyTest, ConcurrentGetListsDuringLoadUnload) {
|
|
constexpr int kSmall = kThreads;
|
|
for (int i = 0; i < kSmall; ++i) {
|
|
std::string path = modules[i].path.toStdString();
|
|
char* name = logos_core_process_module(path.c_str());
|
|
delete[] name;
|
|
}
|
|
|
|
Barrier barrier(kThreads);
|
|
std::atomic<bool> done{false};
|
|
std::vector<std::thread> threads;
|
|
|
|
// Writer threads repeatedly load then unload the same small set.
|
|
threads.emplace_back([&]() {
|
|
barrier.wait();
|
|
for (int iter = 0; iter < kIterations; ++iter) {
|
|
std::string name = modules[iter % kSmall].name.toStdString();
|
|
(void)logos_core_load_module(name.c_str(), false);
|
|
(void)logos_core_unload_module(name.c_str(), false);
|
|
}
|
|
done.store(true, std::memory_order_release);
|
|
});
|
|
|
|
// Reader threads call both list accessors in a tight loop.
|
|
for (int t = 1; t < kThreads; ++t) {
|
|
threads.emplace_back([&]() {
|
|
barrier.wait();
|
|
while (!done.load(std::memory_order_acquire)) {
|
|
char** known = logos_core_get_known_modules();
|
|
EXPECT_NE(known, nullptr);
|
|
freeStringArray(known);
|
|
|
|
char** loaded = logos_core_get_loaded_modules();
|
|
EXPECT_NE(loaded, nullptr);
|
|
freeStringArray(loaded);
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto& th : threads) th.join();
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Process all plugins, then each thread loads a disjoint slice via
|
|
// logos_core_load_module. Tests the load path under concurrent pressure.
|
|
// With logos_host available (LOGOS_HOST_PATH set), loads succeed; without it
|
|
// they return 0. Either way the registry must remain consistent.
|
|
// -----------------------------------------------------------------------------
|
|
TEST_F(RealPluginThreadSafetyTest, ConcurrentLoadPlugin) {
|
|
for (const DummyModule& m : modules) {
|
|
std::string path = m.path.toStdString();
|
|
char* name = logos_core_process_module(path.c_str());
|
|
delete[] name;
|
|
}
|
|
|
|
{
|
|
char** known = logos_core_get_known_modules();
|
|
ASSERT_EQ(stringArrayLen(known), kModuleCount);
|
|
freeStringArray(known);
|
|
}
|
|
|
|
Barrier barrier(kThreads);
|
|
std::vector<std::thread> threads;
|
|
|
|
int perThread = kModuleCount / kThreads;
|
|
|
|
for (int t = 0; t < kThreads; ++t) {
|
|
int start = t * perThread;
|
|
int end = (t == kThreads - 1) ? kModuleCount : start + perThread;
|
|
|
|
threads.emplace_back([&, start, end]() {
|
|
barrier.wait();
|
|
for (int i = start; i < end; ++i) {
|
|
std::string name = modules[i].name.toStdString();
|
|
(void)logos_core_load_module(name.c_str(), false);
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto& th : threads) th.join();
|
|
|
|
char** known = logos_core_get_known_modules();
|
|
EXPECT_EQ(stringArrayLen(known), kModuleCount);
|
|
for (const DummyModule& m : modules) {
|
|
std::string name = m.name.toStdString();
|
|
EXPECT_TRUE(stringArrayContains(known, name.c_str())) << name;
|
|
}
|
|
freeStringArray(known);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// All threads hammer the SAME small set of plugins with logos_core_load_module.
|
|
// Tests mutex contention and the "already loaded" fast-return branch.
|
|
// -----------------------------------------------------------------------------
|
|
TEST_F(RealPluginThreadSafetyTest, ConcurrentLoadSamePlugin) {
|
|
constexpr int kSmall = kThreads;
|
|
for (int i = 0; i < kSmall; ++i) {
|
|
std::string path = modules[i].path.toStdString();
|
|
char* name = logos_core_process_module(path.c_str());
|
|
delete[] name;
|
|
}
|
|
|
|
{
|
|
char** known = logos_core_get_known_modules();
|
|
ASSERT_EQ(stringArrayLen(known), kSmall);
|
|
freeStringArray(known);
|
|
}
|
|
|
|
Barrier barrier(kThreads);
|
|
std::vector<std::thread> threads;
|
|
|
|
for (int t = 0; t < kThreads; ++t) {
|
|
threads.emplace_back([&]() {
|
|
barrier.wait();
|
|
for (int i = 0; i < kSmall; ++i) {
|
|
std::string name = modules[i].name.toStdString();
|
|
(void)logos_core_load_module(name.c_str(), false);
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto& th : threads) th.join();
|
|
|
|
char** known = logos_core_get_known_modules();
|
|
EXPECT_EQ(stringArrayLen(known), kSmall);
|
|
freeStringArray(known);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Each thread loads a disjoint slice via logos_core_load_module(…, true).
|
|
// Tests dependency resolution and loadMutex acquisition from multiple threads.
|
|
// -----------------------------------------------------------------------------
|
|
TEST_F(RealPluginThreadSafetyTest, ConcurrentLoadWithDeps) {
|
|
for (const DummyModule& m : modules) {
|
|
std::string path = m.path.toStdString();
|
|
char* name = logos_core_process_module(path.c_str());
|
|
delete[] name;
|
|
}
|
|
|
|
{
|
|
char** known = logos_core_get_known_modules();
|
|
ASSERT_EQ(stringArrayLen(known), kModuleCount);
|
|
freeStringArray(known);
|
|
}
|
|
|
|
Barrier barrier(kThreads);
|
|
std::vector<std::thread> threads;
|
|
|
|
int perThread = kModuleCount / kThreads;
|
|
|
|
for (int t = 0; t < kThreads; ++t) {
|
|
int start = t * perThread;
|
|
int end = (t == kThreads - 1) ? kModuleCount : start + perThread;
|
|
|
|
threads.emplace_back([&, start, end]() {
|
|
barrier.wait();
|
|
for (int i = start; i < end; ++i) {
|
|
std::string name = modules[i].name.toStdString();
|
|
(void)logos_core_load_module(name.c_str(), true);
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto& th : threads) th.join();
|
|
|
|
char** known = logos_core_get_known_modules();
|
|
EXPECT_EQ(stringArrayLen(known), kModuleCount);
|
|
freeStringArray(known);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Half the threads call logos_core_load_module while the other half call
|
|
// logos_core_unload_module on the same small module set. Tests the load/unload
|
|
// interplay under concurrent pressure.
|
|
// -----------------------------------------------------------------------------
|
|
TEST_F(RealPluginThreadSafetyTest, ConcurrentLoadUnloadInterleaved) {
|
|
constexpr int kSmall = kThreads;
|
|
for (int i = 0; i < kSmall; ++i) {
|
|
std::string path = modules[i].path.toStdString();
|
|
char* name = logos_core_process_module(path.c_str());
|
|
delete[] name;
|
|
}
|
|
|
|
{
|
|
char** known = logos_core_get_known_modules();
|
|
ASSERT_EQ(stringArrayLen(known), kSmall);
|
|
freeStringArray(known);
|
|
}
|
|
|
|
Barrier barrier(kThreads);
|
|
std::vector<std::thread> threads;
|
|
|
|
int loaders = kThreads / 2;
|
|
int unloaders = kThreads - loaders;
|
|
|
|
for (int t = 0; t < loaders; ++t) {
|
|
threads.emplace_back([&]() {
|
|
barrier.wait();
|
|
for (int iter = 0; iter < 10; ++iter) {
|
|
for (int i = 0; i < kSmall; ++i) {
|
|
std::string name = modules[i].name.toStdString();
|
|
(void)logos_core_load_module(name.c_str(), false);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
for (int t = 0; t < unloaders; ++t) {
|
|
threads.emplace_back([&]() {
|
|
barrier.wait();
|
|
for (int iter = 0; iter < 10; ++iter) {
|
|
for (int i = 0; i < kSmall; ++i) {
|
|
std::string name = modules[i].name.toStdString();
|
|
(void)logos_core_unload_module(name.c_str(), false);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto& th : threads) th.join();
|
|
|
|
// All modules must still be registered; load/unload must not corrupt the registry.
|
|
char** known = logos_core_get_known_modules();
|
|
EXPECT_EQ(stringArrayLen(known), kSmall);
|
|
for (int i = 0; i < kSmall; ++i) {
|
|
std::string name = modules[i].name.toStdString();
|
|
EXPECT_TRUE(stringArrayContains(known, name.c_str())) << name;
|
|
}
|
|
freeStringArray(known);
|
|
}
|