Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
This commit is contained in:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -0,0 +1,124 @@
if(NOT DISABLE_EDM)
list(APPEND CMAKE_MODULE_PATH ${CPM_PACKAGE_catch2_SOURCE_DIR}/extras)
endif()
include(Catch)
set(TEST_TARGET_NAME ${PROJECT_NAME}_tests)
add_executable(${TEST_TARGET_NAME})
target_include_directories(${TEST_TARGET_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_sources(${TEST_TARGET_NAME} PRIVATE
test_config.cpp
test_config_sqlite.cpp
test_conversions.cpp
test_filesystem_helpers.cpp
test_helpers.cpp
test_message_handler.cpp
test_message_handler_scaling_policy.cpp
helpers.cpp
)
target_compile_definitions(${TEST_TARGET_NAME}
PRIVATE
EVEREST_FRAMEWORK_EXPECTED_THREAD_POOL_SCALING_POLICY=${EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_ID}
EVEREST_FRAMEWORK_EXPECTED_THREAD_POOL_SCALING_LATENCY_THRESHOLD_MS=${EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_THRESHOLD_MS}
EVEREST_FRAMEWORK_EXPECTED_THREAD_POOL_SCALING_LATENCY_TICK_MS=${EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_TICK_MS}
EVEREST_FRAMEWORK_EXPECTED_THREAD_POOL_SCALING_FIXED_SIZE_THRESHOLD=${EVEREST_FRAMEWORK_THREAD_POOL_SCALING_FIXED_SIZE_THRESHOLD}
)
add_subdirectory(controller)
target_link_libraries(${TEST_TARGET_NAME}
PRIVATE
everest::framework
everest::log
everest::sqlite
Catch2::Catch2WithMain
)
catch_discover_tests(${TEST_TARGET_NAME})
include(test_utilities.cmake)
setup_test_directory(empty_config)
setup_test_directory(valid_config)
setup_test_directory(valid_config_custom_prefix)
file(RENAME ${CMAKE_CURRENT_BINARY_DIR}/valid_config_custom_prefix ${CMAKE_CURRENT_BINARY_DIR}/valid_config_custom_prefix_tmp)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/valid_config_custom_prefix)
file(RENAME ${CMAKE_CURRENT_BINARY_DIR}/valid_config_custom_prefix_tmp ${CMAKE_CURRENT_BINARY_DIR}/valid_config_custom_prefix/usr)
file(RENAME ${CMAKE_CURRENT_BINARY_DIR}/valid_config_custom_prefix/usr/etc ${CMAKE_CURRENT_BINARY_DIR}/valid_config_custom_prefix/etc)
setup_test_directory(valid_module_config TESTValidManifest test_interface)
setup_test_directory(valid_module_config_userconfig TESTValidManifest test_interface
CONFIG valid_module_config_config.yaml
USER_CONFIG valid_module_config_userconfig.yaml
)
setup_test_directory(valid_module_config_validate TESTValidManifestCmdVar test_interface_cmd_var
TYPE_FILES test_type.yaml) # FIXME (aw): type is missing
setup_test_directory(valid_module_config_json TESTValidManifest test_interface
CONFIG valid_module_config_json_config.json
)
setup_test_directory(broken_yaml)
setup_test_directory(empty_yaml_object USE_FILESYSTEM_HIERARCHY_STANDARD)
setup_test_directory(empty_yaml USE_FILESYSTEM_HIERARCHY_STANDARD)
setup_test_directory(null_yaml USE_FILESYSTEM_HIERARCHY_STANDARD)
setup_test_directory(string_yaml USE_FILESYSTEM_HIERARCHY_STANDARD)
setup_test_directory(missing_module)
setup_test_directory(broken_manifest_1 TESTBrokenManifest1 test_interface)
setup_test_directory(broken_manifest_2 TESTBrokenManifest2 test_interface)
setup_test_directory(broken_manifest_3 TESTBrokenManifest3 test_interface)
setup_test_directory(broken_manifest_4 TESTBrokenManifest4 test_interface)
setup_test_directory(missing_interface TESTMissingInterface)
setup_test_directory(unknown_impls TESTValidManifest test_interface)
setup_test_directory(missing_config_entry TESTValidManifest test_interface)
setup_test_directory(invalid_config_entry_type TESTValidManifest test_interface)
setup_test_directory(missing_impl_config_entry TESTValidManifest test_interface)
setup_test_directory(
valid_complete_config
CONFIG valid_complete_config.json
MODULES TESTValidManifest TESTValidManifestCmdVar TESTValidManifestRequires
INTERFACE_FILES test_interface test_interface_cmd_var
)
setup_test_directory(
two_module_test
CONFIG two_modules.yaml
MODULES TESTModuleA TESTModuleB
INTERFACE_FILES test_interface test_interface_cmd_var
)
file(COPY ${PROJECT_SOURCE_DIR}/schemas/migrations
DESTINATION ${CMAKE_CURRENT_BINARY_DIR}
)
evc_include(CodeCoverage)
append_coverage_compiler_flags_to_target(framework)
append_coverage_compiler_flags_to_target(manager)
if (EVEREST_ENABLE_ADMIN_PANEL_BACKEND)
append_coverage_compiler_flags_to_target(controller)
endif()
# set(GCOVR_ADDITIONAL_ARGS "--gcov-ignore-errors=all")
setup_target_for_coverage_gcovr_html(
NAME ${PROJECT_NAME}_gcovr_coverage
EXECUTABLE ctest --output-on-failure
BASE_DIRECTORY "${PROJECT_SOURCE_DIR}"
DEPENDENCIES ${PROJECT_NAME}_tests everest::framework
EXCLUDE "${CMAKE_BINARY_DIR}/*" "${CPM_SOURCE_CACHE}"
)
setup_target_for_coverage_gcovr_xml(
NAME ${PROJECT_NAME}_gcovr_coverage_xml
EXECUTABLE ctest --output-on-failure
BASE_DIRECTORY "${PROJECT_SOURCE_DIR}"
DEPENDENCIES ${PROJECT_NAME}_tests everest::framework
EXCLUDE "${CMAKE_BINARY_DIR}/*" "${CPM_SOURCE_CACHE}"
)

View File

@@ -0,0 +1,22 @@
set (TRANSPILE_CONFIG_TEST "${TEST_TARGET_NAME}_transpile_config")
add_executable(
${TRANSPILE_CONFIG_TEST}
test_transpile_config.cpp
${PROJECT_SOURCE_DIR}/src/controller/transpile_config.cpp
)
target_include_directories(${TRANSPILE_CONFIG_TEST}
PRIVATE
${PROJECT_SOURCE_DIR}/src/controller
)
target_link_libraries(${TRANSPILE_CONFIG_TEST}
PRIVATE
Catch2::Catch2WithMain
nlohmann_json::nlohmann_json
ryml::ryml
)
catch_discover_tests(${TRANSPILE_CONFIG_TEST})

View File

@@ -0,0 +1,186 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest
#include <sstream>
#include <string>
#include <catch2/catch_all.hpp>
#include <nlohmann/json.hpp>
#include <ryml.hpp>
#include <ryml_std.hpp>
#include "transpile_config.hpp"
using json = nlohmann::json;
const std::string json_strings = R"(
"string": {
"complex name": "it is",
"empty": "",
"complex-name": "with hypen!",
"couldBeANumber": "e"
}
)";
const std::string yaml_strings = R"(string:
complex name: it is
'complex-name': with hypen!
couldBeANumber: e
empty: ''
)";
const std::string json_numbers = R"(
"number": {
"intger": 1,
"float": 1.5,
"eNotation": 6.02214086E23
}
)";
const std::string yaml_numbers = R"(number:
eNotation: 6.02214086e+23
float: 1.5
intger: 1
)";
const std::string json_booleans = R"(
"boolean": {
"true": true,
"false": false
}
)";
const std::string yaml_booleans = R"(boolean:
false: false
true: true
)";
const std::string json_arrays = R"(
"array": {
"strings": ["The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog", ""],
"number": [1, 2, 3, 4, 5, 6, 7, 8, 9],
"boolean": [true, false, true, true, false, false, false],
"mixed": [true, "a string", 42, "", null]
}
)";
const std::string yaml_arrays = R"(array:
boolean:
- true
- false
- true
- true
- false
- false
- false
mixed:
- true
- a string
- 42
- ''
- null
number:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
strings:
- The
- quick
- brown
- fox
- jumps
- over
- the
- lazy
- dog
- ''
)";
const std::string json_null = R"(
"null": null
)";
const std::string yaml_null = R"(null: null
)";
SCENARIO("Check config transpiler", "[!throws]") {
GIVEN("only strings") {
std::string json_serialized = "{";
json_serialized += json_strings;
json_serialized += "}";
json parsed = json::parse(json_serialized);
auto result_yaml = ryml::emitrs<std::string>(transpile_config(parsed));
THEN("It should contain the relevant data") {
CHECK(result_yaml == yaml_strings);
}
}
GIVEN("only numbers") {
std::string json_serialized = "{";
json_serialized += json_numbers;
json_serialized += "}";
json parsed = json::parse(json_serialized);
auto result_yaml = ryml::emitrs<std::string>(transpile_config(parsed));
THEN("It should contain the relevant data") {
CHECK(result_yaml == yaml_numbers);
}
}
GIVEN("only booleans") {
std::string json_serialized = "{";
json_serialized += json_booleans;
json_serialized += "}";
json parsed = json::parse(json_serialized);
auto result_yaml = ryml::emitrs<std::string>(transpile_config(parsed));
THEN("It should contain the relevant data") {
CHECK(result_yaml == yaml_booleans);
}
}
GIVEN("only arrays") {
std::string json_serialized = "{";
json_serialized += json_arrays;
json_serialized += "}";
json parsed = json::parse(json_serialized);
auto result_yaml = ryml::emitrs<std::string>(transpile_config(parsed));
THEN("It should contain the relevant data") {
CHECK(result_yaml == yaml_arrays);
}
}
GIVEN("only null") {
std::string json_serialized = "{";
json_serialized += json_null;
json_serialized += "}";
json parsed = json::parse(json_serialized);
auto result_yaml = ryml::emitrs<std::string>(transpile_config(parsed));
THEN("It should contain the relevant data") {
CHECK(result_yaml == yaml_null);
}
}
GIVEN("everything") {
std::string json_serialized = "{";
json_serialized += json_arrays;
json_serialized += ",";
json_serialized += json_booleans;
json_serialized += ",";
json_serialized += json_null;
json_serialized += ",";
json_serialized += json_numbers;
json_serialized += ",";
json_serialized += json_strings;
json_serialized += "}";
json parsed = json::parse(json_serialized);
std::string expected_yaml = "";
expected_yaml += yaml_arrays;
expected_yaml += yaml_booleans;
expected_yaml += yaml_null;
expected_yaml += yaml_numbers;
expected_yaml += yaml_strings;
auto result_yaml = ryml::emitrs<std::string>(transpile_config(parsed));
THEN("It should contain the relevant data") {
CHECK(result_yaml == expected_yaml);
}
}
}

View File

@@ -0,0 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <tests/helpers.hpp>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
namespace Everest {
namespace tests {
fs::path get_bin_dir() {
return fs::canonical("/proc/self/exe").parent_path();
}
} // namespace tests
} // namespace Everest

View File

@@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef TESTS_HELPERS_HPP
#include <filesystem>
namespace fs = std::filesystem;
namespace Everest {
namespace tests {
fs::path get_bin_dir();
} // namespace tests
} // namespace Everest
#endif // TESTS_HELPERS_HPP

View File

