diff --git a/dependencies.yaml b/dependencies.yaml index 1aa2f85ff..13e1cb316 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -90,3 +90,11 @@ gtest: git: https://github.com/google/googletest.git git_tag: release-1.12.1 cmake_condition: "EVEREST_CORE_BUILD_TESTING" +sqlite_cpp: + git: https://github.com/SRombauts/SQLiteCpp.git + git_tag: 3.3.1 + cmake_condition: "EVEREST_DEPENDENCY_ENABLED_SQLITE_CPP" +catch2: + git: https://github.com/catchorg/Catch2.git + git_tag: v3.4.0 + cmake_condition: "EVEREST_CORE_BUILD_TESTING" diff --git a/interfaces/error_history.yaml b/interfaces/error_history.yaml new file mode 100644 index 000000000..0135ef9ed --- /dev/null +++ b/interfaces/error_history.yaml @@ -0,0 +1,14 @@ +description: This interface provides access to the error history of the EVerest framework +cmds: + get_errors: + description: Takes a list of filters and returns a list of errors + arguments: + filters: + type: object + description: Filters to apply to the list of errors + $ref: /error_history#/FilterArguments + result: + description: List of filtered errors + type: array + items: + $ref: /error_history#/ErrorObject diff --git a/module-dependencies.cmake b/module-dependencies.cmake index 95c2f0f7a..8c9a88a4d 100644 --- a/module-dependencies.cmake +++ b/module-dependencies.cmake @@ -57,6 +57,10 @@ ev_define_dependency( OUTPUT_VARIABLE_SUFFIX LIBEVSE_SECURITY DEPENDENT_MODULES_LIST OCPP OCPP201 EvseSecurity) +ev_define_dependency( + DEPENDENCY_NAME sqlite_cpp + DEPENDENT_MODULES_LIST ErrorHistory) + if(NOT everest-gpio IN_LIST EVEREST_EXCLUDE_DEPENDENCIES) set(EVEREST_DEPENDENCY_ENABLED_EVEREST_GPIO ON) else() diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index 4a142cbb9..807cc9ecd 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -2,6 +2,7 @@ ev_add_module(API) ev_add_module(Auth) ev_add_module(EnergyManager) ev_add_module(EnergyNode) +ev_add_module(ErrorHistory) ev_add_module(EvseManager) ev_add_module(EvseSecurity) ev_add_module(EvseSlac) diff --git a/modules/ErrorHistory/CMakeLists.txt b/modules/ErrorHistory/CMakeLists.txt new file mode 100644 index 000000000..a1604cfe4 --- /dev/null +++ b/modules/ErrorHistory/CMakeLists.txt @@ -0,0 +1,39 @@ +# +# AUTO GENERATED - MARKED REGIONS WILL BE KEPT +# template version 3 +# + +# module setup: +# - ${MODULE_NAME}: module name +ev_setup_cpp_module() + +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 +option(BUILD_TESTING "Run unit tests" OFF) +set(CMAKE_PREFIX_PATH "/usr/lib/x86_64-linux-gnu" ${CMAKE_PREFIX_PATH}) + +find_package(SQLite3 REQUIRED) +if (DISABLE_EDM) + find_package(SQLiteCpp REQUIRED) +endif() + +target_link_libraries(${MODULE_NAME} + PRIVATE + SQLiteCpp + SQLite::SQLite3 +) +target_sources(${MODULE_NAME} + PRIVATE + "ErrorDatabaseSqlite.cpp" +) +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 + +target_sources(${MODULE_NAME} + PRIVATE + "error_history/error_historyImpl.cpp" +) + +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 +if(EVEREST_CORE_BUILD_TESTING) + add_subdirectory(tests) +endif() +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 diff --git a/modules/ErrorHistory/ErrorDatabaseSqlite.cpp b/modules/ErrorHistory/ErrorDatabaseSqlite.cpp new file mode 100644 index 000000000..0c841f5b3 --- /dev/null +++ b/modules/ErrorHistory/ErrorDatabaseSqlite.cpp @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include "ErrorDatabaseSqlite.hpp" + +#include +#include + +#include +#include + +namespace module { + +ErrorDatabaseSqlite::ErrorDatabaseSqlite(const fs::path& db_path_, const bool reset_) : + db_path(fs::absolute(db_path_)) { + BOOST_LOG_FUNCTION(); + std::lock_guard lock(this->db_mutex); + + bool reset = reset_ || !fs::exists(this->db_path); + if (reset) { + EVLOG_info << "Resetting database"; + this->reset_database(); + } else { + EVLOG_info << "Using database at " << this->db_path; + this->check_database(); + } +} + +void ErrorDatabaseSqlite::check_database() { + BOOST_LOG_FUNCTION(); + EVLOG_info << "Checking database"; + std::shared_ptr db; + try { + db = std::make_shared(this->db_path.string(), SQLite::OPEN_READONLY); + } catch (std::exception& e) { + EVLOG_error << "Error opening database: " << e.what(); + throw; + } + try { + std::string sql = "SELECT name"; + sql += " FROM sqlite_schema"; + sql += " WHERE type = 'table' AND name NOT LIKE 'sqlite_%';"; + SQLite::Statement stmt(*db, sql); + bool has_errors_table = false; + while (stmt.executeStep()) { + std::string table_name = stmt.getColumn(0); + if (table_name == "errors") { + if (has_errors_table) { + throw Everest::EverestConfigError("Database contains multiple errors tables"); + } + has_errors_table = true; + EVLOG_debug << "Found errors table"; + } else { + EVLOG_warning << "Found unknown table: " << table_name; + } + } + if (!has_errors_table) { + throw Everest::EverestConfigError("Database does not contain errors table"); + } + } catch (std::exception& e) { + EVLOG_error << "Error checking whether table 'errors' exist" << e.what(); + throw; + } +} + +void ErrorDatabaseSqlite::reset_database() { + BOOST_LOG_FUNCTION(); + fs::path database_directory = this->db_path.parent_path(); + if (!fs::exists(database_directory)) { + fs::create_directories(database_directory); + } + if (fs::exists(this->db_path)) { + fs::remove(this->db_path); + } + try { + SQLite::Database db(this->db_path.string(), SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); + std::string sql = "CREATE TABLE errors(" + "uuid TEXT PRIMARY KEY NOT NULL," + "type TEXT NOT NULL," + "description TEXT NOT NULL," + "message TEXT NOT NULL," + "from_module TEXT NOT NULL," + "from_implementation TEXT NOT NULL," + "timestamp TEXT NOT NULL," + "severity TEXT NOT NULL," + "state TEXT NOT NULL);"; + db.exec(sql); + } catch (std::exception& e) { + EVLOG_error << "Error creating database: " << e.what(); + throw; + } +} + +void ErrorDatabaseSqlite::add_error(Everest::error::ErrorPtr error) { + std::lock_guard lock(this->db_mutex); + this->add_error_without_mutex(error); +} + +void ErrorDatabaseSqlite::add_error_without_mutex(Everest::error::ErrorPtr error) { + BOOST_LOG_FUNCTION(); + try { + SQLite::Database db(this->db_path.string(), SQLite::OPEN_READWRITE); + std::string sql = "INSERT INTO errors(uuid, type, description, message, from_module, from_implementation, " + "timestamp, severity, state) VALUES("; + sql += "?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9);"; + SQLite::Statement stmt(db, sql); + stmt.bind(1, error->uuid.to_string()); + stmt.bind(2, error->type); + stmt.bind(3, error->description); + stmt.bind(4, error->message); + stmt.bind(5, error->from.module_id); + stmt.bind(6, error->from.implementation_id); + stmt.bind(7, Everest::Date::to_rfc3339(error->timestamp)); + stmt.bind(8, Everest::error::severity_to_string(error->severity)); + stmt.bind(9, Everest::error::state_to_string(error->state)); + stmt.exec(); + } catch (std::exception& e) { + EVLOG_error << "Error adding error to database: " << e.what(); + throw; + } +} + +std::string ErrorDatabaseSqlite::filter_to_sql_condition(const Everest::error::ErrorFilter& filter) { + std::string condition = ""; + switch (filter.get_filter_type()) { + case Everest::error::FilterType::State: { + condition = "(state = '" + Everest::error::state_to_string(filter.get_state_filter()) + "')"; + } break; + case Everest::error::FilterType::Origin: { + condition = "(from_module = '" + filter.get_origin_filter().module_id + "' AND " + "from_implementation = '" + + filter.get_origin_filter().implementation_id + "')"; + } break; + case Everest::error::FilterType::Type: { + condition = "(type = '" + filter.get_type_filter() + "')"; + } break; + case Everest::error::FilterType::Severity: { + switch (filter.get_severity_filter()) { + case Everest::error::SeverityFilter::LOW_GE: { + condition = "(severity = '" + Everest::error::severity_to_string(Everest::error::Severity::Low) + + "' OR severity = '" + Everest::error::severity_to_string(Everest::error::Severity::Medium) + + "' OR severity = '" + Everest::error::severity_to_string(Everest::error::Severity::High) + "')"; + } break; + case Everest::error::SeverityFilter::MEDIUM_GE: { + condition = "(severity = '" + Everest::error::severity_to_string(Everest::error::Severity::Medium) + + "' OR severity = '" + Everest::error::severity_to_string(Everest::error::Severity::High) + "')"; + } break; + case Everest::error::SeverityFilter::HIGH_GE: { + condition = "(severity = '" + Everest::error::severity_to_string(Everest::error::Severity::High) + "')"; + } break; + } + } break; + case Everest::error::FilterType::TimePeriod: { + condition = "(timestamp BETWEEN '" + Everest::Date::to_rfc3339(filter.get_time_period_filter().from) + + "' AND '" + Everest::Date::to_rfc3339(filter.get_time_period_filter().to) + "')"; + } break; + case Everest::error::FilterType::Handle: { + condition = "(uuid = '" + filter.get_handle_filter().to_string() + "')"; + } break; + } + return condition; +} + +std::optional +ErrorDatabaseSqlite::filters_to_sql_condition(const std::list& filters) { + std::optional condition = std::nullopt; + if (!filters.empty()) { + auto it = filters.begin(); + condition = filter_to_sql_condition(*it); + it++; + while (it != filters.end()) { + condition = condition.value() + " AND " + ErrorDatabaseSqlite::filter_to_sql_condition(*it); + it++; + } + } + return condition; +} + +std::list +ErrorDatabaseSqlite::get_errors(const std::list& filters) const { + std::lock_guard lock(this->db_mutex); + return this->get_errors(ErrorDatabaseSqlite::filters_to_sql_condition(filters)); +} + +std::list ErrorDatabaseSqlite::get_errors(const std::optional& condition) const { + BOOST_LOG_FUNCTION(); + std::list result; + try { + SQLite::Database db(this->db_path.string(), SQLite::OPEN_READONLY); + std::string sql = "SELECT * FROM errors"; + if (condition.has_value()) { + sql += " WHERE " + condition.value(); + } + EVLOG_debug << "Executing SQL statement: " << sql; + SQLite::Statement stmt(db, sql); + while (stmt.executeStep()) { + const Everest::error::ErrorType err_type(stmt.getColumn("type").getText()); + const std::string err_description = stmt.getColumn("description").getText(); + const std::string err_msg = stmt.getColumn("message").getText(); + const std::string err_from_module_id = stmt.getColumn("from_module").getText(); + const std::string err_from_impl_id = stmt.getColumn("from_implementation").getText(); + const ImplementationIdentifier err_from(err_from_module_id, err_from_impl_id); + const Everest::error::Error::time_point err_timestamp = + Everest::Date::from_rfc3339(stmt.getColumn("timestamp").getText()); + const Everest::error::Severity err_severity = + Everest::error::string_to_severity(stmt.getColumn("severity").getText()); + const Everest::error::State err_state = Everest::error::string_to_state(stmt.getColumn("state").getText()); + const Everest::error::ErrorHandle err_handle(Everest::error::ErrorHandle(stmt.getColumn("uuid").getText())); + Everest::error::ErrorPtr error = std::make_shared( + err_type, err_msg, err_description, err_from, err_severity, err_timestamp, err_handle, err_state); + result.push_back(error); + } + } catch (std::exception& e) { + EVLOG_error << "Error getting errors from database: " << e.what(); + throw; + } + return result; +} + +std::list +ErrorDatabaseSqlite::edit_errors(const std::list& filters, EditErrorFunc edit_func) { + std::lock_guard lock(this->db_mutex); + std::list result = this->remove_errors_without_mutex(filters); + for (Everest::error::ErrorPtr& error : result) { + edit_func(error); + this->add_error_without_mutex(error); + } + return result; +} + +std::list +ErrorDatabaseSqlite::remove_errors(const std::list& filters) { + std::lock_guard lock(this->db_mutex); + return this->remove_errors_without_mutex(filters); +} + +std::list +ErrorDatabaseSqlite::remove_errors_without_mutex(const std::list& filters) { + BOOST_LOG_FUNCTION(); + std::optional condition = ErrorDatabaseSqlite::filters_to_sql_condition(filters); + std::list result = this->get_errors(condition); + try { + SQLite::Database db(this->db_path.string(), SQLite::OPEN_READWRITE); + std::string sql = "DELETE FROM errors"; + if (condition.has_value()) { + sql += " WHERE " + condition.value(); + } + db.exec(sql); + } catch (std::exception& e) { + EVLOG_error << "Error removing errors from database: " << e.what(); + throw; + } + return result; +} + +} // namespace module diff --git a/modules/ErrorHistory/ErrorDatabaseSqlite.hpp b/modules/ErrorHistory/ErrorDatabaseSqlite.hpp new file mode 100644 index 000000000..4f7f71a51 --- /dev/null +++ b/modules/ErrorHistory/ErrorDatabaseSqlite.hpp @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#ifndef ERROR_HISTORY_ERROR_DATABASE_SQLITE_HPP +#define ERROR_HISTORY_ERROR_DATABASE_SQLITE_HPP + +#include + +#include + +namespace fs = std::filesystem; + +namespace module { + +class ErrorDatabaseSqlite : public Everest::error::ErrorDatabase { +public: + explicit ErrorDatabaseSqlite(const fs::path& db_path_, const bool reset_ = false); + + void add_error(Everest::error::ErrorPtr error) override; + std::list + get_errors(const std::list& filters) const override; + std::list edit_errors(const std::list& filters, + EditErrorFunc edit_func) override; + std::list remove_errors(const std::list& filters) override; + +private: + void add_error_without_mutex(Everest::error::ErrorPtr error); + std::list + remove_errors_without_mutex(const std::list& filters); + std::list get_errors(const std::optional& condition) const; + static std::string filter_to_sql_condition(const Everest::error::ErrorFilter& filter); + static std::optional filters_to_sql_condition(const std::list& filters); + + void reset_database(); + void check_database(); + const fs::path db_path; + mutable std::mutex db_mutex; +}; + +} // namespace module + +#endif // ERROR_HISTORY_ERROR_DATABASE_SQLITE_HPP diff --git a/modules/ErrorHistory/ErrorHistory.cpp b/modules/ErrorHistory/ErrorHistory.cpp new file mode 100644 index 000000000..9df5b023e --- /dev/null +++ b/modules/ErrorHistory/ErrorHistory.cpp @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#include "ErrorHistory.hpp" + +namespace module { + +void ErrorHistory::init() { + invoke_init(*p_error_history); +} + +void ErrorHistory::ready() { + invoke_ready(*p_error_history); +} + +} // namespace module diff --git a/modules/ErrorHistory/ErrorHistory.hpp b/modules/ErrorHistory/ErrorHistory.hpp new file mode 100644 index 000000000..9f730ee99 --- /dev/null +++ b/modules/ErrorHistory/ErrorHistory.hpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef ERROR_HISTORY_HPP +#define ERROR_HISTORY_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 2 +// + +#include "ld-ev.hpp" + +// headers for provided interface implementations +#include + +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 +// insert your custom include headers here +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 + +namespace module { + +struct Conf {}; + +class ErrorHistory : public Everest::ModuleBase { +public: + ErrorHistory() = delete; + ErrorHistory(const ModuleInfo& info, std::unique_ptr p_error_history, Conf& config) : + ModuleBase(info), p_error_history(std::move(p_error_history)), config(config){}; + + const std::unique_ptr p_error_history; + const Conf& config; + + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + // insert your public definitions here + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + +protected: + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + // insert your protected definitions here + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + +private: + friend class LdEverest; + void init(); + void ready(); + + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 + // insert your private definitions here + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 +}; + +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 +// insert other definitions here +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 + +} // namespace module + +#endif // ERROR_HISTORY_HPP diff --git a/modules/ErrorHistory/error_history/error_historyImpl.cpp b/modules/ErrorHistory/error_history/error_historyImpl.cpp new file mode 100644 index 000000000..f50617f73 --- /dev/null +++ b/modules/ErrorHistory/error_history/error_historyImpl.cpp @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include "error_historyImpl.hpp" + +#include "../ErrorDatabaseSqlite.hpp" + +#include +#include + +namespace fs = std::filesystem; +namespace module { +namespace error_history { + +void error_historyImpl::init() { + this->db = std::make_shared(this->config.database_path); + + Everest::error::StateFilter state_filter(Everest::error::State::Active); + Everest::error::ErrorFilter error_filter(state_filter); + this->db->edit_errors( + {error_filter}, [](Everest::error::ErrorPtr error) { error->state = Everest::error::State::ClearedByReboot; }); + subscribe_global_all_errors( + [this](const Everest::error::Error& error) { this->handle_global_all_errors(error); }, + [this](const Everest::error::Error& error) { this->handle_global_all_errors_cleared(error); }); +} + +void error_historyImpl::ready() { +} + +Everest::error::StateFilter convert_state_filter(types::error_history::State filter) { + switch (filter) { + case types::error_history::State::Active: + return Everest::error::StateFilter::Active; + case types::error_history::State::ClearedByModule: + return Everest::error::StateFilter::ClearedByModule; + case types::error_history::State::ClearedByReboot: + return Everest::error::StateFilter::ClearedByReboot; + } + throw std::out_of_range("No known enum conversion from enum type types::error_history::State to enum type " + "Everest::error::StateFilter"); +} + +Everest::error::SeverityFilter convert_severity_filter(types::error_history::SeverityFilter filter) { + switch (filter) { + case types::error_history::SeverityFilter::LOW_GE: + return Everest::error::SeverityFilter::LOW_GE; + case types::error_history::SeverityFilter::MEDIUM_GE: + return Everest::error::SeverityFilter::MEDIUM_GE; + case types::error_history::SeverityFilter::HIGH_GE: + return Everest::error::SeverityFilter::HIGH_GE; + } + throw std::out_of_range("No known enum conversion from enum type types::error_history::SeverityFilter to enum type " + "Everest::error::SeverityFilter"); +} + +std::vector +error_historyImpl::handle_get_errors(types::error_history::FilterArguments& filters) { + std::list error_filters; + if (filters.state_filter.has_value()) { + Everest::error::StateFilter state_filter = convert_state_filter(filters.state_filter.value()); + error_filters.push_back(Everest::error::ErrorFilter(state_filter)); + } + if (filters.origin_filter.has_value()) { + Everest::error::OriginFilter origin_filter(filters.origin_filter.value().module_id, + filters.origin_filter.value().implementation_id); + error_filters.push_back(Everest::error::ErrorFilter(origin_filter)); + } + if (filters.type_filter.has_value()) { + Everest::error::TypeFilter type_filter(filters.type_filter.value()); + error_filters.push_back(Everest::error::ErrorFilter(type_filter)); + } + if (filters.severity_filter.has_value()) { + Everest::error::SeverityFilter severity_filter = convert_severity_filter(filters.severity_filter.value()); + error_filters.push_back(Everest::error::ErrorFilter(severity_filter)); + } + if (filters.timeperiod_filter.has_value()) { + Everest::error::TimePeriodFilter timeperiod_filter; + timeperiod_filter.from = Everest::Date::from_rfc3339(filters.timeperiod_filter.value().timestamp_from); + timeperiod_filter.to = Everest::Date::from_rfc3339(filters.timeperiod_filter.value().timestamp_to); + error_filters.push_back(Everest::error::ErrorFilter(timeperiod_filter)); + } + if (filters.handle_filter.has_value()) { + Everest::error::HandleFilter handle_filter(filters.handle_filter.value()); + error_filters.push_back(Everest::error::ErrorFilter(handle_filter)); + } + std::list errors = this->db->get_errors(error_filters); + std::vector result; + std::transform(errors.begin(), errors.end(), std::back_inserter(result), [](Everest::error::ErrorPtr error) { + types::error_history::ErrorObject error_object; + error_object.uuid = error->uuid.to_string(); + error_object.timestamp = Everest::Date::to_rfc3339(error->timestamp); + std::string string_state = Everest::error::state_to_string(error->state); + error_object.state = types::error_history::string_to_state(string_state); + std::string string_severity = Everest::error::severity_to_string(error->severity); + error_object.severity = types::error_history::string_to_severity(string_severity); + error_object.type = error->type; + error_object.origin.module_id = error->from.module_id; + error_object.origin.implementation_id = error->from.implementation_id; + error_object.message = error->message; + error_object.description = error->description; + return error_object; + }); + return result; +} + +void error_historyImpl::handle_global_all_errors(const Everest::error::Error& error) { + Everest::error::ErrorPtr error_ptr = std::make_shared(error); + this->db->add_error(error_ptr); // LTODO check if error is already in db -> write test case +} + +void error_historyImpl::handle_global_all_errors_cleared(const Everest::error::Error& error) { + Everest::error::HandleFilter handle_filter(error.uuid); + Everest::error::ErrorFilter error_filter(handle_filter); + int edited_errors = + this->db + ->edit_errors({error_filter}, + [](Everest::error::ErrorPtr error) { error->state = Everest::error::State::ClearedByModule; }) + .size(); + if (edited_errors == 0) { + throw Everest::EverestBaseLogicError("ErrorHistory: Error with uuid " + error.uuid.to_string() + + " not found in database."); + } else if (edited_errors > 1) { + throw Everest::EverestBaseLogicError("ErrorHistory: Multiple errors with uuid " + error.uuid.to_string() + + " found in database."); + } +} + +} // namespace error_history +} // namespace module diff --git a/modules/ErrorHistory/error_history/error_historyImpl.hpp b/modules/ErrorHistory/error_history/error_historyImpl.hpp new file mode 100644 index 000000000..9c8bcca54 --- /dev/null +++ b/modules/ErrorHistory/error_history/error_historyImpl.hpp @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef ERROR_HISTORY_ERROR_HISTORY_IMPL_HPP +#define ERROR_HISTORY_ERROR_HISTORY_IMPL_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 3 +// + +#include + +#include "../ErrorHistory.hpp" + +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 +#include "../ErrorDatabaseSqlite.hpp" +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 + +namespace module { +namespace error_history { + +struct Conf { + std::string database_path; +}; + +class error_historyImpl : public error_historyImplBase { +public: + error_historyImpl() = delete; + error_historyImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : + error_historyImplBase(ev, "error_history"), mod(mod), config(config){}; + + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + // insert your public definitions here + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + +protected: + // command handler functions (virtual) + virtual std::vector + handle_get_errors(types::error_history::FilterArguments& filters) override; + + // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 + // insert your protected definitions here + // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 + +private: + const Everest::PtrContainer& mod; + const Conf& config; + + virtual void init() override; + virtual void ready() override; + + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 + // insert your private definitions here + void handle_global_all_errors(const Everest::error::Error& error); + void handle_global_all_errors_cleared(const Everest::error::Error& error); + + std::shared_ptr db; + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 +}; + +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 +// insert other definitions here +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 + +} // namespace error_history +} // namespace module + +#endif // ERROR_HISTORY_ERROR_HISTORY_IMPL_HPP diff --git a/modules/ErrorHistory/manifest.yaml b/modules/ErrorHistory/manifest.yaml new file mode 100644 index 000000000..3ab8ef89b --- /dev/null +++ b/modules/ErrorHistory/manifest.yaml @@ -0,0 +1,14 @@ +description: This module provides a persistent error history +provides: + error_history: + description: Error history + interface: error_history + config: + database_path: + type: string + description: Absolute path to the database file +enable_global_errors: true +metadata: + license: https://spdx.org/licenses/Apache-2.0.html + authors: + - Andreas Heinrich diff --git a/modules/ErrorHistory/tests/CMakeLists.txt b/modules/ErrorHistory/tests/CMakeLists.txt new file mode 100644 index 000000000..161d0923a --- /dev/null +++ b/modules/ErrorHistory/tests/CMakeLists.txt @@ -0,0 +1,29 @@ +set(TARGET_NAME ${PROJECT_NAME}_module_error_history_tests) +add_executable(${TARGET_NAME}) + +target_sources(${TARGET_NAME} + PRIVATE + error_database_sqlite_tests.cpp + ../ErrorDatabaseSqlite.cpp + helpers.cpp +) + +# target_include_directories(module_error_history_tests +# PRIVATE +# ) + +target_link_libraries(${TARGET_NAME} + PRIVATE + everest::framework + everest::log + SQLiteCpp + SQLite::SQLite3 + Catch2::Catch2WithMain +) +if(NOT DISABLE_EDM) + list(APPEND CMAKE_MODULE_PATH ${CPM_PACKAGE_catch2_SOURCE_DIR}/extras) + include(Catch) + catch_discover_tests(${TARGET_NAME}) +endif() + +add_test(${TARGET_NAME} ${TARGET_NAME}) diff --git a/modules/ErrorHistory/tests/error_database_sqlite_tests.cpp b/modules/ErrorHistory/tests/error_database_sqlite_tests.cpp new file mode 100644 index 000000000..756a64049 --- /dev/null +++ b/modules/ErrorHistory/tests/error_database_sqlite_tests.cpp @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include + +#include "../ErrorDatabaseSqlite.hpp" +#include "helpers.hpp" + +SCENARIO("Check ErrorDatabaseSqlite class", "[!throws]") { + GIVEN("An ErrorDatabaseSqlite object") { + const std::string bin_dir = get_bin_dir().string() + "/"; + const std::string db_name = get_unique_db_name(); + TestDatabase db(bin_dir + "/databases/" + db_name, true); + WHEN("Getting all errors") { + THEN("The database should be empty") { + auto errors = db.get_errors(std::list()); + REQUIRE(errors.empty()); + } + } + WHEN("Adding an error") { + std::vector test_errors = {std::make_shared( + "test_type", "test_message", "test_description", + ImplementationIdentifier("test_from_module", "test_from_implementation"), Everest::error::Severity::Low, + date::utc_clock::now(), Everest::error::UUID(), Everest::error::State::Active)}; + db.add_error(test_errors.at(0)); + THEN("The error should be in the database") { + check_expected_errors_in_list(test_errors, db.get_errors(std::list())); + } + } + WHEN("Adding multiple errors") { + std::vector test_errors = { + std::make_shared( + "test_type_a", "test_message_a", "test_description_a", + ImplementationIdentifier("test_from_module_a", "test_from_implementation_a"), + Everest::error::Severity::High, date::utc_clock::now(), Everest::error::UUID(), + Everest::error::State::ClearedByModule), + std::make_shared( + "test_type_b", "test_message_b", "test_description_b", + ImplementationIdentifier("test_from_module_b", "test_from_implementation_b"), + Everest::error::Severity::Medium, date::utc_clock::now(), Everest::error::UUID(), + Everest::error::State::ClearedByReboot)}; + for (Everest::error::ErrorPtr error : test_errors) { + db.add_error(error); + } + THEN("The errors should be in the database") { + auto errors = db.get_errors(std::list()); + check_expected_errors_in_list(test_errors, errors); + } + } + } + GIVEN("12 Errors in a connected ErrorDatabaseSqlite object") { + const std::string bin_dir = get_bin_dir().string() + "/"; + const std::string db_name = get_unique_db_name(); + TestDatabase db(bin_dir + "/databases/" + db_name, true); + std::vector test_errors = get_test_errors(); + for (Everest::error::ErrorPtr error : test_errors) { + db.add_error(error); + } + WHEN("Getting all errors") { + auto errors = db.get_errors(std::list()); + THEN("The result should contain all 12 errors") { + check_expected_errors_in_list(test_errors, errors); + } + } + WHEN("Getting all errors with StateFilter") { + auto errors = db.get_errors({Everest::error::ErrorFilter(Everest::error::StateFilter::Active)}); + THEN("The result should contain specific errors") { + std::vector expected_errors({test_errors[3], test_errors[4], test_errors[5]}); + check_expected_errors_in_list(expected_errors, errors); + } + } + WHEN("Getting all errors with OriginFilter") { + auto errors = db.get_errors({Everest::error::ErrorFilter( + Everest::error::OriginFilter("test_from_module_a", "test_from_implementation_a"))}); + THEN("The result should contain specific errors") { + std::vector expected_errors( + {test_errors[0], test_errors[2], test_errors[4], test_errors[6], test_errors[8], test_errors[10]}); + check_expected_errors_in_list(expected_errors, errors); + } + } + WHEN("Getting all errors with TypeFilter") { + auto errors = db.get_errors({Everest::error::ErrorFilter(Everest::error::TypeFilter("test_type_c"))}); + THEN("The result should contain specific errors") { + std::vector expected_errors( + {test_errors[2], test_errors[3], test_errors[4], test_errors[5], test_errors[9], test_errors[10]}); + check_expected_errors_in_list(expected_errors, errors); + } + } + WHEN("Getting all errors with SeverityFilter") { + auto errors = db.get_errors({Everest::error::ErrorFilter(Everest::error::SeverityFilter::MEDIUM_GE)}); + THEN("The result should contain specific errors") { + std::vector expected_errors({test_errors[4], test_errors[5], test_errors[6], + test_errors[7], test_errors[8], test_errors[9], + test_errors[10], test_errors[11]}); + check_expected_errors_in_list(expected_errors, errors); + } + } + WHEN("Getting all errors with TimePeriodFilter") { + auto errors = db.get_errors({Everest::error::ErrorFilter( + Everest::error::TimePeriodFilter{date::utc_clock::now() + std::chrono::minutes(150), + date::utc_clock::now() + std::chrono::minutes(270)})}); + THEN("The result should contain specific errors") { + std::vector expected_errors({test_errors[3], test_errors[4]}); + check_expected_errors_in_list(expected_errors, errors); + } + } + WHEN("Getting all errors with HandleFilter") { + auto errors = + db.get_errors({Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))}); + THEN("The result should contain specific errors") { + std::vector expected_errors({test_errors[4]}); + check_expected_errors_in_list(expected_errors, errors); + } + } + WHEN("Getting all errors with multiple filters") { + auto errors = db.get_errors({Everest::error::ErrorFilter(Everest::error::StateFilter::Active), + Everest::error::ErrorFilter(Everest::error::OriginFilter( + "test_from_module_a", "test_from_implementation_a"))}); + THEN("The result should contain specific errors") { + std::vector expected_errors({test_errors[4]}); + check_expected_errors_in_list(expected_errors, errors); + } + } + WHEN("Filtering all errors out") { + auto errors = db.get_errors({ + Everest::error::ErrorFilter(Everest::error::StateFilter::ClearedByModule), + Everest::error::ErrorFilter( + Everest::error::OriginFilter("test_from_module_a", "test_from_implementation_a")), + Everest::error::ErrorFilter(Everest::error::TypeFilter("test_type_c")), + Everest::error::ErrorFilter(Everest::error::SeverityFilter::HIGH_GE), + }); + THEN("The result should contain no errors") { + REQUIRE(errors.empty()); + } + } + + WHEN("Edit error type") { + std::list filters = { + Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))}; + Everest::error::ErrorDatabase::EditErrorFunc edit_func = [](Everest::error::ErrorPtr error) { + error->type = "new_type"; + }; + REQUIRE(db.get_errors(filters).size() > 0); + db.edit_errors(filters, edit_func); + THEN("The error should be edited") { + auto errors = db.get_errors(filters); + REQUIRE(errors.size() == 1); + REQUIRE(errors.front()->type == "new_type"); + } + } + WHEN("Edit error state") { + std::list filters = { + Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))}; + Everest::error::ErrorDatabase::EditErrorFunc edit_func = [](Everest::error::ErrorPtr error) { + error->state = Everest::error::State::ClearedByModule; + }; + REQUIRE(db.get_errors(filters).size() > 0); + db.edit_errors(filters, edit_func); + THEN("The error should be edited") { + auto errors = db.get_errors(filters); + REQUIRE(errors.size() == 1); + REQUIRE(errors.front()->state == Everest::error::State::ClearedByModule); + } + } + WHEN("Edit error severity") { + std::list filters = { + Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))}; + Everest::error::ErrorDatabase::EditErrorFunc edit_func = [](Everest::error::ErrorPtr error) { + error->severity = Everest::error::Severity::High; + }; + REQUIRE(db.get_errors(filters).size() > 0); + db.edit_errors(filters, edit_func); + THEN("The error should be edited") { + auto errors = db.get_errors(filters); + REQUIRE(errors.size() == 1); + REQUIRE(errors.front()->severity == Everest::error::Severity::High); + } + } + WHEN("Edit error message") { + std::list filters = { + Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))}; + Everest::error::ErrorDatabase::EditErrorFunc edit_func = [](Everest::error::ErrorPtr error) { + error->message = "new_message"; + }; + REQUIRE(db.get_errors(filters).size() > 0); + db.edit_errors(filters, edit_func); + THEN("The error should be edited") { + auto errors = db.get_errors(filters); + REQUIRE(errors.size() == 1); + REQUIRE(errors.front()->message == "new_message"); + } + } + WHEN("Edit error description") { + std::list filters = { + Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))}; + Everest::error::ErrorDatabase::EditErrorFunc edit_func = [](Everest::error::ErrorPtr error) { + error->description = "new_description"; + }; + REQUIRE(db.get_errors(filters).size() > 0); + db.edit_errors(filters, edit_func); + THEN("The error should be edited") { + auto errors = db.get_errors(filters); + REQUIRE(errors.size() == 1); + REQUIRE(errors.front()->description == "new_description"); + } + } + WHEN("Edit error origin") { + std::list filters = { + Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))}; + Everest::error::ErrorDatabase::EditErrorFunc edit_func = [](Everest::error::ErrorPtr error) { + error->from = ImplementationIdentifier("new_from_module", "new_from_implementation"); + }; + REQUIRE(db.get_errors(filters).size() > 0); + db.edit_errors(filters, edit_func); + THEN("The error should be edited") { + auto errors = db.get_errors(filters); + REQUIRE(errors.size() == 1); + REQUIRE(errors.front()->from == ImplementationIdentifier("new_from_module", "new_from_implementation")); + } + } + WHEN("Edit error timestamp") { + std::list filters = { + Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))}; + auto new_timestamp = date::utc_clock::now() + std::chrono::hours(10); + Everest::error::ErrorDatabase::EditErrorFunc edit_func = [&new_timestamp](Everest::error::ErrorPtr error) { + error->timestamp = new_timestamp; + }; + REQUIRE(db.get_errors(filters).size() > 0); + db.edit_errors(filters, edit_func); + THEN("The error should be edited") { + auto errors = db.get_errors(filters); + REQUIRE(errors.size() == 1); + REQUIRE(Everest::Date::to_rfc3339(errors.front()->timestamp) == + Everest::Date::to_rfc3339(new_timestamp)); + } + } + WHEN("Edit error uuid") { + Everest::error::UUID new_uuid; + std::list filters = { + Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))}; + Everest::error::ErrorDatabase::EditErrorFunc edit_func = [&new_uuid](Everest::error::ErrorPtr error) { + error->uuid = new_uuid; + }; + REQUIRE(db.get_errors(filters).size() > 0); + db.edit_errors(filters, edit_func); + THEN("The error should be edited") { + auto errors = db.get_errors(filters); + REQUIRE(errors.size() == 0); + errors = db.get_errors({Everest::error::ErrorFilter(Everest::error::HandleFilter(new_uuid))}); + REQUIRE(errors.size() == 1); + } + } + + WHEN("Remove error") { + std::list filters = { + Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))}; + REQUIRE(db.get_errors(filters).size() > 0); + db.remove_errors(filters); + THEN("The error should be removed") { + auto errors = db.get_errors(filters); + REQUIRE(errors.size() == 0); + } + } + WHEN("Remove multiple errors") { + std::list filters = { + Everest::error::ErrorFilter(Everest::error::StateFilter::Active), + Everest::error::ErrorFilter( + Everest::error::OriginFilter("test_from_module_c", "test_from_implementation_c"))}; + REQUIRE(db.get_errors(filters).size() > 0); + db.remove_errors(filters); + THEN("The errors should be removed") { + auto errors = db.get_errors(filters); + REQUIRE(errors.size() == 0); + } + } + WHEN("Remove all errors") { + std::list filters = {}; + REQUIRE(db.get_errors(filters).size() > 0); + db.remove_errors(filters); + THEN("The errors should be removed") { + auto errors = db.get_errors(filters); + REQUIRE(errors.size() == 0); + } + } + } +} diff --git a/modules/ErrorHistory/tests/helpers.cpp b/modules/ErrorHistory/tests/helpers.cpp new file mode 100644 index 000000000..02040e86f --- /dev/null +++ b/modules/ErrorHistory/tests/helpers.cpp @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include "helpers.hpp" + +#include +#include + +fs::path get_bin_dir() { + return fs::canonical("/proc/self/exe").parent_path(); +} + +std::string get_unique_db_name() { + return "error_database_sqlite_" + Everest::error::UUID().to_string() + ".db"; +} + +std::vector get_test_errors() { + return { + // index 0 + std::make_shared( + "test_type_a", "test_message_a", "test_description_a", + ImplementationIdentifier("test_from_module_a", "test_from_implementation_a"), Everest::error::Severity::Low, + date::utc_clock::now(), Everest::error::UUID(), Everest::error::State::ClearedByModule), + // index 1 + std::make_shared( + "test_type_b", "test_message_b", "test_description_b", + ImplementationIdentifier("test_from_module_b", "test_from_implementation_b"), Everest::error::Severity::Low, + date::utc_clock::now() + std::chrono::hours(1), Everest::error::UUID(), + Everest::error::State::ClearedByModule), + // index 2 + std::make_shared( + "test_type_c", "test_message_c", "test_description_c", + ImplementationIdentifier("test_from_module_a", "test_from_implementation_a"), Everest::error::Severity::Low, + date::utc_clock::now() + std::chrono::hours(2), Everest::error::UUID(), + Everest::error::State::ClearedByModule), + // index 3 + std::make_shared( + "test_type_c", "test_message_c", "test_description_c", + ImplementationIdentifier("test_from_module_c", "test_from_implementation_c"), Everest::error::Severity::Low, + date::utc_clock::now() + std::chrono::hours(3), Everest::error::UUID(), Everest::error::State::Active), + // index 4 + std::make_shared( + "test_type_c", "test_message_c", "test_description_c", + ImplementationIdentifier("test_from_module_a", "test_from_implementation_a"), + Everest::error::Severity::Medium, date::utc_clock::now() + std::chrono::hours(4), Everest::error::UUID(), + Everest::error::State::Active), + // index 5 + std::make_shared( + "test_type_c", "test_message_c", "test_description_c", + ImplementationIdentifier("test_from_module_c", "test_from_implementation_c"), + Everest::error::Severity::Medium, date::utc_clock::now() + std::chrono::hours(5), Everest::error::UUID(), + Everest::error::State::Active), + // index 6 + std::make_shared( + "test_type_a", "test_message_a", "test_description_a", + ImplementationIdentifier("test_from_module_a", "test_from_implementation_a"), + Everest::error::Severity::Medium, date::utc_clock::now() + std::chrono::hours(6), Everest::error::UUID(), + Everest::error::State::ClearedByReboot), + // index 7 + std::make_shared( + "test_type_a", "test_message_a", "test_description_a", + ImplementationIdentifier("test_from_module_c", "test_from_implementation_c"), + Everest::error::Severity::Medium, date::utc_clock::now() + std::chrono::hours(7), Everest::error::UUID(), + Everest::error::State::ClearedByReboot), + // index 8 + std::make_shared( + "test_type_a", "test_message_a", "test_description_a", + ImplementationIdentifier("test_from_module_a", "test_from_implementation_a"), + Everest::error::Severity::High, date::utc_clock::now() + std::chrono::hours(8), Everest::error::UUID(), + Everest::error::State::ClearedByReboot), + // index 9 + std::make_shared( + "test_type_c", "test_message_c", "test_description_c", + ImplementationIdentifier("test_from_module_c", "test_from_implementation_c"), + Everest::error::Severity::High, date::utc_clock::now() + std::chrono::hours(9), Everest::error::UUID(), + Everest::error::State::ClearedByReboot), + // index 10 + std::make_shared( + "test_type_c", "test_message_c", "test_description_c", + ImplementationIdentifier("test_from_module_a", "test_from_implementation_a"), + Everest::error::Severity::High, date::utc_clock::now() + std::chrono::hours(10), Everest::error::UUID(), + Everest::error::State::ClearedByReboot), + // index 11 + std::make_shared( + "test_type_b", "test_message_b", "test_description_b", + ImplementationIdentifier("test_from_module_c", "test_from_implementation_c"), + Everest::error::Severity::High, date::utc_clock::now() + std::chrono::hours(11), Everest::error::UUID(), + Everest::error::State::ClearedByReboot)}; +} + +void check_expected_errors_in_list(const std::vector& expected_errors, + const std::list& errors) { + REQUIRE(expected_errors.size() == errors.size()); + for (Everest::error::ErrorPtr exp_err : expected_errors) { + auto result = std::find_if(errors.begin(), errors.end(), [&exp_err](const Everest::error::ErrorPtr& err) { + return exp_err->uuid == err->uuid; + }); + REQUIRE(result != errors.end()); + REQUIRE((*result)->type == exp_err->type); + REQUIRE((*result)->message == exp_err->message); + REQUIRE((*result)->description == exp_err->description); + REQUIRE((*result)->from == exp_err->from); + REQUIRE((*result)->severity == exp_err->severity); + REQUIRE(Everest::Date::to_rfc3339((*result)->timestamp) == Everest::Date::to_rfc3339(exp_err->timestamp)); + REQUIRE((*result)->state == exp_err->state); + } +} + +TestDatabase::TestDatabase(const fs::path& db_path_, const bool reset_) : + db_path(db_path_), db(std::make_unique(db_path_, reset_)) { +} + +TestDatabase::~TestDatabase() { + fs::remove(db_path); +} + +void TestDatabase::add_error(Everest::error::ErrorPtr error) { + db->add_error(error); +} + +std::list +TestDatabase::get_errors(const std::list& filters) const { + return db->get_errors(filters); +} + +std::list TestDatabase::edit_errors(const std::list& filters, + Everest::error::ErrorDatabase::EditErrorFunc edit_func) { + return db->edit_errors(filters, edit_func); +} + +std::list TestDatabase::remove_errors(const std::list& filters) { + return db->remove_errors(filters); +} diff --git a/modules/ErrorHistory/tests/helpers.hpp b/modules/ErrorHistory/tests/helpers.hpp new file mode 100644 index 000000000..e674a74c7 --- /dev/null +++ b/modules/ErrorHistory/tests/helpers.hpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef ERROR_HISTORY_TESTS_HELPERS_HPP +#define ERROR_HISTORY_TESTS_HELPERS_HPP + +#include "../ErrorDatabaseSqlite.hpp" +#include +#include +#include + +namespace fs = std::filesystem; + +/// +/// \brief get the path to the binary directory +/// \return the path to the binary directory +/// +fs::path get_bin_dir(); + +/// +/// \brief get a unique database name +/// \return a unique database name +/// +std::string get_unique_db_name(); + +/// +/// \brief get a vector of test errors +/// \return a vector of test errors +/// +std::vector get_test_errors(); + +/// +/// \brief check if the given errors are equal +/// \param expected_errors the expected errors +/// \param errors the errors to check +/// +void check_expected_errors_in_list(const std::vector& expected_errors, + const std::list& errors); + +/// +/// \brief wrapper class for the ErrorDatabaseSqlite class +/// This class is used to test the ErrorDatabaseSqlite class +/// It proxies the ErrorDatabaseSqlite class, but +/// the destructor deletes the database file +/// +class TestDatabase { +public: + explicit TestDatabase(const fs::path& db_path_, const bool reset_ = false); + ~TestDatabase(); + void add_error(Everest::error::ErrorPtr error); + std::list get_errors(const std::list& filters) const; + std::list edit_errors(const std::list& filters, + Everest::error::ErrorDatabase::EditErrorFunc edit_func); + std::list remove_errors(const std::list& filters); + +private: + std::unique_ptr db; + const fs::path db_path; +}; + +#endif // ERROR_HISTORY_TESTS_HELPERS_HPP diff --git a/tests/conftest.py b/tests/conftest.py index 6cac93ee4..a37917370 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,7 +22,7 @@ def pytest_configure(config): pytest.everest_configs['ids'] = [] for config_path in everest_configs: config_id = config_path.stem - if config_id == 'config-sil-gen-pm' or config_id == 'config-test-cpp-error-handling': + if config_id == 'config-sil-gen-pm' or config_id.startswith('config-test-'): # skip continue pytest.everest_configs['params'].append(config_path) diff --git a/tests/core_tests/error_history_tests.py b/tests/core_tests/error_history_tests.py new file mode 100644 index 000000000..3a67a8248 --- /dev/null +++ b/tests/core_tests/error_history_tests.py @@ -0,0 +1,318 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +from copy import deepcopy +from typing import Dict +import uuid +import pytest +import asyncio + +from everest.testing.core_utils.fixtures import * +from everest.testing.core_utils.everest_core import EverestCore +from everest.testing.core_utils.probe_module import ProbeModule + +def assert_error(expected_error, error): + assert expected_error['uuid'] == error['uuid'] + assert expected_error['type'] == error['type'] + assert expected_error['message'] == error['message'] + assert expected_error['severity'] == error['severity'] + assert expected_error['state'] == error['state'] + assert expected_error['description'] == error['description'] + assert expected_error['origin']['module_id'] == error['origin']['module_id'] + assert expected_error['origin']['implementation_id'] == error['origin']['implementation_id'] + +def assert_errors(expected_errors, errors): + assert len(expected_errors) == len(errors) + for exp_err in expected_errors: + index = None + for (i, err) in zip(range(len(errors)), errors): + if exp_err['uuid'] == err['uuid']: + if index is not None: + assert False, f'Found multiple errors with uuid {exp_err["uuid"]}' + index = i + + assert index is not None + assert_error(exp_err, errors[index]) + +class ErrorHistoryModuleConfigurationStrategy(EverestConfigAdjustmentStrategy): + def __init__(self, db_name): + self.db_name = db_name + def adjust_everest_configuration(self, everest_config: Dict) -> Dict: + db_path = f'{self.db_name}.db' + adjusted_config = deepcopy(everest_config) + module_config = adjusted_config['active_modules']['error_history'] + module_config['config_implementation'] = { + 'error_history': { + 'database_path': db_path + } + } + return adjusted_config + +class UniqueDBNameStrategy(ErrorHistoryModuleConfigurationStrategy): + def __init__(self): + db_name = 'database_' + str(uuid.uuid4()) + super().__init__(db_name=db_name) + def adjust_everest_configuration(self, everest_config: Dict) -> Dict: + return super().adjust_everest_configuration(everest_config) + +class TestErrorHistory: + """ + Tests for the ErrorHistory module. + """ + + @pytest.mark.asyncio + @pytest.mark.everest_core_config('config-test-error-history-module.yaml') + @pytest.mark.everest_config_adaptions(UniqueDBNameStrategy()) + async def test_error_history(self, everest_core: EverestCore): + """ + Test the ErrorHistory module. + """ + probe_module = ProbeModule(everest_core.get_runtime_session()) + + everest_core.start(standalone_module='probe') + + call_args = { + 'filters': {} + } + result = await probe_module.call_command( + 'error_history', + 'get_errors', + call_args + ) + assert len(result) == 0 + + err_args = { + 'type': 'test_errors/TestErrorA', + 'message': 'Test Error A', + 'severity': 'Low' + } + uuid = await probe_module.call_command( + 'test_error_handling', + 'raise_error', + err_args + ) + expected_error = { + 'uuid': uuid, + 'type': err_args['type'], + 'message': err_args['message'], + 'severity': err_args['severity'], + 'state': 'Active', + 'description': 'Test error A', + 'origin': { + 'module_id': 'test_error_handling', + 'implementation_id': 'main' + } + } + await asyncio.sleep(0.5) + result = await probe_module.call_command( + 'error_history', + 'get_errors', + call_args + ) + assert len(result) == 1 + assert_error(expected_error, result[0]) + + await probe_module.call_command( + 'test_error_handling', + 'clear_error_by_uuid', + {'uuid': uuid} + ) + expected_error['state'] = 'ClearedByModule' + await asyncio.sleep(0.5) + result = await probe_module.call_command( + 'error_history', + 'get_errors', + call_args + ) + assert_error(expected_error, result[0]) + + @pytest.mark.asyncio + @pytest.mark.everest_core_config('config-test-error-history-module.yaml') + @pytest.mark.everest_config_adaptions(UniqueDBNameStrategy()) + async def test_error_history_filter(self, everest_core: EverestCore): + """ + Test the ErrorHistory module with filters. + """ + probe_module = ProbeModule(everest_core.get_runtime_session()) + everest_core.start(standalone_module='probe') + # insert 12 errors + test_errors = [ + # index 0 + { + 'uuid': None, + 'type': 'test_errors/TestErrorA', + 'message': 'Test Error A', + 'severity': 'Low', + 'description': 'Test error A', + 'state': 'Active', + 'origin': { + 'module_id': 'test_error_handling', + 'implementation_id': 'main' + } + }, + # index 1 + { + 'uuid': None, + 'type': 'test_errors/TestErrorB', + 'message': 'Test Error B', + 'severity': 'Low', + 'description': 'Test error B', + 'state': 'Active', + 'origin': { + 'module_id': 'test_error_handling', + 'implementation_id': 'main' + } + }, + # index 2 + { + 'uuid': None, + 'type': 'test_errors/TestErrorA', + 'message': 'Test Error A', + 'severity': 'High', + 'description': 'Test error A', + 'state': 'Active', + 'origin': { + 'module_id': 'test_error_handling', + 'implementation_id': 'main' + } + }, + # index 3 + { + 'uuid': None, + 'type': 'test_errors/TestErrorB', + 'message': 'Test Error B', + 'severity': 'High', + 'description': 'Test error B', + 'state': 'Active', + 'origin': { + 'module_id': 'test_error_handling', + 'implementation_id': 'main' + } + }, + # index 4 + { + 'uuid': None, + 'type': 'test_errors/TestErrorA', + 'message': 'Test Error A', + 'severity': 'Medium', + 'description': 'Test error A', + 'state': 'Active', + 'origin': { + 'module_id': 'test_error_handling', + 'implementation_id': 'main' + } + }, + # index 5 + { + 'uuid': None, + 'type': 'test_errors/TestErrorB', + 'message': 'Test Error B', + 'severity': 'Medium', + 'description': 'Test error B', + 'state': 'Active', + 'origin': { + 'module_id': 'test_error_handling', + 'implementation_id': 'main' + } + } + ] + for err in test_errors: + err_args = { + 'type': err['type'], + 'message': err['message'], + 'severity': err['severity'] + } + err['uuid'] = await probe_module.call_command( + 'test_error_handling', + 'raise_error', + err_args + ) + await asyncio.sleep(0.5) + + # get all errors + call_args = { + 'filters': {} + } + result = await probe_module.call_command( + 'error_history', + 'get_errors', + call_args + ) + assert_errors(test_errors, result) + + # get all errors with type TestErrorA + call_args['filters'] = { + 'type_filter': 'test_errors/TestErrorA' + } + result = await probe_module.call_command( + 'error_history', + 'get_errors', + call_args + ) + assert_errors([test_errors[0], test_errors[2], test_errors[4]], result) + + # get all errors from module test_error_handling + call_args['filters'] = { + 'origin_filter': { + 'module_id': 'test_error_handling', + 'implementation_id': 'main' + } + } + result = await probe_module.call_command( + 'error_history', + 'get_errors', + call_args + ) + assert_errors(test_errors, result) + + # get all errors with severity Low + call_args['filters'] = { + 'severity_filter': 'HIGH_GE' + } + result = await probe_module.call_command( + 'error_history', + 'get_errors', + call_args + ) + assert_errors([test_errors[2], test_errors[3]], result) + + # get error by uuid + call_args['filters'] = { + 'handle_filter': test_errors[0]['uuid'] + } + result = await probe_module.call_command( + 'error_history', + 'get_errors', + call_args + ) + assert_errors([test_errors[0]], result) + + # get all 'Active' errors + call_args['filters'] = { + 'state_filter': 'Active' + } + result = await probe_module.call_command( + 'error_history', + 'get_errors', + call_args + ) + assert_errors(test_errors, result) + + for err in test_errors[:3]: + await probe_module.call_command( + 'test_error_handling', + 'clear_error_by_uuid', + {'uuid': err['uuid']} + ) + err['state'] = 'ClearedByModule' + await asyncio.sleep(0.5) + + call_args['filters'] = { + 'state_filter': 'ClearedByModule' + } + result = await probe_module.call_command( + 'error_history', + 'get_errors', + call_args + ) + assert_errors(test_errors[:3], result) diff --git a/tests/everest-core_tests/config/config-test-error-history-module.yaml b/tests/everest-core_tests/config/config-test-error-history-module.yaml new file mode 100644 index 000000000..e18c496c3 --- /dev/null +++ b/tests/everest-core_tests/config/config-test-error-history-module.yaml @@ -0,0 +1,18 @@ +active_modules: + error_history: + module: ErrorHistory + test_error_handling: + module: TestErrorHandling + connections: + error_raiser: + - module_id: probe + implementation_id: main + probe: + module: ProbeModule + connections: + test_error_handling: + - module_id: test_error_handling + implementation_id: main + error_history: + - module_id: error_history + implementation_id: error_history diff --git a/tests/everest-core_tests/modules/TestErrorHandling/main/test_error_handlingImpl.cpp b/tests/everest-core_tests/modules/TestErrorHandling/main/test_error_handlingImpl.cpp index ef8287a79..f05bdccd6 100644 --- a/tests/everest-core_tests/modules/TestErrorHandling/main/test_error_handlingImpl.cpp +++ b/tests/everest-core_tests/modules/TestErrorHandling/main/test_error_handlingImpl.cpp @@ -4,6 +4,7 @@ #include "test_error_handlingImpl.hpp" #include +#include #include namespace module { diff --git a/types/error_history.yaml b/types/error_history.yaml new file mode 100644 index 000000000..801a54ce5 --- /dev/null +++ b/types/error_history.yaml @@ -0,0 +1,103 @@ +description: This file contains the types for the error history interface +types: + ImplementationIdentifier: + description: Identifier of an implementation + type: object + required: + - module_id + - implementation_id + properties: + module_id: + type: string + minLength: 2 + implementation_id: + type: string + minLength: 2 + State: + description: State of an error + type: string + enum: + - Active + - ClearedByModule + - ClearedByReboot + SeverityFilter: + description: Severity filter for errors + type: string + enum: + - HIGH_GE + - MEDIUM_GE + - LOW_GE + TimeperiodFilter: + description: Timeperiod filter for errors + type: object + required: + - timestamp_from + - timestamp_to + properties: + timestamp_from: + type: string + format: date-time + timestamp_to: + type: string + format: date-time + FilterArguments: + description: Arguments for the get_errors command + type: object + required: [] + properties: + state_filter: + $ref: /error_history#/State + origin_filter: + $ref: /error_history#/ImplementationIdentifier + type_filter: + type: string + severity_filter: + $ref: /error_history#/SeverityFilter + timeperiod_filter: + $ref: /error_history#/TimeperiodFilter + handle_filter: + type: string + description: Handle of an error + Severity: + description: Severity of an error + type: string + enum: + - High + - Medium + - Low + ErrorObject: + description: Represents an error + type: object + required: + - type + - description + - message + - origin + - timestamp + - uuid + - severity + - state + properties: + type: + type: string + minLength: 2 + description: + type: string + minLength: 2 + message: + type: string + minLength: 2 + severity: + $ref: /error_history#/Severity + origin: + $ref: /error_history#/ImplementationIdentifier + timestamp: + type: string + format: date-time + uuid: + type: string + minLength: 2 + state: + $ref: /error_history#/State + additionalProperties: false +