329 lines
12 KiB
C++
329 lines
12 KiB
C++
/**
|
|
* @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 <atomic>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#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<bool>(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<PluginFunction>(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<PluginFunction>(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<bool> stop{false};
|
|
std::atomic<int> readSuccesses{0};
|
|
std::vector<std::thread> 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<std::thread> loaderThreads;
|
|
std::atomic<int> 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<PluginFunction>(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
|