@@ -0,0 +1,130 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#pragma once
#include <future>
#include <optional>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include <nlohmann/json.hpp>
#include <utils/mqtt_abstraction.hpp>
namespace Everest {
namespace tests {
/// \brief A mock MQTTAbstraction for unit testing.
///
/// Overrides get() to return a pre-configured JSON response and records
/// all publish() and register_handler() calls for inspection in tests.
class MockMQTTAbstraction : public MQTTAbstraction {
public:
explicit MockMQTTAbstraction(std::string everest_prefix = "everest/") :
m_everest_prefix(std::move(everest_prefix)) {
}
// --- Configurable behaviour ---
/// \brief Set the JSON that the next get() call will return.
void set_get_response(nlohmann::json response) {
m_get_response = std::move(response);
}
// --- Recorded state ---
/// \brief Returns the MQTTRequest passed to the last get() call, if any.
const std::optional<MQTTRequest>& last_get_request() const {
return m_last_get_request;
}
/// \brief Returns all (topic, payload) pairs passed to publish().
const std::vector<std::pair<std::string, nlohmann::json>>& published() const {
return m_published;
}
/// \brief Returns all handlers registered via register_handler(), keyed by topic.
const std::unordered_map<std::string, std::shared_ptr<TypedHandler>>& registered_handlers() const {
return m_handlers;
}
// --- MQTTAbstraction overrides ---
nlohmann::json get(const MQTTRequest& request, std::size_t /*retries*/ = 0) override {
m_last_get_request = request;
return m_get_response;
}
nlohmann::json get(const std::string& topic, QOS qos, std::size_t retries = 0) override {
MQTTRequest request;
request.response_topic = topic;
request.qos = qos;
return get(request, retries);
}
void publish(const std::string& topic, const nlohmann::json& json) override {
m_published.emplace_back(topic, json);
}
void publish(const std::string& topic, const nlohmann::json& json, QOS /*qos*/, bool /*retain*/ = false) override {
m_published.emplace_back(topic, json);
}
void publish(const std::string& topic, const std::string& data) override {
m_published.emplace_back(topic, nlohmann::json(data));
}
void publish(const std::string& topic, const std::string& data, QOS /*qos*/, bool /*retain*/ = false) override {
m_published.emplace_back(topic, nlohmann::json(data));
}
void register_handler(const std::string& topic, std::shared_ptr<TypedHandler> handler, QOS /*qos*/) override {
m_handlers[topic] = std::move(handler);
}
void unregister_handler(const std::string& topic, const Token& /*token*/) override {
m_handlers.erase(topic);
}
bool connect() override {
return true;
}
void disconnect() override {
}
void subscribe(const std::string& /*topic*/) override {
}
void subscribe(const std::string& /*topic*/, QOS /*qos*/) override {
}
void unsubscribe(const std::string& /*topic*/) override {
}
void clear_retained_topics() override {
}
std::shared_future<void> spawn_main_loop_thread() override {
return {};
}
std::shared_future<void> get_main_loop_future() override {
return {};
}
const std::string& get_everest_prefix() const override {
return m_everest_prefix;
}
const std::string& get_external_prefix() const override {
static const std::string empty;
return empty;
}
private:
std::string m_everest_prefix;
nlohmann::json m_get_response;
std::optional<MQTTRequest> m_last_get_request;
std::vector<std::pair<std::string, nlohmann::json>> m_published;
std::unordered_map<std::string, std::shared_ptr<TypedHandler>> m_handlers;
};
} // namespace tests
} // namespace Everest

View File

@@ -0,0 +1,388 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <catch2/catch_all.hpp>
#include <framework/runtime.hpp>
#include <tests/helpers.hpp>
#include <utils/config.hpp>
namespace fs = std::filesystem;
SCENARIO("Check ManagerSettings Constructor", "[!throws]") {
auto bin_dir = Everest::tests::get_bin_dir().string() + "/";
GIVEN("An invalid prefix, but a valid config file") {
THEN("It should throw BootException") {
CHECK_THROWS_AS(
Everest::ManagerSettings(bin_dir + "non-valid-prefix/", bin_dir + "valid_config/config.yaml"),
Everest::BootException);
}
}
GIVEN("A valid prefix, but a non existing config file") {
THEN("It should throw BootException") {
CHECK_THROWS_AS(Everest::ManagerSettings(bin_dir + "valid_config/", bin_dir + "non-existing-config.yaml"),
Everest::BootException);
}
}
GIVEN("A valid prefix and a valid config file") {
THEN("It should not throw") {
CHECK_NOTHROW(Everest::ManagerSettings(bin_dir + "valid_config/", bin_dir + "valid_config/config.yaml"));
}
}
GIVEN("A valid prefix and a valid config file with a custom prefix") {
THEN("It should not throw") {
auto ms = Everest::ManagerSettings(bin_dir + "valid_config_custom_prefix/usr",
bin_dir + "valid_config_custom_prefix/usr/config.yaml");
CHECK(ms.runtime_settings.etc_dir == bin_dir + "valid_config_custom_prefix/etc/everest");
}
}
GIVEN("A broken yaml file") {
// FIXME (aw): this also throws, if the folder doesn't even exists or some other things fail
THEN("It should throw") {
CHECK_THROWS(Everest::ManagerSettings(bin_dir + "broken_yaml/", bin_dir + "broken_yaml/config.yaml"));
}
}
GIVEN("A empty yaml file") {
THEN("It shouldn't throw") {
CHECK_NOTHROW(Everest::ManagerSettings(bin_dir + "empty_yaml/", bin_dir + "empty_yaml/config.yaml"));
}
}
GIVEN("A empty yaml object file") {
THEN("It shouldn't throw") {
CHECK_NOTHROW(
Everest::ManagerSettings(bin_dir + "empty_yaml_object/", bin_dir + "empty_yaml_object/config.yaml"));
}
}
GIVEN("A null yaml file") {
THEN("It shouldn't throw") {
CHECK_NOTHROW(Everest::ManagerSettings(bin_dir + "null_yaml/", bin_dir + "null_yaml/config.yaml"));
}
}
GIVEN("A string yaml file") {
THEN("It should throw Everest::EverestConfigError") {
CHECK_THROWS_AS(Everest::ManagerSettings(bin_dir + "string_yaml/", bin_dir + "string_yaml/config.yaml"),
Everest::BootException);
}
}
GIVEN("A non-exsiting database file with ConfigurationBootMode::DatabaseInit") {
THEN("It should not throw and create the file") {
CHECK_NOTHROW(Everest::ManagerSettings(bin_dir + "valid_config/", bin_dir + "valid_config/config.yaml",
"valid_config/non_existing.db"));
}
}
}
SCENARIO("Check ManagerConfig Constructor", "[!throws]") {
auto bin_dir = Everest::tests::get_bin_dir().string() + "/";
GIVEN("A config without modules") {
auto ms = Everest::ManagerSettings(bin_dir + "empty_config/", bin_dir + "empty_config/config.yaml");
auto config = Everest::ManagerConfig(ms);
THEN("It should not contain the module some_module") {
CHECK(!config.contains("some_module"));
}
}
GIVEN("A config file referencing a non existent module") {
auto ms = Everest::ManagerSettings(bin_dir + "missing_module/", bin_dir + "missing_module/config.yaml");
THEN("It should throw Everest::EverestConfigError") {
CHECK_THROWS_AS(Everest::ManagerConfig(ms), Everest::EverestConfigError);
}
}
GIVEN("A config file using a module with broken manifest (missing meta data)") {
auto ms = Everest::ManagerSettings(bin_dir + "broken_manifest_1/", bin_dir + "broken_manifest_1/config.yaml");
THEN("It should throw Everest::EverestConfigError") {
CHECK_THROWS_AS(Everest::ManagerConfig(ms), Everest::EverestConfigError);
}
}
GIVEN("A config file using a module with broken manifest (empty file)") {
auto ms = Everest::ManagerSettings(bin_dir + "broken_manifest_2/", bin_dir + "broken_manifest_2/config.yaml");
THEN("It should throw Everest::EverestConfigError") {
// FIXME: an empty manifest breaks the test?
CHECK_THROWS_AS(Everest::ManagerConfig(ms), Everest::EverestConfigError);
}
}
GIVEN("A config file using a module with broken manifest (broken module config)") {
auto ms = Everest::ManagerSettings(bin_dir + "broken_manifest_3/", bin_dir + "broken_manifest_3/config.yaml");
THEN("It should throw Everest::EverestConfigError") {
CHECK_THROWS_AS(Everest::ManagerConfig(ms), Everest::EverestConfigError);
}
}
GIVEN("A config file using a module with broken manifest (broken implementation config)") {
auto ms = Everest::ManagerSettings(bin_dir + "broken_manifest_4/", bin_dir + "broken_manifest_4/config.yaml");
THEN("It should throw Everest::EverestConfigError") {
CHECK_THROWS_AS(Everest::ManagerConfig(ms), Everest::EverestConfigError);
}
}
GIVEN("A config file with an unknown implementation config") {
auto ms = Everest::ManagerSettings(bin_dir + "unknown_impls/", bin_dir + "unknown_impls/config.yaml");
THEN("It should throw Everest::EverestConfigError") {
CHECK_THROWS_AS(Everest::ManagerConfig(ms), Everest::EverestConfigError);
}
}
GIVEN("A config file with an missing config entry") {
auto ms =
Everest::ManagerSettings(bin_dir + "missing_config_entry/", bin_dir + "missing_config_entry/config.yaml");
THEN("It should throw Everest::EverestConfigError") {
CHECK_THROWS_AS(Everest::ManagerConfig(ms), Everest::EverestConfigError);
}
}
GIVEN("A config file with an missing implementation config entry") {
auto ms = Everest::ManagerSettings(bin_dir + "missing_impl_config_entry/",
bin_dir + "missing_impl_config_entry/config.yaml");
THEN("It should throw Everest::EverestConfigError") {
CHECK_THROWS_AS(Everest::ManagerConfig(ms), Everest::EverestConfigError);
}
}
GIVEN("A config file with an invalid type of an implementation config entry") {
auto ms = Everest::ManagerSettings(bin_dir + "invalid_config_entry_type/",
bin_dir + "invalid_config_entry_type/config.yaml");
THEN("It should throw Everest::EverestConfigError") {
CHECK_THROWS_AS(Everest::ManagerConfig(ms), Everest::EverestConfigError);
}
}
GIVEN("A config file using a module with an invalid interface (missing "
"interface)") {
auto ms = Everest::ManagerSettings(bin_dir + "missing_interface/", bin_dir + "missing_interface/config.yaml");
THEN("It should throw Everest::EverestConfigError") {
CHECK_THROWS_AS(Everest::ManagerConfig(ms), Everest::EverestConfigError);
}
}
GIVEN("A valid config") {
auto ms = Everest::ManagerSettings(bin_dir + "valid_config/", bin_dir + "valid_config/config.yaml");
THEN("It should not throw at all") {
CHECK_NOTHROW(Everest::ManagerConfig(ms));
}
}
GIVEN("A valid config with a valid module") {
auto ms =
Everest::ManagerSettings(bin_dir + "valid_module_config/", bin_dir + "valid_module_config/config.yaml");
THEN("It should not throw at all") {
CHECK_NOTHROW(Everest::ManagerConfig(ms));
}
}
GIVEN("A valid config with a valid module and a user-config applied") {
auto ms = Everest::ManagerSettings(bin_dir + "valid_module_config_userconfig/",
bin_dir + "valid_module_config_userconfig/config.yaml");
THEN("It should not throw at all") {
CHECK_NOTHROW([&]() {
auto mc = Everest::ManagerConfig(ms);
auto module_configs = mc.get_module_configurations();
bool found = false;
const auto config_params = module_configs.at("valid_module").configuration_parameters;
for (const auto& param : config_params.at("!module")) {
if (param.name == "valid_config_entry") {
found = true;
CHECK(std::get<std::string>(param.value) == "hi");
}
}
if (!found) {
FAIL("Expected configuration parameter 'valid_config_entry' not found.");
}
}());
}
}
GIVEN("A valid config with a valid module and enabled schema validation") {
auto ms = Everest::ManagerSettings(bin_dir + "valid_module_config_validate/",
bin_dir + "valid_module_config_validate/config.yaml");
THEN("It should not throw at all") {
CHECK_NOTHROW([&]() {
auto mc = Everest::ManagerConfig(ms);
auto interfaces = mc.get_interfaces();
CHECK(interfaces.size() == 1);
CHECK(interfaces.contains("TESTValidManifestCmdVar"));
CHECK(interfaces.at("TESTValidManifestCmdVar").at("main") == "test_interface_cmd_var");
auto types = mc.get_types();
CHECK(types.size() == 1);
CHECK(types.contains("/test_type"));
}());
}
}
GIVEN("A valid config in legacy json format with a valid module") {
auto ms = Everest::ManagerSettings(bin_dir + "valid_module_config_json/",
bin_dir + "valid_module_config_json/config.json");
THEN("It should not throw at all") {
CHECK_NOTHROW(Everest::ManagerConfig(ms));
}
}
GIVEN("A config file that does not exist") {
THEN("It should throw Everest::BootException") {
CHECK_THROWS_AS(Everest::ManagerSettings(bin_dir + "valid_module_config_json/",
bin_dir + "valid_module_config_json/config.yaml"),
Everest::BootException);
}
}
GIVEN("A valid config in legacy json format with multiple connected valid modules") {
auto ms =
Everest::ManagerSettings(bin_dir + "valid_complete_config/", bin_dir + "valid_complete_config/config.json");
THEN("It should not throw at all") {
CHECK_NOTHROW(Everest::ManagerConfig(ms));
}
}
GIVEN("ManagerSettings are instantiated two times - first with fallback to init from config file, second with "
"database") {
auto db_path = bin_dir + "valid_config/everest.db";
// Clean up before test
if (fs::exists(db_path)) {
fs::remove(db_path);
}
auto ms = Everest::ManagerSettings(bin_dir + "valid_config/", bin_dir + "valid_config/config.yaml", db_path);
CHECK(ms.storage->contains_valid_config() == false);
THEN("In the first intstantiation the database is not initialized") {
CHECK_NOTHROW(Everest::ManagerConfig(ms));
THEN("In the second instantiation the database is initialized and valid") {
ms = Everest::ManagerSettings(bin_dir + "valid_config/", bin_dir + "valid_config/config.yaml", db_path);
CHECK(ms.storage->contains_valid_config() == true);
CHECK_NOTHROW(Everest::ManagerConfig(ms));
}
THEN("It should be possible to construct the ManagerSettings with a database path") {
CHECK_NOTHROW(Everest::ManagerSettings(bin_dir + "valid_config/", db_path, Everest::DatabaseTag{}));
}
}
}
}
SCENARIO("Check everest config parsing", "[!throws]") {
auto bin_dir = Everest::tests::get_bin_dir().string() + "/";
auto valid_complete_config_json = bin_dir + "valid_complete_config/config.json";
GIVEN("A complete and valid config") {
auto config = Everest::load_yaml(valid_complete_config_json);
THEN("It should not throw") {
CHECK_NOTHROW(everest::config::parse_module_configs(config.value("active_modules", json::object())));
}
}
GIVEN("A valid config that misses module connections") {
auto config = Everest::load_yaml(valid_complete_config_json);
config["active_modules"]["valid_module_requires"].erase("connections");
THEN("It should not throw") {
CHECK_NOTHROW(everest::config::parse_module_configs(config.value("active_modules", json::object())));
}
}
GIVEN("A valid config that misses a mapping") {
auto config = Everest::load_yaml(valid_complete_config_json);
config["active_modules"]["valid_module"].erase("mapping");
THEN("It should not throw") {
CHECK_NOTHROW(everest::config::parse_module_configs(config.value("active_modules", json::object())));
}
}
GIVEN("A config where a module is missing the 'module' field") {
auto config = Everest::load_yaml(valid_complete_config_json);
config["active_modules"]["valid_module"].erase("module");
THEN("It should throw ConfigParseException for missing 'module'") {
CHECK_THROWS_AS(everest::config::parse_module_configs(config.value("active_modules", json::object())),
ConfigParseException);
}
}
GIVEN("A config with only 'active_modules' and no 'settings'") {
json config;
config["active_modules"] = {{"valid_module", {{"module", "TESTValidManifest"}}}};
THEN("It should not throw and parse default settings") {
CHECK_NOTHROW(everest::config::parse_module_configs(config.value("active_modules", json::object())));
}
}
GIVEN("A config with empty 'active_modules'") {
json config;
config["active_modules"] = json::object(); // empty object
THEN("It should not throw and result in no modules") {
auto result = everest::config::parse_module_configs(config.value("active_modules", json::object()));
CHECK(result.empty());
}
}
GIVEN("A config with unsupported JSON type in configuration parameter") {
json config;
config["active_modules"] = {
{"test_module", {{"module", "test"}, {"config_module", {{"param1", json::array({1, 2, 3})}}}}}};
THEN("It should throw due to unsupported config parameter type") {
CHECK_THROWS(everest::config::parse_module_configs(config.value("active_modules", json::object())));
}
}
}
json complete_serialized_mod_config(json& serialized_mod_config, Everest::ManagerConfig& mc) {
serialized_mod_config["interface_definitions"] = mc.get_interface_definitions();
serialized_mod_config["types"] = mc.get_types();
serialized_mod_config["module_provides"] = mc.get_interfaces();
serialized_mod_config["settings"] = mc.get_settings();
serialized_mod_config["schemas"] = mc.get_schemas();
serialized_mod_config["module_names"] = mc.get_module_names();
serialized_mod_config["manifests"] = mc.get_manifests();
serialized_mod_config["error_map"] = mc.get_error_types();
return serialized_mod_config;
}
SCENARIO("Check config constructor and functions", "[!throws]") {
auto bin_dir = Everest::tests::get_bin_dir().string() + "/";
auto ms = Everest::ManagerSettings(bin_dir + "two_module_test/", bin_dir + "two_module_test/config.yaml");
GIVEN("A config with two connected modules") {
THEN("It should not throw") {
CHECK_NOTHROW([&]() {
auto mc = Everest::ManagerConfig(ms);
auto serialized_mod_config =
Everest::get_serialized_module_config("module_a", mc.get_module_configurations());
complete_serialized_mod_config(serialized_mod_config, mc);
Everest::MQTTSettings mqtt_settings;
const auto config = Everest::Config(mqtt_settings, serialized_mod_config);
config.get_requirement_initialization("module_a");
}());
}
}
}
SCENARIO("Config constructor throws on missing required fields in serialized config", "[Config][throws]") {
GIVEN("A serialized config missing required fields") {
Everest::MQTTSettings mqtt_settings;
Everest::json serialized_config = Everest::json::object();
serialized_config["module_config"] = Everest::json::object();
serialized_config["module_config"]["module_a"] = Everest::json::object();
THEN("It should throw an exception") {
CHECK_THROWS_AS(Everest::Config(mqtt_settings, serialized_config), json::exception);
}
}
}
SCENARIO("Config returns correct module info", "[Config]") {
auto bin_dir = Everest::tests::get_bin_dir().string() + "/";
auto ms = Everest::ManagerSettings(bin_dir + "two_module_test/", bin_dir + "two_module_test/config.yaml");
auto mc = Everest::ManagerConfig(ms);
auto serialized = Everest::get_serialized_module_config("module_a", mc.get_module_configurations());
complete_serialized_mod_config(serialized, mc);
Everest::MQTTSettings mqtt_settings;
GIVEN("A valid serialized config") {
Everest::Config config(mqtt_settings, serialized);
WHEN("Calling get_module_info") {
auto info = config.get_module_info("module_a");
THEN("It should return the correct name and license") {
CHECK(info.id == "module_a");
CHECK(info.name == "TESTModuleA");
CHECK(info.license == "https://opensource.org/licenses/Apache-2.0");
CHECK(info.authors.at(0) == "author@example.com");
}
}
}
}
SCENARIO("Config returns parsed module configs", "[Config]") {
auto bin_dir = Everest::tests::get_bin_dir().string() + "/";
auto ms = Everest::ManagerSettings(bin_dir + "two_module_test/", bin_dir + "two_module_test/config.yaml");
auto mc = Everest::ManagerConfig(ms);
auto serialized = Everest::get_serialized_module_config("module_a", mc.get_module_configurations());
complete_serialized_mod_config(serialized, mc);
Everest::MQTTSettings mqtt_settings;
Everest::Config config(mqtt_settings, serialized);
GIVEN("A valid config for module_a") {
auto configs = config.get_module_configs("module_a");
THEN("It should contain the correct config values") {
CHECK(configs.find("main") != configs.end());
CHECK(configs.find("!module") != configs.end());
CHECK(std::get<std::string>(configs["!module"]["valid_module_config_entry"]) == "test");
CHECK(std::get<int>(configs["main"]["valid_impl_config_entry"]) == 42);
}
}
}

