/** * @file Main.cpp * @brief Exercises hot_reload::Context directly -- the modern C++ API -- rather than through * hot_reload.h, which HotReloadTests/Main.cpp already covers thoroughly. Two things only that * test cannot check at all: that Context is genuinely usable without ever touching the C API, * and that it is safe to call concurrently (see runConcurrencyStress() below, meant to be run * under ThreadSanitizer -- see source/HotReload/README.md's "What's actually been tested"). * * Reuses PluginV1/PluginV2 from source/examples/HotReloadDemo as fixtures, exactly as * HotReloadTests/Main.cpp does and for the same reason (see that file's own comment). */ #include "hot_reload/Context.hpp" #include "hot_reload/Demangle.hpp" #include #include #include #include #include #include #ifndef HOTRELOAD_TESTS_SKIP namespace { int g_failures = 0; /// Records a failed check to stderr and increments g_failures. Called only through CHECK(). /// @param condition The check's result. /// @param expression The checked expression, stringified. /// @param file The source file the check appears in. /// @param line The line the check appears on. void checkImpl(bool condition, const char* expression, const char* file, int line) { if (!condition) { std::fprintf(stderr, "CHECK FAILED: %s (%s:%d)\n", expression, file, line); ++g_failures; } } } /// Checks @p expr and records a failure (with its source location) if it is false, without /// relying on assert(), which NDEBUG (this project's default Release build) compiles out. #define CHECK(expr) checkImpl((expr), #expr, __FILE__, __LINE__) namespace { using PluginFunction = int (*)(int); /// Basic single-threaded correctness: load, resolve, call, reload, call the same pointer /// again -- the same story HotReloadTests/Main.cpp already tells through the C API, told /// here entirely through hot_reload::Context instead, to confirm the C API is a wrapper /// around this and not the only way to get this behavior. void runBasicUsage() { hot_reload::Context ctx; hot_reload::Result loaded = ctx.load("plugin", PLUGIN_V1_PATH); CHECK(loaded.ok()); CHECK(static_cast(loaded)); CHECK(loaded.status() == hot_reload::Status::Ok); CHECK(loaded.message().empty()); // Loading the same name twice is InvalidArgument, with a message that explains why -- // reload() is for this, not a second load(). hot_reload::Result reloadedAsLoad = ctx.load("plugin", PLUGIN_V1_PATH); CHECK(!reloadedAsLoad.ok()); CHECK(reloadedAsLoad.status() == hot_reload::Status::InvalidArgument); auto tick = reinterpret_cast(ctx.findSymbol("plugin", "plugin_tick")); CHECK(tick != nullptr); CHECK(tick(10) == 20); // V1: input * 2 int patchedCount = 0; int removedCount = 0; hot_reload::Options options; options.onSymbolEvent = [&patchedCount, &removedCount](const hot_reload::SymbolEventInfo& event) { if (event.kind == hot_reload::SymbolEvent::Patched) { ++patchedCount; } else if (event.kind == hot_reload::SymbolEvent::Removed) { ++removedCount; CHECK(event.name == "plugin_legacy_feature"); } CHECK(!event.summary().empty()); // exercises SymbolEventInfo::summary() for real }; hot_reload::Context ctxWithCallback(std::move(options)); CHECK(ctxWithCallback.load("plugin", PLUGIN_V1_PATH).ok()); hot_reload::Result reloadResult = ctxWithCallback.reload("plugin", PLUGIN_V2_PATH); CHECK(reloadResult.ok()); CHECK(patchedCount > 0); CHECK(removedCount == 1); auto tickAfterReload = reinterpret_cast(ctxWithCallback.findSymbol("plugin", "plugin_tick")); CHECK(tickAfterReload != nullptr); CHECK(tickAfterReload(10) == 30); // V2: input * 3 // reload() for a name that was never load()'ed. hot_reload::Context emptyCtx; hot_reload::Result noPrevious = emptyCtx.reload("never-loaded", PLUGIN_V1_PATH); CHECK(!noPrevious.ok()); CHECK(noPrevious.status() == hot_reload::Status::NoPreviousVersion); // load() for a file that cannot possibly exist. hot_reload::Result loadFailed = emptyCtx.load("bogus", "/this/path/does/not/exist.so"); CHECK(!loadFailed.ok()); CHECK(loadFailed.status() == hot_reload::Status::LoadFailed); CHECK(!loadFailed.message().empty()); // findSymbol() for an unknown module, and for an unknown symbol in a known one. CHECK(ctx.findSymbol("never-loaded", "anything") == nullptr); CHECK(ctx.findSymbol("plugin", "this_symbol_does_not_exist") == nullptr); } /// Result::success()/failure() constructed directly, outside any Context call at all -- /// confirms the type is a freely usable value type, not something only Context can produce. void runResultAsValueType() { hot_reload::Result ok = hot_reload::Result::success(); CHECK(ok.ok()); CHECK(ok.status() == hot_reload::Status::Ok); CHECK(ok.message().empty()); hot_reload::Result failed = hot_reload::Result::failure(hot_reload::Status::LoadFailed, "a specific reason"); CHECK(!failed.ok()); CHECK(failed.status() == hot_reload::Status::LoadFailed); CHECK(failed.message() == "a specific reason"); for (hot_reload::Status status : {hot_reload::Status::Ok, hot_reload::Status::InvalidArgument, hot_reload::Status::LoadFailed, hot_reload::Status::NoPreviousVersion, hot_reload::Status::UnsupportedArchitecture}) { CHECK(!hot_reload::toString(status).empty()); // every enumerator must map to something real } for (hot_reload::SymbolEvent event : {hot_reload::SymbolEvent::Patched, hot_reload::SymbolEvent::Added, hot_reload::SymbolEvent::Removed, hot_reload::SymbolEvent::SkippedNotCode, hot_reload::SymbolEvent::SkippedTooSmall, hot_reload::SymbolEvent::SkippedUnwritable}) { CHECK(!hot_reload::toString(event).empty()); } } /// Context is movable even though its Impl holds a (non-movable) std::shared_mutex -- /// confirms the PIMPL actually buys the movability Context.hpp's doc comment promises. void runMoveSemantics() { hot_reload::Context original; CHECK(original.load("plugin", PLUGIN_V1_PATH).ok()); hot_reload::Context moved(std::move(original)); CHECK(moved.findSymbol("plugin", "plugin_tick") != nullptr); hot_reload::Context another; another = std::move(moved); CHECK(another.findSymbol("plugin", "plugin_tick") != nullptr); } /// hot_reload::demangle() used directly, not through hot_reload_demangle() -- see /// Demangle.hpp. A real mangled name, not a hand-typed guess: taken from the demo's own /// SymbolEvent log for CppWidget's constructor, which is exactly what a real caller would /// have in hand (a name straight out of a SymbolEventInfo). void runDemangle() { hot_reload::Context ctx; CHECK(ctx.load("plugin", PLUGIN_V1_PATH).ok()); std::string realMangledName; hot_reload::Options options; options.onSymbolEvent = [&realMangledName](const hot_reload::SymbolEventInfo& event) { if (realMangledName.empty() && event.name.find("CppWidget") != std::string_view::npos) { realMangledName = event.name; } }; hot_reload::Context ctxForReload(std::move(options)); CHECK(ctxForReload.load("plugin", PLUGIN_V1_PATH).ok()); CHECK(ctxForReload.reload("plugin", PLUGIN_V2_PATH).ok()); CHECK(!realMangledName.empty()); std::string demangled = hot_reload::demangle(realMangledName); CHECK(demangled.find("CppWidget") != std::string::npos); // Something that is definitely not a mangled C++ name comes back unchanged, not empty // and not garbage. CHECK(hot_reload::demangle("plugin_tick") == "plugin_tick"); } /// The thread-safety claim in Context.hpp's own doc comment, actually exercised: several /// threads calling findSymbol() on a shared Context concurrently with other threads /// load()/reload()-ing (some on the SAME module name as each other -- a genuine race for /// which build ends up active, which this makes no assertion about -- and some on /// per-thread-unique names, which this does check resolve correctly). Run this binary under /// ThreadSanitizer for the result that actually matters: zero reported data races, not just /// "it didn't crash this time". void runConcurrencyStress() { constexpr int kReaderThreads = 4; constexpr int kReaderIterations = 500; constexpr int kSharedReloaderThreads = 2; constexpr int kSharedReloadIterations = 25; constexpr int kUniqueLoaderThreads = 4; hot_reload::Context ctx; CHECK(ctx.load("plugin", PLUGIN_V1_PATH).ok()); std::atomic stop{false}; std::atomic readSuccesses{0}; std::vector threads; // Readers: hammer findSymbol() on the one module every writer thread below is also // touching, for the whole stress run. for (int i = 0; i < kReaderThreads; ++i) { threads.emplace_back( [&ctx, &stop, &readSuccesses]() { int iterations = 0; while (!stop.load(std::memory_order_relaxed) && iterations < kReaderIterations) { const void* symbol = ctx.findSymbol("plugin", "plugin_tick"); if (symbol != nullptr) { readSuccesses.fetch_add(1, std::memory_order_relaxed); } ++iterations; } }); } // Writers: alternate reload()-ing the SAME module name between V1 and V2, concurrently // with each other and with the readers above. No assertion on which build "wins" any // one moment -- only that nothing crashes and the context stays internally consistent. for (int i = 0; i < kSharedReloaderThreads; ++i) { threads.emplace_back( [&ctx]() { for (int iteration = 0; iteration < kSharedReloadIterations; ++iteration) { const char* path = (iteration % 2 == 0) ? PLUGIN_V2_PATH : PLUGIN_V1_PATH; hot_reload::Result result = ctx.reload("plugin", path); checkImpl(result.ok(), "result.ok() during concurrent reload", __FILE__, __LINE__); } }); } // Loaders: each thread load()s its OWN uniquely-named module (same underlying file, // different logical names) -- concurrent INSERTS into the same map, from different // threads, each then confirmed to resolve correctly afterward. std::vector loaderThreads; std::atomic loadFailures{0}; for (int i = 0; i < kUniqueLoaderThreads; ++i) { threads.emplace_back( [&ctx, &loadFailures, i]() { std::string name = "unique-module-" + std::to_string(i); hot_reload::Result result = ctx.load(name, PLUGIN_V1_PATH); if (!result.ok()) { loadFailures.fetch_add(1, std::memory_order_relaxed); return; } const void* symbol = ctx.findSymbol(name, "plugin_tick"); if (symbol == nullptr) { loadFailures.fetch_add(1, std::memory_order_relaxed); } }); } for (std::thread& thread : threads) { thread.join(); } stop.store(true, std::memory_order_relaxed); CHECK(readSuccesses.load() > 0); // readers actually found the symbol at least sometimes CHECK(loadFailures.load() == 0); // every per-thread-unique load()+findSymbol() succeeded // The context is still fully usable after all of that -- one more reload, one more call, // for a final sanity check that nothing was left in a corrupted state. CHECK(ctx.reload("plugin", PLUGIN_V2_PATH).ok()); auto tick = reinterpret_cast(ctx.findSymbol("plugin", "plugin_tick")); CHECK(tick != nullptr); CHECK(tick(10) == 30); } } int main() { runBasicUsage(); runResultAsValueType(); runMoveSemantics(); runDemangle(); runConcurrencyStress(); if (g_failures == 0) { std::printf("context_tests: all checks passed\n"); } else { std::fprintf(stderr, "context_tests: %d check(s) failed\n", g_failures); } return g_failures == 0 ? 0 : 1; } #else int main() { std::printf("context_tests: skipped (HOTRELOAD_TESTS_SKIP set -- needs BUILD_EXAMPLES=ON too)\n"); return 0; } #endif