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:
124
tools/EVerest-main/lib/everest/framework/tests/CMakeLists.txt
Normal file
124
tools/EVerest-main/lib/everest/framework/tests/CMakeLists.txt
Normal 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}"
|
||||
)
|
||||
@@ -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})
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
tools/EVerest-main/lib/everest/framework/tests/helpers.cpp
Normal file
18
tools/EVerest-main/lib/everest/framework/tests/helpers.cpp
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
388
tools/EVerest-main/lib/everest/framework/tests/test_config.cpp
Normal file
388
tools/EVerest-main/lib/everest/framework/tests/test_config.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
null
|
||||
@@ -0,0 +1 @@
|
||||
"This is a string!"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
active_modules:
|
||||
valid_module:
|
||||
module: TESTValidManifest
|
||||
config_module:
|
||||
valid_config_entry: "hi"
|
||||
@@ -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"
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/#/"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
description: "This defines a minimal valid interface"
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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: {}
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user