View File

@@ -0,0 +1,224 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <catch2/catch_all.hpp>
#include <everest/database/exceptions.hpp>
#include <everest/database/sqlite/connection.hpp>
#include <tests/helpers.hpp>
#include <utils/config/settings.hpp>
#include <utils/config/storage_sqlite.hpp>
#include <utils/yaml_loader.hpp>
using namespace everest::config;
Everest::ManagerSettings get_example_settings() {
auto bin_dir = Everest::tests::get_bin_dir().string() + "/";
return Everest::ManagerSettings(bin_dir + "valid_config/", bin_dir + "valid_config/config.yaml");
}
ModuleConfigurations get_example_module_configs() {
ModuleConfigurations module_configs;
ModuleConfig module_config;
module_config.module_name = "example_module";
module_config.standalone = true;
module_config.capabilities = {"capability1,capability2"};
module_config.telemetry_enabled = true;
module_config.telemetry_config = TelemetryConfig(1);
Fulfillment fulfillment;
fulfillment.module_id = "module_id1";
fulfillment.implementation_id = "implementation_id1";
fulfillment.requirement = {"requirement_id1", 0};
module_config.connections.insert({"connection1", {fulfillment}});
Mapping module_mapping = {1};
Mapping impl_mapping = {1, 1};
module_config.mapping.module = module_mapping;
module_config.mapping.implementations.insert({"implementation_id1", impl_mapping});
ConfigurationParameterCharacteristics characteristics1;
characteristics1.datatype = Datatype::Integer;
characteristics1.mutability = Mutability::ReadWrite;
characteristics1.unit = "ms";
ConfigurationParameterCharacteristics characteristics2;
characteristics2.datatype = Datatype::String;
characteristics2.mutability = Mutability::ReadOnly;
ConfigurationParameterCharacteristics characteristics4;
characteristics4.datatype = Datatype::Decimal;
characteristics4.mutability = Mutability::ReadWrite;
ConfigurationParameterCharacteristics characteristics5;
characteristics5.datatype = Datatype::Boolean;
characteristics5.mutability = Mutability::ReadWrite;
ConfigurationParameter param1;
param1.name = "integer_param";
param1.value = 10;
param1.characteristics = characteristics1;
ConfigurationParameter param2;
param2.name = "string_param";
param2.value = std::string("example_value");
param2.characteristics = characteristics2;
ConfigurationParameter param4;
param4.name = "decimal_param";
param4.value = 42.23;
param4.characteristics = characteristics4;
ConfigurationParameter param5;
param5.name = "boolean_param";
param5.value = true;
param5.characteristics = characteristics5;
module_config.configuration_parameters["!module"].push_back({param1});
module_config.configuration_parameters["implementation_id1"].push_back({param2});
module_config.configuration_parameters["!module"].push_back({param4});
module_config.configuration_parameters["!module"].push_back({param5});
module_configs["example_module"] = module_config;
ModuleConfig module_config2;
module_config2.module_name = "Module1";
module_config2.standalone = false;
module_config2.telemetry_enabled = false;
module_configs["module1"] = module_config2;
return module_configs;
}
SCENARIO("Database initialization", "[db_initialization]") {
const auto bin_dir = Everest::tests::get_bin_dir().string() + "/";
const auto migrations_dir = bin_dir + "migrations";
GIVEN("A valid migration path") {
THEN("It should not throw") {
CHECK_NOTHROW(SqliteStorage("file::memory:?cache=shared", migrations_dir));
}
}
GIVEN("An invalid migration path") {
THEN("It should throw") {
CHECK_THROWS_AS(SqliteStorage("file::memory:?cache=shared", "invalid_migrations"),
everest::db::MigrationException);
}
}
}
TEST_CASE("Database operations", "[db_operation]") {
auto bin_dir = Everest::tests::get_bin_dir().string() + "/";
const auto migrations_dir = bin_dir + "migrations";
everest::db::sqlite::Connection c("file::memory:?cache=shared");
c.open_connection(); // keep at least one connection to keep the in-memory database alive
SqliteStorage storage("file::memory:?cache=shared", migrations_dir);
SECTION("Empty settings can not be retrieved") {
auto response = storage.get_settings();
REQUIRE(response.status == GenericResponseStatus::Failed);
}
SECTION("Empty module config can be retrieved") {
auto response = storage.get_module_configs();
REQUIRE(response.status == GenericResponseStatus::OK);
REQUIRE(response.module_configs.size() == 0);
}
const auto module_configs = get_example_module_configs();
const auto settings = get_example_settings();
// valid config and settings can be successfully written
REQUIRE(storage.write_module_configs(module_configs) == GenericResponseStatus::OK);
REQUIRE(storage.write_settings(settings) == GenericResponseStatus::OK);
SECTION("Module configurations can be written and correctly retrieved") {
auto response = storage.get_module_configs();
REQUIRE(response.status == GenericResponseStatus::OK);
REQUIRE(response.module_configs.size() == 2);
}
SECTION("Configuration parameters can be retrieved") {
auto response1 = storage.get_configuration_parameter({"example_module", "integer_param"});
REQUIRE(response1.status == GetSetResponseStatus::OK);
REQUIRE(response1.configuration_parameter.has_value());
REQUIRE(std::get<int>(response1.configuration_parameter.value().value) == 10);
auto response2 = storage.get_configuration_parameter({"example_module", "string_param", "implementation_id1"});
REQUIRE(response2.status == GetSetResponseStatus::OK);
REQUIRE(response2.configuration_parameter.has_value());
REQUIRE(std::get<std::string>(response2.configuration_parameter.value().value) == "example_value");
auto response4 = storage.get_configuration_parameter({"example_module", "decimal_param"});
REQUIRE(response4.status == GetSetResponseStatus::OK);
REQUIRE(response4.configuration_parameter.has_value());
REQUIRE(std::get<double>(response4.configuration_parameter.value().value) == 42.23);
auto response5 = storage.get_configuration_parameter({"example_module", "boolean_param"});
REQUIRE(response5.status == GetSetResponseStatus::OK);
REQUIRE(response5.configuration_parameter.has_value());
REQUIRE(std::get<bool>(response5.configuration_parameter.value().value) == true);
}
SECTION("Unknown configuration can not be found") {
auto response =
storage.get_configuration_parameter({"module_that_does_not_exist", "param_that_does_not_exist"});
REQUIRE(response.status == GetSetResponseStatus::NotFound);
}
SECTION("Configuration parameters can be updated") {
auto response = storage.update_configuration_parameter({"example_module", "integer_param"}, "20");
REQUIRE(response == GetSetResponseStatus::OK);
}
SECTION("Unknown configuration can not be updated") {
auto response =
storage.update_configuration_parameter({"module_that_does_not_exist", "param_that_does_not_exist"}, "20");
REQUIRE(response == GetSetResponseStatus::NotFound);
}
SECTION("Settings can be retrieved") {
auto response = storage.get_settings();
REQUIRE(response.status == GenericResponseStatus::OK);
REQUIRE(response.settings.has_value());
}
SECTION("Unknown module config can not be retrieved") {
auto response = storage.get_module_config("unknown_module_id");
REQUIRE(response.status == GenericResponseStatus::Failed);
}
SECTION("Configuration parameter for unknown configuration parameter identifier can not be retrieved") {
ConfigurationParameterIdentifier id;
id.module_id = "unknown_module_id";
auto response = storage.get_configuration_parameter(id);
REQUIRE(response.status == GetSetResponseStatus::NotFound);
}
SECTION("Configuration parameter for unknown configuration parameter identifier can not be written") {
ConfigurationParameterIdentifier id;
id.module_id = "unknown_module_id";
ConfigurationParameterCharacteristics characteristics;
characteristics.datatype = Datatype::String;
characteristics.mutability = Mutability::ReadWrite;
auto response = storage.write_configuration_parameter(id, characteristics, "value");
REQUIRE(response == GetSetResponseStatus::NotFound);
}
SECTION("Configuration parameter for wrong type can not be retrieved") {
ConfigurationParameterIdentifier id;
id.module_id = "example_module";
id.configuration_parameter_name = "integer_param";
id.module_implementation_id = "!module";
ConfigurationParameterCharacteristics characteristics;
characteristics.datatype = Datatype::Integer;
characteristics.mutability = Mutability::ReadWrite;
auto write_response = storage.write_configuration_parameter(id, characteristics, "value");
REQUIRE(write_response == GetSetResponseStatus::OK);
auto get_response = storage.get_configuration_parameter(id);
REQUIRE(get_response.status == GetSetResponseStatus::Failed);
}
SECTION("Config is not valid if not marked as valid") {
REQUIRE(storage.contains_valid_config() == false);
storage.mark_valid(false, "Test", std::nullopt);
REQUIRE(storage.contains_valid_config() == false);
}
SECTION("Config is valid if marked as valid") {
storage.mark_valid(true, "Test", "Test");
REQUIRE(storage.contains_valid_config() == true);
}
SECTION("Config can be wiped from the database") {
REQUIRE(storage.wipe() == GenericResponseStatus::OK);
}
}

