Added ARM32 support
This commit is contained in:
1 parent
df792729ef
commit
1212cb9dbe
36 files changed
+2381
-783
No files matched your search
@@ -0,0 +1,16 @@
|
||||
# Exercises hot_reload::Context (the C++ API) directly, including a real multi-threaded stress
|
||||
# test -- see Main.cpp's own file comment. Reuses PluginV1/PluginV2 from
|
||||
# source/examples/HotReloadDemo as fixtures, exactly as HotReloadTests does and for the same
|
||||
# reason (see that CMakeLists.txt's own comment); requires BUILD_EXAMPLES as well as BUILD_TESTS
|
||||
# for that reason.
|
||||
add_test_module(context_tests OUTPUT_NAME "context_tests" DEPENDS hotreload_lib)
|
||||
|
||||
if(BUILD_EXAMPLES)
|
||||
add_dependencies(context_tests hotreload_demo_plugin_v1 hotreload_demo_plugin_v2)
|
||||
target_compile_definitions(context_tests PRIVATE
|
||||
"PLUGIN_V1_PATH=\"$<TARGET_FILE:hotreload_demo_plugin_v1>\""
|
||||
"PLUGIN_V2_PATH=\"$<TARGET_FILE:hotreload_demo_plugin_v2>\"")
|
||||
else()
|
||||
message(WARNING "context_tests needs BUILD_EXAMPLES=ON too (it reuses the HotReloadDemo plugins as fixtures); skipping its checks.")
|
||||
target_compile_definitions(context_tests PRIVATE HOTRELOAD_TESTS_SKIP=1)
|
||||
endif()
|
||||
@@ -0,0 +1,328 @@
|
||||
/**
|
||||
* @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
|
||||
Reference in new issue
Block a user