From 3722a239bf1502606a3ef4f025021f23ac3fcb88 Mon Sep 17 00:00:00 2001 From: Mattes D Date: Wed, 23 Jan 2019 20:54:29 +0100 Subject: BlockTypeRegistry: Initial skeleton --- src/BlockTypeRegistry.cpp | 153 +++++++++++++++ src/BlockTypeRegistry.h | 158 +++++++++++++++ src/CMakeLists.txt | 2 + tests/BlockTypeRegistry/BlockTypeRegistryTest.cpp | 228 ++++++++++++++++++++++ tests/BlockTypeRegistry/CMakeLists.txt | 40 ++++ tests/CMakeLists.txt | 1 + tests/TestHelpers.h | 79 ++++++++ 7 files changed, 661 insertions(+) create mode 100644 src/BlockTypeRegistry.cpp create mode 100644 src/BlockTypeRegistry.h create mode 100644 tests/BlockTypeRegistry/BlockTypeRegistryTest.cpp create mode 100644 tests/BlockTypeRegistry/CMakeLists.txt create mode 100644 tests/TestHelpers.h diff --git a/src/BlockTypeRegistry.cpp b/src/BlockTypeRegistry.cpp new file mode 100644 index 000000000..45ad20082 --- /dev/null +++ b/src/BlockTypeRegistry.cpp @@ -0,0 +1,153 @@ + +#include "Globals.h" +#include "BlockTypeRegistry.h" + + + + +//////////////////////////////////////////////////////////////////////////////// +// BlockInfo: + +BlockInfo::BlockInfo( + const AString & aPluginName, + const AString & aBlockTypeName, + std::shared_ptr aHandler, + const std::map & aHints, + const std::map & aHintCallbacks +): + mPluginName(aPluginName), + mBlockTypeName(aBlockTypeName), + mHandler(aHandler), + mHints(aHints), + mHintCallbacks(aHintCallbacks) +{ +} + + + + + +AString BlockInfo::hintValue( + const AString & aHintName, + const BlockState & aBlockState +) +{ + // Search the hint callbacks first: + auto itrC = mHintCallbacks.find(aHintName); + if (itrC != mHintCallbacks.end()) + { + // Hint callback found, use it: + return itrC->second(mBlockTypeName, aBlockState); + } + + // Search the static hints: + auto itr = mHints.find(aHintName); + if (itr != mHints.end()) + { + // Hint found, use it: + return itr->second; + } + + // Nothing found, return empty string: + return AString(); +} + + + + + +//////////////////////////////////////////////////////////////////////////////// +// BlockTypeRegistry: + +void BlockTypeRegistry::registerBlockType( + const AString & aPluginName, + const AString & aBlockTypeName, + std::shared_ptr aHandler, + const std::map & aHints, + const std::map & aHintCallbacks +) +{ + auto blockInfo = std::make_shared(aPluginName, aBlockTypeName, aHandler, aHints, aHintCallbacks); + + // Check previous registrations: + cCSLock lock(mCSRegistry); + auto itr = mRegistry.find(aBlockTypeName); + if (itr != mRegistry.end()) + { + if (itr->second->pluginName() != aPluginName) + { + throw AlreadyRegisteredException(itr->second, blockInfo); + } + } + + // Store the registration: + mRegistry[aBlockTypeName] = blockInfo; +} + + + + + +std::shared_ptr BlockTypeRegistry::blockInfo(const AString & aBlockTypeName) +{ + cCSLock lock(mCSRegistry); + auto itr = mRegistry.find(aBlockTypeName); + if (itr == mRegistry.end()) + { + return nullptr; + } + return itr->second; +} + + + + + +void BlockTypeRegistry::removeAllByPlugin(const AString & aPluginName) +{ + cCSLock lock(mCSRegistry); + for (auto itr = mRegistry.begin(); itr != mRegistry.end();) + { + if (itr->second->pluginName() == aPluginName) + { + itr = mRegistry.erase(itr); + } + else + { + ++itr; + } + } +} + + + + + +//////////////////////////////////////////////////////////////////////////////// +// BlockTypeRegistry::AlreadyRegisteredException: + +BlockTypeRegistry::AlreadyRegisteredException::AlreadyRegisteredException( + std::shared_ptr aPreviousRegistration, + std::shared_ptr aNewRegistration +) : + Super(message(aPreviousRegistration, aNewRegistration)), + mPreviousRegistration(aPreviousRegistration), + mNewRegistration(aNewRegistration) +{ +} + + + + + +AString BlockTypeRegistry::AlreadyRegisteredException::message( + std::shared_ptr aPreviousRegistration, + std::shared_ptr aNewRegistration +) +{ + return Printf("Attempting to register BlockTypeName %s from plugin %s, while it is already registered in plugin %s", + aNewRegistration->blockTypeName().c_str(), + aNewRegistration->pluginName().c_str(), + aPreviousRegistration->pluginName().c_str() + ); +} diff --git a/src/BlockTypeRegistry.h b/src/BlockTypeRegistry.h new file mode 100644 index 000000000..6a24445c5 --- /dev/null +++ b/src/BlockTypeRegistry.h @@ -0,0 +1,158 @@ +#pragma once + + + + + +#include + + + + + +// fwd: +class BlockState; + + + + + +/** Complete information about a single block type. +The BlockTypeRegistry uses this structure to store the registered information. */ +class BlockInfo +{ +public: + + /** Callback is used to query block hints dynamically, based on the current BlockState. + Useful for example for redstone lamps that can be turned on or off. */ + using HintCallback = std::function; + + + /** Creates a new instance with the specified BlockTypeName and handler / hints / callbacks. + aPluginName specifies the name of the plugin to associate with the block type (to allow unload / reload). */ + BlockInfo( + const AString & aPluginName, + const AString & aBlockTypeName, + std::shared_ptr aHandler, + const std::map & aHints = std::map(), + const std::map & aHintCallbacks = std::map() + ); + + + /** Retrieves the value associated with the specified hint for this specific BlockTypeName and BlockState. + Queries callbacks first, then hints if a callback doesn't exist. + Returns an empty string if hint not found at all. */ + AString hintValue( + const AString & aHintName, + const BlockState & aBlockState + ); + + // Simple getters: + const AString & pluginName() const { return mPluginName; } + const AString & blockTypeName() const { return mBlockTypeName; } + std::shared_ptr handler() const { return mHandler; } + + +private: + + /** The name of the plugin that registered the block. */ + AString mPluginName; + + /** The name of the block type, such as "minecraft:redstone_lamp" */ + AString mBlockTypeName; + + /** The callbacks to call for various interaction. */ + std::shared_ptr mHandler; + + /** Optional hints for any subsystem to use, such as "IsSnowable" -> "1". */ + std::map mHints; + + /** The callbacks for dynamic evaluation of hints, such as "LightValue" -> function(BlockTypeName, BlockState). */ + std::map mHintCallbacks; +}; + + + + + +/** Stores information on all known block types. +Can dynamically add and remove block types. +Block types are identified using BlockTypeName. +Supports unregistering and re-registering the same type by the same plugin. +Stores the name of the plugin that registered the type, for better plugin error messages ("already registered in X") +and so that we can unload and reload plugins. */ +class BlockTypeRegistry +{ +public: + // fwd: + class AlreadyRegisteredException; + + + /** Creates an empty new instance of the block type registry */ + BlockTypeRegistry() = default; + + /** Registers the specified block type. + If the block type already exists and the plugin is the same, updates the registration. + If the block type already exists and the plugin is different, throws an AlreadyRegisteredException. */ + void registerBlockType( + const AString & aPluginName, + const AString & aBlockTypeName, + std::shared_ptr aHandler, + const std::map & aHints = std::map(), + const std::map & aHintCallbacks = std::map() + ); + + /** Returns the registration information for the specified BlockTypeName. + Returns nullptr if BlockTypeName not found. */ + std::shared_ptr blockInfo(const AString & aBlockTypeName); + + /** Removes all registrations done by the specified plugin. */ + void removeAllByPlugin(const AString & aPluginName); + + +private: + + /** The actual block type registry. + Maps the BlockTypeName to the BlockInfo instance. */ + std::map> mRegistry; + + /** The CS that protects mRegistry against multithreaded access. */ + cCriticalSection mCSRegistry; +}; + + + + + +/** The exception thrown from BlockTypeRegistry::registerBlockType() if the same block type is being registered from a different plugin. */ +class BlockTypeRegistry::AlreadyRegisteredException: public std::runtime_error +{ + using Super = std::runtime_error; + +public: + + /** Creates a new instance of the exception that provides info on both the original registration and the newly attempted + registration that caused the failure. */ + AlreadyRegisteredException( + std::shared_ptr aPreviousRegistration, + std::shared_ptr aNewRegistration + ); + + // Simple getters: + std::shared_ptr previousRegistration() const { return mPreviousRegistration; } + std::shared_ptr newRegistration() const { return mNewRegistration; } + + +private: + + std::shared_ptr mPreviousRegistration; + std::shared_ptr mNewRegistration; + + + /** Returns the general exception message formatted by the two registrations. + The output is used when logging. */ + static AString message( + std::shared_ptr aPreviousRegistration, + std::shared_ptr aNewRegistration + ); +}; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e895b1657..75eb112c0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -17,6 +17,7 @@ SET (SRCS BlockArea.cpp BlockID.cpp BlockInfo.cpp + BlockTypeRegistry.cpp BrewingRecipes.cpp Broadcaster.cpp BoundingBox.cpp @@ -84,6 +85,7 @@ SET (HDRS BlockInServerPluginInterface.h BlockInfo.h BlockTracer.h + BlockTypeRegistry.h BrewingRecipes.h BoundingBox.h BuildInfo.h diff --git a/tests/BlockTypeRegistry/BlockTypeRegistryTest.cpp b/tests/BlockTypeRegistry/BlockTypeRegistryTest.cpp new file mode 100644 index 000000000..c2d8717e5 --- /dev/null +++ b/tests/BlockTypeRegistry/BlockTypeRegistryTest.cpp @@ -0,0 +1,228 @@ + +#include "Globals.h" +#include +#include "BlockTypeRegistry.h" +#include "../TestHelpers.h" + + + + +/** Dummy BlockState implementation */ +class BlockState +{ +public: + BlockState() = default; +}; + + + + +/** Dummy cBlockHandler implementation that allows simple checking for equality through mIdent. */ +class cBlockHandler +{ +public: + cBlockHandler(UInt32 aIdent): + mIdent(aIdent) + { + } + + UInt32 mIdent; +}; + + + + + +/** Tests simple block type name registration. +Registers a block type, checks that the type is then registered. */ +static void testSimpleReg() +{ + LOGD("Testing simple registration..."); + + // Register the block type: + BlockTypeRegistry reg; + AString blockTypeName("test:block1"); + AString pluginName("testPlugin"); + AString hint1("testHint1"); + AString hint1Value("value1"); + std::shared_ptr handler(new cBlockHandler(0x12345678)); + std::map hints = {{hint1, hint1Value}, {"testHint2", "value2"}}; + std::map hintCallbacks; + reg.registerBlockType(pluginName, blockTypeName, handler, hints, hintCallbacks); + + // Query the registration: + auto blockInfo = reg.blockInfo(blockTypeName); + TEST_NOTEQUAL(blockInfo, nullptr); + TEST_EQUAL(blockInfo->blockTypeName(), blockTypeName); + TEST_EQUAL(blockInfo->pluginName(), pluginName); + TEST_EQUAL(blockInfo->handler(), handler); + TEST_EQUAL(blockInfo->hintValue(hint1, BlockState()), hint1Value); + TEST_EQUAL(blockInfo->hintValue("nonexistent", BlockState()), ""); +} + + + + + +/** Tests that the plugin-based information is used correctly for registration. +Registers two different block types with two different plugins, then tries to re-register them from a different plugin. +Finally removes the registration through removeAllByPlugin() and checks its success. */ +static void testPlugins() +{ + LOGD("Testing plugin-based checks / removal..."); + + // Register the block types: + BlockTypeRegistry reg; + AString blockTypeName1("test:block1"); + AString pluginName1("testPlugin1"); + AString hint1("testHint1"); + AString hint1Value("value1"); + std::shared_ptr handler1(new cBlockHandler(1)); + std::map hints = {{hint1, hint1Value}, {"testHint2", "value2"}}; + std::map hintCallbacks; + reg.registerBlockType(pluginName1, blockTypeName1, handler1, hints, hintCallbacks); + AString blockTypeName2("test:block2"); + AString pluginName2("testPlugin2"); + std::shared_ptr handler2(new cBlockHandler(2)); + reg.registerBlockType(pluginName2, blockTypeName2, handler2, hints, hintCallbacks); + + // Test the refusal to register under a different plugin: + TEST_THROWS(reg.registerBlockType(pluginName2, blockTypeName1, handler2, hints, hintCallbacks), BlockTypeRegistry::AlreadyRegisteredException); + TEST_EQUAL(reg.blockInfo(blockTypeName1)->handler()->mIdent, 1); // Did we overwrite the old registration? + reg.registerBlockType(pluginName1, blockTypeName1, handler1, hints, hintCallbacks); // Re-registering must succeed + + // Unregister by plugin, then re-register from a different plugin: + reg.removeAllByPlugin(pluginName1); + TEST_EQUAL(reg.blockInfo(blockTypeName1), nullptr); // Unregistered successfully + TEST_NOTEQUAL(reg.blockInfo(blockTypeName2), nullptr); // Didn't unregister from the other plugin + std::shared_ptr handler3(new cBlockHandler(3)); + reg.registerBlockType(pluginName2, blockTypeName1, handler3, hints, hintCallbacks); + TEST_NOTEQUAL(reg.blockInfo(blockTypeName1), nullptr); // Registered successfully + TEST_EQUAL(reg.blockInfo(blockTypeName1)->pluginName(), pluginName2); + TEST_EQUAL(reg.blockInfo(blockTypeName1)->handler()->mIdent, 3); + TEST_EQUAL(reg.blockInfo(blockTypeName2)->handler()->mIdent, 2); + reg.removeAllByPlugin(pluginName2); + TEST_EQUAL(reg.blockInfo(blockTypeName1), nullptr); // Unregistered successfully + TEST_EQUAL(reg.blockInfo(blockTypeName2), nullptr); // Unregistered successfully +} + + + + +/** Tests that the callback-based hints work properly. */ +static void testHintCallbacks() +{ + LOGD("Testing hint callbacks..."); + + // Register the block type: + BlockTypeRegistry reg; + AString blockTypeName("test:block1"); + AString pluginName("testPlugin"); + AString hint1("testHint1"); + AString hint1Value("value1"); + AString hc1("hintCallback1"); + int callbackCount = 0; + auto callback1 = [&callbackCount](const AString & aBlockType, const BlockState & aBlockState) + { + callbackCount = callbackCount + 1; + return aBlockType + "_hint"; + }; + std::shared_ptr handler(new cBlockHandler(0x12345678)); + std::map hints = {{hint1, hint1Value}, {"testHint2", "value2"}}; + std::map hintCallbacks = {{hc1, callback1}}; + reg.registerBlockType(pluginName, blockTypeName, handler, hints, hintCallbacks); + + // Check that querying the hint using a callback works: + TEST_EQUAL(reg.blockInfo(blockTypeName)->hintValue(hc1, BlockState()), blockTypeName + "_hint"); + TEST_EQUAL(callbackCount, 1); // Called exactly once +} + + + + + +/** Tests whether thread-locking works properly by running two threads, +one constantly (re-)registering and the other one constantly querying the same block type. */ +static void testThreadLocking() +{ + LOGD("Testing thread locking..."); + + // Register the block type: + BlockTypeRegistry reg; + AString blockTypeName("test:block1"); + AString pluginName("testPlugin"); + AString hint1("testHint1"); + AString hint1Value("value1"); + std::shared_ptr handler(new cBlockHandler(0x12345678)); + std::map hints = {{hint1, hint1Value}, {"testHint2", "value2"}}; + std::map hintCallbacks; + reg.registerBlockType(pluginName, blockTypeName, handler, hints, hintCallbacks); + + // Run the two threads for at least a second: + auto endTime = time(nullptr) + 2; + auto keepRegistering = [&]() + { + while (time(nullptr) < endTime) + { + reg.registerBlockType(pluginName, blockTypeName, handler, hints, hintCallbacks); + } + }; + auto keepQuerying = [&]() + { + unsigned numQueries = 0; + while (time(nullptr) < endTime) + { + TEST_NOTEQUAL(reg.blockInfo(blockTypeName), nullptr); + numQueries += 1; + } + LOGD("%u queries have been executed", numQueries); + }; + std::thread thr1(keepRegistering); + std::thread thr2(keepQuerying); + thr1.join(); + thr2.join(); +} + + + + + +static void testBlockTypeRegistry() +{ + testSimpleReg(); + testPlugins(); + testHintCallbacks(); + testThreadLocking(); +} + + + + + +int main() +{ + LOGD("BlockTypeRegistryTest started"); + + try + { + testBlockTypeRegistry(); + } + catch (const TestException & exc) + { + LOGERROR("BlockTypeRegistryTest has failed, an unhandled exception was thrown: %s", exc.mMessage.c_str()); + return 1; + } + catch (...) + { + LOGERROR("BlockTypeRegistryTest has failed, an unhandled exception was thrown."); + return 1; + } + + LOGD("BlockTypeRegistryTest finished"); + + return 0; +} + + + + diff --git a/tests/BlockTypeRegistry/CMakeLists.txt b/tests/BlockTypeRegistry/CMakeLists.txt new file mode 100644 index 000000000..25b18c373 --- /dev/null +++ b/tests/BlockTypeRegistry/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.0.2) +enable_testing() + +include_directories(${CMAKE_SOURCE_DIR}/src/) + +add_definitions(-DTEST_GLOBALS=1) + + + + + +# Define individual test executables: + +# BlockTypeRegistryTest: Verify that the BlockTypeRegistry class works as intended: +add_executable(BlockTypeRegistryTest + BlockTypeRegistryTest.cpp + ../TestHelpers.h + ${CMAKE_SOURCE_DIR}/src/BlockTypeRegistry.cpp + ${CMAKE_SOURCE_DIR}/src/StringUtils.cpp + ${CMAKE_SOURCE_DIR}/src/OSSupport/CriticalSection.cpp +) +target_link_libraries(BlockTypeRegistryTest fmt::fmt) + + + + + +# Define individual tests: + +add_test(NAME BlockTypeRegistryTest COMMAND BlockTypeRegistryTest) + + + + + +# Put all the tests into a solution folder (MSVC): +set_target_properties( + BlockTypeRegistryTest + PROPERTIES FOLDER Tests/BlockTypeRegistry +) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 20ae1bfa3..74e4323ec 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,6 +6,7 @@ endif() add_definitions(-DTEST_GLOBALS=1) +add_subdirectory(BlockTypeRegistry) add_subdirectory(BoundingBox) add_subdirectory(ByteBuffer) add_subdirectory(ChunkData) diff --git a/tests/TestHelpers.h b/tests/TestHelpers.h new file mode 100644 index 000000000..2e43e1e0d --- /dev/null +++ b/tests/TestHelpers.h @@ -0,0 +1,79 @@ +// Helper macros for writing exception-based tests + + + + +/** The exception that is thrown if a test fails. +It doesn't inherit from any type so that it is not possible to catch it by a mistake, +it needs to be caught explicitly (used in the TEST_THROWS). +It bears a single message that is to be displayed to stderr. */ +class TestException +{ +public: + TestException(const AString & aMessage): + mMessage(aMessage) + { + } + + AString mMessage; +}; + + + + + +/** Checks that the two values are equal; if not, throws a TestException. */ +#define TEST_EQUAL(VAL1, VAL2) \ + if (VAL1 != VAL2) \ + { \ + throw TestException(Printf("%s (line %d): Equality test failed: %s != %s", \ + __FUNCTION__, __LINE__, \ + #VAL1, #VAL2 \ + )); \ + } + + + +/** Checks that the two values are not equal; if they are, throws a TestException. */ +#define TEST_NOTEQUAL(VAL1, VAL2) \ + if (VAL1 == VAL2) \ + { \ + throw TestException(Printf("%s (line %d): Inequality test failed: %s == %s", \ + __FUNCTION__, __LINE__, \ + #VAL1, #VAL2 \ + )); \ + } + + + +/** Checks that the statement throws an exception of the specified class. */ +#define TEST_THROWS(Stmt, ExcClass) \ + try \ + { \ + Stmt; \ + throw TestException(Printf("%s (line %d): Failed to throw an exception of type %s", \ + __FUNCTION__, __LINE__, \ + #ExcClass \ + )); \ + } \ + catch (const ExcClass &) \ + { \ + /* This is the expected case. */ \ + } \ + catch (const std::exception & exc) \ + { \ + throw TestException(Printf("%s (line %d): An unexpected std::exception descendant was thrown, was expecting type %s. Message is: %s", \ + __FUNCTION__, __LINE__, \ + #ExcClass, exc.what() \ + )); \ + } \ + catch (...) \ + { \ + throw TestException(Printf("%s (line %d): An unexpected exception object was thrown, was expecting type %s", \ + __FUNCTION__, __LINE__, \ + #ExcClass \ + )); \ + } + + + -- cgit v1.2.3