View File

@@ -0,0 +1,11 @@
active_modules:
test_missing:
module: "TESTBrokenManifest"
settings:
interfaces_dir: "interfaces"
modules_dir: "modules"
types_dir: "types"
errors_dir: "errors"
schemas_dir: "schemas"
www_dir: "www"
logging_config_file: "logging.ini"

View File

@@ -0,0 +1,11 @@
active_modules:
test_missing:
module: "TESTBrokenManifest2"
settings:
interfaces_dir: "interfaces"
modules_dir: "modules"
types_dir: "types"
errors_dir: "errors"
schemas_dir: "schemas"
www_dir: "www"
logging_config_file: "logging.ini"

View File

@@ -0,0 +1,11 @@
active_modules:
test_missing:
module: "TESTBrokenManifest3"
settings:
interfaces_dir: "interfaces"
modules_dir: "modules"
types_dir: "types"
errors_dir: "errors"
schemas_dir: "schemas"
www_dir: "www"
logging_config_file: "logging.ini"

View File

@@ -0,0 +1,11 @@
active_modules:
test_missing:
module: "TESTBrokenManifest4"
settings:
interfaces_dir: "interfaces"
modules_dir: "modules"
types_dir: "types"
errors_dir: "errors"
schemas_dir: "schemas"
www_dir: "www"
logging_config_file: "logging.ini"

View File

@@ -0,0 +1,9 @@
active_modules: {}
settings::::
interfaces_dir: "interfaces"
modules_dir: "modules"
types_dir: "types"
errors_dir: "errors"
schemas_dir: "schemas"
www_dir: "www"
logging_config_file: "logging.ini"

View File

@@ -0,0 +1,9 @@
active_modules: {}
settings:
interfaces_dir: "interfaces"
modules_dir: "modules"
types_dir: "types"
errors_dir: "errors"
schemas_dir: "schemas"
www_dir: "www"
logging_config_file: "logging.ini"

View File

@@ -0,0 +1,17 @@
active_modules:
valid_module:
module: TESTValidManifest
config_module:
valid_config_entry: "hello there"
config_implementation:
main:
valid_config_entry: 42 # wrong type
settings:
interfaces_dir: "interfaces"
modules_dir: "modules"
types_dir: "types"
errors_dir: "errors"
schemas_dir: "schemas"
www_dir: "www"
logging_config_file: "logging.ini"

View File

@@ -0,0 +1,14 @@
active_modules:
valid_module:
module: TESTValidManifest
config_implementation:
main:
valid_config_entry: "hello there"
settings:
interfaces_dir: "interfaces"
modules_dir: "modules"
types_dir: "types"
errors_dir: "errors"
schemas_dir: "schemas"
www_dir: "www"
logging_config_file: "logging.ini"

View File

@@ -0,0 +1,15 @@
active_modules:
valid_module:
module: TESTValidManifest
config_module:
valid_config_entry: "hello there"
settings:
interfaces_dir: "interfaces"
modules_dir: "modules"
types_dir: "types"
errors_dir: "errors"
schemas_dir: "schemas"
www_dir: "www"
logging_config_file: "logging.ini"

View File

@@ -0,0 +1,11 @@
active_modules:
missing_interface:
module: "TESTMissingInterface"
settings:
interfaces_dir: "interfaces"
modules_dir: "modules"
types_dir: "types"
errors_dir: "errors"
schemas_dir: "schemas"
www_dir: "www"
logging_config_file: "logging.ini"

View File

@@ -0,0 +1,11 @@
active_modules:
test_missing:
module: "TESTMissingModule"
settings:
interfaces_dir: "interfaces"
modules_dir: "modules"
types_dir: "types"
errors_dir: "errors"
schemas_dir: "schemas"
www_dir: "www"
logging_config_file: "logging.ini"

View File

@@ -0,0 +1 @@
"This is a string!"

View File

@@ -0,0 +1,23 @@
active_modules:
module_a:
module: TESTModuleA
config_module:
valid_module_config_entry: "test"
config_implementation:
main:
valid_impl_config_entry: 42
connections:
req1:
- module_id: module_b
implementation_id: impl1
module_b:
module: TESTModuleB
config_module: {}
settings:
interfaces_dir: "interfaces"
modules_dir: "modules"
types_dir: "types"
errors_dir: "errors"
schemas_dir: "schemas"
www_dir: "www"
logging_config_file: "logging.ini"

View File

@@ -0,0 +1,19 @@
active_modules:
valid_module:
module: TESTValidManifest
config_module:
valid_config_entry: "hello there"
config_implementation:
main:
valid_config_entry: "hello there"
this_does_not_exist:
unknown: "hi"
settings:
interfaces_dir: "interfaces"
modules_dir: "modules"
types_dir: "types"
errors_dir: "errors"
schemas_dir: "schemas"
www_dir: "www"
logging_config_file: "logging.ini"

View File

@@ -0,0 +1,63 @@
{
"settings": {
"interfaces_dir": "interfaces",
"modules_dir": "modules",
"types_dir": "types",
"errors_dir": "errors",
"schemas_dir": "schemas",
"www_dir": "www",
"logging_config_file": "logging.ini"
},
"active_modules": {
"valid_module": {
"mapping": {
"module": {
"evse": 1
},
"implementations": {
"main": {
"evse": 1,
"connector": 1
}
}
},
"module": "TESTValidManifest",
"config_module": {
"valid_config_entry": "hello there"
},
"config_implementation": {
"main": {
"valid_config_entry": "hello there"
}
}
},
"valid_module_cmd_var": {
"module": "TESTValidManifestCmdVar",
"config_module": {
"valid_config_entry": "hello there"
},
"config_implementation": {
"main": {
"valid_config_entry": "hello there"
}
}
},
"valid_module_requires": {
"module": "TESTValidManifestRequires",
"mapping": {
"module": {
"evse": 2
}
},
"connections": {
"test_cmd_var": [
{
"module_id": "valid_module_cmd_var",
"implementation_id": "main"
}
]
}
}
},
"x-module-layout": {}
}

View File

@@ -0,0 +1,9 @@
active_modules: {}
settings:
interfaces_dir: "interfaces"
modules_dir: "modules"
types_dir: "types"
errors_dir: "errors"
schemas_dir: "schemas"
www_dir: "www"
logging_config_file: "logging.ini"

View File

@@ -0,0 +1,9 @@
active_modules: {}
settings:
interfaces_dir: "interfaces"
modules_dir: "modules"
types_dir: "types"
errors_dir: "errors"
schemas_dir: "schemas"
www_dir: "www"
logging_config_file: "logging.ini"

View File

@@ -0,0 +1,22 @@
active_modules:
valid_module:
module: TESTValidManifest
capabilities:
- "capability1"
- "capability2"
config_module:
valid_config_entry: "hello there"
unknown_config_entry: 42 # this just logs an error nowadays
config_implementation:
main:
valid_config_entry: "hello there"
unknown_config_entry: 42 # this just logs an error nowadays
settings:
interfaces_dir: "interfaces"
modules_dir: "modules"
types_dir: "types"
errors_dir: "errors"
schemas_dir: "schemas"
www_dir: "www"
logging_config_file: "logging.ini"

View File

@@ -0,0 +1,26 @@
{
"active_modules": {
"valid_module": {
"module": "TESTValidManifest",
"config_module": {
"valid_config_entry": "hello there",
"unknown_config_entry": 42
},
"config_implementation": {
"main": {
"valid_config_entry": "hello there",
"unknown_config_entry": 42
}
}
}
},
"settings": {
"interfaces_dir": "interfaces",
"modules_dir": "modules",
"types_dir": "types",
"errors_dir": "errors",
"schemas_dir": "schemas",
"www_dir": "www",
"logging_config_file": "logging.ini"
}
}

View File

@@ -0,0 +1,5 @@
active_modules:
valid_module:
module: TESTValidManifest
config_module:
valid_config_entry: "hi"

View File

@@ -0,0 +1,20 @@
active_modules:
valid_module:
module: TESTValidManifestCmdVar
config_module:
valid_config_entry: "hello there"
unknown_config_entry: 42 # this just logs an error nowadays
config_implementation:
main:
valid_config_entry: "hello there"
unknown_config_entry: 42 # this just logs an error nowadays
settings:
validate_schema: true
interfaces_dir: "interfaces"
modules_dir: "modules"
types_dir: "types"
errors_dir: "errors"
schemas_dir: "schemas"
www_dir: "www"
logging_config_file: "logging.ini"

View File

@@ -0,0 +1,75 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <catch2/catch_all.hpp>
#include <chrono>
#include <iostream>
#include <utils/conversions.hpp>
#include <utils/date.hpp>
#include <utils/types.hpp>
SCENARIO("Check conversions", "[!throws]") {
GIVEN("Valid CmdErrors") {
THEN("It shouldn't throw") {
CHECK(Everest::conversions::cmd_error_type_to_string(Everest::CmdErrorType::MessageParsingError) ==
"MessageParsingError");
CHECK(Everest::conversions::cmd_error_type_to_string(Everest::CmdErrorType::SchemaValidationError) ==
"SchemaValidationError");
CHECK(Everest::conversions::cmd_error_type_to_string(Everest::CmdErrorType::HandlerException) ==
"HandlerException");
CHECK(Everest::conversions::cmd_error_type_to_string(Everest::CmdErrorType::CmdTimeout) == "CmdTimeout");
CHECK(Everest::conversions::cmd_error_type_to_string(Everest::CmdErrorType::Shutdown) == "Shutdown");
CHECK(Everest::conversions::cmd_error_type_to_string(Everest::CmdErrorType::NotReady) == "NotReady");
}
}
GIVEN("Invalid CmdErrors") {
THEN("It should throw") {
CHECK_THROWS(Everest::conversions::cmd_error_type_to_string(static_cast<Everest::CmdErrorType>(-1)));
}
}
GIVEN("Valid CmdError strings") {
THEN("It shouldn't throw") {
CHECK(Everest::conversions::string_to_cmd_error_type("MessageParsingError") ==
Everest::CmdErrorType::MessageParsingError);
CHECK(Everest::conversions::string_to_cmd_error_type("SchemaValidationError") ==
Everest::CmdErrorType::SchemaValidationError);
CHECK(Everest::conversions::string_to_cmd_error_type("HandlerException") ==
Everest::CmdErrorType::HandlerException);
CHECK(Everest::conversions::string_to_cmd_error_type("CmdTimeout") == Everest::CmdErrorType::CmdTimeout);
CHECK(Everest::conversions::string_to_cmd_error_type("Shutdown") == Everest::CmdErrorType::Shutdown);
CHECK(Everest::conversions::string_to_cmd_error_type("NotReady") == Everest::CmdErrorType::NotReady);
}
}
GIVEN("Invalid CmdError strings") {
THEN("It should throw") {
CHECK_THROWS(Everest::conversions::string_to_cmd_error_type("ThisIsAnInvalidCmdErrorString"));
}
}
GIVEN("Valid CmdErrorError") {
THEN("It shouldn't throw") {
Everest::CmdResultError cmd_result_error = {Everest::CmdErrorType::Shutdown, "message", nullptr};
json cmd_result_error_json = {{Everest::conversions::ERROR_TYPE, "Shutdown"},
{Everest::conversions::ERROR_MSG, "message"}};
Everest::CmdResultError cmd_result_error_from_json = cmd_result_error_json;
CHECK(json(cmd_result_error) == cmd_result_error_json);
CHECK(json(cmd_result_error_from_json) == cmd_result_error_json);
}
}
GIVEN("Valid timestamp") {
THEN("It should parse") {
const auto now_utc = date::utc_clock::now();
const auto now_str = Everest::Date::to_rfc3339(now_utc);
const auto tp_from_slow = Everest::Date::from_rfc3339_slow(now_str);
const auto tp_from_fast = Everest::Date::from_rfc3339(now_str);
CHECK(tp_from_slow == tp_from_fast);
}
}
}

View File

@@ -0,0 +1,46 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <catch2/catch_all.hpp>
#include <tests/helpers.hpp>
#include <utils/exceptions.hpp>
#include <utils/filesystem.hpp>
namespace fs = std::filesystem;
SCENARIO("Check module config helper functions", "[!throws]") {
std::string bin_dir = Everest::tests::get_bin_dir().string() + "/";
auto dir = fs::path(bin_dir);
auto file = dir / "empty_yaml" / "config.yaml";
GIVEN("A file instead of a directory") {
THEN("It should throw") {
CHECK_THROWS_AS(Everest::assert_dir(file, "alias"), Everest::BootException);
}
}
GIVEN("A directory") {
THEN("It shouldn't throw") {
CHECK_NOTHROW(Everest::assert_dir(dir, "alias"));
}
}
GIVEN("A directory instead of a file") {
THEN("It should throw") {
CHECK_THROWS_AS(Everest::assert_file(dir, "alias"), Everest::BootException);
}
}
GIVEN("A file") {
THEN("It shouldn't throw") {
CHECK_NOTHROW(Everest::assert_file(file, "alias"));
}
}
GIVEN("A file with yaml extension and expecting yaml") {
THEN("It should have the expected extension") {
CHECK(Everest::has_extension(file.string(), ".yaml"));
}
}
GIVEN("A file with yaml extension and expecting json") {
THEN("It should not have the expected extension") {
CHECK_FALSE(Everest::has_extension(file.string(), ".json"));
}
}
}

View File

@@ -0,0 +1,29 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <catch2/catch_all.hpp>
#include <chrono>
#include <iostream>
#include "../lib/message_handler.cpp"
SCENARIO("Check static helper functions", "[!throws]") {
GIVEN("Valid wildcard topic") {
THEN("It should match") {
CHECK(Everest::check_topic_matches("same_topic", "same_topic"));
CHECK(not Everest::check_topic_matches("same_topic_not", "same_topic"));
CHECK(not Everest::check_topic_matches("same_topic", "same_topic_not"));
CHECK(Everest::check_topic_matches("full/topic/to/check", "full/#"));
CHECK(Everest::check_topic_matches("full/topic/to/check", "full/+/to/check"));
CHECK(Everest::check_topic_matches("full//to/check", "full/+/to/check"));
CHECK(not Everest::check_topic_matches("full/topic/to/check", "full/topic/to/check+"));
CHECK(not Everest::check_topic_matches("full/topic/to/check", "full/+/not/check"));
CHECK(Everest::check_topic_matches("full/topic/to/check", "full/+/+/check"));
CHECK(Everest::check_topic_matches("full/topic/to/check", "full/+/to/#"));
CHECK(Everest::check_topic_matches("full/topic/to/check", "+/+/+/+"));
// these are technically not allowed, but we treat the first # as meaning "nothing comes after this"
CHECK(Everest::check_topic_matches("full/topic/to/check", "full/#/to/check"));
CHECK(Everest::check_topic_matches("full/topic/to/check", "full/+/to/#/"));
}
}
}

View File

@@ -0,0 +1,2 @@
description: "This defines a minimal valid interface"

View File

@@ -0,0 +1,17 @@
description: "This defines a valid interface with cmds and vars"
cmds:
a_cmd:
description: A command
arguments:
value:
description: A value using the AnObject from test_type
type: object
$ref: /test_type#/AnObject
result:
description: True on success
type: boolean
vars:
a_var:
description: A variable
type: object
$ref: /test_type#/AnObject

View File

@@ -0,0 +1,18 @@
# for documentation on this file format see:
# https://www.boost.org/doc/libs/1_54_0/libs/log/doc/html/log/detailed/utilities.html#log.detailed.utilities.setup.filter_formatter
[Core]
DisableLogging=false
Filter="%Severity% >= DEBG"
[Sinks.Console]
Destination=Console
# Filter="%Target% contains \"MySink1\""
Format="%TimeStamp% [%Severity%] \033[1;32m%Process%\033[0m \033[1;36m%function%\033[0m \033[1;30m%file%:\033[0m\033[1;32m%line%\033[0m: %Message%"
Asynchronous=false
AutoFlush=true
SeverityStringColorDebug="\033[1;30m"
SeverityStringColorInfo="\033[1;37m"
SeverityStringColorWarning="\033[1;33m"
SeverityStringColorError="\033[1;31m"
SeverityStringColorCritical="\033[1;35m"

View File

@@ -0,0 +1,974 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <utils/message_handler.hpp>
#include <catch2/catch_test_macros.hpp>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <map>
#include <mutex>
#include <set>
#include <thread>
#include <vector>
using namespace Everest;
using namespace std::chrono_literals;
namespace {
// Helper class to track execution order and timing
class ExecutionTracker {
public:
struct Event {
std::string topic;
int sequence;
std::chrono::steady_clock::time_point timestamp;
};
void record(const std::string& topic, int sequence) {
std::lock_guard<std::mutex> lock(mutex_);
events_.push_back({topic, sequence, std::chrono::steady_clock::now()});
cv_.notify_all();
}
void wait_for_count(size_t count, std::chrono::milliseconds timeout = 5000ms) {
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait_for(lock, timeout, [this, count] { return events_.size() >= count; });
}
std::vector<Event> get_events() const {
std::lock_guard<std::mutex> lock(mutex_);
return events_;
}
size_t count() const {
std::lock_guard<std::mutex> lock(mutex_);
return events_.size();
}
void clear() {
std::lock_guard<std::mutex> lock(mutex_);
events_.clear();
}
private:
mutable std::mutex mutex_;
std::condition_variable cv_;
std::vector<Event> events_;
};
ParsedMessage create_message(const std::string& topic, const std::string& msg_type, const json& data = json{}) {
ParsedMessage msg;
msg.topic = topic;
msg.data = {{"msg_type", msg_type}, {"data", data}};
return msg;
}
ParsedMessage create_cmd_message(const std::string& topic, int sequence = 0) {
json data = {{"sequence", sequence}};
return create_message(topic, "Cmd", data);
}
ParsedMessage create_var_message(const std::string& topic, int sequence = 0) {
json data = {{"data", {{"sequence", sequence}}}};
ParsedMessage msg;
msg.topic = topic;
msg.data = {{"msg_type", "Var"}, {"data", data}};
return msg;
}
ParsedMessage create_external_mqtt_message(const std::string& topic, int value = 0) {
ParsedMessage msg;
msg.topic = topic;
msg.data = {{"value", value}}; // ExternalMQTT uses data directly, no msg_type wrapper
return msg;
}
ParsedMessage create_raise_error_message(const std::string& topic, int error_code = 0) {
json data = {{"error_code", error_code}};
ParsedMessage msg;
msg.topic = topic;
msg.data = {{"msg_type", "RaiseError"}, {"data", data}};
return msg;
}
ParsedMessage create_clear_error_message(const std::string& topic) {
ParsedMessage msg;
msg.topic = topic;
msg.data = {{"msg_type", "ClearError"}, {"data", json{}}};
return msg;
}
class MessageHandlerFixture {
public:
MessageHandlerFixture() : handler(std::make_unique<MessageHandler>()) {
}
~MessageHandlerFixture() {
if (handler) {
handler->stop();
}
}
MessageHandler* operator->() {
return handler.get();
}
MessageHandler& get() {
return *handler;
}
private:
std::unique_ptr<MessageHandler> handler;
};
} // namespace
// ============================================================================
// Test: Basic Message Processing
// ============================================================================
TEST_CASE("MessageHandler processes single message", "[message_handler][basic]") {
MessageHandlerFixture handler;
ExecutionTracker tracker;
auto handler_func = std::make_shared<Handler>(
[&tracker](const std::string& topic, const json& data) { tracker.record(topic, data.value("sequence", 0)); });
auto test_handler = std::make_shared<TypedHandler>(HandlerType::Call, handler_func);
handler->register_handler("test/topic", test_handler);
ParsedMessage msg = create_cmd_message("test/topic", 1);
handler->add(msg);
tracker.wait_for_count(1);
auto events = tracker.get_events();
REQUIRE(events.size() == 1);
CHECK(events[0].topic == "test/topic");
CHECK(events[0].sequence == 1);
}
// ============================================================================
// Test: Same Topic Ordering (Core Functionality)
// ============================================================================
TEST_CASE("MessageHandler processes same topic messages in order", "[message_handler][ordering]") {
MessageHandlerFixture handler;
ExecutionTracker tracker;
std::atomic<bool> first_message_processing{false};
std::atomic<bool> release_first_message{false};
auto handler_func = std::make_shared<Handler>([&](const std::string& topic, const json& data) {
int seq = data.value("sequence", 0);
if (seq == 1) {
first_message_processing = true;
// Block first message until we release it
while (!release_first_message) {
std::this_thread::sleep_for(10ms);
}
}
tracker.record(topic, seq);
});
auto test_handler = std::make_shared<TypedHandler>(HandlerType::Call, handler_func);
handler->register_handler("test/topic", test_handler);
// Send 3 messages to same topic
handler->add(create_cmd_message("test/topic", 1));
handler->add(create_cmd_message("test/topic", 2));
handler->add(create_cmd_message("test/topic", 3));
// Wait for first message to start processing
while (!first_message_processing) {
std::this_thread::sleep_for(10ms);
}
// Give some time for second/third messages to potentially be processed (they shouldn't be)
std::this_thread::sleep_for(100ms);
CHECK(tracker.count() == 0); // First message still blocked
// Release first message
release_first_message = true;
// Wait for all messages
tracker.wait_for_count(3);
auto events = tracker.get_events();
REQUIRE(events.size() == 3);
// Verify order
CHECK(events[0].sequence == 1);
CHECK(events[1].sequence == 2);
CHECK(events[2].sequence == 3);
}
// ============================================================================
// Test: Different Topics Concurrent Processing
// ============================================================================
TEST_CASE("MessageHandler processes different topics concurrently", "[message_handler][concurrency]") {
MessageHandlerFixture handler;
ExecutionTracker tracker;
std::atomic<int> concurrent_count{0};
std::atomic<int> max_concurrent{0};
std::mutex concurrent_mutex;
// Handler duration must exceed the latency threshold so that the first task is still
// running when the second task has been queued long enough to trigger scaling.
constexpr auto HANDLER_DURATION = std::chrono::milliseconds(THREAD_POOL_SCALING_LATENCY_THRESHOLD_MS * 3);
auto handler_func = std::make_shared<Handler>([&](const std::string& topic, const json& data) {
{
std::lock_guard<std::mutex> lock(concurrent_mutex);
concurrent_count++;
if (concurrent_count > max_concurrent) {
max_concurrent = concurrent_count.load();
}
}
tracker.record(topic, data.value("sequence", 0));
std::this_thread::sleep_for(HANDLER_DURATION);
{
std::lock_guard<std::mutex> lock(concurrent_mutex);
concurrent_count--;
}
});
auto test_handler = std::make_shared<TypedHandler>(HandlerType::Call, handler_func);
handler->register_handler("topic/1", test_handler);
handler->register_handler("topic/2", test_handler);
handler->register_handler("topic/3", test_handler);
// The pool starts with a single thread and uses LatencyScaling: it spawns a new thread
// only when a queued task has been waiting longer than THREAD_POOL_SCALING_LATENCY_THRESHOLD_MS.
//
// Sequence to trigger scaling reliably:
// 1. topic/1 submitted → single worker picks it up immediately (queue is empty)
// 2. topic/2 submitted → queues up (worker is busy with topic/1)
// 3. Sleep past the latency threshold → topic/2's wait time exceeds the threshold
// 4. topic/3 submitted → queue size becomes 2 and oldest task (topic/2) has waited
// beyond the threshold → scaling fires, worker 2 is spawned → topic/1 and topic/2
// now run concurrently
handler->add(create_cmd_message("topic/1", 1));
handler->add(create_cmd_message("topic/2", 2));
std::this_thread::sleep_for(std::chrono::milliseconds(THREAD_POOL_SCALING_LATENCY_THRESHOLD_MS + 10));
handler->add(create_cmd_message("topic/3", 3));
tracker.wait_for_count(3);
INFO("max_concurrent was " << max_concurrent.load());
CHECK(max_concurrent.load() > 1);
}
// ============================================================================
// Test: Multiple Topics with Queuing
// ============================================================================
TEST_CASE("MessageHandler handles multiple topics with queuing", "[message_handler][queuing]") {
MessageHandlerFixture handler;
ExecutionTracker tracker;
std::atomic<bool> block_topic1{true};
std::atomic<bool> block_topic2{true};
auto handler_func = std::make_shared<Handler>([&](const std::string& topic, const json& data) {
int seq = data.value("sequence", 0);
if (topic == "topic/1" && seq == 1) {
while (block_topic1) {
std::this_thread::sleep_for(10ms);
}
} else if (topic == "topic/2" && seq == 1) {
while (block_topic2) {
std::this_thread::sleep_for(10ms);
}
}
tracker.record(topic, seq);
});
auto test_handler = std::make_shared<TypedHandler>(HandlerType::Call, handler_func);
handler->register_handler("topic/1", test_handler);
handler->register_handler("topic/2", test_handler);
// Queue multiple messages for each topic
handler->add(create_cmd_message("topic/1", 1));
handler->add(create_cmd_message("topic/1", 2));
handler->add(create_cmd_message("topic/1", 3));
handler->add(create_cmd_message("topic/2", 1));
handler->add(create_cmd_message("topic/2", 2));
handler->add(create_cmd_message("topic/2", 3));
std::this_thread::sleep_for(100ms);
// Release topic/1
block_topic1 = false;
tracker.wait_for_count(3);
// Topic 1 should be complete, topic 2 still blocked
auto events = tracker.get_events();
for (const auto& event : events) {
CHECK(event.topic == "topic/1");
}
// Release topic/2
block_topic2 = false;
tracker.wait_for_count(6);
events = tracker.get_events();
REQUIRE(events.size() == 6);
// Verify ordering per topic
std::vector<int> topic1_seqs;
std::vector<int> topic2_seqs;
for (const auto& event : events) {
if (event.topic == "topic/1") {
topic1_seqs.push_back(event.sequence);
} else {
// "topic/2"
topic2_seqs.push_back(event.sequence);
}
}
CHECK(topic1_seqs == std::vector<int>({1, 2, 3}));
CHECK(topic2_seqs == std::vector<int>({1, 2, 3}));
}
// ============================================================================
// Test: High Load Stress Test
// ============================================================================
TEST_CASE("MessageHandler handles high load", "[message_handler][stress]") {
MessageHandlerFixture handler;
ExecutionTracker tracker;
constexpr int TOPICS_COUNT = 5;
constexpr int MESSAGES_PER_TOPIC = 20;
auto handler_func = std::make_shared<Handler>([&](const std::string& topic, const json& data) {
// Simulate variable processing time
std::this_thread::sleep_for(std::chrono::milliseconds(1 + (rand() % 5)));
tracker.record(topic, data.value("sequence", 0));
});
auto test_handler = std::make_shared<TypedHandler>(HandlerType::Call, handler_func);
// Register handlers for all topics
for (int t = 0; t < TOPICS_COUNT; ++t) {
std::string topic = "topic/" + std::to_string(t);
handler->register_handler(topic, test_handler);
}
// Send messages
for (int t = 0; t < TOPICS_COUNT; ++t) {
for (int m = 0; m < MESSAGES_PER_TOPIC; ++m) {
std::string topic = "topic/" + std::to_string(t);
handler->add(create_cmd_message(topic, m));
}
}
tracker.wait_for_count(TOPICS_COUNT * MESSAGES_PER_TOPIC, 10s);
auto events = tracker.get_events();
REQUIRE(events.size() == TOPICS_COUNT * MESSAGES_PER_TOPIC);
// Verify ordering per topic
std::map<std::string, std::vector<int>> sequences_by_topic;
for (const auto& event : events) {
sequences_by_topic[event.topic].push_back(event.sequence);
}
for (const auto& [topic, sequences] : sequences_by_topic) {
INFO("Checking topic: " << topic);
REQUIRE(sequences.size() == MESSAGES_PER_TOPIC);
// Verify ascending order
for (size_t i = 0; i < sequences.size(); ++i) {
INFO("Position: " << i);
CHECK(sequences[i] == static_cast<int>(i));
}
}
}
// ============================================================================
// Test: Pending Queue Cleanup
// ============================================================================
TEST_CASE("MessageHandler cleans up pending queues", "[message_handler][cleanup]") {
// This test verifies that empty queues are removed from pending_operation_messages_by_topic
// We can't directly access the internal state, but we can verify behavior is consistent
MessageHandlerFixture handler;
ExecutionTracker tracker;
std::atomic<bool> block{true};
auto handler_func = std::make_shared<Handler>([&](const std::string& topic, const json& data) {
int seq = data.value("sequence", 0);
if (seq == 1) {
while (block) {
std::this_thread::sleep_for(10ms);
}
}
tracker.record(topic, seq);
});
auto test_handler = std::make_shared<TypedHandler>(HandlerType::Call, handler_func);
handler->register_handler("test/topic", test_handler);
// Queue messages
handler->add(create_cmd_message("test/topic", 1));
handler->add(create_cmd_message("test/topic", 2));
std::this_thread::sleep_for(100ms);
// Release and process all
block = false;
tracker.wait_for_count(2);
// Send another message - if cleanup didn't happen, behavior might be different
handler->add(create_cmd_message("test/topic", 3));
tracker.wait_for_count(3);
auto events = tracker.get_events();
CHECK(events.size() == 3);
CHECK(events[2].sequence == 3);
}
// ============================================================================
// Test: Result Message Processing (different queue)
// ============================================================================
TEST_CASE("MessageHandler processes result messages separately", "[message_handler][result]") {
MessageHandlerFixture handler;
ExecutionTracker tracker;
auto handler_func = std::make_shared<Handler>(
[&](const std::string& topic, const json& data) { tracker.record(topic, data.value("sequence", 0)); });
auto result_handler =
std::make_shared<TypedHandler>("test-handler", "test-id-123", HandlerType::Result, handler_func);
handler->register_handler("", result_handler);
ParsedMessage msg;
msg.topic = "test/result";
msg.data = {{"msg_type", "CmdResult"}, {"data", {{"data", {{"id", "test-id-123"}, {"sequence", 42}}}}}};
handler->add(msg);
tracker.wait_for_count(1);
auto events = tracker.get_events();
REQUIRE(events.size() == 1);
CHECK(events[0].sequence == 42);
}
// ============================================================================
// Test: Shutdown with Pending Messages
// ============================================================================
TEST_CASE("MessageHandler shuts down gracefully with pending messages", "[message_handler][shutdown]") {
ExecutionTracker tracker;
std::atomic<int> processing_count{0};
std::atomic<bool> handler_started{false};
{
MessageHandlerFixture handler;
auto handler_func = std::make_shared<Handler>([&](const std::string& topic, const json& data) {
processing_count++;
handler_started.store(true);
// Simulate some processing time but don't block indefinitely
std::this_thread::sleep_for(20ms);
tracker.record(topic, data.value("sequence", 0));
});
auto test_handler = std::make_shared<TypedHandler>(HandlerType::Call, handler_func);
handler->register_handler("test/topic", test_handler);
// Queue multiple messages
handler->add(create_cmd_message("test/topic", 1));
handler->add(create_cmd_message("test/topic", 2));
handler->add(create_cmd_message("test/topic", 3));
handler->add(create_cmd_message("test/topic", 4));
handler->add(create_cmd_message("test/topic", 5));
// Give time for first message to start processing but not finish
std::this_thread::sleep_for(10ms);
// At this point, message 1 should be processing and 2-5 should be queued
REQUIRE(handler_started.load()); // Verify first message started
}
// Destructor calls stop(), which sets running=false
// Verify that pending messages (2-5) were NOT processed after shutdown was initiated.
// Only message 1 (in-flight) and any that completed before running=false should be in the tracker.
// Since we sleep 10ms and give the handler ~20ms, at most message 1 completes.
// Messages 2-5 should be abandoned when shutdown is requested.
CHECK(tracker.count() <= 1);
INFO("Processed " << processing_count.load() << " messages total");
}
// ============================================================================
// Test: Thread Safety - Concurrent Adds
// ============================================================================
TEST_CASE("MessageHandler handles concurrent message addition", "[message_handler][thread_safety]") {
MessageHandlerFixture handler;
ExecutionTracker tracker;
auto handler_func = std::make_shared<Handler>(
[&](const std::string& topic, const json& data) { tracker.record(topic, data.value("sequence", 0)); });
auto test_handler = std::make_shared<TypedHandler>(HandlerType::Call, handler_func);
handler->register_handler("topic/1", test_handler);
handler->register_handler("topic/2", test_handler);
constexpr int THREADS = 4;
constexpr int MESSAGES_PER_THREAD = 25;
std::vector<std::thread> threads;
for (int t = 0; t < THREADS; ++t) {
threads.emplace_back([&handler, t]() {
std::string topic = (t % 2 == 0) ? "topic/1" : "topic/2";
for (int m = 0; m < MESSAGES_PER_THREAD; ++m) {
handler->add(create_cmd_message(topic, t * 1000 + m));
std::this_thread::sleep_for(1ms);
}
});
}
for (auto& thread : threads) {
thread.join();
}
tracker.wait_for_count(THREADS * MESSAGES_PER_THREAD, 10s);
auto events = tracker.get_events();
CHECK(events.size() == THREADS * MESSAGES_PER_THREAD);
}
// ============================================================================
// Test: Var Message Processing
// ============================================================================
TEST_CASE("MessageHandler processes Var messages", "[message_handler][var]") {
MessageHandlerFixture handler;
ExecutionTracker tracker;
auto handler_func = std::make_shared<Handler>(
[&](const std::string& topic, const json& data) { tracker.record(topic, data.value("sequence", 0)); });
auto test_handler = std::make_shared<TypedHandler>(HandlerType::SubscribeVar, handler_func);
handler->register_handler("module/impl/var_name", test_handler);
// Create Var message
ParsedMessage msg;
msg.topic = "module/impl/var_name";
msg.data = {{"msg_type", "Var"}, {"data", {{"data", {{"sequence", 42}}}}}};
handler->add(msg);
tracker.wait_for_count(1);
auto events = tracker.get_events();
REQUIRE(events.size() == 1);
CHECK(events[0].topic == "module/impl/var_name");
CHECK(events[0].sequence == 42);
}
TEST_CASE("MessageHandler processes multiple Var messages on same topic in order", "[message_handler][var][ordering]") {
MessageHandlerFixture handler;
ExecutionTracker tracker;
std::atomic<bool> first_var_processing{false};
std::atomic<bool> release_first_var{false};
auto handler_func = std::make_shared<Handler>([&](const std::string& topic, const json& data) {
int seq = data.value("sequence", 0);
if (seq == 1) {
first_var_processing = true;
while (!release_first_var) {
std::this_thread::sleep_for(10ms);
}
}
tracker.record(topic, seq);
});
auto test_handler = std::make_shared<TypedHandler>(HandlerType::SubscribeVar, handler_func);
handler->register_handler("module/impl/var_name", test_handler);
// Send 3 var messages to same topic
for (int i = 1; i <= 3; ++i) {
ParsedMessage msg;
msg.topic = "module/impl/var_name";
msg.data = {{"msg_type", "Var"}, {"data", {{"data", {{"sequence", i}}}}}};
handler->add(msg);
}
// Wait for first message to start processing
while (!first_var_processing) {
std::this_thread::sleep_for(10ms);
}
// Give time for potential out-of-order processing (shouldn't happen)
std::this_thread::sleep_for(100ms);
CHECK(tracker.count() == 0); // First message still blocked
// Release first message
release_first_var = true;
// Wait for all messages
tracker.wait_for_count(3);
auto events = tracker.get_events();
REQUIRE(events.size() == 3);
// Verify order
CHECK(events[0].sequence == 1);
CHECK(events[1].sequence == 2);
CHECK(events[2].sequence == 3);
}
TEST_CASE("MessageHandler handles multiple Var subscribers to same topic", "[message_handler][var]") {
MessageHandlerFixture handler;
ExecutionTracker tracker1;
ExecutionTracker tracker2;
auto handler_func1 = std::make_shared<Handler>(
[&](const std::string& topic, const json& data) { tracker1.record(topic, data.value("sequence", 0)); });
auto test_handler1 = std::make_shared<TypedHandler>(HandlerType::SubscribeVar, handler_func1);
auto handler_func2 = std::make_shared<Handler>(
[&](const std::string& topic, const json& data) { tracker2.record(topic, data.value("sequence", 0)); });
auto test_handler2 = std::make_shared<TypedHandler>(HandlerType::SubscribeVar, handler_func2);
handler->register_handler("module/impl/var_name", test_handler1);
handler->register_handler("module/impl/var_name", test_handler2);
ParsedMessage msg;
msg.topic = "module/impl/var_name";
msg.data = {{"msg_type", "Var"}, {"data", {{"data", {{"sequence", 99}}}}}};
handler->add(msg);
tracker1.wait_for_count(1);
tracker2.wait_for_count(1);
auto events1 = tracker1.get_events();
auto events2 = tracker2.get_events();
REQUIRE(events1.size() == 1);
REQUIRE(events2.size() == 1);
CHECK(events1[0].sequence == 99);
CHECK(events2[0].sequence == 99);
}
// ============================================================================
// Test: ExternalMQTT Message Processing
// ============================================================================
TEST_CASE("MessageHandler processes ExternalMQTT messages", "[message_handler][external_mqtt]") {
MessageHandlerFixture handler;
ExecutionTracker tracker;
auto handler_func = std::make_shared<Handler>([&](const std::string& topic, const json& data) {
// ExternalMQTT passes data directly, not nested
tracker.record(topic, data.value("value", 0));
});
auto test_handler = std::make_shared<TypedHandler>(HandlerType::ExternalMQTT, handler_func);
handler->register_handler("external/topic/+", test_handler);
ParsedMessage msg;
msg.topic = "external/topic/sensor1";
msg.data = {{"value", 123}}; // ExternalMQTT uses data directly, no msg_type
handler->add(msg);
tracker.wait_for_count(1);
auto events = tracker.get_events();
REQUIRE(events.size() == 1);
CHECK(events[0].topic == "external/topic/sensor1");
CHECK(events[0].sequence == 123);
}
TEST_CASE("MessageHandler handles ExternalMQTT with wildcard topics", "[message_handler][external_mqtt]") {
MessageHandlerFixture handler;
ExecutionTracker tracker;
auto handler_func = std::make_shared<Handler>(
[&](const std::string& topic, const json& data) { tracker.record(topic, data.value("value", 0)); });
auto test_handler = std::make_shared<TypedHandler>(HandlerType::ExternalMQTT, handler_func);
// Register with wildcard
handler->register_handler("sensors/#", test_handler);
// Send messages to different sub-topics
for (int i = 1; i <= 3; ++i) {
ParsedMessage msg;
msg.topic = "sensors/floor" + std::to_string(i) + "/temp";
msg.data = {{"value", i * 10}}; // ExternalMQTT uses data directly
handler->add(msg);
}
tracker.wait_for_count(3);
auto events = tracker.get_events();
REQUIRE(events.size() == 3);
// Verify all different topics were received
std::set<std::string> topics;
for (const auto& event : events) {
topics.insert(event.topic);
}
CHECK(topics.size() == 3);
}
// ============================================================================
// Test: Error Message Processing
// ============================================================================
TEST_CASE("MessageHandler processes RaiseError messages", "[message_handler][error]") {
MessageHandlerFixture handler;
ExecutionTracker tracker;
auto handler_func = std::make_shared<Handler>(
[&](const std::string& topic, const json& data) { tracker.record(topic, data.value("error_code", 0)); });
auto test_handler = std::make_shared<TypedHandler>(HandlerType::SubscribeError, handler_func);
handler->register_handler("module/impl/error/#", test_handler);
ParsedMessage msg;
msg.topic = "module/impl/error/critical";
msg.data = {{"msg_type", "RaiseError"}, {"data", {{"error_code", 500}}}};
handler->add(msg);
tracker.wait_for_count(1);
auto events = tracker.get_events();
REQUIRE(events.size() == 1);
CHECK(events[0].sequence == 500);
}
TEST_CASE("MessageHandler processes ClearError messages", "[message_handler][error]") {
MessageHandlerFixture handler;
ExecutionTracker tracker;
auto handler_func =
std::make_shared<Handler>([&](const std::string& topic, const json& data) { tracker.record(topic, 0); });
auto test_handler = std::make_shared<TypedHandler>(HandlerType::SubscribeError, handler_func);
handler->register_handler("module/impl/error/#", test_handler);
ParsedMessage msg;
msg.topic = "module/impl/error/critical";
msg.data = {{"msg_type", "ClearError"}, {"data", json{}}};
handler->add(msg);
tracker.wait_for_count(1);
auto events = tracker.get_events();
CHECK(events.size() == 1);
}
// ============================================================================
// Test: GetConfig Message Processing
// ============================================================================
TEST_CASE("MessageHandler processes GetConfig messages", "[message_handler][config]") {
MessageHandlerFixture handler;
ExecutionTracker tracker;
auto handler_func =
std::make_shared<Handler>([&](const std::string& topic, const json& data) { tracker.record(topic, 1); });
auto test_handler = std::make_shared<TypedHandler>(HandlerType::GetConfig, handler_func);
handler->register_handler("config/request", test_handler);
ParsedMessage msg;
msg.topic = "config/request";
msg.data = {{"msg_type", "GetConfig"}, {"data", json{}}};
handler->add(msg);
tracker.wait_for_count(1);
auto events = tracker.get_events();
CHECK(events.size() == 1);
}
// ============================================================================
// Test: Mixed Message Types
// ============================================================================
TEST_CASE("MessageHandler handles mixed message types concurrently", "[message_handler][mixed]") {
MessageHandlerFixture handler;
ExecutionTracker cmd_tracker;
ExecutionTracker var_tracker;
ExecutionTracker ext_tracker;
auto cmd_handler_func = std::make_shared<Handler>(
[&](const std::string& topic, const json& data) { cmd_tracker.record(topic, data.value("sequence", 0)); });
auto cmd_handler = std::make_shared<TypedHandler>(HandlerType::Call, cmd_handler_func);
auto var_handler_func = std::make_shared<Handler>(
[&](const std::string& topic, const json& data) { var_tracker.record(topic, data.value("sequence", 0)); });
auto var_handler = std::make_shared<TypedHandler>(HandlerType::SubscribeVar, var_handler_func);
auto ext_handler_func = std::make_shared<Handler>(
[&](const std::string& topic, const json& data) { ext_tracker.record(topic, data.value("sequence", 0)); });
auto ext_handler = std::make_shared<TypedHandler>(HandlerType::ExternalMQTT, ext_handler_func);
handler->register_handler("cmd/topic", cmd_handler);
handler->register_handler("var/topic", var_handler);
handler->register_handler("ext/topic", ext_handler);
// Send mixed message types
ParsedMessage cmd_msg = create_cmd_message("cmd/topic", 1);
ParsedMessage var_msg;
var_msg.topic = "var/topic";
var_msg.data = {{"msg_type", "Var"}, {"data", {{"data", {{"sequence", 2}}}}}};
ParsedMessage ext_msg;
ext_msg.topic = "ext/topic";
ext_msg.data = {{"sequence", 3}}; // ExternalMQTT uses data directly
handler->add(cmd_msg);
handler->add(var_msg);
handler->add(ext_msg);
cmd_tracker.wait_for_count(1);
var_tracker.wait_for_count(1);
ext_tracker.wait_for_count(1);
CHECK(cmd_tracker.get_events().size() == 1);
CHECK(var_tracker.get_events().size() == 1);
CHECK(ext_tracker.get_events().size() == 1);
}
TEST_CASE("MessageHandler: GlobalReady arrives before register_handler") {
auto handler = std::make_unique<MessageHandler>();
ExecutionTracker tracker;
bool handler_called = false;
// Send GlobalReady BEFORE registering the handler (critical race condition)
ParsedMessage ready_msg;
ready_msg.topic = "global";
ready_msg.data = {{"msg_type", "GlobalReady"}, {"data", {{"ready_data", true}}}};
handler->add(ready_msg);
// Give the ready_thread a moment to execute (it should find no handler registered)
std::this_thread::sleep_for(100ms);
// Now register the handler - too late, the ready_thread has already exited
auto ready_handler_func = std::make_shared<Handler>([&](const std::string& topic, const json& data) {
handler_called = true;
tracker.record(topic, 1);
});
auto ready_handler = std::make_shared<TypedHandler>(HandlerType::GlobalReady, ready_handler_func);
handler->register_handler("global", ready_handler);
// Wait briefly to ensure no deferred execution
std::this_thread::sleep_for(100ms);
// Handler should NOT have been called since it was registered after the message arrived
CHECK(handler_called == false);
CHECK(tracker.count() == 0);
handler->stop();
}
TEST_CASE("MessageHandler: Per-topic mutual exclusion (at most one in-flight per topic)") {
auto handler = std::make_unique<MessageHandler>();
// Track concurrent execution for each topic
std::map<std::string, std::atomic<int>> in_flight_count;
std::map<std::string, int> max_concurrent;
std::mutex concurrent_mutex;
ExecutionTracker tracker;
auto create_blocking_handler = [&](const std::string& topic) {
auto handler_func = std::make_shared<Handler>([&, topic](const std::string& msg_topic, const json& data) {
// Increment in-flight count for this topic
in_flight_count[topic]++;
int current = in_flight_count[topic];
// Record the start of execution
tracker.record(msg_topic, data.value("sequence", 0));
{
std::lock_guard<std::mutex> lock(concurrent_mutex);
max_concurrent[topic] = std::max(max_concurrent[topic], current);
}
// Simulate work with a delay
std::this_thread::sleep_for(50ms);
// Decrement in-flight count
in_flight_count[topic]--;
});
return std::make_shared<TypedHandler>(HandlerType::SubscribeVar, handler_func);
};
// Register handlers for multiple topics
handler->register_handler("topic/A", create_blocking_handler("topic/A"));
handler->register_handler("topic/B", create_blocking_handler("topic/B"));
// Send 3 messages for topic A (should be queued, not concurrent)
for (int i = 1; i <= 3; ++i) {
ParsedMessage msg = create_var_message("topic/A", i);
handler->add(msg);
}
// Send 3 messages for topic B (should also not be concurrent with each other)
for (int i = 1; i <= 3; ++i) {
ParsedMessage msg = create_var_message("topic/B", i);
handler->add(msg);
}
// Wait for all messages to be processed
tracker.wait_for_count(6, 10000ms);
// Verify both topics processed all 3 messages
auto events = tracker.get_events();
std::map<std::string, int> processed_count;
for (const auto& event : events) {
processed_count[event.topic]++;
}
CHECK(processed_count["topic/A"] == 3);
CHECK(processed_count["topic/B"] == 3);
// Verify at most 1 message in-flight per topic at any time
CHECK(max_concurrent["topic/A"] <= 1);
CHECK(max_concurrent["topic/B"] <= 1);
// Verify ordering is preserved within each topic
int last_seq_A = 0;
int last_seq_B = 0;
for (const auto& event : events) {
if (event.topic == "topic/A") {
CHECK(event.sequence > last_seq_A);
last_seq_A = event.sequence;
} else if (event.topic == "topic/B") {
CHECK(event.sequence > last_seq_B);
last_seq_B = event.sequence;
}
}
handler->stop();
}

View File

@@ -0,0 +1,35 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <utils/message_handler_scaling_policy.hpp>
#include <catch2/catch_test_macros.hpp>
#include <type_traits>
TEST_CASE("MessageHandler scaling policy matches CMake configuration", "[message_handler][scaling_policy]") {
STATIC_REQUIRE(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY ==
EVEREST_FRAMEWORK_EXPECTED_THREAD_POOL_SCALING_POLICY);
STATIC_REQUIRE(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_THRESHOLD_MS ==
EVEREST_FRAMEWORK_EXPECTED_THREAD_POOL_SCALING_LATENCY_THRESHOLD_MS);
STATIC_REQUIRE(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_TICK_MS ==
EVEREST_FRAMEWORK_EXPECTED_THREAD_POOL_SCALING_LATENCY_TICK_MS);
STATIC_REQUIRE(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_FIXED_SIZE_THRESHOLD ==
EVEREST_FRAMEWORK_EXPECTED_THREAD_POOL_SCALING_FIXED_SIZE_THRESHOLD);
#if EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY == EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM
STATIC_REQUIRE(std::is_same_v<Everest::detail::MessageHandlerScalingPolicy,
EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE>);
#elif EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY == EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_GREEDY
STATIC_REQUIRE(std::is_same_v<Everest::detail::MessageHandlerScalingPolicy, everest::lib::util::GreedyScaling>);
#elif EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY == EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CONSERVATIVE
STATIC_REQUIRE(
std::is_same_v<Everest::detail::MessageHandlerScalingPolicy, everest::lib::util::ConservativeScaling>);
#elif EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY == EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_FIXED_SIZE
STATIC_REQUIRE(
std::is_same_v<Everest::detail::MessageHandlerScalingPolicy, Everest::detail::MessageHandlerFixedSizeScaling>);
#else
STATIC_REQUIRE(
std::is_same_v<Everest::detail::MessageHandlerScalingPolicy, Everest::detail::MessageHandlerLatencyScaling>);
#endif
}

View File

@@ -0,0 +1,6 @@
description: "This is an invalid manifest (metadata is missing)."
provides:
main:
description: "This unit has an invalid manifest without metadata"
interface: "test_interface"

View File

@@ -0,0 +1,13 @@
description: "This is an invalid manifest (broken config)."
config:
invalidvalid_config_entry:
description: "This is a invalid config entry"
type: string
default: 42
provides:
main:
description: "This unit has a invalid manifest."
interface: "test_interface"
metadata:
license: "https://opensource.org/licenses/Apache-2.0"
authors: ["Kai-Uwe Hermann"]

View File

@@ -0,0 +1,13 @@
description: "This is an invalid manifest (broken config)."
provides:
main:
description: "This unit has a invalid manifest."
interface: "test_interface"
config:
invalidvalid_config_entry:
description: "This is a invalid config entry"
type: string
default: 42
metadata:
license: "https://opensource.org/licenses/Apache-2.0"
authors: ["Kai-Uwe Hermann"]

View File

@@ -0,0 +1,8 @@
description: "This is a valid manifest."
provides:
main:
description: "This unit has a valid manifest but no corresponding class"
interface: "missing_interface"
metadata:
license: "https://opensource.org/licenses/Apache-2.0"
authors: ["Kai-Uwe Hermann"]

View File

@@ -0,0 +1,22 @@
description: ModuleA
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- author@example.com
config:
valid_module_config_entry:
description: "This is a valid config entry"
type: string
provides:
main:
description: test description
interface: test_interface
config:
valid_impl_config_entry:
description: "This is a valid impl config entry"
type: integer
requires:
req1:
interface: test_interface_cmd_var
min_connections: 1
max_connections: 1

View File

@@ -0,0 +1,11 @@
description: ModuleB
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- author@example.com
provides:
impl1:
description: test description
interface: test_interface_cmd_var
requires: {}
config: {}

View File

@@ -0,0 +1,20 @@
description: "This is a valid manifest."
config:
valid_config_entry:
description: "This is a valid config entry"
type: string
valid_config_entry_with_default:
description: "This is a valid config entry with a default"
type: number
default: 42
provides:
main:
description: "This unit has a valid manifest but no corresponding class"
interface: "test_interface"
config:
valid_config_entry:
description: "This is a valid config entry"
type: string
metadata:
license: "https://opensource.org/licenses/Apache-2.0"
authors: ["Kai-Uwe Hermann"]

View File

@@ -0,0 +1,20 @@
description: "This is a valid manifest."
config:
valid_config_entry:
description: "This is a valid config entry"
type: string
valid_config_entry_with_default:
description: "This is a valid config entry with a default"
type: number
default: 42
provides:
main:
description: "Provides an interface with a cmd and a var"
interface: "test_interface_cmd_var"
config:
valid_config_entry:
description: "This is a valid config entry"
type: string
metadata:
license: "https://opensource.org/licenses/Apache-2.0"
authors: ["Kai-Uwe Hermann"]

View File

@@ -0,0 +1,13 @@
description: "This is a valid manifest."
provides:
main:
description: "Empty"
interface: "test_interface"
requires:
test_cmd_var:
interface: "test_interface_cmd_var"
min_connections: 0
max_connections: 1
metadata:
license: "https://opensource.org/licenses/Apache-2.0"
authors: ["Kai-Uwe Hermann"]

View File

@@ -0,0 +1,13 @@
description: Test object type
types:
AnObject:
description: An object type
type: object
additionalProperties: false
properties:
first:
description: first property
type: number
second:
description: second property
type: string

View File

@@ -0,0 +1,138 @@
function (setup_test_directory)
# NOTE (aw): this function got quite complex, it could be simpler if
# the tests would use a more consistent directory layout
set(options
USE_FILESYSTEM_HIERARCHY_STANDARD
)
set(one_value_args
CONFIG
USER_CONFIG
)
set(multi_value_args
TYPE_FILES
MODULES
INTERFACE_FILES
)
cmake_parse_arguments(arg "${options}" "${one_value_args}" "${multi_value_args}" ${ARGN})
if (NOT arg_UNPARSED_ARGUMENTS)
message(FATAL_ERROR "${CMAKE_CURRENT_FUNCTION} requires first argument to be the name")
endif()
list(GET arg_UNPARSED_ARGUMENTS 0 NAME)
if (NOT NAME STREQUAL ARGV0)
message(FATAL_ERROR "${CMAKE_CURRENT_FUNCTION} unexpected argument: ${NAME}")
endif()
list(LENGTH arg_UNPARSED_ARGUMENTS UNPARSED_ARGUMENTS_COUNT)
if (UNPARSED_ARGUMENTS_COUNT GREATER 1)
list(GET arg_UNPARSED_ARGUMENTS 1 MODULE)
if (NOT MODULE STREQUAL ARGV1)
message(FATAL_ERROR "${CMAKE_CURRENT_FUNCTION} unexpected argument: ${MODULE}")
endif()
endif()
if (UNPARSED_ARGUMENTS_COUNT GREATER 2)
list(GET arg_UNPARSED_ARGUMENTS 2 INTERFACE_FILE)
if (NOT INTERFACE_FILE STREQUAL ARGV2)
message(FATAL_ERROR "${CMAKE_CURRENT_FUNCTION} unexpected argument: ${INTERFACE_FILE}")
endif()
endif()
if (UNPARSED_ARGUMENTS_COUNT GREATER 3)
list(GET arg_UNPARSED_ARGUMENTS 3 UNKNOWN_ARG)
message(FATAL_ERROR "${CMAKE_CURRENT_FUNCTION} unknown argument: ${UNKNOWN_ARG}")
endif()
set(DIR ${CMAKE_CURRENT_BINARY_DIR}/${NAME})
file(MAKE_DIRECTORY "${DIR}")
set(SHARE_EVEREST_DIR ${DIR})
if (arg_USE_FILESYSTEM_HIERARCHY_STANDARD)
set(SHARE_EVEREST_DIR "${DIR}/share/everest")
endif ()
file(MAKE_DIRECTORY "${SHARE_EVEREST_DIR}")
file(COPY ${PROJECT_SOURCE_DIR}/schemas/migrations DESTINATION ${DIR}/share/everest/)
set (SCHEMAS_DIR "${SHARE_EVEREST_DIR}/schemas")
file(MAKE_DIRECTORY "${SCHEMAS_DIR}")
file(COPY ${PROJECT_SOURCE_DIR}/schemas/ DESTINATION ${SCHEMAS_DIR}/)
set (INTERFACES_DIR "${SHARE_EVEREST_DIR}/interfaces")
file(MAKE_DIRECTORY "${INTERFACES_DIR}")
set (TYPES_DIR "${SHARE_EVEREST_DIR}/types")
file(MAKE_DIRECTORY "${TYPES_DIR}")
file(MAKE_DIRECTORY "${SHARE_EVEREST_DIR}/errors")
file(MAKE_DIRECTORY "${SHARE_EVEREST_DIR}/www")
set(MODULES_DIR "${DIR}/modules")
if (arg_USE_FILESYSTEM_HIERARCHY_STANDARD)
set(MODULES_DIR "${DIR}/libexec/everest/modules")
endif()
file(MAKE_DIRECTORY "${MODULES_DIR}")
set(CONFIG_DIR ${DIR})
if (arg_USE_FILESYSTEM_HIERARCHY_STANDARD)
set(CONFIG_DIR "${DIR}/etc/everest")
endif ()
file(MAKE_DIRECTORY "${CONFIG_DIR}")
if (arg_USE_FILESYSTEM_HIERARCHY_STANDARD)
configure_file(test_logging.ini ${CONFIG_DIR}/default_logging.cfg COPYONLY)
else ()
configure_file(test_logging.ini ${CONFIG_DIR}/logging.ini COPYONLY)
endif()
# FIXME (aw): these two need to exist anyway
file(MAKE_DIRECTORY "${DIR}/etc/everest")
file(MAKE_DIRECTORY "${DIR}/share/everest")
# FIXME (aw): config files get special directory treatment, this
# should be consistent too (tests need to be changed)
if (arg_CONFIG)
# NOTE (aw): this is for json config file compatibility, but
# this is probably not in use anymore and should be removed
get_filename_component(CONFIG_EXT ${arg_CONFIG} LAST_EXT)
configure_file(test_configs/${arg_CONFIG} ${DIR}/config${CONFIG_EXT} COPYONLY)
else ()
configure_file(test_configs/${NAME}_config.yaml ${DIR}/config.yaml COPYONLY)
endif()
if (arg_USER_CONFIG)
configure_file(test_configs/${arg_USER_CONFIG} ${DIR}/user-config/config.yaml COPYONLY)
endif()
if (MODULE)
file(COPY test_modules/${MODULE} DESTINATION ${MODULES_DIR})
endif()
if (INTERFACE_FILE)
file(COPY test_interfaces/${INTERFACE_FILE}.yaml DESTINATION ${INTERFACES_DIR}/)
endif()
if (arg_TYPE_FILES)
foreach(TYPE_FILE ${arg_TYPE_FILES})
file(COPY test_types/${TYPE_FILE} DESTINATION ${TYPES_DIR}/)
endforeach()
endif()
if (arg_MODULES)
foreach(MODULE ${arg_MODULES})
file(COPY test_modules/${MODULE} DESTINATION ${MODULES_DIR})
endforeach()
endif()
if (arg_INTERFACE_FILES)
foreach(INTERFACE_FILE ${arg_INTERFACE_FILES})
file(COPY test_interfaces/${INTERFACE_FILE}.yaml DESTINATION ${INTERFACES_DIR}/)
endforeach()
endif()
endfunction()