diff --git a/mw/log/BUILD b/mw/log/BUILD new file mode 100644 index 0000000..5556bb4 --- /dev/null +++ b/mw/log/BUILD @@ -0,0 +1,259 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//platform/aas/mw:common_features.bzl", "COMPILER_WARNING_FEATURES") +load("//platform/aas/tools/itf/bazel_gen:itf_gen.bzl", "py_unittest_qnx_test") + +cc_library( + name = "log", + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = ["//visibility:public"], + deps = [ + ":frontend", + "//platform/aas/mw/log/detail:recorder_factory", + ], +) + +cc_library( + name = "recorder_interface", + srcs = ["irecorder_factory.cpp"], + hdrs = ["irecorder_factory.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log/detail:__subpackages__", + ], + deps = [":recorder"], +) + +# TODO: make use of implementation_deps and private header files +cc_library( + name = "frontend", + srcs = [ + "log_stream_factory.cpp", + "logger.cpp", + "logger_container.cpp", + "logging.cpp", + "runtime.cpp", + ], + hdrs = [ + "log_stream_factory.h", + "logger.h", + "logger_container.h", + "logging.h", + "runtime.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/ara/log:__subpackages__", + "//platform/aas/lib/memory/shared:__subpackages__", # Needed to break cyclic dependency () + "//platform/aas/mw/log/detail:__subpackages__", + "//platform/aas/mw/log/legacy_non_verbose_api:__pkg__", + "//platform/aas/mw/log/test:__subpackages__", + "//platform/aas/mw/log/test/sctf_example:__subpackages__", + ], + deps = [ + ":log_stream", + ":recorder", + ":recorder_interface", + ":shared_types", + "//platform/aas/lib/result", + "//platform/aas/mw/log/detail:circular_allocator", + "//platform/aas/mw/log/detail:thread_local_guard", + "//platform/aas/mw/log/detail/wait_free_stack", + "@amp", + ] + select({ + "@platforms//os:qnx": [ + "//platform/aas/lib/os/qnx:channel", + "//platform/aas/lib/os/qnx:dispatch", + "//platform/aas/lib/os/qnx:iofunc", + ], + "//conditions:default": [], + }), +) + +cc_library( + name = "shared_types", + srcs = [ + "log_common.cpp", + "log_level.cpp", + "slot_handle.cpp", + ], + hdrs = [ + "log_common.h", + "log_level.h", + "log_mode.h", + "log_types.h", + "slot_handle.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/ara/log:__subpackages__", + "//platform/aas/mw/log/configuration:__pkg__", + "//platform/aas/mw/log/detail:__pkg__", + "//platform/aas/mw/log/detail/data_router:__pkg__", + "//platform/aas/mw/log/detail/file_logging:__pkg__", + "//platform/aas/mw/log/detail/slog:__pkg__", + "//platform/aas/test/pas/logging/utils:__pkg__", + ], + deps = ["@amp"], +) + +cc_library( + name = "recorder", + srcs = ["recorder.cpp"], + hdrs = ["recorder.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log/detail:__pkg__", + "//platform/aas/mw/log/detail/data_router:__pkg__", + "//platform/aas/mw/log/detail/file_logging:__pkg__", + "//platform/aas/mw/log/detail/slog:__pkg__", + ], + deps = [ + ":shared_types", + "@amp", + ], +) + +cc_library( + name = "recorder_mock", + testonly = True, + hdrs = ["recorder_mock.h"], + features = COMPILER_WARNING_FEATURES, + visibility = [ + "//application/adp/aas/logging:__pkg__", + "//ecu/domains/cdc/cdc_wasm:__subpackages__", + "//platform/aas/ara/AasDiagProvider:__subpackages__", + "//platform/aas/ara/log/test:__pkg__", + "//platform/aas/lib/os/test:__pkg__", + "//platform/aas/lib/result:__pkg__", + "//platform/aas/mw/log/detail:__pkg__", + "//platform/aas/os/secpol/secpollog:__pkg__", + "//platform/aas/pas/monitoring/hmon/code/app:__pkg__", + "//platform/aas/sysfunc/ConfigDaemon/code/plugins/calibration:__subpackages__", + "//platform/aas/sysfunc/ISOCMinion/code:__subpackages__", + "//platform/aas/sysfunc/SoftwareIdProvider:__subpackages__", + "//platform/aas/sysfunc/SoftwareUpdate:__subpackages__", + "//platform/aas/sysfunc/common:__subpackages__", + ], + deps = [ + ":recorder", + "//third_party/googletest", + ], +) + +cc_library( + name = "log_stream", + srcs = ["log_stream.cpp"], + hdrs = ["log_stream.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/lib/os:__pkg__", + "//platform/aas/lib/result:__pkg__", + "//platform/aas/mw/log/detail:__pkg__", + "//platform/aas/mw/log/test/my_custom_lib:__pkg__", + "//platform/aas/mw/log/test/sample_ara_core_logging:__pkg__", + ], + deps = [ + ":recorder", + ":shared_types", + "//platform/aas/mw/log/detail:logging_identifier", + "//platform/aas/mw/log/detail:thread_local_guard", + "@amp", + ], +) + +TEST_DEPS = [ + ":frontend", + ":log_stream", + ":recorder_mock", + "//platform/aas/mw/log/detail:backend_mock", + "//platform/aas/mw/log/detail:recorder_factory", + "//platform/aas/mw/log/test/my_custom_lib:my_custom_type_mw_log", + "//platform/aas/mw/log/test/sample_ara_core_logging:sample_ara_core_logging", + "//third_party/googletest:main", +] + +cc_test( + name = "unit_test", + srcs = [ + ":log_level_test.cpp", + ":log_stream_test.cpp", + ":log_types_test.cpp", + ":logger_container_test.cpp", + ":logger_test.cpp", + ":logging_test.cpp", + ":runtime_test.cpp", + ":slot_handle_test.cpp", + ], + features = COMPILER_WARNING_FEATURES + [ + "aborts_upon_exception", + ], + tags = ["unit"], + deps = TEST_DEPS, +) + +cc_test( + name = "runtime_test_without_pointer_initialization", + srcs = [ + "runtime_test_without_pointer_initialization.cpp", + ], + features = COMPILER_WARNING_FEATURES + [ + "aborts_upon_exception", + ], + tags = ["unit"], + deps = TEST_DEPS, +) + +test_suite( + name = "unit_tests", + tests = [ + ":runtime_test_without_pointer_initialization", + ":unit_test", + "//platform/aas/mw/log/configuration:unit_tests", + "//platform/aas/mw/log/detail:unit_tests", + "//platform/aas/mw/log/legacy_non_verbose_api:unit_tests", + ], + visibility = ["//platform/aas/mw:__pkg__"], +) + +py_unittest_qnx_test( + name = "pkg_unit_tests_qnx", + data_files = [ + "//platform/aas/mw/log/configuration:tests_config_files", + ], + test_cases = [ + ":unit_test", + ":runtime_test_without_pointer_initialization", + ], + visibility = ["//platform/aas:__subpackages__"], +) + +py_unittest_qnx_test( + name = "unit_tests_qnx", + tags = ["manual"], + test_suites = [ + ":pkg_unit_tests_qnx", + "//platform/aas/mw/log/configuration:unit_tests_qnx", + "//platform/aas/mw/log/detail:unit_tests_qnx", + "//platform/aas/mw/log/legacy_non_verbose_api:unit_tests_qnx", + "//platform/aas/mw/log/test:serializer_unit_tests_qnx", + ], + visibility = ["//platform/aas/mw:__subpackages__"], +) diff --git a/mw/log/README.md b/mw/log/README.md new file mode 100644 index 0000000..34dfccd --- /dev/null +++ b/mw/log/README.md @@ -0,0 +1,653 @@ + + + + +# Table of Contents + +[Introduction](#introduction) + +[Requirements](#requirements) + +[Motivation](#motivation) + +[Detailed Design](#detailed-design) + +[Assumptions of Use](#assumptions-of-use-aous) + +[Limitations of Use](#limitations-of-use) + +[Configuration](#configuration) + +[Logging modes](#logging-modes) + +[Usage](#usage) + +[LogHex and LogBin](#loghex-and-logbin) + +[Overload mw::log::LogStream with custom user types](#overload-mwloglogstream-with-custom-user-types) + +## Introduction + +The `mw::log` library enables logging for the users. Throughout its lifetime, +any application may log messages by calling the `mw::log` API. For example: + +```c++ +bmw::mw::log::LogFatal() << "Hello World!!!"; +bmw::mw::log::LogError() << bmw::mw::log::LogHex32{1470258}; +``` + +Depending on the selected logging mode, the log traces are written into the +terminal or a respective `.dlt` file. See the [details below](#logging-modes). + +## Requirements + +The requirements are maintained in [Codebeamer](), and the high-level architecture is illustrated in the [Magic Draw diagram](:27112/collaborator/document/4ff2a028-8ac7-4f7a-b42e-dd90816fec81?viewId=a0683e3f-a47e-4269-9000-255abd14110a&viewType=model§ionId=977f79a2-d84f-4932-9190-1e520cc3696b) + +## Motivation + +You can find more information about the history and motivation for mw::log library in the [Motivation](). + +With `mw::log` deterministic memory management is ensured in the logging context. +I.e. memory is only allocated within the initialization (phase). This is achieved +by usage of pre-allocated buffers. + +The benefits of using `mw::log` are: + +* Gives developers the full control over logging code. +* Resolves dependencies to the AUTOSAR `ara::log` interfaces. +* Reduces memory demands of logging (considerably) as compared to its predecessor + `ara::log`. + +## Detailed Design + +The Detailed Design for the `mw::log` implementation can be found under the [design]() directory. + +## Assumptions of use (AoUs) + +The AoUs are maintained in [Codebeamer](/cb/share/f55e57bf89054000a975502375922db5) + +## Limitations of use + +Logging is a best effort operation that shall ensure freedom from interference at all times. +Log messages may be dropped if the available resources or the limits of the implementation are exceeded. + +## Configuration + +The configuration of `mw::log` is done via `.json` files. + +* **ecu_logging_config.json** - one file per ECU, +* the environment config file indicated by the `MW_LOG_CONFIG_FILE` environmental variable. +* **logging.json** - one file per application. Besides that there is the one default +logging.json which is used when the particular file for the given app is missing. + +When the configuration can be stored only under the user-defined path, this path can be indicated in +the `MW_LOG_CONFIG_FILE` environmental variable. If it is defined, the application specific configuration +will be omitted. + +The user shall consider configurable logging parameters in `ecu_logging_config.json` +file and check whether those values need to be overridden by `logging.json` for +their specific app needs. + +The default path for the config file should be: `/opt/$APP_NAME/etc/logging.json`. +Where $APP_NAME is a placeholder for the respective application's name. +If the syntax or semantics of `logging.json` is incorrect, the default are read +and applied. + +The following attributes are read from configuration files: + +* **ecuId** -- 4-character identifier for the ECU +* **appId** -- 4-character identifier for the application +* **appDesc** -- description, not very important +* **logMode** -- default value: `LogMode::kRemote`; note: multiple modes can be +defined simultaneously. Hence, e.g. logging to the file and console is possible +in parallel +* **logFilePath** -- used for file logging, if ```logMode``` includes +```kFile```, this is the directory, where the logfile ```appId.dlt``` will be +put +* **logLevel** -- default value: LogLevel::kWarn, global log level threshold +for the application +* **logLevelThresholdConsole** -- if ```logMode``` includes ```kConsole```, +console message will be filtered by this log level. Note, the global +setting/configuration, however, has priority +* **contextConfigs** -- it's a map which may contain different log levels for +different contexts. This overrides the global log level threshold defined above +for the specified context. +* **stackBufferSize** -- ssize of the linear buffer used for storing type +information (metadata). If your application sends a lot of different datatypes, +you might consider increasing this limit +* **ringBufferSize** -- size of ringbuffer for data sent to datarouter. All +verbose and non-verbose DLT messages along with CDC land in the ringbuffer, so +please consider increasing this one if you lose messages +* **overwriteOnFull** -- defines the ringbuffer write strategy, default +(=`true`) is to overwrite when the buffer is full; alternatively +(=`false`) the messages would just be dropped, the oldest unread messages +are left intact (useful to identify the scenarios of ringbuffer overflow, for +example) +* **numberOfSlots** -- default value: 8, the name refers to the implementation +detail of `mw::log`, which is based on CircularBuffer with the given amount of +slots. Thus the amount of the memory used for logs storage is dependent on this +number. Less slots require fewer memory, but the likelihood of logs being +dropped increases. +NOTE: Although this is configurable, the maximum number of slots is limited +based on the underlying recorder/backend implmentation. +* **slotSizeBytes** -- default value: 2048, the size of a single slot, which +may affect memory usage. If the slot is too short, the message is truncated. + +## Logging modes + +```c++ +enum class LogMode : uint8_t +{ + kRemote = 0x01, ///< Sent remotely + kFile = 0x02, ///< Save to file + kConsole = 0x04, ///< Forward to console, + kSystem = 0x08, ///< QNX: forward to slog, + kInvalid = 0xff ///< Invalid log mode, +}; +``` + +The above code presents the modes of the logging: + +* **kRemote** -- the logs are sent remotely via network by DLT protocol. +* **kFile** -- the logs are written to the file `.dlt` located + on a target. +* **kConsole** -- the logs are written into the terminal. +* **kSystem** -- the logs are written into the QNX slogger2. +* **kInvalid** -- self-explanatory - no logging then. + +## Usage + +If one wants to use `mw::log`, the following include shall be necessary: + +```c++ +#include "platform/aas/mw/log/logging.h" +``` + +And the following bazel dependency as well: + +```bazel +deps = [ + "//platform/aas/mw/log:log", +], +``` + +### How to log something the most easy way? + +``` +#include "platform/aas/mw/log/logging.h" + +bmw::mw::LogError() << "Your custom message"; +``` + +### How to log something under a specific context? + +``` +#include "platform/aas/mw/log/Logger.h" + +bmw::mw::Logger my_context{"MCTX"}; +my_context.LogError() << "My custom message"; +``` + +or + +``` +#include "platform/aas/mw/log/logging.h" + +bmw::mw::LogError("MCTX") << "My Message"; +``` + +### How to log my custom type? + +Refer section [logging custom type](#overload-mwloglogstream-with-custom-user-types). + +### How to ignore logging in Unit-Testing + +You can easily inject your custom recorder implementation. For most unit tests you just want to ignore the log messages. +For this we provide an empty recorder. + +``` +bmw::mw::log::EmptyRecorder my_recorder{}; +bmw::mw::test::SetLogRecorder(my_recorder); +``` + +For more advanced usage like logging custom type, go to the section of +[custom types](#overload-mwloglogstream-with-custom-user-types). + +Before listing the possible log calls, here are possible log levels listed: + +```c++ +enum class LogLevel : std::uint8_t +{ + kOff = 0x00, + kFatal = 0x01, + kError = 0x02, + kWarn = 0x03, + kInfo = 0x04, + kDebug = 0x05, + kVerbose = 0x06 +}; +``` + +Above log levels are easily matched to the direct log calls: + +```c++ +LogStream LogFatal() noexcept; +LogStream LogError() noexcept; +LogStream LogWarn() noexcept; +LogStream LogInfo() noexcept; +LogStream LogDebug() noexcept; +LogStream LogVerbose() noexcept; +LogStream LogFatal(const amp::string_view context_id) noexcept; +LogStream LogError(const amp::string_view context_id) noexcept; +LogStream LogWarn(const amp::string_view context_id) noexcept; +LogStream LogInfo(const amp::string_view context_id) noexcept; +LogStream LogDebug(const amp::string_view context_id) noexcept; +LogStream LogVerbose(const amp::string_view context_id) noexcept; +``` + +When `context_id` is not passed, the default value is: `"DFLT"`. + +There are plenty of types which can be logged with `mw::log` using `operator <<` +(to preserve the similarity to standard C++ ostream usage): + +```c++ +LogStream& operator<<(const bool) const noexcept; +LogStream& operator<<(const std::int8_t) const noexcept; +LogStream& operator<<(const std::int16_t) const noexcept; +LogStream& operator<<(const std::int32_t) const noexcept; +LogStream& operator<<(const std::int64_t) const noexcept; +LogStream& operator<<(const std::uint8_t) const noexcept; +LogStream& operator<<(const std::uint16_t) const noexcept; +LogStream& operator<<(const std::uint32_t) const noexcept; +LogStream& operator<<(const std::uint64_t) const noexcept; +LogStream& operator<<(const float) const noexcept; +LogStream& operator<<(const double) const noexcept; +LogStream& operator<<(const amp::string_view) const noexcept; +LogStream& operator<<(const std::string&) const noexcept; +LogStream& operator<<(const bmw::StringLiteral) const noexcept; +LogStream& operator<<(const LogHex8& value) const noexcept; +LogStream& operator<<(const LogHex16& value) const noexcept; +LogStream& operator<<(const LogHex32& value) const noexcept; +LogStream& operator<<(const LogHex64& value) const noexcept; +LogStream& operator<<(const LogBin8& value) const noexcept; +LogStream& operator<<(const LogBin16& value) const noexcept; +LogStream& operator<<(const LogBin32& value) const noexcept; +LogStream& operator<<(const LogBin64& value) const noexcept; +LogStream& operator<<(const LogRawBuffer& value) const noexcept; +LogStream& operator<<(const LogSlog2Message& value) noexcept; +``` + +`LogHex*` family enables logging numbers in hexadecimal form, while `LogBin*` +family enables logging number in binary form. +`LogRawBuffer` is a span over characters. +`LogSlog2Message` provides means to forward qnx specific slog2 code to the slog backend. This is advantageous for clients that intend to take additional actions based on the code. E.g.: Security clients like secpolllog can use this code to indicate severity/type of security failures and take necessary actions (setting DTCs) accordingly. +NOTE: By default the slog2 code is set to zero if not set explicitly. + +User shall register an empty recodrder in case of log messages wanted to be +ignored in unit tests. + +```c++ +bmw::mw::log::EmptyRecorder my_recorder{}; +bmw::mw::test::SetLogRecorder(my_recorder); +``` + +It's preferred to link your app directly with `mw/log` no need for fakes. + +### LogHex and LogBin + +It is very useful feature to log numbers in hexadecimal or binary representation. +What is important is that `LogHex*` and `LogBin*` structures stores **unsigned** +integers with the appropriate size, thus `LogHex8` stores `uint8_t`, `LogHex16` +stores `uint16_t` and so on. +The examples of usage from the code: + +```c++ +const bmw::mw::log::LogHex8 log_hex_8{10}; +bmw::mw::log::LogInfo() << "log_hex_8: " << log_hex_8; +``` + +And it produces the output: + +```bash +log_hex_8: 0x0a +``` + +```c++ +const bmw::mw::log::LogBin16 log_bin_16{9'012}; +bmw::mw::log::LogInfo() << "log_bin_16: " << log_bin_16; +``` + +And it produces the output: + +```bash +log_bin_16: 0b0010 0011 0011 0100 +``` + +### DLT-formatted payload + +Verbose DLT-Logging is _not_ just a sequence of characters. In fact, different types are encoded differently. +This is described in the [DLT-Protocol (PRS_Dlt_00625)](https://www.autosar.org/fileadmin/user_upload/standards/foundation/1-0/AUTOSAR_PRS_DiagnosticLogAndTraceProtocol.pdf) +. + +To give an example. + +``` +Logging() << "ABC" << 0xFF +``` + +would end up as something like + +``` +StringIdentifier|0x41|0x42|0x43|Uint8Identifier|0xFF +``` + +This format reduces memory usage especially for long-numbers. + +### Datarouter backend configuration guidelines +For this particular configuration all logging clients are relying on daemon process called Datarouter. Messages are passed to particular system process with a use of shared memory file. This architectual decision is impacting characteristic of communication and messages loss. To avoid problems with logs being dropped consider adjusting **ringBufferSize** to be twice as much as the highest amount of bytes logging application may produce in any 100ms period or limit logging content and frequency. + +The rule of thumb for calculating buffer size is: +``` +ring_buffer_size >= 2 * logging_volume_100ms +``` +where **logging_volume_100ms** is size in bytes of the logging volume produced by application at any given period of 100ms. +Note, however, that other limitation like total bandwith of network or processing power availability apply. For example for logging data bandwidth of around 30Mb/s is reserved on the mPAD platform. + +Dropped log messages are monitored and reported by Datarouter Statistics module: [link](/swh/ddad_platform/tree/master/aas/pas/logging#statistics) + +#### Datarouter security policy (secpol) configuration +For the [message exchange between Logging Clients and the Datarouter](/swh/ddad_platform/tree/master/aas/mw/log/design/datarouter_backend#message-exchange-between-logging-clients-and-datarouter), it is crucial that the secpol for the Datarouter (datarouter.secpol) contains the secpol type of the client process in its channel connect configuration. Otherwise, the Datarouter will be unable to establish a connection with the client, and consequently, it cannot forward the logs to remote. + +``` +allow datarouter_t { + ClientApp_t // Example secpol type for ClientApp +}:channel connect; +``` + +## mw::log::LogStream overloads + +The goal is to enable users to define their own types and to log them the same +way as all types listed with `operator<<` in section [Usage](#usage). + +The main assumption is to limit the dependencies to the minimum amount. Thus +it's not required to depend on the whole `mw::log`, since the only needed library +is `log_stream`. + +**Important:** It is mandatory to add unit tests for each custom overload. + +### Overload mw::log::LogStream with custom user types + +The example implementation can be viewed [here](/swh/ddad_platform/tree/master/aas/mw/log/test/my_custom_lib). + +The key sections of this implementation are going to be described in this document. + +The primary assumption is to **have separate library** for each custom user type. +Thus the first step is to create separate directory. For the purpose of presenting +an example there was a directory created inside `mw/log` test directory, e.g.: +`aas/mw/log/test/my_custom_lib` and create `BUILD` file there, e.g.: + +```bazel +cc_library( + name = "my_custom_type_mw_log", + ... + srcs = [ + "my_custom_type_mw_log.cpp", + ], + hdrs = [ + "my_custom_type.h", + "my_custom_type_mw_log.h", + ], + ... + deps = [ + "//platform/aas/mw/log:log_stream", + ], +) +``` + +In `my_custome_type.h` there is user type defined: + +```c++ +#include + +namespace my +{ +namespace custom +{ +namespace type +{ + +struct MyCustomType +{ + std::int32_t int_field; + std::string string_field; +}; + +} // namespace type +} // namespace custom +} // namespace my +``` + +The content of C++ files are declaration and definition of `operator<<` needed +for logging, e.g. header `my_custom_type_mw_log.h`: + +```c++ +#include "platform/aas/mw/log/log_stream.h" +#include "platform/aas/mw/log/test/my_custom_lib/my_custom_type.h" + +namespace my +{ +namespace custom +{ +namespace type +{ + +bmw::mw::log::LogStream& operator<<(bmw::mw::log::LogStream& log_stream, + const my::custom::type::MyCustomType& my_custom_type) noexcept; + +} // namespace type +} // namespace custom +} // namespace my +``` + +and source file `my_custom_type_mw_log.cpp`: + +```c++ +#include "my_custom_type_mw_log.h" + +namespace my +{ +namespace custom +{ +namespace type +{ + +bmw::mw::log::LogStream& operator<<(bmw::mw::log::LogStream& log_stream, + const my::custom::type::MyCustomType& my_custom_type) noexcept +{ + log_stream << "my_custom_type: int_field : " << my_custom_type.int_field + << " , string_field : " << my_custom_type.string_field; + return log_stream; +} + +} // namespace type +} // namespace custom +} // namespace my +``` + +For `LogStream` usage there is the dependency in `BUILD` file necessary: + +```bazel +deps = [ + "//platform/aas/mw/log:log_stream", +], +``` + +And also `visibility` for `log_stream` needs to be added: + +```bazel +cc_library( + name = "log_stream", + ... + visibility = [ + ... + "//platform/aas/mw/log/my_custom_lib:__pkg__", + ], +``` + +It's not a big deal to extend `ara::log::LogStream` with the custom user type +the same way as `bmw::mw::log::LogStream`. What is needed: + +**1.** Create the library for `ara::log` in `BUILD` file: + +```bazel +cc_library( + name = "my_custom_type_ara_log", + ... + srcs = [ + "my_custom_type_ara_log.cpp", + ], + hdrs = [ + "my_custom_type.h", + "my_custom_type_ara_log.h", + ], + ... + deps = [ + "//platform/aas/ara/log", + ], +) +], +``` + +**2.** Add the overload in header file: + +```c++ +#include "platform/aas/ara/log/inc/ara/log/logstream.h" +#include "platform/aas/mw/log/test/my_custom_lib/my_custom_type.h" + +namespace my +{ +namespace custom +{ +namespace type +{ + +ara::log::LogStream& operator<<(ara::log::LogStream& log_stream, + const my::custom::type::MyCustomType& my_custom_type) noexcept; + +} // namespace type +} // namespace custom +} // namespace my +``` + +**3.** Add the implementation to the source file: + +```c++ +#include "my_custom_type_ara_log.h" + +namespace my +{ +namespace custom +{ +namespace type +{ + +ara::log::LogStream& operator<<(ara::log::LogStream& log_stream, + const my::custom::type::MyCustomType& my_custom_type) noexcept +{ + log_stream << "my_custom_type: int_field : " << my_custom_type.int_field + << " , string_field : " << my_custom_type.string_field; + return log_stream; +} + +} // namespace type +} // namespace custom +} // namespace my +``` + +#### mw::log::LogStream overloading for ara::core::Result + +The example implementation can be viewed [here](/swh/ddad_platform/tree/master/aas/mw/log/test/sample_ara_core_logging). + +The main point to consider is **ADL (Argument-dependent lookup)**. This rule requires that operator overloads and the data types used in the overload are declared in the same namespace. + +To log `ara::core` types, a (new) directory containing a bazel file `/path/to/directory/BUILD` should be created. Then the following steps have to be considered.``` + +**1.** In order to use `bmw::mw::log::LogStream` in the new bazel target the `visibility` parameter in `//platform/aas/mw/log/BUILD` has to updated: + +```c++ +cc_library( + name = "log_stream", + ... + visibility = [ + ... + "//path/to/sample:__pkg__", + ], +``` + +Normally, similar visibility updates would be needed within the target of the type to be logged. Since `ara::core` types have public visibility, however, no further action is required. + +**2.** Update the dependencies of the new `BUILD` file to include `log_stream` and `ara::core` such as: + +```c++ +cc_library( + name = "sample_ara_core_logging", + testonly = True, + hdrs = [ + "sample_ara_core_logging.h", + ], + features = [ + ... + ], + visibility = [ + ... + ], + deps = [ + "//platform/aas/ara/core", + "//platform/aas/mw/log:log_stream", + ], +) +``` + +**3.** Provide a custom meaningful implementation, e.g. in `sample_ara_core_logging.h` + +```c++ +#include "platform/aas/mw/log/log_stream.h" +#include + +namespace ara +{ +namespace core +{ + +template +bmw::mw::log::LogStream& operator<<(bmw::mw::log::LogStream& log_stream, const ara::core::Result& result) noexcept +{ + if (result.HasValue()) + { + log_stream << "Result value: " << result.Value(); + } + else + { + log_stream << "Error message: " << result.Error().UserMessage(); + } + + return log_stream; +} + +} // namespace core +} // namespace ara +``` + +In case the `Result` holds a value, the `operator<<` is applied to this value. This requires an appropriate overload is provided for the given type of `T`. + +Overloads of `mw::log::LogStream` for other `ara::core` could be provided congruently. diff --git a/mw/log/configuration/BUILD b/mw/log/configuration/BUILD new file mode 100644 index 0000000..852250d --- /dev/null +++ b/mw/log/configuration/BUILD @@ -0,0 +1,246 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//platform/aas/mw:common_features.bzl", "COMPILER_WARNING_FEATURES") +load("//platform/aas/tools/itf/bazel_gen:itf_gen.bzl", "py_unittest_qnx_test") + +cc_library( + name = "configuration", + srcs = [ + "configuration.cpp", + ], + hdrs = [ + "configuration.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log:__subpackages__", + "//platform/aas/pas/logging:__pkg__", + ], + deps = [ + "//platform/aas/mw/log:shared_types", + "//platform/aas/mw/log/detail:logging_identifier", + ], +) + +cc_library( + name = "configuration_interface", + srcs = [ + "iconfiguration_file_discoverer.cpp", + "itarget_config_reader.cpp", + ], + hdrs = [ + "iconfiguration_file_discoverer.h", + "itarget_config_reader.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log/detail:__pkg__", + ], + deps = [ + ":configuration", + "//platform/aas/lib/result", + ], +) + +cc_library( + name = "configuration_parser", + srcs = [ + "configuration_file_discoverer.cpp", + "target_config_reader.cpp", + ], + hdrs = [ + "configuration_file_discoverer.h", + "target_config_reader.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log/detail:__pkg__", + ], + deps = [ + ":configuration", + ":configuration_interface", + "//platform/aas/lib/json:json_parser", + "//platform/aas/lib/memory:split_string_view", + "//platform/aas/lib/os:stdlib", + "//platform/aas/lib/os:unistd", + "//platform/aas/lib/os/utils:path", + "//platform/aas/mw/log/detail:initialization_reporter", + "//platform/aas/mw/log/detail:types_and_errors", + ], +) + +cc_library( + name = "target_config_reader_mock", + testonly = True, + srcs = ["target_config_reader_mock.cpp"], + hdrs = ["target_config_reader_mock.h"], + features = COMPILER_WARNING_FEATURES, + visibility = [ + "//platform/aas/mw/log:__pkg__", + "//platform/aas/mw/log/detail:__pkg__", + "//platform/aas/mw/log/detail/data_router:__pkg__", + "//platform/aas/mw/log/detail/slog:__pkg__", + ], + deps = [ + ":configuration_interface", + "//third_party/googletest", + ], +) + +cc_library( + name = "configuration_file_discoverer_mock", + testonly = True, + srcs = ["configuration_file_discoverer_mock.cpp"], + hdrs = ["configuration_file_discoverer_mock.h"], + features = COMPILER_WARNING_FEATURES, + visibility = [ + "//platform/aas/mw/log/detail:__pkg__", + ], + deps = [ + ":configuration_interface", + "//third_party/googletest", + ], +) + +filegroup( + name = "tests_config_files", + srcs = [ + "test/data/app_config.json", + "test/data/ecu_config.json", + "test/data/error-json-structure.json", + "test/data/invalid_app_config.json", + "test/data/syntax_error.json", + "test/data/wrong-context-config-value.json", + "test/data/wrong-loglevel-value.json", + ], + visibility = ["//platform/aas/mw/log:__pkg__"], +) + +cc_test( + name = "unit_test", + srcs = [ + "configuration_file_discoverer_test.cpp", + "configuration_test.cpp", + "target_config_reader_test.cpp", + ], + data = [ + ":tests_config_files", + ], + features = COMPILER_WARNING_FEATURES + [ + "aborts_upon_exception", + ], + tags = ["unit"], + deps = [ + ":configuration_file_discoverer_mock", + ":configuration_parser", + ":target_config_reader_mock", + "//platform/aas/lib/os/mocklib:fcntl_mock", + "//platform/aas/lib/os/mocklib:stdlib_mock", + "//platform/aas/lib/os/mocklib:unistd_mock", + "//platform/aas/lib/os/utils/mocklib:path_mock", + "//platform/aas/mw/log/test/console_logging_environment", + "//third_party/googletest:main", + ], +) + +cc_library( + name = "nvconfig", + srcs = [ + "nvconfig.cpp", + ], + hdrs = [ + "nvconfig.h", + "nvmsgdescriptor.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log:__subpackages__", + "//platform/aas/pas/logging:__subpackages__", + ], + deps = [ + "//platform/aas/lib/json:json_parser", + "//platform/aas/mw/log/detail:logging_identifier", + "@amp", + ], +) + +cc_test( + name = "nvconfig_test", + srcs = [ + "nvconfig_test.cpp", + ], + data = [ + "test/data/empty-class-id.json", + "test/data/empty-json-class-id.json", + "test/data/error-content-1-json-class-id.json", + "test/data/error-content-2-json-class-id.json", + "test/data/error-content-3-json-class-id.json", + "test/data/error-content-wrong-id-value.json", + "test/data/error-parse-1-json-class-id.json", + "test/data/error-parse-empty-json-object.json", + "test/data/second-test-class-id.json", + "test/data/test-class-id.json", + ], + features = COMPILER_WARNING_FEATURES + [ + "aborts_upon_exception", + ], + tags = ["unit"], + deps = [ + ":nvconfig", + "//third_party/googletest:main", + ], +) + +filegroup( + name = "nvconfig_test_config_files", + srcs = [ + "test/data/empty-class-id.json", + "test/data/empty-json-class-id.json", + "test/data/error-content-1-json-class-id.json", + "test/data/error-content-2-json-class-id.json", + "test/data/error-content-3-json-class-id.json", + "test/data/error-content-wrong-id-value.json", + "test/data/error-parse-1-json-class-id.json", + "test/data/error-parse-empty-json-object.json", + "test/data/second-test-class-id.json", + "test/data/test-class-id.json", + ], + visibility = ["//platform/aas/mw/log:__pkg__"], +) + +test_suite( + name = "unit_tests", + tests = [ + ":nvconfig_test", + ":unit_test", + ], + visibility = ["//platform/aas/mw/log:__pkg__"], +) + +py_unittest_qnx_test( + name = "unit_tests_qnx", + data_files = [ + ":tests_config_files", + ":nvconfig_test_config_files", + ], + test_cases = [ + ":unit_test", + ":nvconfig_test", + ], + visibility = ["//platform/aas/mw:__subpackages__"], +) diff --git a/mw/log/configuration/configuration.cpp b/mw/log/configuration/configuration.cpp new file mode 100644 index 0000000..548a118 --- /dev/null +++ b/mw/log/configuration/configuration.cpp @@ -0,0 +1,219 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/configuration/configuration.h" + +#include "platform/aas/mw/log/detail/logging_identifier.h" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +namespace +{ + +std::string ToString(const amp::string_view view) +{ + return {view.data(), view.size()}; +} + +} // namespace + + +amp::string_view Configuration::GetEcuId() const noexcept + +{ + return ecu_id_.GetStringView(); +} + +void Configuration::SetEcuId(const amp::string_view ecu_id) noexcept +{ + ecu_id_ = LoggingIdentifier{ecu_id}; +} + +amp::string_view Configuration::GetAppId() const noexcept +{ + return app_id_.GetStringView(); +} + +void Configuration::SetAppId(const amp::string_view app_id) noexcept +{ + app_id_ = LoggingIdentifier{app_id}; +} + +amp::string_view Configuration::GetAppDescription() const noexcept +{ + return app_description_; +} + +void Configuration::SetAppDescription(const amp::string_view app_description) noexcept +{ + app_description_ = ToString(app_description); +} + +const std::unordered_set& Configuration::GetLogMode() const noexcept +{ + return log_mode_; +} + +void Configuration::SetLogMode(const std::unordered_set& log_mode) noexcept +{ + log_mode_ = log_mode; +} + +amp::string_view Configuration::GetLogFilePath() const noexcept +{ + return log_file_path_; +} + +void Configuration::SetLogFilePath(const amp::string_view log_file_path) noexcept +{ + log_file_path_ = ToString(log_file_path); +} + +LogLevel Configuration::GetDefaultLogLevel() const noexcept +{ + return default_log_level_; +} + +void Configuration::SetDefaultLogLevel(const LogLevel default_log_level) noexcept +{ + default_log_level_ = default_log_level; +} + +LogLevel Configuration::GetDefaultConsoleLogLevel() const noexcept +{ + return default_console_log_level_; +} + +void Configuration::SetDefaultConsoleLogLevel(const LogLevel default_console_log_level) noexcept +{ + default_console_log_level_ = default_console_log_level; +} + +const ContextLogLevelMap& Configuration::GetContextLogLevel() const noexcept +{ + return context_log_level_; +} + +void Configuration::SetContextLogLevel(const ContextLogLevelMap& context_log_level) noexcept +{ + context_log_level_ = context_log_level; +} + +std::size_t Configuration::GetStackBufferSize() const noexcept +{ + return stack_buffer_size_; +} + +void Configuration::SetStackBufferSize(const std::size_t stack_buffer_size) noexcept +{ + stack_buffer_size_ = stack_buffer_size; +} + +std::size_t Configuration::GetRingBufferSize() const noexcept +{ + return ring_buffer_size_; +} + +void Configuration::SetRingBufferSize(const std::size_t ring_buffer_size) noexcept +{ + ring_buffer_size_ = ring_buffer_size; +} + +bool Configuration::GetRingBufferOverwriteOnFull() const noexcept +{ + return ring_buffer_overwrite_on_full_; +} + +void Configuration::SetRingBufferOverwriteOnFull(const bool ring_buffer_overwrite_on_full) noexcept +{ + ring_buffer_overwrite_on_full_ = ring_buffer_overwrite_on_full; +} + +std::size_t Configuration::GetNumberOfSlots() const noexcept +{ + return number_of_slots_; +} + +void Configuration::SetNumberOfSlots(const std::size_t number_of_slots) noexcept +{ + number_of_slots_ = number_of_slots; +} + +std::size_t Configuration::GetSlotSizeInBytes() const noexcept +{ + return slot_size_bytes_; +} + +void Configuration::SetSlotSizeInBytes(const std::size_t slot_size_bytes) noexcept +{ + slot_size_bytes_ = slot_size_bytes; +} + +bool Configuration::IsLogLevelEnabled(const LogLevel& log_level, + const amp::string_view context, + const bool check_for_console) const noexcept +{ + auto max_log_level = LogLevel::kOff; + + const auto context_log_level_result = context_log_level_.find(LoggingIdentifier{context}); + const auto log_level_is_in_context = context_log_level_result != context_log_level_.cend(); + if (log_level_is_in_context) + { + max_log_level = context_log_level_result->second; + } + else if (check_for_console == false) + { + max_log_level = default_log_level_; + } + else + { + max_log_level = default_console_log_level_; + } + + return log_level <= max_log_level; +} + +void Configuration::SetDataRouterUid(const std::size_t uid) noexcept +{ + data_router_uid_ = uid; +} + +std::size_t Configuration::GetDataRouterUid() const noexcept +{ + return data_router_uid_; +} + +bool Configuration::GetDynamicDatarouterIdentifiers() const noexcept +{ + return dynamic_datarouter_identifiers_; +} + +void Configuration::SetDynamicDatarouterIdentifiers(const bool enable_dynamic_identifiers) noexcept +{ + dynamic_datarouter_identifiers_ = enable_dynamic_identifiers; +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/configuration/configuration.h b/mw/log/configuration/configuration.h new file mode 100644 index 0000000..9679dbf --- /dev/null +++ b/mw/log/configuration/configuration.h @@ -0,0 +1,158 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_CONFIGURATION_H +#define PLATFORM_AAS_MW_LOG_DETAIL_CONFIGURATION_H + +#include "platform/aas/mw/log/detail/logging_identifier.h" +#include "platform/aas/mw/log/log_level.h" +#include "platform/aas/mw/log/log_mode.h" + +#include "amp_string_view.hpp" + +#include +#include +#include + + +namespace bmw + +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +using ContextLogLevelMap = std::unordered_map; + +class Configuration final +{ + public: + Configuration() = default; + + amp::string_view GetEcuId() const noexcept; + void SetEcuId(const amp::string_view) noexcept; + + amp::string_view GetAppId() const noexcept; + void SetAppId(const amp::string_view) noexcept; + + amp::string_view GetAppDescription() const noexcept; + void SetAppDescription(const amp::string_view) noexcept; + + const std::unordered_set& GetLogMode() const noexcept; + void SetLogMode(const std::unordered_set&) noexcept; + + amp::string_view GetLogFilePath() const noexcept; + void SetLogFilePath(const amp::string_view) noexcept; + + LogLevel GetDefaultLogLevel() const noexcept; + void SetDefaultLogLevel(const LogLevel) noexcept; + + LogLevel GetDefaultConsoleLogLevel() const noexcept; + void SetDefaultConsoleLogLevel(const LogLevel) noexcept; + + const ContextLogLevelMap& GetContextLogLevel() const noexcept; + void SetContextLogLevel(const ContextLogLevelMap&) noexcept; + + std::size_t GetStackBufferSize() const noexcept; + void SetStackBufferSize(const std::size_t stack_buffer_size) noexcept; + + std::size_t GetRingBufferSize() const noexcept; + void SetRingBufferSize(const std::size_t ring_buffer_size) noexcept; + + bool GetRingBufferOverwriteOnFull() const noexcept; + void SetRingBufferOverwriteOnFull(const bool) noexcept; + + std::size_t GetNumberOfSlots() const noexcept; + void SetNumberOfSlots(const std::size_t number_of_slots) noexcept; + + std::size_t GetSlotSizeInBytes() const noexcept; + void SetSlotSizeInBytes(const std::size_t slot_size_bytes) noexcept; + + void SetDataRouterUid(const std::size_t uid) noexcept; + std::size_t GetDataRouterUid() const noexcept; + + bool GetDynamicDatarouterIdentifiers() const noexcept; + void SetDynamicDatarouterIdentifiers(const bool enable_dynamic_identifiers) noexcept; + + /// \brief Returns true if the log level is enabled for the context. + /// \param use_console_default_level Set to true if threshold for console logging should be considered as default + /// log level. Otherwise default_log_level_ will be used instead. + /// \details Uses the threshold from context_log_level map if the map contains the context, otherwise consider the + /// default threshold. + bool IsLogLevelEnabled(const LogLevel& log_level, + const amp::string_view context, + const bool check_for_console = false) const noexcept; + + + private: + + + + /// \brief DLT ECU ID, four bytes max. + LoggingIdentifier ecu_id_{"ECU1"}; + + /// \brief DLT ECU ID, four bytes max. + LoggingIdentifier app_id_{"NONE"}; + + + /// \brief Short description of the application. + std::string app_description_{""}; + + /// \brief Active logging backends/sinks. + std::unordered_set log_mode_{LogMode::kRemote}; + + /// \brief Directory path used for file logging. + std::string log_file_path_{"/tmp"}; + + /// \brief Default log maximum log level. + LogLevel default_log_level_{LogLevel::kWarn}; + + /// \brief Default log maximum log level for the console. + LogLevel default_console_log_level_{LogLevel::kWarn}; + + /// \brief Maximum log level per context. + ContextLogLevelMap context_log_level_{}; + + /// \brief Stack buffer size used for libtracing. + std::size_t stack_buffer_size_{65536}; + + /// \brief Ring buffer size used for libtracing. + std::size_t ring_buffer_size_{2097152}; + + /// \brief Overwrite ring buffer on full for libtracing. + bool ring_buffer_overwrite_on_full_{true}; + + /// \brief Number of Slots for the Datarouter backend. + std::size_t number_of_slots_{8}; + + /// \brief Slot size of each Slot in the Datarouter backend. + std::size_t slot_size_bytes_{2048}; + + /// \brief uid of data router. + std::size_t data_router_uid_{1038}; + + /// \brief Toggle between dynamic datarouter identifiers. + bool dynamic_datarouter_identifiers_{false}; + +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_DLT_FORMAT_H diff --git a/mw/log/configuration/configuration_file_discoverer.cpp b/mw/log/configuration/configuration_file_discoverer.cpp new file mode 100644 index 0000000..50619c8 --- /dev/null +++ b/mw/log/configuration/configuration_file_discoverer.cpp @@ -0,0 +1,148 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/configuration/configuration_file_discoverer.h" + +#include "amp_span.hpp" +#include "amp_string_view.hpp" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +namespace +{ + + + + +static const std::string kGlobalConfigPath = "/etc/ecu_logging_config.json"; +static const std::string kLocalEtcConfigPath = "etc/logging.json"; +static constexpr char kCwdConfigPath[] = "logging.json"; +static const amp::span kEnvironmentVariableConfig = "MW_LOG_CONFIG_FILE"; + + + + +} // namespace + +ConfigurationFileDiscoverer::ConfigurationFileDiscoverer(amp::pmr::unique_ptr&& path, + amp::pmr::unique_ptr&& stdlib, + amp::pmr::unique_ptr&& unistd) + : IConfigurationFileDiscoverer{}, path_(std::move(path)), stdlib_(std::move(stdlib)), unistd_(std::move(unistd)) +{ +} + +std::vector ConfigurationFileDiscoverer::FindConfigurationFiles() const noexcept +{ + std::vector existing_config_files; + + const auto global_file = GetGlobalConfigFile(); + if (global_file.has_value()) + { + existing_config_files.push_back(global_file.value()); + } + + const auto env_file = FindEnvironmentConfigFile(); + if (env_file.has_value()) + { + existing_config_files.push_back(env_file.value()); + } + else + { + const auto local_file = FindLocalConfigFile(); + if (local_file.has_value()) + { + existing_config_files.push_back(local_file.value()); + } + } + + return existing_config_files; +} + +amp::optional ConfigurationFileDiscoverer::GetGlobalConfigFile() const noexcept +{ + if (FileExists(kGlobalConfigPath)) + { + return kGlobalConfigPath; + } + return {}; +} + +/// \brief Return true if the file with the given path exists. +/// Yes, a similiar utility already exists in lib/filesystem, but we cannot use it here since lib/filesystem is using +/// logging. +bool ConfigurationFileDiscoverer::FileExists(const std::string& path) const noexcept +{ + return unistd_->access(path.c_str(), bmw::os::Unistd::AccessMode::kExists).has_value(); +} + +amp::optional ConfigurationFileDiscoverer::FindLocalConfigFile() const noexcept +{ + const std::vector> candidates{ + {GetConfigFileByExecutableLocation(), kLocalEtcConfigPath, kCwdConfigPath}}; + + const auto result = std::find_if(candidates.cbegin(), candidates.cend(), [this](const auto& candidate) { + return (candidate.has_value() == true) && (FileExists(candidate.value()) == true); + }); + + + if (result != candidates.cend()) + { + return *result; + } + + return {}; + +} + + +amp::optional ConfigurationFileDiscoverer::FindEnvironmentConfigFile() const noexcept +{ + const auto environmental_config_path = stdlib_->getenv(kEnvironmentVariableConfig.data()); + if (environmental_config_path != nullptr) + { + if (FileExists(environmental_config_path) == true) + { + return environmental_config_path; + } + } + + return {}; +} + + +amp::optional ConfigurationFileDiscoverer::GetConfigFileByExecutableLocation() const noexcept +{ + const auto exec_path = path_->get_exec_path(); + if (exec_path.has_value()) + { + const auto bin_path = path_->get_parent_dir(exec_path.value()); + const auto app_path = path_->get_parent_dir(bin_path); + return app_path + "/" + kLocalEtcConfigPath; + } + return {}; +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/configuration/configuration_file_discoverer.h b/mw/log/configuration/configuration_file_discoverer.h new file mode 100644 index 0000000..da860b8 --- /dev/null +++ b/mw/log/configuration/configuration_file_discoverer.h @@ -0,0 +1,63 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_CONFIG_FILE_DISCOVERER_H +#define PLATFORM_AAS_MW_LOG_DETAIL_CONFIG_FILE_DISCOVERER_H + +#include "platform/aas/mw/log/configuration/iconfiguration_file_discoverer.h" + +#include + +#include "amp_optional.hpp" + +#include "platform/aas/lib/os/stdlib.h" +#include "platform/aas/lib/os/unistd.h" +#include "platform/aas/lib/os/utils/path.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +class ConfigurationFileDiscoverer final : public IConfigurationFileDiscoverer +{ + public: + ConfigurationFileDiscoverer(amp::pmr::unique_ptr&& path, + amp::pmr::unique_ptr&& stdlib, + amp::pmr::unique_ptr&& unistd); + + std::vector FindConfigurationFiles() const noexcept final override; + + private: + amp::optional GetGlobalConfigFile() const noexcept; + bool FileExists(const std::string& path) const noexcept; + amp::optional FindLocalConfigFile() const noexcept; + amp::optional FindEnvironmentConfigFile() const noexcept; + amp::optional GetConfigFileByExecutableLocation() const noexcept; + + amp::pmr::unique_ptr path_; + amp::pmr::unique_ptr stdlib_; + amp::pmr::unique_ptr unistd_; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_ICONFIG_FILE_DISCOVERER_H diff --git a/mw/log/configuration/configuration_file_discoverer_mock.cpp b/mw/log/configuration/configuration_file_discoverer_mock.cpp new file mode 100644 index 0000000..af52efa --- /dev/null +++ b/mw/log/configuration/configuration_file_discoverer_mock.cpp @@ -0,0 +1,15 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/configuration/configuration_file_discoverer_mock.h" diff --git a/mw/log/configuration/configuration_file_discoverer_mock.h b/mw/log/configuration/configuration_file_discoverer_mock.h new file mode 100644 index 0000000..53bfed1 --- /dev/null +++ b/mw/log/configuration/configuration_file_discoverer_mock.h @@ -0,0 +1,48 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_CONFIG_FILE_DISCOVERER_MOCK_H +#define PLATFORM_AAS_MW_LOG_DETAIL_CONFIG_FILE_DISCOVERER_MOCK_H + +#include "platform/aas/mw/log/configuration/iconfiguration_file_discoverer.h" + +#include "gmock/gmock.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +class ConfigurationFileDiscovererMock final : public IConfigurationFileDiscoverer +{ + public: + + /* Not actual for mock class; Internal GTest Framework code caused the violation; */ + + /* Not actual for mock class; Internal GTest Framework code caused the violation; */ + MOCK_METHOD((std::vector), FindConfigurationFiles, (), (const, noexcept, override)); + + +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_ICONFIG_FILE_DISCOVERER_H diff --git a/mw/log/configuration/configuration_file_discoverer_test.cpp b/mw/log/configuration/configuration_file_discoverer_test.cpp new file mode 100644 index 0000000..0e0b232 --- /dev/null +++ b/mw/log/configuration/configuration_file_discoverer_test.cpp @@ -0,0 +1,327 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/configuration/configuration_file_discoverer.h" +#include "platform/aas/lib/os/libgen.h" +#include "platform/aas/lib/os/mocklib/stdlib_mock.h" +#include "platform/aas/lib/os/mocklib/unistdmock.h" +#include "platform/aas/lib/os/utils/mocklib/pathmock.h" + +#include "gtest/gtest.h" + +using testing::_; + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +const std::string kGlobalConfigFile{"/etc/ecu_logging_config.json"}; +const std::string kLocalConfigFileInPwdEtc{"etc/logging.json"}; +const std::string kLocalConfigFileInPwd{"logging.json"}; +const std::string kLocalConfigFileInExecPath{"/opt/app/etc/logging.json"}; +const std::string kExecPath{"/opt/app/bin/app"}; +const bmw::os::Error kError{bmw::os::Error::createFromErrno(0)}; +static const char* kEnvConfigFilePath = "/persistent/app/logging.json"; + +class ConfigurationFileDiscovererFixture : public ::testing::Test +{ + public: + void SetUp() override + { + ON_CALL(*unistd_mock_, access(_, _)) + .WillByDefault([this](const char* pathname, bmw::os::Unistd::AccessMode mode) { + return this->OnAccessCall(pathname, mode); + }); + + ON_CALL(*path_mock_, get_exec_path()).WillByDefault([this]() { return this->OnExecPathCall(); }); + + ON_CALL(*path_mock_, get_parent_dir(_)) + .WillByDefault([libgen_mock_ = libgen_mock_pmr_.get()](const std::string& path) { + std::vector path_mutable{path.begin(), path.end()}; + path_mutable.push_back('\0'); + return std::string{libgen_mock_->dirname(path_mutable.data())}; + }); + + ON_CALL(*stdlib_mock_, getenv(_)).WillByDefault(::testing::Return(const_cast(kEnvConfigFilePath))); + } + + void TearDown() override {} + + amp::expected_blank OnAccessCall(const char* pathname, bmw::os::Unistd::AccessMode mode) + { + amp::expected_blank result{amp::make_unexpected(kError)}; + + if (mode != bmw::os::Unistd::AccessMode::kExists) + { + return result; + } + + if (std::find(existing_files_.begin(), existing_files_.end(), std::string{pathname}) != existing_files_.end()) + { + result = amp::expected_blank{}; + } + + return result; + } + + amp::expected OnExecPathCall() + { + amp::expected result{amp::make_unexpected(kError)}; + + if (!exec_path_shall_fail_) + { + result = kExecPath; + } + + return result; + } + + void AddExistingFile(std::string path) { existing_files_.push_back(path); } + + void SetExecPathShallFail(bool shall_fail = true) { exec_path_shall_fail_ = shall_fail; } + + amp::pmr::memory_resource* memory_resource_ = amp::pmr::get_default_resource(); + + amp::pmr::unique_ptr libgen_mock_pmr_ = os::Libgen::Default(memory_resource_); + + amp::pmr::unique_ptr unistd_mock_pmr = amp::pmr::make_unique(memory_resource_); + amp::pmr::unique_ptr path_mock_pmr = amp::pmr::make_unique(memory_resource_); + amp::pmr::unique_ptr stdlib_mock_pmr = amp::pmr::make_unique(memory_resource_); + + os::UnistdMock* unistd_mock_ = unistd_mock_pmr.get(); + os::PathMock* path_mock_ = path_mock_pmr.get(); + os::StdlibMock* stdlib_mock_ = stdlib_mock_pmr.get(); + + amp::pmr::unique_ptr unistd_mock_pmr_ = std::move(unistd_mock_pmr); + amp::pmr::unique_ptr path_mock_pmr_ = std::move(path_mock_pmr); + amp::pmr::unique_ptr stdlib_mock_pmr_ = std::move(stdlib_mock_pmr); + + ConfigurationFileDiscoverer discoverer_{std::move(path_mock_pmr_), + std::move(stdlib_mock_pmr_), + std::move(unistd_mock_pmr_)}; + std::vector existing_files_; + bool exec_path_shall_fail_{}; +}; + +TEST_F(ConfigurationFileDiscovererFixture, DiscovererShallFindGlobalConfigurationFile) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies that the discoverer shall find the global configuration file."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // When ... + AddExistingFile(kGlobalConfigFile); + + // ... then ... + const auto result = discoverer_.FindConfigurationFiles(); + EXPECT_EQ(result.size(), 1); + EXPECT_EQ(result.at(0), kGlobalConfigFile); +} + +TEST_F(ConfigurationFileDiscovererFixture, DiscovererShallFindConfigurationFileInCwdEtc) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "Verifies that the discoverer shall find the application specific configuration file under /etc."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // When ... + AddExistingFile(kGlobalConfigFile); + AddExistingFile(kLocalConfigFileInPwdEtc); + AddExistingFile(kLocalConfigFileInPwd); + + // ... then ... + const auto result = discoverer_.FindConfigurationFiles(); + EXPECT_EQ(result.size(), 2); + EXPECT_EQ(result.at(0), kGlobalConfigFile); + EXPECT_EQ(result.at(1), kLocalConfigFileInPwdEtc); +} + +TEST_F(ConfigurationFileDiscovererFixture, DiscovererShallFindConfigurationFileInCwd) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies that the discoverer shall find the application specific configuration file under ."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // When ... + AddExistingFile(kGlobalConfigFile); + AddExistingFile(kLocalConfigFileInPwd); + + // ... then ... + const auto result = discoverer_.FindConfigurationFiles(); + EXPECT_EQ(result.size(), 2); + EXPECT_EQ(result.at(0), kGlobalConfigFile); + EXPECT_EQ(result.at(1), kLocalConfigFileInPwd); +} + +TEST_F(ConfigurationFileDiscovererFixture, DiscovererShallFindConfigurationFileInExecPath) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "Verifies that the discoverer shall find the application specific configuration file under the binary path."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // When ... + AddExistingFile(kGlobalConfigFile); + AddExistingFile(kLocalConfigFileInExecPath); + AddExistingFile(kLocalConfigFileInPwdEtc); + AddExistingFile(kLocalConfigFileInPwd); + + // ... then ... + const auto result = discoverer_.FindConfigurationFiles(); + EXPECT_EQ(result.size(), 2); + EXPECT_EQ(result.at(0), kGlobalConfigFile); + EXPECT_EQ(result.at(1), kLocalConfigFileInExecPath); +} + +TEST_F(ConfigurationFileDiscovererFixture, DiscovererShallFindConfigurationFileInEnvPath) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies that the discoverer shall find the application specific configuration file under the " + "environment variable path."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // When ... + AddExistingFile(kGlobalConfigFile); + AddExistingFile(kEnvConfigFilePath); + + // ... then ... + const auto result = discoverer_.FindConfigurationFiles(); + EXPECT_EQ(result.size(), 2); + EXPECT_EQ(result.at(0), kGlobalConfigFile); + EXPECT_EQ(result.at(1), kEnvConfigFilePath); +} + +TEST_F(ConfigurationFileDiscovererFixture, DiscovererShallFindConfigurationFileInEnvPathOverrideCwdEtc) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies that the discoverer shall find the application specific configuration file under the " + "environment variable path. The /etc. path should be ignored"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // When ... + AddExistingFile(kGlobalConfigFile); + AddExistingFile(kLocalConfigFileInPwdEtc); + AddExistingFile(kEnvConfigFilePath); + + // ... then ... + const auto result = discoverer_.FindConfigurationFiles(); + EXPECT_EQ(result.size(), 2); + EXPECT_EQ(result.at(0), kGlobalConfigFile); + EXPECT_EQ(result.at(1), kEnvConfigFilePath); +} + +TEST_F(ConfigurationFileDiscovererFixture, DiscovererShallFindConfigurationFileInEnvPathOverrideCwd) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies that the discoverer shall find the application specific configuration file under the " + "environment variable path. The path should be ignored"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // When ... + AddExistingFile(kGlobalConfigFile); + AddExistingFile(kLocalConfigFileInPwd); + AddExistingFile(kEnvConfigFilePath); + + // ... then ... + const auto result = discoverer_.FindConfigurationFiles(); + EXPECT_EQ(result.size(), 2); + EXPECT_EQ(result.at(0), kGlobalConfigFile); + EXPECT_EQ(result.at(1), kEnvConfigFilePath); +} + +TEST_F(ConfigurationFileDiscovererFixture, DiscovererShallFindConfigurationFileInEnvPathOverrideExecPath) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies that the discoverer shall find the application specific configuration file under the " + "environment variable path. The binary path should be ignored"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // When ... + AddExistingFile(kGlobalConfigFile); + AddExistingFile(kLocalConfigFileInExecPath); + AddExistingFile(kEnvConfigFilePath); + + // ... then ... + const auto result = discoverer_.FindConfigurationFiles(); + EXPECT_EQ(result.size(), 2); + EXPECT_EQ(result.at(0), kGlobalConfigFile); + EXPECT_EQ(result.at(1), kEnvConfigFilePath); +} + +TEST_F(ConfigurationFileDiscovererFixture, DiscovererShallReturnEmptyIfNothingExists) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies that the discoverer shall return an empty list if no config file exists."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // When no file exists then ... + const auto result = discoverer_.FindConfigurationFiles(); + EXPECT_EQ(result.size(), 0); +} + +TEST_F(ConfigurationFileDiscovererFixture, DiscovererShallReturnEmptyIfExecPathFails) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies that the discoverer shall return an empty list if an error occurs."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // When... + AddExistingFile(kLocalConfigFileInExecPath); + SetExecPathShallFail(); + + // ... then ... + const auto result = discoverer_.FindConfigurationFiles(); + EXPECT_EQ(result.size(), 0); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/configuration/configuration_test.cpp b/mw/log/configuration/configuration_test.cpp new file mode 100644 index 0000000..796c6df --- /dev/null +++ b/mw/log/configuration/configuration_test.cpp @@ -0,0 +1,177 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/configuration/configuration.h" + +#include "gtest/gtest.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +TEST(ConfigurationTestSuite, IsLogEnabledShallReturnTrueIfLogLevelIsBelowThreshold) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "The log message shall be enabled if the log level is below the threshold."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + Configuration config{}; + + const amp::string_view kCtx{"CTX1"}; + const ContextLogLevelMap context_log_level_map{{LoggingIdentifier{kCtx}, LogLevel::kError}}; + config.SetContextLogLevel(context_log_level_map); + EXPECT_TRUE(config.IsLogLevelEnabled(LogLevel::kFatal, kCtx)); +} + +TEST(ConfigurationTestSuite, IsLogEnabledShallReturnTrueIfLogLevelIsEqualThreshold) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "The log message shall be enabled if the log level is equal to the threshold."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + Configuration config{}; + + const amp::string_view kCtx{"CTX1"}; + const ContextLogLevelMap context_log_level_map{{LoggingIdentifier{kCtx}, LogLevel::kError}}; + config.SetContextLogLevel(context_log_level_map); + EXPECT_TRUE(config.IsLogLevelEnabled(LogLevel::kError, kCtx)); +} + +TEST(ConfigurationTestSuite, IsLogEnabledShallReturnFalseIfLogLevelIsAboveThreshold) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "The log message shall be disabled if the log level is above to the threshold."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + Configuration config{}; + + const amp::string_view kCtx{"CTX1"}; + const ContextLogLevelMap context_log_level_map{{LoggingIdentifier{kCtx}, LogLevel::kError}}; + config.SetContextLogLevel(context_log_level_map); + EXPECT_FALSE(config.IsLogLevelEnabled(LogLevel::kInfo, kCtx)); +} + +TEST(ConfigurationTestSuite, IsLogEnabledShallReturnTrueIfLogLevelIsAboveOrEqualDefaultThreshold) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "The log message shall be enabled if the log level is equal to the default log level threshold."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + Configuration config{}; + + const amp::string_view kCtx{"CTX1"}; + config.SetDefaultLogLevel(LogLevel::kInfo); + EXPECT_TRUE(config.IsLogLevelEnabled(LogLevel::kInfo, kCtx)); +} + +TEST(ConfigurationTestSuite, IsLogEnabledShallReturnFalseIfLogLevelIsBelowDefaultThreshold) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "The log message shall be disabled if the log level is below the default log level threshold."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + Configuration config{}; + + const amp::string_view kCtx{"CTX1"}; + config.SetDefaultLogLevel(LogLevel::kInfo); + EXPECT_FALSE(config.IsLogLevelEnabled(LogLevel::kVerbose, kCtx)); +} + +TEST(ConfigurationTestSuite, IsLogEnabledShallReturnTrueIfLogLevelIsAboveOrEqualDefaultThresholdForConsole) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "The log message for the console shall be enabled if the log level is equal to the default log level " + "threshold for the console."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + Configuration config{}; + + const amp::string_view kCtx{"CTX1"}; + config.SetDefaultConsoleLogLevel(LogLevel::kInfo); + EXPECT_TRUE(config.IsLogLevelEnabled(LogLevel::kInfo, kCtx, true)); +} + +TEST(ConfigurationTestSuite, IsLogEnabledShallReturnFalseIfLogLevelIsBelowDefaultThresholdForConsole) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "The log message shall be disabled for the console if the log level is below the default log level " + "threshold for the console."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + Configuration config{}; + + const amp::string_view kCtx{"CTX1"}; + config.SetDefaultConsoleLogLevel(LogLevel::kInfo); + EXPECT_FALSE(config.IsLogLevelEnabled(LogLevel::kVerbose, kCtx, true)); +} + +TEST(ConfigurationTestSuite, AppidWithMoreThanFourCharactersShallBeTruncated) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "The application identifier shall be limited to four characters."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + Configuration config{}; + + config.SetAppId("12345"); + EXPECT_EQ(config.GetAppId(), amp::string_view{"1234"}); +} + +TEST(ConfigurationTestSuite, EcuidWithMoreThanFourCharactersShallBeTruncated) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "The ECU identifier shall be limited to four characters."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + Configuration config{}; + + config.SetEcuId("12345"); + EXPECT_EQ(config.GetEcuId(), amp::string_view{"1234"}); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/configuration/iconfiguration_file_discoverer.cpp b/mw/log/configuration/iconfiguration_file_discoverer.cpp new file mode 100644 index 0000000..9eacfb4 --- /dev/null +++ b/mw/log/configuration/iconfiguration_file_discoverer.cpp @@ -0,0 +1,33 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/configuration/iconfiguration_file_discoverer.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + + +IConfigurationFileDiscoverer::~IConfigurationFileDiscoverer() noexcept = default; + + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/configuration/iconfiguration_file_discoverer.h b/mw/log/configuration/iconfiguration_file_discoverer.h new file mode 100644 index 0000000..af036fd --- /dev/null +++ b/mw/log/configuration/iconfiguration_file_discoverer.h @@ -0,0 +1,63 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_ICONFIG_FILE_DISCOVERER_H +#define PLATFORM_AAS_MW_LOG_DETAIL_ICONFIG_FILE_DISCOVERER_H + +#include +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +class IConfigurationFileDiscoverer +{ + public: + + + IConfigurationFileDiscoverer() noexcept = default; + IConfigurationFileDiscoverer(IConfigurationFileDiscoverer&&) noexcept = delete; + IConfigurationFileDiscoverer(const IConfigurationFileDiscoverer&) noexcept = delete; + IConfigurationFileDiscoverer& operator=(IConfigurationFileDiscoverer&&) noexcept = delete; + IConfigurationFileDiscoverer& operator=(const IConfigurationFileDiscoverer&) noexcept = delete; + + virtual ~IConfigurationFileDiscoverer() noexcept; + + /// \brief Find and return the path to the global and application specific config files. + /// \details Only existing paths are returned. + /// Global configuration: + /// 1. /etc/logging.json if it exists. + /// + /// Application configuration: + /// 1. /etc/logging.json + /// 2. /logging.json + /// 3. /../etc/logging.json + virtual std::vector FindConfigurationFiles() const noexcept = 0; + + +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_ICONFIG_FILE_DISCOVERER_H diff --git a/mw/log/configuration/itarget_config_reader.cpp b/mw/log/configuration/itarget_config_reader.cpp new file mode 100644 index 0000000..79d98d5 --- /dev/null +++ b/mw/log/configuration/itarget_config_reader.cpp @@ -0,0 +1,33 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/configuration/itarget_config_reader.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + + +ITargetConfigReader::~ITargetConfigReader() noexcept = default; + + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/configuration/itarget_config_reader.h b/mw/log/configuration/itarget_config_reader.h new file mode 100644 index 0000000..d3a36c0 --- /dev/null +++ b/mw/log/configuration/itarget_config_reader.h @@ -0,0 +1,48 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_ITARGET_CONFIG_READER_H +#define PLATFORM_AAS_MW_LOG_DETAIL_ITARGET_CONFIG_READER_H + +#include "platform/aas/lib/result/result.h" +#include "platform/aas/mw/log/configuration/configuration.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +class ITargetConfigReader +{ + public: + ITargetConfigReader() noexcept = default; + ITargetConfigReader(ITargetConfigReader&&) noexcept = delete; + ITargetConfigReader(const ITargetConfigReader&) noexcept = delete; + ITargetConfigReader& operator=(ITargetConfigReader&&) noexcept = delete; + ITargetConfigReader& operator=(const ITargetConfigReader&) noexcept = delete; + virtual ~ITargetConfigReader() noexcept; + + virtual bmw::Result ReadConfig() const noexcept = 0; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_ITARGET_CONFIG_READER_H diff --git a/mw/log/configuration/nvconfig.cpp b/mw/log/configuration/nvconfig.cpp new file mode 100644 index 0000000..9f6c4c2 --- /dev/null +++ b/mw/log/configuration/nvconfig.cpp @@ -0,0 +1,171 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "nvconfig.h" + +#include "platform/aas/lib/json/json_parser.h" + +#include "amp_string_view.hpp" + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +namespace +{ +constexpr mw::log::LogLevel kDefaultLogLevel = mw::log::LogLevel::kInfo; + +config::NvMsgDescriptor GetMsgDescriptor(const std::uint32_t id, + const bmw::json::Object& object_array_value, + const bmw::json::Object::const_iterator& object_appid_iterator, + const bmw::json::Object::const_iterator& object_ctxid_iterator) noexcept +{ + const auto& object_loglevel_iterator = object_array_value.find("loglevel"); + + /* log_level visibility is correct, since it is used in the NvMsgDescriptor. */ + auto log_level = kDefaultLogLevel; + + if (object_loglevel_iterator != object_array_value.end()) + { + log_level = mw::log::TryGetLogLevelFromU8(object_loglevel_iterator->second.As().value()) + .value_or(kDefaultLogLevel); + } + + auto object_appid_result = object_appid_iterator->second.As(); + + /* Braces are correctly placed. */ + const bmw::mw::log::detail::LoggingIdentifier appid(object_appid_result.value()); + + auto object_ctxid_result = object_ctxid_iterator->second.As(); + + /* Braces are correctly placed. */ + const bmw::mw::log::detail::LoggingIdentifier ctxid(object_ctxid_result.value()); + + + /* It's initialization list, not variables declaring. */ + + return config::NvMsgDescriptor{id, appid, ctxid, log_level}; + + +} + +NvConfig::ReadResult HandleParseResult(const bmw::json::Object& parse_result, NvConfig::typemap_t& typemap) noexcept +{ + for (auto& result_iterator : parse_result) + { + auto object_array_result = result_iterator.second.As(); + + if (!object_array_result.has_value()) + + { + return NvConfig::ReadResult::kERROR_PARSE; + } + + auto& object_array_value = object_array_result.value().get(); + const auto& object_ctxid_iterator = object_array_value.find("ctxid"); + if (object_ctxid_iterator == object_array_value.end()) + { + return NvConfig::ReadResult::kERROR_CONTENT; + } + + const auto& object_id_iterator = object_array_value.find("id"); + if (object_id_iterator == object_array_value.end()) + { + return NvConfig::ReadResult::kERROR_CONTENT; + } + + const auto& object_appid_iterator = object_array_value.find("appid"); + if (object_appid_iterator == object_array_value.end()) + { + return NvConfig::ReadResult::kERROR_CONTENT; + } + + auto id = object_id_iterator->second.As(); + + + if (id.has_value() == false) + + + { + return NvConfig::ReadResult::kERROR_CONTENT; + } + + auto object_name = result_iterator.first.GetAsStringView(); + typemap[object_name.data()] = + GetMsgDescriptor(id.value(), object_array_value, object_appid_iterator, object_ctxid_iterator); + } + return NvConfig::ReadResult::kOK; +} + +} // namespace + + +NvConfig::NvConfig(const std::string& file_path) : json_path_(file_path), typemap_{} {} + + +NvConfig::ReadResult NvConfig::parseFromJson() noexcept + + +{ + const bmw::json::JsonParser json_parser_obj; + // FromFile() is safe, if the JSON file is stored in stored on qtsafefs (integrity protection). + // See + // NOLINTNEXTLINE(bmw-banned-function) - Argumentation is above. + auto root = json_parser_obj.FromFile(json_path_); + + + if (!root.has_value()) + + + { + return ReadResult::kERROR_PARSE; + } + + auto parse_result = root.value().As(); + + if (!parse_result.has_value()) + + { + return ReadResult::kERROR_PARSE; + } + + return HandleParseResult(parse_result.value(), typemap_); +} + + + + +const config::NvMsgDescriptor* NvConfig::getDltMsgDesc(const std::string& typeName) const noexcept + + + +{ + auto desc = typemap_.find(typeName); + + if (desc != typemap_.end()) + { + return &desc->second; + } + else + { + return nullptr; + } +} + +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/configuration/nvconfig.h b/mw/log/configuration/nvconfig.h new file mode 100644 index 0000000..4328d61 --- /dev/null +++ b/mw/log/configuration/nvconfig.h @@ -0,0 +1,61 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_NVCONFIG_H_ +#define PLATFORM_AAS_MW_LOG_NVCONFIG_H_ + +#include "nvmsgdescriptor.h" + +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +// +class NvConfig +{ + public: + enum class ReadResult : std::uint8_t + { + kOK = 0, + kERROR_PARSE, + kERROR_CONTENT + }; + + using typemap_t = std::unordered_map; + + explicit NvConfig(const std::string& file_path = "/bmw/platform/opt/datarouter/etc/class-id.json"); + + ReadResult parseFromJson() noexcept; + const config::NvMsgDescriptor* getDltMsgDesc(const std::string& typeName) const noexcept; + + + private: + + + const std::string json_path_; + typemap_t typemap_; + +}; + +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_NVCONFIG_H_ diff --git a/mw/log/configuration/nvconfig_test.cpp b/mw/log/configuration/nvconfig_test.cpp new file mode 100644 index 0000000..4588d1b --- /dev/null +++ b/mw/log/configuration/nvconfig_test.cpp @@ -0,0 +1,223 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "nvconfig.h" + +#include "platform/aas/mw/log/log_level.h" + +#include "gtest/gtest.h" + +namespace +{ + +using NvConfig = bmw::mw::log::NvConfig; +using ReadResult = NvConfig::ReadResult; + +const std::string JSON_PATH = "platform/aas/mw/log/configuration/test/data/test-class-id.json"; +const std::string JSON_PATH_2 = "platform/aas/mw/log/configuration/test/data/second-test-class-id.json"; +const std::string EMPTY_FILE = "platform/aas/mw/log/configuration/test/data/empty-class-id.json"; +const std::string EMPTY_JSON_OBJECT = "platform/aas/mw/log/configuration/test/data/error-parse-empty-json-object.json"; +const std::string WRONG_JSON_PATH = "platform/aas/mw/log/configuration/test/data/wrong-path-class-id.json"; +const std::string EMPTY_JSON = "platform/aas/mw/log/configuration/test/data/empty-json-class-id.json"; +const std::string ERROR_PARSE_1_PATH = "platform/aas/mw/log/configuration/test/data/error-parse-1-json-class-id.json"; +const std::string ERROR_CONTENT_1_PATH = + "platform/aas/mw/log/configuration/test/data/error-content-1-json-class-id.json"; +const std::string ERROR_CONTENT_2_PATH = + "platform/aas/mw/log/configuration/test/data/error-content-2-json-class-id.json"; +const std::string ERROR_CONTENT_3_PATH = + "platform/aas/mw/log/configuration/test/data/error-content-3-json-class-id.json"; +const std::string ERROR_CONTENT_WRONG_ID_VALUE = + "platform/aas/mw/log/configuration/test/data/error-content-wrong-id-value.json"; + +TEST(NonVerboseConfig, NvConfigReturnsExpectedValues) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Logging libraries use static configuration based on .json files."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + NvConfig nvc1(JSON_PATH); + EXPECT_EQ(ReadResult::kOK, nvc1.parseFromJson()); // ok json results + EXPECT_EQ(301, nvc1.getDltMsgDesc("bmw::logging::PersistentLogFileEvent")->id_msg_descriptor_); // id value reading + EXPECT_EQ(bmw::mw::log::LogLevel::kFatal, + nvc1.getDltMsgDesc("bmw::logging::PersistentLogFileEvent")->logLevel_); // loglevel value reading + EXPECT_EQ(bmw::mw::log::LogLevel::kError, + nvc1.getDltMsgDesc("LogMark::stdframe")->logLevel_); // loglevel value reading other example + EXPECT_EQ(bmw::mw::log::LogLevel::kInfo, + nvc1.getDltMsgDesc("poseng::logging::ReprocessingCycle") + ->logLevel_); // loglevel value reading when using default loglevel + EXPECT_EQ(10, nvc1.getDltMsgDesc("LogMark::stdframe")->id_msg_descriptor_); // id value reading other example + EXPECT_EQ(amp::string_view{"Repr"}, + nvc1.getDltMsgDesc("aas::logging::ReprocessingCycle")->ctxid_.GetStringView()); // ctxid value reading + EXPECT_EQ(amp::string_view{"PE"}, + nvc1.getDltMsgDesc("poseng::logging::ReprocessingCycle")->appid_.GetStringView()); // appid value reading + EXPECT_EQ(amp::string_view{"PERL"}, + nvc1.getDltMsgDesc("bmw::logging::PersistentLogFileEvent") + ->ctxid_.GetStringView()); // ctxid value reading other example + EXPECT_EQ(amp::string_view{"STDF"}, + nvc1.getDltMsgDesc("LogMark::stdframe")->appid_.GetStringView()); // appid value reading other example +} + +TEST(NonVerboseConfig, NvConfigReturnsExpectedValuesWithOtherFile) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Logging libraries use static configuration based on .json files."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + NvConfig nvc2(JSON_PATH_2); + EXPECT_EQ(ReadResult::kOK, nvc2.parseFromJson()); // ok json results + EXPECT_EQ(8650816, nvc2.getDltMsgDesc("adp::planning::awa::DebugData")->id_msg_descriptor_); // id value reading + EXPECT_EQ(bmw::mw::log::LogLevel::kWarn, + nvc2.getDltMsgDesc("adp::logging::DynamicInsight")->logLevel_); // loglevel value reading + EXPECT_EQ(bmw::mw::log::LogLevel::kError, + nvc2.getDltMsgDesc("bmw::logging::timesync::DltTimeSyncTimestamp") + ->logLevel_); // loglevel value reading other example + EXPECT_EQ(8650814, + nvc2.getDltMsgDesc("adp::perception::CrocStateTraceable") + ->id_msg_descriptor_); // id value reading other example + EXPECT_EQ(amp::string_view{"Repr"}, + nvc2.getDltMsgDesc("aas::logging::ReprocessingEvent")->ctxid_.GetStringView()); // ctxid value reading + EXPECT_EQ(amp::string_view{"Fasi"}, + nvc2.getDltMsgDesc("bmw::sli::TsfBaseConfig")->appid_.GetStringView()); // appid value reading + EXPECT_EQ(amp::string_view{"DTNV"}, + nvc2.getDltMsgDesc("adp::planning::driving_tube::DiagnosticLogsData") + ->ctxid_.GetStringView()); // ctxid value reading other example + EXPECT_EQ(amp::string_view{"Plan"}, + nvc2.getDltMsgDesc("adp::planning::driving_tube::DiagnosticLogsData") + ->appid_.GetStringView()); // appid value reading other example +} + +TEST(NonVerboseConfig, NvConfigReturnsErrorOpenWhenGivenEmptyFile) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the inability of parsing a general empty file."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + NvConfig nvc3(EMPTY_FILE); + EXPECT_EQ(ReadResult::kERROR_PARSE, nvc3.parseFromJson()); // error parse because it is a general empty file +} + +TEST(NonVerboseConfig, NvConfigReturnsErrorOpenWhenGivenPathToNonExistentFile) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "ara::log shall discard configuration files that are not found."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + NvConfig nvc4(WRONG_JSON_PATH); + EXPECT_EQ(ReadResult::kERROR_PARSE, nvc4.parseFromJson()); // error parse because the file doesn't exist +} + +TEST(NonVerboseConfig, NvConfigReturnsOkWhenGivenEmptyJsonFile) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of parsing an empty JSON file."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + NvConfig nvc5(EMPTY_JSON); + EXPECT_EQ(ReadResult::kOK, nvc5.parseFromJson()); // ok because this json file doesn't have items +} + +TEST(NonVerboseConfig, NvConfigReturnsErrorParseIfEmptyObject) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies the inability of parsing JSON file that has array instead of JSON object as a root node."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + NvConfig nvc6(EMPTY_JSON_OBJECT); + EXPECT_EQ(ReadResult::kERROR_PARSE, + nvc6.parseFromJson()); // array instead of json object as one of the values +} + +TEST(NonVerboseConfig, NvConfigReturnsErrorParseIfThereIsSomethingElseInstedOfObjectAsValue) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the inability of parsing a JSON file that does not have object as value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + NvConfig nvc6(ERROR_PARSE_1_PATH); + EXPECT_EQ(ReadResult::kERROR_PARSE, + nvc6.parseFromJson()); // aray instead of json object as one of the values +} + +TEST(NonVerboseConfig, NvConfigReturnsErrorContentIfCtxidValuePairDoesntExistsForOneObject) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "Verifies the inability of parsing JSON file that misses ctxid key-value pair for one of the objects."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + NvConfig nvc7(ERROR_CONTENT_1_PATH); + EXPECT_EQ(ReadResult::kERROR_CONTENT, + nvc7.parseFromJson()); // ctxid key-value pair is missing in one of the objects +} + +TEST(NonVerboseConfig, NvConfigReturnsErrorContentIfAppidValuePairDoesntExistsForOneObject) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "Verifies the inability of parsing JSON file that misses appid key-value pair for one of the objects."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + NvConfig nvc8(ERROR_CONTENT_2_PATH); + EXPECT_EQ(ReadResult::kERROR_CONTENT, + nvc8.parseFromJson()); // appid key-value pair is missing in one of the objects +} + +TEST(NonVerboseConfig, NvConfigReturnsErrorContentIfIdValuePairDoesntExistsForOneObject) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies the inability of parsing JSON file that misses id key-value pair for one of the objects."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + NvConfig nvc9(ERROR_CONTENT_3_PATH); + EXPECT_EQ(ReadResult::kERROR_CONTENT, + nvc9.parseFromJson()); // id key-value pair is missing in one of the objects +} + +TEST(NonVerboseConfig, NvConfigReturnsErrorIfIdDataTypeIsWrong) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the inability of parsing JSON file that has wrong id value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + NvConfig nvc10(ERROR_CONTENT_WRONG_ID_VALUE); + EXPECT_EQ(ReadResult::kERROR_CONTENT, nvc10.parseFromJson()); // wrong ID data type (string instead of int). +} + +} // namespace diff --git a/mw/log/configuration/nvmsgdescriptor.h b/mw/log/configuration/nvmsgdescriptor.h new file mode 100644 index 0000000..a84aaa6 --- /dev/null +++ b/mw/log/configuration/nvmsgdescriptor.h @@ -0,0 +1,51 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_CONFIG_NVMSGDESCRIPTOR_H_ +#define PLATFORM_AAS_MW_LOG_CONFIG_NVMSGDESCRIPTOR_H_ + +#include "platform/aas/mw/log/detail/logging_identifier.h" +#include "platform/aas/mw/log/log_level.h" + +#include +#include + + +namespace bmw + +{ +namespace mw +{ +namespace log +{ +namespace config +{ + +// +struct NvMsgDescriptor +{ + uint32_t id_msg_descriptor_{}; + + bmw::mw::log::detail::LoggingIdentifier appid_{"NULL"}; + bmw::mw::log::detail::LoggingIdentifier ctxid_{"NULL"}; + + mw::log::LogLevel logLevel_{}; +}; + +} // namespace config +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_CONFIG_NVMSGDESCRIPTOR_H_ diff --git a/mw/log/configuration/target_config_reader.cpp b/mw/log/configuration/target_config_reader.cpp new file mode 100644 index 0000000..e09b73f --- /dev/null +++ b/mw/log/configuration/target_config_reader.cpp @@ -0,0 +1,495 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/configuration/target_config_reader.h" + +#include "amp_callback.hpp" +#include "platform/aas/lib/json/json_parser.h" +#include "platform/aas/lib/memory/split_string_view.h" +#include "platform/aas/mw/log/detail/error.h" +#include "platform/aas/mw/log/detail/initialization_reporter.h" + +#include + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +namespace +{ + +constexpr StringLiteral kEcuIdKey{"ecuId"}; +constexpr StringLiteral kAppIdKey{"appId"}; +constexpr StringLiteral kAppDescriptionKey{"appDesc"}; +constexpr StringLiteral kLogFilePathKey{"logFilePath"}; +constexpr StringLiteral kLogModeKey{"logMode"}; +constexpr StringLiteral kLogLevelKey{"logLevel"}; +constexpr StringLiteral kLogLevelThresholdConsoleKey{"logLevelThresholdConsole"}; +constexpr StringLiteral kContextConfigsKey{"contextConfigs"}; +constexpr StringLiteral kContextNameKey{"name"}; +constexpr StringLiteral kStackBufferSizeKey{"stackBufferSize"}; +constexpr StringLiteral kRingBufferSizeKey{"ringBufferSize"}; +constexpr StringLiteral kOverwriteOnFullKey{"overwriteOnFull"}; +constexpr StringLiteral kNumberOfSlotsKey{"numberOfSlots"}; +constexpr StringLiteral kSlotSizeBytes{"slotSizeBytes"}; +constexpr StringLiteral kDatarouterUid{"datarouterUid"}; +constexpr StringLiteral kDynamicDatarouterIdentifiers{"dynamicDatarouterIdentifiers"}; + +const std::unordered_map kStringToLogLevel{{{"kOff", LogLevel::kOff}, + {"kFatal", LogLevel::kFatal}, + {"kError", LogLevel::kError}, + {"kWarn", LogLevel::kWarn}, + {"kWarning", LogLevel::kWarn}, + {"kInfo", LogLevel::kInfo}, + {"kDebug", LogLevel::kDebug}, + {"kVerbose", LogLevel::kVerbose}}}; + +constexpr std::string::value_type kLogModeCombineChar = '|'; + +const std::unordered_map kStringToLogMode{{{"kRemote", LogMode::kRemote}, + {"kConsole", LogMode::kConsole}, + {"kFile", LogMode::kFile}, + {"kSystem", LogMode::kSystem}}}; + +/// \brief Provide user feedback in case a configuration file contains errors. +template +void ReportOnError(bmw::Result result, const std::string& file_name) noexcept +{ + if (result.has_value() == true) + { + return; + } + + ReportInitializationError(result.error(), amp::string_view{file_name.data(), file_name.size()}); +} + +// Forward declare GetElementAsImpl +template +class GetElementAsImpl; + +template +auto GetElementAs(const bmw::json::Object& obj, const StringLiteral key) noexcept +{ + // To prevent using function template specializations, we use class template specialization in the implementation of + // GetElementAs() + return GetElementAsImpl::GetElementAs(obj, key); +} + +template +class GetElementAsImpl +{ + public: + static bmw::Result GetElementAs(const bmw::json::Object& obj, const StringLiteral key) noexcept + { + const auto find_result = obj.find(key); + if (find_result == obj.end()) + { + return bmw::MakeUnexpected(Error::kConfigurationOptionalJsonKeyNotFound, key); + } + + return find_result->second.As(); + } +}; + +template +bmw::Result> GetElementAsRef(const bmw::json::Object& obj, + const StringLiteral key) noexcept +{ + return GetElementAs, T>(obj, key); +} + +template +using GetElementCallback = amp::callback; + +template +bmw::ResultBlank GetElementAndThen(const bmw::json::Object& obj, + const StringLiteral key, + GetElementCallback update) noexcept +{ + const auto parser_result = GetElementAs(obj, key); + if (parser_result.has_value() == false) + { + + return bmw::MakeUnexpected(parser_result.error()); + + } + + update(parser_result.value()); + + return {}; +} + +bmw::ResultBlank ParseEcuId(const bmw::json::Object& root, Configuration& config) noexcept +{ + + /* The lambda will be executed within this stack. Thus, all references are still valid */ + return GetElementAndThen( + root, kEcuIdKey, [&config](auto value) noexcept { config.SetEcuId(value); }); + +} + +bmw::ResultBlank ParseAppId(const bmw::json::Object& root, Configuration& config) noexcept +{ + + /* The lambda will be executed within this stack. Thus, all references are still valid */ + return GetElementAndThen( + root, kAppIdKey, [&config](auto value) noexcept { config.SetAppId(value); }); + +} + +bmw::ResultBlank ParseAppDescription(const bmw::json::Object& root, Configuration& config) noexcept +{ + + /* The lambda will be executed within this stack. Thus, all references are still valid */ + return GetElementAndThen( + root, kAppDescriptionKey, [&config](auto value) noexcept { config.SetAppDescription(value); }); + +} + +bmw::ResultBlank ParseLogFilePath(const bmw::json::Object& root, Configuration& config) noexcept +{ + + /* The lambda will be executed within this stack. Thus, all references are still valid */ + return GetElementAndThen( + root, kLogFilePathKey, [&config](auto value) noexcept { config.SetLogFilePath(value); }); + +} + +/// \brief Returns the corresponding log mode of the string. +bmw::Result LogModeFromString(const amp::string_view str) noexcept +{ + const auto result = kStringToLogMode.find(str); + + if (result == kStringToLogMode.end()) + { + return MakeUnexpected(Error::kInvalidLogModeString, "Expected `kRemote`, `kConsole`, `kSystem` or `kFile`."); + } + + return result->second; +} + +/// \brief Returns the corresponding combined log mode(s) of the string. +bmw::Result> LogModesFromString(const amp::string_view str) noexcept +{ + const auto segments = bmw::memory::LazySplitStringView{str, kLogModeCombineChar}; + + std::unordered_set result; + + for (const amp::string_view segment : segments) + { + const auto log_mode = LogModeFromString(segment); + if (log_mode.has_value() == false) + { + return MakeUnexpected(Error::kInvalidLogModeString, + "Expected `kRemote`, `kConsole`, `kSystem` or `kFile`."); + } + amp::ignore = result.emplace(log_mode.value()); + } + return result; +} + +bmw::ResultBlank ParseLogMode(const bmw::json::Object& root, Configuration& config) noexcept +{ + + /* The lambda will be executed within this stack. Thus, all references are still valid */ + return GetElementAndThen(root, kLogModeKey, [&config](auto value) noexcept { + const auto log_mode_result = LogModesFromString(value); + if (log_mode_result.has_value()) + { + config.SetLogMode(log_mode_result.value()); // LCOV_EXCL_BR_LINE: no branches here to be covered. + } + }); + +} + +/// \brief Returns the corresponding log level of the string. +bmw::Result LogLevelFromString(const amp::string_view str) noexcept +{ + const auto result = kStringToLogLevel.find(str); + if (result == kStringToLogLevel.end()) + { + return MakeUnexpected(Error::kInvalidLogLevelString, + "Expected `kOff`, `kFatal`, `kWarn`, `kError`, `kInfo`, `kDebug` or `kVerbose`."); + } + return result->second; +} + +/// \brief Returns the element of a JSON object as a LogLevel. +template <> +class GetElementAsImpl +{ + public: + static bmw::Result GetElementAs(const bmw::json::Object& obj, const StringLiteral key) noexcept + { + const auto string_result = GetElementAsImpl::GetElementAs(obj, key); + if (string_result.has_value() == false) + { + + return bmw::MakeUnexpected(string_result.error()); + + } + + return LogLevelFromString(string_result.value()); + } +}; + +bmw::ResultBlank ParseLogLevel(const bmw::json::Object& root, Configuration& config) noexcept +{ + + /* The lambda will be executed within this stack. Thus, all references are still valid */ + return GetElementAndThen(root, kLogLevelKey, [&config](auto value) noexcept { + const auto log_level_result = LogLevelFromString(value); + if (log_level_result.has_value()) + { + config.SetDefaultLogLevel(log_level_result.value()); // LCOV_EXCL_BR_LINE: no branches here to be covered. + } + }); + +} + +bmw::ResultBlank ParseLogLevelConsole(const bmw::json::Object& root, Configuration& config) noexcept +{ + + /* The lambda will be executed within this stack. Thus, all references are still valid */ + return GetElementAndThen(root, kLogLevelThresholdConsoleKey, [&config](auto value) noexcept { + const auto log_level_result = LogLevelFromString(value); + if (log_level_result.has_value()) + { + config.SetDefaultConsoleLogLevel(log_level_result.value()); + } + }); + +} + +bmw::ResultBlank ParseContextLogLevel(const bmw::json::Object& root, + Configuration& config, + const std::string& path_for_reporting) noexcept +{ + const auto context_config = GetElementAsRef(root, kContextConfigsKey); + if (context_config.has_value() == false) + { + // coverage: coverage shown by manual inspection. + // Reasoning: building without code optimization (-O0) it is possible to set and hit the breakpoint. + // See also: + + return bmw::MakeUnexpected(context_config.error()); + + } + + auto context_config_map = config.GetContextLogLevel(); + + + for (const auto& context_item : context_config.value().get()) + { + const auto context_result = context_item.As(); + if (context_result.has_value() == false) + { + ReportOnError(context_result, path_for_reporting); + continue; + } + const auto context_obj = context_result.value(); + + const auto context_name_result = GetElementAsRef(context_obj, kContextNameKey); + if (context_name_result.has_value() == false) + { + ReportOnError(context_name_result, path_for_reporting); + continue; + } + + const auto context_log_level_result = GetElementAs(context_obj, kLogLevelKey); + if (context_log_level_result.has_value() == false) + { + ReportOnError(context_name_result, path_for_reporting); + continue; + } + + context_config_map[LoggingIdentifier{context_name_result.value().get()}] = context_log_level_result.value(); + } + + config.SetContextLogLevel(context_config_map); + + return {}; +} + +bmw::ResultBlank ParseStackBufferSize(const bmw::json::Object& root, Configuration& config) noexcept +{ + + /* The lambda will be executed within this stack. Thus, all references are still valid */ + return GetElementAndThen( + root, kStackBufferSizeKey, [&config](auto value) noexcept { config.SetStackBufferSize(value); }); + +} + +bmw::ResultBlank ParseRingBufferSize(const bmw::json::Object& root, Configuration& config) noexcept +{ + + /* The lambda will be executed within this stack. Thus, all references are still valid */ + return GetElementAndThen( + root, kRingBufferSizeKey, [&config](auto value) noexcept { config.SetRingBufferSize(value); }); + +} + +bmw::ResultBlank ParseOverwriteOnFull(const bmw::json::Object& root, Configuration& config) noexcept +{ + + /* The lambda will be executed within this stack. Thus, all references are still valid */ + return GetElementAndThen( + root, kOverwriteOnFullKey, [&config](auto value) noexcept { config.SetRingBufferOverwriteOnFull(value); }); + +} + +bmw::ResultBlank ParseNumberOfSlots(const bmw::json::Object& root, Configuration& config) noexcept +{ + + /* The lambda will be executed within this stack. Thus, all references are still valid */ + return GetElementAndThen( + root, kNumberOfSlotsKey, [&config](auto value) noexcept { config.SetNumberOfSlots(value); }); + +} + +bmw::ResultBlank ParseSlotSizeBytes(const bmw::json::Object& root, Configuration& config) noexcept +{ + + /* The lambda will be executed within this stack. Thus, all references are still valid */ + return GetElementAndThen( + root, kSlotSizeBytes, [&config](auto value) noexcept { config.SetSlotSizeInBytes(value); }); + +} + +bmw::ResultBlank ParseDatarouterUid(const bmw::json::Object& root, Configuration& config) noexcept +{ + + /* The lambda will be executed within this stack. Thus, all references are still valid */ + return GetElementAndThen( + root, kDatarouterUid, [&config](const auto value) noexcept { config.SetDataRouterUid(value); }); + +} + +bmw::ResultBlank ParseDynamicDatarouterIdentifiers(const bmw::json::Object& root, Configuration& config) noexcept +{ + + /* The lambda will be executed within this stack. Thus, all references are still valid */ + return GetElementAndThen(root, kDynamicDatarouterIdentifiers, [&config](const auto value) noexcept { + config.SetDynamicDatarouterIdentifiers(value); + }); + +} + +void ParseConfigurationElements(const bmw::json::Object& root, const std::string& path, Configuration& config) noexcept +{ + ReportOnError(ParseEcuId(root, config), path); + ReportOnError(ParseAppId(root, config), path); + ReportOnError(ParseAppDescription(root, config), path); + ReportOnError(ParseLogFilePath(root, config), path); + ReportOnError(ParseLogMode(root, config), path); + ReportOnError(ParseLogLevel(root, config), path); + ReportOnError(ParseLogLevelConsole(root, config), path); + ReportOnError(ParseContextLogLevel(root, config, path), path); + ReportOnError(ParseStackBufferSize(root, config), path); + ReportOnError(ParseRingBufferSize(root, config), path); + ReportOnError(ParseOverwriteOnFull(root, config), path); + ReportOnError(ParseNumberOfSlots(root, config), path); + ReportOnError(ParseSlotSizeBytes(root, config), path); + ReportOnError(ParseDatarouterUid(root, config), path); + ReportOnError(ParseDynamicDatarouterIdentifiers(root, config), path); +} + +bmw::Result ParseAndUpdateConfiguration(const std::string& path, Configuration config) noexcept +{ + const bmw::json::JsonParser json_parser_obj; + // FromFile() is safe, if the JSON file is stored in stored on qtsafefs (integrity protection). + // See + // NOLINTNEXTLINE(bmw-banned-function) - Argumentation is above. + const auto json_result = json_parser_obj.FromFile(path); + if (json_result.has_value() == false) + { + + return bmw::MakeUnexpected(json_result.error()); + + } + + const auto root_result = json_result.value().As(); + if (root_result.has_value() == false) + { + // coverage: coverage shown by manual inspection. + // Reasoning: building without code optimization (-O0) it is possible to set and hit the breakpoint. + // See also: + + return bmw::MakeUnexpected(root_result.error()); + + } + + ParseConfigurationElements(root_result->get(), path, config); + + return config; + +} + +} // namespace + + + +TargetConfigReader::TargetConfigReader(std::unique_ptr discoverer) noexcept + : ITargetConfigReader{}, discoverer_(std::move(discoverer)) +{ +} + + + + +bmw::Result TargetConfigReader::ReadConfig() const noexcept +{ + const auto config_files = discoverer_->FindConfigurationFiles(); + + if (config_files.empty()) + { + return MakeUnexpected(Error::kConfigurationFilesNotFound); + } + + Configuration config{}; + + // Update the config instance by iterating over the config_files + // Each config file can overwrite the previous config files. + + for (const auto& config_file : config_files) + { + auto result = ParseAndUpdateConfiguration(config_file, config); + + if (result.has_value() == false) + { + ReportOnError(result, config_file); + continue; + } + + config = std::move(result.value()); + } + + + + return config; + +} + + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/configuration/target_config_reader.h b/mw/log/configuration/target_config_reader.h new file mode 100644 index 0000000..491ce04 --- /dev/null +++ b/mw/log/configuration/target_config_reader.h @@ -0,0 +1,48 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_TARGET_CONFIG_READER_H +#define PLATFORM_AAS_MW_LOG_DETAIL_TARGET_CONFIG_READER_H + +#include "platform/aas/lib/os/unistd.h" +#include "platform/aas/lib/os/utils/path_impl.h" +#include "platform/aas/mw/log/configuration/configuration_file_discoverer.h" +#include "platform/aas/mw/log/configuration/itarget_config_reader.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +class TargetConfigReader final : public ITargetConfigReader +{ + public: + explicit TargetConfigReader(std::unique_ptr discoverer) noexcept; + + bmw::Result ReadConfig() const noexcept final override; + + private: + std::unique_ptr discoverer_; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_TARGET_CONFIG_READER_H diff --git a/mw/log/configuration/target_config_reader_mock.cpp b/mw/log/configuration/target_config_reader_mock.cpp new file mode 100644 index 0000000..8ab16e3 --- /dev/null +++ b/mw/log/configuration/target_config_reader_mock.cpp @@ -0,0 +1,15 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/configuration/target_config_reader_mock.h" diff --git a/mw/log/configuration/target_config_reader_mock.h b/mw/log/configuration/target_config_reader_mock.h new file mode 100644 index 0000000..84acedd --- /dev/null +++ b/mw/log/configuration/target_config_reader_mock.h @@ -0,0 +1,48 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_TARGET_CONFIG_READER_MOCK_H +#define PLATFORM_AAS_MW_LOG_DETAIL_TARGET_CONFIG_READER_MOCK_H + +#include "platform/aas/mw/log/configuration/itarget_config_reader.h" + +#include "gmock/gmock.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +class TargetConfigReaderMock final : public ITargetConfigReader +{ + public: + + /* Not actual for mock class; Internal GTest Framework code caused the violation; */ + + /* Not actual for mock class; Internal GTest Framework code caused the violation; */ + MOCK_METHOD((bmw::Result), ReadConfig, (), (const, noexcept, override)); + + +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_TARGET_CONFIG_READER_MOCK_H diff --git a/mw/log/configuration/target_config_reader_test.cpp b/mw/log/configuration/target_config_reader_test.cpp new file mode 100644 index 0000000..aadecf0 --- /dev/null +++ b/mw/log/configuration/target_config_reader_test.cpp @@ -0,0 +1,473 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/configuration/target_config_reader.h" +#include "platform/aas/mw/log/configuration/configuration_file_discoverer_mock.h" +#include "platform/aas/mw/log/detail/error.h" + +#include "gtest/gtest.h" + +using testing::_; + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +/// \brief Example config with all possible configuration settings. +const std::string kEcuConfigFile{"platform/aas/mw/log/configuration/test/data/ecu_config.json"}; +const std::string kAppConfigFile{"platform/aas/mw/log/configuration/test/data/app_config.json"}; +const std::string kSyntaxErrorConfigFile{"platform/aas/mw/log/configuration/test/data/syntax_error.json"}; +const std::string kInvalidAppConfigFile{"platform/aas/mw/log/configuration/test/data/invalid_app_config.json"}; +const std::string kInvalidConfigFilePath{"platform/aas/mw/log/configuration/test/data/___nonexistent___.json"}; +const std::string kEmptyConfigFile{"platform/aas/mw/log/configuration/test/data/empty_config.json"}; +const std::string kCtxLevelBrokenConfigFile{ + "platform/aas/mw/log/configuration/test/data/context_level_broken_config.json"}; +const std::string kErrorContent{"platform/aas/mw/log/configuration/test/data/error-json-structure.json"}; +const std::string kWrongLogLevel{"platform/aas/mw/log/configuration/test/data/wrong-loglevel-value.json"}; +const std::string kWrongContextConfig{"platform/aas/mw/log/configuration/test/data/wrong-context-config-value.json"}; + +const amp::string_view kEcuConfigEcuId{"ECU1"}; +const amp::string_view kEcuConfigAppId{"UNKN"}; +const amp::string_view kDefaultConfigAppId{"NONE"}; +const amp::string_view kAppConfigAppId{"App1"}; +const LogLevel kEcuConfigLogLevel{LogLevel::kInfo}; +const LogLevel kAppConfigLogLevel{LogLevel::kError}; +const std::unordered_set kEcuConfigLogMode{LogMode::kRemote, + LogMode::kConsole, + LogMode::kFile, + LogMode::kSystem}; +const std::unordered_set kAppConfigLogMode{LogMode::kRemote}; +const amp::string_view kAppDescription{"Application One Description"}; +const std::size_t kEcuConfigStackBufferSize{3000}; +const std::size_t kEcuConfigRingBufferSize{4096}; +const LogLevel kEcuConfigLogLevelConsole{LogLevel::kVerbose}; +const amp::string_view kAppConfigLogFilePath{"/var/tmp"}; +const ContextLogLevelMap kCombinedContextLogLevel{{LoggingIdentifier{"DTC"}, LogLevel::kInfo}, + {LoggingIdentifier{"FOO"}, LogLevel::kWarn}, + {LoggingIdentifier{"vcip"}, LogLevel::kDebug}, + {LoggingIdentifier{"vcom"}, LogLevel::kOff}}; +const ContextLogLevelMap kEcuConfigContextLogLevel{{LoggingIdentifier{"DTC"}, LogLevel::kInfo}, + {LoggingIdentifier{"FOO"}, LogLevel::kWarn}, + {LoggingIdentifier{"vcip"}, LogLevel::kError}, + {LoggingIdentifier{"vcom"}, LogLevel::kOff}}; +const ContextLogLevelMap kContextConfigLogLevel{{LoggingIdentifier{"vcip"}, LogLevel::kError}, + {LoggingIdentifier{"vcom"}, LogLevel::kOff}}; +const std::size_t kNumberOfSlots{8}; +const std::size_t kSlotSizeBytes{1500}; +const std::size_t kDatarouterUid{1038}; +const bool kDynamicDatarouterIdentifiers{true}; +class TargetConfigReaderFixture : public ::testing::Test +{ + public: + void SetUp() override + { + auto discoverer_mock = std::make_unique(); + + ON_CALL(*discoverer_mock, FindConfigurationFiles).WillByDefault([&]() { return configuration_file_paths_; }); + + reader_ = std::make_unique(std::move(discoverer_mock)); + } + + void TearDown() override {} + + void SetConfigurationFiles(std::vector files) { configuration_file_paths_ = files; } + + TargetConfigReader& GetReader() { return *reader_; } + + private: + std::vector configuration_file_paths_{kEcuConfigFile, kAppConfigFile}; + std::unique_ptr reader_; +}; + +TEST_F(TargetConfigReaderFixture, NoConfigFilesShallFail) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TargetConfigReader shall return an error if no configuration files are found"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // For the case that no configuration files exist. + SetConfigurationFiles({}); + + // ReadConfig shall return an error. + EXPECT_EQ(GetReader().ReadConfig().error(), Error::kConfigurationFilesNotFound); +} + +TEST_F(TargetConfigReaderFixture, ConfigReaderShallParseEcuIdFromEcuConfig) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "TargetConfigReader shall parse the DLT ECU ID from the configuration file correctly."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_EQ(GetReader().ReadConfig()->GetEcuId(), kEcuConfigEcuId); +} + +TEST_F(TargetConfigReaderFixture, ConfigReaderShallParseAppIdFromAppConfig) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "TargetConfigReader shall parse the DLT Application ID from the configuration file correctly."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_EQ(GetReader().ReadConfig()->GetAppId(), kAppConfigAppId); +} + +TEST_F(TargetConfigReaderFixture, ConfigReaderShallParseLogLevelFromAppConfig) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "TargetConfigReader shall parse the DLT Application log level from the configuration file correctly."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_EQ(GetReader().ReadConfig()->GetDefaultLogLevel(), kAppConfigLogLevel); +} + +TEST_F(TargetConfigReaderFixture, ConfigReaderShallParseLogModeFromAppConfig) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "TargetConfigReader shall parse the logging mode from the configuration file correctly."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_EQ(GetReader().ReadConfig()->GetLogMode(), kAppConfigLogMode); +} + +TEST_F(TargetConfigReaderFixture, ConfigReaderShallParseAppDescriptionFromAppConfig) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "TargetConfigReader shall parse the application description from the configuration file correctly."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_EQ(GetReader().ReadConfig()->GetAppDescription(), kAppDescription); +} + +TEST_F(TargetConfigReaderFixture, ConfigReaderShallParseBufferOverwriteOnFullStatusFromAppConfig) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "TargetConfigReader shall parse the overwrite ring buffer option from the configuration file correctly."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_TRUE(GetReader().ReadConfig()->GetRingBufferOverwriteOnFull()); +} + +TEST_F(TargetConfigReaderFixture, ConfigReaderShallParseStackBufferSizeFromEcuConfig) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "TargetConfigReader shall parse the stack buffer size in shared memory from the configuration file correctly."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_EQ(GetReader().ReadConfig()->GetStackBufferSize(), kEcuConfigStackBufferSize); +} + +TEST_F(TargetConfigReaderFixture, ConfigReaderShallParseRingBufferSizeFromEcuConfig) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "TargetConfigReader shall parse the ring buffer size in shared memory from the configuration file correctly."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_EQ(GetReader().ReadConfig()->GetRingBufferSize(), kEcuConfigRingBufferSize); +} + +TEST_F(TargetConfigReaderFixture, ConfigReaderShallParseLogLevelConsoleFromEcuConfig) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "TargetConfigReader shall parse the default log level threshold for console logging from the " + "configuration file correctly."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_EQ(GetReader().ReadConfig()->GetDefaultConsoleLogLevel(), kEcuConfigLogLevelConsole); +} + +TEST_F(TargetConfigReaderFixture, ConfigReaderShallParseLogFilePathFromAppConfig) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "TargetConfigReader shall parse the default log file path for console logging from the " + "configuration file correctly."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_EQ(GetReader().ReadConfig()->GetLogFilePath(), kAppConfigLogFilePath); +} + +TEST_F(TargetConfigReaderFixture, ConfigReaderShallParseContextLogLevelFromEcuAndAppConfig) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "TargetConfigReader shall parse and combine the context log levels from the " + "configuration files correctly."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_EQ(GetReader().ReadConfig()->GetContextLogLevel(), kCombinedContextLogLevel); +} + +TEST_F(TargetConfigReaderFixture, ConfigReaderShallParseNumberOfSlots) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TargetConfigReader shall parse the number of slots for preallocation correctly."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_EQ(GetReader().ReadConfig()->GetNumberOfSlots(), kNumberOfSlots); +} + +TEST_F(TargetConfigReaderFixture, ConfigReaderShallParseSlotSizeBytes) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TargetConfigReader shall parse the size of the slots for preallocation correctly."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_EQ(GetReader().ReadConfig()->GetSlotSizeInBytes(), kSlotSizeBytes); +} + +TEST_F(TargetConfigReaderFixture, ConfigReaderShallParseDynamicDatarouterIdentifiers) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "TargetConfigReader shall parse if datarouter dyanmic identifiers flag is enabled or not."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_EQ(GetReader().ReadConfig()->GetDynamicDatarouterIdentifiers(), kDynamicDatarouterIdentifiers); +} + +TEST_F(TargetConfigReaderFixture, ConfigReaderShallParseDataRouterUid) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TargetConfigReader shall parse datarouter user ID."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_EQ(GetReader().ReadConfig()->GetDataRouterUid(), kDatarouterUid); +} + +TEST_F(TargetConfigReaderFixture, AppConfigSyntaxErrorShallFallbackToEcuConfig) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "TargetConfigReader fall back to the ECU config file if the application config files contains syntax errors."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // The application config contains a syntax error. + SetConfigurationFiles({kEcuConfigFile, kSyntaxErrorConfigFile}); + + // ReadConfig shall still return the value from the ECU config. + EXPECT_EQ(GetReader().ReadConfig()->GetAppId(), kEcuConfigAppId); +} + +TEST_F(TargetConfigReaderFixture, WrongStructureConfigFileShallCauseDefaultAppId) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "TargetConfigReader fall back to the hard-coded default application id if the configuration file " + "does not contain a valid JSON structure"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // The application config has wrong structure. + SetConfigurationFiles({kErrorContent}); + + // ReadConfig shall return the default value. + EXPECT_EQ(GetReader().ReadConfig()->GetAppId(), kDefaultConfigAppId); +} + +TEST_F(TargetConfigReaderFixture, WrongLogLevelValueShallFallbackToEcuConfig) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "TargetConfigReader fall back to the ECU config file if the if the logLevelThresholdConsole " + "has wrong value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // The application config has wrong structure. + SetConfigurationFiles({kWrongLogLevel}); + + // ReadConfig shall return the default value. + EXPECT_EQ(GetReader().ReadConfig()->GetAppId(), kEcuConfigAppId); +} + +TEST_F(TargetConfigReaderFixture, AppConfigInvalidLogLevelFallbackToEcuConfig) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "TargetConfigReader fall back to the valid value from the ECU configuration file if the application " + "config file contains an invalid log level."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // The application config contains invalid default log level. + SetConfigurationFiles({kEcuConfigFile, kInvalidAppConfigFile}); + + // ReadConfig shall still return the value from the ECU config. + EXPECT_EQ(GetReader().ReadConfig()->GetDefaultLogLevel(), kEcuConfigLogLevel); +} + +TEST_F(TargetConfigReaderFixture, AppConfigInvalidLogModeFallbackToEcuConfig) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "TargetConfigReader fall back to the valid value from the ECU configuration file if the application " + "config file contains an invalid log mode."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // The application config contains invalid log mode. + SetConfigurationFiles({kEcuConfigFile, kInvalidAppConfigFile}); + + // ReadConfig shall still return the value from the ECU config. + EXPECT_EQ(GetReader().ReadConfig()->GetLogMode(), kEcuConfigLogMode); +} + +TEST_F(TargetConfigReaderFixture, AppConfigInvalidContextConfigFallbackToEcuConfig) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "TargetConfigReader fall back to the valid value from the ECU configuration file if the application " + "config file contains an invalid log level for a context."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // The application config contains invalid context log level entries. + SetConfigurationFiles({kEcuConfigFile, kInvalidAppConfigFile}); + + // ReadConfig shall still return the value from the ECU config. + EXPECT_EQ(GetReader().ReadConfig()->GetContextLogLevel(), kEcuConfigContextLogLevel); +} + +TEST_F(TargetConfigReaderFixture, WrongEntriesToContextConfigslShallReturnEmptyContextLogLevel) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "TargetConfigReader shall return empty context config log level when providing wrong entries."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // The application config contains invalid context config entries. + SetConfigurationFiles({kWrongContextConfig}); + + struct a : amp::blank + { + }; + // ReadConfig shall returns empty context. + EXPECT_TRUE(GetReader().ReadConfig()->GetContextLogLevel().empty()); +} + +TEST_F(TargetConfigReaderFixture, WhenInvalidFilePathReaderShallReturnDefaultAppId) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "TargetConfigReader fall back to the hard-coded default application id if the configuration file " + "does not exist"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // The application config does not exist + SetConfigurationFiles({kInvalidConfigFilePath}); + + // ReadConfig shall return the default value. + EXPECT_EQ(GetReader().ReadConfig()->GetAppId(), kDefaultConfigAppId); +} + +TEST_F(TargetConfigReaderFixture, EmptyConfigFileShallCauseDefaultAppId) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "TargetConfigReader fall back to the hard-coded default application id if the configuration file " + "does not contain any value"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // The application config does not exist + SetConfigurationFiles({kEmptyConfigFile}); + + // ReadConfig shall return the default value. + EXPECT_EQ(GetReader().ReadConfig()->GetAppId(), kDefaultConfigAppId); +} + +TEST_F(TargetConfigReaderFixture, ConfigReaderShallFallBackToContextLogLevelDefaultWhenNoValidValueInConfigurationFiles) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "TargetConfigReader fall back to the hard-coded default context log level values if there is no " + "valid value in the configuration files"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + SetConfigurationFiles({kCtxLevelBrokenConfigFile}); + + EXPECT_EQ(GetReader().ReadConfig()->GetContextLogLevel(), ContextLogLevelMap{}); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/configuration/test/data/App1.secpol b/mw/log/configuration/test/data/App1.secpol new file mode 100644 index 0000000..ea625c6 --- /dev/null +++ b/mw/log/configuration/test/data/App1.secpol @@ -0,0 +1,14 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + + +type App1_t; +allow App1_t self:ability {}; diff --git a/mw/log/configuration/test/data/app_config.json b/mw/log/configuration/test/data/app_config.json new file mode 100644 index 0000000..77ab138 --- /dev/null +++ b/mw/log/configuration/test/data/app_config.json @@ -0,0 +1,47 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "appId": "App1", + "appDesc": "Application One Description", + "logLevel": "kError", + "logMode": "kRemote", + "overwriteOnFull": "true", + "contextConfigs":[ + { + "name": "DTC", + "logLevel": "kInfo" + }, + { + "name": "FOO", + "logLevel": "kWarning" + }, + { + "name": "vcip", + "logLevel": "kDebug" + }, + [] + ], + "logFilePath": "/var/tmp", + "mac_configuration": [ + { + "variant": "base", + "config": {} + }, + { + "variant": "qnx", + "config": { + "policy_path": "/proc/boot/secpol.bin", + "application_type": "App1_t" + } + } + ] +} diff --git a/mw/log/configuration/test/data/context_level_broken_config.json b/mw/log/configuration/test/data/context_level_broken_config.json new file mode 100644 index 0000000..6de3a9c --- /dev/null +++ b/mw/log/configuration/test/data/context_level_broken_config.json @@ -0,0 +1,15 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "appId": "CTXB", + "contextConfigs":"abc" +} diff --git a/mw/log/configuration/test/data/ecu_config.json b/mw/log/configuration/test/data/ecu_config.json new file mode 100644 index 0000000..b99ed59 --- /dev/null +++ b/mw/log/configuration/test/data/ecu_config.json @@ -0,0 +1,37 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "ecuId": "ECU1", + "appId": "UNKN", + "logLevel": "kInfo", + "logMode": "kRemote|kConsole|kFile|kSystem", + "contextConfigs": [ + { + "name": "vcip", + "logLevel": "kError" + }, + { + "name": "vcom", + "logLevel": "kOff" + } + ], + "stackBufferSize": 3000, + "ringBufferSize": 4096, + "logLevelThresholdConsole": "kVerbose", + "overwriteOnFull": true, + "logFilePath": "/tmp", + "numberOfSlots": 8, + "slotSizeBytes": 1500, + "safeDatarouterIpc": true, + "datarouterUid": 1038, + "dynamicDatarouterIdentifiers" : true +} diff --git a/mw/log/configuration/test/data/empty-class-id.json b/mw/log/configuration/test/data/empty-class-id.json new file mode 100644 index 0000000..d3c008b --- /dev/null +++ b/mw/log/configuration/test/data/empty-class-id.json @@ -0,0 +1,11 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + diff --git a/mw/log/configuration/test/data/empty-json-class-id.json b/mw/log/configuration/test/data/empty-json-class-id.json new file mode 100644 index 0000000..9afa785 --- /dev/null +++ b/mw/log/configuration/test/data/empty-json-class-id.json @@ -0,0 +1,13 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ +} diff --git a/mw/log/configuration/test/data/empty_config.json b/mw/log/configuration/test/data/empty_config.json new file mode 100644 index 0000000..2d5c12f --- /dev/null +++ b/mw/log/configuration/test/data/empty_config.json @@ -0,0 +1,12 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +[] diff --git a/mw/log/configuration/test/data/error-content-1-json-class-id.json b/mw/log/configuration/test/data/error-content-1-json-class-id.json new file mode 100644 index 0000000..ea8af16 --- /dev/null +++ b/mw/log/configuration/test/data/error-content-1-json-class-id.json @@ -0,0 +1,72 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "adp::perception::CrocStateTraceable": { + "id": 8650814, + "ctxid": "CRoC", + "appid": "Plan", + "loglevel": 3 + }, + "adp::perception::obstacles::ObstaclesTraceable": { + "id": 8650815, + "ctxid": "OBSF", + "appid": "Plan", + "loglevel": 3 + }, + "adp::planning::awa::DebugData": { + "id": 8650816, + "ctxid": "AWA", + "appid": "Plan", + "loglevel": 2 + }, + "adp::planning::driving_tube::DiagnosticLogsData": { + "id": 8650817, + "ctxid": "DTNV", + "appid": "Plan", + "loglevel": 4 + }, + "bmw::logging::timesync::DltTimeSyncTimestamp": { + "id": 27, + "ctxid": "TSNC", + "appid": "STDF", + "loglevel": 2 + }, + "aas::logging::ReprocessingEvent": { + "id": 8650752, + "ctxid": "Repr", + "appid": "Plan", + "loglevel": 4 + }, + "adp::time::StdTimestamp": { + "id": 8650753, + "appid": "DR", + "loglevel": 4 + }, + "adp::logging::DynamicInsight": { + "id": 8650754, + "ctxid": "VIS", + "appid": "Plan", + "loglevel": 3 + }, + "bmw::sli::CommonStrategiesConfig": { + "id": 8458555, + "ctxid": "sli", + "appid": "Fasi", + "loglevel": 4 + }, + "bmw::sli::TsfBaseConfig": { + "id": 8458423, + "ctxid": "sli", + "appid": "Fasi", + "loglevel": 4 + } +} diff --git a/mw/log/configuration/test/data/error-content-2-json-class-id.json b/mw/log/configuration/test/data/error-content-2-json-class-id.json new file mode 100644 index 0000000..fd575c5 --- /dev/null +++ b/mw/log/configuration/test/data/error-content-2-json-class-id.json @@ -0,0 +1,72 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "adp::perception::CrocStateTraceable": { + "id": 8650814, + "ctxid": "CRoC", + "appid": "Plan", + "loglevel": 3 + }, + "adp::perception::obstacles::ObstaclesTraceable": { + "id": 8650815, + "ctxid": "OBSF", + "appid": "Plan", + "loglevel": 3 + }, + "adp::planning::awa::DebugData": { + "id": 8650816, + "ctxid": "AWA", + "appid": "Plan", + "loglevel": 2 + }, + "adp::planning::driving_tube::DiagnosticLogsData": { + "id": 8650817, + "ctxid": "DTNV", + "appid": "Plan", + "loglevel": 4 + }, + "bmw::logging::timesync::DltTimeSyncTimestamp": { + "id": 27, + "ctxid": "TSNC", + "appid": "STDF", + "loglevel": 2 + }, + "aas::logging::ReprocessingEvent": { + "id": 8650752, + "ctxid": "Repr", + "appid": "Plan", + "loglevel": 4 + }, + "adp::time::StdTimestamp": { + "id": 8650753, + "ctxid": "CTX1", + "loglevel": 4 + }, + "adp::logging::DynamicInsight": { + "id": 8650754, + "ctxid": "VIS", + "appid": "Plan", + "loglevel": 3 + }, + "bmw::sli::CommonStrategiesConfig": { + "id": 8458555, + "ctxid": "sli", + "appid": "Fasi", + "loglevel": 4 + }, + "bmw::sli::TsfBaseConfig": { + "id": 8458423, + "ctxid": "sli", + "appid": "Fasi", + "loglevel": 4 + } +} diff --git a/mw/log/configuration/test/data/error-content-3-json-class-id.json b/mw/log/configuration/test/data/error-content-3-json-class-id.json new file mode 100644 index 0000000..328577b --- /dev/null +++ b/mw/log/configuration/test/data/error-content-3-json-class-id.json @@ -0,0 +1,72 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "adp::perception::CrocStateTraceable": { + "id": 8650814, + "ctxid": "CRoC", + "appid": "Plan", + "loglevel": 3 + }, + "adp::perception::obstacles::ObstaclesTraceable": { + "ctxid": "OBSF", + "appid": "Plan", + "loglevel": 3 + }, + "adp::planning::awa::DebugData": { + "id": 8650816, + "ctxid": "AWA", + "appid": "Plan", + "loglevel": 2 + }, + "adp::planning::driving_tube::DiagnosticLogsData": { + "id": 8650817, + "ctxid": "DTNV", + "appid": "Plan", + "loglevel": 4 + }, + "bmw::logging::timesync::DltTimeSyncTimestamp": { + "id": 27, + "ctxid": "TSNC", + "appid": "STDF", + "loglevel": 2 + }, + "aas::logging::ReprocessingEvent": { + "id": 8650752, + "ctxid": "Repr", + "appid": "Plan", + "loglevel": 4 + }, + "adp::time::StdTimestamp": { + "id": 8650753, + "ctxid": "CTX1", + "appid": "DR", + "loglevel": 4 + }, + "adp::logging::DynamicInsight": { + "id": 8650754, + "ctxid": "VIS", + "appid": "Plan", + "loglevel": 3 + }, + "bmw::sli::CommonStrategiesConfig": { + "id": 8458555, + "ctxid": "sli", + "appid": "Fasi", + "loglevel": 4 + }, + "bmw::sli::TsfBaseConfig": { + "id": 8458423, + "ctxid": "sli", + "appid": "Fasi", + "loglevel": 4 + } +} diff --git a/mw/log/configuration/test/data/error-content-wrong-id-value.json b/mw/log/configuration/test/data/error-content-wrong-id-value.json new file mode 100644 index 0000000..22391fd --- /dev/null +++ b/mw/log/configuration/test/data/error-content-wrong-id-value.json @@ -0,0 +1,19 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "adp::perception::CrocStateTraceable": { + "id": "string instead of int", + "ctxid": "CRoC", + "appid": "Plan", + "loglevel": 3 + } +} diff --git a/mw/log/configuration/test/data/error-json-structure.json b/mw/log/configuration/test/data/error-json-structure.json new file mode 100644 index 0000000..962f03b --- /dev/null +++ b/mw/log/configuration/test/data/error-json-structure.json @@ -0,0 +1,12 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +[] \ No newline at end of file diff --git a/mw/log/configuration/test/data/error-parse-1-json-class-id.json b/mw/log/configuration/test/data/error-parse-1-json-class-id.json new file mode 100644 index 0000000..7d473ed --- /dev/null +++ b/mw/log/configuration/test/data/error-parse-1-json-class-id.json @@ -0,0 +1,68 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "adp::perception::CrocStateTraceable": { + "id": 8650814, + "ctxid": "CRoC", + "appid": "Plan", + "loglevel": 3 + }, + "adp::perception::obstacles::ObstaclesTraceable": { + "id": 8650815, + "ctxid": "OBSF", + "appid": "Plan", + "loglevel": 3 + }, + "adp::planning::awa::DebugData": [], + "adp::planning::driving_tube::DiagnosticLogsData": { + "id": 8650817, + "ctxid": "DTNV", + "appid": "Plan", + "loglevel": 4 + }, + "bmw::logging::timesync::DltTimeSyncTimestamp": { + "id": 27, + "ctxid": "TSNC", + "appid": "STDF", + "loglevel": 2 + }, + "aas::logging::ReprocessingEvent": { + "id": 8650752, + "ctxid": "Repr", + "appid": "Plan", + "loglevel": 4 + }, + "adp::time::StdTimestamp": { + "id": 8650753, + "ctxid": "CTX1", + "appid": "DR", + "loglevel": 4 + }, + "adp::logging::DynamicInsight": { + "id": 8650754, + "ctxid": "VIS", + "appid": "Plan", + "loglevel": 3 + }, + "bmw::sli::CommonStrategiesConfig": { + "id": 8458555, + "ctxid": "sli", + "appid": "Fasi", + "loglevel": 4 + }, + "bmw::sli::TsfBaseConfig": { + "id": 8458423, + "ctxid": "sli", + "appid": "Fasi", + "loglevel": 4 + } +} diff --git a/mw/log/configuration/test/data/error-parse-empty-json-object.json b/mw/log/configuration/test/data/error-parse-empty-json-object.json new file mode 100644 index 0000000..2d5c12f --- /dev/null +++ b/mw/log/configuration/test/data/error-parse-empty-json-object.json @@ -0,0 +1,12 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +[] diff --git a/mw/log/configuration/test/data/invalid_app_config.json b/mw/log/configuration/test/data/invalid_app_config.json new file mode 100644 index 0000000..2f0315c --- /dev/null +++ b/mw/log/configuration/test/data/invalid_app_config.json @@ -0,0 +1,37 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "appId": "App1", + "logLevel": "kFoobar", + "logMode": "kNull", + "contextConfigs":[ + { + "name": "DTC", + "logLevel": "kInfo" + }, + { + "name": "FOO", + "logLevel": "kWarning" + }, + { + "name": "vcip", + "logLevel": "kBlah" + }, + { + "logLevel": "kInfo" + }, + { + "name": "Nan" + } + ], + "logFilePath": "/var/tmp" +} \ No newline at end of file diff --git a/mw/log/configuration/test/data/second-test-class-id.json b/mw/log/configuration/test/data/second-test-class-id.json new file mode 100644 index 0000000..9e1cf56 --- /dev/null +++ b/mw/log/configuration/test/data/second-test-class-id.json @@ -0,0 +1,73 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "adp::perception::CrocStateTraceable": { + "id": 8650814, + "ctxid": "CRoC", + "appid": "Plan", + "loglevel": 3 + }, + "adp::perception::obstacles::ObstaclesTraceable": { + "id": 8650815, + "ctxid": "OBSF", + "appid": "Plan", + "loglevel": 3 + }, + "adp::planning::awa::DebugData": { + "id": 8650816, + "ctxid": "AWA", + "appid": "Plan", + "loglevel": 2 + }, + "adp::planning::driving_tube::DiagnosticLogsData": { + "id": 8650817, + "ctxid": "DTNV", + "appid": "Plan", + "loglevel": 4 + }, + "bmw::logging::timesync::DltTimeSyncTimestamp": { + "id": 27, + "ctxid": "TSNC", + "appid": "STDF", + "loglevel": 2 + }, + "aas::logging::ReprocessingEvent": { + "id": 8650752, + "ctxid": "Repr", + "appid": "Plan", + "loglevel": 4 + }, + "adp::time::StdTimestamp": { + "id": 8650753, + "ctxid": "CTX1", + "appid": "DR", + "loglevel": 4 + }, + "adp::logging::DynamicInsight": { + "id": 8650754, + "ctxid": "VIS", + "appid": "Plan", + "loglevel": 3 + }, + "bmw::sli::CommonStrategiesConfig": { + "id": 8458555, + "ctxid": "sli", + "appid": "Fasi", + "loglevel": 4 + }, + "bmw::sli::TsfBaseConfig": { + "id": 8458423, + "ctxid": "sli", + "appid": "Fasi", + "loglevel": 4 + } +} diff --git a/mw/log/configuration/test/data/syntax_error.json b/mw/log/configuration/test/data/syntax_error.json new file mode 100644 index 0000000..92cf660 --- /dev/null +++ b/mw/log/configuration/test/data/syntax_error.json @@ -0,0 +1,15 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "appId": "foo" + "logLevel": "bar, +} \ No newline at end of file diff --git a/mw/log/configuration/test/data/test-class-id.json b/mw/log/configuration/test/data/test-class-id.json new file mode 100644 index 0000000..b967a36 --- /dev/null +++ b/mw/log/configuration/test/data/test-class-id.json @@ -0,0 +1,36 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "bmw::logging::PersistentLogFileEvent": { + "id": 301, + "ctxid": "PERL", + "appid": "DRC", + "loglevel": 1 + }, + "aas::logging::ReprocessingCycle": { + "id": 150, + "ctxid": "Repr", + "appid": "Plan", + "loglevel": 4 + }, + "poseng::logging::ReprocessingCycle": { + "id": 41000, + "ctxid": "Cycl", + "appid": "PE" + }, + "LogMark::stdframe": { + "id": 10, + "ctxid": "STDA", + "appid": "STDF", + "loglevel": 2 + } +} diff --git a/mw/log/configuration/test/data/wrong-context-config-value.json b/mw/log/configuration/test/data/wrong-context-config-value.json new file mode 100644 index 0000000..b112a1c --- /dev/null +++ b/mw/log/configuration/test/data/wrong-context-config-value.json @@ -0,0 +1,18 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "ecuId": "ECU1", + "appId": "UNKN", + "logLevel": "kInfo", + "logMode": "kRemote|kConsole|kFile|kSystem", + "contextConfigs": "" +} \ No newline at end of file diff --git a/mw/log/configuration/test/data/wrong-loglevel-value.json b/mw/log/configuration/test/data/wrong-loglevel-value.json new file mode 100644 index 0000000..e50f9df --- /dev/null +++ b/mw/log/configuration/test/data/wrong-loglevel-value.json @@ -0,0 +1,30 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "ecuId": "ECU1", + "appId": "UNKN", + "logLevel": "kInfo", + "logMode": "kRemote|kConsole|kFile|kSystem", + "contextConfigs": [ + { + "name": "vcip", + "logLevel": "kError" + }, + { + "name": "vcom", + "logLevel": "kOff" + } + ], + "stackBufferSize": 3000, + "ringBufferSize": 4096, + "logLevelThresholdConsole": "" +} \ No newline at end of file diff --git a/mw/log/design/README.md b/mw/log/design/README.md new file mode 100644 index 0000000..d7fba5c --- /dev/null +++ b/mw/log/design/README.md @@ -0,0 +1,203 @@ + + + + +# Logging + +## History and Motivation + +We had two different logging implementations. One is the `ara::log` implementation, the other one is +the `lib::logging` one. Unfortunately, `lib::logging` is not a real abstraction of `ara::log` since it exposes its +symbols. + +Most of our applications use `ara::log` directly, also in their business logic. The problem arises that +`ara::log` has a strong dependency to `ara::core`. The second problem is that a lot of our current +unit-tests are mocking the Adaptive AUTOSAR Stack. Especially the `ara::com` interfaces. These interfaces also have a +dependency to `ara::core`. Since we don't want to have a dependency to the Supplier middleware communication stack in +our Unit-Tests, we also have a custom implementation of `ara::core`. Due to miss-use (also in transitive dependencies) +we have issues with this situation on a daily basis. For example duplicated symbols of `ara::core` types in tests. + +It could be that in future for our use case we may not need the Adaptive AUTOSAR Stack itself. Leading to the +motivation for the `mw`-Abstraction. Logging is an essential part and will be used in any business logic. +Another motivation is to provide a safe logging library to use in an ASIL-B context. + +## Introduction + +Within this document we are describing an improved way to implement a client logging library. We are especially not +describing how to implement the `ara::log` API, since it will depend on third-party deliveries (e.g. the Supplier stack). + +This proposal has few benefits over the older implementation: + +- No dependency to `ara::core` and the adaptive AUTOSAR Stack +- Object-Seam way for mocking (no link-time mocking) +- No dynamic memory allocation as end goal (as step in between we will have still _one_ location of dynamic memory + allocation) +- Faster & more Efficient + +Due to the nature of logging we want to support free functions. A Removal of free-functions would lead to a major issue +in all current users and thus is impractical. Also a integration into `ara::log` would not be possible, since the API +clearly forces free functions. Thus, the abstraction is not straight forward and needs to be split into two parts. A not +mockable and a mockable part. + +Before going into detail on these two parts, another basic principle shall be agreed on. C++ mainly +uses [streams](https://en.cppreference.com/w/cpp/io) to perform input and output operations. We want to follow this +example to provide a consistent interface for the general C++ user. We understand that there are individuals and even +use-cases where a function-based logging improves the overall situation. It shall be noted that we do not blindly +decline these requests, but in order to optimally use the [DLT protocol](#dlt-formatted-payload) (which is our main +backend), a stream based approach has clear benefits as explained later. + +## Design + +The main point of our design is based on a slot oriented lock-free ringbuffer, where each slot can be marked as +currently used. This kind of data structure is illustrated in the following graphic. +![Buffer Structure](./buffer_data_structure.svg) + +The goal is to create slots that allow individual writing and reading simultaneously. The reader can determine +whether they can continue or not by checking a flag indicating if the slot has been written to. If each slot is +filled, the writer will - like in a normal ring buffer - wrap around. This data structure includes multiple +ideas that are important in the context of logging. + +First, the buffer size (per slot and the number of slots) is allocated at creation time. This will ensure that no +dynamic memory will be allocated during runtime. Which will improve speed as also it fulfills our safety requirements. + +Second, it allows multiple writes at the same time, without requiring that the start / stop in a synchronized manner. +This is particularly important for a use case where we have nested log messages. + +``` +LogError() << "My message:" << func(); + + +int func() { + LogError() << "My nested message"; + return 1; +} +``` + +It shall be recognized that even within one thread, one message is started to be written, then we start and finish +another one, only to then finish the former. Locking our global data structure for each _stream_ would lead to a +deadlock. + +Third, due to the non locking nature, we can write from multiple threads in parallel into the data structure. + +We willingly accept the trade-off of sacrificing space in order to achieve a stable runtime behavior, which +includes being lock-free and not using dynamic memory. + +While lock free data structures are not a novel idea, the specific constrains given in this part for this data +structure, could be novel and a further literature research shall be applied. + +Nevertheless, starting from this general idea, we separate our static design into the following major four parts: + +1. `Formatter` + We must support different kind of logging formats. The most major one is the [DLT format](). + Another one is the raw format (no special formatting applied). A future extension with other formats will be easy, + separating this concern from the others mentioned below. +2. `Backend` + This represents the place where the system will store the logging messages, send them to other processes, or + take any other applicable action. The main idea is that someone can ask to reserve a slot, then get the memory + address of that slot and then again notify that the writing to the slot has been finished. Basically + the `Backend` abstracts from our data structure that was previously described. +3. `Recorder` + In any case, the end-user will always expect a combination of `Formatter` and `Backend`. e.g. it does not make sense + to send raw formatted streams via DLT. But it could make sense to store DLT-formatted messages in a file. So + a `Recorder` encapsulates a combination of `Formatter` and `Backend` for a user. +4. `LogStream` + Is the real user facing API. While theoretically it could also makes sense that a user directly access a recorder, it + makes more sense to provide a RAII pattern from him to easily interact with our API. This is exactly done by our + `LogStream`. Upon creation, a slot will be reserved and is _owned_ by the `LogStream`. Upon destruction, the + respective slot will be made available again. + +This design allows that any content streamed by the user into a `LogStream`, will directly end up in the respective slot +implementation. + +![Verbose Logging Static Design](/swh/ddad_platform/aas/mw/log/design/verbose_logging_static.uxf?ref=2abe1e60f3585f2181807bdbcdf0075a274ba321) + +![Sequence Design](/swh/ddad_platform/aas/mw/log/design/verbose_logging_sequence.uxf?ref=a0f7d7e092a6d561d0c889a2faf752acc969f474) + +![Non-Verbose Logging Static Design](/swh/ddad_platform/aas/mw/log/design/non_verbose_logging_static.uxf?ref=a0f7d7e092a6d561d0c889a2faf752acc969f474) + +![ErrorDomain](/swh/ddad_platform/aas/mw/log/design/error_domain.uxf?ref=2abe1e60f3585f2181807bdbcdf0075a274ba321) + +### Types of recorders + +Following recorders are supported: +1. DataRouterRecorder - which enables remote logging, which is the most common. +2. TextRecorder - which enables logging to a text based backend (e.g. console, or system backend). +3. FileRecorder - which enables logging to the file. +4. CompositeRecorder - which may combine multiple logging modes mentioned above, what means that enables logging + to e.g. console and file in parallel. +5. EmptyRecoder - used when logging is off. +Because of the similarity between TextRecorder and FileRecorder it was decided to use the common Backend for both. +The static design of these recorders is presented below. + +![Recorders Static Design](/swh/ddad_platform/aas/mw/log/design/mw_log_recorders.uxf?ref=2abe1e60f3585f2181807bdbcdf0075a274ba321) + +![DataRouterRecorder Static Design](/swh/ddad_platform/aas/mw/log/design/mw_log_datarouter_recorder.uxf?ref=a0f7d7e092a6d561d0c889a2faf752acc969f474) + +### Activity diagrams + +![CircularAllocator::AcquireSlotToWrite Activity diagram](/swh/ddad_platform/aas/mw/log/design/circular_buffer_allocator_acquireslottowrite.uxf?ref=7eb439030d5c699c020186618c251cc897150bcd) + +![DatarouterMessageClientImpl::ConnectToDatarouter Activity diagram](/swh/ddad_platform/aas/mw/log/design/datarouter_message_client_impl_connecttodatarouter.uxf?ref=bf407faf781174023c3d921cbdf176dd5ef718c3) + +![SharedMemoryReader::Read Activity diagram](/swh/ddad_platform/aas/mw/log/design/shared_memory_reader_read.uxf?ref=bf407faf781174023c3d921cbdf176dd5ef718c3) + +![SharedMemoryReader::AllocAndWrite Activity diagram](/swh/ddad_platform/aas/mw/log/design/shared_memory_writer_allocandwrite.uxf?ref=bf407faf781174023c3d921cbdf176dd5ef718c3) + +![WaitFreeStack::TryPush Activity diagram](/swh/ddad_platform/aas/mw/log/design/wait_free_stack_trypush.uxf?ref=bf407faf781174023c3d921cbdf176dd5ef718c3) + +### DataRouter backend + +The detailed design for the Safe Datarouter backend is avaiable [here](./datarouter_backend/README.md) + +### Integration with ara::log + +The main idea is that `ara::log` will just act as facade for our abstraction. The careful reader will say that this +sentence does not make any sense and that is true. Since we are controlling the `ara::log` implementation, it does not +make sense to create an abstraction for it and thus loos runtime performance. Thus, we will treat our logging +implementation as first-class-citizen and `ara::log` will have the overhead for the facade, even though it might be +quite small (e.g. one addition function call). Apparently `ara::log` will have certain functionalities which are not +supported by our mw-implementation. It will even have Vector-Specific extensions. We accept that and try to keep these +extensions to a minimum to reduce maintaining efforts. + +![Facade](/swh/ddad_platform/aas/mw/log/design/verbose_logging_ara_log_interaction.uxf?ref=a943a898650ec31fc9c7f95961164c09cc84fe95) + +### User-Facing APIs + +As mentioned the `LogStream` will be the actual user facing API for streaming content into our buffers. But the creation +of this `LogStream` is a little tricky, since the injection of the correct `Recorder` is required. We don't want to +bother the user in the normal case with this behaviour. Which is why we make the `LogStream` only constructable by our +factory method. The factory method could be invoked by the user, but is in a respective implementation specific +namespace. At the end, the user will be either able to create a `LogStream` via our free-functions (stated in the +beginning of this document), or a context aware logger. At the end it shall be noted, that the ownership of +the `LogStream` is transferred from the middleware to the user, which is why these calls must be performed by value and +not interface is possible at that point in time. This transfer of ownership is important, since otherwise further +dynamic memory allocation would be necessary. + +## Library Dependencies + +A comprehensive list of library dependencies for mw::log can be queried using bazel via: +`bazel query 'kind("cc_library", deps(//platform/aas/mw/log))'` + +Few of the external dependencies include: +- @amp//:amp +- @amp//:math +- @xpad_qnx_sdp//:libsecpol +- @xpad_qnx_sdp//:libslog2 +- @ipnext_qnx_sdp//:libsecpol +- @ipnext_qnx_sdp//:libslog2 +- @bazel_tools//third_party/def_parser:def_parser_lib +- @bazel_tools//tools/cpp:malloc + +## How-Tos + +In order to better understand the design we describe different common use-cases for users of the mw::log APIs: See [Usage](/swh/ddad_platform/blob/master/aas/mw/log/README.md#usage) diff --git a/mw/log/design/buffer_data_structure.svg b/mw/log/design/buffer_data_structure.svg new file mode 100644 index 0000000..5f10a62 --- /dev/null +++ b/mw/log/design/buffer_data_structure.svg @@ -0,0 +1,2 @@ + +Slotcurrently written?static buffer1No2No3Yes4Yes5NoC0readMultiple Producer Single Consumer Slot based Ring Buffer (with reserved slot size)P1P2writewritewasted spaceparallel accessusedspace \ No newline at end of file diff --git a/mw/log/design/circular_buffer_allocator_acquireslottowrite.uxf b/mw/log/design/circular_buffer_allocator_acquireslottowrite.uxf new file mode 100644 index 0000000..d085497 --- /dev/null +++ b/mw/log/design/circular_buffer_allocator_acquireslottowrite.uxf @@ -0,0 +1,15 @@ +10Return slot indexUMLSpecialState240402020type=initialRelation240503060lt=<-10;40;10;10Relation2401203070lt=<-10;50;10;10UMLState14017022040Increment loop counterUMLSpecialState120310260100Loop counter > circular buffer size +bg=red +type=decisionRelation2402703060lt=<-10;40;10;10UMLState14024022040Increment claimed slotRelation2402003060lt=<-10;40;10;10Relation70350180470lt=<- +m2=[Yes]160;450;160;420;30;420;30;10;50;10Relation2404005060lt=<- +[No]10;40;10;10Relation2404703060lt=<-10;40;10;10UMLSpecialState2408302020type=final +UMLState1409022040Loop counter = 0UMLState14044022040Slot index = claimed slot % circular +buffer capacityUMLSpecialState120510260100Circular buffer at Slot index in use? +bg=red +type=decisionUMLState14064022040Mark Slot as in useUMLState14071022040Return Slot indexRelation2406005060lt=<- +[No]10;40;10;10Relation2406703060lt=<-10;40;10;10Relation2407403080lt=<-10;60;10;10Relation240140200450lt=<- +m2=[Yes]10;10;170;10;170;420;140;420UMLObject700370870CircularAllocator::AcquireSlotToWrite +valign=topUMLSyncBarHorizontal2107908020lw=5 +Relation2407903060lt=->10;10;10;40Text1808206040Finish +halign=center +style=autoresize \ No newline at end of file diff --git a/mw/log/design/configuration_design.md b/mw/log/design/configuration_design.md new file mode 100644 index 0000000..6fff113 --- /dev/null +++ b/mw/log/design/configuration_design.md @@ -0,0 +1,137 @@ + + + + +# Low Level Design of the Configuration and Initialization for `mw::log` + +- [Low Level Design of the Configuration and Initialization for `mw::log`](#low-level-design-of-the-configuration-and-initialization-for-mwlog) + - [Introduction](#introduction) + - [Use Cases](#use-cases) + - [Class Diagram](#class-diagram) + - [Initialization Sequence Diagram](#initialization-sequence-diagram) + +## Introduction + +Many aspects in the logging library shall be configurable for the user in static +configuration files. For instance, the user wants to change the minimum logging +level without recompiling the code just by modifying a configuration file on +target. + +For Verbose Logging, an adaptive application on target loads two configuration files: + +1. The ECU-wide configuration file under `/etc/ecu_logging_config.json`. +2. The environmental config file from the path saved under `MW_LOG_CONFIG_FILE` environmental variable + or + The application-specific configuration file under `/opt//etc/logging.json` or `/etc/logging.json` or `/logging.json`. + +In the ECU-wide config file, we specify for instance the ECUID that shall be set +in the DLT header. The environmental configuration then shall override the ECU-wide configuration. +If not defined, the application-specific configuration then shall be applied with the settings +from the application developer. In particular, an application identifier (APPID) shall be provided. +Often also the default log level is overridden from the ECU-wide configuration, and +context-specific log levels may be provided. + +Note that for non-verbose logging there is a third configuration file: + +3. The non-verbose message configuration: `/opt/datarouter/etc/class-id.json` + +`class-id.json` provides the logging level that a non-verbose message shall use +when sent using the `TRACE()` macro. In this design, we currently only consider +the verbose logging configuration and omit the non-verbose aspect. + +## Use Cases + +The use case diagram below shows three typical use cases we support in our design: + +![Use Case Diagram](/swh/ddad_platform/aas/mw/log/design/configuration_use_cases.uxf?ref=a943a898650ec31fc9c7f95961164c09cc84fe95) + +## Class Diagram + +This design is based on the concept of having multiple backends and recorders +from the original `mw::log` [design document](README.md). We omit the details +for the `Recorder` and `Runtime` classes, and focus on the new classes +introduced in this design: + +![Class diagram](/swh/ddad_platform/aas/mw/log/design/configuration_static.uxf?ref=a0f7d7e092a6d561d0c889a2faf752acc969f474) + +The design is centered around the `RecorderFactory` class, which contains three +static methods for each major use case. By default for on-target logging the +`Runtime` will initialize its recorder instance using the `CreateForTarget()` +static method. Internally, this will instantiate the `TargetConfigReader` to +obtain the logging configuration. Depending on the configuration, multiple +recorders might be needed. For instance, a user might select console logging and +remote logging, i.e. DLT logging. Thus we need the `DataRouterRecorder` and the +`TextRecorder` in parallel. + +The responsibility of the `RecorderComposite` is to facilitate multiple active +recorders in parallel. As the name implies, we follow the composite design +pattern here. In the constructor, the class receives a list of Recorder +instances. The class implements the `Recorder` interface methods by calling all +the corresponding method on each instance in its list. + +The `Configuration` is kept simple as an aggregate class and passed by value in +the constructor in each `Recorder` instance that needs it. Since this shall be +done only once during initialization on target, this is not on a hot path. Thus +we prefer a simple design and accept the extra copies as a tradeoff. + +On target, the configuration shall be loaded by the `TargetConfigReader` class. +As this class has several external dependencies we also introduce the +corresponding interfaces and mock class to unit-test the `RecorderFactory` +methods. `TargetConfigReader` uses `ConfigurationFileDiscoverer` to obtain the +file paths for the configuration. After that it uses `bmw::json` library to read +the JSON documents and extract the configuration information from there. + +For unit-testing, we introduce the `IConfigurationFileDiscoverer` interface and +corresponding mock. This enables dependency-injection of +`MockConfigurationFileDiscoverer` into the `TargetConfigReader` for testing. + +On lowest layer, `ConfigurationFileDiscoverer` makes use of `bmw::os` for +general purpose file and directory operations, as well as finding the path of +the current executable. We reuse the mocking facilities of OSAL to unit test the +file discoverer. + +## Initialization Sequence Diagram + +The sequence diagram below depicts the initialization that takes place once when +the user makes the first invokes `mw::log`: + +![Class diagram](/swh/ddad_platform/aas/mw/log/design/configuration_sequence.uxf?ref=a0f7d7e092a6d561d0c889a2faf752acc969f474) + +In the diagram, the user calls `mw::log::Error()` to send the first log message +of the program. The `LogStreamFactory` then calls `GetRecorder()` on the +`Runtime` class to obtain the handler of the log message. Since this is the +first invocation, the instance of the `Runtime` shall be empty and thus the +`Runtime` will call `RecorderFactory::CreateForTarget()` to create the recorder +according to user configuration. Thus we invoke the `ReadConfig()` method of the +local instance of `ITargetConfigReader`. + +The concrete `TargetConfigReader` then will use the +`ConfigurationFileDiscoverer` to check if the global, environmental xor application-specific +config files exist. It will first load the JSON document of the global +configuration, and then override the values with the environmental xor application-specific +configuration. Finally, the obtained instance of `Configuration` is returned. + +Back in the method `RecorderFactory::CreateForTarget()` we instantiate the +configured recorder types. In the example, `kConsole` and `kRemote` are given. +Thus the method creates a vector that contains `DatarouterRecorder` and +`TextRecorder`. From that list, we construct the `CompositeRecorder` that +will forward the messages to both recorders. + +The created instance of `CompositeRecorder` is returned by +`RecorderFactory::CreateForTarget()` and stored in the `Runtime`. Henceforth, +this recorder will be used for all following `mw::log` invocations by the user. + +If a user wants to avoid the latency on the first log message, we provide +`mw::log::Initialize()` that shall take care of initializing the runtime. If +this method is called, the first log message will directly use the initialized +recorders. diff --git a/mw/log/design/configuration_sequence.uxf b/mw/log/design/configuration_sequence.uxf new file mode 100644 index 0000000..5335fc7 --- /dev/null +++ b/mw/log/design/configuration_sequence.uxf @@ -0,0 +1,677 @@ + + + 10 + + UMLGeneric + + 60 + 50 + 20 + 1140 + + + + + + Relation + + 60 + 30 + 30 + 40 + + lt=. + 10.0;10.0;10.0;20.0 + + + UMLGeneric + + 20 + 10 + 100 + 30 + + _:App_ + + + + UMLGeneric + + 260 + 10 + 160 + 30 + + FreeFunctions + + + + Relation + + 70 + 50 + 290 + 40 + + lt=<- +bmw::mw::log::Error() + 270.0;20.0;10.0;20.0 + + + Relation + + 330 + 30 + 30 + 1170 + + lt=. + 10.0;10.0;10.0;1150.0 + + + Relation + + 330 + 100 + 250 + 40 + + lt=<- +GetStream(LogLevel) + 230.0;20.0;10.0;20.0 + + + UMLGeneric + + 490 + 10 + 160 + 30 + + LogStreamFactory + + + + Relation + + 570 + 120 + 230 + 40 + + lt=<- +GetRecorder() + 210.0;20.0;10.0;20.0 + + + UMLGeneric + + 560 + 100 + 20 + 1080 + + + + + + UMLGeneric + + 700 + 10 + 160 + 30 + + Runtime + + + + + Relation + + 770 + 30 + 30 + 1160 + + lt=. + 10.0;10.0;10.0;1140.0 + + + Relation + + 770 + 130 + 240 + 40 + + lt=<- +CreateForTarget() + 220.0;20.0;10.0;20.0 + + + UMLGeneric + + 920 + 10 + 160 + 30 + + RecorderFactory + + + + + UMLGeneric + + 1160 + 10 + 160 + 30 + + TargetConfigReader + + + + + Relation + + 980 + 30 + 30 + 1160 + + lt=. + 10.0;10.0;10.0;1140.0 + + + UMLGeneric + + 1400 + 10 + 180 + 30 + + ConfigurationFileDiscoverer + + + + + Relation + + 1230 + 30 + 30 + 870 + + lt=. + 10.0;10.0;10.0;850.0 + + + Relation + + 980 + 170 + 280 + 40 + + lt=<- +ReadConfig() + 260.0;20.0;10.0;20.0 + + + UMLGeneric + + 1640 + 10 + 180 + 30 + + bmw::json + + + + + Relation + + 1480 + 30 + 30 + 630 + + lt=. + 10.0;10.0;10.0;610.0 + + + Relation + + 1230 + 210 + 280 + 40 + + lt=<- +FindConfigurationFiles() + 260.0;20.0;10.0;20.0 + + + Relation + + 1720 + 30 + 30 + 800 + + lt=. + 10.0;10.0;10.0;780.0 + + + UMLGeneric + + 1880 + 10 + 180 + 30 + + bmw::os + + + + + Relation + + 1950 + 30 + 30 + 540 + + lt=. + 10.0;10.0;10.0;520.0 + + + Relation + + 1480 + 240 + 500 + 40 + + lt=<- +access(/etc/ecu_logging_config.json) + 480.0;20.0;10.0;20.0 + + + Relation + + 1480 + 480 + 500 + 40 + + lt=<- +access(<binary path>/../etc/logging.json) + 480.0;20.0;10.0;20.0 + + + Relation + + 1220 + 550 + 300 + 50 + + lt=<- +Return global and environmental or application +config file paths. + 20.0;20.0;270.0;20.0 + + + Relation + + 1230 + 590 + 520 + 40 + + lt=<- +bmw::json::FromFile(<global file path>) + 500.0;20.0;10.0;20.0 + + + Relation + + 1230 + 750 + 520 + 40 + + lt=<- +bmw::json::FromFile(<app file path>) + 500.0;20.0;10.0;20.0 + + + Relation + + 980 + 840 + 280 + 50 + + lt=<- +Return Configuration +instance + 10.0;20.0;260.0;20.0 + + + UMLGeneric + + 1150 + 910 + 180 + 30 + + DatarouterRecorder + + + + + UMLGeneric + + 1410 + 910 + 180 + 30 + + TextRecorder + + + + + UMLGeneric + + 1630 + 910 + 180 + 30 + + CompositeRecorder + + + + + Relation + + 1230 + 930 + 30 + 110 + + lt=. + 10.0;10.0;10.0;90.0 + + + UMLNote + + 1240 + 800 + 200 + 50 + + Overwrite global configuration +with environmental or application configuration. +bg=yellow + + + + Relation + + 1480 + 930 + 30 + 150 + + lt=. + 10.0;10.0;10.0;130.0 + + + Relation + + 980 + 980 + 280 + 50 + + lt=<- +Create instance of +with configuration + 260.0;20.0;10.0;20.0 + + + Relation + + 980 + 1020 + 530 + 50 + + lt=<- +Create instance of +with configuration + 510.0;20.0;10.0;20.0 + + + Relation + + 1720 + 930 + 30 + 230 + + lt=. + 10.0;10.0;10.0;210.0 + + + Relation + + 770 + 1130 + 240 + 40 + + lt=<- +Stores returned instance + 10.0;20.0;220.0;20.0 + + + Relation + + 570 + 1140 + 230 + 40 + + lt=<- +Returns Recorder instance + 10.0;20.0;210.0;20.0 + + + UMLNote + + 880 + 910 + 240 + 50 + + For example, kRemote and kConsole +requested in configuration. +bg=yellow + + + + UMLNote + + 90 + 90 + 240 + 130 + + If a user wants to avoid the latency +on the first log message, we provide +`mw::log::Initialize()` that shall +take care of initializing the runtime. +If this method is called, the first +log message will directly use the +initialized recorders. +bg=yellow + + + + UMLNote + + 1500 + 190 + 200 + 50 + + Find and return the existing +config files. +bg=yellow + + + + UMLNote + + 1740 + 970 + 260 + 60 + + Composite Recorder will +forward the logs to DatarouterRecorder +and TextRecorder +bg=yellow + + + + UMLNote + + 640 + 180 + 190 + 50 + + If no recorder exists yet, +create one using the factory. +bg=yellow + + + + Relation + + 1480 + 320 + 500 + 40 + + lt=<- +access(MW_LOG_CONFIG_FILE) + 480.0;20.0;10.0;20.0 + + + Relation + + 1480 + 440 + 500 + 40 + + lt=<- +access(<cwd>/logging.json) + 480.0;20.0;10.0;20.0 + + + Relation + + 1480 + 390 + 500 + 40 + + lt=<- +access(<cwd>/etc/logging.json) + 480.0;20.0;10.0;20.0 + + + UMLFrame + + 1390 + 280 + 670 + 250 + + Alternative +-- +MW_LOG_CONFIG_FILE is defined + + + +-- +MW_LOG_CONFIG_FILE is undefined + + + + UMLFrame + + 1180 + 640 + 610 + 150 + + Alternative +-- +MW_LOG_CONFIG_FILE is defined + + + +-- +MW_LOG_CONFIG_FILE is undefined + + + + Relation + + 1230 + 680 + 520 + 40 + + lt=<- +bmw::json::FromFile(<environmental file path>) + 500.0;20.0;10.0;20.0 + + + UMLFrame + + 860 + 880 + 1200 + 250 + + Possibilities +-- + + + + + Relation + + 980 + 1080 + 770 + 40 + + lt=<- +Create with Datarouter and StandardOut recorders + 750.0;20.0;10.0;20.0 + + diff --git a/mw/log/design/configuration_static.uxf b/mw/log/design/configuration_static.uxf new file mode 100644 index 0000000..35b2853 --- /dev/null +++ b/mw/log/design/configuration_static.uxf @@ -0,0 +1,928 @@ + + + Space for diagram notes + 10 + + UMLClass + + 820 + 30 + 420 + 60 + + <<Interface>> +bmw:mw::log::Recorder + +bg=white +fontsize=14 + + + + UMLClass + + 720 + 150 + 290 + 30 + + TextRecorder + +bg=white +fontsize=14 + + + + UMLClass + + 350 + 150 + 320 + 50 + + DataRouterRecorder +-- +- configuration: bmw::mw::log::Configuration + +bg=white +fontsize=14 + + + + UMLClass + + 1050 + 150 + 290 + 30 + + FileRecorder + +bg=white +fontsize=14 + + + + UMLClass + + 1390 + 150 + 290 + 30 + + Empty + +bg=white +fontsize=14 + + + + Relation + + 530 + 80 + 530 + 90 + + lt=<<- + 510.0;10.0;510.0;40.0;10.0;40.0;10.0;70.0 + + + Relation + + 870 + 80 + 190 + 90 + + lt=<<- + 170.0;10.0;170.0;40.0;10.0;40.0;10.0;70.0 + + + Relation + + 1030 + 80 + 180 + 90 + + lt=<<- + 10.0;10.0;10.0;40.0;160.0;40.0;160.0;70.0 + + + Relation + + 1030 + 80 + 540 + 90 + + lt=<<- + 10.0;10.0;10.0;40.0;520.0;40.0;520.0;70.0 + + + UMLClass + + 870 + 250 + 350 + 90 + + RecorderComposite +-- +RecorderComposite(recorders: vector<Recorder>) +-- +recorders_: vector<Recorder> + +bg=white +fontsize=14 + + + + Relation + + 1030 + 80 + 30 + 190 + + lt=<<. + 10.0;10.0;10.0;170.0 + + + UMLClass + + 20 + 320 + 520 + 890 + + bmw::mw::log::Configuration +-- +- ecu_id_: LoggingIdentifier +- app_id_: LoggingIdentifier +- app_description_: std::string +- log_mode_: std::unordered_set<LogMode> +- log_file_path_: std::string +- default_log_level_: LogLevel +- default_console_log_level_: LogLevel +- context_log_level_: ContextLogLevelMap +- stack_buffer_size_: std::size_t +- ring_buffer_size_: std::size_t +- ring_buffer_overwrite_on_full_: bool +- number_of_slots_: std::size_t +- slot_size_bytes_: std::size_t +- data_router_uid_: std::size_t +- dynamic_datarouter_identifiers_: bool +-- ++ Configuration() ++ GetEcuId(): amp::string_view ++ SetEcuId(const amp::string_view): void ++ GetAppId(): amp::string_view ++ SetAppId(const amp::string_view): void ++ GetAppDescription(): amp::string_view ++ SetAppDescription(const amp::string_view): void ++ GetLogMode(): const std::unordered_set<LogMode>& ++ SetLogMode(const std::unordered_set<LogMode>&): void ++ GetLogFilePath(): amp::string_view ++ SetLogFilePath(const amp::string_view): void ++ GetDefaultLogLevel(): LogLevel ++ SetDefaultLogLevel(const LogLevel): void ++ GetDefaultConsoleLogLevel(): LogLevel ++ SetDefaultConsoleLogLevel(const LogLevel): void ++ GetContextLogLevel(): ContextLogLevelMap& ++ SetContextLogLevel(const ContextLogLevelMap&): void ++ GetStackBufferSize(): std::size_t ++ SetStackBufferSize(const std::size_t stack_buffer_size) ++ GetRingBufferSize(): std::size_t ++ SetRingBufferSize(const std::size_t ring_buffer_size): void ++ GetRingBufferOverwriteOnFull(): bool ++ SetRingBufferOverwriteOnFull(const bool): void ++ GetNumberOfSlots(): std::size_t ++ SetNumberOfSlots(const std::size_t number_of_slots): void ++ GetSlotSizeInBytes(): std::size_t ++ SetSlotSizeInBytes(const std::size_t slot_size_bytes): void ++ SetDataRouterUid(const std::size_t uid): void ++ GetDataRouterUid(): std::size_t ++ GetDynamicDatarouterIdentifiers(): bool ++ SetDynamicDatarouterIdentifiers(const bool enable_dynamic_identifier): void ++ IsLogLevelEnabled(const LogLevel& log_level, + const amp::string_view context, + const bool check_for_console = false): bool + + +bg=white +fontsize=14 + + + + UMLClass + + 860 + 800 + 430 + 120 + + TargetConfigReader +-- ++ TargetConfigReader(discoverer: IConfigurationFileDiscoverer) ++ ReadConfig(): bmw::Result<Configuration> +-- +discoverer_: unique_ptr<IConfigurationFileDiscovery> + +bg=white +fontsize=14 + + + + Relation + + 860 + 910 + 70 + 200 + + lt=<- +uses + 50.0;180.0;10.0;180.0;10.0;10.0 + + + Relation + + 530 + 870 + 350 + 40 + + lt=<- +creates + 10.0;20.0;330.0;20.0 + + + UMLClass + + 860 + 380 + 410 + 310 + + RecorderFactory +-- +- GetRemoteRecorder(const Configuration&): + std::unique_ptr<Recorder> +- GetFileRecorder(const Configuration& config + const std::unique_ptr<bmw::os::Fcntl>&): + std::unique_ptr<Recorder> +- GetConsoleRecorder(const Configuration&): + std::unique_ptr<Recorder> +-- ++ CreateFromConfiguration(): std::unique_ptr<Recorder> ++ CreateFromConfiguration( + const std::unique_ptr<const ITargetConfigReader>): + std::unique_ptr<Recorder> ++ CreateWithConsoleLoggingOnly(): std::unique_ptr<Recorder> ++ CreateStub(): std::unique_ptr<Recorder> ++ CreateRecorderFromLogMode( + const LogMode&, + const Configuration&, + const std::unique_ptr<bmw::os::Fcntl>) + +bg=white +fontsize=14 + + + + Relation + + 1030 + 680 + 60 + 140 + + lt=<- +uses + 10.0;120.0;10.0;10.0 + + + UMLNote + + 560 + 240 + 290 + 60 + + Forwards the logs to all recorders in the list +passed in the constructor. +bg=yellow +layer=0 + + + + UMLNote + + 560 + 320 + 280 + 180 + + CreateForTarget(): Loads the configuration +and instantiates the requested +recorder types. + +CreateForUnitTests(): Prepares a recorder +that puts logs in standard output stream +for unit testing. + +CreateStub(): Returns Empty recorder that +discards all the logs. + +bg=yellow +layer=0 + + + + UMLNote + + 560 + 510 + 280 + 90 + + Reads config from +/opt/<app>/etc/logging.json +and /ecu/logging.json and creates the +Configuration object. +bg=yellow +layer=0 + + + + UMLPackage + + 910 + 1040 + 310 + 100 + + bmw::json + +bg=white +fontsize=14 + + + + Relation + + 460 + 190 + 60 + 150 + + lt=<<<<<- +owns + 10.0;10.0;10.0;130.0 + + + Relation + + 1030 + 330 + 70 + 70 + + lt=<- +creates + 10.0;10.0;10.0;50.0 + + + UMLClass + + 1430 + 440 + 220 + 30 + + bmw::mw::log::detail::Runtime + +bg=white +fontsize=14 + + + + + UMLPackage + + 1260 + 1370 + 290 + 170 + + bmw::os +-- + +bg=white +fontsize=14 + + + + + + Relation + + 1260 + 430 + 190 + 40 + + lt=<- +Uses on initialization + 10.0;20.0;170.0;20.0 + + + Relation + + 840 + 270 + 50 + 30 + + lt=. + 10.0;10.0;30.0;10.0 + + + Relation + + 830 + 420 + 50 + 30 + + lt=. + 10.0;10.0;30.0;10.0 + + + Relation + + 460 + 170 + 370 + 80 + + lt=<<<<<- + 350.0;10.0;350.0;60.0;10.0;60.0 + + + Relation + + 800 + 170 + 370 + 80 + + lt=<<<<<- + 350.0;10.0;350.0;60.0;10.0;60.0 + + + UMLClass + + 1140 + 700 + 320 + 70 + + <<Interface>> +ITargetConfigReader +-- +/+ ReadConfig(): bmw::Result<Configuration> = 0/ + +bg=white +fontsize=14 + + + + Relation + + 1160 + 760 + 30 + 60 + + lt=<<. + 10.0;10.0;10.0;40.0 + + + UMLClass + + 1320 + 800 + 360 + 40 + + TargetConfigReaderMock + +bg=white +fontsize=14 + + + + + Relation + + 1370 + 760 + 30 + 60 + + lt=<<. + 10.0;10.0;10.0;40.0 + + + UMLNote + + 1320 + 860 + 310 + 30 + + Used for unit testing the RecorderFactory. +bg=yellow +layer=0 + + + + Relation + + 1430 + 830 + 30 + 50 + + lt=. + 10.0;30.0;10.0;10.0 + + + UMLClass + + 1290 + 960 + 480 + 90 + + <<Interface>> + +IConfigurationFileDiscoverer +-- +/+FindConfigurationFiles(): vector<string>/ + +bg=white +fontsize=14 + + + + UMLClass + + 1240 + 1140 + 330 + 190 + + ConfigurationFileDiscoverer + +-- +- path_: std::unique_ptr<os::Path> +- stdlib_: std::unique_ptr<os::Stdlib> +- unistd_: std::unique_ptr<os::Unistd> +-- ++ ConfigurationFileDiscoverer( + std::unique_ptr<os::Path>&&, + std::unique_ptr<os::Stdlib>&&, + std::unique_ptr<os::Unistd>&&) ++ FindConfigurationFiles(): std::vector<std::string> + + +bg=white +fontsize=14 + + + + Relation + + 1320 + 1040 + 30 + 120 + + lt=<<- + 10.0;10.0;10.0;100.0 + + + Relation + + 1320 + 1320 + 60 + 70 + + lt=<- +uses + 10.0;50.0;10.0;10.0 + + + UMLClass + + 1610 + 1080 + 240 + 30 + + ConfigurationFileDiscovererMock + +bg=white +fontsize=14 + + + + Relation + + 1650 + 1040 + 30 + 60 + + lt=<<- + 10.0;10.0;10.0;40.0 + + + Relation + + 1120 + 910 + 190 + 90 + + lt=<<<<<- + + 10.0;10.0;10.0;70.0;170.0;70.0 + + + Relation + + 1230 + 50 + 640 + 410 + + lt=<<<<<- +owns instance of + 290.0;390.0;290.0;360.0;520.0;360.0;520.0;20.0;10.0;10.0 + + + UMLNote + + 1620 + 1140 + 380 + 240 + + Finds the file paths to the global, environmental and +application configuration files and returns all that exists. + +Global configuration: +1. /etc/ecu_logging_config.json if it exists. + +Environmental configuration: +1. path under the MW_LOG_CONFIG_FILE environmental +variable if it defined + +Application configuration: +1. <cwd>/etc/logging.json +2. <cwd>/logging.json +3. <binary path>/../etc/logging.json + + +bg=yellow +layer=0 + + + + Relation + + 1560 + 1150 + 80 + 30 + + lt=. + 60.0;10.0;10.0;10.0 + + + UMLClass + + 1270 + 1400 + 270 + 60 + + utils/path +-- +_+get_exec_path(): Result<string>_ +_+get_parent_dir(): string_ + +bg=white +fontsize=14 +layer=1 + + + + UMLNote + + 910 + 960 + 250 + 40 + + +Dependency injection for unit testing. +bg=yellow +layer=0 + + + + UMLClass + + 1270 + 1480 + 100 + 50 + + Unistd +-- ++ access() + +bg=white +fontsize=14 +layer=1 + + + + UMLClass + + 920 + 1090 + 290 + 40 + + -- ++ FromFile(path): Result<bmw::json::Any> + +bg=white +fontsize=14 +layer=1 + + + + UMLNote + + 360 + 20 + 170 + 50 + + Configuration passed +the constructor by value. +bg=yellow +layer=0 + + + + Relation + + 440 + 60 + 30 + 110 + + lt=. + 10.0;90.0;10.0;10.0 + + + UMLClass + + 20 + 150 + 320 + 50 + + CompositeRecorder +-- +- config_: bmw::mw::log::detail::Configuration + +bg=white +fontsize=14 + + + + Relation + + 160 + 80 + 900 + 90 + + lt=<<- + 880.0;10.0;880.0;40.0;10.0;40.0;10.0;70.0 + + + Relation + + 160 + 190 + 330 + 60 + + lt=<<<<<- + 10.0;10.0;10.0;40.0;310.0;40.0 + + + UMLClass + + 1390 + 1480 + 100 + 50 + + Stdlib +-- ++ getenv() + +bg=white +fontsize=14 +layer=1 + + + + UMLClass + + 610 + 1170 + 490 + 150 + + LoggingIdentifier + +-- ++ LoggingIdentifier(const amp::string_view) ++ GetStringView(): amp::string_view ++ friend operator==(const LoggingIdentifier&, const LoggingIdentifier&): bool ++ friend operator!=(const LoggingIdentifier&, const LoggingIdentifier&): bool ++ kMaxLength: std::size_t ++ data_: std::array<amp::string_view::value_type, kMaxLength> + + +bg=white +fontsize=14 + + + + Relation + + 530 + 1170 + 100 + 40 + + lt=<<<<<- + + 10.0;20.0;80.0;20.0 + + + UMLClass + + 1320 + 250 + 410 + 150 + + << interface >> +IRecorderFactory +-- ++ CreateFromConfiguration: std::unique_ptr<Recorder> ++ CreateWithConsoleLoggingOnly(): std::unique_ptr<Recorder> ++ CreateStub(): std::unique_ptr<Recorder> + +bg=white +fontsize=14 + + + + Relation + + 1260 + 370 + 80 + 40 + + lt=<- +uses + 60.0;20.0;10.0;20.0 + + diff --git a/mw/log/design/configuration_use_cases.uxf b/mw/log/design/configuration_use_cases.uxf new file mode 100644 index 0000000..e175bdb --- /dev/null +++ b/mw/log/design/configuration_use_cases.uxf @@ -0,0 +1,194 @@ + + + 10 + + UMLActor + + 130 + 90 + 260 + 120 + + Developer, +Debug Application deployed on target + + + + UMLUseCase + + 470 + 140 + 250 + 90 + + Application configuration +-- +APPID = Para +LogMode = kRemote | kConsole +LogLevel = kDebug +valign=top + + + + UMLUseCase + + 490 + 30 + 210 + 90 + + ECU wide configuration +-- +ECUID = MPP1 +LogLevel = kError +valign=top + + + + Relation + + 350 + 60 + 160 + 130 + + lt=->> + 10.0;110.0;140.0;10.0 + + + Relation + + 350 + 160 + 150 + 30 + + lt=->> + 10.0;10.0;130.0;10.0 + + + UMLActor + + 210 + 300 + 100 + 120 + + Developer, +Unit testing + + + + UMLUseCase + + 380 + 300 + 340 + 50 + + Print the logs on the console for +analysis of unit test failures. + + + + UMLUseCase + + 80 + 50 + 120 + 40 + + Use case 1 + + + + UMLUseCase + + 80 + 290 + 120 + 40 + + Use case 2 + + + + UMLUseCase + + 80 + 470 + 120 + 40 + + Use case 3 + + + + UMLUseCase + + 380 + 370 + 340 + 40 + + No logging.json files, it should just work (tm). + + + + Relation + + 280 + 320 + 120 + 90 + + lt=->> + 10.0;10.0;100.0;70.0 + + + Relation + + 280 + 310 + 120 + 40 + + lt=->> + 10.0;20.0;100.0;10.0 + + + UMLActor + + 200 + 490 + 100 + 120 + + Performance +Engineer + + + + UMLUseCase + + 380 + 500 + 340 + 50 + + Disable logging completely +Measure performance impact of logging + + + + Relation + + 270 + 510 + 130 + 30 + + lt=->> + 10.0;10.0;110.0;10.0 + + diff --git a/mw/log/design/datarouter_backend/README.md b/mw/log/design/datarouter_backend/README.md new file mode 100644 index 0000000..d3b14ef --- /dev/null +++ b/mw/log/design/datarouter_backend/README.md @@ -0,0 +1,321 @@ + + + + +# Safe Datarouter Backend + +- [Safe Datarouter Backend](#safe-datarouter-backend) + - [Introduction](#introduction) + - [Inter-process communication](#inter-process-communication) + - [Class diagram](#class-diagram) + - [Activity diagrams](#activity-diagrams) + - [Lock-free Shared Memory Design](#lock-free-shared-memory-design) + - [Lazy logging client](#lazy-logging-client) + - [Establishing a logging session](#establishing-a-logging-session) + - [Avoiding shared memory leaks](#avoiding-shared-memory-leaks) + - [Post-mortem logging](#post-mortem-logging) + - [Limited impact of incoming messages in the logging client](#limited-impact-of-incoming-messages-in-the-logging-client) + - [Message exchange between Logging Clients and Datarouter](#message-exchange-between-logging-clients-and-datarouter) + - [Establish a new logging session](#establish-a-new-logging-session) + - [Request from the client to Datarouter](#request-from-the-client-to-datarouter) + - [Acquire data to read on the ring buffer](#acquire-data-to-read-on-the-ring-buffer) + - [Request from Datarouter to acquire](#request-from-datarouter-to-acquire) + - [Response from the Client to confirm acquire](#response-from-the-client-to-confirm-acquire) + +## Introduction + +The `mw::log` implementation development goal is to reach ASIL qualification and +to ensure freedom of interference of logging with customer functions. The +logging mode `kRemote` in `mw::log` transports the logs from the client process +to Ethernet via the Datarouter process. The design goals for the communication +are as follows: + +1. Lock-free and wait-free: Logging calls from the client context shall not + block. The client context shall not wait on other log calls from client + context, neither on the internal logging thread. + +2. Deterministic memory management: Dynamic memory allocations are only allowed + during the initialization phase. The implementation shall preallocate all + necessary memory upon the first invocation of the logging API. + +3. Protection of safety-qualified logging client processes from non-qualified + Datarouter process. The logging library shall validate and treat inputs from + Datarouter as untrusted. + +## Inter-process communication + +In the Datarouter backend the goal is to safely and efficiently transfer the log +messages from the user process to the Datarouter process. + +The inter-process communication takes place via shared memory and the [message +passing library](../../../com/message_passing/design/README.md). This library +only supports unidirectional message transport. As we need bidirectional +communication over the side channel, we establish two independent unidirectional +channels. On the highest level of abstraction we see the ASIL-B qualified client +process on the one side and the datarouter process on the other + +![Inter-process communication](/swh/ddad_platform/aas/mw/log/design/datarouter_backend/inter_process_communication.uxf?ref=1f63d6572c73339987778857d3e2f35b831d877b). + +The logs are written by the client into shared memory and read-out by +datarouter. Freedom of interference is ensured since the datarouter process has +read-only access to the shared-memory. In order to reuse the logging buffers in +shared-memory the client may overwrite them after datarouter has acknowledged it +has successfully read them. This acknowledgement is sent via the +safety-qualified message passing library. + +## Class diagram + +![Class diagram](/swh/ddad_platform/aas/mw/log/design/datarouter_backend/class_diagram.uxf?ref=a0f7d7e092a6d561d0c889a2faf752acc969f474) + +The class diagram above shows the relevant classes client-side and in +Datarouter. Client-side the `DatarouterBackend` contains a circular allocator +for the preallocation of the verbose messages. When a new verbose message is +created a slot is reserved first for the user in the circular allocator. The +memory inside the slot is preallocated and lives in the process space of the +client. When the user flushes the verbose message, the content of the slot is +copied into the ring buffer that lives in shared memory through the `TRACE()` +macro. The shared memory buffer is encapsulated by the `SharedMemoryWriter` +class. It is owned by the `platform::Logger` as a Singleton. For non-verbose +logging, or structured logging `mw::log` is not involved, but `TRACE()` is used +directly. The `TRACE()` macro internally calls +`SharedMemoryWriter::AllocateAndWrite()`, which locks a client-side only mutex +and writes the input structure in serialized form into the ring-buffer. When +Datarouter is reading from the ring buffer the data shall not be overwritten. +The responsibility of the `DatarouterMessageClient` class is to synchronize the +access to the ring buffer between Datarouter and the client. + +## Activity diagrams + +![DataRouterBackend::DataRouterBackend Activity diagram](/swh/ddad_platform/aas/mw/log/design/datarouter_backend_datarouterbackend.uxf?ref=5e05cd648f508d14acff7fc405f7cba93c4c5dff) + +## Lock-free Shared Memory Design + +The log messages from apps are transferred to Datarouter through shared memory. +For lock- and wait-free data exchange we use the `WaitFreeAlternatingWriter` as +documented [here](../../detail/wait_free_producer_queue/README.md) as part of +the `SharedMemoryWriter` class. The data in shared memory is layed out in +consecutive and contiguous segments: + +1. Control structure `mw::log::detail::SharedData` at offset = 0. +2. Linear buffer one. +3. Linear buffer two. + +Datarouter shall be able to open this file read-only. The control structure +contains fundamental information about the buffer sizes, and the atomic +variables. + +The `SharedMemoryWriter` has two main responsibilities. First is to register +data types using `TryRegisterType()` and secondly to push instances of those +types using `AllocAndWrite()`. Both aspects use the `WaitFreeAlternatingWriter` +to ensure lock-free writing of data. + +Before a data type can be used it needs to be successfully registered. In the +registration, the `SharedMemoryWriter` send the fully qualified type name and a +corresponding numeric type identifier. + +This identifier is then used by `AllocAndWrite()`. For each call, the method +tries to acquire one contiguous area of memory on the current linear buffer. +This area then contains two sub-segments: + +1. The header `mw::log::BufferEntryHeader` which contains a timestamp and type + identification. +2. Followed by the payload as written by the user. + +## Lazy logging client + +The logging client shall remain in the passive role in the message exchange with +Datarouter, i.e., it should react and respond to messages from Datarouter. The +reason for that is that we restrict the client to only a single thread for +logging to save resources. In addition, there shall be no messages sent from a +user context in the client. By user context we mean an public API call to +logging of any user thread that is not the logging thread. Sending a message +with the `mw::com` `Sender` results in a context switch to the receiver (i.e. +Datarouter). This operation could block for an undefined period of time. This +would violate our requirements and design goals of not blocking the user of +logging. Hence we only allow message sending in the logging thread in the client +process. The only exception is during the initialization phase. Here, the client +may send a connect message to Datarouter. + +## Establishing a logging session + +Before the information exchange between Datarouter and the Logging client can +begin, a session needs to be established. By that we mean the two side-channels +for message passing need to be connected. The client needs to instantiate a +`Sender` that connects with the `Receiver` of Datarouter. Datarouter needs to +instantiate a `Sender` that connects to the `Receiver` of the client. In +addition, Datarouter has to open the shared memory file from the logging +client. + +We need to support logging with all possible startup orders of the processes: + +1. A logging client starts after Datarouter started. +2. A logging client started before Datarouter starts. + +When a client starts, it creates a ring buffer in a shared-memory page and maps +it to a file. From that point on, the user is able to write logs in the +shared-memory ring buffer even in early startup phase. In the initialization +phase of the logger library, a connect message is attempted to be sent to +Datarouter. This is only possible if Datarouter is already running and ready to +receive. If a client exits before Datarouter has connected the resource must be +cleaned up and the logs are lost. + +For each client, there shall be two files: + +1. The shared memory file of the client `/tmp/logging...shmem`. +2. The receiver file of the client `/mw_com/logging..`. + +The shared memory file shall be writable only for the UID of the client, and +read-only for everything else. + +On startup, Datarouter creates its own receiver and attaches it to a file: + +- `/mw_com/logging.datarouter_recv` + +After Datarouter creates its receiver the logging clients are unblocked and +establish the connection by sending the connect message. + +## Avoiding shared memory leaks + +The logging library should ensure the shared memory resources are freed when no +longer needed to avoid leaks. Logging clients may exit and restart multiple +times. The shared memory file that contains the ring buffer read by datarouter +can only be cleaned by the client and not by Datarouter due the restrictive +permissions. Hence, the logging client shall unlink the shared memory file when +it receives the first message from Datarouter. At this point it is ensured that +Datarouter has already opened the file and is able to consume it. This way the +resource is cleaned up as soon as all processes have closed the file. + +In addition, files created by the logging client shall be unlinked on exit. Due +to this design decision leaks should be avoided, but if an app exits before +Datarouter can establish the connection the logs are lost. If, however, the +client crashes before being able to unlink the file, the resources cannot be +freed. After a crash the app may be restarted. Thus, on startup, the client +shall check for shared memory instances of previous runs and unlink them as well +to fix the leak. + +## Post-mortem logging + +Datarouter should completely retrieve the all the logs of an application even +after that app exited. The only condition, as explained in the previous section, +is that Datarouter already established a connection to the app. Datarouter +should detect that an app is terminated if sending a message to that app fails. +At this point, Datarouter should switch the `SharedMemoryReader` to detached +mode. In detached mode, the reader assumes all data remains unchanged and thus +readable without synchronization. Only after reading the remaining data from the +ring buffer, should Datarouter unmap the shared memory page. Then the OS should +free up the used resources. + +![SharedMemoryReader Class diagram](/swh/ddad_platform/aas/mw/log/design/mw_log_shared_memory_reader.uxf?ref=a0f7d7e092a6d561d0c889a2faf752acc969f474) + +## Limited impact of incoming messages in the logging client + +Consider a fault in Datarouter that leads to permanently repeated message +sending to one of the logging clients in a hot loop. First of all, this would +not directly impact the user threads of the clients but the logger thread in the +client process. The logger thread is spawned by the thread pool (containing a +single thread) in the `DatarouterMessagePassingClient` in `DatarouterBackend`. + +The implementation shall limit the impact of such a "denial-of-service"-like +situation on the client threads. First, the lock-free implementation of the +shared memory buffers ensures that the logger cannot block the running operation +of a client writing to the shared memory. It is always the reader i.e. the +logger thread and Datarouter that on the writers to finish, and not vice-versa. + +Secondly, we also mitigate CPU starvation of the clients by limiting the maximum +number of requests a logger thread may execute in a fixed time interval. This +rate limitation is implemented with a fixed sleep period in the logger thread +after each incoming request. + +## Message exchange between Logging Clients and Datarouter + +The side channel in logging is needed to synchronize the access to the data in +shared memory between the client and Datarouter. Previously, the channel was +implemented using unix domain sockets. Here, we replace unix domain sockets by +[message passing](../../../com/message_passing/design/README.md). + +Datarouter periodically scans for new clients and initiates the message +exchange. In the message exchange between Datarouter and the Clients, +Datarouter always takes the active role. Datarouter sends a request and the +Client replies with a response to the request. + +There is only a single receiver in Datarouter for all clients. Each message +from `mw::com::MessagePassing` includes a message id and a PID of the sender +besides the Payload. We use the `ShortMessage` and `MediumMessage` type for all +messages, which can currently hold a payload length of 8 Bytes and 16 Bytes, +respectively. The PID is used as an identifier for the concrete logging client +in Datarouter. + +### Establish a new logging session + +When a logging client starts, it sends a connect message to Datarouter. This is +only possible if Datarouter is already running. If Datarouter is not yet running +sending the message will block until Datarouter becomes ready to receive +messages. If the application shuts down before receiving the connect message, +the logs of that application will be lost. + +#### Request from the client to Datarouter + +Message ID: `kConnectToClient` + +Payload: `struct ConnectMessage` + +This is the only message that shall be initiated by the client itself. The +message shall contain the necessary information for Datarouter to find the +shared memory file location of the client and establish the corresponding +message passing channel. + +There are two modes that a client could use: + +1. Deterministic identifier mode that is is used in production. +2. Dynamic identifier mode that is used only for testing purposes. + +In deterministic mode the client is identified using its DLT app id and user id. +By system design, each process shall have an unique uid so that there should be +no collisions. + +Dynamic mode is provided for testing purposes where we cannot ensure a unique +app id and user id. Instead it uses a random unique 6 character long identifier +that is obtained using `mkstemp()`. Enabling this mode in production is +prevented by secpol enforcement as the message passing channel path is fixed by +secpol policy. + +### Acquire data to read on the ring buffer + +In a periodic interval, Datarouter request to acquire the current data in the +ring buffer from a client. This allows to synchronize reading the ring buffer in +shared memory and prevents that the client modifies that data during reading. +Each acquire request corresponds to toggling the linear buffer for reading and +writing. + +#### Request from Datarouter to acquire + +Message ID: `kAcquireRequest` + +Payload: None + +#### Response from the Client to confirm acquire + +When the data is acquired on client side by a call to +`AlternatingReader::Switch()`, the client confirms that by responding. The +response contains the range in the ring buffer that is ready to be read as well +as the updated type index. When a user traces a data type for the C++ fully +qualified type name is put on the type stack in the `MwsrWriter`. This allows +Datarouter to identify the payload in the ring buffer by index to the type +stack. + +Message ID: `kAcquireResponse` + +Message type: `MediumMessage` + +Payload: `struct ReadAcquireResult` + diff --git a/mw/log/design/datarouter_backend/class_diagram.uxf b/mw/log/design/datarouter_backend/class_diagram.uxf new file mode 100644 index 0000000..63d23c2 --- /dev/null +++ b/mw/log/design/datarouter_backend/class_diagram.uxf @@ -0,0 +1,1258 @@ + + + Space for diagram notes + + 8 + + UMLClass + + 16 + 96 + 272 + 168 + + DataRouterBackend +- +- buffer_: CircularAllocator +- message_client_: DatarouterMessageClient +-- ++ DataRouterBackend(const std::size_t, + const LogRecord&, + DatarouterMessageClientFactory&, + const Configuration&, + WriterFactory) ++ ReserveSlot: amp::optional<SlotHandle> ++ FlushSlot((const SlotHandle&): void ++ GetLogRecord((const SlotHandle&): LogRecord& + + +bg=white +fontsize=14 + + + + UMLClass + + 1152 + 1096 + 248 + 24 + + mw::com::message_passing::IReceiver + +bg=white +fontsize=14 + + + + Relation + + 1080 + 1096 + 88 + 24 + + lt=<<<<<- + 10.0;10.0;90.0;10.0 + + + UMLClass + + 296 + 360 + 456 + 232 + + SharedMemoryWriter +-- +- shared_data: SharedData& +- alternating_writer_: WaitFreeAlternatingWriter +- alternating_reader_: AlternatingReader +- unmap_callback_ : UmapCallback +- type_identifier_: atomic<TypeIdentifier> +- moved_from_: bool +-- ++ SharedMemoryWriter(SharedData&, UnmapCallback) ++ SharedMemoryWriter(SharedMemoryWriter&&) ++ GetMaxPayloadSize: Length ++ AllocAndWrite(const TimePoint, const TypeIdentifier, const Length, WriteCallback): void ++ AllocAndWrite(const TypeIdentifier, const Length, WriteCallback): void ++ AllocAndWrite(WriteCallback, const TypeIdentifier, const Length): void ++ TryRegisterType(Typeinfo): optional<TypeIdentifier> ++ ReadAcquire(): ReadAcquireResult ++ DetachWriter(): void + + +bg=white +fontsize=14 + + + + UMLClass + + 312 + 232 + 176 + 72 + + <<Singleton>> +bmw::platform::logger +-- +- writer: SharedMemoryWriter + +bg=white +fontsize=14 + + + + Relation + + 400 + 296 + 24 + 80 + + lt=<<<<<- + 10.0;10.0;10.0;80.0 + + + UMLClass + + 904 + 272 + 296 + 136 + + <<Shared Memory>> + +SharedData +-- ++ control_block: AlternatingControlBlock ++ linear_buffer_1_offset: Length ++ number_of_drops_buffer_full: std::atomic<Length> ++ number_of_drops_invalid_size: std::atomic<Length> ++ writer_detached std::atomic<bool> ++ producer_pid: pid_t + + +bg=white +fontsize=14 + + + + Relation + + 744 + 368 + 176 + 24 + + lt=<<<<- + 10.0;10.0;200.0;10.0 + + + UMLClass + + 1152 + 1144 + 248 + 24 + + mw::com::message_passing::ISender + +bg=white +fontsize=14 + + + + Relation + + 1080 + 1144 + 88 + 24 + + lt=<<<<<- + 10.0;10.0;90.0;10.0 + + + UMLClass + + 16 + 0 + 128 + 40 + + lt=. +*mw::log* +*Datarouter backend* + +bg=white +fontsize=14 + + + + UMLClass + + 504 + 0 + 376 + 304 + + WriterFactory +-- +- GetStaticLoggingClientFilename(const amp::string_view): std::string +- UnlinkExistingFile(const std::string&): void +- OpenAndTruncateFile(const std::size_t, const std::string&, + const bmw::os::Fcntl::Open): amp::optional<int32_t> +- MapSharedMemory(const std::size_t, const int32_t, const std::string&): + amp::optional<void* const> +- IsMemoryAligned(void* const): bool +- ConstructSharedData(void* const, const std::size_t): SharedData* + +- osal_: OsalInstances +- mmap_result_: amp::expected<void*, bmw::os::Error> +- unmap_callback_: UnmapCallback +- identifier_: std::string +- file_name_: std::string +-- ++ WriterFactory(OsalInstances osal) ++ Create(const std::size_t, const bool, const amp::string_view): + amp::optional<SharedMemoryWriter> ++ GetIdentifier(): std::string ++ GetFileName(): std::string ++ GetIdentifierFromFilename(const std::string&): static std::string + + +bg=white +fontsize=14 + + + + Relation + + 592 + 296 + 80 + 80 + + lt=<. +<<creates>> + 10.0;80.0;10.0;10.0 + + + Relation + + 144 + 56 + 376 + 56 + + lt=<. +<<uses>> + 450.0;20.0;10.0;20.0;10.0;50.0 + + + UMLClass + + 1304 + 32 + 128 + 32 + + bmw::os::Fcntl +-- +fontsize=14 +bg=white + + + + Relation + + 1192 + 32 + 128 + 24 + + lt=<<<<<- + 10.0;10.0;140.0;10.0 + + + UMLClass + + 1304 + 112 + 128 + 32 + + bmw::os::Mman +-- +fontsize=14 +bg=white + + + + Relation + + 1192 + 72 + 128 + 64 + + lt=<<<<<- + 10.0;10.0;140.0;60.0 + + + UMLClass + + 856 + 512 + 368 + 64 + + WaitFreeAlternatingWriter +-- ++ Acquire(const Length length): amp::optional<AlternatingAcquiredData> ++ Release(const AlternatingAcquiredData&): void + +bg=white +fontsize=14 + + + + UMLClass + + 856 + 432 + 216 + 56 + + AlternatingReader +-- ++ Read(): amp::optional<amp::span<Byte>> ++ Switch(): void + + + +bg=white +fontsize=14 + + + + Relation + + 744 + 536 + 128 + 24 + + lt=<<<<<- + 10.0;10.0;140.0;10.0 + + + Relation + + 744 + 456 + 128 + 24 + + lt=<<<<<- + 10.0;10.0;140.0;10.0 + + + UMLClass + + 1304 + 72 + 128 + 32 + + bmw::os::Unistd +-- +fontsize=14 +bg=white + + + + Relation + + 1192 + 48 + 128 + 56 + + lt=<<<<<- + 10.0;10.0;140.0;50.0 + + + UMLClass + + 1216 + 592 + 392 + 208 + + <<interface>> + +MessagePassingFactory +-- ++ CreateReceiver(const amp::string_view, + concurrency::Executor&, + const amp::span<const uid_t>, + const bmw::mw::com::message_passing::ReceiverConfig&, + amp::pmr::memory_resource*): + amp::pmr::unique_ptr<bmw::mw::com::message_passing::IReceiver> ++ CreateSender(const amp::string_view, + const amp::stop_token&, + const bmw::mw::com::message_passing::SenderConfig&, + bmw::mw::com::message_passing::LoggingCallback, + amp::pmr::memory_resource*): + amp::pmr::unique_ptr<bmw::mw::com::message_passing::ISender> + + + +bg=white +fontsize=14 + + + + + Relation + + 744 + 608 + 64 + 32 + + lt=.>> + + 60.0;20.0;10.0;20.0 + + + UMLClass + + 792 + 592 + 320 + 184 + + DatarouterMessageClientFactoryImpl +-- +- created_once_: bool +- config_: Configuration +- message_passing_factory_: MessagePassingFactory +- unistd_: bmw::os::Unistd +- pthread_: bmw::os::Pthread +-- ++ DatarouterMessageClientFactoryImp(const Configuration&, + std::unique_ptr<MessagePassingFactory>, + std::unique_ptr<bmw::os::Unistd>, + std::unique_ptr<bmw::os::Pthread>) ++ CreateOnce(const std::string&, + const std::string&): + std::unique_ptr<DatarouterMessageClient> + + +bg=white +fontsize=14 + + + + + UMLClass + + 56 + 416 + 176 + 80 + + <<interface>> + +DatarouterMessageClient +- ++ Run(): void ++ Shutdown(): void + + +bg=white +fontsize=14 + + + + Relation + + 32 + 256 + 440 + 400 + + lt=<. + 10.0;10.0;10.0;480.0;530.0;480.0 + + + UMLClass + + 464 + 816 + 624 + 680 + + DatarouterMessageClientImpl +-- +- RunConnectTask(): bool +- OnAcquireRequest(): void +- UnlinkSharedMemoryFile(): void +- HandleFirstMessageReceived(): void +- RequestInternalShutdown(): void +- CheckExitRequestAndSendConnectMessage(): void +- BuildMessage(const DatarouterMessageIdentifier&, +const bmw::mw::com::message_passing::MediumMessagePayload&): bmw::mw::com::message_passing::MediumMessage +- SendMessage(const Message&): void + +- run_started_: bool +- msg_client_ids_: MsgClientIdentifiers +- use_dynamic_datarouter_ids_: bool +- first_message_received_: std::atomic_bool +- utils_: MsgClientUtils +- unlinked_shared_memory_file_: bool +- shared_memory_writer_: SharedMemoryWriter& +- writer_file_name_: std::string +- message_passing_factory_: std::unique_ptr<MessagePassingFactory> +- monotonic_resource_buffer_: std::array<uint8_t, GetMonotonicResourceSize()> +- monotonic_resource_: amp::pmr::monotonic_buffer_resource +- stop_source_: amp::stop_source +- thread_pool_: bmw::concurrency::ThreadPool +- sender_: amp::pmr::unique_ptr<bmw::mw::com::message_passing::ISender> +- receiver_: amp::pmr::unique_ptr<bmw::mw::com::message_passing::IReceiver> +- connect_task_: bmw::concurrency::TaskResult<void> +-- ++ DatarouterMessageClientImpl(const MsgClientIdentifiers&, + MsgClientBackend, + MsgClientUtils, + const amp::stop_source) ++ Run(): void ++ Shudown(): void ++ SetupReceiver(): void ++ CreateSender(): void ++ SendConnectMessage(): void ++ SetThreadName(): void ++ ConnectToDatarouter(): void ++ BlockTermSignal(): void ++ GetReceiverIdentifier(): const std::string& ++ GetThisProcessPid(): const pid_t& ++ GetWriterFileName(): const std::string& ++ GetAppid(): const LoggingIdentifier& + +bg=white +fontsize=14 + + + + + Relation + + 80 + 488 + 432 + 344 + + lt=.>> + 520.0;410.0;520.0;350.0;10.0;350.0;10.0;10.0 + + + UMLClass + + 1152 + 1192 + 248 + 24 + + mw::com::message_passing::ShortMessage + +bg=white +fontsize=14 + + + + UMLClass + + 1152 + 1240 + 248 + 24 + + mw::com::message_passing::MediumMessage + +bg=white +fontsize=14 + + + + Relation + + 1080 + 1192 + 88 + 24 + + lt=<<<<<- + 10.0;10.0;90.0;10.0 + + + Relation + + 1080 + 1240 + 88 + 24 + + lt=<<<<<- + 10.0;10.0;90.0;10.0 + + + UMLClass + + 1152 + 840 + 248 + 240 + + MsgClientIdentifiers +-- +- receiver_id_: std::string +- this_process_id_: pid_t +- app_id_ating_reader_: LoggingIdentifier +- datarouter_uid_ : uid_t +- uid_: uid_t +-- ++ MsgClientIdentifiers(const std::string&, + const pid_t, + const LoggingIdentifier&, + const uid_t, + const uid_t) ++ GetReceiverID(): std::string& ++ GetThisProcID(): const pid_t& ++ GetAppID(): const LoggingIdentifier& ++ GetDatarouterUID(): uid_t ++ GetUID(): uid_t + + +bg=white +fontsize=14 + + + + Relation + + 928 + 768 + 80 + 64 + + lt=<. +<<creates>> + 10.0;60.0;10.0;10.0 + + + UMLClass + + 40 + 864 + 376 + 192 + + MsgClientBackend +-- +- shared_memory_writer_: SharedMemoryWriter& +- writer_file_name_: std::string +- message_passing_factory_: std::unique_ptr<MessagePassingFactory> +- use_dynamic_datarouter_ids_ : bool +-- ++ MsgClientBackend(SharedMemoryWriter&, + const std::string&, + std::unique_ptr<MessagePassingFactory>, + const bool) ++ GetShMemWriter(): SharedMemoryWriter& ++ GetWriterFilename(): const std::string& ++ GetMsgPassingFactory(): std::unique_ptr<MessagePassingFactory>& ++ IsUsingDynamicDatarouterIDs(): bool + + +bg=white +fontsize=14 + + + + Relation + + 408 + 936 + 72 + 24 + + lt=<. + 10.0;10.0;70.0;10.0 + + + UMLClass + + 40 + 1080 + 376 + 144 + + MsgClientUtils +-- +- unistd_: std::unique_ptr<bmw::os::Unistd> +- pthread_: std::unique_ptr<bmw::os::Pthread> +- signal_: std::unique_ptr<bmw::os::Signal> +-- ++ MsgClientUtils(std::unique_ptr<bmw::os::Unistd>, + std::unique_ptr<bmw::os::Pthread>, std::unique_ptr<bmw::os::Signal>) ++ GetUnistd(): bmw::os::Unistd& ++ GetPthread(): bmw::os::Pthread& ++ GetSignal(): bmw::os::Signal& + + +bg=white +fontsize=14 + + + + + UMLClass + + 456 + 616 + 296 + 88 + + <<interface>> + +DatarouterMessageClientFactory +-- ++ CreateOnce((const std::string&, (const std::string&): + std::unique_ptr<DatarouterMessageClient> + + + +bg=white +fontsize=14 + + + + + UMLClass + + 1416 + 840 + 360 + 192 + + MessagePassingFactoryImpl +-- ++ CreateReceiver(const amp::string_view, + concurrency::Executor&, + const amp::span<const uid_t>, + const bmw::mw::com::message_passing::ReceiverConfig&, + amp::pmr::memory_resource*): + amp::pmr::unique_ptr<bmw::mw::com::message_passing::IReceiver> ++ CreateSender(const amp::string_view, + const amp::stop_token&, + const bmw::mw::com::message_passing::SenderConfig&, + bmw::mw::com::message_passing::LoggingCallback, + amp::pmr::memory_resource*): + amp::pmr::unique_ptr<bmw::mw::com::message_passing::ISender> + + +bg=white +fontsize=14 + + + + + Relation + + 1480 + 792 + 24 + 64 + + lt=.>> + + 10.0;60.0;10.0;10.0 + + + Relation + + 1104 + 608 + 128 + 24 + + lt=<- + 140.0;10.0;10.0;10.0 + + + Relation + + 1080 + 848 + 88 + 24 + + lt=<<<<<- + 10.0;10.0;90.0;10.0 + + + Relation + + 248 + 256 + 352 + 576 + + lt=->>>>> + 420.0;700.0;420.0;610.0;10.0;610.0;10.0;10.0 + + + Relation + + 136 + 256 + 24 + 176 + + lt=<- + 10.0;10.0;10.0;200.0 + + + UMLClass + + 1240 + 272 + 304 + 72 + + AlternatingControlBlock +-- ++ control_block_1: LinearControlBlock ++ control_block_1: LinearControlBlock ++ is_control_block_1_active_for_writing: std::atomic<bool> + + +bg=white +fontsize=14 + + + + UMLClass + + 1240 + 384 + 304 + 80 + + LinearControlBlock +-- ++ data: amp::span<Byte> ++ acquired_index: std::atomic<Length> ++ written_index: std::atomic<Length> ++ number_of_writers: std::atomic<Length> + + +bg=white +fontsize=14 + + + + Relation + + 1192 + 296 + 64 + 24 + + lt=<<<<<- + 10.0;10.0;60.0;10.0 + + + Relation + + 1368 + 336 + 24 + 64 + + lt=<<<<<- + 10.0;10.0;10.0;60.0 + + + UMLNote + + 1456 + 440 + 152 + 56 + + Note.. + +using Length = std::uint64_t; + + +bg=blue +layer=1 +fontsize=14 + + + + UMLClass + + 304 + 1248 + 112 + 32 + + bmw::os::Signal +-- +fontsize=14 +bg=white + + + + Relation + + 872 + 32 + 96 + 24 + + lt=<<<<<- + 10.0;10.0;100.0;10.0 + + + UMLClass + + 952 + 32 + 248 + 96 + + OsalInstances +-- ++ fcntl_osal: std::unique_ptr<bmw::os::Fcntl> ++ unistd: std::unique_ptr<bmw::os::Unistd> ++ mman: std::unique_ptr<bmw::os::Mman> ++ stat_osal: std::unique_ptr<bmw::os::Stat> ++ stdlib: std::unique_ptr<bmw::os::Stdlib> + + +fontsize=14 +bg=white + + + + UMLClass + + 1304 + 152 + 128 + 32 + + bmw::os::Stat +-- +fontsize=14 +bg=white + + + + UMLClass + + 1304 + 200 + 128 + 32 + + bmw::os::Stdlib +-- +fontsize=14 +bg=white + + + + Relation + + 1192 + 96 + 128 + 80 + + lt=<<<<<- + 10.0;10.0;140.0;80.0 + + + Relation + + 1192 + 112 + 128 + 120 + + lt=<<<<<- + 10.0;10.0;140.0;130.0 + + + UMLClass + + 336 + 96 + 128 + 56 + + <<ainterface>> + +Backend +- + + +bg=white +fontsize=14 + + + + Relation + + 280 + 104 + 72 + 32 + + lt=.>> + + 10.0;20.0;70.0;20.0 + + + UMLClass + + 336 + 168 + 128 + 48 + + template <typename T> +CircularAllocator +- + + +bg=white +fontsize=14 + + + + Relation + + 280 + 176 + 72 + 24 + + lt=<<<<<- + 70.0;10.0;10.0;10.0 + + + Relation + + 312 + 584 + 64 + 296 + + lt=<. + + + + + + + + + + + + + + + + +<<uses>> + 10.0;10.0;10.0;350.0 + + + Relation + + 384 + 632 + 848 + 248 + + lt=->>>>> + 1040.0;10.0;930.0;10.0;930.0;200.0;10.0;200.0;10.0;290.0 + + + Relation + + 352 + 584 + 328 + 248 + + lt=<. +<<uses>> + 10.0;10.0;10.0;170.0;390.0;170.0;390.0;290.0 + + + Relation + + 1080 + 672 + 152 + 168 + + lt=<<<<<- + 10.0;190.0;90.0;190.0;90.0;10.0;170.0;10.0 + + + UMLClass + + 1152 + 1344 + 200 + 32 + + bmw::concurrency::ThreadPool +-- +fontsize=14 +bg=white + + + + Relation + + 1080 + 1344 + 88 + 24 + + lt=<<<<<- + 10.0;10.0;90.0;10.0 + + + UMLClass + + 168 + 1248 + 128 + 32 + + bmw::os::Pthread +-- +fontsize=14 +bg=white + + + + UMLClass + + 40 + 1248 + 120 + 32 + + bmw::os::Unistd +-- +fontsize=14 +bg=white + + + + Relation + + 88 + 1216 + 24 + 48 + + lt=<<<<<- + 10.0;10.0;10.0;40.0 + + + Relation + + 224 + 1216 + 24 + 48 + + lt=<<<<<- + 10.0;10.0;10.0;40.0 + + + UMLClass + + 1152 + 1392 + 272 + 136 + + ConnectMessageFromClient +-- ++ appid: LoggingIdentifier ++ uid: uid_t ++ use_dynamic_identifier: bool ++ random_part: std::array<std::string::value_type, 6> ++ operator==(const ConnectMessageFromClient&, + const ConnectMessageFromClient&): bool ++ operator!=(const ConnectMessageFromClient&, + const ConnectMessageFromClient&): bool + + + +bg=white +fontsize=14 + + + + Relation + + 1080 + 1408 + 88 + 24 + + lt=<- + 90.0;10.0;10.0;10.0 + + + Relation + + 408 + 1120 + 72 + 24 + + lt=<<<<<- + 70.0;10.0;10.0;10.0 + + + Relation + + 352 + 1216 + 24 + 48 + + lt=<<<<<- + 10.0;10.0;10.0;40.0 + + diff --git a/mw/log/design/datarouter_backend/datarouter_backend_datarouterbackend.uxf b/mw/log/design/datarouter_backend/datarouter_backend_datarouterbackend.uxf new file mode 100644 index 0000000..86dfd86 --- /dev/null +++ b/mw/log/design/datarouter_backend/datarouter_backend_datarouterbackend.uxf @@ -0,0 +1,2 @@ +10UMLSpecialState2001002020type=initialUMLState10015022040Create shared memory writerUMLState10022022040Create message clientUMLState10029022040Run message clientRelation2001103060lt=<-10;40;10;10Relation2001803060lt=<-10;40;10;10Relation2002503060lt=<-10;40;10;10Relation2003203060lt=<-10;40;10;10UMLSpecialState2003602020type=finalUMLObject7060280350DataRouterBackend::DataRouterBackend +valign=top \ No newline at end of file diff --git a/mw/log/design/datarouter_backend/inter_process_communication.uxf b/mw/log/design/datarouter_backend/inter_process_communication.uxf new file mode 100644 index 0000000..1789ce7 --- /dev/null +++ b/mw/log/design/datarouter_backend/inter_process_communication.uxf @@ -0,0 +1,62 @@ +13Space for diagram notesUMLGeneric5278312455<<Process>> +Logging Client + +layer=1 +bg=white +fontsize=14UMLGeneric135278234455<<Process>> +Datarouter +layer=1 + +bg=white +fontsize=14UMLState78018219552Shared Memory +fontsize=12 +bg=white +fontsize=14Relation35118245578lt=<<<<<- +Creates and writes to +/tmp/logging.<appid>.<uid>.shmem +with permissions 0644 +fontsize=1410;20;330;20Relation96218241665lt=<- +Read-only access + +fontsize=1410;20;300;20Relation35132552078lt=-() +Instantiates side channel to receive messages from +/mw_com/message_passing/logging.<app>.<uid> + +bg=white +fontsize=1410;20;380;20Relation84532553378lt=)- +Creates a Sender for each client +to send requests to the client + +fontsize=1420;20;390;20UMLGeneric130156234351symbol=component +mw::log + +DatarouterBackend + +layer=2 +bg=white +fontsize=14UMLClass15633820839/mw::com::Receiver/ + +layer=3 +bg=white +fontsize=14UMLClass135232520839/mw::com::Sender/ + +layer=2 +bg=white +fontsize=14UMLClass15642920839/mw::com::Sender/ + +layer=3 +bg=white +fontsize=14UMLClass135241620839/mw::com::Receiver/ + +layer=2 +bg=white +fontsize=14Relation35141652078lt=)- +Creates a Sender +to reply to requests from Datarouter + +fontsize=14370;20;10;20Relation84541653378lt=-() +One receiver for all clients +/mw_com/message_passing/logging.datarouter_recv + +bg=white +fontsize=14390;20;10;20 diff --git a/mw/log/design/datarouter_backend/lock_free_concept.md b/mw/log/design/datarouter_backend/lock_free_concept.md new file mode 100644 index 0000000..0611e46 --- /dev/null +++ b/mw/log/design/datarouter_backend/lock_free_concept.md @@ -0,0 +1,243 @@ + + + + +# Lock-free logging with Datarouter backend + +- [Lock-free logging with Datarouter backend](#lock-free-logging-with-datarouter-backend) + - [Problem statement](#problem-statement) + - [Mutually exclusive ring buffer implementation](#mutually-exclusive-ring-buffer-implementation) + - [Towards a lock-free implementation](#towards-a-lock-free-implementation) + - [Solution A: Predictive Serializer](#solution-a-predictive-serializer) + - [Solution B: Ring buffer array: One ring buffer for each critical thread](#solution-b-ring-buffer-array-one-ring-buffer-for-each-critical-thread) + - [Comparison: implementation complexity VS configuration complexity](#comparison-implementation-complexity-vs-configuration-complexity) + - [Conclusion](#conclusion) + + +## Problem statement + +The logging public API is thread safe. It supports that users call logging from +multiple threads in parallel without external synchronization. Consider the +following example: + +```C++ +void Thread1(){ + LogInfo() << "Hello"; +} + +void Thread2(){ + LogInfo() << "Foobar"; +} +``` + +In addition, the user expects that the execution of the logging call does not +block on other threads. This is formulated by the robust design measures +([][1]) requirements to ensure freedom from interference (FFI). + +1. Cross-locking: Cross-application and cross-thread dependencies shall be + avoided. A thread logging in a loop shall not block other thread's execution. + ([][2]) + +2. Avoid locks: ara::log shall be free of time-unbound locks and shall implement + strategies to limit wait time. Time limit shall be defined based on + requirements from functions on individual ECU. Mutual exclusion mechanisms + shall include priority inversion protection. ([][3]) + +In the current implementation these requirements are violated because a mutex is +used to exclusively access a ring buffer. Thus it can be seen in applications +with many concurrent threads that invoke logging that there is mutex contention +and blocking: + +![Trace from ](trace_blocking.png) + +The trace from [][4] illustrates this problem clearly: Both the +sensor_cleaning and condition_evalu threads are logging concurrently and +execution is blocking each other. Thus the runtime behavior is such as if the +threads are running on a single CPU core due to the mutex. + +The implementation of the logging library needs to change to avoid such blocking +behavior and fullfil `` and ``. + +## Mutually exclusive ring buffer implementation + +To understand how the logging library was implemented initially with a central +mutex we need to dive into the details. For a verbose message the information +flow looks like this. + +1. Creation of LogStream instance + +On the creation of LogStream instance a slot is reserved on in the private +address space of the app. + +```C++ +auto log_stream = LogInfo(...); +``` + +The slot memory is reserved by mw::log on +initialization. For the reservation of a slot a lock-free algorithm is used. The +algorithm in essence iterates over all slots and picks the first available slot. +If no slot is available, the log message is dropped. This algorithm is +implemented using atomics and therefore lock free and has a fixed upper runtime +bound. + +```C++ +log_stream << "my message"; +``` + +The user then fills this slot with data that comprise the verbose +message. + +2. Flush of the LogStream + +```C++ +log_stream.Flush(); +``` + +When the user determines that the log message is complete the LogStream is +flushed. Then the content of the slot is copied into a ring buffer using +`MwsrWriter`. The ring buffer is allocated in a shared memory space and is read +by Datarouter. Datarouter is another process that has read only access to the +ring buffer via shared memory. The mechanism for flushing a log stream is same +as a sending a non-verbose message. In both cases the TRACE macro is used to +submit the message to Datarouter. + +3. Inside the TRACE macro + +```C++ +TRACE(log_entry); +``` + +Up until this point the logic is without mutex and lock free. Inside the TRACE +macro we now enter the critical section: + +```C++ +logger::instancePtr->wr_.allocAndWrite([this, &t](auto data, auto size) { + using s = ::bmw::common::visitor::logging_serializer; + auto idsize = s::serialize(id_, data, size); + return idsize + s::serialize(t, data + idsize, size - idsize); + }); +``` + +`allocAndWrite()` is a member of the `MwsrWriter` class that handles the access +to shared memory. It accepts a callback that provides a span to the reserved + memory in the ring buffer. In pseudo code, `allocAndWrite()` works like this: + +```C++ +void allocAndWrite(timestamp_t timestamp, Callback callback) { + lock(); + addr, available_space = ReserveMaximumRingBufferSpace(); + auto used_bytes = callback(addr, available_space); + ShrinkRingBufferToActuallyNeedSpace(used_bytes); + unlock(); +} +``` + +Thus the `allocAndWrite()` function defines a critical section that is mutually +exclusive for all calls to the logging API. The implementation was done in this +way because the serializer cannot tell in advance how many bytes the output will +have. Thus a maximum message length is at first reserved in the buffer, then the +serializer runs and returns the actual length of the output. Finally the ring +buffer shrinks again to the actual used size. Obviously such an implementation +cannot work concurrently. As each message is directly placed after the +previous message in the ring buffer it needs to know where the previous message +ends. Thus it has to wait until the completion of the previous message. + +## Towards a lock-free implementation + +Changing the implementation to avoid mutex contention we have at least two options: + +(A) Implement a predictive serializer that can output the needed capacity for a +message in advance before serialization. + +(B) Introduce one individual ring buffer for each 'critical' thread. + +### Solution A: Predictive Serializer + +Since the serializer could predict how big a serialized message is going to be, +we can attempt to reserve that memory without blocking concurrent writers. Once +the memory is reserved concurrent writers can already reserve chunks directly +after the last reserved chunk. After a chunk is completely filled with the +serialized data, then we mark it as ready for reading by setting an atomic flag. +Using another atomic flag, the reader will mark a chunk as read. Then the +writing threads know that it is safe to reuse that memory for future entries +into the ring buffer. + +![Single concurrent ring buffer](single_concurrent_ring_buffer.svg) + +With this approach we would need to reach the following milestones + +1. Use the serializer library to output a size prediction. +2. Refactor MwsrWriter and MwsrReader to replace the mutex with lock free algorithms. + +Note that we could consider this approach without the predictive serializer. We +could allocate the maximum possible message length (around 64K) for each +message. The serializer could use the available space and the unused parts would +be left as padding. However, most messages only contain a few hundred bytes and +therefore reserving 64 K for each message would be a massive waste of RAM. As we +are strongly memory constrained on target, this simple but wasteful idea is not +feasible in practice. + +### Solution B: Ring buffer array: One ring buffer for each critical thread + +Alternatively we could consider introducing a ring buffer array for each +critical thread. By critical thread we mean threads that are defined by the user +that need the mutex-free behavior of the logging library. We forsee that not +each thread on the system will need such guarantees and thus we can save RAM by +minimizing the number of ring buffers we create. + +![Ring buffer array](ring_buffer_array.svg) + +With this approach we have the following milestones: + +1. Refactor MwrsWriter and MwsrReader to support multiple ring buffers + internally. +2. Adjust the side channel communication including Datarouter to support + multiple ring buffers. +3. Extend the configuration to allow for multiple ring buffers with different + sizes. +4. Apply the correct configuration for each user. + +Some practical concerns come to mind with this approach. For example how to +identify a critical thread on runtime. This could be done through the thread +name, but the thread name could change dynamically. Also some threads are +implemented by third parties (e.g. AA stack). For those it is hard to identify +which threads should be considered critical, and thread names might change over +time, or new threads are added. + +### Comparison: implementation complexity VS configuration complexity + +The main advantage of solution A is that it would be memory efficient. With +multiple ring buffer some memory would be wasted, as each ring buffer has to be +sized for the worst case load in order to avoid drops. + +One downside of solution A would be that the size prediction of the serializer +would cost some runtime performance. The serializer would probably run twice, +once in a 'visitor only mode' without copying any data just to determine the +size, and a second time doing the actual serialization. However, the visitation +step could be optimized further to reduce the runtime impact. + +Regarding solution B the implementation effort would be smaller as we don't have +to change the serializer. However, there is a higher burden on the user to +correctly specify all the critical threads in the logging configuration. Thus we +expect a longer and more complex integration phase with this approach. + +## Conclusion + +Due to the advantages of Solution A in terms of memory consumption and ease of +use we move forward with solution A. The main challenges thereby lie in the +refactoring of MwsrWriter to introduce a lock-free operation based on that. + +[1]: +[2]: +[3]: +[4]: /browse/ diff --git a/mw/log/design/datarouter_backend/ring_buffer_array.svg b/mw/log/design/datarouter_backend/ring_buffer_array.svg new file mode 100644 index 0000000..818ee19 --- /dev/null +++ b/mw/log/design/datarouter_backend/ring_buffer_array.svg @@ -0,0 +1,5 @@ + + + + +
Ring Buffer Array (Shared Memory)
Ring Buffer Arra...
header
header
Payload
Payload
header
header
Payload
Payload
Thread A
is writing 
Thread A...
Thread B
is writing
Thread B...
header
header
Payload
Payload
Free space
Free space
Ring Buffer 0
Ring Buffe...
Ring Buffer 1
Ring Buffe...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/mw/log/design/datarouter_backend/single_concurrent_ring_buffer.svg b/mw/log/design/datarouter_backend/single_concurrent_ring_buffer.svg new file mode 100644 index 0000000..8c7b924 --- /dev/null +++ b/mw/log/design/datarouter_backend/single_concurrent_ring_buffer.svg @@ -0,0 +1,5 @@ + + + + +
Ring Buffer (Shared Memory)
Ring Buffer (Sha...
read_index: atomic_int
read_index: a...
read: atomic_bool
ready: atomic_bool
length: size_t
read: atomic_bool...
Payload
Payload
read: atomic_bool
ready: atomic_bool
length: size_t
read: atomic_bool...
Payload
Payload
write_index: atomic_int
write_inde...
Thread A
is writing 
Thread A...
read: atomic_bool
ready: atomic_bool
length: size_t
read: atomic_bool...
Payload
Payload
Thread B
is writing
Thread B...
Datarouter
is reading
Datarouter...
Update through
side-channel IPC
Update thro...
Process bounday
Process bo...
Read-only access to shared memory
Read-only access to...
Free space
Free space
Text is not SVG - cannot display
\ No newline at end of file diff --git a/mw/log/design/datarouter_backend/trace_blocking.png b/mw/log/design/datarouter_backend/trace_blocking.png new file mode 100644 index 0000000..67f2251 Binary files /dev/null and b/mw/log/design/datarouter_backend/trace_blocking.png differ diff --git a/mw/log/design/datarouter_message_client_impl_connecttodatarouter.uxf b/mw/log/design/datarouter_message_client_impl_connecttodatarouter.uxf new file mode 100644 index 0000000..7967367 --- /dev/null +++ b/mw/log/design/datarouter_message_client_impl_connecttodatarouter.uxf @@ -0,0 +1,20 @@ +10Space for diagram notesUMLSpecialState230402020type=initialUMLState1109025040Block termination SignalUMLState11016025040Create senderUMLState11023025040Start receiverUMLSpecialState2306902020type=finalRelation230503060lt=<-10;40;10;10Relation2301203060lt=<-10;40;10;10Relation2301903060lt=<-10;40;10;10Relation2305205060lt=<- +[No]10;40;10;10UMLSpecialState110430260100Is exit requsted? +bg=red +type=decisionRelation36047070150lt=<- +m2=[Yes]20;130;20;10;10;10Relation2305903090lt=<-10;70;10;10UMLObject800550740DatarouterMessageClientImpl::ConnectToDatarouter +valign=topUMLSpecialState110300260100Receiver started? +bg=red +type=decisionRelation2303905060lt=<- +[Yes]10;40;10;10Relation2302603060lt=<-10;40;10;10Relation360340170100lt=<- +m2=[No]150;80;150;10;10;10UMLState11056025040Send connect messageUMLState42042018040Unlink shared memory fileUMLState42049018040Request stop for connectionRelation5004503060lt=<-10;40;10;10UMLState42056018040Shutdown thread poolRelation5005203060lt=<-10;40;10;10Relation40059013060lt=<-10;40;110;40;110;10UMLNote37070220140style=wordwrap + +*Note:* +The logger thread (owned by adaptive application) should not react to the Terminate Signal. Instead the Terminate Signal should be handled by mw::lifecycle for Adaptive applications. + +bg=orangeText1706806040Finish +halign=center +style=autoresizeUMLSpecialState3506006060merge +type=decision +layer=-2Relation2306503060lt=->10;10;10;40UMLSyncBarHorizontal2006508020lw=5 +Relation25062012060lt=<-10;40;10;10;100;10 \ No newline at end of file diff --git a/mw/log/design/dependency_graph.md b/mw/log/design/dependency_graph.md new file mode 100644 index 0000000..8905406 --- /dev/null +++ b/mw/log/design/dependency_graph.md @@ -0,0 +1,47 @@ + + + + +# Dependency graph + +## Internal mw::log dependency graph + +The dependency graph below shows the separation of the frontend from the +implementation details of the backends. Applying the component principles from +[1] the goal here is to structure the dependencies in a way that instable +components depend on stable components. + +![Use Case Diagram](/swh/ddad_platform/aas/mw/log/design/frontend_dependency_graph.uxf?ref=c83de0a646f18071d97680cae5786c3c44f9d848) + +The `mw::log` component is divided in a stable frontend and the backends with +the implementation details. The frontend shall be API compatible to the ara::log +standard 20-11 for the `Logger` and `LogStream`. In addition, the frontend +contains the interfaces `IRecorder` and `IRecorderFactory` to abstract the +backend. The backend implementation will depend on the frontend and implement +the interfaces and the concrete recorder factory. + +Important is that the component boundaries between frontend and details are only +crossed **one-way**. Thus the frontend component must **not** depend on the details +component, and should only contain the essential entities that are needed for +the public API. + +In order to instantiate the concrete recorder factory, the `Runtime` depends on +the `RecorderFactory`. This would violate the one-way principle from above. +However, we break the cyclic dependency cycle by declaring the +`CreateRecorderFactory()` function in the `IRecorderFactory` interface and only +supply the implementation of that in the details component. + +## References + +[1] Martin, Robert C., et al. Clean architecture: a craftsman's guide to +software structure and design. No. s 31. Prentice Hall, 2018. diff --git a/mw/log/design/error_domain.uxf b/mw/log/design/error_domain.uxf new file mode 100644 index 0000000..d3ce4e2 --- /dev/null +++ b/mw/log/design/error_domain.uxf @@ -0,0 +1,51 @@ + + + + 10 + + UMLClass + + 40 + 30 + 480 + 90 + + <<Interface>> + +bmw::result::ErrorDomain +-- +/+MessageFor(const bmw::result::ErrorCode&): amp::string_view/ + +bg=white +fontsize=14 + + + + UMLClass + + 60 + 170 + 430 + 80 + + ErrorDomain + +-- ++ MessageFor(const bmw::result::ErrorCode&): amp::string_view + +bg=white +fontsize=14 + + + + Relation + + 260 + 110 + 30 + 80 + + lt=<<. + 10.0;10.0;10.0;60.0 + + diff --git a/mw/log/design/file_output_backend.md b/mw/log/design/file_output_backend.md new file mode 100644 index 0000000..ce00851 --- /dev/null +++ b/mw/log/design/file_output_backend.md @@ -0,0 +1,61 @@ + + + + +# FileOutputBackend + +`LogFlushBackend` implements `mw::log::Backend`. It is responsible for providing +buffers by means of `ReserveSlot()` call and writing data using provided +callable object when the buffer is returned by the user by means of calling +`FlushSlot()`. It assumes non-blocking calls and implements measures to store +and manage buffers till all the data is pushed out in subsequent calls. Logs +are stored in a circular buffer in the case when the current `LogRecord` is not +completely flushed due to provided sink method not consuming all the data. The +rest of the data is to be pushed in subsequent calls to `FlushSlot` looping over +a circular buffer to reduce the queue of unprocessed logs in the order provided +by the caller. Whenever each `LogRecord` is flushed completely, the buffer is +released to the allocator by Release Slot. +In case of depletion of a fixed number of buffers, none is returned to the +caller rather than overwriting old ones. +The zero-copy approach was used inside the backend design. Circular buffers +store objects of `SlotHandle` type which is just a handle to buffers storing +data. It is part of the job of the Recorder to format and copy content into the +buffers. +The zero-copy principle does not hold for the data provided at a level of and +above Recorder. +Configuration of the backend is done through resources passed down in +constructor arguments consisting of slot allocator and circular buffer. + +Design decision has been made to make backend independent of loosely coupled +to details of data it processes which is presented by `LogRecord` type. To do +so object providing `IMessageBuilder` interface is passed down by Recorder to +allow access data in polymorphic way. This interface makes it possible to access +set of sequential data buffers in order they should be serialized into a file +or onto the console. +`MessageBuilder` is responsible for serving data in correct order of flushing +into file or console and stores some of common parts of the header. + +![Static Design](/swh/ddad_platform/aas/mw/log/design/mw_log_file_backend.uxf?ref=a0f7d7e092a6d561d0c889a2faf752acc969f474) + +`SlotDrainer` is responsible for storing and disposal of already serialized +data. First data gets inserted into circular buffer. After that step program +flow enters a loop that iterates over available slots in ring buffer and then +by means of `NonBlockingWriter` iterates over all spans of each message. + +![Sequence Design](/swh/ddad_platform/aas/mw/log/design/slot_drainer_sequence_design.uxf?ref=be7b21852077fc3db76686d352d9907ec8495a08) + +Flush procedure exits whenever all available data is written to the file or +writing procedure would block i.e. write operation reports that number of bytes +written is less then requested. + +![Action Diagram](/swh/ddad_platform/aas/mw/log/design/slot_drainer_action_diagram_design.uxf?ref=be7b21852077fc3db76686d352d9907ec8495a08) diff --git a/mw/log/design/frontend_dependency_graph.uxf b/mw/log/design/frontend_dependency_graph.uxf new file mode 100644 index 0000000..d5380ee --- /dev/null +++ b/mw/log/design/frontend_dependency_graph.uxf @@ -0,0 +1,19 @@ +10UMLClass51012033060Logger + +Includes classes and free functionsUMLClass52029014030LogStream +UMLClass69029047030Runtime +UMLClass75021017030LoggerContainerUMLClass51021022030LogStreamFactory +Relation64017030140lt=<-10;120;10;10UMLGeneric200501040390symbol=component +mw::log frontend + //platform/aas/mw/log:frontendUMLNote9079018060Instable components shall +depend on stable classes. +bg=yellowRelation120070810lt=[stability]<-30;10;30;790UMLClass56037010030Log TypesRelation6003103080lt=<-10;60;10;10UMLClass70039020030IRecorderRelation5302303080lt=<-10;60;10;10Relation8102303080lt=<-10;10;10;60Relation7601703060lt=<-10;10;10;40Relation79031030100lt=<-10;80;10;10UMLClass56061014030DataRouterRecorderRelation620410170220lt=<-150;10;10;200UMLClass74061014030ConsoleRecorderUMLClass90061014030FileRecorderRelation79041040220lt=<-10;10;20;200Relation820410140220lt=<-10;10;120;200Relation5301703060lt=<-10;40;10;10UMLNote2108029060Frontend contains the public user API, +and the necessary classes to interface +with the backend and implementation details. + +bg=yellowUMLClass30071077030RecorderFactoryRelation1050310170420lt=<. +decoupled dependency +through +CreateRecorderFactory()10;400;10;10UMLGeneric2004601040310symbol=component +mw::log implementation details + //platform/aas/mw/log/detailRelation620630210100lt=<-10;10;190;80Relation81063030100lt=<-10;10;10;80Relation830630150100lt=<-130;10;10;80Relation7002303080lt=<-10;60;10;10UMLClass93039018030IRecorderFactoryRelation98031030100lt=<-10;80;10;10UMLClass25050013030ConfigurationUMLClass22061014030TargetConfigReaderRelation30063070100lt=<-10;10;50;80Relation34052070210lt=<-10;10;50;190Relation27052050110lt=<-30;10;10;90UMLClass39061014030CompositeRecorderRelation520630260100lt=<-10;10;240;80Relation450410320220lt=<-300;10;10;200 \ No newline at end of file diff --git a/mw/log/design/mw_log_datarouter_recorder.uxf b/mw/log/design/mw_log_datarouter_recorder.uxf new file mode 100644 index 0000000..4acf550 --- /dev/null +++ b/mw/log/design/mw_log_datarouter_recorder.uxf @@ -0,0 +1,228 @@ + + + 10 + + UMLClass + + 170 + 20 + 540 + 230 + + DataRouterRecorder + +-- +- LogData(const SlotHandle&, const T data): void +- SetApplicationId(LogRecord&): void +- backend_: std::unique_ptr<Backend> +- config_: Configuration +- statistics_reporter_: StatisticsReporter +-- ++ DataRouterRecorder(std::unique_ptr<Backend>&&, const Configuration&) ++ StartRecord(const amp::string_view, const LogLevel): amp::optional<SlotHandle> ++ StopRecrod(const SlotHandle&): void ++ Log(const SlotHandle&, const T) - family of funtions ++ IsLogEnabled(const LogLevel&, const amp::string_view) : bool + + + + +layer=2 +bg=white +fontsize=14 + + + + UMLClass + + 10 + 120 + 130 + 50 + + <<Singleton>> +mw::log::Runtime + + +layer=2 +bg=white +fontsize=14 + + + + Relation + + 50 + 50 + 140 + 90 + + lt=<- +is owned by +layer=2 +fontsize=14 + + 20.0;70.0;20.0;20.0;120.0;20.0 + + + UMLClass + + 170 + 300 + 430 + 250 + + StatisticsReporter +-- +- recorder_: Recorder& +- report_interval_: std::chrono::seconds +- number_of_slots_: std::size_t +- slot_size_bytes_: std::size_t +- no_slot_available_counter_: std::atomic<std::size_t> +- message_too_long_counter_: std::atomic<std::size_t> +- last_report_time_point_nanoseconds_: std::atomic<std::int64_t> +- currently_reporting_: std::atomic_bool +-- ++ StatisticsReporter(Recorder&, const std::chrono::seconds, + const std::size_t, const std::size_t) ++ IncrementNoSlotAvailable(): void ++ IncrementMessageTooLong(): voi+ g mn d ++ Update(const std::chrono::steady_clock::time_point&): void + + + + +layer=2 +bg=white +fontsize=14 + + + + Relation + + 370 + 240 + 30 + 80 + + lt=<<<<<- +layer=2 + 10.0;10.0;10.0;60.0 + + + Relation + + 700 + 140 + 180 + 30 + + lt=<<<<<- +layer=2 + 10.0;10.0;160.0;10.0 + + + UMLClass + + 670 + 300 + 390 + 120 + + << interface >> + +IStatisticsReporter +-- ++ IncrementNoSlotAvailable(): void ++ IncrementMessageTooLong(): void ++ Update(const std::chrono::steady_clock::time_point&): void + +bg=white +fontsize=14 + + + + UMLClass + + 860 + 120 + 150 + 50 + + Configuration +-- + + +bg=white +fontsize=14 + + + + Relation + + 590 + 330 + 100 + 40 + + lt=.>> + + 10.0;20.0;80.0;20.0 + + + UMLClass + + 860 + 30 + 150 + 50 + + DataRouterBackend +-- + +bg=white +fontsize=14 + + + + Relation + + 700 + 40 + 180 + 30 + + lt=<<<<<- +layer=2 + 10.0;10.0;160.0;10.0 + + + UMLClass + + 670 + 450 + 130 + 50 + + Recorder +-- + +layer=2 +bg=white +fontsize=14 + + + + Relation + + 590 + 450 + 100 + 50 + + lt=-> +uses + + 10.0;20.0;80.0;20.0 + + diff --git a/mw/log/design/mw_log_file_backend.uxf b/mw/log/design/mw_log_file_backend.uxf new file mode 100644 index 0000000..f68d620 --- /dev/null +++ b/mw/log/design/mw_log_file_backend.uxf @@ -0,0 +1,891 @@ + + + Space for diagram notes + 10 + + Relation + + 420 + 1130 + 80 + 90 + + lt=<<<<<- + 10.0;70.0;60.0;10.0 + + + UMLClass + + 880 + 300 + 420 + 560 + + mw::log::detail::SlotDrainer +-- +-TryFlushSlots(): amp::expected<FlushResult, + \ bmw::mw::log::detail::Error> +-TryFlushSpans(): amp::expected<FlushResult, + \ bmw::mw::log::detail::Error> +-MoreSlotsAvailableAndLoaded():bool +-MoreSpansAvailableAndLoaded():bool + +-allocator_: std::unique_ptr<CircularAllocator<LogRecord>>& +-message_builder_:std::unique_ptr<IMessageBuilder> +-context_mutex_:std::mutex +-circular_buffer_:amp::circular_buffer<SlotHandle, kMaxCircularBufferSize> +-current_slot_:amp::optional<std::reference_wrapper<const SlotHandle>> +-non_blocking_writer_: NonBlockingWriter +-limit_slots_in_one_cycle_: const std::size_t +-- ++SlotDrainer(std::unique_ptr<IMessageBuilder>, + std::unique_ptr<CircularAllocator<LogRecord>>&, + const std::int32_t, + const std::size_t, + std::unique_ptr<bmw::os::Unistd>) ++PushBack(const SlotHandle&):void ++Flush():void +-- +Responsibilities +-- Drains slots - empties circular_buffer +when subsequent data has been flushed +-- Returns status if data was flushed or stalled + + + +bg=white +fontsize=14 + + + + UMLClass + + 110 + 1110 + 320 + 240 + + mw::log::Recorder +-- ++StartRecord(ctx:amp::string_view, ll:LogLevel): + amp::optional<SlotHandle> ++StopRecord(slot:SlotHandle&) ++Log( T data ) - family of functions + +-- +-backend:unique_ptr<mw::log::Backend> + +-- +Responsibilities: +-- Opens and closes file. +Uses formatters to serialize +user data and header. + + +bg=white +fontsize=14 + + + + UMLClass + + 480 + 1020 + 340 + 120 + + mw::log::detail::Backend +{interface} +-- ++ReserveSlot(): amp::optional<SlotHandle> ++FlushSlot(const SlotHandle&) ++GetLogRecord(const SlotHandle&): LogRecord& + + + + +bg=white +fontsize=14 + + + + + UMLClass + + 880 + 910 + 420 + 470 + + mw::log::detail::FileOutputBackend +{implements backend} +-- +-buffer_allocator_: std::unique_ptr<CircularAllocator<LogRecord>> +-slot_drainer_:SlotDrainer +-- ++FileOutputBackend(std::unique_ptr<IMessageBuilder>, + const std::int32_t, + std::unique_ptr<CircularAllocator<LogRecord>>, + std::unique_ptr<bmw::os::Fcntl>) ++ReserveSlot():amp::optional<SlotHandle> ++FlushSlotconst SlotHandle&):void ++GetLogRecord(const SlotHandle&): LogRecord& +-- +Responsibilities +-- keeps statistics of dropped messages + +-- +Notes: +-- Maybe running in separate thread +-- part of configuration will be passed through + + +bg=white +fontsize=14 + + + + UMLClass + + 1470 + 750 + 330 + 260 + + template <typename T> + +mw::log::detail::CircularAllocator +-- +-claimed_sequence_:std::atomic<std::size_t> +-buffer_:std::vector<Slot<T>> +-- ++CircularAllocator(std::size_t, const T&) ++AcquireSlotToWrite(): amp::optional<std::size_t> ++GetUnderlyingBufferFor(std::size_t): T& ++ReleaseSlot(std::size_t):void ++GetUsedCount():size_t + + +bg=white +fontsize=14 + + + + Relation + + 1290 + 450 + 200 + 30 + + lt=<<<<<- + 10.0;10.0;180.0;10.0 + + + UMLClass + + 210 + 10 + 540 + 370 + + mw::log::detail::NonBlockingWriter +-- +- InternalFlush(const uint64_t): amp::expected<ssize_t, bmw::os::Error> +- unistd_: std::unique_ptr<bmw::os::Unistd> +- file_handle_: std::int32_t +- number_of_flushed_bytes_: uint64_t +- buffer_: amp::span<const std::uint8_t> +- buffer_flushed_: Result +- max_chunk_size_: std::size_t +-- ++ NonBlockingWriter(const std::int32_t, std::size_t, std::unique_ptr<bmw::os::Unistd>) ++ FlushIntoFile(): amp::expected<Result, bmw::mw::log::detail::Error> ++ SetSpan(const amp::span<const std::uint8_t>&): void ++ GetMaxChunkSize(): std::size_t +-- +Responsibilities +-- Write logs into file + +bg=white +fontsize=14 + + + + UMLClass + + 1470 + 350 + 330 + 320 + + amp::circular_buffer<T=SlotHandle,MaxSize> +-- +Responsibilities +-- stores elements of static size +-- overwrites data when full +-- realizes FIFO pattern + + +bg=white +fontsize=14 + + + + Relation + + 740 + 330 + 160 + 30 + + lt=<<<<<- + 140.0;10.0;10.0;10.0 + + + Relation + + 1290 + 630 + 220 + 140 + + lt=<<<<- + 10.0;10.0;200.0;120.0 + + + Relation + + 1080 + 850 + 30 + 90 + + lt=<<<<<- + 10.0;62.0;10.0;10.0 + + + Relation + + 1290 + 840 + 200 + 300 + + lt=<<<<<- + 10.0;280.0;180.0;10.0 + + + Relation + + 1290 + 800 + 200 + 50 + + lt=<. +GetUnderlyingBufferFor() +ReleaseSlot() + 180.0;20.0;10.0;22.0 + + + Relation + + 1670 + 660 + 270 + 110 + + lt=<. +Created based on CircularAllocatorSize + 10.0;10.0;10.0;90.0 + + + Relation + + 1290 + 900 + 270 + 320 + + lt=<. +AcquireSlotToWrite() +GetUnderlyingBufferFor() + 180.0;10.0;10.0;300.0 + + + UMLClass + + 880 + 120 + 110 + 50 + + OSAL::Unistd +-- + + +bg=white +fontsize=14 + + + + Relation + + 740 + 120 + 160 + 30 + + lt=<<<<<- + 10.0;10.0;140.0;10.0 + + + UMLClass + + 330 + 1020 + 100 + 40 + + OSAL::Unistd +-- + + +bg=white +fontsize=14 + + + + Relation + + 740 + 140 + 160 + 40 + + lt=<. +write + 140.0;17.0;10.0;20.0 + + + Relation + + 190 + 1040 + 260 + 90 + + lt=<. +close + 240.0;17.0;10.0;70.0 + + + UMLClass + + 10 + 1020 + 100 + 40 + + OSAL::fcntl +-- + + +bg=white +fontsize=14 + + + + Relation + + 70 + 1020 + 180 + 110 + + lt=<. +open +SetNonBlocking / fctrl call + 40.0;17.0;130.0;90.0 + + + Relation + + 810 + 1040 + 90 + 60 + + lt=<<. + 10.0;40.0;70.0;10.0 + + + UMLClass + + 180 + 730 + 270 + 130 + + mw::log::detail::DltMessageBuilder +-- ++DltMessageBuilder(ecu_id) ++GetNextSpan() + :amp::optional< + / amp::span<const std::uint8_t>> ++SetNextMessage(LogRecord&): void + + +bg=white +fontsize=14 + + + + UMLClass + + 480 + 1240 + 340 + 140 + + mw::log::detail::BackendLogMock +-- ++ReserveSlot(): amp::optional<SlotHandle> ++FlushSlot(const SlotHandle&) ++GetLogRecord(const SlotHandle&): LogRecord& + + + +bg=pink +fontsize=14 + + + + Relation + + 630 + 1130 + 30 + 130 + + lt=<<. + 10.0;10.0;10.0;110.0 + + + UMLClass + + 1470 + 1090 + 330 + 250 + + mw::log::detail::LogRecord +-- +-logEntry_:LogEntry +-verbosePayload_:VerbosePayload +-- +LogRecord(const std::size_t) +getLogEntry(): LogEntry& +getLogEntry(): const LogEntry& +getVerbosePayload(): VerbosePayload& +getVerbosePayload(): const VerbosePayload& + + + +bg=orange +fontsize=14 + + + + + UMLClass + + 360 + 450 + 250 + 230 + + /mw::log::detail::IMessageBuilder/ +-- ++GetNextSpan() + :amp::optional< + / amp::span<const std::uint8_t>> ++SetNextMessage(LogRecord&): void +-- +Responsibility: +iterates over spans of data to be +serialized into output stream. + + +bg=white +fontsize=14 + + + + UMLClass + + 460 + 730 + 320 + 130 + + mw::log::detail::TextMessageBuilder +-- ++TextMessageBuilder(app_id, ecu_id, ) ++GetNextSpan() + :amp::optional< + / amp::span<const std::uint8_t>> ++SetNextMessage(log_record:LogRecord&): void + + +bg=white +fontsize=14 + + + + Relation + + 500 + 670 + 160 + 80 + + lt=<<- + 10.0;10.0;140.0;60.0 + + + Relation + + 280 + 670 + 200 + 80 + + lt=<<- + 180.0;10.0;10.0;60.0 + + + UMLNote + + 560 + 270 + 180 + 100 + + Test strategy: +Inject OSAL mock +by constructor argument +with default value + + +bg=green +fontsize=14 +layer=1 + + + + UMLNote + + 1090 + 750 + 200 + 100 + + Test strategy: +Inject NonBlocking Writer +and using OSAL mock +Configure circular buffer size +Use MessageBuilderMock + + +bg=green +fontsize=14 +layer=1 + + + + UMLNote + + 1590 + 580 + 200 + 80 + + Test strategy: +using amp::circular_buffer +not tested here + + +bg=green +fontsize=14 +layer=1 + + + + UMLNote + + 890 + 1260 + 180 + 110 + + Test strategy: +Test just a glue code and +not underlying classes +Inject SlotDrainer with +mocked OSAL +Use MessageBuilderMock + + +bg=green +fontsize=14 +layer=1 + + + + UMLNote + + 1590 + 950 + 200 + 50 + + Test strategy: +Tested in data_router + + + +bg=green +fontsize=14 +layer=1 + + + + UMLNote + + 400 + 620 + 200 + 50 + + Test strategy: +Insert the data, verify outcome + + +bg=green +fontsize=14 +layer=1 + + + + UMLNote + + 1480 + 460 + 310 + 110 + + By design we exclude possibility of dropped +messages in CircularBuffer because the +size of SlotAllocator is smaller or less +than CircularBuffer size. + +Size of amp::CircularBuffer + + +bg=yellow +fontsize=14 +layer=1 + + + + UMLNote + + 1080 + 1260 + 210 + 110 + + Only place that messages +may be dropped for the +reason of failure to get buffer +from SlotAllocatror + + +bg=yellow +fontsize=14 +layer=1 + + + + UMLClass + + 170 + 410 + 620 + 590 + + Synchronised access only: guarded by SlotDrainer context mutex. + + + +fontsize=14 + + + + UMLNote + + 740 + 1260 + 70 + 30 + + Mock + + + +bg=green +fontsize=14 +layer=1 + + + + Relation + + 600 + 530 + 300 + 30 + + lt=<<<<<- + 280.0;10.0;10.0;10.0 + + + UMLNote + + 1530 + 1250 + 260 + 80 + + Note.. + +Used for data types +that differ from message to message + + +bg=blue +fontsize=14 +layer=1 + + + + UMLClass + + 1860 + 810 + 190 + 120 + + template <typename T> + +mw::log::detail::Slot +-- ++ data: T ++ in_use: std::atomic<bool> + + +bg=white +fontsize=14 + + + + Relation + + 1790 + 830 + 90 + 40 + + lt=<<<<<- +m1=0..n + 10.0;10.0;70.0;10.0 + + + UMLClass + + 370 + 940 + 200 + 50 + + VerbosePayload +-- + + +bg=white +fontsize=14 + + + + UMLClass + + 370 + 880 + 200 + 50 + + LoggingIdentifier +-- + + +bg=white +fontsize=14 + + + + Relation + + 310 + 850 + 80 + 70 + + lt=<<<<<- + 10.0;10.0;60.0;50.0 + + + Relation + + 250 + 850 + 140 + 130 + + lt=<<<<<- + 10.0;10.0;120.0;110.0 + + + Relation + + 560 + 850 + 70 + 70 + + lt=<<<<<- + 50.0;10.0;10.0;50.0 + + + Relation + + 560 + 850 + 120 + 130 + + lt=<<<<<- + 100.0;10.0;10.0;110.0 + + diff --git a/mw/log/design/mw_log_recorders.uxf b/mw/log/design/mw_log_recorders.uxf new file mode 100644 index 0000000..0874cb2 --- /dev/null +++ b/mw/log/design/mw_log_recorders.uxf @@ -0,0 +1,693 @@ + + + Space for diagram notes + 10 + + UMLClass + + 80 + 430 + 620 + 220 + + mw::log::detail::TextRecorder +-- +- backend_ : std::unique_ptr<Backend> +- config_ : Configuration +- check_log_level_for_console_: bool +-- ++ TextRecorder(const detail::Configuration&, + std::unique_ptr<detail::Backend>, + const bool) ++ StartRecord(ctx:amp::string_view, const LogLevel): amp::optional<SlotHandle> ++ StopRecord(const SlotHandle&): void ++ IsLogEnabled(const LogLevel&, const amp::string_view): bool + ++ Log(const SlotHandle&, T data) - family of functions + + + +bg=white +fontsize=14 + + + + UMLClass + + 30 + 1280 + 670 + 230 + + << interface >> + +mw::log::Recorder +-- ++ StartRecord(ctx:amp::string_view, log_level:LogLevel) : amp::optional<SlotHandle> ++ StopRecord(slot:SlotHandle&): void ++ Log(const SlotHandle&, const T data) - family of functions ++ IsLogEnabled(const LogLevel&, const amp::string_view): bool + + +bg=white +fontsize=14 + + + + Relation + + 40 + 360 + 30 + 940 + + lt=<<. + 10.0;920.0;10.0;10.0 + + + UMLClass + + 820 + 1060 + 440 + 140 + + <<interface>> + +mw::log::detail::Backend +-- ++ReserveSlot() : amp::optional<SlotHandle> ++FlushSlot(slot : const SlotHandle&) ++GetLogRecord(slot : const SlotHandle&): LogRecord& + + +bg=white +fontsize=14 + + + + UMLClass + + 820 + 370 + 440 + 50 + + mw::log::detail::FileOutputBackend + + +bg=white +fontsize=14 + + + + UMLClass + + 40 + 170 + 660 + 200 + + mw::log::detail::FileRecorder +-- +- backend_ : std::unique_ptr<Backend> +- config_ : Configuration +-- ++ FileRecorder(const detail::Configuration&, + std::unique_ptr<detail::Backend>) ++ StartRecord(ctx:amp::string_view, const LogLevel): amp::optional<SlotHandle> ++ StopRecord(const SlotHandle&): void ++ IsLogEnabled(const LogLevel&, const amp::string_view): bool + ++ Log(const SlotHandle&, T data) - family of functions + + +bg=white +fontsize=14 + + + + Relation + + 80 + 640 + 30 + 660 + + lt=<<. + 10.0;640.0;10.0;10.0 + + + UMLClass + + 580 + 0 + 120 + 40 + + OSAL::Unistd + + +bg=white +fontsize=14 + + + + Relation + + 530 + 20 + 140 + 170 + + lt=<. +close + 120.0;17.0;10.0;150.0 + + + UMLClass + + 270 + 0 + 110 + 40 + + OSAL::fcntl + + +bg=white +fontsize=14 + + + + Relation + + 330 + 20 + 230 + 170 + + lt=<. +open +SetNonBlocking / fctrl call + 10.0;17.0;210.0;150.0 + + + UMLClass + + 460 + 1190 + 240 + 50 + + mw::log::detail::RecorderMock + + +bg=pink +fontsize=14 + + + + Relation + + 540 + 1230 + 30 + 70 + + lt=<<. + 10.0;50.0;10.0;10.0 + + + Relation + + 1170 + 840 + 30 + 240 + + lt=<<. + 10.0;220.0;10.0;10.0 + + + UMLClass + + 820 + 230 + 440 + 110 + + DltArgumentCounter +-- +- counter_ : std::uint8_t& +-- +DltArgumentCounter(std::uint8_t&) +-- ++ TryAddArgument(add_argument_callback) : AddArgumentResult + + +bg=white +fontsize=14 + + + + Relation + + 690 + 180 + 150 + 90 + + lt=<<<<<- + 10.0;10.0;130.0;70.0 + + + Relation + + 690 + 240 + 150 + 320 + + lt=<<<<<- + 10.0;300.0;130.0;10.0 + + + UMLClass + + 820 + 0 + 440 + 210 + + mw::log::detail::TextFormat +-- +_+ PutFormattedTime(VerbosePayload&) : void_ +_+ TerminateLog(VerbosePayload&) : void_ +_+ Log(VerbosePayload&, const T, const IntegerRepresentation) : void_ + - family of functions +_+ Log(VerbosePayload&, const T) : void_ + - family of functions + +bg=white +fontsize=14 + + + + Relation + + 640 + 80 + 200 + 110 + + lt=<. +Log + 180.0;17.0;10.0;90.0 + + + Relation + + 680 + 110 + 160 + 340 + + lt=<. +Log + 140.0;17.0;10.0;320.0 + + + UMLClass + + 820 + 580 + 380 + 270 + + mw::log::detail::SlogBackend +-- +- app_id_: std:;string +- buffer_: CircularAllocator<mw::log::detail::LogRecord> +- slog_buffer_: slog2_buffer_t +- slog_buffer_config_: slog2_buffer_set_config_t +- slog2_instance_: std::unique_ptr<bmw::os::qnx::Slog2> +-- ++ SlogBackend(const std::size_t, + const LogRecord&, + const amp::string_view, + std::unique_ptr<bmw::os::qnx::Slog2>) ++ ReserveSlot(): amp::optional<SlotHandle> ++ FlushSlot(const SlotHandle&): void ++ GetLogRecord(const SlotHandle&): LogRecord& +-- +- Init(verbosity: std::uint8_t) : void + + +bg=white +fontsize=14 + + + + Relation + + 1230 + 410 + 30 + 670 + + lt=<<. + 10.0;650.0;10.0;10.0 + + + UMLClass + + 120 + 710 + 580 + 60 + + mw::log::detail::DataRouterRecorder +-- + + +bg=white +fontsize=14 + + + + Relation + + 120 + 760 + 30 + 540 + + lt=<<. + 10.0;520.0;10.0;10.0 + + + UMLClass + + 190 + 1030 + 510 + 130 + + mw::log::detail::EmptyRecorder +-- ++ StartRecord(const amp::string_view, const LogLevel): + amp::optional<SlotHandle> ++ StopRecord(const SlotHandle&): void ++ IsLogEnabled(const LogLevel&, const amp::string_view): bool + ++ Log(const SlotHandle& , T data) - family of functions + + +bg=white +fontsize=14 + + + + Relation + + 350 + 1150 + 30 + 150 + + lt=<<. + 10.0;130.0;10.0;10.0 + + + UMLNote + + 270 + 1420 + 420 + 80 + + Note.. +bg=blue + +Refer Recorder.h for all the Log(const SlotHandle&, const T data) +- family of functions +layer=1 + + + + UMLClass + + 820 + 900 + 240 + 50 + + mw::log::detail::DataRouterBackend + + +bg=white +fontsize=14 + + + + Relation + + 1030 + 940 + 30 + 140 + + lt=<<. + 10.0;120.0;10.0;10.0 + + + Relation + + 690 + 740 + 150 + 200 + + lt=<<<<<- + 10.0;10.0;130.0;180.0 + + + Relation + + 690 + 220 + 150 + 190 + + lt=<<<<<- + 10.0;10.0;130.0;170.0 + + + Relation + + 690 + 610 + 150 + 30 + + lt=<<<<<- + 10.0;10.0;130.0;10.0 + + + UMLClass + + 140 + 810 + 560 + 180 + + mw::log::detail::CompositeRecorder +-- +- recorders_ : std::vector<std::unique_ptr<Recorder>> +-- ++ CompositeRecorder(std::vector<std::unique_ptr<Recorder>> recorders) ++ StartRecord(const amp::string_view, const LogLevel): amp::optional<SlotHandle> ++ StopRecord(slot:SlotHandle&): void ++ GetRecorders(): std::vector<std::unique_ptr<Recorder>>& ++ IsLogEnabled(const LogLevel&, const amp::string_view): bool + ++ Log(const SlotHandle&, const T) - family of functions + + + + +bg=white +fontsize=14 + + + + UMLClass + + 820 + 980 + 200 + 50 + + mw::log::detail::BackendMock + + +bg=pink +fontsize=14 + + + + Relation + + 890 + 1020 + 30 + 60 + + lt=<<. + 10.0;40.0;10.0;10.0 + + + Relation + + 160 + 980 + 30 + 320 + + lt=<<. + 10.0;300.0;10.0;10.0 + + + UMLNote + + 1000 + 120 + 250 + 80 + + Note.. +bg=blue + +Refer text_format.h for all the Log(...) +- family of functions +layer=1 + + + + Relation + + 690 + 170 + 150 + 320 + + lt=<<<<<- + 10.0;300.0;130.0;10.0 + + + UMLClass + + 820 + 450 + 380 + 110 + + mw::log::detail::DLTFormat +-- + ++ Log(VerbosePayload&, const bool): AddArgumentResult ++ Log(VerbosePayload&, T, const IntegerRepresentation): + AddArgumentResult - family of functions + + +bg=white +fontsize=14 + + + + Relation + + 690 + 510 + 150 + 250 + + lt=<- +uses + 130.0;17.0;10.0;230.0 + + + Relation + + 690 + 330 + 150 + 220 + + lt=<- +uses + 130.0;197.0;10.0;10.0 + + + UMLClass + + 1280 + 580 + 230 + 80 + + template <typename T> + +mw::log::detail::CircularAllocator +-- + + + +bg=white +fontsize=14 + + + + Relation + + 1190 + 610 + 110 + 30 + + lt=<<<<<- + 10.0;10.0;90.0;10.0 + + + UMLClass + + 1280 + 690 + 230 + 50 + + mw::log::detail::LogRecord +-- + + + + +bg=white +fontsize=14 + + + + + Relation + + 1190 + 690 + 110 + 40 + + lt=-> +uses + 10.0;20.0;90.0;20.0 + + diff --git a/mw/log/design/mw_log_shared_memory_reader.uxf b/mw/log/design/mw_log_shared_memory_reader.uxf new file mode 100644 index 0000000..c3acc64 --- /dev/null +++ b/mw/log/design/mw_log_shared_memory_reader.uxf @@ -0,0 +1,293 @@ + + + 10 + + UMLClass + + 680 + 130 + 510 + 110 + + ReaderFactoryImpl + +-- +- mman_: bmw::os::Mman& +- stat_: bmw::os::Stat +-- ++ Create(const std::int32_t, const pid_t): amp::optional<SharedMemoryReader> + + +bg=white +fontsize=14 + + + + UMLClass + + 70 + 130 + 510 + 110 + + <<Interface>> + +ReaderFactory +-- ++ Create(const std::int32_t, const pid_t): amp::optional<SharedMemoryReader> ++ Default(): std::unique_ptr<ReaderFactory> + + +bg=white +fontsize=14 + + + + Relation + + 570 + 160 + 130 + 30 + + lt=<<. + 10.0;10.0;110.0;10.0 + + + Relation + + 920 + 230 + 90 + 130 + + lt=<. +<<creates>> + 10.0;110.0;10.0;10.0 + + + UMLClass + + 680 + 340 + 510 + 430 + + SharedMemoryReader + +-- +- shared_data_: SharedData& +- buffer1_: const amp::span<Byte> +- buffer2_: const amp::span<Byte> +- unmap_callback_: UnmapCallback +- acquired_data_: amp::optional<ReadAcquireResult> +- number_of_acquired_bytes_: Length +- finished_reading_after_detach_: bool +- is_buffer1_expected_to_read_next_: bool +- is_writer_detached_: bool + +- CreateReaderForBuffer(const bool): LinearReader +- ReadAcquiredData(const TypeRegistrationCallback&, + const NewRecordCallback&) +-- ++ SharedMemoryReader(const SharedData&, const amp::span<Byte>, + const amp::span<Byte>, UnmapCallback) ++ Read(const TypeRegistrationCallback&, const NewRecordCallback&): void ++ NotifyAcquisition(const ReadAcquireResult&): void ++ DetachWriter(): void ++ GetNumberOfDropsWithBufferFull(): Length ++ GetNumberOfDropsWithInvalidSize(): Length ++ GetRingBufferSizeBytes(): Length ++ GetNumberOfAcquiredBytes(): Length ++ IsWriterDetached(): bool + + +bg=white +fontsize=14 + + + + UMLClass + + 1280 + 310 + 160 + 100 + + <<Shared Memory>> + +SharedData +-- + + +bg=white +fontsize=14 + + + + Relation + + 1180 + 370 + 120 + 40 + + lt=-> +<< uses >> + 10.0;20.0;100.0;20.0 + + + Relation + + 10 + 780 + 1710 + 30 + + lt=. + 10.0;10.0;1690.0;10.0 + + + UMLClass + + 70 + 950 + 500 + 110 + + DataRouter +-- +- subscriberDatabase_: SubscriberDatabase +- databaseCallback_ : WriteSubscriberDatabaseCallback +-- ++ new_source_session(): SessionPtr ++ new_subscriber_session(): SessionPtr + + + + Relation + + 250 + 230 + 80 + 740 + + lt=-> +<< uses >> + 10.0;720.0;10.0;10.0 + + + UMLClass + + 1280 + 130 + 160 + 40 + + bmw::os::Mman +-- +fontsize=14 +bg=white + + + + UMLClass + + 1280 + 200 + 160 + 40 + + bmw::os::Stat +-- +fontsize=14 +bg=white + + + + Relation + + 1180 + 120 + 120 + 40 + + lt=-> +<< uses >> + 10.0;20.0;100.0;20.0 + + + Relation + + 1180 + 190 + 120 + 40 + + lt=-> +<< uses >> + 10.0;20.0;100.0;20.0 + + + UMLClass + + 40 + 830 + 200 + 50 + + lt=. +*bmw::platform::datarouter* + +bg=white +fontsize=14 + + + + UMLClass + + 30 + 30 + 160 + 50 + + lt=. +*mw::log* + +bg=white +fontsize=14 + + + + UMLClass + + 1280 + 450 + 290 + 110 + + LinearReader +-- +- data_: amp::span<Byte> +- read_index_: Length +-- ++ LinearReader(const amp::span<Byte>&) ++ Read(): amp::optional<amp::span<Byte>> + +bg=white +fontsize=14 + + + + Relation + + 1180 + 460 + 120 + 40 + + lt=-> +<< uses >> + 10.0;20.0;100.0;20.0 + + diff --git a/mw/log/design/non_verbose_logging_static.uxf b/mw/log/design/non_verbose_logging_static.uxf new file mode 100644 index 0000000..5556881 --- /dev/null +++ b/mw/log/design/non_verbose_logging_static.uxf @@ -0,0 +1,357 @@ + + + 10 + + UMLClass + + 20 + 180 + 490 + 300 + + <<Singleton>> + +bmw::platform::logger +-- +- config_: Configuration +- nvconfig_: NvConfig +- shared_memory_writer_: SharedMemoryWriter +- log_entry: log_entry +-- ++ instance(const amp::optional<const bmw::mw::log::detail::Configuration>&, +const amp::optional<const bmw::mw::log::NvConfig>&, +amp::optional<bmw::mw::log::detail::SharedMemoryWriter>) ++ RegisterType(): amp::optional<bmw::mw::log::detail::TypeIdentifier> ++ get_type_level(): LogLevel ++ get_type_threshold: LogLevel ++ get_config(): const Configuration& ++ get_non_verbose_config(): const NvConfig& ++ GetSharedMemoryWriter(): SharedMemoryWriter& ++ InjectTestInstance(logger* const logger_ptr): static void + + + +bg=white +fontsize=14 + + + + UMLClass + + 610 + 250 + 490 + 220 + + log_entry +-- +- default_enabled_: bool +- shared_memory_id_: std::atomic<bmw::mw::log::detail::TypeIdentifier> +-- ++ instance(): static log_entry& ++ RegisterTypeGetId(): amp::optional<bmw::mw::log::detail::TypeIdentifier> ++ TrySerializeIntoSharedMemory(T): void ++ TryWriteIntoSharedMemoryrd(const T&): void ++ log_at_time(timestamp_t, const T&): void ++ log_serialized(const char*, const msgsize_t): void ++ enabled(): bool ++ enabled_at(LogLevel): bool + + +bg=white +fontsize=14 + + + + Relation + + 500 + 270 + 130 + 40 + + lt=<. +<< friend >> + 110.0;17.0;10.0;20.0 + + + UMLClass + + 40 + 60 + 120 + 60 + + Configuration +-- + +bg=white +fontsize=14 + + + + Relation + + 90 + 110 + 60 + 90 + + lt=<<<<- +bg=black +owns + 10.0;70.0;10.0;10.0 + + + UMLClass + + 190 + 20 + 440 + 120 + + NvConfig +-- +- json_path_: const std::string +- typemap_: bool +-- ++ NvConfig(const std::string&) ++ parseFromJson(): ReadResult ++ getDltMsgDesc(const std::string&): const config::NvMsgDescriptor* + +bg=white +fontsize=14 + + + + Relation + + 400 + 130 + 60 + 70 + + lt=<<<<- +bg=black +owns + 10.0;50.0;10.0;10.0 + + + UMLClass + + 610 + 180 + 150 + 60 + + SharedMemoryWriter +-- + +bg=white +fontsize=14 + + + + Relation + + 500 + 190 + 130 + 40 + + lt=<<<<- +bg=black +owns + 10.0;20.0;110.0;20.0 + + + UMLClass + + 580 + 560 + 150 + 60 + + TRACE +-- + +bg=white +fontsize=14 + + + + UMLClass + + 750 + 560 + 150 + 60 + + TRACE_* +-- + +bg=white +fontsize=14 + + + + Relation + + 810 + 450 + 30 + 130 + + lt=<- + 10.0;17.0;10.0;110.0 + + + Relation + + 640 + 450 + 30 + 130 + + lt=<- + 10.0;17.0;10.0;110.0 + + + UMLClass + + 920 + 560 + 150 + 60 + + TRACE_LEVEL +-- + +bg=white +fontsize=14 + + + + Relation + + 990 + 450 + 30 + 130 + + lt=<- + 10.0;17.0;10.0;110.0 + + + UMLClass + + 410 + 560 + 150 + 60 + + LOG_ENTRY +-- + +bg=white +fontsize=14 + + + + Relation + + 480 + 450 + 160 + 130 + + lt=<- + 140.0;17.0;10.0;110.0 + + + UMLNote + + 190 + 530 + 910 + 130 + + Free public functions + + +bg=blue +fontsize=14 + + + + Relation + + 500 + 330 + 130 + 40 + + lt=-> +<< uses >> + 110.0;20.0;10.0;20.0 + + + UMLClass + + 690 + 20 + 220 + 120 + + NvMsgDescriptor +-- ++ id_msg_descriptor_: uint32_t ++ appid_: LoggingIdentifier ++ ctxid_: LoggingIdentifier ++ logLevel_: uint8_t + +bg=white +fontsize=14 + + + + Relation + + 620 + 40 + 90 + 40 + + lt=-> +<< uses >> + 10.0;20.0;70.0;20.0 + + + UMLClass + + 950 + 20 + 150 + 60 + + LoggingIdentifier +-- + +bg=white +fontsize=14 + + + + Relation + + 900 + 20 + 70 + 40 + + lt=<<<<- +bg=black + + 10.0;20.0;50.0;20.0 + + diff --git a/mw/log/design/shared_memory_reader_read.uxf b/mw/log/design/shared_memory_reader_read.uxf new file mode 100644 index 0000000..f9f8c06 --- /dev/null +++ b/mw/log/design/shared_memory_reader_read.uxf @@ -0,0 +1,73 @@ +9Space for diagram notesUMLSpecialState144541818type=initialRelation144632763lt=<-10;50;10;10UMLSpecialState3610823490Finished reading after detach? +bg=red +type=decisionRelation1719003654lt=<- +No10;40;10;10Relation162144171252lt=<- +m2=[Yes]10;260;10;220;170;220;170;10;120;10Relation477632763lt=<-10;50;10;10Relation1442522754lt=<-10;40;10;10Relation1443152781lt=<-10;70;10;10UMLSpecialState36910823490Is Data acquired? +bg=red +type=decisionRelation4771895454lt=<- +[Yes]10;40;10;10Relation4772522754lt=<-10;40;10;10Relation4773152754lt=<-10;40;10;10Text4321810836ReadAcquiredData +style=wordwrap +halign=center +style=autoresizeUMLState3622523436ReadAquiredDataUMLSpecialState477541818type=initialText721817136SharedMemoryReader::Read +style=wordwrap +halign=center +style=autoresizeUMLState36922522536CreateReaderForBufferUMLState36928822536ReadLinearBufferUMLState36935122554Set flag to indicate if buffer1 is expected to read next is already acquired or not based on acquired data +style=wordwrapUMLState36942322536Reset acquired dataRelation4773962745lt=<-10;30;10;10Relation4774502754lt=<-10;40;10;10Relation495144171360lt=<- +m2=[No]10;380;10;360;170;360;170;10;120;10Text7561813536CreateReaderForBuffer +style=wordwrap +halign=center +style=autoresizeUMLSpecialState810541818type=initialRelation810632745lt=<-10;30;10;10UMLState68420718036control_block=control_block_1 +style=wordwrapUMLState69327924336Choose the relevant bufferUMLState69334224336length = written index to respective control block +style=wordwrapUMLState69346824336CreateLinearReaderFromDataAndLengthRelation8104952745lt=<-10;30;10;10Relation8103062754lt=<-10;40;10;10Relation8103692754lt=<-10;40;10;10Relation8102342763lt=<-10;50;10;10UMLSpecialState8105221818type=finalUMLState3628823436ReadRemainingDataIfWriterIsDetachedUMLState69340524336Number of acquired bytes = lengthRelation8104322754lt=<-10;40;10;10Text12655822536ReadLinearBuffer +style=wordwrapUMLObject001089549valign=topUMLObject054910891152valign=topUMLSpecialState1715941818type=initialRelation1716032781lt=<-10;70;10;10UMLSpecialState6381923490Length of Read result < +buffer entry header size +bg=red +type=decisionRelation2888466336lt=<- +[Yes]50;20;10;20UMLState6393622536Initialize header span +from Read resultUMLSpecialState63112523490Header type identifier +== register type token +bg=red +type=decisionRelation1719632754lt=<-10;40;10;10Relation1441894554lt=<- +[No]10;40;10;10Relation189630189225lt=<-10;40;10;10;190;10;190;230Relation288116120799lt=<- +m2=[No]210;90;210;10;10;10UMLState360124222536Init shared memory record with +existing header and payload spanUMLState6399922536Copy header span to buffer entry +headerUMLState63106222536Initialize payload span +from Read resultRelation17110262754lt=<-10;40;10;10Relation17110892754lt=<-10;40;10;10UMLState360130522536Trigger new message callback +with shared memory recordRelation46812692754lt=<-10;40;10;10Relation351882279549lt=<-10;10;10;290;290;290;290;590;170;590Relation46813322772lt=<-10;60;10;10Relation17112065454lt=<- +[Yes]10;40;10;10UMLSpecialState63124223490Payload span length < +type registration ID +bg=red +type=decisionRelation2881278180153lt=<- +[Yes]180;150;50;150;50;10;10;10UMLState72135922545Copy memory from payload +span to type registration IDRelation17113234554lt=<- +[No]10;40;10;10UMLState72142222545Type ID size = size of type +registration ID +UMLState72148522545Type registration data = subspan of +type ID size from payload spanRelation17113952745lt=<-10;30;10;10Relation17114582745lt=<-10;30;10;10Relation1711431324198lt=<-340;10;340;200;10;200;10;170UMLState72154822536Trigger type registration callback with +created type registrationRelation17115212745lt=<-10;30;10;10UMLSpecialState6369323499Read successfull? +bg=red +type=decisionRelation1717835454lt=<- +[Yes]10;40;10;10UMLSpecialState17116561818type=finalRelation9738189936lt=<- +m2=[No]190;1020;190;990;10;990;10;10;60;10Text61255822536ReadRemainingDataIfWriterIsDetached +style=wordwrapUMLSpecialState7025941818type=initialRelation7026032754lt=<-10;40;10;10UMLSpecialState59463923490Is writer detached? +bg=red +type=decisionRelation7027205454lt=<- +[Yes]10;40;10;10Relation720675171306lt=<- +m2=[No]10;320;10;300;170;300;170;10;120;10UMLState60381922536ReadLinearBufferUMLState60375622536Reader = CreateReaderForBufferRelation7027832754lt=<-10;40;10;10UMLState60388222536Finished reading after detach = trueRelation7028462754lt=<-10;40;10;10Relation7029092772lt=<-10;60;10;10UMLSpecialState7029901818type=finalUMLSpecialState1444051818type=finalRelation1443692754lt=->10;10;10;40UMLSyncBarHorizontal1173697218lw=5 +Text813965436Finish +halign=center +style=autoresizeUMLSpecialState4775221818type=finalUMLSyncBarHorizontal4504777218lw=5 +Relation4774772763lt=->10;10;10;50UMLSpecialState7029023490Check flag that indicates what +buffer1 has acquired +style=wordwrap +bg=red +type=decisionUMLState88220718036control_block=control_block_2 +style=wordwrapRelation81017114454lt=<- +[Not Acquired Buffer1]10;40;10;10Relation92712613599lt=<- +m2=[Acquired Buffer1]70;90;70;10;10;10Relation8912342763lt=<-10;50;10;10UMLSyncBarHorizontal6759547218lw=5 +Relation7029542754lt=->10;10;10;40UMLSpecialState3338375454merge +type=decision +layer=1UMLSpecialState45013865454merge +type=decision +layer=1Relation1716572754lt=<-10;40;10;10UMLSyncBarHorizontal1446577218lw=5 + \ No newline at end of file diff --git a/mw/log/design/shared_memory_writer_allocandwrite.uxf b/mw/log/design/shared_memory_writer_allocandwrite.uxf new file mode 100644 index 0000000..26bf814 --- /dev/null +++ b/mw/log/design/shared_memory_writer_allocandwrite.uxf @@ -0,0 +1,17 @@ +10Space for diagram notesUMLSpecialState200402020type=initialUMLSpecialState8090260100Payload size > max payload size +bg=red +type=decisionRelation2001805070lt=<- +[No]10;50;10;10UMLState10023022040Acquire dataUMLState10043022040Write headerRelation200503060lt=<-10;40;10;10UMLState10050022040access memoryUMLState10057022040Write payloadUMLState10064022040Release acquired dataRelation2004603060lt=<-10;40;10;10Relation2005303060lt=<-10;40;10;10Relation2006003060lt=<-10;40;10;10Relation2006703080lt=<-10;60;10;10UMLSpecialState2007602020type=finalRelation220370450380lt=<-10;360;10;330;430;330;430;10UMLObject500660790SharedMemoryWriter::AllocAndWrite +valign=topUMLSpecialState80300260100Is data aquired? +bg=red +type=decisionRelation2002603060lt=<-10;40;10;10UMLState37012022040Increment number of drops counter due to invalid size +style=wordwrapUMLState37033022040Increment number of drops counter due to buffer full +style=wordwrapRelation5803406030lt=<-40;10;10;10Relation3301206040lt=<- +[Yes]40;20;10;20Relation3303306040lt=<- +[No]40;20;10;20Relation2003906060lt=<- +[Yes]10;40;10;10UMLSpecialState6203206060merge +type=decision +layer=1Relation58013090210lt=<-70;190;70;10;10;10Relation2007203060lt=->10;10;10;40UMLSyncBarHorizontal1707208020lw=5 +Text1407506040Finish +halign=center +style=autoresize \ No newline at end of file diff --git a/mw/log/design/slot_drainer_action_diagram_design.uxf b/mw/log/design/slot_drainer_action_diagram_design.uxf new file mode 100755 index 0000000..6933131 --- /dev/null +++ b/mw/log/design/slot_drainer_action_diagram_design.uxf @@ -0,0 +1,65 @@ + + + 8 + + com.baselet.element.old.allinone.ActivityDiagramText + + 520 + 48 + 558 + 814 + + title:Slot disposal diagram +bg=orange +Start +Flush slot\ call +circular_buffer.push(LogRecord&) + +While[circular_buffer not empty] + get_next_slot + While[next span available] + get_next_span + write_file + If + [would block]->exit + + [all data written] + Load number of buffers flushed + If + [limit reached]->exit + + [within limit] +exit~exit +End + + + + + + UMLPackage + + 528 + 128 + 536 + 728 + + SlotDrainer +-- +bg=orange + + + + UMLPackage + + 704 + 256 + 352 + 320 + + NonBlockingWriter +-- +bg=#3c7a00 +layer=2 + + + diff --git a/mw/log/design/slot_drainer_sequence_design.uxf b/mw/log/design/slot_drainer_sequence_design.uxf new file mode 100755 index 0000000..6d88695 --- /dev/null +++ b/mw/log/design/slot_drainer_sequence_design.uxf @@ -0,0 +1,434 @@ + + + 10 + + UMLGeneric + + 530 + 90 + 140 + 30 + + _:SlotDrainer_ + + + + Relation + + 580 + 110 + 30 + 110 + + lt=. + 10.0;10.0;10.0;90.0 + + + UMLGeneric + + 740 + 90 + 130 + 30 + + _:NonBlockingWriter_ + + + + Relation + + 340 + 180 + 260 + 50 + + lt=<- +flush(ctx_id, log_level) + + 240.0;20.0;10.0;20.0 + + + Relation + + 590 + 190 + 410 + 40 + + lt=<- +push() + 390.0;20.0;10.0;20.0 + + + Relation + + 800 + 110 + 30 + 690 + + lt=. + 10.0;10.0;10.0;670.0 + + + UMLGeneric + + 930 + 90 + 100 + 30 + + _:CircularBuffer_ + + + + Relation + + 970 + 110 + 30 + 650 + + lt=. + 10.0;10.0;10.0;630.0 + + + UMLGeneric + + 1050 + 90 + 130 + 30 + + _:MessageBuilder_ + + + + Relation + + 1090 + 110 + 30 + 660 + + lt=. + 10.0;10.0;10.0;640.0 + + + Relation + + 340 + 690 + 260 + 30 + + lt=<- + 10.0;10.0;240.0;10.0 + + + UMLGeneric + + 270 + 90 + 160 + 30 + + _:Backend_ + + + + Relation + + 340 + 110 + 30 + 660 + + lt=. + 10.0;10.0;10.0;640.0 + + + Relation + + 590 + 290 + 240 + 40 + + lt=<- +flush() + 220.0;20.0;10.0;20.0 + + + Relation + + 590 + 230 + 720 + 40 + + lt=<- +GetUnderlyingbufferFor(slot) + 700.0;20.0;10.0;20.0 + + + Relation + + 590 + 210 + 410 + 40 + + lt=<- +pop() + 390.0;20.0;10.0;20.0 + + + Relation + + 590 + 250 + 530 + 40 + + lt=<- +SetNextMessage(log_record) + 510.0;20.0;10.0;20.0 + + + UMLGeneric + + 1200 + 90 + 180 + 30 + + _:SlotAllocator<LogRecord>_ + + + + Relation + + 1280 + 110 + 30 + 720 + + lt=. + 10.0;10.0;10.0;700.0 + + + Relation + + 590 + 270 + 530 + 40 + + lt=<- +GetNextSpan(log_record) + 510.0;20.0;10.0;20.0 + + + Relation + + 590 + 330 + 240 + 40 + + lt=<- +flush() + 220.0;20.0;10.0;20.0 + + + Relation + + 590 + 310 + 530 + 40 + + lt=<- +GetNextSpan(log_record) + 510.0;20.0;10.0;20.0 + + + Relation + + 580 + 700 + 30 + 110 + + lt=. + 10.0;10.0;10.0;90.0 + + + Relation + + 590 + 520 + 240 + 40 + + lt=<- +flush() + 220.0;20.0;10.0;20.0 + + + Relation + + 590 + 460 + 720 + 40 + + lt=<- +GetUnderlyingbufferFor(slot) + 700.0;20.0;10.0;20.0 + + + Relation + + 590 + 440 + 410 + 40 + + lt=<- +pop() + 390.0;20.0;10.0;20.0 + + + Relation + + 590 + 480 + 530 + 40 + + lt=<- +SetNextMessage(log_record) + 510.0;20.0;10.0;20.0 + + + Relation + + 590 + 500 + 530 + 40 + + lt=<- +GetNextSpan(log_record) + 510.0;20.0;10.0;20.0 + + + Relation + + 590 + 560 + 240 + 40 + + lt=<- +flush() + 220.0;20.0;10.0;20.0 + + + Relation + + 590 + 540 + 530 + 40 + + lt=<- +GetNextSpan(log_record) + 510.0;20.0;10.0;20.0 + + + UMLGeneric + + 580 + 440 + 20 + 270 + + + + + + UMLGeneric + + 580 + 200 + 20 + 160 + + + + + + Relation + + 580 + 350 + 30 + 110 + + lt=. + 10.0;10.0;10.0;90.0 + + + UMLPackage + + 450 + 140 + 860 + 230 + + First Slot +-- +bg=orange + + + + UMLPackage + + 450 + 410 + 860 + 230 + + Last Slot +-- +bg=orange + + + + Relation + + 590 + 660 + 410 + 40 + + lt=<- +pop() + 390.0;20.0;10.0;20.0 + + + Relation + + 590 + 680 + 410 + 40 + + lt=<- +<empty> + 10.0;20.0;390.0;20.0 + + diff --git a/mw/log/design/verbose_logging_ara_log_interaction.uxf b/mw/log/design/verbose_logging_ara_log_interaction.uxf new file mode 100644 index 0000000..597dcfb --- /dev/null +++ b/mw/log/design/verbose_logging_ara_log_interaction.uxf @@ -0,0 +1,49 @@ + + 10 + + UMLClass + + 790 + 260 + 420 + 160 + + Free Functions +-- +bmw::mw::LogError(): bmw::mw::LogStream +bmw::mw::LogWarn(): bmw::mw::LogStream +bmw::mw::LogInfo(): bmw::mw::LogStream +bmw::mw::LogDebug(): bmw::mw::LogStream + + + + + UMLClass + + 220 + 260 + 420 + 160 + + Free Functions +-- +ara::log::LogError(): ara::log::LogStream +ara::log::LogWarn(): ara::log::LogStream +ara::log::LogInfo(): ara::log::LogStream +ara::log::LogDebug(): ara::log::LogStream + + + + + Relation + + 630 + 320 + 180 + 40 + + lt=<- +facade + 160.0;20.0;10.0;20.0 + + diff --git a/mw/log/design/verbose_logging_sequence.uxf b/mw/log/design/verbose_logging_sequence.uxf new file mode 100644 index 0000000..e148a3e --- /dev/null +++ b/mw/log/design/verbose_logging_sequence.uxf @@ -0,0 +1,819 @@ + + + 8 + + UMLGeneric + + 8 + 48 + 72 + 24 + + _:App_ + + + + Relation + + 32 + 64 + 24 + 32 + + lt=. + 10.0;10.0;10.0;20.0 + + + UMLGeneric + + 168 + 48 + 128 + 24 + + FreeFunctions + + + + Relation + + 224 + 64 + 24 + 656 + + lt=. + 10.0;10.0;10.0;800.0 + + + Relation + + 40 + 72 + 208 + 32 + + lt=<<- +bmw::mw::log::Error() + 240.0;20.0;10.0;20.0 + + + UMLGeneric + + 312 + 48 + 128 + 24 + + LogStreamFactory + + + + Relation + + 224 + 112 + 160 + 32 + + lt=<<- +GetStream(LogLevel) + 180.0;20.0;10.0;20.0 + + + UMLGeneric + + 368 + 112 + 16 + 280 + + + + + + UMLGeneric + + 584 + 48 + 128 + 24 + + _:LogStream_ + + + + Relation + + 368 + 64 + 24 + 64 + + lt=. + 10.0;10.0;10.0;60.0 + + + Relation + + 376 + 264 + 288 + 32 + + lt=<- +construct + 340.0;20.0;10.0;20.0 + + + UMLGeneric + + 640 + 296 + 16 + 96 + + + + + + Relation + + 640 + 64 + 24 + 248 + + lt=. + 10.0;10.0;10.0;290.0 + + + UMLGeneric + + 480 + 48 + 72 + 24 + + Runtime + + + + + Relation + + 504 + 64 + 24 + 656 + + lt=. + 10.0;10.0;10.0;800.0 + + + Relation + + 376 + 128 + 152 + 32 + + lt=<<- +GetRecorder() + 170.0;20.0;10.0;20.0 + + + Relation + + 376 + 152 + 152 + 32 + + lt=<- +Recorder + 10.0;20.0;170.0;20.0 + + + UMLGeneric + + 776 + 48 + 128 + 24 + + _:DataRouterRecorder_ + + + + + UMLGeneric + + 832 + 296 + 16 + 288 + + + + + + Relation + + 504 + 136 + 352 + 32 + + lt=<- +creates + 420.0;20.0;10.0;20.0 + + + Relation + + 832 + 64 + 24 + 248 + + lt=. + 10.0;10.0;10.0;290.0 + + + Relation + + 648 + 288 + 200 + 32 + + lt=<<- +StartRecord(ctx, LogLevel) + 230.0;20.0;10.0;20.0 + + + Relation + + 840 + 296 + 360 + 32 + + lt=<<- +ReserveSlot() + 430.0;20.0;10.0;20.0 + + + Relation + + 648 + 352 + 200 + 32 + + lt=<- +amp::optional<SlotHandle> + 10.0;20.0;230.0;20.0 + + + Relation + + 376 + 368 + 280 + 32 + + lt=<- +LogStream + 10.0;20.0;330.0;20.0 + + + Relation + + 224 + 368 + 160 + 32 + + lt=<- +LogStream + 10.0;20.0;180.0;20.0 + + + UMLGeneric + + 32 + 80 + 16 + 608 + + + + + + Relation + + 40 + 368 + 208 + 32 + + lt=<- +LogStream + 10.0;20.0;240.0;20.0 + + + Relation + + 40 + 400 + 616 + 32 + + lt=<<- +<< Some Data + 750.0;20.0;10.0;20.0 + + + Relation + + 40 + 560 + 616 + 32 + + lt=<- +LogStream + 10.0;20.0;750.0;20.0 + + + Relation + + 40 + 608 + 616 + 32 + + lt=<<- +Destruct + 750.0;20.0;10.0;20.0 + + + Relation + + 648 + 616 + 200 + 32 + + lt=<<- +StopRecord(SlotHandle) + 230.0;20.0;10.0;20.0 + + + Relation + + 1016 + 512 + 160 + 56 + + lt=<<- +write formated data +into stream + 10.0;50.0;60.0;50.0;60.0;10.0;10.0;10.0 + + + UMLGeneric + + 1304 + 600 + 128 + 24 + + TRACE + + + + + Relation + + 1360 + 616 + 24 + 80 + + lt=. + 10.0;10.0;10.0;80.0 + + + Relation + + 1192 + 632 + 192 + 32 + + lt=<<- +TRACE(LogEntry) + 220.0;20.0;10.0;20.0 + + + Relation + + 648 + 664 + 200 + 24 + + lt=<- + 10.0;10.0;230.0;10.0 + + + UMLGeneric + + 640 + 408 + 16 + 184 + + + + + + UMLGeneric + + 640 + 616 + 16 + 72 + + + + + + Relation + + 640 + 384 + 24 + 40 + + lt=. + 10.0;10.0;10.0;30.0 + + + Relation + + 640 + 680 + 24 + 40 + + lt=. + 10.0;10.0;10.0;30.0 + + + UMLSpecialState + + 640 + 696 + 16 + 16 + + type=termination + + + + Relation + + 32 + 680 + 24 + 32 + + lt=. + 10.0;10.0;10.0;20.0 + + + Relation + + 40 + 680 + 616 + 24 + + lt=<- + 10.0;10.0;750.0;10.0 + + + UMLNote + + 1248 + 688 + 264 + 72 + + bg=red +This part will be removed in future, +once we can directly stream into MrWriter +this will then remove any +dynamic memory allocation. + + + + UMLGeneric + + 952 + 48 + 128 + 24 + + DLTFormater + + + + + Relation + + 648 + 400 + 200 + 32 + + lt=<<- +log(SlotHandle, Data) + 230.0;20.0;10.0;20.0 + + + Relation + + 648 + 560 + 200 + 24 + + lt=<- + 10.0;10.0;230.0;10.0 + + + UMLGeneric + + 1128 + 48 + 128 + 24 + + _:DataRouterBackend_ + + + + + Relation + + 1184 + 64 + 24 + 248 + + lt=. + 10.0;10.0;10.0;290.0 + + + UMLGeneric + + 1184 + 296 + 16 + 48 + + + + + + Relation + + 840 + 320 + 360 + 32 + + lt=<- +amp::optional<SlotHandle> + 10.0;20.0;430.0;20.0 + + + Relation + + 1184 + 336 + 24 + 96 + + lt=. + 10.0;10.0;10.0;100.0 + + + UMLGeneric + + 1184 + 416 + 16 + 48 + + + + + + Relation + + 840 + 408 + 360 + 32 + + lt=<<- +GetStream(SlotHandle) + 430.0;20.0;10.0;20.0 + + + Relation + + 840 + 432 + 360 + 32 + + lt=<- +std::streambuf& + 10.0;20.0;430.0;20.0 + + + Relation + + 1008 + 64 + 24 + 448 + + lt=. + 10.0;10.0;10.0;540.0 + + + Relation + + 840 + 488 + 184 + 32 + + lt=<<- +Log(std::streambuf&, data) + 210.0;20.0;10.0;20.0 + + + UMLGeneric + + 1008 + 496 + 16 + 72 + + + + + + Relation + + 840 + 552 + 184 + 24 + + lt=<- + 10.0;10.0;210.0;10.0 + + + Relation + + 1184 + 456 + 24 + 184 + + lt=. + 10.0;10.0;10.0;210.0 + + + UMLGeneric + + 1184 + 624 + 16 + 48 + + + + + + Relation + + 840 + 624 + 360 + 32 + + lt=<<- +FlushSlot(SlotHandle) + 430.0;20.0;10.0;20.0 + + + Relation + + 840 + 656 + 360 + 24 + + lt=<- + 10.0;10.0;430.0;10.0 + + + Relation + + 832 + 672 + 24 + 40 + + lt=. + 10.0;10.0;10.0;30.0 + + + Relation + + 1008 + 560 + 24 + 144 + + lt=. + 10.0;10.0;10.0;160.0 + + + Relation + + 1184 + 664 + 24 + 40 + + lt=. + 10.0;10.0;10.0;30.0 + + + Relation + + 368 + 384 + 24 + 336 + + lt=. + 10.0;10.0;10.0;400.0 + + + Relation + + 640 + 584 + 24 + 48 + + lt=. + 10.0;10.0;10.0;40.0 + + + UMLGeneric + + 832 + 624 + 16 + 56 + + + + + + Relation + + 832 + 576 + 24 + 64 + + lt=. + 10.0;10.0;10.0;60.0 + + diff --git a/mw/log/design/verbose_logging_static.uxf b/mw/log/design/verbose_logging_static.uxf new file mode 100644 index 0000000..ee5fa3f --- /dev/null +++ b/mw/log/design/verbose_logging_static.uxf @@ -0,0 +1,1037 @@ + + + 10 + + UMLClass + + 150 + 1290 + 360 + 260 + + bmw::mw::Logger +-- +- context_: LoggingIdentifier +-- ++ Logger(amp::string_view ctxId) ++ LogFatal(): LogStream ++ LogError(): LogStream ++ LogWarn(): LogStream ++ LogInfo(): LogStream ++ LogDebug(): LogStream ++ LogVerbose(): LogStream ++ WithLevel(const LogLevel): log::LogStream ++ IsLogEnabled(const LogLevel): bool ++ IsEnabled(const LogLevel): bool ++ GetContext(): amp::string_view + + + +bg=white +fontsize=14 + + + + UMLClass + + 540 + 1290 + 460 + 260 + + Free Functions +-- ++ bmw::mw::LogFatal(): LogStream ++ bmw::mw::LogError(): LogStream ++ bmw::mw::LogWarn(): LogStream ++ bmw::mw::LogInfo(): LogStream ++ bmw::mw::LogDebug(): LogStream ++ bmw::mw::LogVerbose(): LogStream ++ bmw::mw::LogFatal(amp::string_view): LogStream ++ bmw::mw::LogError(amp::string_view): LogStream ++ bmw::mw::LogWarn(amp::string_view): LogStream ++ bmw::mw::LogInfo(amp::string_view): LogStream ++ bmw::mw::LogDebug(amp::string_view): LogStream ++ bmw::mw::LogVerbose(amp::string_view): LogStream ++ bmw::mw::GetDefaultLogRecorder(): Recorder& ++ bmw::mw::test::SetLogRecorder(bmw::mw::log::Recorder* const): void + + +bg=white +fontsize=14 + + + + UMLClass + + 310 + 1110 + 630 + 70 + + bmw::mw::log::detail::LogStreamFactory +-- +__+ GetStream(LogLevel, ctxId: amp::string_view = "DFLT"): LogStream__ + + +bg=white +fontsize=14 + + + + Relation + + 370 + 1170 + 60 + 140 + + lt=<- +uses + 10.0;10.0;10.0;120.0 + + + UMLClass + + 1750 + 1220 + 150 + 30 + + TRACE_MACRO + +bg=white +fontsize=14 + + + + + + Relation + + 1790 + 1040 + 100 + 200 + + lt=<- +<< on sync >> + 10.0;180.0;10.0;10.0 + + + UMLNote + + 710 + 0 + 800 + 80 + + bg=white +fontsize=14 + +halign=center +This diagram only illustrates the use-case of verbose logging. + + + + + UMLClass + + 270 + 890 + 580 + 170 + + bmw::mw::LogStream +-- +- recorder_: Recorder& +- slot_handle_: amp::optional<SlotHandle> +- context_id_: detail::LoggingIdentifier +- log_level_: LogLevel +- LogStream(Recorder&, const LogLevel, const amp::string_view) +-- ++ LogStream(LogStream&&) ++ Flush(): void + + + + +bg=white +fontsize=14 + + + + Relation + + 520 + 1050 + 70 + 80 + + lt=<- +create + 10.0;10.0;10.0;60.0 + + + UMLClass + + 1560 + 1310 + 220 + 140 + + LogEntry +-- +-- ++ appId: dltid_t ++ ctxId: dltid_t ++ sessionId: dltid_t ++ logLevel: uint8_t ++ num_of_args: int8_t ++ payload: std::string + + + +bg=white +fontsize=14 + + + + + UMLClass + + 560 + 610 + 400 + 210 + + bmw::mw::log::detail::Runtime + +-- +- Runtime(Recorder* const recorder) +- static Instance(Recorder* const): Runtime& + +- logger_container_instance_: LoggerContainer +- recorder_ins tance_: Recorder +- default_recorder_: std::unique_ptr<Recorder> +-- +// here check for Testing instance +__+ GetRecorder(): Recorder*__ +__+ SetRecorder(Recorder*): void__ +__+ GetLoggerContainer(): bmw::mw::log::LoggerContainer&__ + + +bg=white +fontsize=14 + + + + + UMLClass + + 1500 + 930 + 400 + 120 + + bmw::mw::log::detail::DataRouterBackend +-- ++ ReserveSlot(): amp::optional<SlotHandle> ++ FlushSlot(const SlotHandle&): void ++ GetLogRecord(const SlotHandle&): LogRecord& +-- +buffer_: CircularAllocator<bmw::mw::log::detail::LogRecord> +message_client_: std::unique_ptr<DatarouterMessageClient> + + + +bg=white +fontsize=14 + + + + + Relation + + 1750 + 740 + 240 + 210 + + lt=<<. + 220.0;10.0;220.0;160.0;10.0;160.0;10.0;190.0 + + + UMLClass + + 1810 + 640 + 350 + 110 + + << interface >> + +bmw::mw::log::detail::Backend +-- +/+ ReserveSlot(): amp::optional<SlotHandle>/ +/+ FlushSlot(const SlotHandle&)/ +/+ GetLogRecord(const SlotHandle&): LogRecord&/ + +bg=white +fontsize=14 + + + + UMLNote + + 1710 + 1440 + 270 + 50 + + bg=red +In future this will be removed +and we will stream into MwsrWriter + + + + + + UMLClass + + 320 + 330 + 240 + 150 + + lt=. +bmw::mw::LogLevel +-- +kOff = 0x00, +kFatal = 0x01, +kError = 0x02, +kWarn = 0x03, +kInfo = 0x04, +kDebug = 0x05, +kVerbose = 0x06 + + +bg=white +fontsize=14 + + + + UMLClass + + 1090 + 970 + 120 + 30 + + Configuration + +bg=white +fontsize=14 + + + + Relation + + 880 + 810 + 100 + 320 + + lt=<- +get current +recorder + 10.0;10.0;10.0;300.0 + + + Relation + + 1130 + 850 + 120 + 140 + + lt=<<<<- +bg=black +get application +specific config + 10.0;10.0;10.0;120.0 + + + Relation + + 780 + 1170 + 60 + 140 + + lt=<- +uses + 10.0;10.0;10.0;120.0 + + + UMLClass + + 1030 + 140 + 20 + 1460 + + lw=2 +bg=black + + + + UMLNote + + 1800 + 1240 + 270 + 50 + + bg=red +In future this will be removed +and we will stream into MwsrWriter + + + + + + Relation + + 940 + 810 + 30 + 500 + + lt=<- + + 10.0;10.0;10.0;480.0 + + + UMLNote + + 0 + 140 + 1010 + 30 + + Not Mockable Part + +halign=center +bg=white +fontsize=14 + + + + UMLNote + + 1070 + 140 + 1200 + 30 + + bg=white +fontsize=14 +halign=center +Mockable Part + + + + UMLClass + + 120 + 1210 + 900 + 20 + + lw=2 +bg=black + + + + UMLNote + + 140 + 1570 + 870 + 30 + + halign=center +User-Facing API + + +bg=white +fontsize=14 + + + + UMLClass + + 1090 + 640 + 610 + 150 + + << interface >> + +bmw::mw::log::Recorder +-- ++ Recorder() +/+ StartRecord(context_id: amp::string_view, LogLevel): amp::optional<SlotHandle>/ +/+ StopRecord(SlotHandle const&): void/ +/+ Log(SlotHandle, std::uint8_t): void/ +/+ Log(SlotHandle, std::int8_t): void/ + + + +bg=white +fontsize=14 + + + + UMLClass + + 1120 + 830 + 150 + 30 + + DataRouterRecorder + +bg=white +fontsize=14 + + + + UMLClass + + 1280 + 830 + 180 + 30 + + TextRecorder + +bg=white +fontsize=14 + + + + UMLNote + + 1150 + 1260 + 270 + 50 + + In future this will be removed +and we will stream into MwsrWriter + +bg=red +fontsize=14 + + + + + + Relation + + 1190 + 780 + 110 + 70 + + lt=<<. + 90.0;10.0;90.0;30.0;10.0;30.0;10.0;50.0 + + + Relation + + 1270 + 780 + 100 + 70 + + lt=<<. + 10.0;10.0;10.0;30.0;80.0;30.0;80.0;50.0 + + + Relation + + 950 + 790 + 240 + 60 + + lt=<<<<- +bg=black +owns statically + 10.0;20.0;220.0;20.0;220.0;40.0 + + + Relation + + 840 + 640 + 270 + 290 + + lt=<<<<- + + + + + +records data + 10.0;270.0;150.0;270.0;150.0;10.0;250.0;10.0 + + + UMLClass + + 1470 + 830 + 180 + 30 + + FileRecorder + +bg=white +fontsize=14 + + + + Relation + + 1270 + 780 + 310 + 70 + + lt=<<. + 10.0;10.0;10.0;30.0;290.0;30.0;290.0;50.0 + + + UMLNote + + 1090 + 1020 + 340 + 100 + + For the time beeing, +DataRouterRecorder will not only be a facade +but contain DLT specific logic. +In this case calling +StartRecord() and StopRecord() + +bg=red +fontsize=14 + + + + + UMLClass + + 1370 + 1130 + 360 + 140 + + bmw::mw::log::detail::LogRecord +-- +- logEntry_: LogEntry +- verbosePayload_: VerbosePayload +-- ++ getLogEntry(): LogEntry& ++ getLogEntry: const LogEntry& ++ getVerbosePayload(): detail::VerbosePayload& ++ getVerbosePayload(): const detail::VerbosePayload& + + + +bg=white +fontsize=14 + + + + UMLClass + + 20 + 540 + 520 + 280 + + bmw::mw::log::SlotHandle +-- +- recorder_to_slot_: std::array<SlotIndex, kMaxRecorders> +- recorder_slot_available_: std::bitset<kMaxRecorders> +- selected_recorder_: RecorderIdentifier +-- ++ SlotHandle() ++ SlotHandle(const SlotIndex) ++ GetSlotOfSelectedRecorder(): SlotIndex ++ GetSlot(const RecorderIdentifier): SlotIndex ++ SetSlot(const SlotIndex, const RecorderIdentifier): void ++ GetSelectedRecorder(): RecorderIdentifier ++ SetSelectedRecorder(const RecorderIdentifier): void ++ IsRecorderActive(const RecorderIdentifier): bool ++ friend operator==(const SlotHandle& l_value, const SlotHandle& r_value): bool ++ friend operator!=(const SlotHandle& l_value, const SlotHandle& r_value): bool ++ kMaxRecorders: static constexpr std::size_t + + + + + + + + + +bg=white +fontsize=14 + + + + Relation + + 1580 + 1040 + 150 + 110 + + lt=<<<<- +bg=black +<< creates and fills >> + + + + 10.0;10.0;10.0;90.0 + + + UMLNote + + 1090 + 550 + 310 + 70 + + A user is allowed to use the Recorder interface +for its abstraction. But its not recommended due +to its more complicated usage. + +bg=blue +fontsize=14 + + + + + + Relation + + 290 + 810 + 60 + 100 + + lt=<<<<- +bg=black +owns + 10.0;80.0;10.0;10.0 + + + Relation + + 1270 + 780 + 470 + 70 + + lt=<<. + 10.0;10.0;10.0;30.0;450.0;30.0;450.0;50.0 + + + UMLClass + + 1670 + 830 + 120 + 30 + + EmptyRecorder + +bg=white +fontsize=14 + + + + UMLClass + + 1090 + 1320 + 420 + 190 + + bmw::mw::log::detail::VerbosePayload +-- +- buffer_: std::reference_wrapper<ByteVector> +-- ++ VerbosePayload(std::size_t, std::string&) ++ Put(const Byte*, const std::size_t):void ++ Put(const ReserveCallback, const std::size_t):std::size_t ++ GetSpan(): amp::span<const std::uint8_t> ++ Reset(): void ++ WillOverflow(const std::size_t): bool ++ RemainingCapacity(): std::size_t ++ SetBuffer(ByteVector&): void + + + + +bg=white +fontsize=14 + + + + UMLClass + + 1550 + 500 + 360 + 60 + + TextFormat +-- + +bg=white +fontsize=14 + + + + Relation + + 1510 + 550 + 70 + 110 + + lt=<. +Log + 40.0;10.0;10.0;90.0 + + + Relation + + 1240 + 850 + 480 + 100 + + lt=<<<<- +<< reserve and flush slots >> + + +bg=black + 10.0;10.0;10.0;50.0;460.0;50.0;460.0;80.0 + + + UMLClass + + 1290 + 970 + 140 + 30 + + StatisticsReporter + +bg=white +fontsize=14 + + + + Relation + + 1180 + 850 + 200 + 140 + + lt=<<<<- + + +bg=black + 10.0;10.0;10.0;100.0;180.0;100.0;180.0;120.0 + + + Relation + + 1640 + 1260 + 150 + 70 + + lt=<<<<- +bg=black +<< creates and fills >> + 10.0;10.0;10.0;50.0 + + + Relation + + 1430 + 1260 + 150 + 80 + + lt=<<<<- +bg=black +<< creates and fills >> + 10.0;10.0;10.0;60.0 + + + UMLClass + + 1500 + 200 + 470 + 220 + + template <typename Element> +bmw::mw::log::detail::WaitFreeStack + +-- +- elements_: std::vector<amp::optional<Element>> +- elements_written_: std::vector<std::atomic_int> +- write_index_: AtomicIndex +- capacity_full_: AtomicBool +-- ++ WaitFreeStack(const size_t) ++ TryPush(Element&&) + : amp::optional<std::reference_wrapper<Element>> ++ Find(const FindPredicate<Element>&) + : amp::optional<std::reference_wrapper<Element>> + + + +bg=white +fontsize=14 + + + + + UMLClass + + 1090 + 200 + 340 + 190 + + bmw::mw::log::LoggerContainer + +-- +- InsertNewLogger(const amp::string_view): Logger& +- FindExistingLogger(const amp::string_view) + : amp::optional<std::reference_wrapper<Logger>> +- stack_: WaitFreeStack<Logger> +- default_logger_: Logger +-- ++ GetLogger(const amp::string_view): Logger& ++ GetCapacity(): size_t ++ GetDefaultLogger(): Logger& + + +bg=white +fontsize=14 + + + + + Relation + + 760 + 230 + 350 + 400 + + lt=<<<<- +bg=black +owns + 10.0;380.0;10.0;10.0;330.0;10.0 + + + Relation + + 1420 + 220 + 100 + 40 + + lt=<<<<- +bg=black +owns + 10.0;20.0;80.0;20.0 + + + UMLClass + + 50 + 420 + 230 + 60 + + lt=. +SlotHandle::RecorderIdentifier +-- ++ value: std::size_t + + +bg=white +fontsize=14 + + + + Relation + + 410 + 280 + 700 + 1030 + + lt=<<<<- +bg=black +owns + 680.0;10.0;600.0;10.0;600.0;980.0;10.0;980.0;10.0;1010.0 + + + UMLClass + + 150 + 1110 + 140 + 70 + + LoggingIdentifier +-- + +bg=white +fontsize=14 + + + + Relation + + 210 + 1170 + 60 + 140 + + lt=<<<<- +bg=black +owns + 10.0;10.0;10.0;120.0 + + diff --git a/mw/log/design/wait_free_stack_trypush.uxf b/mw/log/design/wait_free_stack_trypush.uxf new file mode 100644 index 0000000..f9cb5a1 --- /dev/null +++ b/mw/log/design/wait_free_stack_trypush.uxf @@ -0,0 +1,15 @@ +10UMLSpecialState270552020type=initialUMLSpecialState150105260100Stack capacity is full? +bg=red +type=decisionRelation270653060lt=<-10;40;10;10Relation2701955060lt=<- +[No]10;40;10;10Relation400145120450lt=<- +m2=[Yes]50;430;50;410;100;410;100;10;10;10UMLState17023522040Current write index = write indexUMLSpecialState150375260100Current write index >= number +of elements in stack +bg=red +type=decisionRelation2703353060lt=<-10;40;10;10UMLState17030522040Increment write indexRelation2702653060lt=<-10;40;10;10Relation40041580110lt=<- +[Yes]30;90;30;10;10;10Relation2704655060lt=<- +[No]10;40;10;10UMLState33057516040Return null valueUMLState17050515040Push element to stackUMLState17057515040Return element valueRelation2705353060lt=<-10;40;10;10Relation2706053080lt=<-10;60;10;10Relation29060516080lt=<-10;60;10;30;140;30;140;10UMLSpecialState2706952020type=final +UMLState33050516040Stack capacity full = trueRelation4205353060lt=<-10;40;10;10UMLObject11025430710WaitFreeStack::TryPush +valign=topRelation2706553060lt=->10;10;10;40UMLSyncBarHorizontal2406558020lw=5 +Text2106856040Finish +halign=center +style=autoresize \ No newline at end of file diff --git a/mw/log/detail/BUILD b/mw/log/detail/BUILD new file mode 100644 index 0000000..19cafbe --- /dev/null +++ b/mw/log/detail/BUILD @@ -0,0 +1,412 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//platform/aas/mw:common_features.bzl", "COMPILER_WARNING_FEATURES") +load("//platform/aas/tools/itf/bazel_gen:itf_gen.bzl", "py_unittest_qnx_test") + +cc_library( + name = "recorder_factory", + srcs = [ + "recorder_factory.cpp", + ], + hdrs = [ + "recorder_factory.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/ara/log/test:__subpackages__", + "//platform/aas/mw/log:__subpackages__", + "//platform/aas/tools/kaitai/kaitai_zero_copy_generator:__subpackages__", + ], + deps = [ + ":composite_recorder", + ":empty_recorder", + ":initialization_reporter", + "//platform/aas/mw/log:recorder_interface", + "//platform/aas/mw/log/configuration:configuration_parser", + "//platform/aas/mw/log/detail/data_router:data_router_backend", + "//platform/aas/mw/log/detail/file_logging:file_recorder", + "//platform/aas/mw/log/detail/file_logging:text_recorder", + ] + select({ + "@platforms//os:linux": [], + "@platforms//os:qnx": [ + "//platform/aas/mw/log/detail/slog", + ], + }), + alwayslink = True, +) + +cc_library( + name = "logging_identifier", + srcs = [ + "logging_identifier.cpp", + ], + hdrs = [ + "logging_identifier.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log:__subpackages__", + "//platform/aas/pas/logging:__pkg__", + ], + deps = [ + "//third_party/static_reflection_with_serialization_pkg:visitor", + "@amp", + ], +) + +cc_library( + name = "types_and_errors", + srcs = ["error.cpp"], + hdrs = ["error.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log:__pkg__", + "//platform/aas/mw/log/configuration:__pkg__", + "//platform/aas/mw/log/detail/data_router:__pkg__", + "//platform/aas/mw/log/detail/file_logging:__pkg__", + ], + deps = [ + "//platform/aas/lib/result", + ], +) + +cc_library( + name = "log_entry_deserialize", + srcs = ["log_entry_deserialize.h"], + features = COMPILER_WARNING_FEATURES, + visibility = [ + "//platform/aas/pas/logging:__pkg__", + "//platform/aas/pas/logging/test:__pkg__", + ], + deps = [ + ":logging_identifier", + "//platform/aas/mw/log:shared_types", + "//third_party/static_reflection_with_serialization_pkg:visitor", + ], +) + +cc_library( + name = "log_entry", + srcs = ["log_entry.cpp"], + hdrs = ["log_entry.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/ara/log:__pkg__", + "//platform/aas/mw/log/detail/data_router:__pkg__", + "//platform/aas/pas/logging:__pkg__", + "//platform/aas/pas/logging/test:__pkg__", + ], + deps = [ + ":logging_identifier", + "//platform/aas/mw/log:shared_types", + "//third_party/static_reflection_with_serialization_pkg:visitor", + ], +) + +cc_library( + name = "backend_interface", + srcs = ["backend.cpp"], + hdrs = ["backend.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log/detail/data_router:__pkg__", + "//platform/aas/mw/log/detail/file_logging:__pkg__", + "//platform/aas/mw/log/detail/slog:__pkg__", + ], + deps = [ + ":log_data_types", + "//platform/aas/mw/log:recorder", + ], +) + +cc_library( + name = "log_data_types", + srcs = [ + "log_record.cpp", + "verbose_payload.cpp", + ], + hdrs = [ + "log_record.h", + "verbose_payload.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log/detail/data_router:__pkg__", + "//platform/aas/mw/log/detail/file_logging:__pkg__", + ], + deps = [ + ":log_entry", + ], +) + +cc_library( + name = "statistics_reporter", + srcs = [ + "istatistics_reporter.cpp", + "statistics_reporter.cpp", + ], + hdrs = [ + "istatistics_reporter.h", + "statistics_reporter.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log/detail/data_router:__pkg__", + "//platform/aas/mw/log/detail/file_logging:__pkg__", + ], + deps = [ + "//platform/aas/mw/log:recorder", + ], +) + +cc_library( + name = "dlt_argument_counter", + srcs = ["dlt_argument_counter.cpp"], + hdrs = [ + "add_argument_result.h", + "dlt_argument_counter.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log/detail/data_router:__pkg__", + "//platform/aas/mw/log/detail/file_logging:__pkg__", + ], + deps = [ + "@amp", + ], +) + +cc_library( + name = "integer_representation", + srcs = [ + "integer_representation.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log/detail/file_logging:__pkg__", + ], +) + +cc_library( + name = "dlt_content_formatting", + srcs = [ + "dlt_format.cpp", + ], + hdrs = [ + "dlt_format.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log/detail/data_router:__pkg__", + "//platform/aas/mw/log/detail/file_logging:__pkg__", + ], + deps = [ + ":dlt_argument_counter", + ":integer_representation", + ":log_data_types", + "//platform/aas/lib/bitmanipulation", + "//platform/aas/lib/memory:string_literal", + "//platform/aas/mw/log:shared_types", + "@amp", + ], +) + +cc_library( + name = "initialization_reporter", + srcs = [ + "initialization_reporter.cpp", + ], + hdrs = [ + "initialization_reporter.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log:__pkg__", + "//platform/aas/mw/log/configuration:__pkg__", + "//platform/aas/mw/log/detail/data_router:__pkg__", + "//platform/aas/mw/log/detail/slog:__pkg__", + ], + deps = [ + ":types_and_errors", + ], +) + +cc_library( + name = "composite_recorder", + srcs = [ + "composite_recorder.cpp", + ], + hdrs = [ + "composite_recorder.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log:__pkg__", + "//platform/aas/mw/log/detail/data_router:__pkg__", + ], + deps = [ + ":initialization_reporter", + ":types_and_errors", + "//platform/aas/mw/log:recorder", + ], +) + +cc_library( + name = "empty_recorder", + srcs = [ + "empty_recorder.cpp", + ], + hdrs = [ + "empty_recorder.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log:__pkg__", + "//platform/aas/mw/log/detail/data_router:__pkg__", + ], + deps = [ + ":log_data_types", + ":log_entry", + ":logging_identifier", + ":types_and_errors", + "//platform/aas/mw/log:recorder", + "@amp", + ], +) + +cc_library( + name = "thread_local_guard", + srcs = ["thread_local_guard.cpp"], + hdrs = ["thread_local_guard.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log:__subpackages__", + ], +) + +cc_test( + name = "unit_test", + srcs = [ + "circular_allocator_test.cpp", + "composite_recorder_test.cpp", + "dlt_argument_counter_test.cpp", + "dlt_format_test.cpp", + "empty_recorder_test.cpp", + "error_test.cpp", + "log_entry_deserialize_test.cpp", + "log_record_test.cpp", + "logging_identifier_test.cpp", + "recorder_factory_test.cpp", + "statistics_reporter_test.cpp", + "thread_local_guard_test.cpp", + "verbose_payload_test.cpp", + ], + data = [ + ], + features = COMPILER_WARNING_FEATURES + [ + "aborts_upon_exception", + ], + tags = ["unit"], + deps = [ + ":backend_mock", + ":log_entry_deserialize", + ":recorder_factory", + ":thread_local_guard", + "//platform/aas/lib/os/mocklib:fcntl_mock", + "//platform/aas/lib/os/mocklib:unistd_mock", + "//platform/aas/lib/os/utils/mocklib:path_mock", + "//platform/aas/mw/log:recorder_mock", + "//platform/aas/mw/log/configuration:configuration_file_discoverer_mock", + "//platform/aas/mw/log/configuration:target_config_reader_mock", + "//platform/aas/mw/log/test/console_logging_environment", + "//third_party/googletest:main", + ], +) + +cc_library( + name = "circular_allocator", + srcs = ["circular_allocator.cpp"], + hdrs = ["circular_allocator.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log:__subpackages__", + ], + deps = ["@amp"], +) + +cc_library( + name = "backend_mock", + testonly = True, + srcs = ["backend_mock.cpp"], + hdrs = ["backend_mock.h"], + features = COMPILER_WARNING_FEATURES, + visibility = [ + "//platform/aas/mw/log:__pkg__", + "//platform/aas/mw/log/detail/data_router:__pkg__", + "//platform/aas/mw/log/detail/file_logging:__pkg__", + ], + deps = [ + "//platform/aas/mw/log/detail:backend_interface", + "//third_party/googletest", + ], +) + +test_suite( + name = "unit_tests", + tests = [ + ":unit_test", + "//platform/aas/mw/log/detail/data_router:unit_tests", + "//platform/aas/mw/log/detail/file_logging:unit_tests", + "//platform/aas/mw/log/detail/slog:unit_tests", + "//platform/aas/mw/log/detail/wait_free_producer_queue:unit_tests", + "//platform/aas/mw/log/detail/wait_free_stack:unit_tests", + ], + visibility = ["//platform/aas/mw/log:__pkg__"], +) + +py_unittest_qnx_test( + name = "pkg_unit_tests_qnx", + test_cases = [ + ":unit_test", + ], +) + +py_unittest_qnx_test( + name = "unit_tests_qnx", + tags = ["manual"], + test_suites = [ + ":pkg_unit_tests_qnx", + "//platform/aas/mw/log/detail/data_router:unit_tests_qnx", + "//platform/aas/mw/log/detail/file_logging:unit_tests_qnx", + "//platform/aas/mw/log/detail/slog:unit_tests_qnx", + "//platform/aas/mw/log/detail/wait_free_producer_queue:unit_tests_qnx", + "//platform/aas/mw/log/detail/wait_free_stack:unit_tests_qnx", + ], + visibility = ["//platform/aas/mw/log:__pkg__"], +) diff --git a/mw/log/detail/add_argument_result.h b/mw/log/detail/add_argument_result.h new file mode 100644 index 0000000..65ddca9 --- /dev/null +++ b/mw/log/detail/add_argument_result.h @@ -0,0 +1,39 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_ADD_ARGUMENT_RESULT_H +#define PLATFORM_AAS_MW_LOG_DETAIL_ADD_ARGUMENT_RESULT_H + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +enum class AddArgumentResult : bool +{ + NotAdded = false, + Added = true, +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif diff --git a/mw/log/detail/backend.cpp b/mw/log/detail/backend.cpp new file mode 100644 index 0000000..3cdd958 --- /dev/null +++ b/mw/log/detail/backend.cpp @@ -0,0 +1,33 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/backend.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + + +Backend::~Backend() = default; + + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/backend.h b/mw/log/detail/backend.h new file mode 100644 index 0000000..a66f524 --- /dev/null +++ b/mw/log/detail/backend.h @@ -0,0 +1,75 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_BACKEND_H +#define PLATFORM_AAS_MW_LOG_DETAIL_BACKEND_H + +#include "platform/aas/mw/log/detail/log_record.h" +#include "platform/aas/mw/log/detail/verbose_payload.h" +#include "platform/aas/mw/log/recorder.h" + +#include "amp_optional.hpp" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +/// \brief The backend represents an interface that abstracts a buffer where the final log data shall be stored in a +/// thread-safe lock-free manner. Therefore, a user can request a _Slot_ where he can write data. He shall flush the +/// slot, once he finished putting his data in. +class Backend +{ + public: + /// \brief Before a producer can store data in our buffer, he has to reserve a slot. + /// + /// \return SlotHandle if a slot was able to be reserved, empty otherwise. + /// + /// \post This ensures that no other thread will write to the reserved slot until FlushSlot() is invoked. + virtual amp::optional ReserveSlot() = 0; + + /// \brief After a producer finished writing into a slot Flush() needs to be called. + /// + /// \param slot The slot that shall be flushed + /// + /// \pre ReserveSlot() was invoked to get a SlotHandle that shall be flushed + /// \post This ensures that afterwards the respective slot can be either read or overwritten + virtual void FlushSlot(const SlotHandle& slot) = 0; + + /// \brief In order to stream data into a slot, the underlying slot buffer needs to be exposed. + /// + /// \param slot The slot for which the associated buffer shall be returned + /// \return The payload associated with slot. (Where a producer can add its data) + /// + /// \pre ReserveSlot() was invoked to receive a SlotHandle + virtual LogRecord& GetLogRecord(const SlotHandle& slot) = 0; + + Backend() = default; + virtual ~Backend(); + Backend(const Backend&) = delete; + Backend(Backend&&) = delete; + Backend& operator=(Backend&&) = delete; + Backend& operator=(const Backend&) = delete; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_BACKEND_H diff --git a/mw/log/detail/backend_mock.cpp b/mw/log/detail/backend_mock.cpp new file mode 100644 index 0000000..512db9c --- /dev/null +++ b/mw/log/detail/backend_mock.cpp @@ -0,0 +1,16 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#include "platform/aas/mw/log/detail/backend_mock.h" diff --git a/mw/log/detail/backend_mock.h b/mw/log/detail/backend_mock.h new file mode 100644 index 0000000..df9aeaa --- /dev/null +++ b/mw/log/detail/backend_mock.h @@ -0,0 +1,51 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_BACKEND_MOCK_H +#define PLATFORM_AAS_MW_LOG_DETAIL_BACKEND_MOCK_H + +#include "platform/aas/mw/log/detail/backend.h" +#include "platform/aas/mw/log/detail/log_record.h" + +#include "gmock/gmock.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +class BackendMock : public Backend +{ + public: + + /* Not actual for mock class; Internal GTest Framework code caused the violation; */ + + /* Not actual for mock class; Internal GTest Framework code caused the violation; */ + MOCK_METHOD((amp::optional), ReserveSlot, (), (override)); + MOCK_METHOD((void), FlushSlot, (const SlotHandle&), (override)); + MOCK_METHOD((LogRecord&), GetLogRecord, (const SlotHandle&), (override)); + + +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_BACKEND_MOCK_H diff --git a/mw/log/detail/circular_allocator.cpp b/mw/log/detail/circular_allocator.cpp new file mode 100644 index 0000000..a446e78 --- /dev/null +++ b/mw/log/detail/circular_allocator.cpp @@ -0,0 +1,15 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/circular_allocator.h" diff --git a/mw/log/detail/circular_allocator.h b/mw/log/detail/circular_allocator.h new file mode 100644 index 0000000..4ae8239 --- /dev/null +++ b/mw/log/detail/circular_allocator.h @@ -0,0 +1,149 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_LIB_CONTAINER_RING_BUFFER_RING_BUFFER_H +#define PLATFORM_AAS_LIB_CONTAINER_RING_BUFFER_RING_BUFFER_H + +#include "amp_assert.hpp" +#include "amp_optional.hpp" + +#include +#include +#include +#include +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +namespace detail +{ + + +/* False positive, 'override' is a keyword and not a variable definition in header file */ + +/* False positive, 'noexcept' is a keyword and not an identifier which could be hidden */ + +template +struct Slot +{ + T data; + std::atomic in_use{false}; +}; + +/// \brief A Ring-Buffer that allows multiple producers to stream data in a lock-free manner +/// +/// \details This implementation is right now specific to our first iteration of our logging implementation. +/// As it can be seen below there is no way for a consumer to acquire data. Further, we need to make this implementation +/// Shared-Memory ready. But, for our first iteration this is good enough :). +/// +/// \tparam T Any type that shall be stored within the Ring-Buffer. +template +class CircularAllocator final +{ + public: + /// \brief Constructs a Ring-Buffer of capacity, without further acquiring memory during runtime. + /// + /// \param capacity The size of how many elements of T shall be stored within the Ring-Buffer + explicit CircularAllocator(std::size_t capacity, const T& initial_value = T{}) + : claimed_sequence_{}, buffer_(capacity) + { + for (auto& slot : buffer_) + { + slot.data = initial_value; + } + } + + /// \brief Starts a Transaction for a producer to stream data into a slot + /// + /// \return The slot in which data can be written + /// + /// \post Slot is acquired and able to be written + amp::optional + AcquireSlotToWrite() noexcept + { + const auto max_number_of_loops = buffer_.size(); + std::uint32_t loop_limiter{0}; + std::size_t slot_index{}; + bool currently_used{false}; + do + { + loop_limiter++; + claimed_sequence_++; + slot_index = claimed_sequence_; + slot_index %= buffer_.capacity(); + + currently_used = false; + if (loop_limiter > max_number_of_loops) + { + return {}; + } + + } while (buffer_.at(slot_index).in_use.compare_exchange_weak(currently_used, true) == false); + + + return slot_index; + } + + /// \brief Get a buffer for a specific slot to write data into it + /// + /// \param slot The slot where the underlying buffer shall be returned + /// \return The buffer of T for the respective slot + /// + /// \pre Slot is acquired by AcquireSlotToWrite() + T& GetUnderlyingBufferFor( + std::size_t slot) noexcept + { + return buffer_.at(slot).data; + } + + /// \brief Stops the transaction of manipulating a specific slot + /// \param slot The slot that is now no longer manipulated + /// + /// \pre slot was acquired by AcquireSlotToWrite() and data was written via GetUnderlyingBufferFor() + /// \post Slot is marked as finished and could be overwritten by another call to AcquireSlotToWrite() + void ReleaseSlot(std::size_t slot) noexcept { buffer_.at(slot).in_use.store(false); } + + /// \brief Returns number of used elements + /// + /// \return The number of used elements + std::size_t GetUsedCount() noexcept + { + // Static cast is allowed as count_if should not return negative value: + return static_cast( + std::count_if(buffer_.begin(), buffer_.end(), [](auto&& i) { return i.in_use.load(); })); + } + + private: + std::atomic claimed_sequence_; + + // For the beginning this is still an std::vector with standard allocator. Once we refactor the IPC to DataRouter, + // this data type will be directly placed in SharedMemory and a custom allocator will be added. + std::vector> buffer_; +}; + + + + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_LIB_CONTAINER_RING_BUFFER_RING_BUFFER_H diff --git a/mw/log/detail/circular_allocator_test.cpp b/mw/log/detail/circular_allocator_test.cpp new file mode 100644 index 0000000..2d9c06d --- /dev/null +++ b/mw/log/detail/circular_allocator_test.cpp @@ -0,0 +1,215 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/circular_allocator.h" + +#include "gtest/gtest.h" + +#include "amp_utility.hpp" + +#include +#include +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +class CircularAllocatorFixture : public ::testing::Test +{ + public: + void WriteInto(CircularAllocator& unit, std::int32_t value) const noexcept + { + const auto slot = unit.AcquireSlotToWrite(); + ASSERT_TRUE(slot.has_value()); + unit.GetUnderlyingBufferFor(slot.value()) = value; + unit.ReleaseSlot(slot.value()); + } +}; + +using CircularAllocatorFixtureDeathTest = CircularAllocatorFixture; + +TEST_F(CircularAllocatorFixture, WriteSingleEntryWithoutThreads) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Writing into the circular allocator shall succeed if there are multiple free slots."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given a Ring-Buffer with enough space + CircularAllocator unit{5}; + + // When writing into it a single value + WriteInto(unit, 42); + + // Then no exceptions, wrong allocations or other errors happen +} + +TEST_F(CircularAllocatorFixture, WriteSingleEntryWithoutThreadsWithSingleSlotCapacity) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Writing into the circular allocator shall succeed if a single slot is free."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given a Ring-Buffer with enough space + CircularAllocator unit{1}; + + // When writing into it a single value + WriteInto(unit, 42); + + // Then no exceptions, wrong allocations or other errors happen +} + +TEST_F(CircularAllocatorFixture, WriteSingleThreadedOverBufferSize) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "When writing more slots in the circular allocator than capacity, the write to the next slot shall " + "wrap around and succeed."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Overview of the expected storage layout: + // | Slot 0 | Slot 1 | Slot 2 | + // | N/A | 0 | 1 | <- Before overrun + // | 2 | 3 | 1 | <- After overrun + + // Given a Ring-Buffer with to few space + CircularAllocator unit{3}; + + // When adding more into the buffer than its capacity + for (std::uint8_t counter{}; counter < 4; counter++) + { + WriteInto(unit, counter); + } + + // Then old unused values are overwritten + ASSERT_EQ(unit.GetUnderlyingBufferFor(std::size_t{0}), 2); + ASSERT_EQ(unit.GetUnderlyingBufferFor(std::size_t{1}), 3); + ASSERT_EQ(unit.GetUnderlyingBufferFor(std::size_t{2}), 1); +} + +TEST_F(CircularAllocatorFixture, WritingFromMultipleThreadsIsSafe) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "When writing to the CircularAllocator from multiple threads and each thread shall occupy one slot at a time " + "every slot allocation from every thread shall succeed if the number of threads is equal the " + "number of slots."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given a Ring-Buffer + constexpr size_t number_of_slots = 10U; + CircularAllocator unit{number_of_slots}; + + // When writing into it from multiple threads + std::array threads{}; + for (std::uint8_t counter{}; counter < threads.size(); counter++) + { + threads.at(counter) = std::thread([&unit, counter, this]() noexcept { + // TODO: Re-enable multithreaded test for N threads allocating from N-element pool. See + // i.e. the loop: 'for (std::int32_t number{}; number < 100; number++) { WriteInto(unit, number); }' + WriteInto(unit, counter); + }); + } + + // Then no memory corruption or race conditions happen (checked by TSAN, ASAN, valgrind) + for (auto& thread : threads) + { + thread.join(); + } +} + +TEST_F(CircularAllocatorFixture, WritingFromMultipleThreadsIsSafeWithInsufficientCapacity) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "When writing to CircularAllocator with multiple threads in parallel and trying to allocate more " + "slots than capacity, the number of reserved slots shall be equal to the capacity."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given a Ring-Buffer + constexpr std::size_t number_of_slots{100}; + CircularAllocator unit{number_of_slots}; + + // When trying to write into it from multiple threads such that the number of slots is insufficient. + std::array threads{}; + std::array number_of_reserved_slots_per_thread{}; + for (std::size_t counter{}; counter < threads.size(); counter++) + { + threads.at(counter) = std::thread([&unit, counter, &number_of_reserved_slots_per_thread]() noexcept { + for (std::size_t number{}; number < 50; number++) + { + if (unit.AcquireSlotToWrite().has_value()) + { + number_of_reserved_slots_per_thread[counter]++; + } + } + }); + } + + // Then no memory corruption or race conditions happen (checked by TSAN, ASAN, valgrind) + for (auto& thread : threads) + { + thread.join(); + } + + // And the number of reserved slots shall be equal to the capacity. + const std::size_t number_of_reserved_slots = + std::accumulate(number_of_reserved_slots_per_thread.begin(), number_of_reserved_slots_per_thread.end(), 0u); + EXPECT_EQ(number_of_reserved_slots, number_of_slots); +} + +TEST_F(CircularAllocatorFixture, TryAcquireWhenAllSlotsAcquired) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "When every slot is occupied acquiring another slot shall return an empty result."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given a Ring-Buffer where all slots are acquired + CircularAllocator unit{1}; + EXPECT_TRUE(unit.AcquireSlotToWrite().has_value()); + + // When acquiring another one + const auto slot = unit.AcquireSlotToWrite(); + + // Then no slot can be acquired and empty optional is returned + EXPECT_FALSE(slot.has_value()); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/composite_recorder.cpp b/mw/log/detail/composite_recorder.cpp new file mode 100644 index 0000000..e8e51f8 --- /dev/null +++ b/mw/log/detail/composite_recorder.cpp @@ -0,0 +1,278 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/composite_recorder.h" + +#include "platform/aas/lib/result/result.h" +#include "platform/aas/mw/log/detail/error.h" +#include "platform/aas/mw/log/detail/initialization_reporter.h" + +#include "amp_callback.hpp" +#include "amp_utility.hpp" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +constexpr std::size_t kRecorderWithRecorderIdCallbackCapacity = 64UL; +using RecorderWithRecorderIdCallback = + amp::callback; + +/// \brief Iterates over all recorders and invokes a callback. +void ForEachRecorder(const std::vector>& recorders, + const RecorderWithRecorderIdCallback callback) noexcept +{ + std::size_t recorder_index{}; + + /* The lambda will be executed within this stack. Thus, all references are still valid */ + amp::ignore = std::for_each(recorders.begin(), recorders.end(), [&recorder_index, &callback](auto& recorder) { + callback(*recorder, SlotHandle::RecorderIdentifier{recorder_index}); + ++recorder_index; + }); + +} + +using RecorderWithSlotCallback = amp::callback; + +/// \brief Iterates over all recorders with an active slot. +/// \details For each recorder, we check if the composite_slot contains a corresponding slot handle. If the slot handle +/// is available we invoke the callback with the pair of concrete recorder and corresponding slot. +void ForEachActiveSlot(const std::vector>& recorders, + const SlotHandle composite_slot, + const RecorderWithSlotCallback callback) noexcept +{ + SlotHandle slot_for_recorder{}; + + /* The lambda will be executed within this stack. Thus, all references are still valid */ + ForEachRecorder(recorders, + [&slot_for_recorder, &composite_slot, &callback](Recorder& recorder, + const SlotHandle::RecorderIdentifier recorder_id) { + if (composite_slot.IsRecorderActive(recorder_id)) + { + slot_for_recorder.SetSlot(composite_slot.GetSlot(recorder_id)); + callback(recorder, slot_for_recorder); + } + }); + +} + +template +void LogForEachActiveSlot(const std::vector>& recorders, + const SlotHandle& composite_slot, + const T& arg) +{ + ForEachActiveSlot(recorders, composite_slot, [arg](Recorder& recorder, const SlotHandle& slot) noexcept { + recorder.Log(slot, arg); + }); +} +} // namespace + +CompositeRecorder::CompositeRecorder(std::vector> recorders) noexcept + : Recorder{}, recorders_{std::move(recorders)} +{ + if (recorders_.size() > SlotHandle::kMaxRecorders) + { + ReportInitializationError(Error::kMaximumNumberOfRecordersExceeded); + recorders_.resize(SlotHandle::kMaxRecorders); + } +} + + +amp::optional CompositeRecorder::StartRecord(const amp::string_view context_id, + const LogLevel log_level) noexcept + +{ + SlotHandle composite_slot{}; + + + /* The lambda will be executed within this stack. Thus, all references are still valid */ + ForEachRecorder( + recorders_, + [&context_id, &log_level, &composite_slot](Recorder& recorder, + const SlotHandle::RecorderIdentifier recorder_id) { + const auto result = recorder.StartRecord(context_id, log_level); + if (result.has_value()) + { + composite_slot.SetSlot( + result->GetSlotOfSelectedRecorder(), // LCOV_EXCL_BR_LINE: there are no branches to be covered. + recorder_id); + } + }); + + + return composite_slot; +} + +void CompositeRecorder::StopRecord(const SlotHandle& composite_slot) noexcept +{ + ForEachActiveSlot(recorders_, composite_slot, [](Recorder& recorder, const SlotHandle& slot) noexcept { + recorder.StopRecord(slot); + }); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const bool arg) noexcept +{ + LogForEachActiveSlot(recorders_, composite_slot, arg); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const std::uint8_t arg) noexcept +{ + LogForEachActiveSlot(recorders_, composite_slot, arg); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const std::int8_t arg) noexcept +{ + LogForEachActiveSlot(recorders_, composite_slot, arg); +} +void CompositeRecorder::Log(const SlotHandle& composite_slot, const std::uint16_t arg) noexcept +{ + LogForEachActiveSlot(recorders_, composite_slot, arg); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const std::int16_t arg) noexcept +{ + LogForEachActiveSlot(recorders_, composite_slot, arg); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const std::uint32_t arg) noexcept +{ + LogForEachActiveSlot(recorders_, composite_slot, arg); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const std::int32_t arg) noexcept +{ + LogForEachActiveSlot(recorders_, composite_slot, arg); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const std::uint64_t arg) noexcept +{ + LogForEachActiveSlot(recorders_, composite_slot, arg); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const std::int64_t arg) noexcept +{ + LogForEachActiveSlot(recorders_, composite_slot, arg); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const float arg) noexcept +{ + LogForEachActiveSlot(recorders_, composite_slot, arg); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const double arg) noexcept +{ + LogForEachActiveSlot(recorders_, composite_slot, arg); +} + + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const amp::string_view arg) noexcept + +{ + LogForEachActiveSlot(recorders_, composite_slot, arg); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const LogHex8 arg) noexcept + +{ + LogForEachActiveSlot(recorders_, composite_slot, arg.value); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const LogHex16 arg) noexcept + +{ + LogForEachActiveSlot(recorders_, composite_slot, arg.value); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const LogHex32 arg) noexcept + +{ + LogForEachActiveSlot(recorders_, composite_slot, arg.value); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const LogHex64 arg) noexcept + +{ + LogForEachActiveSlot(recorders_, composite_slot, arg.value); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const LogBin8 arg) noexcept + +{ + LogForEachActiveSlot(recorders_, composite_slot, arg.value); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const LogBin16 arg) noexcept + +{ + LogForEachActiveSlot(recorders_, composite_slot, arg.value); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const LogBin32 arg) noexcept + +{ + LogForEachActiveSlot(recorders_, composite_slot, arg.value); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const LogBin64 arg) noexcept + +{ + LogForEachActiveSlot(recorders_, composite_slot, arg.value); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const LogRawBuffer arg) noexcept + +{ + LogForEachActiveSlot(recorders_, composite_slot, arg); +} + +void CompositeRecorder::Log(const SlotHandle& composite_slot, const LogSlog2Message arg) noexcept +{ + LogForEachActiveSlot(recorders_, composite_slot, arg.GetMessage()); +} + +const std::vector>& CompositeRecorder::GetRecorders() const noexcept +{ + return recorders_; +} + +bool CompositeRecorder::IsLogEnabled(const LogLevel& log_level, const amp::string_view context) const noexcept +{ + // Return true if at least one recorder is enabled. + + bool is_log_enabled = false; + + + ForEachRecorder( + recorders_, + [&is_log_enabled, log_level, context](const auto& recorder, const SlotHandle::RecorderIdentifier) noexcept { + is_log_enabled = is_log_enabled || recorder.IsLogEnabled(log_level, context); + }); + + + return is_log_enabled; +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/composite_recorder.h b/mw/log/detail/composite_recorder.h new file mode 100644 index 0000000..76b74da --- /dev/null +++ b/mw/log/detail/composite_recorder.h @@ -0,0 +1,83 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_COMPOSITE_RECORDER_H +#define PLATFORM_AAS_MW_LOG_DETAIL_COMPOSITE_RECORDER_H + +#include "platform/aas/mw/log/recorder.h" + +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +/// \brief CompositeRecorder forwards the log statement to one or more concrete Recorder(s). +class CompositeRecorder final : public Recorder +{ + public: + explicit CompositeRecorder(std::vector> recorders) noexcept; + + amp::optional StartRecord(const amp::string_view context_id, + const LogLevel log_level) noexcept override final; + + void StopRecord(const SlotHandle& composite_slot) noexcept override final; + + void Log(const SlotHandle& composite_slot, const bool arg) noexcept override final; + void Log(const SlotHandle& composite_slot, const std::uint8_t arg) noexcept override final; + void Log(const SlotHandle& composite_slot, const std::int8_t arg) noexcept override final; + void Log(const SlotHandle& composite_slot, const std::uint16_t arg) noexcept override final; + void Log(const SlotHandle& composite_slot, const std::int16_t arg) noexcept override final; + void Log(const SlotHandle& composite_slot, const std::uint32_t arg) noexcept override final; + void Log(const SlotHandle& composite_slot, const std::int32_t arg) noexcept override final; + void Log(const SlotHandle& composite_slot, const std::uint64_t arg) noexcept override final; + void Log(const SlotHandle& composite_slot, const std::int64_t arg) noexcept override final; + void Log(const SlotHandle& composite_slot, const float arg) noexcept override final; + void Log(const SlotHandle& composite_slot, const double arg) noexcept override final; + void Log(const SlotHandle& composite_slot, const amp::string_view arg) noexcept override final; + + void Log(const SlotHandle& composite_slot, const LogHex8 arg) noexcept override final; + void Log(const SlotHandle& composite_slot, const LogHex16 arg) noexcept override final; + void Log(const SlotHandle& composite_slot, const LogHex32 arg) noexcept override final; + void Log(const SlotHandle& composite_slot, const LogHex64 arg) noexcept override final; + + void Log(const SlotHandle& composite_slot, const LogBin8 arg) noexcept override final; + void Log(const SlotHandle& composite_slot, const LogBin16 arg) noexcept override final; + void Log(const SlotHandle& composite_slot, const LogBin32 arg) noexcept override final; + void Log(const SlotHandle& composite_slot, const LogBin64 arg) noexcept override final; + + void Log(const SlotHandle& composite_slot, const LogRawBuffer arg) noexcept override final; + + void Log(const SlotHandle&, const LogSlog2Message) noexcept override final; + + const std::vector>& GetRecorders() const noexcept; + + bool IsLogEnabled(const LogLevel&, const amp::string_view context) const noexcept override; + + private: + std::vector> recorders_; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_CONSOLE_RECORDER_H diff --git a/mw/log/detail/composite_recorder_test.cpp b/mw/log/detail/composite_recorder_test.cpp new file mode 100644 index 0000000..2dd53a6 --- /dev/null +++ b/mw/log/detail/composite_recorder_test.cpp @@ -0,0 +1,316 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/composite_recorder.h" + +#include "platform/aas/mw/log/recorder_mock.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "amp_callback.hpp" + +#include + +using testing::_; +using testing::Return; + +using namespace amp::literals::string_view_literals; + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +namespace +{ + +const amp::string_view kContext{"aCtx"}; +const LogLevel kLogLevel{LogLevel::kInfo}; + +const bool kBool{true}; +const std::uint8_t kUint8{std::numeric_limits::max()}; +const std::uint16_t kUint16{std::numeric_limits::max()}; +const std::uint32_t kUint32{std::numeric_limits::max()}; +const std::uint64_t kUint64{std::numeric_limits::max()}; +const std::int8_t kInt8{std::numeric_limits::max()}; +const std::int16_t kInt16{std::numeric_limits::max()}; +const std::int32_t kInt32{std::numeric_limits::max()}; +const std::int64_t kInt64{std::numeric_limits::max()}; +const float kFloat{std::numeric_limits::max()}; +const double kDouble{std::numeric_limits::max()}; +const amp::string_view kStringView{"Hello World"_sv}; +// subtract one from max uint values to expect hex logging. +const LogHex8 kHex8{kUint8 - 1}; +const LogHex16 kHex16{kUint16 - 1}; +const LogHex32 kHex32{kUint32 - 1}; +const LogHex64 kHex64{kUint64 - 1}; + +// subtract 2 from max uint values to expect binary logging. +const LogBin8 kBin8{kUint8 - 2}; +const LogBin16 kBin16{kUint16 - 2}; +const LogBin32 kBin32{kUint32 - 2}; +const LogBin64 kBin64{kUint64 - 2}; + +LogRawBuffer log_raw_buffer{nullptr, 0}; +class CompositeRecorderFixture : public ::testing::Test +{ + public: + void SetUp() override {} + + void TearDown() override {} + + void AddRecorder(std::unique_ptr recorder) noexcept { recorders_.emplace_back(std::move(recorder)); } + + void CreateAllAvailableRecorders(amp::callback(size_t)> create_recorder) noexcept + { + for (size_t i = 0; i < SlotHandle::kMaxRecorders; ++i) + { + AddRecorder(create_recorder(i)); + } + } + + CompositeRecorder& CreateCompositeRecorder() noexcept + { + composite_recorder_ = std::make_unique(std::move(recorders_)); + + return *composite_recorder_; + } + + protected: + std::vector> recorders_{}; + std::unique_ptr composite_recorder_{}; +}; + +TEST_F(CompositeRecorderFixture, CompositeRecorderShallCropExceedingNumberOfRecorders) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "adding recorders to composite recorder which exceed number of recorders will have no effect and " + "will be cropped."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + std::vector> recorders; + + // Add the number of allowed recorders + CreateAllAvailableRecorders([](std::size_t /*recorder*/) { + auto mock_recorder = std::make_unique(); + EXPECT_CALL(*mock_recorder, StartRecord(kContext, kLogLevel)); + return mock_recorder; + }); + + { + // Add one recorder exceeding the number of allowed recorders. + auto mock_recorder = std::make_unique(); + + // Since this recorder shall be dropped, StartRecord shall not be called. + EXPECT_CALL(*mock_recorder, StartRecord(kContext, kLogLevel)).Times(0); + + AddRecorder(std::move(mock_recorder)); + } + + auto& composite_recorder = CreateCompositeRecorder(); + + composite_recorder.StartRecord(kContext, kLogLevel); +} + +TEST_F(CompositeRecorderFixture, StartRecordWithSlotAvailableShallRetainCorrectSlot) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "if a slot is available, starting record in CompositeRecorder shall retain a correct slot."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + std::vector> recorders; + + CreateAllAvailableRecorders([](std::size_t recorder) { + auto mock_recorder = std::make_unique(); + + // Set the slot index to be equal to the recorder index. + EXPECT_CALL(*mock_recorder, StartRecord(kContext, kLogLevel)).WillOnce(Return(recorder)); + + EXPECT_CALL(*mock_recorder, StopRecord(SlotHandle{static_cast(recorder)})); + return mock_recorder; + }); + + auto& composite_recorder = CreateCompositeRecorder(); + + const auto slot = composite_recorder.StartRecord(kContext, kLogLevel); + ASSERT_TRUE(slot.has_value()); + composite_recorder.StopRecord(slot.value()); + + for (std::size_t recorder = 0; recorder < SlotHandle::kMaxRecorders; ++recorder) + { + EXPECT_TRUE(slot->IsRecorderActive(SlotHandle::RecorderIdentifier{recorder})); + + // Slot index shall be equal to the recorder index in this test case. + EXPECT_EQ(slot->GetSlot(SlotHandle::RecorderIdentifier{recorder}), recorder); + } +} + +TEST_F(CompositeRecorderFixture, StartRecordWithNoSlotAvailableShallDropRecorder) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "if no slot is available, starting record CompositeRecorder shall drop recorder."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + std::vector> recorders; + + CreateAllAvailableRecorders([](std::size_t /*recorder*/) { + auto mock_recorder = std::make_unique(); + EXPECT_CALL(*mock_recorder, StartRecord(kContext, kLogLevel)).WillOnce(Return(amp::optional{})); + EXPECT_CALL(*mock_recorder, StopRecord(_)).Times(0); + return mock_recorder; + }); + + auto& composite_recorder = CreateCompositeRecorder(); + + const auto slot = composite_recorder.StartRecord(kContext, kLogLevel); + ASSERT_TRUE(slot.has_value()); + composite_recorder.StopRecord(slot.value()); + + for (std::size_t recorder = 0; recorder < SlotHandle::kMaxRecorders; ++recorder) + { + EXPECT_FALSE(slot->IsRecorderActive(SlotHandle::RecorderIdentifier{recorder})); + } +} + +TEST_F(CompositeRecorderFixture, LogInvocationShallBeForwardedToAllAvailableRecorders) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "log invocation shall be forworded to any supported logging type."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + std::vector> recorders; + + CreateAllAvailableRecorders([](std::size_t recorder) { + auto mock_recorder = std::make_unique(); + SlotHandle recorder_slot{static_cast(recorder)}; + EXPECT_CALL(*mock_recorder, StartRecord(kContext, kLogLevel)).WillOnce(Return(recorder_slot)); + EXPECT_CALL(*mock_recorder, LogBool(recorder_slot, kBool)); + EXPECT_CALL(*mock_recorder, LogUint8(recorder_slot, kUint8)); + EXPECT_CALL(*mock_recorder, LogUint16(recorder_slot, kUint16)); + EXPECT_CALL(*mock_recorder, LogUint32(recorder_slot, kUint32)); + EXPECT_CALL(*mock_recorder, LogUint64(recorder_slot, kUint64)); + EXPECT_CALL(*mock_recorder, LogInt8(recorder_slot, kInt8)); + EXPECT_CALL(*mock_recorder, LogInt16(recorder_slot, kInt16)); + EXPECT_CALL(*mock_recorder, LogInt32(recorder_slot, kInt32)); + EXPECT_CALL(*mock_recorder, LogInt64(recorder_slot, kInt64)); + EXPECT_CALL(*mock_recorder, LogFloat(recorder_slot, kFloat)); + EXPECT_CALL(*mock_recorder, LogDouble(recorder_slot, kDouble)); + EXPECT_CALL(*mock_recorder, LogStringView(recorder_slot, kStringView)); + EXPECT_CALL(*mock_recorder, LogUint8(recorder_slot, kHex8.value)); + EXPECT_CALL(*mock_recorder, LogUint16(recorder_slot, kHex16.value)); + EXPECT_CALL(*mock_recorder, LogUint32(recorder_slot, kHex32.value)); + EXPECT_CALL(*mock_recorder, LogUint64(recorder_slot, kHex64.value)); + + EXPECT_CALL(*mock_recorder, LogUint8(recorder_slot, kBin8.value)); + EXPECT_CALL(*mock_recorder, LogUint16(recorder_slot, kBin16.value)); + EXPECT_CALL(*mock_recorder, LogUint32(recorder_slot, kBin32.value)); + EXPECT_CALL(*mock_recorder, LogUint64(recorder_slot, kBin64.value)); + + EXPECT_CALL( + *mock_recorder, + Log_LogRawBuffer(recorder_slot, log_raw_buffer.data(), static_cast(log_raw_buffer.size()))); + return mock_recorder; + }); + + auto& composite_recorder = CreateCompositeRecorder(); + + const auto slot = composite_recorder.StartRecord(kContext, kLogLevel); + ASSERT_TRUE(slot.has_value()); + + composite_recorder.Log(slot.value(), kBool); + composite_recorder.Log(slot.value(), kUint8); + composite_recorder.Log(slot.value(), kUint16); + composite_recorder.Log(slot.value(), kUint32); + composite_recorder.Log(slot.value(), kUint64); + composite_recorder.Log(slot.value(), kInt8); + composite_recorder.Log(slot.value(), kInt16); + composite_recorder.Log(slot.value(), kInt32); + composite_recorder.Log(slot.value(), kInt64); + composite_recorder.Log(slot.value(), kFloat); + composite_recorder.Log(slot.value(), kDouble); + composite_recorder.Log(slot.value(), kStringView); + composite_recorder.Log(slot.value(), kHex8); + composite_recorder.Log(slot.value(), kHex16); + composite_recorder.Log(slot.value(), kHex32); + composite_recorder.Log(slot.value(), kHex64); + + composite_recorder.Log(slot.value(), kBin8); + composite_recorder.Log(slot.value(), kBin16); + composite_recorder.Log(slot.value(), kBin32); + composite_recorder.Log(slot.value(), kBin64); + + composite_recorder.Log(slot.value(), log_raw_buffer); +} + +TEST_F(CompositeRecorderFixture, LogShallBeEnabledIfAtLeastOneRecorderIsEnabled) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "if at least one recorder is enabled, the logging shall be enabled."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + const auto enable_first_recorder = [](std::size_t recorder_index) { + auto mock_recorder = std::make_unique(); + // Return enabled only for the first recorder. + EXPECT_CALL(*mock_recorder, IsLogEnabled(kLogLevel, kContext)).WillRepeatedly(Return(recorder_index == 0)); + return mock_recorder; + }; + + CreateAllAvailableRecorders(enable_first_recorder); + + auto& composite_recorder = CreateCompositeRecorder(); + EXPECT_TRUE(composite_recorder.IsLogEnabled(kLogLevel, kContext)); +} + +TEST_F(CompositeRecorderFixture, LogShallBeDisabledIfAllRecorderAreDisabled) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "if all recorders is disabled, the logging shall be disabled."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + std::vector> recorders; + + CreateAllAvailableRecorders([](std::size_t /*recorder*/) { + auto mock_recorder = std::make_unique(); + EXPECT_CALL(*mock_recorder, IsLogEnabled(kLogLevel, kContext)).WillRepeatedly(Return(false)); + return mock_recorder; + }); + + auto& composite_recorder = CreateCompositeRecorder(); + EXPECT_FALSE(composite_recorder.IsLogEnabled(kLogLevel, kContext)); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/dlt_argument_counter.cpp b/mw/log/detail/dlt_argument_counter.cpp new file mode 100644 index 0000000..58b1bc9 --- /dev/null +++ b/mw/log/detail/dlt_argument_counter.cpp @@ -0,0 +1,53 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#include "platform/aas/mw/log/detail/dlt_argument_counter.h" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + + +DltArgumentCounter::DltArgumentCounter(std::uint8_t& counter) noexcept : counter_(counter) {} + +AddArgumentResult DltArgumentCounter::TryAddArgument(add_argument_callback callback) noexcept +{ + + constexpr uint8_t MAX_COUNTER = std::numeric_limits::type>::max(); + if (counter_ == MAX_COUNTER) + { + return AddArgumentResult::NotAdded; + } + + const auto result = callback(); + if (result == AddArgumentResult::Added) + { + counter_++; + } + + return result; +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/dlt_argument_counter.h b/mw/log/detail/dlt_argument_counter.h new file mode 100644 index 0000000..6014925 --- /dev/null +++ b/mw/log/detail/dlt_argument_counter.h @@ -0,0 +1,48 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_DLT_ARGUMENT_COUNTER_H +#define PLATFORM_AAS_MW_LOG_DETAIL_DLT_ARGUMENT_COUNTER_H + +#include "amp_callback.hpp" +#include "platform/aas/mw/log/detail/add_argument_result.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +class DltArgumentCounter +{ + public: + using add_argument_callback = amp::callback; + + explicit DltArgumentCounter(std::uint8_t&) noexcept; + AddArgumentResult TryAddArgument(add_argument_callback) noexcept; + + private: + std::uint8_t& counter_; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif diff --git a/mw/log/detail/dlt_argument_counter_test.cpp b/mw/log/detail/dlt_argument_counter_test.cpp new file mode 100644 index 0000000..1779a4e --- /dev/null +++ b/mw/log/detail/dlt_argument_counter_test.cpp @@ -0,0 +1,89 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/dlt_argument_counter.h" + +#include "gtest/gtest.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +TEST(DltArgumentCounterShould, IncreaseCounter) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "DltArgumentCounter should increase counter when an argument is added"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + uint8_t counter = 0; + DltArgumentCounter sut{counter}; + EXPECT_EQ(AddArgumentResult::Added, sut.TryAddArgument([]() noexcept { return AddArgumentResult::Added; })); + EXPECT_EQ(counter, 1); +} + +TEST(DltArgumentCounterShould, NotIncreaseCounterBecauseArgumentNotAdded) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "DltArgumentCounter should not increase counter when no argument is added"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + uint8_t counter = 0; + DltArgumentCounter sut{counter}; + EXPECT_EQ(AddArgumentResult::NotAdded, sut.TryAddArgument([]() noexcept { return AddArgumentResult::NotAdded; })); + EXPECT_EQ(counter, 0); +} + +TEST(DltArgumentCounterShould, NotIncreaseCounterBecauseMaxCounterReached) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "DltArgumentCounter should not increase counter when the counter has maximum value"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + uint8_t counter = 255; + DltArgumentCounter sut{counter}; + EXPECT_EQ(AddArgumentResult::NotAdded, sut.TryAddArgument([]() noexcept { return AddArgumentResult::Added; })); + EXPECT_EQ(counter, 255); +} + +TEST(DltArgumentCounterShould, NotIncreaseCounterBecauseMaxCounterReachedAndNoArgumentAdded) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "DltArgumentCounter should not increase counter when the counter has maximum value and no argument is added"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + uint8_t counter = 255; + DltArgumentCounter sut{counter}; + EXPECT_EQ(AddArgumentResult::NotAdded, sut.TryAddArgument([]() noexcept { return AddArgumentResult::NotAdded; })); + EXPECT_EQ(counter, 255); +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/dlt_format.cpp b/mw/log/detail/dlt_format.cpp new file mode 100644 index 0000000..a7746c9 --- /dev/null +++ b/mw/log/detail/dlt_format.cpp @@ -0,0 +1,598 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/dlt_format.h" + +#include "platform/aas/lib/bitmanipulation/bit_manipulation.h" + +#include "amp_assert.hpp" +#include "amp_span.hpp" +#include +#include + +#include + +/// For specification of the DLT protocol, please see: +/// https://www.autosar.org/fileadmin/user_upload/standards/foundation/1-0/AUTOSAR_PRS_DiagnosticLogAndTraceProtocol.pdf +/// +/// As described in chapter 5.1 the general format of an DLT message looks as follows: +/// +-----------------+-----------------+---------+ +/// | Standard Header | Extended Header | Payload | +/// +-----------------+-----------------+---------+ +/// +/// The `Standard Header` and `Extended Header` are for now no concern in this implementation. They will be filled by +/// the `DataRouter` application. For now we focus in this document on the `Payload` part. +/// +/// The payload section can be filled in two ways: Non-Verbose (chapter 5.1.2.1) or Verbose (5.1.2.2). +/// This part of our code only implements the Verbose-Mode. +/// +/// The verbose mode is further split into argument sections (PRS_Dlt_00459). +/// +-----------------+-----------------+-----------------------------------------------------+ +/// | Standard Header | Extended Header | Payload | +/// | | +--------------------------+--------------------------+ +/// | | | Argument 1 | Argument 2 | +/// | | +-----------+--------------+-----------+--------------+ +/// | | | Type Info | Data Payload | Type Info | Data Payload | +/// +-----------------+-----------------+-----------+--------------+-----------+--------------+ +/// +/// For now also this handling of argument numbers is not handled within this class. This class rather interprets each +/// call to Log() as another argument that is added towards the payload. For this it takes case that the Type Info and +/// Data Payload are filled correctly. Any recorder using this formatter, will for now take care of the argument +/// handling. + +namespace +{ + +// \Requirement PRS_Dlt_00626, PRS_Dlt_00354 +enum class TypeLength : std::uint32_t +{ + kNotDefined = 0x00, + k8Bit = 0x01, + k16Bit = 0x02, + k32Bit = 0x03, + k64Bit = 0x04, + k128Bi = 0x05, +}; + +// \Requirement PRS_Dlt_00627, PRS_Dlt_00182, PRS_Dlt_00366 +enum class StringEncoding : std::uint32_t +{ + kASCII = 0x00, + kUTF8 = 0x01, +}; + +// \Requirement PRS_Dlt_00783 +enum class IntegerRepresentation : std::uint32_t +{ + kBase10 = 0x00, + kBase8 = 0x01, + kBase16 = 0x02, + kBase2 = 0x03 +}; + +class TypeInfo +{ + public: + explicit TypeInfo(const std::uint32_t type) + { + amp::ignore = bmw::platform::SetBit(underlying_type_, static_cast(type)); + } + + // \Requirement PRS_Dlt_00625 + constexpr static std::size_t TYPE_BOOL_BIT = 4U; + constexpr static std::size_t TYPE_SIGNED_BIT = 5U; + constexpr static std::size_t TYPE_UNSIGNED_BIT = 6U; + constexpr static std::size_t TYPE_FLOAT_BIT = 7U; + constexpr static std::size_t TYPE_STRING_BIT = 9U; + constexpr static std::size_t TYPE_RAW_BIT = 10U; + + // \Requirement PRS_Dlt_00354 + amp::optional Set(const TypeLength length) + { + static_assert(std::is_same::type, std::uint32_t>::value, + "Mismatching underlying type. Cast not valid."); + underlying_type_ |= static_cast(length); + return *this; + } + + // \Requirement PRS_Dlt_00183, PRS_Dlt_00367 + amp::optional Set(const StringEncoding encoding) noexcept + { + const auto is_type_string_bit_set = bmw::platform::CheckBit(underlying_type_, TYPE_STRING_BIT); + const auto is_trace_info_bit_set = bmw::platform::CheckBit(underlying_type_, TRACE_INFO_BIT); + + + if ((is_type_string_bit_set || is_trace_info_bit_set) == false) + { + return {}; + } + + + + static_assert(std::is_same::type, std::uint32_t>::value, + "Mismatching underlying type. Cast not valid."); + underlying_type_ |= (static_cast(encoding) << STRING_ENCODING_START); + return *this; + } + + // \Requirement PRS_Dlt_00782, PRS_Dlt_00783 + amp::optional Set(const bmw::mw::log::detail::IntegerRepresentation encoding) noexcept + { + const auto is_type_unsigned_bit_set = bmw::platform::CheckBit(underlying_type_, TYPE_UNSIGNED_BIT); + const auto is_type_signed_bit_set = bmw::platform::CheckBit(underlying_type_, TYPE_SIGNED_BIT); + + + if ((is_type_unsigned_bit_set || is_type_signed_bit_set) == + false) // LCOV_EXCL_BR_LINE: can't achieve the other conditions because can't control the + // "underlying_type_" value. + { + return {}; + } + + + + // Make sure the enum values match the values from the standard requirement PRS_Dlt_00783. + static_assert(static_cast(IntegerRepresentation::kBase10) == 0U, + "Value shall match PRS_Dlt_00783"); + static_assert(static_cast(IntegerRepresentation::kBase8) == 1U, + "Value shall match PRS_Dlt_00783"); + static_assert(static_cast(IntegerRepresentation::kBase16) == 2U, + "Value shall match PRS_Dlt_00783"); + static_assert(static_cast(IntegerRepresentation::kBase2) == 3U, + "Value shall match PRS_Dlt_00783"); + + static_assert( + std::is_same::type, std::uint8_t>::value, + "Mismatching underlying type. Cast not valid."); + underlying_type_ |= (static_cast(encoding) << INTEGER_ENCODING_START); + return *this; + } + + private: + // \Requirement PRS_Dlt_00135 + std::uint32_t underlying_type_{}; + + + // \Requirement PRS_Dlt_00625 + // constexpr static auto VARIABLE_INFO_BIT = 11U; not supported in our implementation + // constexpr static auto FIXED_POINT_BIT = 12U; not supported in our implementation + constexpr static std::size_t TRACE_INFO_BIT{13}; + constexpr static auto STRING_ENCODING_START = 15U; + // \Requirement PRS_Dlt_00782 + constexpr static auto INTEGER_ENCODING_START = 15U; + +}; + +template +std::size_t Sum(T t) +{ + return t; +} + +template +std::size_t Sum(T t, Rest... rest) +{ + return t + Sum(rest...); +} + + +template +std::size_t SizeOf(T) +{ + return sizeof(T); +} + + +std::size_t SizeOf(const amp::string_view t) +{ + return t.size(); +} + +std::size_t SizeOf(const bmw::mw::log::LogRawBuffer t) +{ + return static_cast(t.size()); +} + +template +bool WillMessageFit(bmw::mw::log::detail::VerbosePayload& payload, T... message_parts) +{ + std::size_t size{}; + size = Sum(SizeOf(message_parts)...); + + return !payload.WillOverflow(size); +} + +using ByteView = amp::span; + +template +ByteView ToByteView(const T& t) +{ + // Yes, this is bad, but its what we want. We want to store the current plain memory + // byte by byte in our buffer. Thus, this reinterpret cast is the only correct way to-do this. + + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) justified above + // reinterpret_cast needed to store current plain memory in logs + // + return ByteView{reinterpret_cast(&t), sizeof(t)}; + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) justified above + +} + +ByteView ToByteView(const amp::string_view& t) +{ + return ByteView{t.data(), static_cast(t.size())}; +} + +ByteView ToByteView(const bmw::mw::log::LogRawBuffer& t) +{ + return ByteView{t.data(), t.size()}; +} + +template +void DoFor(F function, First first, Rest... rest) +{ + function(ToByteView(first)); + + DoFor(function, rest...); +} + + +// coverage: coverage shown by manual inspection. +// Reasoning: empty function doesn't produce any code to be covered. +// See also: +template +void DoFor(F) +{ + // Parameter pack is empty. +} + + +template +bmw::mw::log::detail::AddArgumentResult Store(bmw::mw::log::detail::VerbosePayload& payload, T... data_for_payload) +{ + if (WillMessageFit(payload, data_for_payload...) == true) + { + + /* The lambda will be executed within this stack. Thus, all references are still valid */ + DoFor( + [&payload](const ByteView byte_view) { + payload.Put(byte_view.data(), static_cast(byte_view.size())); + }, + data_for_payload...); + + return bmw::mw::log::detail::AddArgumentResult::Added; + } + return bmw::mw::log::detail::AddArgumentResult::NotAdded; +} + +bmw::mw::log::detail::AddArgumentResult TryStore(bmw::mw::log::detail::VerbosePayload& payload, + const TypeInfo& type_info, + const std::uint64_t max_string_len_incl_null, + const amp::string_view data) noexcept +{ + const std::uint64_t max_string_len = max_string_len_incl_null - std::size_t{1U}; + const auto length_cropped = static_cast(std::min(data.size(), max_string_len)); + const auto length_incl_null = static_cast(length_cropped + 1U); + + const amp::string_view data_cropped{data.data(), length_cropped}; + return Store(payload, type_info, length_incl_null, data_cropped, '\0'); +} + +template +bmw::mw::log::detail::AddArgumentResult LogData(bmw::mw::log::detail::VerbosePayload& payload, + const Resolution data, + const bmw::mw::log::detail::IntegerRepresentation repr, + const std::uint32_t type, + TypeLength type_length) noexcept +{ + // \Requirement PRS_Dlt_00386, PRS_Dlt_00356, PRS_Dlt_00358 + TypeInfo type_info(type); + if ((type_info.Set(type_length).has_value() == false) || (type_info.Set(repr).has_value() == false)) + { + return bmw::mw::log::detail::AddArgumentResult::NotAdded; + } + + // \Requirement PRS_Dlt_00370 + return Store(payload, type_info, data); +} +} // namespace + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, const bool data) noexcept +{ + // \Requirement PRS_Dlt_00139 + TypeInfo type_info(TypeInfo::TYPE_BOOL_BIT); + if (type_info.Set(TypeLength::k8Bit).has_value() == false) + { + return AddArgumentResult::NotAdded; + } + + // \Requirement PRS_Dlt_00422 + + static_assert((sizeof(data) == std::size_t{1U}) == true, "Bool is not of size one Byte"); + + + // \Requirement PRS_Dlt_00369, PRS_Dlt_00423 + return Store(payload, type_info, data); +} + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, + const std::uint8_t data, + const IntegerRepresentation repr) noexcept +{ + return LogData(payload, data, repr, TypeInfo::TYPE_UNSIGNED_BIT, TypeLength::k8Bit); +} + + + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, + const std::uint16_t data, + const IntegerRepresentation repr) noexcept +{ + return LogData(payload, data, repr, TypeInfo::TYPE_UNSIGNED_BIT, TypeLength::k16Bit); +} + + + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, + const std::uint32_t data, + const IntegerRepresentation repr) noexcept +{ + return LogData(payload, data, repr, TypeInfo::TYPE_UNSIGNED_BIT, TypeLength::k32Bit); +} + + + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, + const std::uint64_t data, + const IntegerRepresentation repr) noexcept +{ + return LogData(payload, data, repr, TypeInfo::TYPE_UNSIGNED_BIT, TypeLength::k64Bit); +} + + + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, + const std::int8_t data, + const IntegerRepresentation repr) noexcept +{ + return LogData(payload, data, repr, TypeInfo::TYPE_SIGNED_BIT, TypeLength::k8Bit); +} + + + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, + const std::int16_t data, + const IntegerRepresentation repr) noexcept +{ + return LogData(payload, data, repr, TypeInfo::TYPE_SIGNED_BIT, TypeLength::k16Bit); +} + + + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, + const std::int32_t data, + const IntegerRepresentation repr) noexcept +{ + return LogData(payload, data, repr, TypeInfo::TYPE_SIGNED_BIT, TypeLength::k32Bit); +} + + + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, + const std::int64_t data, + const IntegerRepresentation repr) noexcept +{ + return LogData(payload, data, repr, TypeInfo::TYPE_SIGNED_BIT, TypeLength::k64Bit); +} + + + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, const LogHex8 data, const IntegerRepresentation repr) noexcept +{ + return LogData(payload, data, repr, TypeInfo::TYPE_UNSIGNED_BIT, TypeLength::k8Bit); +} + + + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, + const LogHex16 data, + const IntegerRepresentation repr) noexcept +{ + return LogData(payload, data, repr, TypeInfo::TYPE_UNSIGNED_BIT, TypeLength::k16Bit); +} + + + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, + const LogHex32 data, + const IntegerRepresentation repr) noexcept +{ + return LogData(payload, data, repr, TypeInfo::TYPE_UNSIGNED_BIT, TypeLength::k32Bit); +} + + + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, + const LogHex64 data, + const IntegerRepresentation repr) noexcept +{ + return LogData(payload, data, repr, TypeInfo::TYPE_UNSIGNED_BIT, TypeLength::k64Bit); +} + + + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, const LogBin8 data, const IntegerRepresentation repr) noexcept +{ + return LogData(payload, data, repr, TypeInfo::TYPE_UNSIGNED_BIT, TypeLength::k8Bit); +} + + + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, + const LogBin16 data, + const IntegerRepresentation repr) noexcept +{ + return LogData(payload, data, repr, TypeInfo::TYPE_UNSIGNED_BIT, TypeLength::k16Bit); +} + + + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, + const LogBin32 data, + const IntegerRepresentation repr) noexcept +{ + return LogData(payload, data, repr, TypeInfo::TYPE_UNSIGNED_BIT, TypeLength::k32Bit); +} + + + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, + const LogBin64 data, + const IntegerRepresentation repr) noexcept +{ + return LogData(payload, data, repr, TypeInfo::TYPE_UNSIGNED_BIT, TypeLength::k64Bit); +} + + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, const float data) noexcept +{ + // \Requirement PRS_Dlt_00390, PRS_Dlt_00145 + TypeInfo type_info(TypeInfo::TYPE_FLOAT_BIT); + if (type_info.Set(TypeLength::k32Bit).has_value() == false) + { + return AddArgumentResult::NotAdded; + } + + // \Requirement PRS_Dlt_00371 + return Store(payload, type_info, data); +} + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, const double data) noexcept +{ + // \Requirement PRS_Dlt_00386, PRS_Dlt_00356 + TypeInfo type_info(TypeInfo::TYPE_FLOAT_BIT); + if (type_info.Set(TypeLength::k64Bit).has_value() == false) + { + return AddArgumentResult::NotAdded; + } + + // \Requirement PRS_Dlt_00371 + return Store(payload, type_info, data); +} + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, const amp::string_view data) noexcept +{ + // \Requirement PRS_Dlt_00420, PRS_Dlt_00155 + TypeInfo type_info(TypeInfo::TYPE_STRING_BIT); + if (type_info.Set(StringEncoding::kUTF8).has_value() == false) + { + return AddArgumentResult::NotAdded; + } + + // \Requirement PRS_Dlt_00156, PRS_Dlt_00373 + // The string payload shall be assembled as follows: + // _____________________________________________ + // |16-bit Length including termination character| + // |_____________________________________________| + // |Encoded data string length < 2^16 bytes | + // |_____________________________________________| + // | Zero terminator 1 byte | + // |_____________________________________________| + // Note that in pratice the string must be even shorter as the entire DLT message must fit in max 2^16 bytes + // including the DLT headers. + + // Number of bytes needed for the header needed before the payload. + constexpr std::uint64_t header_size = sizeof(type_info) + sizeof(std::uint16_t); + + if (payload.RemainingCapacity() <= header_size) + { + // No space left in buffer for payload. + return AddArgumentResult::NotAdded; + } + + // Calculate the length including the terminator. + constexpr auto uint16_max = static_cast(std::numeric_limits::max()); + const std::uint64_t max_string_len_incl_null = std::min(payload.RemainingCapacity() - header_size, uint16_max); + + if (max_string_len_incl_null == 0) // LCOV_EXCL_BR_LINE: the condition should not be "true" according to the below + // comments. Excluded from branch coverage. + { + + /* Tolerated by decision. The part of the code, which should never be reached. Excluded from code coverage as + * well. */ + + /* Tolerated by decision. The part of the code, which should never be reached. Excluded from code coverage as + * well. */ + std::abort(); // LCOV_EXCL_LINE defensive programming: Only defined ParsingPhase values are possible to be + // reached. + + /* Tolerated by decision. The part of the code, which should never be reached. Excluded from code coverage as + * well. */ + + /* Tolerated by decision. The part of the code, which should never be reached. Excluded from code coverage as + * well. */ + } + + return TryStore(payload, type_info, max_string_len_incl_null, data); +} + +AddArgumentResult DLTFormat::Log(VerbosePayload& payload, const LogRawBuffer data) noexcept +{ + // \Requirement PRS_Dlt_00625 + const auto type_info = TypeInfo(TypeInfo::TYPE_RAW_BIT); + + // \Requirement PRS_Dlt_00160, PRS_Dlt_00374 + // The string payload shall be assembled as follows: + // _____________________________________________ + // |16-bit Length | + // |_____________________________________________| + // |Data string length <= 2^16 bytes | + // |_____________________________________________| + // Note that in pratice the data must be even shorter as the entire DLT message must fit in max 2^16 bytes + // including the DLT headers. + + // Number of bytes needed for the header needed before the payload. + constexpr std::uint64_t header_size = sizeof(type_info) + sizeof(std::uint16_t); + + if (payload.RemainingCapacity() <= header_size) + { + // No space left in buffer for payload. + return AddArgumentResult::NotAdded; + } + + // Calculate the possibly cropped length + constexpr auto uint16_max = static_cast(std::numeric_limits::max()); + const std::uint64_t max_length = std::min(payload.RemainingCapacity() - header_size, uint16_max); + const std::uint16_t length_cropped = + static_cast(std::min(static_cast(data.size()), max_length)); + + const LogRawBuffer data_cropped{data.data(), length_cropped}; + return Store(payload, type_info, length_cropped, data_cropped); +} + + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/dlt_format.h b/mw/log/detail/dlt_format.h new file mode 100644 index 0000000..10348f1 --- /dev/null +++ b/mw/log/detail/dlt_format.h @@ -0,0 +1,99 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_DLT_FORMAT_H +#define PLATFORM_AAS_MW_LOG_DETAIL_DLT_FORMAT_H + +#include "platform/aas/mw/log/detail/add_argument_result.h" +#include "platform/aas/mw/log/detail/integer_representation.h" +#include "platform/aas/mw/log/detail/verbose_payload.h" +#include "platform/aas/mw/log/log_types.h" + +#include "amp_string_view.hpp" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +class DLTFormat +{ + public: + static AddArgumentResult Log(VerbosePayload&, const bool) noexcept; + static AddArgumentResult Log(VerbosePayload&, + const std::uint8_t, + const IntegerRepresentation = IntegerRepresentation::kDecimal) noexcept; + static AddArgumentResult Log(VerbosePayload&, + const std::uint16_t, + const IntegerRepresentation = IntegerRepresentation::kDecimal) noexcept; + static AddArgumentResult Log(VerbosePayload&, + const std::uint32_t, + const IntegerRepresentation = IntegerRepresentation::kDecimal) noexcept; + static AddArgumentResult Log(VerbosePayload&, + const std::uint64_t, + const IntegerRepresentation = IntegerRepresentation::kDecimal) noexcept; + static AddArgumentResult Log(VerbosePayload&, + const std::int8_t, + const IntegerRepresentation = IntegerRepresentation::kDecimal) noexcept; + static AddArgumentResult Log(VerbosePayload&, + const std::int16_t, + const IntegerRepresentation = IntegerRepresentation::kDecimal) noexcept; + static AddArgumentResult Log(VerbosePayload&, + const std::int32_t, + const IntegerRepresentation = IntegerRepresentation::kDecimal) noexcept; + static AddArgumentResult Log(VerbosePayload&, + const std::int64_t, + const IntegerRepresentation = IntegerRepresentation::kDecimal) noexcept; + static AddArgumentResult Log(VerbosePayload&, + const LogHex8, + const IntegerRepresentation = IntegerRepresentation::kHex) noexcept; + static AddArgumentResult Log(VerbosePayload&, + const LogHex16, + const IntegerRepresentation = IntegerRepresentation::kHex) noexcept; + static AddArgumentResult Log(VerbosePayload&, + const LogHex32, + const IntegerRepresentation = IntegerRepresentation::kHex) noexcept; + static AddArgumentResult Log(VerbosePayload&, + const LogHex64, + const IntegerRepresentation = IntegerRepresentation::kHex) noexcept; + static AddArgumentResult Log(VerbosePayload&, + const LogBin8, + const IntegerRepresentation = IntegerRepresentation::kBinary) noexcept; + static AddArgumentResult Log(VerbosePayload&, + const LogBin16, + const IntegerRepresentation = IntegerRepresentation::kBinary) noexcept; + static AddArgumentResult Log(VerbosePayload&, + const LogBin32, + const IntegerRepresentation = IntegerRepresentation::kBinary) noexcept; + static AddArgumentResult Log(VerbosePayload&, + const LogBin64, + const IntegerRepresentation = IntegerRepresentation::kBinary) noexcept; + static AddArgumentResult Log(VerbosePayload&, const float) noexcept; + static AddArgumentResult Log(VerbosePayload&, const double) noexcept; + static AddArgumentResult Log(VerbosePayload&, const amp::string_view) noexcept; + static AddArgumentResult Log(VerbosePayload&, const LogRawBuffer) noexcept; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_DLT_FORMAT_H diff --git a/mw/log/detail/dlt_format_test.cpp b/mw/log/detail/dlt_format_test.cpp new file mode 100644 index 0000000..11ba679 --- /dev/null +++ b/mw/log/detail/dlt_format_test.cpp @@ -0,0 +1,888 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/dlt_format.h" + +#include "gtest/gtest.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +class DLTFormatFixture : public ::testing::Test +{ + public: + ByteVector buffer_{}; + VerbosePayload payload_{100, buffer_}; + ByteVector size_two_buffer_{}; + VerbosePayload size_two_payload_{2, size_two_buffer_}; +}; + +TEST_F(DLTFormatFixture, TypeInfomrationForBoolean) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00139, PRS_Dlt_00625"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for boolean value is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, true); + + ASSERT_EQ(buffer_.at(0), '\x11'); + ASSERT_EQ(buffer_.at(1), '\x00'); + ASSERT_EQ(buffer_.at(2), '\x00'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, BooleanValueTrueCorrectlyTransformed) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00422, PRS_Dlt_00423, PRS_Dlt_00369"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies boolean value TRUE is correctly transformed."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, true); + + ASSERT_EQ(buffer_.at(4), '\x01'); +} + +TEST_F(DLTFormatFixture, BooleanValueFalseCorrectlyTransformed) +{ + RecordProperty("ParentRequirement", ", , PRS_Dlt_00423, PRS_Dlt_00369"); + RecordProperty("AutosarRequirement", "PRS_Dlt_00423, PRS_Dlt_00369"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies boolean value FALSE is correctly transformed."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, false); + + ASSERT_EQ(buffer_.at(4), '\x00'); +} + +TEST_F(DLTFormatFixture, TypeInfomrationForUint8) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00386, PRS_Dlt_00356, PRS_Dlt_00358, PRS_Dlt_00370"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for uint8 value is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::uint8_t{42U}); + + ASSERT_EQ(buffer_.at(0), '\x41'); + ASSERT_EQ(buffer_.at(1), '\x00'); + ASSERT_EQ(buffer_.at(2), '\x00'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, Uint8ValueTrueCorrectlyTransformed) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00370"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies uint8 value is correctly transformed."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::uint8_t{0x42}); + + ASSERT_EQ(buffer_.at(4), '\x42'); +} + +TEST_F(DLTFormatFixture, TypeIsNotStoredIfNotWholePayloadFitsIntoBuffer) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00386, PRS_Dlt_00356, PRS_Dlt_00358, PRS_Dlt_00370"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for uint8 value is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given a buffer that is to small to fit TypeInformation + Payload + ByteVector buffer{}; + VerbosePayload payload{3, buffer}; + + // When Logging the type + DLTFormat::Log(payload, std::uint8_t{42U}); + + // Then nothing is stored in the buffer (since it would have overflowed) + ASSERT_EQ(buffer.size(), 0U); +} + +TEST_F(DLTFormatFixture, TypeInfomrationForUint16) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00386, PRS_Dlt_00356, PRS_Dlt_00358, PRS_Dlt_00370"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for uint16 value is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::uint16_t{0xABCD}); + + ASSERT_EQ(buffer_.at(0), '\x42'); + ASSERT_EQ(buffer_.at(1), '\x00'); + ASSERT_EQ(buffer_.at(2), '\x00'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, Uint16ValueTrueCorrectlyTransformed) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00370"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies uint16 value is correctly transformed."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::uint16_t{0x42AB}); + + ASSERT_EQ(buffer_.at(4), '\xAB'); + ASSERT_EQ(buffer_.at(5), '\x42'); +} + +TEST_F(DLTFormatFixture, TypeInfomrationForUint32) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00386, PRS_Dlt_00356, PRS_Dlt_00358, PRS_Dlt_00370"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for uint32 value is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::uint32_t{0xABCDEF00}); + + ASSERT_EQ(buffer_.at(0), '\x43'); + ASSERT_EQ(buffer_.at(1), '\x00'); + ASSERT_EQ(buffer_.at(2), '\x00'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, Uint32ValueTrueCorrectlyTransformed) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00370"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies uint32 value is correctly transformed."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::uint32_t{0x42ABCDEF}); + + ASSERT_EQ(buffer_.at(4), '\xEF'); + ASSERT_EQ(buffer_.at(5), '\xCD'); + ASSERT_EQ(buffer_.at(6), '\xAB'); + ASSERT_EQ(buffer_.at(7), '\x42'); +} + +TEST_F(DLTFormatFixture, TypeInfomrationForUint64) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00386, PRS_Dlt_00356, PRS_Dlt_00358, PRS_Dlt_00370"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for uint64 value is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::uint64_t{0xABCDEF00ABCDEF00}); + + ASSERT_EQ(buffer_.at(0), '\x44'); + ASSERT_EQ(buffer_.at(1), '\x00'); + ASSERT_EQ(buffer_.at(2), '\x00'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, Uint64ValueTrueCorrectlyTransformed) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00370"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies uint64 value is correctly transformed."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::uint64_t{0x42ABCDEF01020304}); + + ASSERT_EQ(buffer_.at(4), '\x04'); + ASSERT_EQ(buffer_.at(5), '\x03'); + ASSERT_EQ(buffer_.at(6), '\x02'); + ASSERT_EQ(buffer_.at(7), '\x01'); + ASSERT_EQ(buffer_.at(8), '\xEF'); + ASSERT_EQ(buffer_.at(9), '\xCD'); + ASSERT_EQ(buffer_.at(10), '\xAB'); + ASSERT_EQ(buffer_.at(11), '\x42'); +} + +TEST_F(DLTFormatFixture, TypeInformationForInt8) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00386, PRS_Dlt_00356, PRS_Dlt_00358, PRS_Dlt_00370"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for int8 value is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::int8_t{-42}); + + ASSERT_EQ(buffer_.at(0), '\x21'); + ASSERT_EQ(buffer_.at(1), '\x00'); + ASSERT_EQ(buffer_.at(2), '\x00'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, Int8ValueTrueCorrectlyTransformed) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00370"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies int8 value is correctly transformed."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::int8_t{-42}); + + ASSERT_EQ(buffer_.at(4), '\xd6'); +} + +TEST_F(DLTFormatFixture, TypeInformationForInt16) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00386, PRS_Dlt_00356, PRS_Dlt_00358, PRS_Dlt_00370"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for int16 value is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::int16_t{-32768}); + + ASSERT_EQ(buffer_.at(0), '\x22'); + ASSERT_EQ(buffer_.at(1), '\x00'); + ASSERT_EQ(buffer_.at(2), '\x00'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, Int16ValueTrueCorrectlyTransformed) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00370"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies int16 value is correctly transformed."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::int16_t{-32768}); + + ASSERT_EQ(buffer_.at(4), '\x00'); + ASSERT_EQ(buffer_.at(5), '\x80'); +} + +TEST_F(DLTFormatFixture, TypeInformationForInt32) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00386, PRS_Dlt_00356, PRS_Dlt_00358, PRS_Dlt_00370"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for int32 value is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::int32_t{-2147483648}); + + ASSERT_EQ(buffer_.at(0), '\x23'); + ASSERT_EQ(buffer_.at(1), '\x00'); + ASSERT_EQ(buffer_.at(2), '\x00'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, Int32ValueTrueCorrectlyTransformed) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00370"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies int32 value is correctly transformed."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::int32_t{-2147483648}); + + ASSERT_EQ(buffer_.at(4), '\x00'); + ASSERT_EQ(buffer_.at(5), '\x00'); + ASSERT_EQ(buffer_.at(6), '\x00'); + ASSERT_EQ(buffer_.at(7), '\x80'); +} + +TEST_F(DLTFormatFixture, TypeInformationForInt64) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00386, PRS_Dlt_00356, PRS_Dlt_00358, PRS_Dlt_00370"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for int64 value is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::numeric_limits::min()); + + ASSERT_EQ(buffer_.at(0), '\x24'); + ASSERT_EQ(buffer_.at(1), '\x00'); + ASSERT_EQ(buffer_.at(2), '\x00'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, Int64ValueTrueCorrectlyTransformed) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00370"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies int64 value is correctly transformed."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::numeric_limits::min()); + + ASSERT_EQ(buffer_.at(4), '\x00'); + ASSERT_EQ(buffer_.at(5), '\x00'); + ASSERT_EQ(buffer_.at(6), '\x00'); + ASSERT_EQ(buffer_.at(7), '\x00'); + ASSERT_EQ(buffer_.at(8), '\x00'); + ASSERT_EQ(buffer_.at(9), '\x00'); + ASSERT_EQ(buffer_.at(10), '\x00'); + ASSERT_EQ(buffer_.at(11), '\x80'); +} + +TEST_F(DLTFormatFixture, TypeInformationForFloat) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00390, PRS_Dlt_00145, PRS_Dlt_00371"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for float value is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, float{1.0f}); + + ASSERT_EQ(buffer_.at(0), '\x83'); + ASSERT_EQ(buffer_.at(1), '\x00'); + ASSERT_EQ(buffer_.at(2), '\x00'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, FloatValueCorrectlyTransformed) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00371"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies float value is correctly transformed."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, float{1.0f}); + + ASSERT_EQ(buffer_.at(4), '\x00'); + ASSERT_EQ(buffer_.at(5), '\x00'); + ASSERT_EQ(buffer_.at(6), '\x80'); + ASSERT_EQ(buffer_.at(7), '\x3F'); +} + +TEST_F(DLTFormatFixture, TypeInformationForDouble) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00390, PRS_Dlt_00145, PRS_Dlt_00371"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for double value is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, double{1.0}); + + ASSERT_EQ(buffer_.at(0), '\x84'); + ASSERT_EQ(buffer_.at(1), '\x00'); + ASSERT_EQ(buffer_.at(2), '\x00'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, DoubleValueCorrectlyTransformed) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00371"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies double value is correctly transformed."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, double{1.0}); + + ASSERT_EQ(buffer_.at(4), '\x00'); + ASSERT_EQ(buffer_.at(5), '\x00'); + ASSERT_EQ(buffer_.at(6), '\x00'); + ASSERT_EQ(buffer_.at(7), '\x00'); + ASSERT_EQ(buffer_.at(8), '\x00'); + ASSERT_EQ(buffer_.at(9), '\x00'); + ASSERT_EQ(buffer_.at(10), '\xF0'); + ASSERT_EQ(buffer_.at(11), '\x3F'); +} + +TEST_F(DLTFormatFixture, TypeInformationForString) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00420, PRS_Dlt_00155"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for string value is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, amp::string_view{"Hello World"}); + + ASSERT_EQ(buffer_.at(0), '\x00'); + ASSERT_EQ(buffer_.at(1), '\x82'); + ASSERT_EQ(buffer_.at(2), '\x00'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, StringValueCorrectlyTransformed) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00156, PRS_Dlt_00373"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies string value is correctly transformed."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, amp::string_view{"Hello World"}); + + // Length + ASSERT_EQ(buffer_.at(4), '\x0C'); + ASSERT_EQ(buffer_.at(5), '\x00'); + + // String + ASSERT_EQ(buffer_.at(6), 'H'); + ASSERT_EQ(buffer_.at(7), 'e'); + ASSERT_EQ(buffer_.at(8), 'l'); + ASSERT_EQ(buffer_.at(9), 'l'); + ASSERT_EQ(buffer_.at(10), 'o'); + ASSERT_EQ(buffer_.at(11), ' '); + ASSERT_EQ(buffer_.at(12), 'W'); + ASSERT_EQ(buffer_.at(13), 'o'); + ASSERT_EQ(buffer_.at(14), 'r'); + ASSERT_EQ(buffer_.at(15), 'l'); + ASSERT_EQ(buffer_.at(16), 'd'); + ASSERT_EQ(buffer_.at(17), '\x00'); +} + +TEST_F(DLTFormatFixture, StringValueDoesNotFitNullTermination) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00156, PRS_Dlt_00373"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies string value does not fit in buffer shall be cropped."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + buffer_.clear(); + buffer_.shrink_to_fit(); + buffer_.reserve(17); + DLTFormat::Log(payload_, amp::string_view{"Hello World"}); + + // String + ASSERT_EQ(buffer_.at(6), 'H'); + ASSERT_EQ(buffer_.at(7), 'e'); + ASSERT_EQ(buffer_.at(8), 'l'); + ASSERT_EQ(buffer_.at(9), 'l'); + ASSERT_EQ(buffer_.at(10), 'o'); + ASSERT_EQ(buffer_.at(11), ' '); + ASSERT_EQ(buffer_.at(12), 'W'); + ASSERT_EQ(buffer_.at(13), 'o'); + ASSERT_EQ(buffer_.at(14), 'r'); + ASSERT_EQ(buffer_.at(15), 'l'); + ASSERT_EQ(buffer_.at(16), '\x00'); +} + +TEST_F(DLTFormatFixture, TypeInformationForUint8InHex) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00386, PRS_Dlt_00356, PRS_Dlt_00358, PRS_Dlt_00370, PRS_Dlt_00782"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for uint8 value with hex representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::uint8_t{42U}, bmw::mw::log::detail::IntegerRepresentation::kHex); + + ASSERT_EQ(buffer_.at(0), '\x41'); + ASSERT_EQ(buffer_.at(1), '\x00'); + ASSERT_EQ(buffer_.at(2), '\x01'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, TypeInformationForHex8InHex) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for 8 bits value in hex representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, LogHex8{0xFF}, bmw::mw::log::detail::IntegerRepresentation::kHex); + + ASSERT_EQ(buffer_.at(0), '\x41'); + ASSERT_EQ(buffer_.at(1), '\x00'); + ASSERT_EQ(buffer_.at(2), '\x01'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, TypeInformationForHex16InHex) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for 16 bits value in hex representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, LogHex16{0xFFFF}, bmw::mw::log::detail::IntegerRepresentation::kHex); + + ASSERT_EQ(buffer_.at(0), '\x42'); + ASSERT_EQ(buffer_.at(1), '\x00'); + ASSERT_EQ(buffer_.at(2), '\x01'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, TypeInformationForHex32InHex) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for 32 bits value in hex representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, LogHex32{0xFFFFFF}, bmw::mw::log::detail::IntegerRepresentation::kHex); + + ASSERT_EQ(buffer_.at(0), '\x43'); + ASSERT_EQ(buffer_.at(1), '\x00'); + ASSERT_EQ(buffer_.at(2), '\x01'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, TypeInformationForHex64InHex) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for 64 bits value in hex representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, LogHex64{0xFFFFFFFF}, bmw::mw::log::detail::IntegerRepresentation::kHex); + + ASSERT_EQ(buffer_.at(0), '\x44'); + ASSERT_EQ(buffer_.at(1), '\x00'); + ASSERT_EQ(buffer_.at(2), '\x01'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, TypeInformationForBin8InBin) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for 8 bits value in binary representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, LogBin8{0xFF}, bmw::mw::log::detail::IntegerRepresentation::kBinary); + + ASSERT_EQ(buffer_.at(0), '\x41'); + ASSERT_EQ(buffer_.at(1), '\x80'); + ASSERT_EQ(buffer_.at(2), '\x01'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, TypeInformationForBin16InBin) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for 16 bits value in binary representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, LogBin16{0xFFFF}, bmw::mw::log::detail::IntegerRepresentation::kBinary); + + ASSERT_EQ(buffer_.at(0), '\x42'); + ASSERT_EQ(buffer_.at(1), '\x80'); + ASSERT_EQ(buffer_.at(2), '\x01'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, TypeInformationForBin32InBin) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for 32 bits value in binary representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, LogBin32{0xFFFFFF}, bmw::mw::log::detail::IntegerRepresentation::kBinary); + + ASSERT_EQ(buffer_.at(0), '\x43'); + ASSERT_EQ(buffer_.at(1), '\x80'); + ASSERT_EQ(buffer_.at(2), '\x01'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, TypeInformationForBin64InBin) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for 64 bits value in binary representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, LogBin64{0xFFFFFFFF}, bmw::mw::log::detail::IntegerRepresentation::kBinary); + + ASSERT_EQ(buffer_.at(0), '\x44'); + ASSERT_EQ(buffer_.at(1), '\x80'); + ASSERT_EQ(buffer_.at(2), '\x01'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, TypeInformationForUint16InHex) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00386, PRS_Dlt_00356, PRS_Dlt_00358, PRS_Dlt_00370, PRS_Dlt_00782"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for uint16 value in hex representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::uint16_t{0xABCD}, bmw::mw::log::detail::IntegerRepresentation::kHex); + + ASSERT_EQ(buffer_.at(0), '\x42'); + ASSERT_EQ(buffer_.at(1), '\x00'); + ASSERT_EQ(buffer_.at(2), '\x01'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, TypeInformationForUint32InHex) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00386, PRS_Dlt_00356, PRS_Dlt_00358, PRS_Dlt_00370, PRS_Dlt_00782"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for uint32 value in hex representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::uint32_t{0xABCDEF00}, bmw::mw::log::detail::IntegerRepresentation::kHex); + + ASSERT_EQ(buffer_.at(0), '\x43'); + ASSERT_EQ(buffer_.at(1), '\x00'); + ASSERT_EQ(buffer_.at(2), '\x01'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, TypeInformationForUint64InHex) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00386, PRS_Dlt_00356, PRS_Dlt_00358, PRS_Dlt_00370, PRS_Dlt_00782"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for uint64 value in hex representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::uint64_t{0xABCDEF00ABCDEF00}, bmw::mw::log::detail::IntegerRepresentation::kHex); + + ASSERT_EQ(buffer_.at(0), '\x44'); + ASSERT_EQ(buffer_.at(1), '\x00'); + ASSERT_EQ(buffer_.at(2), '\x01'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, TypeInformationUInt8InBinary) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00386, PRS_Dlt_00356, PRS_Dlt_00358, PRS_Dlt_00370, PRS_Dlt_00782"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for uint8 value with bin representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::uint8_t{42U}, bmw::mw::log::detail::IntegerRepresentation::kBinary); + + ASSERT_EQ(buffer_.at(0), '\x41'); + ASSERT_EQ(buffer_.at(1), '\x80'); + ASSERT_EQ(buffer_.at(2), '\x01'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, TypeInformationUint16InBinary) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00386, PRS_Dlt_00356, PRS_Dlt_00358, PRS_Dlt_00370, PRS_Dlt_00782"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for uint16 value in bin representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::uint16_t{0xABCD}, bmw::mw::log::detail::IntegerRepresentation::kBinary); + + ASSERT_EQ(buffer_.at(0), '\x42'); + ASSERT_EQ(buffer_.at(1), '\x80'); + ASSERT_EQ(buffer_.at(2), '\x01'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, TypeInformationUint32InBinary) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00386, PRS_Dlt_00356, PRS_Dlt_00358, PRS_Dlt_00370, PRS_Dlt_00782"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for uint32 value in bin representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::uint32_t{0xABCDEF00}, bmw::mw::log::detail::IntegerRepresentation::kBinary); + + ASSERT_EQ(buffer_.at(0), '\x43'); + ASSERT_EQ(buffer_.at(1), '\x80'); + ASSERT_EQ(buffer_.at(2), '\x01'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, TypeInformationUint64InBinary) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00386, PRS_Dlt_00356, PRS_Dlt_00358, PRS_Dlt_00370, PRS_Dlt_00782"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for uint64 value in bin representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + DLTFormat::Log(payload_, std::uint64_t{0xABCDEF00ABCDEF00}, bmw::mw::log::detail::IntegerRepresentation::kBinary); + + ASSERT_EQ(buffer_.at(0), '\x44'); + ASSERT_EQ(buffer_.at(1), '\x80'); + ASSERT_EQ(buffer_.at(2), '\x01'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, TypeInformationForRaw) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00625"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for raw value is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + std::vector data{{1, 2, 3}}; + DLTFormat::Log(payload_, LogRawBuffer{data.data(), 3}); + + ASSERT_EQ(buffer_.at(0), '\x00'); + ASSERT_EQ(buffer_.at(1), '\x04'); + ASSERT_EQ(buffer_.at(2), '\x00'); + ASSERT_EQ(buffer_.at(3), '\x00'); +} + +TEST_F(DLTFormatFixture, RawValueCorrectlyTransformed) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00160, PRS_Dlt_00374"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies raw value is correctly transformed."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + std::vector data{{1, 2, 3}}; + DLTFormat::Log(payload_, LogRawBuffer{data.data(), 3}); + + // Length + ASSERT_EQ(buffer_.at(4), '\x03'); + ASSERT_EQ(buffer_.at(5), '\x00'); + + // Data + ASSERT_EQ(buffer_.at(6), 1); + ASSERT_EQ(buffer_.at(7), 2); + ASSERT_EQ(buffer_.at(8), 3); +} + +TEST_F(DLTFormatFixture, RawValueDoesNotFitWhole) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00160, PRS_Dlt_00374"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies raw buffer does not fit in buffer shall be cropped."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + buffer_.clear(); + buffer_.shrink_to_fit(); + buffer_.reserve(4 + 2 + 2); + std::vector data{{1, 2, 3}}; + DLTFormat::Log(payload_, LogRawBuffer{data.data(), 3}); + + // Length + ASSERT_EQ(buffer_.at(4), '\x02'); + ASSERT_EQ(buffer_.at(5), '\x00'); + + // String + ASSERT_EQ(buffer_.at(6), 1); + ASSERT_EQ(buffer_.at(7), 2); +} + +TEST_F(DLTFormatFixture, RawValueDoesNotFitAny) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("AutosarRequirement", "PRS_Dlt_00160, PRS_Dlt_00374"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies empty buffer does not fit any byte, shall be skipped."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + buffer_.clear(); + buffer_.shrink_to_fit(); + buffer_.reserve(4 + 2); + std::vector data{{1, 2, 3}}; + DLTFormat::Log(payload_, LogRawBuffer{data.data(), 3}); + + // Length is zero + ASSERT_EQ(buffer_.size(), 0); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/empty_recorder.cpp b/mw/log/detail/empty_recorder.cpp new file mode 100644 index 0000000..19be2ad --- /dev/null +++ b/mw/log/detail/empty_recorder.cpp @@ -0,0 +1,72 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/empty_recorder.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + + + +amp::optional EmptyRecorder::StartRecord(const amp::string_view, const LogLevel) noexcept +{ + return {}; +} + + +void EmptyRecorder::StopRecord(const SlotHandle&) noexcept {} + +void EmptyRecorder::Log(const SlotHandle&, const bool) noexcept {} +void EmptyRecorder::Log(const SlotHandle&, const std::uint8_t) noexcept {} +void EmptyRecorder::Log(const SlotHandle&, const std::int8_t) noexcept {} +void EmptyRecorder::Log(const SlotHandle&, const std::uint16_t) noexcept {} +void EmptyRecorder::Log(const SlotHandle&, const std::int16_t) noexcept {} +void EmptyRecorder::Log(const SlotHandle&, const std::uint32_t) noexcept {} +void EmptyRecorder::Log(const SlotHandle&, const std::int32_t) noexcept {} +void EmptyRecorder::Log(const SlotHandle&, const std::uint64_t) noexcept {} +void EmptyRecorder::Log(const SlotHandle&, const std::int64_t) noexcept {} +void EmptyRecorder::Log(const SlotHandle&, const float) noexcept {} +void EmptyRecorder::Log(const SlotHandle&, const double) noexcept {} +void EmptyRecorder::Log(const SlotHandle&, const amp::string_view) noexcept {} + +void EmptyRecorder::Log(const SlotHandle&, const LogHex8) noexcept {} +void EmptyRecorder::Log(const SlotHandle&, const LogHex16) noexcept {} +void EmptyRecorder::Log(const SlotHandle&, const LogHex32) noexcept {} +void EmptyRecorder::Log(const SlotHandle&, const LogHex64) noexcept {} + +void EmptyRecorder::Log(const SlotHandle&, const LogBin8) noexcept {} +void EmptyRecorder::Log(const SlotHandle&, const LogBin16) noexcept {} +void EmptyRecorder::Log(const SlotHandle&, const LogBin32) noexcept {} +void EmptyRecorder::Log(const SlotHandle&, const LogBin64) noexcept {} + +void EmptyRecorder::Log(const SlotHandle&, const LogRawBuffer) noexcept {} + +void EmptyRecorder::Log(const SlotHandle&, const LogSlog2Message) noexcept {} + +bool EmptyRecorder::IsLogEnabled(const LogLevel&, const amp::string_view) const noexcept +{ + return false; +} + + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/empty_recorder.h b/mw/log/detail/empty_recorder.h new file mode 100644 index 0000000..8d4fe83 --- /dev/null +++ b/mw/log/detail/empty_recorder.h @@ -0,0 +1,72 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_EMPTY_RECORDER_H +#define PLATFORM_AAS_MW_LOG_DETAIL_EMPTY_RECORDER_H + +#include "platform/aas/mw/log/recorder.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +class EmptyRecorder final : public Recorder +{ + public: + amp::optional StartRecord(const amp::string_view context_id, + const LogLevel log_level) noexcept override; + + void StopRecord(const SlotHandle& slot) noexcept override; + + void Log(const SlotHandle&, const bool data) noexcept override; + void Log(const SlotHandle&, const std::uint8_t) noexcept override; + void Log(const SlotHandle&, const std::int8_t) noexcept override; + void Log(const SlotHandle&, const std::uint16_t) noexcept override; + void Log(const SlotHandle&, const std::int16_t) noexcept override; + void Log(const SlotHandle&, const std::uint32_t) noexcept override; + void Log(const SlotHandle&, const std::int32_t) noexcept override; + void Log(const SlotHandle&, const std::uint64_t) noexcept override; + void Log(const SlotHandle&, const std::int64_t) noexcept override; + void Log(const SlotHandle&, const float) noexcept override; + void Log(const SlotHandle&, const double) noexcept override; + void Log(const SlotHandle&, const amp::string_view) noexcept override; + + void Log(const SlotHandle&, const LogHex8) noexcept override; + void Log(const SlotHandle&, const LogHex16) noexcept override; + void Log(const SlotHandle&, const LogHex32) noexcept override; + void Log(const SlotHandle&, const LogHex64) noexcept override; + + void Log(const SlotHandle&, const LogBin8) noexcept override; + void Log(const SlotHandle&, const LogBin16) noexcept override; + void Log(const SlotHandle&, const LogBin32) noexcept override; + void Log(const SlotHandle&, const LogBin64) noexcept override; + + void Log(const SlotHandle&, const LogRawBuffer) noexcept override; + + void Log(const SlotHandle&, const LogSlog2Message) noexcept override; + + bool IsLogEnabled(const LogLevel&, const amp::string_view context) const noexcept override; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_EMPTY_RECORDER_H diff --git a/mw/log/detail/empty_recorder_test.cpp b/mw/log/detail/empty_recorder_test.cpp new file mode 100644 index 0000000..04fb4c5 --- /dev/null +++ b/mw/log/detail/empty_recorder_test.cpp @@ -0,0 +1,331 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/empty_recorder.h" + +#include "gtest/gtest.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +class EmptyRecorderFixture : public ::testing::Test +{ + public: + EmptyRecorder emptyRecorder; +}; + +TEST_F(EmptyRecorderFixture, StartRecord) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "EmptyRecorder contains StartRecord function for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.StartRecord("DFLT", LogLevel::kInfo); +} + +TEST_F(EmptyRecorderFixture, StopRecord) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "EmptyRecorder contains StopRecord function for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.StopRecord(SlotHandle{}); +} + +TEST_F(EmptyRecorderFixture, LogBool) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "EmptyRecorder contains log function which can log boolean for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, bool{}); +} + +TEST_F(EmptyRecorderFixture, LogUint8_t) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "EmptyRecorder contains log function which can log uint8_t for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, std::uint8_t{}); +} + +TEST_F(EmptyRecorderFixture, LogInt8_t) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "EmptyRecorder contains log function which can log int8_t for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, std::int8_t{}); +} + +TEST_F(EmptyRecorderFixture, LogUint16_t) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "EmptyRecorder contains log function which can log uint16_t for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, std::uint16_t{}); +} + +TEST_F(EmptyRecorderFixture, LogInt16_t) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "EmptyRecorder contains log function which can log int16_t for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, std::int16_t{}); +} + +TEST_F(EmptyRecorderFixture, LogUint32_t) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "EmptyRecorder contains log function which can log uint32_t for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, std::uint32_t{}); +} + +TEST_F(EmptyRecorderFixture, LogInt32_t) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "EmptyRecorder contains log function which can log int32_t for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, std::int32_t{}); +} + +TEST_F(EmptyRecorderFixture, LogUint64_t) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "EmptyRecorder contains log function which can log uint64_t for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, std::uint64_t{}); +} + +TEST_F(EmptyRecorderFixture, LogInt64_t) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "EmptyRecorder contains log function which can log int64_t for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, std::int64_t{}); +} + +TEST_F(EmptyRecorderFixture, LogFloat) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "EmptyRecorder contains log function which can log float for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, float{}); +} + +TEST_F(EmptyRecorderFixture, LogDouble) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "EmptyRecorder contains log function which can log double for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, double{}); +} + +TEST_F(EmptyRecorderFixture, LogStringView) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "EmptyRecorder contains log function which can log string view for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, amp::string_view{}); +} + +TEST_F(EmptyRecorderFixture, Log_LogHex8) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "EmptyRecorder contains log function which can log 8 bits with hex represenataton for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, LogHex8{}); +} + +TEST_F(EmptyRecorderFixture, Log_LogHex16) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "EmptyRecorder contains log function which can log 16 bits with hex represenataton for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, LogHex16{}); +} + +TEST_F(EmptyRecorderFixture, Log_LogHex32) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "EmptyRecorder contains log function which can log 32 bits with hex represenataton for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, LogHex32{}); +} + +TEST_F(EmptyRecorderFixture, Log_LogHex64) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "EmptyRecorder contains log function which can log 64 bits with hex represenataton for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, LogHex64{}); +} + +TEST_F(EmptyRecorderFixture, Log_LogBin8) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "EmptyRecorder contains log function which can log 8 bits with bin represenataton for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, LogBin8{}); +} + +TEST_F(EmptyRecorderFixture, Log_LogBin16) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "EmptyRecorder contains log function which can log 16 bits with bin represenataton for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, LogBin16{}); +} + +TEST_F(EmptyRecorderFixture, Log_LogBin32) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "EmptyRecorder contains log function which can log 32 bits with bin represenataton for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, LogBin32{}); +} + +TEST_F(EmptyRecorderFixture, Log_LogBin64) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "EmptyRecorder contains log function which can log 64 bits with bin represenataton for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, LogBin64{}); +} + +TEST_F(EmptyRecorderFixture, Log_LogRawBuffer) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "EmptyRecorder contains log function which can log raw buffer for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, LogRawBuffer{nullptr, 0}); +} + +TEST_F(EmptyRecorderFixture, Log_LogSlog2Message) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "EmptyRecorder contains log function which can log LogSlog2Message for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + emptyRecorder.Log(SlotHandle{}, LogSlog2Message{11, "slog message"}); +} + +TEST_F(EmptyRecorderFixture, IsLogEnabledShallReturnValue) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "EmptyRecorder contains IsLogEnabled for testing purpose."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_FALSE(emptyRecorder.IsLogEnabled(LogLevel::kInfo, amp::string_view{"DFLT"})); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/error.cpp b/mw/log/detail/error.cpp new file mode 100644 index 0000000..77fe81d --- /dev/null +++ b/mw/log/detail/error.cpp @@ -0,0 +1,72 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/error.h" + +amp::string_view bmw::mw::log::detail::ErrorDomain::MessageFor(const bmw::result::ErrorCode& code) const noexcept +{ + // boundary check is done by default case in switch + // + switch (static_cast(code)) + { + case Error::kInvalidLogLevelString: + return "The string does not contain a valid log level."; + case Error::kInvalidLogModeString: + return "The string does not contain a valid log mode."; + case Error::kConfigurationFilesNotFound: + return "No logging configuration files could be found."; + case Error::kConfigurationOptionalJsonKeyNotFound: + return "Configuration key not found in JSON file."; + case Error::kMaximumNumberOfRecordersExceeded: + return "Exceeded the maximum number of active recorders."; + case Error::kRecorderFactoryUnsupportedLogMode: + return "Unsupported LogMode encountered in the RecorderFactory, using EmptyRecorder instead."; + case Error::kNoLogModeSpecified: + return "No log mode in configuration, using EmptyRecorder instead."; + case Error::kReceiverInitializationError: + return "Failed to initialize message passing receiver"; + case Error::kUnlinkSharedMemoryError: + return "Failed to unlink shared memory file. Memory might be leaked."; + case Error::kFailedToSendMessageToDatarouter: + return "Failed to send message to Datarouter. Logging is shutting down."; + case Error::kFailedToSetLoggerThreadName: + return "Failed to set thread name of logger thread"; + case Error::kSetSharedMemoryPermissionsError: + return "Failed to change ownership of shared memory file."; + case Error::kShutdownDuringInitialization: + return "Shutdown was requested during initialization of logging library."; + case Error::kSloggerError: + return "The slogger2 library returned an error."; + case Error::kLogFileCreationFailed: + return "Failed to create the log file."; + case Error::kBlockingTerminationSignalFailed: + return "Failed to block termination signal."; + case Error::kMemoryResourceError: + return "Failed to get memory resource ."; + case Error::kUnknownError: + default: + return "Unknown Error"; + } +} + +namespace +{ +constexpr bmw::mw::log::detail::ErrorDomain mw_log_error_domain; +} + +bmw::result::Error bmw::mw::log::detail::MakeError(const bmw::mw::log::detail::Error code, + const std::string_view user_message) noexcept +{ + return {static_cast(code), mw_log_error_domain, user_message}; +} diff --git a/mw/log/detail/error.h b/mw/log/detail/error.h new file mode 100644 index 0000000..ca7a0f5 --- /dev/null +++ b/mw/log/detail/error.h @@ -0,0 +1,62 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_ERROR_H +#define PLATFORM_AAS_MW_LOG_ERROR_H + +#include "platform/aas/lib/result/error.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +enum class Error : bmw::result::ErrorCode +{ + kUnknownError = 0, + kInvalidLogLevelString, + kInvalidLogModeString, + kConfigurationFilesNotFound, + kConfigurationOptionalJsonKeyNotFound, + kMaximumNumberOfRecordersExceeded, + kRecorderFactoryUnsupportedLogMode, + kNoLogModeSpecified, + kReceiverInitializationError, + kUnlinkSharedMemoryError, + kFailedToSendMessageToDatarouter, + kFailedToSetLoggerThreadName, + kSetSharedMemoryPermissionsError, + kShutdownDuringInitialization, + kSloggerError, + kLogFileCreationFailed, + kBlockingTerminationSignalFailed, + kMemoryResourceError, +}; + +class ErrorDomain final : public bmw::result::ErrorDomain +{ + amp::string_view MessageFor(const bmw::result::ErrorCode& code) const noexcept override; +}; + +bmw::result::Error MakeError(const bmw::mw::log::detail::Error code, const std::string_view user_message = "") noexcept; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw +#endif // PLATFORM_AAS_MW_LOG_ERROR_H diff --git a/mw/log/detail/error_test.cpp b/mw/log/detail/error_test.cpp new file mode 100644 index 0000000..42f09e8 --- /dev/null +++ b/mw/log/detail/error_test.cpp @@ -0,0 +1,72 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/error.h" + +#include "gtest/gtest.h" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +class LogDetailErrorFixture : public ::testing::TestWithParam +{ + public: + bmw::result::Error ReturnError(const Error err) { return MakeError(err, "You did it!"); } +}; + +INSTANTIATE_TEST_SUITE_P(ErrorCodes, + LogDetailErrorFixture, + ::testing::Values(Error::kUnknownError, + Error::kInvalidLogLevelString, + Error::kInvalidLogModeString, + Error::kConfigurationFilesNotFound, + Error::kConfigurationOptionalJsonKeyNotFound, + Error::kMaximumNumberOfRecordersExceeded, + Error::kRecorderFactoryUnsupportedLogMode, + Error::kNoLogModeSpecified, + Error::kReceiverInitializationError, + Error::kUnlinkSharedMemoryError, + Error::kFailedToSendMessageToDatarouter, + Error::kFailedToSetLoggerThreadName, + Error::kSetSharedMemoryPermissionsError, + Error::kShutdownDuringInitialization, + Error::kSloggerError, + Error::kLogFileCreationFailed)); + +TEST_P(LogDetailErrorFixture, EachErrorShallReturnNonEmptyMessage) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of raising the error codes with a specific error message."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + const auto error_code = ReturnError(GetParam()); + EXPECT_GT(error_code.Message().size(), 0); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/file_logging/BUILD b/mw/log/detail/file_logging/BUILD new file mode 100644 index 0000000..6f1fbc1 --- /dev/null +++ b/mw/log/detail/file_logging/BUILD @@ -0,0 +1,237 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//platform/aas/mw:common_features.bzl", "COMPILER_WARNING_FEATURES") +load("//platform/aas/tools/itf/bazel_gen:itf_gen.bzl", "py_unittest_qnx_test") + +cc_library( + name = "file_recorder", + srcs = [ + "dlt_message_builder.cpp", + "dlt_message_builder.h", + "dlt_message_builder_types.h", + "file_recorder.cpp", + ], + hdrs = [ + "file_recorder.h", + "svp_time.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log/detail:__pkg__", + ], + deps = [ + ":file_output_backend", + ":message_builder_interface", + "//platform/aas/lib/os/utils:high_resolution_steady_clock", + "//platform/aas/mw/log:recorder", + "//platform/aas/mw/log:shared_types", + "//platform/aas/mw/log/configuration", + "//platform/aas/mw/log/detail:backend_interface", + "//platform/aas/mw/log/detail:dlt_content_formatting", + "//platform/aas/mw/log/detail:statistics_reporter", + "//third_party/static_reflection_with_serialization_pkg:serializer", + "@amp", + ], +) + +cc_library( + name = "text_recorder", + srcs = [ + "text_message_builder.cpp", + "text_message_builder.h", + "text_recorder.cpp", + ], + hdrs = [ + "text_recorder.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log/detail:__pkg__", + ], + deps = [ + ":file_output_backend", + ":message_builder_interface", + ":text_content_formatting", + "//platform/aas/lib/os/utils:high_resolution_steady_clock", + "//platform/aas/mw/log:recorder", + "//platform/aas/mw/log:shared_types", + "//platform/aas/mw/log/configuration", + "//platform/aas/mw/log/detail:backend_interface", + "//platform/aas/mw/log/detail:dlt_argument_counter", + "@amp", + ], +) + +cc_library( + name = "text_content_formatting", + srcs = [ + "text_format.cpp", + ], + hdrs = [ + "text_format.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + deps = [ + "//platform/aas/lib/memory:string_literal", + "//platform/aas/mw/log:shared_types", + "//platform/aas/mw/log/detail:dlt_argument_counter", + "//platform/aas/mw/log/detail:integer_representation", + "//platform/aas/mw/log/detail:log_data_types", + "@amp", + ], +) + +cc_library( + name = "non_blocking_writer", + srcs = ["non_blocking_writer.cpp"], + hdrs = ["non_blocking_writer.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log/detail:__pkg__", + ], + deps = [ + "//platform/aas/lib/os:unistd", + "//platform/aas/mw/log/detail:types_and_errors", + "@amp", + ], +) + +cc_library( + name = "message_builder_interface", + srcs = [ + "imessage_builder.cpp", + ], + hdrs = [ + "imessage_builder.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log:__pkg__", + ], + deps = [ + "//platform/aas/mw/log/detail:log_data_types", + ], +) + +cc_library( + name = "file_output_backend", + srcs = [ + "file_output_backend.cpp", + "slot_drainer.cpp", + "slot_drainer.h", + ], + hdrs = [ + "file_output_backend.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = ["//platform/aas/mw/log:__pkg__"], + deps = [ + ":message_builder_interface", + ":non_blocking_writer", + "//platform/aas/lib/os:fcntl", + "//platform/aas/mw/log/detail:backend_interface", + "//platform/aas/mw/log/detail:circular_allocator", + "@amp", + ], +) + +cc_test( + name = "unit_test", + srcs = [ + "dlt_message_builder_test.cpp", + "file_output_backend_test.cpp", + "file_recorder_test.cpp", + "slot_drainer_test.cpp", + "text_format_test.cpp", + "text_message_builder_test.cpp", + "text_recorder_test.cpp", + ], + data = [ + ], + features = COMPILER_WARNING_FEATURES + [ + "aborts_upon_exception", + ], + tags = ["unit"], + deps = [ + ":file_output_backend", + ":file_output_backend_mocks", + ":file_recorder", + ":text_recorder", + "//platform/aas/lib/os/mocklib:fcntl_mock", + "//platform/aas/lib/os/mocklib:unistd_mock", + "//platform/aas/lib/os/utils/mocklib:path_mock", + "//platform/aas/mw/log/detail:backend_mock", + "//platform/aas/mw/log/detail:circular_allocator", + "//platform/aas/mw/log/test/console_logging_environment", + "//third_party/googletest:main", + ], +) + +cc_library( + name = "file_output_backend_mocks", + testonly = True, + hdrs = ["mock/message_builder_mock.h"], + features = COMPILER_WARNING_FEATURES, + deps = [ + ":non_blocking_writer", + "//third_party/googletest", + "@amp", + ], +) + +test_suite( + name = "unit_tests", + tests = [ + ":non_blocking_writer_test", + ":unit_test", + ], + visibility = ["//platform/aas/mw/log:__pkg__"], +) + +cc_test( + name = "non_blocking_writer_test", + srcs = ["non_blocking_writer_test.cpp"], + features = COMPILER_WARNING_FEATURES + [ + "aborts_upon_exception", + ], + tags = ["unit"], + deps = [ + ":non_blocking_writer", + "//platform/aas/lib/os/mocklib:unistd_mock", + "//platform/aas/mw/log/test/console_logging_environment", + "//third_party/googletest:main", + ], +) + +py_unittest_qnx_test( + name = "pkg_unit_tests_qnx", + test_cases = [ + ":unit_test", + ], +) + +py_unittest_qnx_test( + name = "unit_tests_qnx", + tags = ["manual"], + test_suites = [ + ":pkg_unit_tests_qnx", + ], + visibility = ["//platform/aas/mw/log:__subpackages__"], +) diff --git a/mw/log/detail/file_logging/dlt_message_builder.cpp b/mw/log/detail/file_logging/dlt_message_builder.cpp new file mode 100644 index 0000000..6692f1f --- /dev/null +++ b/mw/log/detail/file_logging/dlt_message_builder.cpp @@ -0,0 +1,247 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/file_logging/dlt_message_builder.h" + +#include "platform/aas/lib/os/utils/high_resolution_steady_clock.h" +#include "platform/aas/mw/log/detail/dlt_format.h" +#include "platform/aas/mw/log/detail/file_logging/svp_time.h" +#include "platform/aas/mw/log/log_level.h" + +#include "serialization/for_logging.h" +#include "visitor/visit.h" +#include "visitor/visit_as_struct.h" + +#include +#include +#include + +namespace +{ + +void ConstructDltStorageHeader(bmw::mw::log::detail::DltStorageHeader& storagehdr, + const std::uint32_t secs, + const std::int32_t microsecs) noexcept +{ + storagehdr.pattern[0] = std::uint8_t{'D'}; + storagehdr.pattern[1] = std::uint8_t{'L'}; + storagehdr.pattern[2] = std::uint8_t{'T'}; + storagehdr.pattern[3] = 0x01UL; + storagehdr.seconds = secs; + storagehdr.microseconds = microsecs; + storagehdr.ecu[0] = std::uint8_t{'E'}; + storagehdr.ecu[1] = std::uint8_t{'C'}; + storagehdr.ecu[2] = std::uint8_t{'U'}; + storagehdr.ecu[3] = std::uint8_t{'\0'}; +} + +void ConstructDltStandardHeaderExtra(bmw::mw::log::detail::DltStandardHeaderExtra& standard_extra_header, + const bmw::mw::log::detail::LoggingIdentifier& ecu, + const std::uint32_t tmsp) noexcept +{ + static_assert(sizeof(decltype(standard_extra_header.ecu)) == sizeof(decltype(ecu.data_)), + "Types storing logging ID should be the same."); + amp::ignore = std::copy(ecu.data_.begin(), ecu.data_.end(), standard_extra_header.ecu.begin()); + standard_extra_header.tmsp = htonl(tmsp); +} + +void ConstructDltExtendedHeader(bmw::mw::log::detail::DltExtendedHeader& extended_header, + const bmw::mw::log::LogLevel log_level, + const std::uint8_t number_of_arguments, + const bmw::mw::log::detail::LoggingIdentifier& app_id, + const bmw::mw::log::detail::LoggingIdentifier& ctx_id) noexcept +{ + static_assert(sizeof(std::uint32_t) > sizeof(log_level), "Casting to a more capable type expected"); + const std::uint32_t level_normalized = static_cast(log_level) & std::uint32_t{0b111UL}; + const std::uint32_t message_info = (bmw::mw::log::detail::kDltTypeLOG << bmw::mw::log::detail::kDltMsinMstpShift) | + (level_normalized << bmw::mw::log::detail::kDltMsinMtinShift) | + (bmw::mw::log::detail::kDltMsinVerb); + // static_cast operation within uint8_t range + extended_header.msin = static_cast(message_info); + extended_header.noar = number_of_arguments; + static_assert(sizeof(decltype(extended_header.apid)) == sizeof(decltype(app_id.data_)), + "Types storing logging ID should be the same."); + amp::ignore = std::copy(app_id.data_.begin(), app_id.data_.end(), extended_header.apid.begin()); + static_assert(sizeof(decltype(extended_header.ctid)) == sizeof(decltype(ctx_id.data_)), + "Types storing logging ID should be the same."); + amp::ignore = std::copy(ctx_id.data_.begin(), ctx_id.data_.end(), extended_header.ctid.begin()); +} + +template +std::size_t GetBufferSizeCasted(T buffer_size) noexcept +{ + // We only intend to use conversion function with human readable messages + // plus final memory management method will be avoiding dynamic allocation + // which limits maximum buffer size + static_assert(sizeof(T) <= sizeof(std::size_t), "Buffer size conversion error"); + return static_cast(buffer_size); +} + +void ConstructStorageVerbosePacket(bmw::mw::log::detail::VerbosePayload& header_payload, + const bmw::mw::log::detail::LogEntry& entry, + const bmw::mw::log::detail::LoggingIdentifier& ecu, + const std::uint8_t message_count, + const bmw::mw::log::detail::SVPTime& svp_time) noexcept +{ + // truncate the message to max size if the msg size is exceeding the available buffer size + static_assert(bmw::mw::log::detail::kDltMessageSize > + (bmw::mw::log::detail::kDltStorageHeaderSize + bmw::mw::log::detail::kDltHeaderSize), + "DLT constant values causes undefined behavior"); + const std::size_t size = + std::min(entry.payload.size(), + bmw::mw::log::detail::kDltMessageSize - + (bmw::mw::log::detail::kDltStorageHeaderSize + bmw::mw::log::detail::kDltHeaderSize)); + static_assert(bmw::mw::log::detail::kDltMessageSize <= std::numeric_limits::max(), + "Maximum size of DLT message is too big"); + // 'size' is truncated to allocate header without overflowing uint16_t value + const auto header_size = static_cast(bmw::mw::log::detail::kDltHeaderSize + size); + + bmw::mw::log::detail::DltStorageHeader storage_header{}; + ConstructDltStorageHeader(storage_header, svp_time.sec, svp_time.ms); + + + amp::ignore = header_payload.Put([&storage_header](const amp::span destination) { + const auto destination_size = static_cast(destination.size()); + const auto copy_size = std::min(destination_size, sizeof(storage_header)); + // NOLINTNEXTLINE(bmw-banned-function) memcpy is needed here + amp::ignore = std::memcpy(destination.data(), &storage_header, copy_size); + return copy_size; + }); + + + bmw::mw::log::detail::DltVerboseHeader dlt_header{}; + ::bmw::mw::log::detail::ConstructDltStandardHeaderTypes(dlt_header.standard, header_size, message_count, true); + ConstructDltStandardHeaderExtra(dlt_header.extra, ecu, svp_time.timestamp); + + ConstructDltExtendedHeader(dlt_header.extended, entry.log_level, entry.num_of_args, entry.app_id, entry.ctx_id); + + + amp::ignore = header_payload.Put([&dlt_header](const amp::span destination) { + const auto copy_size = std::min(static_cast(destination.size()), sizeof(dlt_header)); + // NOLINTNEXTLINE(bmw-banned-function) memcpy is needed here + amp::ignore = std::memcpy(destination.data(), &dlt_header, copy_size); + return copy_size; + }); + +} + +} // anonymous namespace + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +void ConstructDltStandardHeaderTypes(DltStandardHeader& standard, + const std::uint16_t msg_size, + const std::uint8_t message_count, + const bool use_extended_header) noexcept +{ + // static_cast allowed due to flags values within uint8_t range + standard.htyp = static_cast(kDltHtypWEID | kDltHtypWTMS | kDltHtypVERS); + + if (use_extended_header) + { + standard.htyp |= static_cast(kDltHtypUEH); + } + + standard.mcnt = message_count; + standard.len = htons(msg_size); +} + +using timestamp_t = bmw::os::HighResolutionSteadyClock::time_point; +using systime_t = std::chrono::system_clock::time_point; +using dlt_duration_t = std::chrono::duration>; + +DltMessageBuilder::DltMessageBuilder(const amp::string_view ecu_id) noexcept + : IMessageBuilder(), + header_payload_(kMaxDltHeaderSize, header_memory_), + parsing_phase_{ParsingPhase::kHeader}, + ecu_id_{ecu_id}, + message_count_{0} +{ +} + +void DltMessageBuilder::SetNextMessage(LogRecord& log_record) noexcept +{ + log_record_ = log_record; + + const auto& entry = log_record.getLogEntry(); + const auto time_stamp = timestamp_t::clock::now().time_since_epoch(); + const auto time_epoch = systime_t::clock::now().time_since_epoch(); + + using secs_u32 = std::chrono::duration>; + const std::uint32_t seconds = std::chrono::duration_cast(time_epoch).count(); + const auto secs_remainder = time_epoch - std::chrono::seconds(seconds); + + using microsecs_i32 = std::chrono::duration; + const std::int32_t microsecs = std::chrono::duration_cast(secs_remainder).count(); + const std::uint32_t timestamp = std::chrono::duration_cast(time_stamp).count(); + + ConstructStorageVerbosePacket(header_payload_, + entry, + bmw::mw::log::detail::LoggingIdentifier{ecu_id_.GetStringView()}, + message_count_, + bmw::mw::log::detail::SVPTime{timestamp, seconds, microsecs}); + message_count_++; +} + +amp::optional> DltMessageBuilder::GetNextSpan() noexcept +{ + if (!log_record_.has_value()) + { + return {}; + } + + + detail::VerbosePayload& verbose_payload = log_record_.value().get().getVerbosePayload(); + + switch (parsing_phase_) // LCOV_EXCL_BR_LINE: exclude the "default" branch. + { + case ParsingPhase::kHeader: + parsing_phase_ = ParsingPhase::kPayload; + return header_payload_.GetSpan(); + case ParsingPhase::kPayload: + parsing_phase_ = ParsingPhase::kReinitialize; + return verbose_payload.GetSpan(); + case ParsingPhase::kReinitialize: + parsing_phase_ = ParsingPhase::kHeader; + header_payload_.Reset(); + verbose_payload.Reset(); + log_record_.reset(); + break; + default: // LCOV_EXCL_LINE + + /* Tolerated by decision. The part of the code, which should never be reached. Excluded from code coverage + * as well. */ + + std::abort(); // LCOV_EXCL_LINE defensive programming: Only defined ParsingPhase values are possible to be + // reached. + + /* Tolerated by decision. The part of the code, which should never be reached. Excluded from code coverage + * as well. */ + + break; + } + return {}; +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/file_logging/dlt_message_builder.h b/mw/log/detail/file_logging/dlt_message_builder.h new file mode 100644 index 0000000..a00b503 --- /dev/null +++ b/mw/log/detail/file_logging/dlt_message_builder.h @@ -0,0 +1,63 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_DLT_MESSAGE_BUILDER_H_ +#define PLATFORM_AAS_MW_LOG_DETAIL_DLT_MESSAGE_BUILDER_H_ + +#include "platform/aas/mw/log/detail/file_logging/dlt_message_builder_types.h" +#include "platform/aas/mw/log/detail/file_logging/imessage_builder.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +// This method was kept here in this namespace for testing purposes. +void ConstructDltStandardHeaderTypes(DltStandardHeader& standard, + const std::uint16_t msg_size, + const std::uint8_t message_count, + const bool use_extended_header = false) noexcept; + +class DltMessageBuilder : public IMessageBuilder +{ + public: + explicit DltMessageBuilder(const amp::string_view ecu_id) noexcept; + amp::optional> GetNextSpan() noexcept override; + void SetNextMessage(LogRecord& log_record) noexcept override; + + private: + enum class ParsingPhase : std::uint8_t + { + kHeader = 0, + kPayload, + kReinitialize, + }; + amp::optional> log_record_; + ByteVector header_memory_; + detail::VerbosePayload header_payload_; + ParsingPhase parsing_phase_; + LoggingIdentifier ecu_id_; + std::uint8_t message_count_; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_DLT_MESSAGE_BUILDER_H_ diff --git a/mw/log/detail/file_logging/dlt_message_builder_test.cpp b/mw/log/detail/file_logging/dlt_message_builder_test.cpp new file mode 100644 index 0000000..5f788ef --- /dev/null +++ b/mw/log/detail/file_logging/dlt_message_builder_test.cpp @@ -0,0 +1,199 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/file_logging/dlt_message_builder.h" +#include "platform/aas/mw/log/detail/file_logging/dlt_message_builder_types.h" + +#include "gtest/gtest.h" +#include + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +using ::testing::_; +using ::testing::HasSubstr; +using ::testing::MatchesRegex; +using ::testing::StrEq; + +const std::map levels = { + {0, "off"}, + {1, "fatal"}, + {2, "error"}, + {3, "warn"}, + {4, "info"}, + {5, "debug"}, + {6, "verbose"}, + {7, "undefined"}, +}; + +class DltMessageBuilderFixture : public ::testing::Test +{ + public: + void SetUp() override + { + auto& log_entry = log_record_.getLogEntry(); + + log_entry.app_id = LoggingIdentifier{"TMB"}; + log_entry.ctx_id = LoggingIdentifier{"CTX"}; + log_entry.num_of_args = 7; + log_entry.log_level = LogLevel::kWarn; + log_entry.payload = ByteVector{'p', 'a', 'y', 'l', 'o', 'a', 'd'}; + } + void TearDown() override {} + + protected: + DltMessageBuilder unit_{"XECU"}; + LogRecord log_record_; +}; + +TEST_F(DltMessageBuilderFixture, ShallDepleteAfterHeaderAndPayload) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("AutosarRequirement", "PRS_Dlt_00405"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Dlt will deplete after header and payload."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + unit_.SetNextMessage(log_record_); + const auto first = unit_.GetNextSpan(); + EXPECT_TRUE(first.has_value()); + const auto second = unit_.GetNextSpan(); + EXPECT_TRUE(second.has_value()); + const auto end = unit_.GetNextSpan(); + EXPECT_FALSE(end.has_value()); +} + +TEST_F(DltMessageBuilderFixture, StorageHeaderShallHaveSpecificElements) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("AutosarRequirement", "PRS_Dlt_00405"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Dlt Storage Header format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + unit_.SetNextMessage(log_record_); + + const auto header_span = unit_.GetNextSpan().value(); + DltStorageHeader storage_header{}; + std::memcpy(&storage_header, header_span.data(), sizeof(storage_header)); + + const std::array dlt_pattern{'D', 'L', 'T', '\1'}; + EXPECT_EQ(storage_header.pattern, dlt_pattern); + const std::array dlt_ecu{'E', 'C', 'U', '\0'}; + EXPECT_EQ(storage_header.ecu, dlt_ecu); +} + +TEST_F(DltMessageBuilderFixture, HeaderShallHaveSpecificElements) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("AutosarRequirement", "PRS_Dlt_00458, PRS_Dlt_00319, PRS_Dlt_00094"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Dlt Header format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + unit_.SetNextMessage(log_record_); + + DltVerboseHeader verbose_header{}; + + const auto header_span = unit_.GetNextSpan().value(); + std::memcpy(&verbose_header, header_span.data() + sizeof(DltStorageHeader), sizeof(verbose_header)); + + EXPECT_EQ(verbose_header.standard.len, htons(29UL)); + EXPECT_EQ(verbose_header.standard.mcnt, 0UL); + + const std::array ecu{'X', 'E', 'C', 'U'}; + EXPECT_EQ(verbose_header.extra.ecu, ecu); +} + +TEST_F(DltMessageBuilderFixture, ExtendedHeaderShallHaveSpecificElements) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("AutosarRequirement", "PRS_Dlt_00617"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Dlt Extended Header format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + unit_.SetNextMessage(log_record_); + + DltVerboseHeader verbose_header{}; + + const auto header_span = unit_.GetNextSpan().value(); + std::memcpy(&verbose_header, header_span.data() + sizeof(DltStorageHeader), sizeof(verbose_header)); + + const std::array app_id{'T', 'M', 'B', '\0'}; + EXPECT_EQ(verbose_header.extended.apid, app_id); + const std::array ctx_id{'C', 'T', 'X', '\0'}; + EXPECT_EQ(verbose_header.extended.ctid, ctx_id); +} + +TEST_F(DltMessageBuilderFixture, PayloadShallHaveSetText) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("AutosarRequirement", "PRS_Dlt_00459"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Dlt message format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + unit_.SetNextMessage(log_record_); + + amp::ignore = unit_.GetNextSpan(); + const auto payload_span = unit_.GetNextSpan().value(); + const auto string_content = + std::string(reinterpret_cast(payload_span.data()), static_cast(payload_span.size())); + EXPECT_THAT(string_content, StrEq("payload")); +} + +TEST(DltMessageBuilderFunctionTest, TestDisableDltExtendedHeader) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("AutosarRequirement", "PRS_Dlt_00459"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies DLT standard header types without the exteded header enabled."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + bmw::mw::log::detail::DltVerboseHeader dlt_header; + constexpr std::uint16_t kMessageSize{0}; + constexpr std::uint8_t kMessageCount{0}; + constexpr bool kUseExtendedHeader = false; + + // When disable the extended header. + bmw::mw::log::detail::ConstructDltStandardHeaderTypes( + dlt_header.standard, kMessageSize, kMessageCount, kUseExtendedHeader); + + // The DLT header type should not contain extended header parameter enabled. + constexpr auto kHeaderType = kDltHtypWEID | kDltHtypWTMS | kDltHtypVERS; + EXPECT_EQ(dlt_header.standard.htyp, kHeaderType); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/file_logging/dlt_message_builder_types.h b/mw/log/detail/file_logging/dlt_message_builder_types.h new file mode 100644 index 0000000..a9531fe --- /dev/null +++ b/mw/log/detail/file_logging/dlt_message_builder_types.h @@ -0,0 +1,159 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_DLT_MESSAGE_BUILDER_TYPES_H_ +#define PLATFORM_AAS_MW_LOG_DETAIL_DLT_MESSAGE_BUILDER_TYPES_H_ + +#include +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + + + + +#ifndef PACKED +#define PACKED __attribute__((aligned(1), packed)) +#endif + + + + + +constexpr std::uint32_t kDltMsinVerb{0x01}; /**< verbose */ +constexpr std::uint32_t kDltMsinMstpShift{1}; /**< shift right offset to get mstp value */ +constexpr std::uint32_t kDltMsinMtinShift{4}; /**< shift right offset to get mtin value */ + +/* + * Definitions of the htyp parameter in standard header. + */ +constexpr std::uint32_t kDltHtypUEH{0x01UL}; /**< use extended header */ +constexpr std::uint32_t kDltHtypWEID{0x04UL}; /**< with ECU ID */ +constexpr std::uint32_t kDltHtypWTMS{0x10UL}; /**< with timestamp */ +constexpr std::uint32_t kDltHtypVERS{0x20UL}; /**< version number, 0x1 */ + +static_assert((kDltMsinVerb | kDltHtypUEH | kDltHtypWEID | kDltHtypWTMS | kDltHtypVERS) <= + std::numeric_limits::max(), + "Flag values out of range"); + +// Cast from int to uint32_t to avoid warning about comparison between signed and unsigned and to prevent implicit +// conversion. This cast is safe for below cases because the variable 'digits' is always positive, and the type 'int' is +// mandated by C++ standard. +static_assert(kDltMsinMstpShift < static_cast(std::numeric_limits::digits), + "Shift operator out of range"); +static_assert(kDltMsinMtinShift < static_cast(std::numeric_limits::digits), + "Shift operator out of range"); + +/* + * Definitions of mstp parameter in extended header. + */ +constexpr std::uint32_t kDltTypeLOG{0x00}; /**< Log message type */ + +constexpr std::size_t kDltIdSize = 4UL; +constexpr std::size_t kDltMessageSize = 65535UL; +constexpr std::size_t kDltHeaderSize = 22UL; +constexpr std::size_t kDltStorageHeaderSize = 16UL; + +constexpr std::size_t kMaxDltHeaderSize = 512UL; + + +// NOLINTBEGIN(bmw-banned-preprocessor-directives) needed to use PACKED attribute +#if defined(__GNUC__) +// needed to use PACKED attribute +// +#pragma GCC diagnostic push +// needed to use PACKED attribute +// +#pragma GCC diagnostic ignored "-Wpacked" +// needed to use PACKED attribute +// +#pragma GCC diagnostic ignored "-Wattributes" +#endif +// NOLINTEND(bmw-banned-preprocessor-directives) needed to use PACKED attribute + + +/** + * The structure of the DLT standard header. This header is used in each DLT message. + * Names of the members are following AUTOSAR specification field names. + */ +struct DltStandardHeader +{ + uint8_t htyp; /**< This parameter contains several informations, see definitions below */ + uint8_t mcnt; /**< The message counter is increased with each sent DLT message */ + uint16_t len; /**< Length of the complete message, without storage header */ +} PACKED; +/** + * The structure of the DLT file storage header. This header is used before each stored DLT message. + */ +struct DltStorageHeader +{ + std::array pattern; /**< This pattern should be DLT0x01 */ + uint32_t seconds; /**< seconds since 1.1.1970 */ + int32_t microseconds; /**< Microseconds */ + std::array + ecu; /**< The ECU id is added, if it is not already in the DLT message itself */ +} PACKED; + +/** + * The structure of the DLT extra header parameters. Each parameter is sent only if enabled in htyp. + * Names of the members are following AUTOSAR specification field names. + */ +struct DltStandardHeaderExtra +{ + std::array ecu; /**< ECU id */ + uint32_t tmsp; /**< Timestamp since system start in 0.1 milliseconds */ +} PACKED; + +/** + * The structure of the DLT extended header. This header is only sent if enabled in htyp parameter. + * Names of the members are following AUTOSAR specification field names. + */ +struct DltExtendedHeader +{ + uint8_t msin; /**< messsage info */ + uint8_t noar; /**< number of arguments */ + std::array apid; /**< application id */ + std::array ctid; /**< context id */ +} PACKED; + +struct DltVerboseHeader +{ + DltStandardHeader standard; + DltStandardHeaderExtra extra; + DltExtendedHeader extended; +} PACKED; + + +#if defined(__GNUC__) +// NOLINTBEGIN(bmw-banned-preprocessor-directives) needed to use PACKED attribute +// needed to use PACKED attribute +// +#pragma GCC diagnostic pop +// NOLINTEND(bmw-banned-preprocessor-directives) needed to use PACKED attribute +#endif + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_DLT_MESSAGE_BUILDER_TYPES_H_ diff --git a/mw/log/detail/file_logging/file_output_backend.cpp b/mw/log/detail/file_logging/file_output_backend.cpp new file mode 100644 index 0000000..7069d5d --- /dev/null +++ b/mw/log/detail/file_logging/file_output_backend.cpp @@ -0,0 +1,77 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/file_logging/file_output_backend.h" +#include "platform/aas/mw/log/detail/log_record.h" +#include "platform/aas/mw/log/slot_handle.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +FileOutputBackend::FileOutputBackend(std::unique_ptr message_builder, + const std::int32_t file_descriptor, + std::unique_ptr> allocator, + amp::pmr::unique_ptr fcntl_instance, + amp::pmr::unique_ptr unistd) noexcept + : Backend(), + buffer_allocator_(std::move(allocator)), + slot_drainer_(std::move(message_builder), buffer_allocator_, file_descriptor, std::move(unistd)) +{ + const auto flags = fcntl_instance->fcntl(file_descriptor, bmw::os::Fcntl::Command::kFileGetStatusFlags); + if (flags.has_value()) + { + amp::ignore = fcntl_instance->fcntl( + file_descriptor, + bmw::os::Fcntl::Command::kFileSetStatusFlags, + flags.value() | bmw::os::Fcntl::Open::kNonBlocking | bmw::os::Fcntl::Open::kCloseOnExec); + } +} + +amp::optional FileOutputBackend::ReserveSlot() noexcept +{ + slot_drainer_.Flush(); + const auto slot = buffer_allocator_->AcquireSlotToWrite(); + + if (slot.has_value()) + { + // CircularAllocator has capacity limited by CheckFoxMaxCapacity thus the cast is valid: + return SlotHandle{static_cast(slot.value())}; + } + + return {}; + +} + +void FileOutputBackend::FlushSlot(const SlotHandle& slot) noexcept +{ + slot_drainer_.PushBack(slot); + slot_drainer_.Flush(); +} + +LogRecord& FileOutputBackend::GetLogRecord(const SlotHandle& slot) noexcept +{ + // Cast to bigger integer type: + return buffer_allocator_->GetUnderlyingBufferFor(static_cast(slot.GetSlotOfSelectedRecorder())); +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/file_logging/file_output_backend.h b/mw/log/detail/file_logging/file_output_backend.h new file mode 100644 index 0000000..5e2f128 --- /dev/null +++ b/mw/log/detail/file_logging/file_output_backend.h @@ -0,0 +1,77 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef MW_LOG_DETAIL_FILE_OUTPUT_BACKEND_H_ +#define MW_LOG_DETAIL_FILE_OUTPUT_BACKEND_H_ + +#include "platform/aas/mw/log/detail/backend.h" +#include "platform/aas/mw/log/detail/circular_allocator.h" +#include "platform/aas/mw/log/detail/file_logging/slot_drainer.h" + +#include "platform/aas/lib/os/fcntl_impl.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +// coverage: coverage false positive candidate +// Reasoning: class definition doesn't produce explicitly any code to be covered. +// See also: +class FileOutputBackend final : public Backend +{ + public: + FileOutputBackend(std::unique_ptr message_builder, + const std::int32_t file_descriptor, + std::unique_ptr> allocator, + amp::pmr::unique_ptr fcntl_instance, + amp::pmr::unique_ptr unistd) noexcept; + /// \brief Before a producer can store data in our buffer, he has to reserve a slot. + /// + /// \return SlotHandle if a slot was able to be reserved, empty otherwise. + /// + /// \post This ensures that no other thread will write to the reserved slot until FlushSlot() is invoked. + amp::optional ReserveSlot() noexcept override; + + /// \brief After a producer finished writing into a slot Flush() needs to be called. + /// + /// \param slot The slot that shall be flushed + /// + /// \pre ReserveSlot() was invoked to get a SlotHandle that shall be flushed + /// \post This ensures that afterwards the respective slot can be either read or overwritten + void FlushSlot(const SlotHandle& slot) noexcept override; + + /// \brief In order to stream data into a slot, the underlying slot buffer needs to be exposed. + /// + /// \param slot The slot for which the associated buffer shall be returned + /// \return The payload associated with slot. (Where a producer can add its data) + /// + /// \pre ReserveSlot() was invoked to receive a SlotHandle + LogRecord& GetLogRecord(const SlotHandle& slot) noexcept override; + + private: + std::unique_ptr> buffer_allocator_; + SlotDrainer slot_drainer_; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // MW_LOG_DETAIL_FILE_OUTPUT_BACKEND_H_ diff --git a/mw/log/detail/file_logging/file_output_backend_test.cpp b/mw/log/detail/file_logging/file_output_backend_test.cpp new file mode 100644 index 0000000..a1d11a9 --- /dev/null +++ b/mw/log/detail/file_logging/file_output_backend_test.cpp @@ -0,0 +1,243 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/file_logging/file_output_backend.h" + +#include "platform/aas/mw/log/configuration/configuration.h" +#include "platform/aas/mw/log/detail/circular_allocator.h" +#include "platform/aas/mw/log/detail/error.h" +#include "platform/aas/mw/log/detail/file_logging/mock/message_builder_mock.h" + +#include "platform/aas/lib/os/mocklib/fcntl_mock.h" +#include "platform/aas/lib/os/mocklib/unistdmock.h" + +#include "gtest/gtest.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +using SpanData = amp::span; +using OptionalSpan = amp::optional; +using ::testing::_; +using ::testing::Exactly; +using ::testing::Return; + +class FileOutputBackendFixture : public ::testing::Test +{ + public: + void SetUp() override + { + allocator_ = std::make_unique>(pool_size_); + raw_allocator_ptr_ = allocator_.get(); + + message_builder_mock_ = std::make_unique(); + raw_message_builder_mock_ = message_builder_mock_.get(); + message_builder = std::move(message_builder_mock_); + memory_resource_ = amp::pmr::get_default_resource(); + } + void TearDown() override {} + + private: + protected: + const std::size_t pool_size_ = 4; + const std::uint8_t data_table_[4] = {}; + std::unique_ptr> allocator_ = nullptr; + CircularAllocator* raw_allocator_ptr_ = nullptr; + std::unique_ptr message_builder = nullptr; + std::unique_ptr message_builder_mock_ = nullptr; + mock::MessageBuilderMock* raw_message_builder_mock_ = nullptr; + std::int32_t file_descriptor_ = 23; + amp::pmr::memory_resource* memory_resource_ = nullptr; +}; + +TEST_F(FileOutputBackendFixture, ReserveSlotShouldTriggerFlushing) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "ReserveSlot shall trigger flushing."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + auto fcntl_mock = amp::pmr::make_unique(memory_resource_); + auto unistd_mock = amp::pmr::make_unique(memory_resource_); + FileOutputBackend unit(std::move(message_builder), + file_descriptor_, + std::move(allocator_), + std::move(fcntl_mock), + std::move(unistd_mock)); + + EXPECT_CALL(*raw_message_builder_mock_, GetNextSpan) + .WillOnce(Return(OptionalSpan{})) + .WillOnce(Return(OptionalSpan{})); + + auto slot = unit.ReserveSlot(); + EXPECT_TRUE(slot.has_value()); +} + +TEST_F(FileOutputBackendFixture, FlushSlotShouldTriggerFlushing) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FlushSlot shall trigger flushing."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + auto fcntl_mock = amp::pmr::make_unique(memory_resource_); + auto unistd_mock = amp::pmr::make_unique(memory_resource_); + + const auto& slot_index = allocator_->AcquireSlotToWrite(); + FileOutputBackend unit(std::move(message_builder), + file_descriptor_, + std::move(allocator_), + std::move(fcntl_mock), + std::move(unistd_mock)); + + EXPECT_CALL(*raw_message_builder_mock_, GetNextSpan) + .WillOnce(Return(OptionalSpan{})) // first unitialized + .WillOnce(Return(SpanData(data_table_, sizeof(data_table_)))) // actual data to be written + .WillRepeatedly(Return(OptionalSpan{})); + + EXPECT_CALL(*raw_message_builder_mock_, SetNextMessage(_)).Times(Exactly(1)); + + unit.FlushSlot(SlotHandle{static_cast(slot_index.value())}); +} + +TEST_F(FileOutputBackendFixture, DepletedAllocatorShouldCauseEmptyOptionalReturn) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "ReserveSlot will return None if all allocator's slots are reserved."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + for (std::size_t i = 0; i < pool_size_; i++) + { + const auto& slot = allocator_->AcquireSlotToWrite(); + EXPECT_TRUE(slot.has_value()); + } + + auto fcntl_mock = amp::pmr::make_unique(memory_resource_); + auto unistd_mock = amp::pmr::make_unique(memory_resource_); + + FileOutputBackend unit(std::move(message_builder), + file_descriptor_, + std::move(allocator_), + std::move(fcntl_mock), + std::move(unistd_mock)); + + EXPECT_CALL(*raw_message_builder_mock_, GetNextSpan) + .WillOnce(Return(OptionalSpan{})) // first unitialized + .WillRepeatedly(Return(OptionalSpan{})); + + auto slot = unit.ReserveSlot(); + EXPECT_FALSE(slot.has_value()); +} + +TEST_F(FileOutputBackendFixture, GetLogRecordReturnsObjectSameAsAllocatorWould) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "GetLogRecord can return object same as returned from the allocator."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + auto fcntl_mock = amp::pmr::make_unique(memory_resource_); + auto unistd_mock = amp::pmr::make_unique(memory_resource_); + FileOutputBackend unit(std::move(message_builder), + file_descriptor_, + std::move(allocator_), + std::move(fcntl_mock), + std::move(unistd_mock)); + + EXPECT_CALL(*raw_message_builder_mock_, GetNextSpan).WillRepeatedly(Return(OptionalSpan{})); + const auto slot = unit.ReserveSlot(); + + const auto& object = unit.GetLogRecord(slot.value()); + // Identify as same based on comparing addresses of objects: + EXPECT_EQ(&object, &(raw_allocator_ptr_->GetUnderlyingBufferFor(slot.value().GetSlotOfSelectedRecorder()))); +} + +TEST_F(FileOutputBackendFixture, BackendConstructionShallCallNonBlockingFileSetup) +{ + RecordProperty("Requirement", ", 7"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "File backend construction shall return non blocking file setup. The component shall set the " + "FD_CLOEXEC (or O_CLOEXEC) flag on all the file descriptor it owns"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + bmw::os::Fcntl::Open flags = bmw::os::Fcntl::Open::kReadWrite; + auto fcntl_mock = amp::pmr::make_unique(memory_resource_); + auto unistd_mock = amp::pmr::make_unique(memory_resource_); + auto fcntl_mock_raw_ptr = fcntl_mock.get(); + + // Expect call to Fcntl setting Non-Blocking properties of a file: + EXPECT_CALL(*fcntl_mock_raw_ptr, fcntl(_, bmw::os::Fcntl::Command::kFileGetStatusFlags)) + .Times(1) + .WillOnce(Return(flags)); + EXPECT_CALL(*fcntl_mock_raw_ptr, + fcntl(_, + bmw::os::Fcntl::Command::kFileSetStatusFlags, + flags | bmw::os::Fcntl::Open::kNonBlocking | bmw::os::Fcntl::Open::kCloseOnExec)) + .Times(1); + + // Given construction + FileOutputBackend unit(std::move(message_builder), + file_descriptor_, + std::move(allocator_), + std::move(fcntl_mock), + std::move(unistd_mock)); +} + +TEST_F(FileOutputBackendFixture, MissingFlagsShallSkipCallToSetupFile) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "File backend construction shall not do file setup if there is any missing flag."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + auto fcntl_mock = amp::pmr::make_unique(memory_resource_); + auto unistd_mock = amp::pmr::make_unique(memory_resource_); + auto fcntl_mock_raw_ptr = fcntl_mock.get(); + + // Expect call to Fcntl setting Non-Blocking properties of a file: + EXPECT_CALL(*fcntl_mock_raw_ptr, fcntl(_, bmw::os::Fcntl::Command::kFileGetStatusFlags)) + .Times(1) + .WillOnce(Return(amp::unexpected<::bmw::os::Error>{::bmw::os::Error::createFromErrno(ENOENT)})); + + EXPECT_CALL(*fcntl_mock_raw_ptr, fcntl(_, _, _)).Times(0); + + // Given construction + FileOutputBackend unit(std::move(message_builder), + file_descriptor_, + std::move(allocator_), + std::move(fcntl_mock), + std::move(unistd_mock)); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/file_logging/file_recorder.cpp b/mw/log/detail/file_logging/file_recorder.cpp new file mode 100644 index 0000000..1b284f7 --- /dev/null +++ b/mw/log/detail/file_logging/file_recorder.cpp @@ -0,0 +1,210 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/file_logging/file_recorder.h" +#include "platform/aas/mw/log/detail/dlt_argument_counter.h" +#include "platform/aas/mw/log/detail/dlt_format.h" + +#include "platform/aas/lib/os/fcntl_impl.h" +#include "platform/aas/lib/os/stat.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +namespace +{ + +template +inline void LogData(const SlotHandle& slot, detail::Backend& backend, const T data) noexcept +{ + auto& log_record = backend.GetLogRecord(slot); + DltArgumentCounter counter{log_record.getLogEntry().num_of_args}; + auto& payload = log_record.getVerbosePayload(); + amp::ignore = counter.TryAddArgument( + + [data, &payload]() { + const auto result = DLTFormat::Log(payload, data); + return result; + } + + ); +} + +} // anonymous namespace + + +FileRecorder::FileRecorder(const detail::Configuration& config, std::unique_ptr backend) + : Recorder(), + backend_(std::move(backend)), + config_(config) // LCOV_EXCL_BR_LINE: there are no branches to be covered, it is just pre-construction. +{ +} + + + +amp::optional FileRecorder::StartRecord(const amp::string_view context_id, + const LogLevel log_level) noexcept +{ + if (IsLogEnabled(log_level, context_id) == false) + { + return {}; + } + + auto slot_handle = backend_->ReserveSlot(); + if (slot_handle.has_value()) + { + auto& log_record = backend_->GetLogRecord(slot_handle.value()); + auto& log_entry = log_record.getLogEntry(); + + const auto app_id = config_.GetAppId(); + log_entry.app_id = detail::LoggingIdentifier{app_id}; + log_entry.ctx_id = detail::LoggingIdentifier{context_id}; + log_entry.num_of_args = 0U; + log_entry.log_level = log_level; + log_record.getVerbosePayload().Reset(); + } + return slot_handle; +} + + +void FileRecorder::StopRecord(const SlotHandle& slot) noexcept +{ + backend_->FlushSlot(slot); +} + +void FileRecorder::Log(const SlotHandle& slot, const bool data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const std::uint8_t data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const std::int8_t data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const std::uint16_t data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const std::int16_t data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const std::uint32_t data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const std::int32_t data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const std::uint64_t data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const std::int64_t data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const float data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const double data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const amp::string_view data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const LogHex8 data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const LogHex16 data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const LogHex32 data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const LogHex64 data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const LogBin8 data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const LogBin16 data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const LogBin32 data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const LogBin64 data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const LogRawBuffer data) noexcept +{ + LogData(slot, *backend_, data); +} + +void FileRecorder::Log(const SlotHandle& slot, const LogSlog2Message data) noexcept +{ + LogData(slot, *backend_, data.GetMessage()); +} + +bool FileRecorder::IsLogEnabled(const LogLevel& log_level, const amp::string_view context) const noexcept +{ + return config_.IsLogLevelEnabled(log_level, context); +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/file_logging/file_recorder.h b/mw/log/detail/file_logging/file_recorder.h new file mode 100644 index 0000000..9fdcb5e --- /dev/null +++ b/mw/log/detail/file_logging/file_recorder.h @@ -0,0 +1,80 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_FILE_RECORDER_H +#define PLATFORM_AAS_MW_LOG_DETAIL_FILE_RECORDER_H + +#include "platform/aas/mw/log/configuration/configuration.h" +#include "platform/aas/mw/log/detail/backend.h" +#include "platform/aas/mw/log/detail/statistics_reporter.h" +#include "platform/aas/mw/log/recorder.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +class FileRecorder final : public Recorder +{ + public: + FileRecorder(const detail::Configuration& config, std::unique_ptr backend); + amp::optional StartRecord(const amp::string_view context_id, + const LogLevel log_level) noexcept override; + + void StopRecord(const SlotHandle& slot) noexcept override; + + void Log(const SlotHandle&, const bool data) noexcept override; + void Log(const SlotHandle&, const std::uint8_t) noexcept override; + void Log(const SlotHandle&, const std::int8_t) noexcept override; + void Log(const SlotHandle&, const std::uint16_t) noexcept override; + void Log(const SlotHandle&, const std::int16_t) noexcept override; + void Log(const SlotHandle&, const std::uint32_t) noexcept override; + void Log(const SlotHandle&, const std::int32_t) noexcept override; + void Log(const SlotHandle&, const std::uint64_t) noexcept override; + void Log(const SlotHandle&, const std::int64_t) noexcept override; + void Log(const SlotHandle&, const float) noexcept override; + void Log(const SlotHandle&, const double) noexcept override; + void Log(const SlotHandle&, const amp::string_view) noexcept override; + + void Log(const SlotHandle&, const LogHex8) noexcept override; + void Log(const SlotHandle&, const LogHex16) noexcept override; + void Log(const SlotHandle&, const LogHex32) noexcept override; + void Log(const SlotHandle&, const LogHex64) noexcept override; + + void Log(const SlotHandle&, const LogBin8) noexcept override; + void Log(const SlotHandle&, const LogBin16) noexcept override; + void Log(const SlotHandle&, const LogBin32) noexcept override; + void Log(const SlotHandle&, const LogBin64) noexcept override; + + void Log(const SlotHandle&, const LogRawBuffer) noexcept override; + + void Log(const SlotHandle&, const LogSlog2Message) noexcept override; + + bool IsLogEnabled(const LogLevel&, const amp::string_view context) const noexcept override; + + private: + std::unique_ptr backend_; + detail::Configuration config_; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_FILE_RECORDER_H diff --git a/mw/log/detail/file_logging/file_recorder_test.cpp b/mw/log/detail/file_logging/file_recorder_test.cpp new file mode 100644 index 0000000..a001d42 --- /dev/null +++ b/mw/log/detail/file_logging/file_recorder_test.cpp @@ -0,0 +1,466 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/file_logging/file_recorder.h" + +#include "platform/aas/mw/log/detail/backend_mock.h" + +#include +#include + +#include "gtest/gtest.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +using ::testing::Invoke; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::ReturnRef; + +constexpr LogLevel kActiveLogLevel = LogLevel::kError; +constexpr LogLevel kInActiveLogLevel = LogLevel::kInfo; +static_assert(static_cast>(kActiveLogLevel) < + static_cast>(kInActiveLogLevel), + "Log Level setup for this test makes no sense."); + +class FileRecorderFixtureWithLogLevelCheck : public testing::Test +{ + public: + void SetUp() override + { + ON_CALL(*backend_, ReserveSlot()).WillByDefault(Return(slot_)); + ON_CALL(*backend_, GetLogRecord(slot_)).WillByDefault(ReturnRef(log_record_)); + const ContextLogLevelMap context_log_level_map{{LoggingIdentifier{context_id_}, kActiveLogLevel}}; + config_.SetContextLogLevel(context_log_level_map); + recorder_ = std::make_unique(config_, std::move(backend_)); + } + + void TearDown() override {} + + protected: + std::unique_ptr backend_ = std::make_unique(); + const amp::string_view context_id_ = "DFLT"; + Configuration config_{}; + std::unique_ptr recorder_; + SlotHandle slot_{}; + LogRecord log_record_{}; +}; + +TEST_F(FileRecorderFixtureWithLogLevelCheck, WillObtainSlotForSufficientLogLevel) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies that recorder shall returns sufficient slots if it started recording successfully."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + const auto slot = recorder_->StartRecord(context_id_, kActiveLogLevel); + EXPECT_TRUE(slot.has_value()); +} + +TEST_F(FileRecorderFixtureWithLogLevelCheck, WillObtainEmptySlotForInsufficentLogLevel) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies that recorder can not get slots in case of InActive LogLevel."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + const auto slot = recorder_->StartRecord(context_id_, kInActiveLogLevel); + EXPECT_FALSE(slot.has_value()); +} + +TEST_F(FileRecorderFixtureWithLogLevelCheck, DisablesOrEnablesLogAccordingToLevel) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of enabling or disabling specific log level"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_TRUE(recorder_->IsLogEnabled(kActiveLogLevel, context_id_)); + EXPECT_FALSE(recorder_->IsLogEnabled(kInActiveLogLevel, context_id_)); +} + +TEST_F(FileRecorderFixtureWithLogLevelCheck, WillObtainEmptySlotsWhenNoSlotsReserved) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Recorder shall returns zero slots if no slots were reserved."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + auto backend_mock = std::make_unique(); + ON_CALL(*backend_mock, ReserveSlot()).WillByDefault(Invoke([]() -> amp::optional { return {}; })); + + const auto recorder = std::make_unique(config_, std::move(backend_mock)); + + const auto slot = recorder->StartRecord(context_id_, kActiveLogLevel); + EXPECT_FALSE(slot.has_value()); +} + +class FileRecorderFixture : public ::testing::Test +{ + public: + void SetUp() override + { + EXPECT_CALL(*backend_, ReserveSlot()).WillOnce(Return(slot_)); + EXPECT_CALL(*backend_, FlushSlot(slot_)); + ON_CALL(*backend_, GetLogRecord(slot_)).WillByDefault(ReturnRef(log_record_)); + recorder_ = std::make_unique(config_, std::move(backend_)); + recorder_->StartRecord(context_id_, kActiveLogLevel); + } + + void TearDown() override + { + const auto& log_entry = log_record_.getLogEntry(); + EXPECT_EQ(log_entry.ctx_id.GetStringView(), context_id_); + EXPECT_EQ(log_entry.log_level, log_level_); + EXPECT_EQ(log_entry.num_of_args, expected_number_of_arguments_at_teardown_); + recorder_->StopRecord(slot_); + } + + protected: + std::unique_ptr> backend_ = std::make_unique>(); + SlotHandle slot_{}; + LogRecord log_record_{}; + LogLevel log_level_ = kActiveLogLevel; + const amp::string_view context_id_ = "DFLT"; + Configuration config_{}; + std::unique_ptr recorder_; + std::uint8_t expected_number_of_arguments_at_teardown_{1}; +}; + +TEST_F(FileRecorderFixture, TooManyArgumentsWillYieldTruncatedLog) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "The log will be truncated in case of too many arguments."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + constexpr std::size_t type_info_byte_size_according_to_specification = 4; + const std::size_t number_of_arguments = log_record_.getLogEntry().payload.capacity() / + (type_info_byte_size_according_to_specification + sizeof(std::uint32_t)); + for (std::size_t i = 0; i < number_of_arguments + 5; ++i) + { + recorder_->Log(SlotHandle{}, std::uint32_t{}); + } + EXPECT_LE(number_of_arguments, std::numeric_limits::max()); + expected_number_of_arguments_at_teardown_ = + static_cast(number_of_arguments); +} + +TEST_F(FileRecorderFixture, TooLargeSinglePayloadWillYieldTruncatedLog) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "The log will be truncated in case of too large single payload."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + const std::size_t too_big_data_size = log_record_.getLogEntry().payload.capacity() + 1UL; + std::vector vec(too_big_data_size); + std::fill(vec.begin(), vec.end(), 'o'); + recorder_->Log(SlotHandle{}, amp::string_view{vec.data(), too_big_data_size}); + recorder_->Log(SlotHandle{}, amp::string_view{"xxx"}); + + // Teardown checks if number of arguments is equal to one which means that second argument was ignored due to no + // space left in the buffer +} + +TEST_F(FileRecorderFixture, LogUint8) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log uint8_t."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, std::uint8_t{}); +} + +TEST_F(FileRecorderFixture, LogBool) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log boolean."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + recorder_->Log(SlotHandle{}, bool{}); +} + +TEST_F(FileRecorderFixture, LogInt8) +{ + + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log int8_t."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, std::int8_t{}); +} + +TEST_F(FileRecorderFixture, LogUint16) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log uint16_t."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, std::uint16_t{}); +} + +TEST_F(FileRecorderFixture, LogInt16) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log int16_t"); + RecordProperty("TestingTechnique", "Requirements-based test."); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, std::int16_t{}); +} + +TEST_F(FileRecorderFixture, LogUint32) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log uint32_t."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, std::uint32_t{}); +} + +TEST_F(FileRecorderFixture, LogInt32) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log int32_t."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, std::int32_t{}); +} + +TEST_F(FileRecorderFixture, LogUint64) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log uint64_t."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, std::uint64_t{}); +} + +TEST_F(FileRecorderFixture, LogInt64) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log int64_t."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, std::int64_t{}); +} + +TEST_F(FileRecorderFixture, LogFloat) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log float."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, float{}); +} + +TEST_F(FileRecorderFixture, LogDouble) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log double."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, double{}); +} + +TEST_F(FileRecorderFixture, LogStringView) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log string view."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, amp::string_view{"Hello world"}); +} + +TEST_F(FileRecorderFixture, LogHex8) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log 8-bit values in hex representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, LogHex8{}); +} + +TEST_F(FileRecorderFixture, LogHex16) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log 16-bit values in hex representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, LogHex16{}); +} + +TEST_F(FileRecorderFixture, LogHex32) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log 32-bit values in hex representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, LogHex32{}); +} + +TEST_F(FileRecorderFixture, LogHex64) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log 64-bit values in hex representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, LogHex64{}); +} + +TEST_F(FileRecorderFixture, LogBin8) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log 8-bit values in bin representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, LogBin8{}); +} + +TEST_F(FileRecorderFixture, LogBin16) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log 16-bit values in bin representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, LogBin16{}); +} + +TEST_F(FileRecorderFixture, LogBin32) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log 32-bit values in bin representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, LogBin32{}); +} + +TEST_F(FileRecorderFixture, LogBin64) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log 64-bit values in bin representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, LogBin64{}); +} + +TEST_F(FileRecorderFixture, LogRawBuffer) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log raw buffer."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + recorder_->Log(SlotHandle{}, LogRawBuffer{"raw", 3}); +} + +TEST_F(FileRecorderFixture, LogSlog2Message) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "FileRecorder can log LogSlog2Message."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + recorder_->Log(SlotHandle{}, LogSlog2Message{11, "slog message"}); +} + +TEST(FileRecorderTests, FileRecorderShouldClearSlotOnStart) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Recorder should clean slots before reuse."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Test setup is here since we cannot reuse the fixture here because we have a specific construction use case. + Configuration config{}; + config.SetDefaultLogLevel(kActiveLogLevel); + auto backend = std::make_unique>(); + ON_CALL(*backend, ReserveSlot()).WillByDefault(Return(SlotHandle{})); + LogRecord log_record{}; + ON_CALL(*backend, GetLogRecord(testing::_)).WillByDefault(ReturnRef(log_record)); + auto recorder = std::make_unique(config, std::move(backend)); + + // Simulate the case that a slot already contains data from a previous message. + constexpr auto context = "ctx0"; + recorder->StartRecord(context, kActiveLogLevel); + const auto payload = amp::string_view{"Hello world"}; + recorder->Log(SlotHandle{}, payload); + recorder->StopRecord(SlotHandle{}); + + // Expect that the previous data is cleared. + recorder->StartRecord(context, kActiveLogLevel); + EXPECT_EQ(log_record.getVerbosePayload().GetSpan().size(), 0); + EXPECT_EQ(log_record.getLogEntry().num_of_args, 0); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/file_logging/imessage_builder.cpp b/mw/log/detail/file_logging/imessage_builder.cpp new file mode 100644 index 0000000..31c6685 --- /dev/null +++ b/mw/log/detail/file_logging/imessage_builder.cpp @@ -0,0 +1,33 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/file_logging/imessage_builder.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + + +IMessageBuilder::~IMessageBuilder() noexcept = default; + + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/file_logging/imessage_builder.h b/mw/log/detail/file_logging/imessage_builder.h new file mode 100644 index 0000000..4968850 --- /dev/null +++ b/mw/log/detail/file_logging/imessage_builder.h @@ -0,0 +1,55 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef MW_LOG_DETAIL_IMESSAGE_BUILDER_H_ +#define MW_LOG_DETAIL_IMESSAGE_BUILDER_H_ + +#include "platform/aas/mw/log/detail/log_record.h" + +#include "amp_optional.hpp" +#include "amp_span.hpp" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +class IMessageBuilder +{ + public: + IMessageBuilder() noexcept = default; + IMessageBuilder(IMessageBuilder&&) noexcept = delete; + IMessageBuilder(const IMessageBuilder&) noexcept = delete; + IMessageBuilder& operator=(IMessageBuilder&&) noexcept = delete; + IMessageBuilder& operator=(const IMessageBuilder&) noexcept = delete; + + /// \brief Get next span for consecutive memory area for next part of message that is getting serialized + /// \details Specific implementations may build and split message into different number of spans + virtual amp::optional> GetNextSpan() noexcept = 0; + /// \brief Set data for building next message + /// \details Build header and payload from data contained in LogRecorder + virtual void SetNextMessage(LogRecord&) noexcept = 0; + virtual ~IMessageBuilder() noexcept; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // MW_LOG_DETAIL_IMESSAGE_BUILDER_H_ diff --git a/mw/log/detail/file_logging/mock/message_builder_mock.h b/mw/log/detail/file_logging/mock/message_builder_mock.h new file mode 100644 index 0000000..3400d6e --- /dev/null +++ b/mw/log/detail/file_logging/mock/message_builder_mock.h @@ -0,0 +1,50 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef _MW_LOG_DETAIL_MOCK_MESSAGE_BUILDER_H_ +#define _MW_LOG_DETAIL_MOCK_MESSAGE_BUILDER_H_ + +#include "platform/aas/mw/log/detail/file_logging/imessage_builder.h" +#include "platform/aas/mw/log/detail/log_record.h" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace mock +{ + +class MessageBuilderMock : public IMessageBuilder +{ + public: + MessageBuilderMock() = default; + ~MessageBuilderMock() = default; + + MOCK_METHOD((amp::optional>), GetNextSpan, (), (noexcept, override)); + MOCK_METHOD(void, SetNextMessage, (LogRecord&), (noexcept, override)); +}; + +} // namespace mock +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // _MW_LOG_DETAIL_MOCK_MESSAGE_BUILDER_H_ diff --git a/mw/log/detail/file_logging/non_blocking_writer.cpp b/mw/log/detail/file_logging/non_blocking_writer.cpp new file mode 100644 index 0000000..6e47a87 --- /dev/null +++ b/mw/log/detail/file_logging/non_blocking_writer.cpp @@ -0,0 +1,124 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/file_logging/non_blocking_writer.h" + +#if defined(__QNXNTO__) +#include +#endif // __QNXNTO__ + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +std::size_t NonBlockingWriter::GetMaxChunkSize() noexcept +{ + +/// \brief Maximum number of bytes to be flushed in one call. +/// For QNX The Max size of bytes to be written shall be less than SSIZE_MAX - sizeof(io_write_t) +#if defined __QNX__ + constexpr std::size_t kMaxChunkSizeSupportedByOs = static_cast(SSIZE_MAX) - sizeof(io_write_t); +#else + constexpr std::size_t kMaxChunkSizeSupportedByOs = static_cast(SSIZE_MAX); +#endif + + return kMaxChunkSizeSupportedByOs; +} + +NonBlockingWriter::NonBlockingWriter(const int32_t fileHandle, + std::size_t max_chunk_size, + amp::pmr::unique_ptr unistd) noexcept + : unistd_{std::move(unistd)}, + file_handle_(fileHandle), + number_of_flushed_bytes_{0}, + buffer_{nullptr, 0}, + buffer_flushed_{Result::kWouldBlock}, + max_chunk_size_(std::min(max_chunk_size, GetMaxChunkSize())) + +{ +} + +void NonBlockingWriter::SetSpan(const amp::span& buffer) noexcept +{ + buffer_flushed_ = Result::kWouldBlock; + number_of_flushed_bytes_ = 0; + buffer_ = buffer; +} + +amp::expected NonBlockingWriter::FlushIntoFile() noexcept +{ + const auto buffer_size = static_cast(buffer_.size()); + + const uint64_t left_over = buffer_size - number_of_flushed_bytes_; + + const uint64_t bytes_to_write = max_chunk_size_ <= left_over ? max_chunk_size_ : left_over; + + const auto flush_output = InternalFlush(bytes_to_write); + + if (!(flush_output.has_value())) + { + return amp::make_unexpected(bmw::mw::log::detail::Error::kUnknownError); + } + + if (number_of_flushed_bytes_ == buffer_size) + { + buffer_flushed_ = Result::kDone; + } + + return buffer_flushed_; +} + +amp::expected NonBlockingWriter::InternalFlush(const uint64_t size_to_flush) noexcept +{ + const auto buffer_size = static_cast(buffer_.size()); + if (number_of_flushed_bytes_ < buffer_size) + { + amp::expected num_of_bytes_written{}; + + * construction above. */ + + + // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) used on span which is an array + // NOLINTBEGIN(bmw-banned-function) it is among safety headers. + // Needs ptr to access amp::span elements + // + num_of_bytes_written = unistd_->write(file_handle_, &(buffer_.data()[number_of_flushed_bytes_]), size_to_flush); + // NOLINTEND(bmw-banned-function) it is among safety headers. + // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) used on span which is an array + + + + if (!num_of_bytes_written.has_value()) + { + return amp::make_unexpected(num_of_bytes_written.error()); + } + else + { + + number_of_flushed_bytes_ += static_cast(num_of_bytes_written.value()); + + } + } + return number_of_flushed_bytes_; +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/file_logging/non_blocking_writer.h b/mw/log/detail/file_logging/non_blocking_writer.h new file mode 100644 index 0000000..28195ff --- /dev/null +++ b/mw/log/detail/file_logging/non_blocking_writer.h @@ -0,0 +1,80 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_NON_BLOCKING_WRITER_H +#define PLATFORM_AAS_MW_LOG_DETAIL_NON_BLOCKING_WRITER_H + +// Be careful what you include here. Each additional header will be included in logging.h and thus exposed to the user. +// We need to try to keep the includes low to reduce the compile footprint of using this library. +#include "platform/aas/lib/os/unistd.h" +#include "platform/aas/mw/log/detail/error.h" + +#include +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +/// \brief NonBlockingWriter Class to write logs into file. +/// +/// \details this class is using write system call to write log messages into file in non blocking manner. +class NonBlockingWriter final +{ + public: + enum class Result : std::uint8_t + { + kWouldBlock = 0, + kDone, + }; + /// \brief Constructor that accepts fileHandle of the file to flush data into it and max_chunk size controlled by + /// user and have limit to kMaxChunkSizeSupportedByOs. + explicit NonBlockingWriter(const std::int32_t fileHandle, + std::size_t max_chunk_size, + amp::pmr::unique_ptr unistd) noexcept; + + /// \brief method to write buffer contents to the given file handle in non blocking manner with SSIZE_MAX. + /// Returns Result::kDone when all the data has been written + amp::expected FlushIntoFile() noexcept; + + /// \brief Method to Re initialize the current instance of the non blocking writer to be used to flush another span. + void SetSpan(const amp::span& buffer) noexcept; + + static std::size_t GetMaxChunkSize() noexcept; + + private: + amp::expected InternalFlush(const uint64_t size_to_flush) noexcept; + + amp::pmr::unique_ptr unistd_; + + std::int32_t file_handle_; // given file handle to write to it. + uint64_t + number_of_flushed_bytes_; // last written byte location to be used to continue writing in other flush calls. + amp::span buffer_; // the sent buffer to flush data from it to the file. + Result buffer_flushed_; // Internal flag used to raise it once the whole buffer is flushed. + std::size_t max_chunk_size_; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_NON_BLOCKING_WRITER_H diff --git a/mw/log/detail/file_logging/non_blocking_writer_test.cpp b/mw/log/detail/file_logging/non_blocking_writer_test.cpp new file mode 100644 index 0000000..68ae163 --- /dev/null +++ b/mw/log/detail/file_logging/non_blocking_writer_test.cpp @@ -0,0 +1,205 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/file_logging/non_blocking_writer.h" + +#include "platform/aas/lib/os/mocklib/unistdmock.h" + +#include "gtest/gtest.h" +#include +#include + +using ::testing::_; +using ::testing::Return; + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +constexpr std::size_t max_chunk_size = 2048; + +struct NonBlockingWriterTestFixture : ::testing::Test +{ + void SetUp() override + { + auto unistd = amp::pmr::make_unique(amp::pmr::get_default_resource()); + unistd_ = unistd.get(); + writer_ = std::make_unique(kFileDescriptor, max_chunk_size, std::move(unistd)); + // bmw::os::Unistd::set_testing_instance(unistd_); + } + + void TearDown() override + { + writer_.reset(); + unistd_ = nullptr; + } + + protected: + std::unique_ptr writer_; + bmw::os::UnistdMock* unistd_{}; + + std::int32_t kFileDescriptor{}; +}; + +TEST_F(NonBlockingWriterTestFixture, NonBlockingWriterWhenFlushingTwiceMaxChunkSizeShallReturnTrue) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "NonBlockingWrite can flush 2 max chunks size."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + std::array payload{}; + + const amp::span buffer{payload.data(), + static_cast::size_type>(payload.size())}; + + writer_->SetSpan(buffer); + + EXPECT_CALL(*unistd_, write(kFileDescriptor, buffer.data(), max_chunk_size)).WillOnce(Return(max_chunk_size)); + + ASSERT_EQ(NonBlockingWriter::Result::kWouldBlock, writer_->FlushIntoFile().value()); + + EXPECT_CALL(*unistd_, write(kFileDescriptor, &(buffer.data()[max_chunk_size]), max_chunk_size)) + .WillOnce(Return(max_chunk_size)); + + ASSERT_EQ(NonBlockingWriter::Result::kDone, writer_->FlushIntoFile().value()); +} + +TEST_F(NonBlockingWriterTestFixture, + NonBlockingWriterWhenFlushing2DifferentSpansWithDifferentSizesShallReturnOkInLastFlushForEachSpan) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "NonBlockingWrite can flush 2 different spans with different sizes."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + std::array first_payload{}; + + amp::span first_span{ + first_payload.data(), static_cast::size_type>(first_payload.size())}; + + writer_->SetSpan(first_span); + + EXPECT_CALL(*unistd_, write(kFileDescriptor, first_span.data(), max_chunk_size)).WillOnce(Return(max_chunk_size)); + + ASSERT_EQ(NonBlockingWriter::Result::kWouldBlock, writer_->FlushIntoFile().value()); + + EXPECT_CALL(*unistd_, write(kFileDescriptor, &(first_span.data()[max_chunk_size]), max_chunk_size)) + .WillOnce(Return(max_chunk_size)); + + ASSERT_EQ(NonBlockingWriter::Result::kWouldBlock, writer_->FlushIntoFile().value()); + + EXPECT_CALL(*unistd_, write(kFileDescriptor, &(first_span.data()[2 * max_chunk_size]), 3)).WillOnce(Return(3)); + + ASSERT_EQ(NonBlockingWriter::Result::kDone, writer_->FlushIntoFile().value()); + + std::vector second_payload(max_chunk_size, 0); + + amp::span second_span{ + second_payload.data(), static_cast::size_type>(second_payload.size())}; + + writer_->SetSpan(second_span); + + EXPECT_CALL(*unistd_, write(kFileDescriptor, second_span.data(), max_chunk_size)).WillOnce(Return(max_chunk_size)); + + ASSERT_EQ(NonBlockingWriter::Result::kDone, writer_->FlushIntoFile().value()); +} + +TEST_F(NonBlockingWriterTestFixture, NonBlockingWriterShallReturnFalseWhenWriteSysCallFailsWithError_EBADF) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "NonBlockingWrite cannot flush if the system call write fails with error EBADF."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + std::array payload{}; + amp::span buffer{payload.data(), + static_cast::size_type>(payload.size())}; + + writer_->SetSpan(buffer); + + auto error = amp::make_unexpected(bmw::os::Error::createFromErrno(EBADF)); + + EXPECT_CALL(*unistd_, write(kFileDescriptor, buffer.data(), max_chunk_size)).WillOnce(Return(error)); + + ASSERT_EQ(writer_->FlushIntoFile().error(), bmw::mw::log::detail::Error::kUnknownError); +} + +TEST_F(NonBlockingWriterTestFixture, + NonBlockingWriterWhenFlushing2kMaxChunkSizeShallReturnTrueWhenFlushingSpanWithCorrectIndexesToWriteSystemCall) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "NonBlockingWrite can flush 2 max chunk sizes when flushing span with correct indexes to system call write"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + std::array payload{}; + amp::span buffer{payload.data(), + static_cast::size_type>(payload.size())}; + + writer_->SetSpan(buffer); + + EXPECT_CALL(*unistd_, write(kFileDescriptor, buffer.data(), max_chunk_size)).WillOnce(Return(max_chunk_size)); + + ASSERT_EQ(NonBlockingWriter::Result::kWouldBlock, writer_->FlushIntoFile().value()); + + EXPECT_CALL(*unistd_, write(kFileDescriptor, &(buffer.data()[max_chunk_size]), max_chunk_size)) + .WillOnce(Return(max_chunk_size)); + + ASSERT_EQ(NonBlockingWriter::Result::kDone, writer_->FlushIntoFile().value()); +} + +TEST_F(NonBlockingWriterTestFixture, + NonBlockingWriterWhenFlushing1kMaxChunkSizeOnTwoTimesSinceWriteWillReturnHalfOfMaxChunkSizeShallReturnTrue) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "NonBlockingWrite can flush 1 k max chunk size on two different times even if the write returns " + "half of max chunk size"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + std::array payload{}; + + const amp::span buffer{payload.data(), + static_cast::size_type>(payload.size())}; + + writer_->SetSpan(buffer); + + EXPECT_CALL(*unistd_, write(kFileDescriptor, buffer.data(), max_chunk_size)).WillOnce(Return(max_chunk_size / 2)); + + ASSERT_EQ(NonBlockingWriter::Result::kWouldBlock, writer_->FlushIntoFile().value()); + + EXPECT_CALL(*unistd_, write(kFileDescriptor, &(buffer.data()[max_chunk_size / 2]), max_chunk_size / 2)) + .WillOnce(Return(max_chunk_size / 2)); + + ASSERT_EQ(NonBlockingWriter::Result::kDone, writer_->FlushIntoFile().value()); +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/file_logging/slot_drainer.cpp b/mw/log/detail/file_logging/slot_drainer.cpp new file mode 100644 index 0000000..e2b847d --- /dev/null +++ b/mw/log/detail/file_logging/slot_drainer.cpp @@ -0,0 +1,143 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/file_logging/slot_drainer.h" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + + +SlotDrainer::SlotDrainer(std::unique_ptr message_builder, + std::unique_ptr>& allocator, + const std::int32_t file_descriptor, + amp::pmr::unique_ptr unistd, + const std::size_t limit_slots_in_one_cycle) + : allocator_(allocator), + message_builder_(std::move(message_builder)), + non_blocking_writer_(file_descriptor, NonBlockingWriter::GetMaxChunkSize(), std::move(unistd)), + limit_slots_in_one_cycle_(limit_slots_in_one_cycle) +{ +} + + +bool SlotDrainer::MoreSpansAvailableAndLoaded() noexcept +{ + // remaining span flushed, try next span: + const auto span = message_builder_->GetNextSpan(); + if (span.has_value()) + { + non_blocking_writer_.SetSpan(span.value()); + return true; + } + return false; +} + +amp::expected SlotDrainer::TryFlushSpans() noexcept +{ + do + { + // First try to flush remaining data from previous cycle: + const auto status = non_blocking_writer_.FlushIntoFile(); + if (status.has_value()) + { + if (status.value() != NonBlockingWriter::Result::kDone) + { + return FlushResult::kPartiallyProcessed; + } + } + else + { + return amp::make_unexpected(status.error()); + } + } while (MoreSpansAvailableAndLoaded()); + + return FlushResult::kAllDataProcessed; // no more entries +} + +bool SlotDrainer::MoreSlotsAvailableAndLoaded() noexcept +{ + if (circular_buffer_.empty()) + { + return false; + } + auto& reference_to_slot = circular_buffer_.front(); + circular_buffer_.pop_front(); + current_slot_ = reference_to_slot; + + // Casting to more capable integer type: + auto& underlying_data = + allocator_->GetUnderlyingBufferFor(static_cast(reference_to_slot.GetSlotOfSelectedRecorder())); + message_builder_->SetNextMessage(underlying_data); + return true; +} + +amp::expected SlotDrainer::TryFlushSlots() noexcept +{ + std::size_t number_of_processed_slots = 0; + do + { + const auto status = TryFlushSpans(); + if ((!status.has_value()) || (FlushResult::kPartiallyProcessed == status.value())) + { + // error or "would block" -> return error or 'false'. + return status; + } + // slot is flushed, try next one: + if (current_slot_.has_value()) // manually release slot + { + // Casting to more capable integer type: + allocator_->ReleaseSlot(static_cast(current_slot_.value().get().GetSlotOfSelectedRecorder())); + current_slot_.reset(); + } + + if (number_of_processed_slots > limit_slots_in_one_cycle_) + { + return FlushResult::kNumberOfProcessedSlotsExceeded; + } + ++number_of_processed_slots; + } while (MoreSlotsAvailableAndLoaded()); + + return FlushResult::kAllDataProcessed; +} + +void SlotDrainer::PushBack(const SlotHandle& slot) noexcept +{ + const std::lock_guard lock(context_mutex_); + circular_buffer_.push_back(slot); +} + +void SlotDrainer::Flush() noexcept +{ + const std::lock_guard lock(context_mutex_); + amp::ignore = TryFlushSlots(); +} + +SlotDrainer::~SlotDrainer() +{ + // Try to flush residual data: + Flush(); +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/file_logging/slot_drainer.h b/mw/log/detail/file_logging/slot_drainer.h new file mode 100644 index 0000000..f668e42 --- /dev/null +++ b/mw/log/detail/file_logging/slot_drainer.h @@ -0,0 +1,87 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef MW_LOG_DETAILS_SLOT_DRAINER_H_ +#define MW_LOG_DETAILS_SLOT_DRAINER_H_ + +#include "platform/aas/mw/log/detail/circular_allocator.h" +#include "platform/aas/mw/log/detail/file_logging/imessage_builder.h" +#include "platform/aas/mw/log/detail/file_logging/non_blocking_writer.h" +#include "platform/aas/mw/log/detail/log_record.h" +#include "platform/aas/mw/log/slot_handle.h" + +#include +#include + +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +class SlotDrainer +{ + public: + enum class FlushResult : std::uint8_t + { + kAllDataProcessed = 0, + kPartiallyProcessed, + kNumberOfProcessedSlotsExceeded, + }; + SlotDrainer(std::unique_ptr message_builder, + std::unique_ptr>& allocator, + const std::int32_t file_descriptor, + amp::pmr::unique_ptr unistd, + const std::size_t limit_slots_in_one_cycle = 32UL); + + SlotDrainer(SlotDrainer&&) noexcept = delete; + SlotDrainer(const SlotDrainer&) noexcept = delete; + SlotDrainer& operator=(SlotDrainer&&) noexcept = delete; + SlotDrainer& operator=(const SlotDrainer&) noexcept = delete; + + void PushBack(const SlotHandle& slot) noexcept; + void Flush() noexcept; + + ~SlotDrainer(); + + private: + amp::expected TryFlushSlots() noexcept; + amp::expected TryFlushSpans() noexcept; + bool MoreSlotsAvailableAndLoaded() noexcept; + bool MoreSpansAvailableAndLoaded() noexcept; + + std::unique_ptr>& allocator_; + std::unique_ptr message_builder_; + std::mutex context_mutex_; + static constexpr std::size_t kMaxCircularBufferSize = 1024UL; + // TODO: assert size of circular_buffer: + amp::circular_buffer circular_buffer_; + // To manually release resource and to set and reset access: + amp::optional> current_slot_; + NonBlockingWriter non_blocking_writer_; + const std::size_t limit_slots_in_one_cycle_; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // MW_LOG_DETAILS_SLOT_DRAINER_H_ diff --git a/mw/log/detail/file_logging/slot_drainer_test.cpp b/mw/log/detail/file_logging/slot_drainer_test.cpp new file mode 100644 index 0000000..8b11b19 --- /dev/null +++ b/mw/log/detail/file_logging/slot_drainer_test.cpp @@ -0,0 +1,211 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/file_logging/slot_drainer.h" + +#include "platform/aas/lib/os/mocklib/unistdmock.h" +#include "platform/aas/mw/log/detail/error.h" +#include "platform/aas/mw/log/detail/file_logging/mock/message_builder_mock.h" + +#include "gtest/gtest.h" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +using ::testing::_; +using ::testing::Exactly; +using ::testing::Return; + +using SpanData = amp::span; +using OptionalSpan = amp::optional; + +constexpr std::uint32_t kLimitSlotsInOneCycle{32}; + +class SlotDrainerFixture : public ::testing::Test +{ + public: + void SetUp() override + { + unistd_ptr_ = amp::pmr::make_unique(amp::pmr::get_default_resource()); + unistd_mock_ = unistd_ptr_.get(); + + allocator_ = std::make_unique>(pool_size_); + + message_builder_mock_ = std::make_unique(); + + raw_message_builder_mock_ = message_builder_mock_.get(); + message_builder = std::move(message_builder_mock_); + } + void TearDown() override {} + + private: + const std::uint8_t pool_size_ = 8; // arbitrary size of circular allocator + + protected: + const std::uint8_t data_table_[64] = {}; // some memory used in test + + amp::pmr::unique_ptr<::bmw::os::UnistdMock> unistd_ptr_{}; + ::bmw::os::UnistdMock* unistd_mock_{}; + std::unique_ptr message_builder = nullptr; + std::unique_ptr> allocator_ = nullptr; + std::int32_t file_descriptor_ = 23; // random number file descriptor + std::unique_ptr message_builder_mock_ = nullptr; + mock::MessageBuilderMock* raw_message_builder_mock_ = nullptr; +}; + +TEST_F(SlotDrainerFixture, TestOneWriteFileFailurePath) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Writes will fail with IO error in case of failure path."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given write file error + SlotDrainer unit( + std::move(message_builder), allocator_, file_descriptor_, std::move(unistd_ptr_), kLimitSlotsInOneCycle); + + EXPECT_CALL(*raw_message_builder_mock_, GetNextSpan) + .WillOnce(Return(OptionalSpan{})) // first unitialized + .WillOnce( + Return(amp::span(data_table_, sizeof(data_table_)))); // actual data to be written + + EXPECT_CALL(*raw_message_builder_mock_, SetNextMessage(_)).Times(Exactly(1)); + + EXPECT_CALL(*unistd_mock_, write(file_descriptor_, _, _)) + .WillRepeatedly(Return(amp::unexpected(bmw::os::Error::createFromErrno(EIO)))); + + const auto slot = allocator_->AcquireSlotToWrite(); + EXPECT_TRUE(slot.has_value()); + + // Cast is not harmful because CircullarAllocator object size is within range of uint8_t + unit.PushBack(SlotHandle{static_cast(slot.value())}); + unit.Flush(); +} + +TEST_F(SlotDrainerFixture, IncompleteWriteFileShouldMakeFlushSpansReturnWouldBlock) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "If write not completed, the Flush API would wait till flushing is complete."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given write file error + SlotDrainer unit( + std::move(message_builder), allocator_, file_descriptor_, std::move(unistd_ptr_), kLimitSlotsInOneCycle); + + EXPECT_CALL(*raw_message_builder_mock_, GetNextSpan) + .WillOnce(Return(OptionalSpan{})) // first unitialized + .WillOnce( + Return(amp::span(data_table_, sizeof(data_table_)))); // actual data to be written + + EXPECT_CALL(*raw_message_builder_mock_, SetNextMessage(_)).Times(Exactly(1)); + + EXPECT_CALL(*unistd_mock_, write(file_descriptor_, _, _)) + .WillOnce([](int, const void*, size_t) { return 1; }) + .WillOnce([](int, const void*, size_t) { return 1; }); + + const auto slot = allocator_->AcquireSlotToWrite(); + EXPECT_TRUE(slot.has_value()); + + // Cast is not harmful because CircullarAllocator object size is within range of uint8_t + unit.PushBack(SlotHandle{static_cast(slot.value())}); + unit.Flush(); +} + +TEST_F(SlotDrainerFixture, TestOneSlotOneSpan) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Writes shall succeed in case of proper arguments."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given one slot flushed + SlotDrainer unit( + std::move(message_builder), allocator_, file_descriptor_, std::move(unistd_ptr_), kLimitSlotsInOneCycle); + + // Expect sequence of calls to check if any spans from possible previous slots are remaining: + EXPECT_CALL(*raw_message_builder_mock_, GetNextSpan) + .WillOnce(Return(OptionalSpan{})) // first unitialized + .WillOnce( + Return(amp::span(data_table_, sizeof(data_table_)))) // actual data to be written + .WillOnce(Return(OptionalSpan{})) // spans depleted in a slot + .WillOnce(Return(OptionalSpan{})); // spans depleted in a slot + + EXPECT_CALL(*raw_message_builder_mock_, SetNextMessage(_)).Times(Exactly(1)); + + EXPECT_CALL(*unistd_mock_, write(file_descriptor_, data_table_, sizeof(data_table_))) + .WillOnce([](int, const void*, size_t) { return sizeof(data_table_); }); + + const auto slot = allocator_->AcquireSlotToWrite(); + EXPECT_TRUE(slot.has_value()); + + // Cast is not harmful because CircullarAllocator object size is within range of uint8_t + unit.PushBack(SlotHandle{static_cast(slot.value())}); + unit.Flush(); +} + +TEST_F(SlotDrainerFixture, TestTooManySlotsForSingleCallShallNotBeAbleToFlushAllSlots) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Fails to flush all slots in case of exceeding the limit of slots to be processed per one call."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + const std::size_t kLimiNumberOfSlotsProcesssedInOneCall = 2UL; + const std::size_t kNumberOfSlotsQueued = 3UL; + const std::size_t kNumberOfUnflushedSlots = 1UL; + + // Given SlotDrainer set to limit number of slots processed in one call to: + SlotDrainer unit(std::move(message_builder), + allocator_, + file_descriptor_, + std::move(unistd_ptr_), + kLimiNumberOfSlotsProcesssedInOneCall); + + // Given 3 slots queued due to 'would block' returns: + EXPECT_CALL(*raw_message_builder_mock_, GetNextSpan) + .WillOnce(Return(OptionalSpan{})) // first unitialized + .WillRepeatedly(Return(OptionalSpan{})); + + for (std::size_t i = 0; i < kNumberOfSlotsQueued; i++) + { + const auto slot = allocator_->AcquireSlotToWrite(); + EXPECT_TRUE(slot.has_value()); + unit.PushBack(SlotHandle{static_cast(slot.value())}); + } + + const auto slot = allocator_->AcquireSlotToWrite(); + unit.PushBack(SlotHandle{static_cast(slot.value())}); + unit.Flush(); + + // Expectation is that one slot is left unflushed: + EXPECT_EQ(allocator_->GetUsedCount(), kNumberOfUnflushedSlots); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/file_logging/svp_time.h b/mw/log/detail/file_logging/svp_time.h new file mode 100644 index 0000000..73251bc --- /dev/null +++ b/mw/log/detail/file_logging/svp_time.h @@ -0,0 +1,41 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef MW_LOG_DETAILS_SVP_TIME_H_ +#define MW_LOG_DETAILS_SVP_TIME_H_ + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +struct SVPTime +{ + std::uint32_t timestamp; + std::uint32_t sec; + std::int32_t ms; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // MW_LOG_DETAILS_SVP_TIME_H_ diff --git a/mw/log/detail/file_logging/text_format.cpp b/mw/log/detail/file_logging/text_format.cpp new file mode 100644 index 0000000..466645b --- /dev/null +++ b/mw/log/detail/file_logging/text_format.cpp @@ -0,0 +1,593 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/file_logging/text_format.h" + +#include "platform/aas/lib/memory/string_literal.h" + +#include "amp_assert.hpp" +#include "amp_span.hpp" +#include "amp_utility.hpp" + +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +namespace +{ + +constexpr size_t kNumberOfBitsInByte = 8U; +constexpr size_t kTwoNibblesPerByte = 2U; +constexpr size_t kReserveSpaceForSpace = 1U; + +// global variable is unsynchronized and means at least so many hits: +std::size_t unsupported_types_count_hits = 0UL; + + +template +std::size_t GetBufferSizeCasted(T buffer_size) noexcept +{ + // We only intend to use conversion function with human readable messages + // plus final memory management method will be avoiding dynamic allocation + // which limits maximum buffer size + static_assert(sizeof(T) <= sizeof(std::size_t), "Buffer size conversion error"); + return static_cast(buffer_size); +} + +std::size_t GetSpanSizeCasted(const amp::span buffer) noexcept +{ + return GetBufferSizeCasted(buffer.size()); +} + +template +struct GetFormatSpecifier +{ +}; + +// to reuse the same mechanism and not to overcompilicate the code with additional template complexity, +// IntegerRepresentation is still used for float and double types: +template <> +struct GetFormatSpecifier +{ + static constexpr bmw::StringLiteral value = "%f "; +}; + +template <> +struct GetFormatSpecifier +{ + static constexpr bmw::StringLiteral value = "%f "; +}; + +template <> +struct GetFormatSpecifier +{ + static constexpr bmw::StringLiteral value = "%hhx "; +}; + +template <> +struct GetFormatSpecifier +{ + static constexpr bmw::StringLiteral value = "%hho "; +}; + +template <> +struct GetFormatSpecifier +{ + static constexpr bmw::StringLiteral value = "%hhu "; +}; + +template <> +struct GetFormatSpecifier +{ + static constexpr bmw::StringLiteral value = "%hu "; +}; + +template <> +struct GetFormatSpecifier +{ + static constexpr bmw::StringLiteral value = "%hx "; +}; + +template <> +struct GetFormatSpecifier +{ + static constexpr bmw::StringLiteral value = "%ho "; +}; + +template <> +struct GetFormatSpecifier +{ + static constexpr bmw::StringLiteral value = "%u "; +}; + +template <> +struct GetFormatSpecifier +{ + static constexpr bmw::StringLiteral value = "%x "; +}; + +template <> +struct GetFormatSpecifier +{ + static constexpr bmw::StringLiteral value = "%o "; +}; + +template <> +struct GetFormatSpecifier +{ + static constexpr bmw::StringLiteral value = "%lu "; +}; + +template <> +struct GetFormatSpecifier +{ + static constexpr bmw::StringLiteral value = "%lx "; +}; + +template <> +struct GetFormatSpecifier +{ + static constexpr bmw::StringLiteral value = "%lo "; +}; + +template <> +struct GetFormatSpecifier +{ + static constexpr bmw::StringLiteral value = "%hhi "; +}; + +template <> +struct GetFormatSpecifier +{ + static constexpr bmw::StringLiteral value = "%hi "; +}; + +template <> +struct GetFormatSpecifier +{ + static constexpr bmw::StringLiteral value = "%i "; +}; + +template <> +struct GetFormatSpecifier +{ + static constexpr bmw::StringLiteral value = "%li "; +}; + +template +static void PutFormattedNumber(VerbosePayload& payload, const T data) noexcept +{ + amp::ignore = payload.Put([data](const amp::span buffer) noexcept { + const auto buffer_space = GetSpanSizeCasted(buffer); + if (buffer_space > 0) // LCOV_EXCL_BR_LINE: lcov complains about lots of uncovered branches here, it is not + // convenient/related to this condition. + { + constexpr bmw::StringLiteral format = GetFormatSpecifier::value; + const auto written = + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) safe to use std::snprintf + FormattingFunctionReturnCast(std::snprintf(buffer.data(), GetSpanSizeCasted(buffer), format, data)); + + const std::size_t last_index = std::min(written, GetSpanSizeCasted(buffer) - 1U); + + // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) used on span which is an array + // False positive: Pointer arithmetic is used on span which is an array + // + buffer.data()[last_index] = ' '; + // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) used on span which is an array + + return written; + } + else + { + return 0UL; + } + }); +} + +template +struct is_formatting_supported +{ + static constexpr bool value = std::is_unsigned::value; +}; + +void HandleUnsupportedTypes() noexcept +{ + unsupported_types_count_hits++; +} + + +template ::value == false, bool> = true> +void PutBinaryFormattedNumber(VerbosePayload&, const T) noexcept +{ + // Unsupported types action: + HandleUnsupportedTypes(); +} + + +template ::value, bool> = true> +void PutBinaryFormattedNumber(VerbosePayload& payload, const T data) noexcept +{ + constexpr auto characters_used = kNumberOfBitsInByte * sizeof(T) + kReserveSpaceForSpace; + amp::ignore = payload.Put( + [data](const amp::span buffer) noexcept { + const auto buffer_space = GetSpanSizeCasted(buffer); + if (buffer_space > 1U) // LCOV_EXCL_BR_LINE: lcov complains about lots of uncovered branches here, it is + // not convenient/related to this condition. + { + constexpr auto number_of_bits = kNumberOfBitsInByte * sizeof(T); + const auto max_possible = std::min(number_of_bits, buffer_space); + std::size_t buffer_index = 0; + + // LCOV_EXCL_BR_START: The loop condition is always true because buffer_index starts at 0 and + // max_possible is always greater than 1 under this condition if (buffer_space > 1U). And there is no + // way to make it false. Therefore, we can safely suppress the coverage warning for this decision. + while (buffer_index < max_possible) // only MSB bits will be filled in case of insufficient memory + // LCOV_EXCL_BR_STOP + { + const auto bits = std::bitset(data); + // bit [] access can only have 0 or 1 return value and even if offset by value of '0' + // is not able to overflow char + // static_cast is needed to perform addition '0' char value to bool value from + // std::bitset static_cast because this is the type of the content of our + // buffer + + // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) used on span which is an array + // False positive: Pointer arithmetic is used on span which is an array + // + buffer.data()[buffer_index] = static_cast( + static_cast('0') + bits[bits.size() - 1U - buffer_index]); + // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) used on span which is an array + + buffer_index++; + } + + + const std::size_t last_index = buffer_space - 1U; + + // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) used on span which is an array + // False positive: Pointer arithmetic is used on span which is an array + // + buffer.data()[last_index] = ' '; + // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) used on span which is an array + + return buffer_index + kReserveSpaceForSpace; + } + else + { + return 0UL; + } + }, + characters_used); +} +} // namespace + +std::size_t FormattingFunctionReturnCast(const std::int32_t i) noexcept +{ + if (i > 0) + { + return GetBufferSizeCasted(i); + } + return std::size_t{0U}; +} + +void TextFormat::PutFormattedTime(VerbosePayload& payload) noexcept +{ + const auto time_point = std::chrono::system_clock::now(); + amp::ignore = payload.Put([time_point](const amp::span buffer) noexcept { + std::size_t total = 0; + const auto now = std::chrono::system_clock::to_time_t(time_point); + struct tm time_structure_buffer + { + }; + const struct tm* const time_structure = + localtime_r(&now, &time_structure_buffer); // LCOV_EXCL_BR_LINE: there are no branches to be covered. + if (nullptr != time_structure) // LCOV_EXCL_BR_LINE: "nullptr" condition can't be controlled via test case. + { + const std::size_t buffer_space = GetSpanSizeCasted(buffer); + + /* False positive: Pointer arithmetic is used on span which is an array. */ + /* Stdlib time library is used just for formatting headers of logs. It is not critical feature */ + // NOLINTNEXTLINE(bmw-banned-function) justified above + const auto written = std::strftime(buffer.data(), buffer_space, "%Y/%m/%d %H:%M:%S", time_structure); + + + total += written; + if (buffer_space > total) + { + const std::size_t last_index = std::min(written, buffer_space - 1U); + // Cast to ptrdiff_t is intended according to amp::at signature. The variable "buffer" is taken from + // amp::span, which size is limited to the positive values of std::ptrdiff_t. + amp::at(buffer, static_cast(last_index)) = + '.'; // LCOV_EXCL_BR_LINE: there are no branches to be covered. + total += 1U; + } + } + return total; + }); + + const auto time_elapsed = + std::chrono::duration_cast(time_point.time_since_epoch()).count() % 10'000'000; + + // time_elapsed values are within the scope from 0 to 9'999'999. Casting to uint32_t is safe then. + const auto time_structure_elapsed = static_cast(time_elapsed); + + PutFormattedNumber(payload, time_structure_elapsed); +} + + +template ::value == false, bool> = true> +void PutHexFormattedNumber(VerbosePayload&, const T) noexcept +{ + // Unsupported types action: + HandleUnsupportedTypes(); +} + + +template ::value, bool> = true> +void PutHexFormattedNumber(VerbosePayload& payload, const T data) noexcept +{ + PutFormattedNumber(payload, data); +} + + +template ::value == false, bool> = true> +void PutOctalFormattedNumber(VerbosePayload&, const T) noexcept +{ + // Unsupported types action: + HandleUnsupportedTypes(); +} + + +template ::value, bool> = true> +void PutOctalFormattedNumber(VerbosePayload& payload, const T data) noexcept +{ + PutFormattedNumber(payload, data); +} + +template +static void LogData(VerbosePayload& payload, + const T data, + const IntegerRepresentation integral_representation = IntegerRepresentation::kDecimal) noexcept +{ + switch (integral_representation) + { + case IntegerRepresentation::kHex: + PutHexFormattedNumber(payload, data); + break; + case IntegerRepresentation::kBinary: + PutBinaryFormattedNumber(payload, data); + break; + case IntegerRepresentation::kOctal: + PutOctalFormattedNumber(payload, data); + break; + case IntegerRepresentation::kDecimal: + default: + PutFormattedNumber(payload, data); + break; + } +} + +void TextFormat::Log(VerbosePayload& payload, const bool data) noexcept +{ + constexpr auto positive_value = amp::string_view{"True"}; + constexpr auto negative_value = amp::string_view{"False"}; + + bmw::mw::log::detail::TextFormat::Log(payload, data ? positive_value : negative_value); +} + +void TextFormat::Log(VerbosePayload& payload, + const std::uint8_t data, + const IntegerRepresentation integral_representation) noexcept +{ + bmw::mw::log::detail::LogData(payload, data, integral_representation); +} + +void bmw::mw::log::detail::TextFormat::Log(VerbosePayload& payload, + const std::uint16_t data, + const IntegerRepresentation integral_representation) noexcept +{ + bmw::mw::log::detail::LogData(payload, data, integral_representation); +} + +void bmw::mw::log::detail::TextFormat::Log(VerbosePayload& payload, + const std::uint32_t data, + const IntegerRepresentation integral_representation) noexcept +{ + bmw::mw::log::detail::LogData(payload, data, integral_representation); +} + +void bmw::mw::log::detail::TextFormat::Log(VerbosePayload& payload, + const std::uint64_t data, + const IntegerRepresentation integral_representation) noexcept +{ + bmw::mw::log::detail::LogData(payload, data, integral_representation); +} + +void bmw::mw::log::detail::TextFormat::Log(VerbosePayload& payload, + const std::int8_t data, + const IntegerRepresentation integral_representation) noexcept +{ + bmw::mw::log::detail::LogData(payload, data, integral_representation); +} + +void bmw::mw::log::detail::TextFormat::Log(VerbosePayload& payload, + const std::int16_t data, + const IntegerRepresentation integral_representation) noexcept +{ + bmw::mw::log::detail::LogData(payload, data, integral_representation); +} + +void bmw::mw::log::detail::TextFormat::Log(VerbosePayload& payload, + const std::int32_t data, + const IntegerRepresentation integral_representation) noexcept +{ + bmw::mw::log::detail::LogData(payload, data, integral_representation); +} + +void bmw::mw::log::detail::TextFormat::Log(VerbosePayload& payload, + const std::int64_t data, + const IntegerRepresentation integral_representation) noexcept +{ + bmw::mw::log::detail::LogData(payload, data, integral_representation); +} + +void bmw::mw::log::detail::TextFormat::Log(VerbosePayload& payload, const LogHex8 data) noexcept +{ + bmw::mw::log::detail::LogData(payload, data.value, IntegerRepresentation::kHex); +} + +void bmw::mw::log::detail::TextFormat::Log(VerbosePayload& payload, const LogHex16 data) noexcept +{ + bmw::mw::log::detail::LogData(payload, data.value, IntegerRepresentation::kHex); +} + +void bmw::mw::log::detail::TextFormat::Log(VerbosePayload& payload, const LogHex32 data) noexcept +{ + bmw::mw::log::detail::LogData(payload, data.value, IntegerRepresentation::kHex); +} + +void bmw::mw::log::detail::TextFormat::Log(VerbosePayload& payload, const LogHex64 data) noexcept +{ + bmw::mw::log::detail::LogData(payload, data.value, IntegerRepresentation::kHex); +} + +void bmw::mw::log::detail::TextFormat::Log(VerbosePayload& payload, const LogBin8 data) noexcept +{ + bmw::mw::log::detail::LogData(payload, data.value, IntegerRepresentation::kBinary); +} + +void bmw::mw::log::detail::TextFormat::Log(VerbosePayload& payload, const LogBin16 data) noexcept +{ + bmw::mw::log::detail::LogData(payload, data.value, IntegerRepresentation::kBinary); +} + +void bmw::mw::log::detail::TextFormat::Log(VerbosePayload& payload, const LogBin32 data) noexcept +{ + bmw::mw::log::detail::LogData(payload, data.value, IntegerRepresentation::kBinary); +} + +void bmw::mw::log::detail::TextFormat::Log(VerbosePayload& payload, const LogBin64 data) noexcept +{ + bmw::mw::log::detail::LogData(payload, data.value, IntegerRepresentation::kBinary); +} + +void bmw::mw::log::detail::TextFormat::Log(VerbosePayload& payload, const float data) noexcept +{ + bmw::mw::log::detail::LogData(payload, data); +} + +void bmw::mw::log::detail::TextFormat::Log(VerbosePayload& payload, const double data) noexcept +{ + bmw::mw::log::detail::LogData(payload, data); +} + +void bmw::mw::log::detail::TextFormat::Log(VerbosePayload& payload, const amp::string_view data) noexcept +{ + if (data.size() > 0) + { + const std::size_t data_length = data.size() + kReserveSpaceForSpace; + + amp::ignore = payload.Put( + [data](const amp::span buffer) { + const std::size_t length = std::min(data.size(), GetSpanSizeCasted(buffer)); + if (length > 0) // LCOV_EXCL_BR_LINE: Cannot be covered in unit tests as 'data.size() > 0' and buffer + // size is uncontrollable. + { + // NOLINTNEXTLINE(bmw-banned-function) memcpy is needed to copy string_view data + amp::ignore = std::memcpy(buffer.data(), data.data(), length); + const std::size_t last_index = GetSpanSizeCasted(buffer) - 1U; + + // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) used on span which is an array + // False positive: Pointer arithmetic is used on span which is an array + // + buffer.data()[last_index] = ' '; + // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) used on span which is an array + + return GetSpanSizeCasted(buffer); + } + return 0LU; // LCOV_EXCL_LINE: Cannot be covered in unit tests as if (length > 0) is uncontrollable. + }, + data_length); + } +} + +void bmw::mw::log::detail::TextFormat::TerminateLog(VerbosePayload& payload) noexcept +{ + payload.Put("\n", kReserveSpaceForSpace); +} + +void bmw::mw::log::detail::TextFormat::Log(VerbosePayload& payload, const LogRawBuffer data) noexcept +{ + const auto max_string_len = kTwoNibblesPerByte * GetBufferSizeCasted(data.size()); + + if (max_string_len > std::size_t{0U}) + { + amp::ignore = payload.Put( + [data](const amp::span buffer) noexcept { + std::size_t total{0}; + std::size_t i{0}; + std::size_t space_left{GetBufferSizeCasted(buffer.size())}; + auto data_source = data.data(); + // proceed if some space is in the buffer and there are still some elements to convert + // LCOV_EXCL_BR_START: The (i < GetBufferSizeCasted(data.size())) condition is always true under current + // logic because 'i' starts at 0 and 'data.size()' is greater than 0 when 'max_string_len > 0' is true. + // Therefore, we can suppress the coverage warning for this decision. + while ((space_left > std::size_t{0U}) && (i < GetBufferSizeCasted(data.size()))) + // LCOV_EXCL_BR_STOP + { + auto data_iter = buffer.data(); + std::advance(data_iter, static_cast(total)); + const auto written = FormattingFunctionReturnCast( + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) safe to use std::snprintf + std::snprintf(data_iter, space_left, "%02hhx", *data_source)); + total += written; + space_left -= written; + std::advance(data_source, 1UL); + i++; + } + // if space left put space character after data + // else overwrite last character with space + if (space_left > std::size_t{0U}) + { + total += std::size_t{1U}; + } + if (total > std::size_t{0U}) + { + + // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) used on span which is an array + // False positive: Pointer arithmetic is used on span which is an array, bound checking done + // + buffer.data()[total - std::size_t{1U}] = ' '; + // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) used on span which is an array + + } + + return total; + }, + max_string_len + kReserveSpaceForSpace); + } +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/file_logging/text_format.h b/mw/log/detail/file_logging/text_format.h new file mode 100644 index 0000000..6d8b967 --- /dev/null +++ b/mw/log/detail/file_logging/text_format.h @@ -0,0 +1,94 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_ASCII_FORMAT_H_ +#define PLATFORM_AAS_MW_LOG_DETAIL_ASCII_FORMAT_H_ + +#include "platform/aas/mw/log/detail/integer_representation.h" +#include "platform/aas/mw/log/detail/verbose_payload.h" +#include "platform/aas/mw/log/log_types.h" + +#include "amp_string_view.hpp" + +#include +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +/// \brief TextFormat Class used to format different data types to form a text log +/// +/// \detail Used by TextMessageBuilder to build header and with (Stdout)Recorder to build payload +class TextFormat +{ + public: + static void Log(VerbosePayload&, const bool) noexcept; + static void Log(VerbosePayload&, + const std::uint8_t, + const IntegerRepresentation = IntegerRepresentation::kDecimal) noexcept; + static void Log(VerbosePayload&, + const std::uint16_t, + const IntegerRepresentation = IntegerRepresentation::kDecimal) noexcept; + static void Log(VerbosePayload&, + const std::uint32_t, + const IntegerRepresentation = IntegerRepresentation::kDecimal) noexcept; + static void Log(VerbosePayload&, + const std::uint64_t, + const IntegerRepresentation = IntegerRepresentation::kDecimal) noexcept; + static void Log(VerbosePayload&, + const std::int8_t, + const IntegerRepresentation = IntegerRepresentation::kDecimal) noexcept; + static void Log(VerbosePayload&, + const std::int16_t, + const IntegerRepresentation = IntegerRepresentation::kDecimal) noexcept; + static void Log(VerbosePayload&, + const std::int32_t, + const IntegerRepresentation = IntegerRepresentation::kDecimal) noexcept; + static void Log(VerbosePayload&, + const std::int64_t, + const IntegerRepresentation = IntegerRepresentation::kDecimal) noexcept; + static void Log(VerbosePayload&, const LogHex8) noexcept; + static void Log(VerbosePayload&, const LogHex16) noexcept; + static void Log(VerbosePayload&, const LogHex32) noexcept; + static void Log(VerbosePayload&, const LogHex64) noexcept; + static void Log(VerbosePayload&, const LogBin8) noexcept; + static void Log(VerbosePayload&, const LogBin16) noexcept; + static void Log(VerbosePayload&, const LogBin32) noexcept; + static void Log(VerbosePayload&, const LogBin64) noexcept; + static void Log(VerbosePayload&, const float) noexcept; + static void Log(VerbosePayload&, const double) noexcept; + static void Log(VerbosePayload&, const amp::string_view) noexcept; + static void Log(VerbosePayload&, const LogRawBuffer) noexcept; + /// \brief Puts formatted time e.g. "2021/03/17 15:19:20.4360057 551684554" + static void PutFormattedTime(VerbosePayload& payload) noexcept; + /// \brief Puts '\n' character at the end of log + static void TerminateLog(VerbosePayload& payload) noexcept; +}; + +// Visibility of this funcation is extended because of coverage requirement +std::size_t FormattingFunctionReturnCast(const std::int32_t i) noexcept; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_ASCII_FORMAT_H_ diff --git a/mw/log/detail/file_logging/text_format_test.cpp b/mw/log/detail/file_logging/text_format_test.cpp new file mode 100644 index 0000000..0c54db8 --- /dev/null +++ b/mw/log/detail/file_logging/text_format_test.cpp @@ -0,0 +1,845 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/file_logging/text_format.h" + +#include "gtest/gtest.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace test +{ +namespace +{ + +class TextFormatFixture : public ::testing::Test +{ + public: + ByteVector buffer_{}; + VerbosePayload payload_{100, buffer_}; + ByteVector zero_sized_buffer_{}; + VerbosePayload depleted_payload_{0, zero_sized_buffer_}; + ByteVector size_two_buffer_{}; + VerbosePayload capacity_two_payload_{2, size_two_buffer_}; +}; + +template +class UnsupportedTypesCoverage : public testing::Test +{ + public: + ByteVector buffer_{}; + VerbosePayload payload_{100, buffer_}; + T value_ = 123; +}; + +using UnsupportedTypes = ::testing::Types; +TYPED_TEST_SUITE(UnsupportedTypesCoverage, UnsupportedTypes, /*unused*/); + +TYPED_TEST(UnsupportedTypesCoverage, VerifyUnsupportedTypesActionsHex) +{ + ::testing::Test::RecordProperty("ParentRequirement", ""); + ::testing::Test::RecordProperty("ASIL", "B"); + ::testing::Test::RecordProperty( + "Description", "Verifies Type-Information for integer values with hex representation can not be logged."); + ::testing::Test::RecordProperty("TestingTechnique", "Requirements-based test"); + ::testing::Test::RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(this->payload_, this->value_, IntegerRepresentation::kHex); + EXPECT_EQ(this->buffer_.size(), 0); +} + +TYPED_TEST(UnsupportedTypesCoverage, VerifyUnsupportedTypesActionsOctal) +{ + ::testing::Test::RecordProperty("ParentRequirement", ""); + ::testing::Test::RecordProperty("ASIL", "B"); + ::testing::Test::RecordProperty( + "Description", "Verifies Type-Information for integer values with octal representation can not be logged."); + ::testing::Test::RecordProperty("TestingTechnique", "Requirements-based test"); + ::testing::Test::RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(this->payload_, this->value_, IntegerRepresentation::kOctal); + EXPECT_EQ(this->buffer_.size(), 0); +} + +TYPED_TEST(UnsupportedTypesCoverage, VerifyUnsupportedTypesActionsBin) +{ + ::testing::Test::RecordProperty("ParentRequirement", ""); + ::testing::Test::RecordProperty("ASIL", "B"); + ::testing::Test::RecordProperty( + "Description", "Verifies Type-Information for integer values with binary representation can not be logged."); + ::testing::Test::RecordProperty("TestingTechnique", "Requirements-based test"); + ::testing::Test::RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(this->payload_, this->value_, IntegerRepresentation::kBinary); + EXPECT_EQ(this->buffer_.size(), 0); +} + +TEST_F(TextFormatFixture, DepletedBufferPassed) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies no log can be set for a zero buffer size."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(depleted_payload_, std::int32_t{123U}); + + EXPECT_EQ(zero_sized_buffer_.size(), 0); +} + +TEST_F(TextFormatFixture, PositiveValueForBool) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for a positive value with bool in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, bool{true}); + + EXPECT_EQ(buffer_.at(0), 'T'); + EXPECT_EQ(buffer_.at(1), 'r'); + EXPECT_EQ(buffer_.at(2), 'u'); + EXPECT_EQ(buffer_.at(3), 'e'); + EXPECT_EQ(buffer_.at(4), ' '); + EXPECT_EQ(buffer_.size(), 5); +} + +TEST_F(TextFormatFixture, NegativeValueForBool) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for a negative value with bool in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, bool{false}); + + EXPECT_EQ(buffer_.at(0), 'F'); + EXPECT_EQ(buffer_.at(1), 'a'); + EXPECT_EQ(buffer_.at(2), 'l'); + EXPECT_EQ(buffer_.at(3), 's'); + EXPECT_EQ(buffer_.at(4), 'e'); + EXPECT_EQ(buffer_.at(5), ' '); + EXPECT_EQ(buffer_.size(), 6); +} + +TEST_F(TextFormatFixture, PositiveValueOnBufferFull) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for a positive value with int64 size in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(capacity_two_payload_, amp::string_view{"xxx"}); + TextFormat::Log(capacity_two_payload_, std::int8_t{123U}); + + // Buffer content not changed by second insertion: + EXPECT_EQ(size_two_buffer_.at(0), 'x'); + EXPECT_EQ(size_two_buffer_.at(1), ' '); + EXPECT_EQ(size_two_buffer_.size(), 2); +} + +TEST_F(TextFormatFixture, PositiveValueForInt8) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for a positive value with int8 size in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, std::int8_t{123U}); + + EXPECT_EQ(buffer_.at(0), '1'); + EXPECT_EQ(buffer_.at(1), '2'); + EXPECT_EQ(buffer_.at(2), '3'); + EXPECT_EQ(buffer_.at(3), ' '); + EXPECT_EQ(buffer_.size(), 4); +} + +TEST_F(TextFormatFixture, NegativeValueInt8) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for a negative value with int8 size in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, std::int8_t{-123}); + + EXPECT_EQ(buffer_.at(0), '-'); + EXPECT_EQ(buffer_.at(1), '1'); + EXPECT_EQ(buffer_.at(2), '2'); + EXPECT_EQ(buffer_.at(3), '3'); + EXPECT_EQ(buffer_.at(4), ' '); + EXPECT_EQ(buffer_.size(), 5); +} + +TEST_F(TextFormatFixture, PositiveValueForInt16) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for a positive value with int16 size in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, std::int16_t{123U}); + + EXPECT_EQ(buffer_.at(0), '1'); + EXPECT_EQ(buffer_.at(1), '2'); + EXPECT_EQ(buffer_.at(2), '3'); + EXPECT_EQ(buffer_.at(3), ' '); + EXPECT_EQ(buffer_.size(), 4); +} + +TEST_F(TextFormatFixture, NegativeValueInt16) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for a negative value with int16 size in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, std::numeric_limits::min()); + EXPECT_EQ(buffer_.size(), 7); + EXPECT_EQ(0, std::memcmp(buffer_.data(), "-32768 ", buffer_.size())); +} + +TEST_F(TextFormatFixture, PositiveValueInt32) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for a positive value with int32 size in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, std::numeric_limits::max()); + EXPECT_EQ(buffer_.size(), 11); + EXPECT_EQ(0, std::memcmp(buffer_.data(), "2147483647 ", buffer_.size())); +} + +TEST_F(TextFormatFixture, NegativeValueInt32) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for a negative value with int32 size in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, std::numeric_limits::min()); + EXPECT_EQ(buffer_.at(0), '-'); + EXPECT_EQ(buffer_.at(1), '2'); + EXPECT_EQ(buffer_.at(2), '1'); + EXPECT_EQ(buffer_.at(3), '4'); + EXPECT_EQ(buffer_.at(4), '7'); + EXPECT_EQ(buffer_.at(5), '4'); + EXPECT_EQ(buffer_.at(6), '8'); + EXPECT_EQ(buffer_.at(7), '3'); + EXPECT_EQ(buffer_.at(8), '6'); + EXPECT_EQ(buffer_.at(9), '4'); + EXPECT_EQ(buffer_.at(10), '8'); + EXPECT_EQ(buffer_.at(11), ' '); + EXPECT_EQ(buffer_.size(), 12); + EXPECT_EQ(0, std::memcmp(buffer_.data(), "-2147483648 ", buffer_.size())); +} + +TEST_F(TextFormatFixture, PositiveValueInt64) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for a positive value with int64 size in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, int64_t{std::numeric_limits::max()}); + + EXPECT_EQ(buffer_.size(), 20); + EXPECT_EQ(0, std::memcmp(buffer_.data(), "9223372036854775807 ", buffer_.size())); +} + +TEST_F(TextFormatFixture, NegativeValueInt64) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for a negative value with int64 size in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, std::numeric_limits::min()); + + EXPECT_EQ(buffer_.size(), 21); + EXPECT_EQ(0, std::memcmp(buffer_.data(), "-9223372036854775808 ", buffer_.size())); +} + +TEST_F(TextFormatFixture, PositiveValueForUint8) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for positive value with uint8 representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, std::uint8_t{234U}); + + EXPECT_EQ(buffer_.at(0), '2'); + EXPECT_EQ(buffer_.at(1), '3'); + EXPECT_EQ(buffer_.at(2), '4'); + EXPECT_EQ(buffer_.at(3), ' '); + EXPECT_EQ(buffer_.size(), 4); +} + +TEST_F(TextFormatFixture, HexFormatUint8) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for uint8 value with hex representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, LogHex8{234U}); + + EXPECT_EQ(buffer_.at(0), 'e'); + EXPECT_EQ(buffer_.at(1), 'a'); + EXPECT_EQ(buffer_.at(2), ' '); + EXPECT_EQ(buffer_.size(), 3); +} + +TEST_F(TextFormatFixture, BinaryFormatUint8) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for uint8 value with binary representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, LogBin8{234U}); + + EXPECT_EQ(buffer_.at(0), '1'); + EXPECT_EQ(buffer_.at(1), '1'); + EXPECT_EQ(buffer_.at(2), '1'); + EXPECT_EQ(buffer_.at(3), '0'); + EXPECT_EQ(buffer_.at(4), '1'); + EXPECT_EQ(buffer_.at(5), '0'); + EXPECT_EQ(buffer_.at(6), '1'); + EXPECT_EQ(buffer_.at(7), '0'); + EXPECT_EQ(buffer_.at(8), ' '); + EXPECT_EQ(buffer_.size(), 9); +} + +TEST_F(TextFormatFixture, OctalFormatUint8) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for uint8 value with octal representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, std::uint8_t{234U}, IntegerRepresentation::kOctal); + + EXPECT_EQ(buffer_.at(0), '3'); + EXPECT_EQ(buffer_.at(1), '5'); + EXPECT_EQ(buffer_.at(2), '2'); + EXPECT_EQ(buffer_.at(3), ' '); + EXPECT_EQ(buffer_.size(), 4); +} + +TEST_F(TextFormatFixture, PositiveValueForUint16) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for a positive value with uint16 representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, std::uint16_t{43456U}); + + EXPECT_EQ(buffer_.at(0), '4'); + EXPECT_EQ(buffer_.at(1), '3'); + EXPECT_EQ(buffer_.at(2), '4'); + EXPECT_EQ(buffer_.at(3), '5'); + EXPECT_EQ(buffer_.at(4), '6'); + EXPECT_EQ(buffer_.at(5), ' '); + EXPECT_EQ(buffer_.size(), 6); +} + +TEST_F(TextFormatFixture, HexFormatUint16) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for uint16 value with hex representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, LogHex16{123U}); + + EXPECT_EQ(buffer_.at(0), '7'); + EXPECT_EQ(buffer_.at(1), 'b'); + EXPECT_EQ(buffer_.at(2), ' '); + EXPECT_EQ(buffer_.size(), 3); +} + +TEST_F(TextFormatFixture, BinaryFormatUint16) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for uint16 value with binary representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, LogBin16{43456U}); + + EXPECT_EQ(buffer_.size(), 17); + EXPECT_EQ(0, std::memcmp(buffer_.data(), "1010100111000000 ", buffer_.size())); +} + +TEST_F(TextFormatFixture, OctalFormatUint16) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for uint16 value with octal representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, std::uint16_t{43456U}, IntegerRepresentation::kOctal); + + EXPECT_EQ(buffer_.at(0), '1'); + EXPECT_EQ(buffer_.at(1), '2'); + EXPECT_EQ(buffer_.at(2), '4'); + EXPECT_EQ(buffer_.at(3), '7'); + EXPECT_EQ(buffer_.at(4), '0'); + EXPECT_EQ(buffer_.at(5), '0'); + EXPECT_EQ(buffer_.at(6), ' '); + EXPECT_EQ(buffer_.size(), 7); +} + +TEST_F(TextFormatFixture, PositiveValueForUint32) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for positive value with uint32_t size in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, std::uint32_t{std::numeric_limits::max()} + 1); + + EXPECT_EQ(buffer_.size(), 11); + EXPECT_EQ(0, std::memcmp(buffer_.data(), "2147483648 ", buffer_.size())); +} + +TEST_F(TextFormatFixture, HexFormatUint32) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for uint32 with hex representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, LogHex32{52345U}); + + EXPECT_EQ(buffer_.at(0), 'c'); + EXPECT_EQ(buffer_.at(1), 'c'); + EXPECT_EQ(buffer_.at(2), '7'); + EXPECT_EQ(buffer_.at(3), '9'); + EXPECT_EQ(buffer_.at(4), ' '); + EXPECT_EQ(buffer_.size(), 5); +} + +TEST_F(TextFormatFixture, BinFormatUint32) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for uint32 with binary representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, LogBin32{52345U}); + + EXPECT_EQ(buffer_.size(), 33); + EXPECT_EQ(0, std::memcmp(buffer_.data(), "00000000000000001100110001111001 ", buffer_.size())); +} + +TEST_F(TextFormatFixture, OctalFormatUint32) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for uint32 with octal representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, std::uint32_t{52349U}, IntegerRepresentation::kOctal); + + EXPECT_EQ(buffer_.size(), 7); + EXPECT_EQ(0, std::memcmp(buffer_.data(), "146175 ", buffer_.size())); +} + +TEST_F(TextFormatFixture, PositiveValueForUint64) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for positive value with uint64_t size in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, uint64_t{std::numeric_limits::max()} + 1); + + EXPECT_EQ(buffer_.size(), 20); + EXPECT_EQ(0, std::memcmp(buffer_.data(), "9223372036854775808 ", buffer_.size())); +} + +TEST_F(TextFormatFixture, BinaryFormat_InsufficientBuffer) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for binary representation shall be cropped in case of insufficent buffer " + "for its bytes."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(capacity_two_payload_, std::uint8_t{234U}, IntegerRepresentation::kBinary); + + ASSERT_EQ(size_two_buffer_.at(0), '1'); + ASSERT_EQ(size_two_buffer_.at(1), ' '); + + ASSERT_EQ(size_two_buffer_.size(), 2); +} + +TEST_F(TextFormatFixture, BinaryFormatWhenBufferFull) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for binary representation shall be cropped in case of insufficent buffer " + "for its bytes."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Make the buffer full + TextFormat::Log(capacity_two_payload_, amp::string_view{"xxx"}); + TextFormat::Log(capacity_two_payload_, std::uint8_t{234U}, IntegerRepresentation::kBinary); + + // When buffer is full no new data should appear + ASSERT_EQ(size_two_buffer_.at(0), 'x'); + ASSERT_EQ(size_two_buffer_.at(1), ' '); + + ASSERT_EQ(size_two_buffer_.size(), 2); +} + +TEST_F(TextFormatFixture, HexFormat_InsufficientBuffer) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for hex representation shall only store bytes of data equal to the " + "allocated capacity."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(capacity_two_payload_, std::uint32_t{52345U}, IntegerRepresentation::kHex); + + ASSERT_EQ(size_two_buffer_.at(0), 'c'); + ASSERT_EQ(size_two_buffer_.at(1), ' '); + + ASSERT_EQ(size_two_buffer_.size(), 2); +} + +TEST_F(TextFormatFixture, HexFormatUint64) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for uint64 with hex representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, LogHex64{12379813812177893520U}); + + EXPECT_EQ(buffer_.size(), 17); + EXPECT_EQ(0, std::memcmp(buffer_.data(), "abcdef1234567890 ", buffer_.size())); +} + +TEST_F(TextFormatFixture, BinaryFormatUint64) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for uint64 with binary representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, LogBin64{12379813812177893520U}); + + EXPECT_EQ(buffer_.size(), 65); + EXPECT_EQ(0, + std::memcmp( + buffer_.data(), "1010101111001101111011110001001000110100010101100111100010010000 ", buffer_.size())); +} + +TEST_F(TextFormatFixture, OctalFormatUint64) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for uint64 with octal representation is in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, std::uint64_t{12379813812177893520U}, IntegerRepresentation::kOctal); + + EXPECT_EQ(buffer_.size(), 23); + EXPECT_EQ(0, std::memcmp(buffer_.data(), "1257157361106425474220 ", buffer_.size())); +} + +TEST_F(TextFormatFixture, LogFloat) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for float in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, 1.23f); + + ASSERT_EQ(buffer_.at(0), '1'); + ASSERT_EQ(buffer_.at(1), '.'); + ASSERT_EQ(buffer_.at(2), '2'); + ASSERT_EQ(buffer_.at(3), '3'); + ASSERT_EQ(buffer_.at(4), '0'); + ASSERT_EQ(buffer_.at(5), '0'); + ASSERT_EQ(buffer_.at(6), '0'); + ASSERT_EQ(buffer_.at(7), '0'); + ASSERT_EQ(buffer_.at(8), ' '); + + EXPECT_EQ(buffer_.size(), 9); +} + +TEST_F(TextFormatFixture, LogDouble) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for double in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, 1.23); + + ASSERT_EQ(buffer_.at(0), '1'); + ASSERT_EQ(buffer_.at(1), '.'); + ASSERT_EQ(buffer_.at(2), '2'); + ASSERT_EQ(buffer_.at(3), '3'); + ASSERT_EQ(buffer_.at(4), '0'); + ASSERT_EQ(buffer_.at(5), '0'); + ASSERT_EQ(buffer_.at(6), '0'); + ASSERT_EQ(buffer_.at(7), '0'); + ASSERT_EQ(buffer_.at(8), ' '); + + EXPECT_EQ(buffer_.size(), 9); +} + +TEST_F(TextFormatFixture, StringValueCorrectlyTransformed) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for string in correct format."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, amp::string_view{"Hello World"}); + + // String + ASSERT_EQ(buffer_.at(0), 'H'); + ASSERT_EQ(buffer_.at(1), 'e'); + ASSERT_EQ(buffer_.at(2), 'l'); + ASSERT_EQ(buffer_.at(3), 'l'); + ASSERT_EQ(buffer_.at(4), 'o'); + ASSERT_EQ(buffer_.at(5), ' '); + ASSERT_EQ(buffer_.at(6), 'W'); + ASSERT_EQ(buffer_.at(7), 'o'); + ASSERT_EQ(buffer_.at(8), 'r'); + ASSERT_EQ(buffer_.at(9), 'l'); + ASSERT_EQ(buffer_.at(10), 'd'); + ASSERT_EQ(buffer_.at(11), ' '); + + ASSERT_EQ(buffer_.size(), 12); +} + +TEST_F(TextFormatFixture, TerminateShallPutNewLine) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies that TerminateLog shall put new line in data buffer."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::TerminateLog(payload_); + ASSERT_EQ(buffer_.at(0), '\n'); + ASSERT_EQ(buffer_.size(), 1); +} + +TEST_F(TextFormatFixture, StringValueWhenBufferFull) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for string shall be intact in case of using full buffer."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Make the buffer full + TextFormat::Log(capacity_two_payload_, amp::string_view{"xxx"}); + // Try to put more data into buffer + TextFormat::Log(capacity_two_payload_, amp::string_view{"Hello World"}); + + // String is not changed + ASSERT_EQ(size_two_buffer_.at(0), 'x'); + ASSERT_EQ(size_two_buffer_.at(1), ' '); + + ASSERT_EQ(size_two_buffer_.size(), 2); +} + +TEST_F(TextFormatFixture, EmptyString) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies Type-Information for empty string will not allocate memory."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + TextFormat::Log(payload_, amp::string_view{""}); + + ASSERT_EQ(buffer_.size(), 0); +} + +TEST_F(TextFormatFixture, RawValueSimpleConversionToHex) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "Verifies Type-Information for raw value will be converted to hex values nibble by nibble before storing."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + std::vector data{{1, 2, 0x1F}}; + TextFormat::Log(payload_, LogRawBuffer{data.data(), 3}); + + // Data + ASSERT_EQ(buffer_.at(0), '0'); + ASSERT_EQ(buffer_.at(1), '1'); + ASSERT_EQ(buffer_.at(2), '0'); + ASSERT_EQ(buffer_.at(3), '2'); + ASSERT_EQ(buffer_.at(4), '1'); + ASSERT_EQ(buffer_.at(5), 'f'); + ASSERT_EQ(buffer_.at(6), ' '); + // Size + ASSERT_EQ(buffer_.size(), 7); +} + +TEST_F(TextFormatFixture, RawValueSimpleConversionToHexInsufficientBuffer) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for raw value will be converted to hex values nibble by nibble and will " + "be cropped in case of using insufficient buffer."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + std::vector data{{1, 2, 0x1F}}; + TextFormat::Log(capacity_two_payload_, LogRawBuffer{data.data(), 3}); + + // Data + ASSERT_EQ(size_two_buffer_.at(0), '0'); + ASSERT_EQ(size_two_buffer_.at(1), ' '); + // Size + ASSERT_EQ(size_two_buffer_.size(), 2); +} + +TEST_F(TextFormatFixture, RawValueZeroLengthBuffer) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies Type-Information for raw value with zero size will not allocate any memory for logging."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + std::vector data{}; + TextFormat::Log(payload_, LogRawBuffer{data.data(), 0}); + + // Size + ASSERT_EQ(buffer_.size(), 0); +} + +TEST_F(TextFormatFixture, RawValueZeroMaxSizeBuffer) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies raw value with zero max size buffer will not allocate any memory for logging."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + ByteVector buffer{}; + LogRawBuffer data{"test data"}; + VerbosePayload payload{0U, buffer}; + + TextFormat::Log(payload, data); + + ASSERT_TRUE(payload.GetSpan().empty()); +} + +TEST(FormattingFunction, ShallConvertNegativeValuesToZero) +{ + ASSERT_EQ(FormattingFunctionReturnCast(std::int32_t{-1}), std::size_t{0UL}); +} + +TEST(FormattingFunction, ShallReturnEmptyIfPayloadMaxSizeEqualToZero) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies getting empty payload in case of the max size for allocated memory is equal to zero."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + ByteVector buffer{}; + VerbosePayload payload{0U, buffer}; + TextFormat::PutFormattedTime(payload); + + ASSERT_TRUE(payload.GetSpan().empty()); +} + +} // namespace +} // namespace test +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/file_logging/text_message_builder.cpp b/mw/log/detail/file_logging/text_message_builder.cpp new file mode 100644 index 0000000..5029731 --- /dev/null +++ b/mw/log/detail/file_logging/text_message_builder.cpp @@ -0,0 +1,166 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#include "platform/aas/mw/log/detail/file_logging/text_message_builder.h" + +#include "platform/aas/lib/os/utils/high_resolution_steady_clock.h" +#include "platform/aas/mw/log/detail/file_logging/text_format.h" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +namespace +{ + + +constexpr std::size_t kMaxHeaderSize = std::size_t{512}; + + + +void LogLevelToString(VerbosePayload& payload, const LogLevel level) noexcept + +{ + switch (level) + { + case LogLevel::kOff: + detail::TextFormat::Log(payload, amp::string_view{"off"}); + break; + case LogLevel::kFatal: + detail::TextFormat::Log(payload, amp::string_view{"fatal"}); + break; + case LogLevel::kError: + detail::TextFormat::Log(payload, amp::string_view{"error"}); + break; + case LogLevel::kWarn: + detail::TextFormat::Log(payload, amp::string_view{"warn"}); + break; + case LogLevel::kInfo: + detail::TextFormat::Log(payload, amp::string_view{"info"}); + break; + case LogLevel::kDebug: + detail::TextFormat::Log(payload, amp::string_view{"debug"}); + break; + case LogLevel::kVerbose: + detail::TextFormat::Log(payload, amp::string_view{"verbose"}); + break; + default: + detail::TextFormat::Log(payload, amp::string_view{"undefined"}); + break; + } +} +inline std::uint32_t TimeStamp() noexcept +{ + const std::uint32_t timestamp = std::chrono::duration_cast>>( + bmw::os::HighResolutionSteadyClock::now().time_since_epoch()) + .count(); + return timestamp; +} + +} // namespace + + +TextMessageBuilder::TextMessageBuilder(const amp::string_view ecu_id) noexcept + : IMessageBuilder(), + header_payload_(kMaxHeaderSize, header_memory_), + parsing_phase_{ParsingPhase::kHeader}, + ecu_id_{ecu_id} +{ +} + + +void TextMessageBuilder::SetNextMessage(LogRecord& log_record) noexcept + +{ + log_record_ = log_record; + + const auto& log_entry = log_record_.value().get().getLogEntry(); + detail::TextFormat::PutFormattedTime(header_payload_); + detail::TextFormat::Log(header_payload_, TimeStamp()); + detail::TextFormat::Log(header_payload_, amp::string_view{"000"}); + detail::TextFormat::Log(header_payload_, ecu_id_.GetStringView()); + detail::TextFormat::Log(header_payload_, log_entry.app_id.GetStringView()); + detail::TextFormat::Log(header_payload_, log_entry.ctx_id.GetStringView()); + detail::TextFormat::Log(header_payload_, amp::string_view{"log"}); + LogLevelToString(header_payload_, log_entry.log_level); + detail::TextFormat::Log(header_payload_, amp::string_view{"verbose"}); + detail::TextFormat::Log(header_payload_, log_entry.num_of_args); + parsing_phase_ = ParsingPhase::kHeader; +} + + + +amp::optional> TextMessageBuilder::GetNextSpan() noexcept + + +{ + + if (!log_record_.has_value()) + + { + return {}; + } + + detail::VerbosePayload& verbose_payload = log_record_.value().get().getVerbosePayload(); + + switch (parsing_phase_) // LCOV_EXCL_BR_LINE: exclude the "default" branch. + { + case ParsingPhase::kHeader: + parsing_phase_ = ParsingPhase::kPayload; + return header_payload_.GetSpan(); + break; + case ParsingPhase::kPayload: + parsing_phase_ = ParsingPhase::kReinitialize; + detail::TextFormat::TerminateLog(verbose_payload); + return log_record_.value().get().getVerbosePayload().GetSpan(); + break; + case ParsingPhase::kReinitialize: + parsing_phase_ = ParsingPhase::kHeader; + header_payload_.Reset(); + verbose_payload.Reset(); + log_record_.reset(); + break; + default: // LCOV_EXCL_LINE + + /* Tolerated by decision. The part of the code, which should never be reached. Excluded from code coverage + * as well. */ + + /* Tolerated by decision. The part of the code, which should never be reached. Excluded from code coverage + * as well. */ + std::abort(); // LCOV_EXCL_LINE defensive programming: Only defined ParsingPhase values are possible to be + // reached. + + /* Tolerated by decision. The part of the code, which should never be reached. Excluded from code coverage + * as well. */ + + /* Tolerated by decision. The part of the code, which should never be reached. Excluded from code coverage + * as well. */ + break; + } + return {}; +} + + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/file_logging/text_message_builder.h b/mw/log/detail/file_logging/text_message_builder.h new file mode 100644 index 0000000..acd0e1c --- /dev/null +++ b/mw/log/detail/file_logging/text_message_builder.h @@ -0,0 +1,57 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_TEXT_MESSAGE_BUILDER_H_ +#define PLATFORM_AAS_MW_LOG_TEXT_MESSAGE_BUILDER_H_ + +#include "platform/aas/mw/log/detail/file_logging/imessage_builder.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +class TextMessageBuilder : public IMessageBuilder +{ + public: + explicit TextMessageBuilder(const amp::string_view ecu_id) noexcept; + + amp::optional> GetNextSpan() noexcept override; + + void SetNextMessage(LogRecord& log_record) noexcept override; + + private: + enum class ParsingPhase : std::uint8_t + { + kHeader = 0, + kPayload, + kReinitialize, + }; + amp::optional> log_record_; + ByteVector header_memory_; + detail::VerbosePayload header_payload_; + ParsingPhase parsing_phase_; + LoggingIdentifier ecu_id_; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_TEXT_MESSAGE_BUILDER_H_ diff --git a/mw/log/detail/file_logging/text_message_builder_test.cpp b/mw/log/detail/file_logging/text_message_builder_test.cpp new file mode 100644 index 0000000..cabe752 --- /dev/null +++ b/mw/log/detail/file_logging/text_message_builder_test.cpp @@ -0,0 +1,183 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/file_logging/text_message_builder.h" +#include "platform/aas/mw/log/log_level.h" + +#include "gtest/gtest.h" +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +using ::testing::_; +using ::testing::HasSubstr; +using ::testing::MatchesRegex; +using ::testing::StrEq; + +const std::map levels = { + {LogLevel::kOff, "off"}, + {LogLevel::kFatal, "fatal"}, + {LogLevel::kError, "error"}, + {LogLevel::kWarn, "warn"}, + {LogLevel::kInfo, "info"}, + {LogLevel::kDebug, "debug"}, + {LogLevel::kVerbose, "verbose"}, +}; + +class TextMessageBuilderFixture : public ::testing::TestWithParam +{ + public: + void SetUp() override + { + auto& log_entry = log_record_.getLogEntry(); + + log_entry.app_id = LoggingIdentifier{"TMB"}; + log_entry.ctx_id = LoggingIdentifier{"CTX"}; + // log_entry.timestamp_steady_nsec = 8791823749; + // log_entry.timestamp_system_nsec = 8872983746; + log_entry.num_of_args = 7; + log_entry.log_level = LogLevel::kWarn; + log_entry.payload = ByteVector{'p', 'a', 'y', 'l', 'o', 'a', 'd'}; + } + void TearDown() override {} + + protected: + TextMessageBuilder unit_{"XECU"}; + LogRecord log_record_; +}; + +TEST_F(TextMessageBuilderFixture, ShallDepleteAfterHeaderAndPayload) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextMessageBuilder shall deplete after getting header and payload."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + unit_.SetNextMessage(log_record_); + const auto first = unit_.GetNextSpan(); + EXPECT_TRUE(first.has_value()); + const auto second = unit_.GetNextSpan(); + EXPECT_TRUE(second.has_value()); + const auto end = unit_.GetNextSpan(); + EXPECT_FALSE(end.has_value()); +} + +TEST_F(TextMessageBuilderFixture, HeaderShallHaveSpecificElements) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Header of TextMessageBuilder shall have specific elements like context id, application id, ecu id, " + "and number of args."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + unit_.SetNextMessage(log_record_); + + const auto header_span = unit_.GetNextSpan().value(); + const auto string_content = + std::string(reinterpret_cast(header_span.data()), static_cast(header_span.size())); + EXPECT_THAT(string_content, HasSubstr(" TMB ")); + EXPECT_THAT(string_content, HasSubstr(" CTX ")); + EXPECT_THAT(string_content, HasSubstr(" 000 XECU ")); + EXPECT_THAT(string_content, HasSubstr(" 7 ")); // number of args + EXPECT_THAT(string_content, HasSubstr(" verbose ")); + EXPECT_THAT(string_content, HasSubstr(" log ")); + EXPECT_THAT(string_content, HasSubstr(" warn ")); +} + +TEST_F(TextMessageBuilderFixture, PayloadShouldHaveSetText) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Payload of TextMessageBuilder shall have the set text."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + unit_.SetNextMessage(log_record_); + + amp::ignore = unit_.GetNextSpan(); + const auto payload_span = unit_.GetNextSpan().value(); + const auto string_content = + std::string(reinterpret_cast(payload_span.data()), static_cast(payload_span.size())); + EXPECT_THAT(string_content, StrEq("payload")); +} + +INSTANTIATE_TEST_SUITE_P(TestLevels, + TextMessageBuilderFixture, + ::testing::Values(LogLevel::kOff, + LogLevel::kFatal, + LogLevel::kError, + LogLevel::kWarn, + LogLevel::kInfo, + LogLevel::kDebug, + LogLevel::kVerbose)); + +TEST_P(TextMessageBuilderFixture, HeaderShallHaveLevelPrintedForAllParams) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Header of TextMessageBuilder shall have printed level for all parameters."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + auto& log_entry = log_record_.getLogEntry(); + log_entry.log_level = GetParam(); + std::string level_string{levels.at(GetParam())}; + unit_.SetNextMessage(log_record_); + + const auto header_span = unit_.GetNextSpan().value(); + const auto string_content = + std::string(reinterpret_cast(header_span.data()), static_cast(header_span.size())); + EXPECT_THAT(string_content, HasSubstr(level_string)); +} + +TEST_F(TextMessageBuilderFixture, LogLevelToStringShouldReturnUndefinedForInvalidLogLevel) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "LogLevelToString should return 'undefined' for an invalid log level."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + auto& log_entry = log_record_.getLogEntry(); + + // Create an invalid log level by casting back to LogLevel + log_entry.log_level = static_cast(static_cast::type>(999)); + + const std::string kLevelStringUndefined = "undefined"; + unit_.SetNextMessage(log_record_); + + const auto header_span = unit_.GetNextSpan().value(); + const auto string_content = + std::string(reinterpret_cast(header_span.data()), static_cast(header_span.size())); + + EXPECT_THAT(string_content, HasSubstr(kLevelStringUndefined)); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/file_logging/text_recorder.cpp b/mw/log/detail/file_logging/text_recorder.cpp new file mode 100644 index 0000000..0e6e2ac --- /dev/null +++ b/mw/log/detail/file_logging/text_recorder.cpp @@ -0,0 +1,216 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/file_logging/text_recorder.h" + +#include "platform/aas/mw/log/detail/dlt_argument_counter.h" +#include "platform/aas/mw/log/detail/file_logging/text_format.h" +#include "platform/aas/mw/log/detail/log_record.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +namespace +{ + +template +inline void GenericLog(const SlotHandle& slot_handle, detail::Backend& backend, const T data) noexcept +{ + auto& log_record = backend.GetLogRecord(slot_handle); + + detail::DltArgumentCounter counter{log_record.getLogEntry().num_of_args}; + amp::ignore = counter.TryAddArgument( + + [data, &log_record]() noexcept { + if (log_record.getVerbosePayload() + .RemainingCapacity() > // LCOV_EXCL_BR_LINE: lcov complains about lots of uncovered branches, it is + // not convenient/related to this condition. + 0U) + { + detail::TextFormat::Log(log_record.getVerbosePayload(), data); + return detail::AddArgumentResult::Added; + } + else + { + return detail::AddArgumentResult::NotAdded; + } + } + + ); +} + +inline void SlogGenericLog(const SlotHandle& slot_handle, detail::Backend& backend, const LogSlog2Message data) noexcept +{ +#if defined __QNX__ + auto& log_record = backend.GetLogRecord(slot_handle); + auto& log_entry = log_record.getLogEntry(); + log_entry.slog2_code = data.GetCode(); +#endif + + GenericLog(slot_handle, backend, data.GetMessage()); +} + +} // anonymous namespace + +TextRecorder::TextRecorder(const detail::Configuration& config, + + is moved a few lines below. */ + std::unique_ptr backend, + + is moved a few lines below. */ + const bool check_log_level_for_console) noexcept + : Recorder(), + backend_(std::move(backend)), + config_(config), + check_log_level_for_console_{check_log_level_for_console} +{ +} + +amp::optional TextRecorder::StartRecord(const amp::string_view context_id, + const LogLevel log_level) noexcept +{ + if (IsLogEnabled(log_level, context_id) == false) + { + return {}; + } + + auto slot_handle = backend_->ReserveSlot(); + + if (slot_handle.has_value()) + + { + auto& payload = backend_->GetLogRecord(slot_handle.value()); + auto& log_entry = payload.getLogEntry(); + + const auto app_id = config_.GetAppId(); + log_entry.app_id = detail::LoggingIdentifier{app_id}; + log_entry.ctx_id = detail::LoggingIdentifier{context_id}; + log_entry.num_of_args = 0U; + log_entry.log_level = log_level; + payload.getVerbosePayload().Reset(); + } + + return slot_handle; +} + +void TextRecorder::StopRecord(const SlotHandle& slot) noexcept +{ + backend_->FlushSlot(slot); +} + +bool TextRecorder::IsLogEnabled(const LogLevel& log_level, const amp::string_view context) const noexcept +{ + return config_.IsLogLevelEnabled(log_level, context, check_log_level_for_console_); +} + +void TextRecorder::Log(const SlotHandle& slot, const bool data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const std::uint8_t data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const std::int8_t data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const std::uint16_t data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const std::int16_t data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const std::uint32_t data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const std::int32_t data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const std::uint64_t data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const std::int64_t data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const float data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const double data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const LogRawBuffer data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const amp::string_view data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const LogHex8 data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const LogHex16 data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const LogHex32 data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const LogHex64 data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const LogBin8 data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const LogBin16 data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const LogBin32 data) noexcept +{ + GenericLog(slot, *backend_, data); +} +void TextRecorder::Log(const SlotHandle& slot, const LogBin64 data) noexcept +{ + GenericLog(slot, *backend_, data); +} + +void TextRecorder::Log(const SlotHandle& slot, const LogSlog2Message data) noexcept +{ + SlogGenericLog(slot, *backend_, data); +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/file_logging/text_recorder.h b/mw/log/detail/file_logging/text_recorder.h new file mode 100644 index 0000000..177bf89 --- /dev/null +++ b/mw/log/detail/file_logging/text_recorder.h @@ -0,0 +1,85 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_TEXT_RECORDER_H_ +#define PLATFORM_AAS_MW_LOG_TEXT_RECORDER_H_ + +#include "platform/aas/mw/log/configuration/configuration.h" +#include "platform/aas/mw/log/detail/backend.h" +#include "platform/aas/mw/log/recorder.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +class TextRecorder : public Recorder +{ + public: + TextRecorder(const detail::Configuration& config, + std::unique_ptr backend, + const bool check_log_level_for_console) noexcept; + TextRecorder(TextRecorder&&) noexcept = delete; + TextRecorder(const TextRecorder&) noexcept = delete; + TextRecorder& operator=(TextRecorder&&) noexcept = delete; + TextRecorder& operator=(const TextRecorder&) noexcept = delete; + + ~TextRecorder() override = default; + + amp::optional StartRecord(const amp::string_view context_id, + const LogLevel log_level) noexcept override; + void StopRecord(const SlotHandle& slot) noexcept override; + + void Log(const SlotHandle& slot, const bool data) noexcept override; + void Log(const SlotHandle& slot, const std::uint8_t data) noexcept override; + void Log(const SlotHandle& slot, const std::int8_t data) noexcept override; + void Log(const SlotHandle& slot, const std::uint16_t data) noexcept override; + void Log(const SlotHandle& slot, const std::int16_t data) noexcept override; + void Log(const SlotHandle& slot, const std::uint32_t data) noexcept override; + void Log(const SlotHandle& slot, const std::int32_t data) noexcept override; + void Log(const SlotHandle& slot, const std::uint64_t data) noexcept override; + void Log(const SlotHandle& slot, const std::int64_t data) noexcept override; + void Log(const SlotHandle& slot, const float data) noexcept override; + void Log(const SlotHandle& slot, const double data) noexcept override; + void Log(const SlotHandle& slot, const LogRawBuffer data) noexcept override; + void Log(const SlotHandle& slot, const amp::string_view data) noexcept override; + void Log(const SlotHandle& slot, const LogHex8 data) noexcept override; + void Log(const SlotHandle& slot, const LogHex16 data) noexcept override; + void Log(const SlotHandle& slot, const LogHex32 data) noexcept override; + void Log(const SlotHandle& slot, const LogHex64 data) noexcept override; + void Log(const SlotHandle& slot, const LogBin8 data) noexcept override; + void Log(const SlotHandle& slot, const LogBin16 data) noexcept override; + void Log(const SlotHandle& slot, const LogBin32 data) noexcept override; + void Log(const SlotHandle& slot, const LogBin64 data) noexcept override; + void Log(const SlotHandle& slot, const LogSlog2Message data) noexcept override; + + bool IsLogEnabled(const LogLevel&, const amp::string_view) const noexcept override; + + private: + std::unique_ptr backend_; + + detail::Configuration config_; + bool check_log_level_for_console_; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_TEXT_RECORDER_H_ diff --git a/mw/log/detail/file_logging/text_recorder_test.cpp b/mw/log/detail/file_logging/text_recorder_test.cpp new file mode 100644 index 0000000..8a14631 --- /dev/null +++ b/mw/log/detail/file_logging/text_recorder_test.cpp @@ -0,0 +1,492 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/file_logging/text_recorder.h" + +#include "platform/aas/mw/log/detail/backend_mock.h" + +#include "gtest/gtest.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +using ::testing::Invoke; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::ReturnRef; + +constexpr LogLevel kActiveLogLevel = LogLevel::kError; +constexpr LogLevel kInActiveLogLevel = LogLevel::kInfo; +constexpr auto kContext = "ctx0"; +static_assert(static_cast>(kActiveLogLevel) < + static_cast>(kInActiveLogLevel), + "Log Level setup for this test makes no sense."); + +class TextRecorderFixtureWithLogLevelCheck : public testing::Test +{ + public: + void SetUp() override + { + ON_CALL(*backend_, ReserveSlot()).WillByDefault(Return(slot_)); + ON_CALL(*backend_, GetLogRecord(slot_)).WillByDefault(ReturnRef(log_record_)); + const ContextLogLevelMap context_log_level_map{{LoggingIdentifier{context_id_}, kActiveLogLevel}}; + config_.SetContextLogLevel(context_log_level_map); + recorder_ = std::make_unique(config_, std::move(backend_), true); + } + + void TearDown() override {} + + protected: + std::unique_ptr> backend_ = std::make_unique>(); + const amp::string_view context_id_ = "DFLT"; + Configuration config_{}; + std::unique_ptr recorder_; + SlotHandle slot_{}; + LogRecord log_record_{}; +}; + +TEST_F(TextRecorderFixtureWithLogLevelCheck, WillObtainSlotForSufficientLogLevel) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "The required slots will be returned in case of insufficent log level"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + const auto slot = recorder_->StartRecord(context_id_, kActiveLogLevel); + EXPECT_TRUE(slot.has_value()); +} + +TEST_F(TextRecorderFixtureWithLogLevelCheck, WillObtainEmptySlotForInsufficentLogLevel) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Empty slots will be returned in case of insufficent log level"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + const auto slot = recorder_->StartRecord(context_id_, kInActiveLogLevel); + EXPECT_FALSE(slot.has_value()); +} + +TEST_F(TextRecorderFixtureWithLogLevelCheck, DisablesOrEnablesLogAccordingToLevel) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of enabling or disabling specific log level"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_TRUE(recorder_->IsLogEnabled(kActiveLogLevel, context_id_)); + EXPECT_FALSE(recorder_->IsLogEnabled(kInActiveLogLevel, context_id_)); +} + +TEST_F(TextRecorderFixtureWithLogLevelCheck, WillObtainEmptySlotsWhenNoSlotsReserved) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Recorder shall returns zero slots if no slots were reserved."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + auto backend_mock = std::make_unique(); + ON_CALL(*backend_mock, ReserveSlot()).WillByDefault(Invoke([]() -> amp::optional { return {}; })); + + constexpr auto check_log_level_for_console = true; + const auto recorder = std::make_unique(config_, std::move(backend_mock), check_log_level_for_console); + + const auto slot = recorder->StartRecord(context_id_, kActiveLogLevel); + EXPECT_FALSE(slot.has_value()); +} + +class TextRecorderFixture : public ::testing::Test +{ + public: + void SetUp() override + { + config_.SetDefaultConsoleLogLevel(kActiveLogLevel); + EXPECT_CALL(*backend_, ReserveSlot()).WillOnce(Return(slot_)); + EXPECT_CALL(*backend_, FlushSlot(slot_)); + ON_CALL(*backend_, GetLogRecord(slot_)).WillByDefault(ReturnRef(log_record_)); + recorder_ = std::make_unique(config_, std::move(backend_), true); + recorder_->StartRecord(context_id_, kActiveLogLevel); + } + + void TearDown() override + { + const auto& log_entry = log_record_.getLogEntry(); + EXPECT_EQ(log_entry.ctx_id.GetStringView(), context_id_); + EXPECT_EQ(log_entry.log_level, log_level_); + EXPECT_EQ(log_entry.num_of_args, expected_number_of_arguments_at_teardown_); + recorder_->StopRecord(slot_); + } + + protected: + std::unique_ptr> backend_ = std::make_unique>(); + SlotHandle slot_{}; + LogRecord log_record_{}; + LogLevel log_level_ = kActiveLogLevel; + Configuration config_{}; + std::unique_ptr recorder_; + const amp::string_view context_id_ = "DFLT"; + std::uint8_t expected_number_of_arguments_at_teardown_{1}; +}; + +TEST_F(TextRecorderFixture, TooManyArgumentsWillYieldTruncatedLog) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "The log will be truncated in case of too many arguments"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + constexpr std::size_t byte_size_of_space_seperator = 1; + const amp::string_view message{"byte"}; + + const std::size_t number_of_arguments = + log_record_.getLogEntry().payload.capacity() / (message.size() + byte_size_of_space_seperator); + for (std::size_t i = 0; i < number_of_arguments + 5; ++i) + { + recorder_->Log(SlotHandle{}, message); + } + EXPECT_LE(number_of_arguments, std::numeric_limits::max()); + expected_number_of_arguments_at_teardown_ = + static_cast(number_of_arguments); +} + +TEST_F(TextRecorderFixture, TooLargeSinglePayloadWillYieldTruncatedLog) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "The logs will be truncated in case of too large single payload"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + const std::size_t too_big_data_size = log_record_.getLogEntry().payload.capacity() + 1UL; + std::vector vec(too_big_data_size); + std::fill(vec.begin(), vec.end(), 'o'); + recorder_->Log(SlotHandle{}, amp::string_view{vec.data(), too_big_data_size}); + recorder_->Log(SlotHandle{}, amp::string_view{"xxx"}); + + // Teardown checks if number of arguments is equal to one which means that second argument was ignored due to no + // space left in the buffer +} + +TEST_F(TextRecorderFixture, LogUint8_t) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log uint8_t."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, std::uint8_t{}); +} + +TEST_F(TextRecorderFixture, LogBool) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log boolean."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, bool{}); +} + +TEST_F(TextRecorderFixture, LogInt8_t) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log int8_t."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, std::int8_t{}); +} + +TEST_F(TextRecorderFixture, LogUint16_t) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log uint16_t."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, std::uint16_t{}); +} + +TEST_F(TextRecorderFixture, LogInt16_t) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log int16_t."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, std::int16_t{}); +} + +TEST_F(TextRecorderFixture, LogUint32_t) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log uint32_t."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, std::uint32_t{}); +} + +TEST_F(TextRecorderFixture, LogInt32_t) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log int32_t."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, std::int32_t{}); +} + +TEST_F(TextRecorderFixture, LogUint64_t) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log uint64_t."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, std::uint64_t{}); +} + +TEST_F(TextRecorderFixture, LogInt64_t) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log int64_t."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, std::int64_t{}); +} + +TEST_F(TextRecorderFixture, LogFloat) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log float."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, float{}); +} + +TEST_F(TextRecorderFixture, LogDouble) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log double."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, double{}); +} + +TEST_F(TextRecorderFixture, LogStringView) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log string view."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, amp::string_view{"Hello world"}); +} + +TEST_F(TextRecorderFixture, LogHex8) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log 8 bits with hex representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, LogHex8{}); +} + +TEST_F(TextRecorderFixture, LogHex16) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log 16 bits with hex representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + recorder_->Log(SlotHandle{}, LogHex16{}); +} + +TEST_F(TextRecorderFixture, LogHex32) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log 32 bits with hex representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + + recorder_->Log(SlotHandle{}, LogHex32{}); +} + +TEST_F(TextRecorderFixture, LogHex64) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log 64 bits with hex representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + + recorder_->Log(SlotHandle{}, LogHex64{}); +} + +TEST_F(TextRecorderFixture, LogBin8) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log 8 bits with bin representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + + recorder_->Log(SlotHandle{}, LogBin8{}); +} + +TEST_F(TextRecorderFixture, LogBin16) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log 16 bits with bin representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + + recorder_->Log(SlotHandle{}, LogBin16{}); +} + +TEST_F(TextRecorderFixture, LogBin32) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log 32 bits with bin representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + + recorder_->Log(SlotHandle{}, LogBin32{}); +} + +TEST_F(TextRecorderFixture, LogBin64) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log 64 bits with bin representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + + recorder_->Log(SlotHandle{}, LogBin64{}); +} + +TEST_F(TextRecorderFixture, LogRawBuffer) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log raw buffer."); + RecordProperty("TestingTechnique", "Requirements-based test"); + recorder_->Log(SlotHandle{}, LogRawBuffer{"raw", 3}); +} + +TEST_F(TextRecorderFixture, LogSlog2Message) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "TextRecorder can log LogSlog2Message."); + RecordProperty("TestingTechnique", "Requirements-based test"); + recorder_->Log(SlotHandle{}, LogSlog2Message{11, "slog message"}); +} + +TEST(TextRecorderTests, DefaultLogLevelShallBeUsedIfCheckForConsoleIsDisabled) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies that the default log level will be used in case of disabling the console logging."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + constexpr auto default_log_level = LogLevel::kDebug; + constexpr auto more_than_default_log_level = LogLevel::kVerbose; + constexpr auto console_log_level = LogLevel::kOff; + static_assert(more_than_default_log_level > default_log_level, + "Test only makes sense if more_than_default_log_level is higher than default_log_level."); + static_assert(default_log_level > console_log_level, + "Test only makes sense if default log level is higher than console log level."); + + // Test setup is here since we cannot reuse the fixture here because we have a specific construction use case. + Configuration config{}; + config.SetDefaultLogLevel(default_log_level); + config.SetDefaultConsoleLogLevel(console_log_level); + auto backend = std::make_unique>(); + ON_CALL(*backend, ReserveSlot()).WillByDefault(Return(SlotHandle{})); + LogRecord log_record{}; + ON_CALL(*backend, GetLogRecord(testing::_)).WillByDefault(ReturnRef(log_record)); + + // When the check for console is disabled... + constexpr auto check_log_level_for_console = false; + auto recorder = std::make_unique(config, std::move(backend), check_log_level_for_console); + // expect that the default log level is checked; + EXPECT_TRUE(recorder->StartRecord(kContext, default_log_level).has_value()); + EXPECT_FALSE(recorder->StartRecord(kContext, more_than_default_log_level).has_value()); +} + +TEST(TextRecorderTests, TextRecorderShouldClearSlotOnStart) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Recorder should clean slots before reuse."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Test setup is here since we cannot reuse the fixture here because we have a specific construction use case. + Configuration config{}; + config.SetDefaultLogLevel(kActiveLogLevel); + auto backend = std::make_unique>(); + ON_CALL(*backend, ReserveSlot()).WillByDefault(Return(SlotHandle{})); + LogRecord log_record{}; + ON_CALL(*backend, GetLogRecord(testing::_)).WillByDefault(ReturnRef(log_record)); + constexpr auto check_log_level_for_console = false; + auto recorder = std::make_unique(config, std::move(backend), check_log_level_for_console); + + // Simulate the case that a slot already contains data from a previous message. + recorder->StartRecord(kContext, kActiveLogLevel); + const auto payload = amp::string_view{"Hello world"}; + recorder->Log(SlotHandle{}, payload); + recorder->StopRecord(SlotHandle{}); + + // Expect that the previous data is cleared. + recorder->StartRecord(kContext, kActiveLogLevel); + EXPECT_EQ(log_record.getVerbosePayload().GetSpan().size(), 0); + EXPECT_EQ(log_record.getLogEntry().num_of_args, 0); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/initialization_reporter.cpp b/mw/log/detail/initialization_reporter.cpp new file mode 100644 index 0000000..ae0f4ba --- /dev/null +++ b/mw/log/detail/initialization_reporter.cpp @@ -0,0 +1,83 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/initialization_reporter.h" + +#include "platform/aas/mw/log/detail/error.h" + +#include + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +namespace +{ + +constexpr bool kVerboseReporting{false}; + +std::ostream& BeginInitReportStream() noexcept +{ + return std::cerr << "mw::log "; +} + +bool IsErrorVerbose(const bmw::result::Error& error) noexcept +{ + if (error == Error::kConfigurationOptionalJsonKeyNotFound) + { + // Do not bother the user about missing optional entries. + return true; + } + + return false; +} + +} // namespace + +void ReportInitializationError(const bmw::result::Error& error, + const amp::string_view context_info, + const amp::optional app_id) noexcept +{ + if ((kVerboseReporting == false) && IsErrorVerbose(error)) + { + return; + } + + auto& error_stream = BeginInitReportStream() << "initialization error: " << error; + + if (app_id.has_value()) + { + error_stream << " for app "; + std::ignore = error_stream.write(app_id.value().data(), static_cast(app_id.value().size())); + } + + if (context_info.size() > 0) + { + error_stream << " with context information: "; + std::ignore = error_stream.write(context_info.data(), static_cast(context_info.size())); + } + error_stream << '\n'; +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/initialization_reporter.h b/mw/log/detail/initialization_reporter.h new file mode 100644 index 0000000..cb467b1 --- /dev/null +++ b/mw/log/detail/initialization_reporter.h @@ -0,0 +1,44 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_INITIALIZATION_REPORTER_H +#define PLATFORM_AAS_MW_LOG_DETAIL_INITIALIZATION_REPORTER_H + +#include "platform/aas/lib/result/error.h" + +#include "amp_optional.hpp" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +/// \brief Provides user feedback in case of errors during initialization of the logging library. +/// \details During the initialization of logging we need a way to report errors to the user, for example if something +/// is wrong in the configuration files. As "regular" logging is not available at this point, we need to define an +/// alternative mechanism. +void ReportInitializationError(const bmw::result::Error& error, + const amp::string_view context_info = "", + const amp::optional app_id = {}) noexcept; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif diff --git a/mw/log/detail/integer_representation.h b/mw/log/detail/integer_representation.h new file mode 100644 index 0000000..8460a82 --- /dev/null +++ b/mw/log/detail/integer_representation.h @@ -0,0 +1,44 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_NUMBER_FORMAT_H +#define PLATFORM_AAS_MW_LOG_DETAIL_NUMBER_FORMAT_H + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +/// \brief The output representation when formatting the number to a string on the receiver side, e.g. in DltViewer. +/// \note Enum values shall correspond to requirement PRS_Dlt_00783. +enum class IntegerRepresentation : std::uint8_t +{ + kDecimal = 0x00, + kOctal = 0x01, + kHex = 0x02, + kBinary = 0x03 +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif diff --git a/mw/log/detail/istatistics_reporter.cpp b/mw/log/detail/istatistics_reporter.cpp new file mode 100644 index 0000000..4a46e10 --- /dev/null +++ b/mw/log/detail/istatistics_reporter.cpp @@ -0,0 +1,33 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/istatistics_reporter.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + + +IStatisticsReporter::~IStatisticsReporter() noexcept = default; + + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/istatistics_reporter.h b/mw/log/detail/istatistics_reporter.h new file mode 100644 index 0000000..f9a37e3 --- /dev/null +++ b/mw/log/detail/istatistics_reporter.h @@ -0,0 +1,64 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_DATA_ROUTER_ISTATISTICS_REPORTER_H +#define PLATFORM_AAS_MW_LOG_DETAIL_DATA_ROUTER_ISTATISTICS_REPORTER_H + +#include "platform/aas/mw/log/recorder.h" + +#include +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +/// \brief This class shall provide logging-related statistics. +/// \details For instance it shall report the reason and number of dropped messages. +/// The public interface shall be implemented in a thread-safe way, except for the SetRecorder method that should only +/// be called in the initialization phase. +class IStatisticsReporter +{ + public: + IStatisticsReporter() noexcept = default; + IStatisticsReporter(IStatisticsReporter&&) noexcept = delete; + IStatisticsReporter(const IStatisticsReporter&) noexcept = delete; + IStatisticsReporter& operator=(IStatisticsReporter&&) noexcept = delete; + IStatisticsReporter& operator=(const IStatisticsReporter&) noexcept = delete; + + virtual ~IStatisticsReporter() noexcept; + + /// \brief Increment the counter that shows the number of dropped messages due to no free slot available. + /// \details This must be implemented in a thread safe way. + virtual void IncrementNoSlotAvailable() noexcept = 0; + + /// \brief Increment the counter that shows the number of dropped messages due to no free slot available. + /// \details This must be implemented in a thread safe way. + virtual void IncrementMessageTooLong() noexcept = 0; + + /// \brief Send a statistics report if needed. + /// \details This method shall be called periodically so that the implementation is able to send a report. + /// Not every call to Update() will trigger a report, but only few reports are send in a specific interval. + virtual void Update(const std::chrono::steady_clock::time_point& now) noexcept = 0; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_DATA_ROUTER_ISTATISTICS_REPORTER_H diff --git a/mw/log/detail/log_entry.cpp b/mw/log/detail/log_entry.cpp new file mode 100644 index 0000000..313050d --- /dev/null +++ b/mw/log/detail/log_entry.cpp @@ -0,0 +1,16 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#include "platform/aas/mw/log/detail/log_entry.h" diff --git a/mw/log/detail/log_entry.h b/mw/log/detail/log_entry.h new file mode 100644 index 0000000..6cd8397 --- /dev/null +++ b/mw/log/detail/log_entry.h @@ -0,0 +1,89 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_LOG_ENTRY_H +#define PLATFORM_AAS_MW_LOG_DETAIL_LOG_ENTRY_H + +#include "platform/aas/mw/log/detail/logging_identifier.h" +#include "platform/aas/mw/log/log_level.h" +#include "visitor/visit_as_struct.h" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + + +using Byte = char; +using ByteVector = std::vector; + + +struct LogEntry +{ + + LoggingIdentifier app_id{""}; + LoggingIdentifier ctx_id{""}; + + ByteVector payload{}; + std::uint64_t timestamp_steady_nsec{}; + std::uint64_t timestamp_system_nsec{}; + std::uint8_t num_of_args{}; + ByteVector header_buffer{}; + LogLevel log_level{}; +#if defined __QNX__ + std::uint16_t slog2_code{0}; +#endif +}; + +constexpr std::uint8_t GetLogLevelU8FromLogEntry(const LogEntry& entry) +{ + return static_cast(entry.log_level); +} + + + +/* (1) False positive: Line contains a single statement. (2) No unused stuff. (3) Expected.*/ + + +// NOLINTBEGIN(bmw-struct-usage-compliance) justified by design +// Forward declaration for struct_visitable_impl is required for implementation +// std::forward(s) added due to CB-#10171555 +// +// +STRUCT_VISITABLE(LogEntry, + app_id, + ctx_id, + payload, + // timestamp_steady_nsec, + // timestamp_system_nsec, + num_of_args, + // header_buffer, + log_level) +// NOLINTEND(bmw-struct-usage-compliance) justified by design + + + + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_LOG_ENTRY_H diff --git a/mw/log/detail/log_entry_deserialize.h b/mw/log/detail/log_entry_deserialize.h new file mode 100644 index 0000000..c017fd8 --- /dev/null +++ b/mw/log/detail/log_entry_deserialize.h @@ -0,0 +1,62 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_LOG_ENTRY_DESERIALIZE_H +#define PLATFORM_AAS_MW_LOG_DETAIL_LOG_ENTRY_DESERIALIZE_H + +#include "platform/aas/mw/log/detail/logging_identifier.h" +#include "platform/aas/mw/log/log_level.h" +#include "visitor/visit_as_struct.h" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace log_entry_deserialization +{ + +// SerializedVectorData is meant to be used as wrapper type to guide template overload resolution +struct SerializedVectorData +{ + amp::span data; +}; + +// The type closely resemble bmw::mw::log::detail::LogEntry for all the member fields that are going to be deserialize +// with the difference in payload which replaces std::vector with amp::span wrapped in custom structure i.e. +// SerializedVectorData to allow template function overload to overwrite default deserialization behaviour +struct LogEntryDeserializationReflection +{ + bmw::mw::log::detail::LoggingIdentifier app_id{""}; + bmw::mw::log::detail::LoggingIdentifier ctx_id{""}; + SerializedVectorData serialized_vector_data; + std::uint8_t num_of_args; + bmw::mw::log::LogLevel log_level; + + amp::span GetPayload() const noexcept { return serialized_vector_data.data; } +}; + +STRUCT_TRACEABLE(LogEntryDeserializationReflection, app_id, ctx_id, serialized_vector_data, num_of_args, log_level) + +} // namespace log_entry_deserialization +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw +#endif // PLATFORM_AAS_MW_LOG_DETAIL_LOG_ENTRY_DESERIALIZE_H diff --git a/mw/log/detail/log_entry_deserialize_test.cpp b/mw/log/detail/log_entry_deserialize_test.cpp new file mode 100644 index 0000000..177da8d --- /dev/null +++ b/mw/log/detail/log_entry_deserialize_test.cpp @@ -0,0 +1,48 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/log_entry_deserialize.h" + +#include "gtest/gtest.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace log_entry_deserialization +{ +namespace test +{ + +TEST(LogEntryDeserialize, PayloadSpanShallBeAccessible) +{ + const std::array data = {}; + LogEntryDeserializationReflection unit{ + LoggingIdentifier{"ABSD"}, LoggingIdentifier{"DFLT"}, {{data.begin(), data.size()}}, 0UL, LogLevel::kOff}; + + const auto result = unit.GetPayload(); + EXPECT_EQ(result.size(), sizeof(data)); + EXPECT_EQ(result.data(), data.begin()); +} + +} // namespace test +} // namespace log_entry_deserialization +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/log_record.cpp b/mw/log/detail/log_record.cpp new file mode 100644 index 0000000..e89c461 --- /dev/null +++ b/mw/log/detail/log_record.cpp @@ -0,0 +1,139 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#include "platform/aas/mw/log/detail/log_record.h" + +#include "amp_utility.hpp" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +namespace +{ + +void SetupBuffer(LogRecord& dst, const std::size_t capacity) noexcept +{ + // Beware that std::vector move assignment only preserves content, not capacity. + dst.getLogEntry().payload.shrink_to_fit(); + dst.getLogEntry().payload.reserve(capacity); + + // Finally update the reference inside verbosePayload to point to the updated payload buffer. + dst.getVerbosePayload().SetBuffer(dst.getLogEntry().payload); +} + +LogRecord& MoveConstruct(LogRecord& dst, LogRecord& src) noexcept +{ + // Get the capacity before we move from src. + const auto capacity = src.getLogEntry().payload.capacity(); + + // Beware of the lifetimes and **assignment order**: VerbosePayload contains a reference to + // logEntry.verbosePayload_ that would be dangling when logEntry_ is assigned. Thus we update verbosePayload_ first, + // and then logEntry_ to rule out any undefined behavior. + dst.getVerbosePayload() = src.getVerbosePayload(); + dst.getLogEntry() = std::move(src.getLogEntry()); + + SetupBuffer(dst, capacity); + + return dst; +} + +LogRecord& CopyConstruct(LogRecord& dst, const LogRecord& src) noexcept +{ + // Beware of the lifetimes and **assignment order**: VerbosePayload contains a reference to + // logEntry.verbosePayload_ that would be dangling when logEntry_ is assigned. Thus we update verbosePayload_ first, + // and then logEntry_ to rule out any undefined behavior. + dst.getVerbosePayload() = src.getVerbosePayload(); + dst.getLogEntry() = src.getLogEntry(); + + SetupBuffer(dst, src.getLogEntry().payload.capacity()); + + return dst; +} + +} // namespace + +// This is false positive. Constructor is declared only once. +// +LogRecord::LogRecord(const std::size_t max_payload_size_bytes) noexcept + : logEntry_{}, verbosePayload_(max_payload_size_bytes, logEntry_.payload) +{ +} + +LogEntry& LogRecord::getLogEntry() noexcept +{ + // Returning address of non-static class member is justified by design + // + return logEntry_; +} + +detail::VerbosePayload& LogRecord::getVerbosePayload() noexcept +{ + // Returning address of non-static class member is justified by design + // + return verbosePayload_; +} + +const LogEntry& LogRecord::getLogEntry() const noexcept +{ + return logEntry_; +} + +const detail::VerbosePayload& LogRecord::getVerbosePayload() const noexcept +{ + return verbosePayload_; +} + +LogRecord::LogRecord(const LogRecord& other) noexcept + : logEntry_{other.logEntry_}, verbosePayload_{other.verbosePayload_} +{ + amp::ignore = CopyConstruct(*this, other); +} + +// a12_8_4 - Conflict with clang_tidy warning: +// std::move of the expression of the trivially-copyable type 'detail::VerbosePayload' has no effect. +// a3_1_1 - This is false positive. Constructor is declared only once. +// +// +LogRecord::LogRecord(LogRecord&& other) noexcept : logEntry_{}, verbosePayload_{other.verbosePayload_} +{ + amp::ignore = MoveConstruct(*this, other); +} + +LogRecord& LogRecord::operator=(const LogRecord& other) noexcept +{ + if (this == &other) + { + return *this; + } + amp::ignore = CopyConstruct(*this, other); + return *this; +} + +LogRecord& LogRecord::operator=(LogRecord&& other) noexcept +{ + amp::ignore = MoveConstruct(*this, other); + return *this; +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/log_record.h b/mw/log/detail/log_record.h new file mode 100644 index 0000000..e9fbd05 --- /dev/null +++ b/mw/log/detail/log_record.h @@ -0,0 +1,62 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_RECORD_H +#define PLATFORM_AAS_MW_LOG_DETAIL_RECORD_H + +#include "platform/aas/mw/log/detail/log_entry.h" +#include "platform/aas/mw/log/detail/verbose_payload.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + + +class LogRecord +{ + public: + explicit LogRecord(const std::size_t max_payload_size_bytes = 255U) noexcept; + + LogEntry& getLogEntry() noexcept; + const LogEntry& getLogEntry() const noexcept; + detail::VerbosePayload& getVerbosePayload() noexcept; + const detail::VerbosePayload& getVerbosePayload() const noexcept; + + /// Rule of five: Need manual constructors and assignment operator to update the internal reference correctly. + ~LogRecord() noexcept = default; + LogRecord(const LogRecord&) noexcept; + LogRecord(LogRecord&&) noexcept; + LogRecord& operator=(const LogRecord&) noexcept; + LogRecord& operator=(LogRecord&&) noexcept; + + private: + LogEntry logEntry_; + + // Caution: contains a reference to logEntry_ internals. + detail::VerbosePayload verbosePayload_; +}; + + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif diff --git a/mw/log/detail/log_record_test.cpp b/mw/log/detail/log_record_test.cpp new file mode 100644 index 0000000..a36b2da --- /dev/null +++ b/mw/log/detail/log_record_test.cpp @@ -0,0 +1,149 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/log_record.h" + +#include "amp_optional.hpp" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ +constexpr std::size_t kMaxPayloadSize = 64U; +constexpr std::size_t kMaxPayloadSizeBiggerSrc = 256U; +constexpr std::size_t kMaxPayloadSizeSmallerSrc = 16U; + +class LogRecordCopyAndMoveOperatorsFixture : public ::testing::TestWithParam +{ + protected: + LogRecord GetSource() const noexcept + { + LogRecord src{GetParam()}; + src.getLogEntry().payload.resize(GetParam() - GetSourceCapacity()); + return src; + } + + std::size_t GetSourceCapacity() const noexcept { return GetParam() / 2; } +}; + +TEST(LogRecord, LogRecordShallReturnExpectedLogEntry) +{ + RecordProperty("Requirement", ", 1016719"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "LogRecord can be constructed with max payload size."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + LogRecord unit{kMaxPayloadSize}; + EXPECT_EQ(unit.getLogEntry().payload.capacity(), kMaxPayloadSize); +} + +TEST(LogRecord, LogRecordShallReturnExpectedVerbosePayload) +{ + RecordProperty("Requirement", ", 1016719"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "LogRecord can provide the expected verbose payload."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + LogRecord unit{kMaxPayloadSize}; + EXPECT_EQ(unit.getVerbosePayload().RemainingCapacity(), kMaxPayloadSize); +} + +TEST_P(LogRecordCopyAndMoveOperatorsFixture, LogRecordShallCopyAssignAndUpdateReferenceCorrectly) +{ + RecordProperty("Requirement", ", 1016719"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "LogRecord can provide copy assignment operator with valid state."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + LogRecord unit{kMaxPayloadSize}; + { + // Test lifetime correctness by letting src go out of scope after assignment. + const LogRecord src{GetSource()}; + unit = src; + } + EXPECT_EQ(unit.getVerbosePayload().RemainingCapacity(), GetSourceCapacity()); +} + +TEST_P(LogRecordCopyAndMoveOperatorsFixture, LogRecordShallCopyConstructAndUpdateReferenceCorrectly) +{ + RecordProperty("Requirement", ", 1016719"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "LogRecord can provide copy constructor with valid state."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + amp::optional unit{}; + { + // Test lifetime correctness by letting src go out of scope after assignment. + const LogRecord src{GetSource()}; + unit.emplace(src); + } + EXPECT_EQ(unit->getVerbosePayload().RemainingCapacity(), GetSourceCapacity()); +} + +TEST_P(LogRecordCopyAndMoveOperatorsFixture, LogRecordShallMoveAssignAndUpdateReferenceCorrectly) +{ + RecordProperty("Requirement", ", 1016719"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "LogRecord can provide move assigment operator with validstate."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + LogRecord unit{kMaxPayloadSize}; + { + // Test lifetime correctness by letting src go out of scope after construction. + LogRecord src{GetSource()}; + unit = std::move(src); + } + EXPECT_EQ(unit.getVerbosePayload().RemainingCapacity(), GetSourceCapacity()); +} + +TEST_P(LogRecordCopyAndMoveOperatorsFixture, LogRecordShallMoveConstructAndUpdateReferenceCorrectly) +{ + RecordProperty("Requirement", ", 1016719"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "LogRecord can provide move constructor with valid state."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + amp::optional unit{}; + { + // Test lifetime correctness by letting src go out of scope after construction. + LogRecord src{GetSource()}; + unit.emplace(std::move(src)); + } + EXPECT_EQ(unit->getVerbosePayload().RemainingCapacity(), GetSourceCapacity()); +} + +INSTANTIATE_TEST_SUITE_P(LogRecordMoveAndCopyTests, + LogRecordCopyAndMoveOperatorsFixture, + ::testing::Values(kMaxPayloadSizeBiggerSrc, kMaxPayloadSizeSmallerSrc)); + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/logging_identifier.cpp b/mw/log/detail/logging_identifier.cpp new file mode 100644 index 0000000..ff15270 --- /dev/null +++ b/mw/log/detail/logging_identifier.cpp @@ -0,0 +1,62 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/logging_identifier.h" + +#include "amp_utility.hpp" +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +LoggingIdentifier::LoggingIdentifier(const amp::string_view identifier) noexcept +{ + data_.fill(0U); + amp::ignore = std::copy_n(identifier.begin(), std::min(identifier.size(), data_.size()), data_.begin()); +} + +amp::string_view LoggingIdentifier::GetStringView() const noexcept +{ + return amp::string_view{data_.data(), strnlen(data_.data(), kMaxLength)}; +} + +std::size_t LoggingIdentifier::HashFunction::operator()(const LoggingIdentifier& id) const +{ + std::int32_t value{}; + static_assert(sizeof(value) == sizeof(LoggingIdentifier::data_), "data_ must have the correct size"); + // NOLINTNEXTLINE(bmw-banned-function) memcpy is needed + amp::ignore = std::memcpy(&value, id.data_.data(), id.data_.size()); + return std::hash{}(value); +} + +bool operator==(const LoggingIdentifier& lhs, const LoggingIdentifier& rhs) noexcept +{ + return std::equal(lhs.data_.begin(), lhs.data_.end(), rhs.data_.begin()); +} + +bool operator!=(const LoggingIdentifier& lhs, const LoggingIdentifier& rhs) noexcept +{ + return !(lhs == rhs); +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/logging_identifier.h b/mw/log/detail/logging_identifier.h new file mode 100644 index 0000000..3488aa5 --- /dev/null +++ b/mw/log/detail/logging_identifier.h @@ -0,0 +1,93 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_LOGGING_IDENTIFIER_H +#define PLATFORM_AAS_MW_LOG_DETAIL_LOGGING_IDENTIFIER_H + +#include "amp_string_view.hpp" + +#include "visitor/visit_as_struct.h" + +#include +#include + + +namespace bmw + +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + + +/// \brief Contains a 4 byte identifier that can be used for ECU, application or context identifiers. +class LoggingIdentifier +{ + public: + /// \brief Crops the given string view to a maximum of kMaxLength bytes. + explicit LoggingIdentifier(const amp::string_view) noexcept; + + /// \brief Returns the underlying fixed-length string view. + amp::string_view GetStringView() const noexcept; + + /// \brief Use this to enable LoggingIdentifier as a key in a map. + class HashFunction + { + public: + size_t operator()(const LoggingIdentifier& id) const; + }; + + friend bool operator==(const LoggingIdentifier&, const LoggingIdentifier&) noexcept; + friend bool operator!=(const LoggingIdentifier&, const LoggingIdentifier&) noexcept; + + LoggingIdentifier() noexcept = default; + LoggingIdentifier& operator=(const LoggingIdentifier& rhs) noexcept = default; + LoggingIdentifier& operator=(LoggingIdentifier&& rhs) noexcept = default; + LoggingIdentifier(const LoggingIdentifier&) noexcept = default; + LoggingIdentifier(LoggingIdentifier&&) noexcept = default; + ~LoggingIdentifier() noexcept = default; + + /// \brief This maximum is fixed to 4 bytes by the DLT protocol standard. + constexpr static std::size_t kMaxLength{4UL}; + + // This variable is public because of necessity to pass it to STRUCT_VISITABLE in log_entry.h + std::array data_{}; +}; + + + + +/* (1) False positive: Line contains a single statement. (2) No unused stuff. (3) Expected.*/ + +// NOLINTBEGIN(bmw-struct-usage-compliance) justified by design +// Forward declaration for struct_visitable_impl is required for implementation +// std::forward(s) added due to CB-#10171555 +// +// +STRUCT_VISITABLE(LoggingIdentifier, data_) + +// NOLINTEND(bmw-struct-usage-compliance) justified by design + + + + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_RUNTIME_H diff --git a/mw/log/detail/logging_identifier_test.cpp b/mw/log/detail/logging_identifier_test.cpp new file mode 100644 index 0000000..7c48267 --- /dev/null +++ b/mw/log/detail/logging_identifier_test.cpp @@ -0,0 +1,104 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/logging_identifier.h" + +#include "gtest/gtest.h" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +TEST(LoggingIdentifierTestSuite, CheckThatLongIdentifiersShallBeCropped) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "logging identifier has maximum length in which the identifier will be cropped."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + LoggingIdentifier identifier{"12345"}; + EXPECT_EQ(identifier.GetStringView(), amp::string_view{"1234"}); +} + +TEST(LoggingIdentifierTestSuite, CheckThatHashMatchesIntHasher) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies that hash matches int hasher."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + amp::string_view ctx{"CTX1"}; + std::int32_t val{}; + std::memcpy(&val, ctx.data(), sizeof(val)); + LoggingIdentifier identifier{ctx}; + EXPECT_EQ(LoggingIdentifier::HashFunction{}(identifier), std::hash{}(val)); +} + +TEST(LoggingIdentifierTestSuite, EqualityOperatorShallReturnTrueForTheSameString) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "logging identifiers with the same context name shall be equal."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + amp::string_view ctx{"CTX1"}; + LoggingIdentifier lhs{ctx}; + LoggingIdentifier rhs{ctx}; + EXPECT_EQ(lhs, rhs); +} + +TEST(LoggingIdentifierTestSuite, InequalityOperatorShallReturnTrueForDifferentStrings) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "logging identifiers with the different context name shall not be equal."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + LoggingIdentifier lhs{amp::string_view{"CTX1"}}; + LoggingIdentifier rhs{amp::string_view{"CTX"}}; + EXPECT_TRUE(lhs != rhs); +} + +TEST(LoggingIdentifierTestSuite, AssignOperatorShallCopyLoggingIdentifier) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Check the assign operator functionality."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + amp::string_view ctx{"CTX1"}; + amp::string_view ctx2{"CTX2"}; + LoggingIdentifier identifier{ctx}; + identifier = LoggingIdentifier{ctx2}; + EXPECT_EQ(identifier.GetStringView(), ctx2); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/recorder_factory.cpp b/mw/log/detail/recorder_factory.cpp new file mode 100644 index 0000000..67da84d --- /dev/null +++ b/mw/log/detail/recorder_factory.cpp @@ -0,0 +1,353 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/recorder_factory.h" + +#include "platform/aas/lib/os/utils/signal_impl.h" +#include "platform/aas/mw/log/detail/composite_recorder.h" +#include "platform/aas/mw/log/detail/data_router/data_router_backend.h" +#include "platform/aas/mw/log/detail/data_router/data_router_message_client_factory_impl.h" +#include "platform/aas/mw/log/detail/data_router/data_router_recorder.h" +#include "platform/aas/mw/log/detail/data_router/message_passing_factory_impl.h" +#include "platform/aas/mw/log/detail/empty_recorder.h" +#include "platform/aas/mw/log/detail/error.h" +#include "platform/aas/mw/log/detail/file_logging/dlt_message_builder.h" +#include "platform/aas/mw/log/detail/file_logging/file_output_backend.h" +#include "platform/aas/mw/log/detail/file_logging/file_recorder.h" +#include "platform/aas/mw/log/detail/file_logging/text_message_builder.h" +#include "platform/aas/mw/log/detail/file_logging/text_recorder.h" +#include "platform/aas/mw/log/detail/initialization_reporter.h" + +#ifdef __QNXNTO__ +#include "platform/aas/mw/log/detail/slog/slog_backend.h" +#endif + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +namespace +{ +std::unique_ptr CreateConsoleLoggingBackend(const Configuration& config, + amp::pmr::memory_resource* memory_resource) noexcept +{ + + * below. */ + auto message_builder = std::make_unique(config.GetEcuId()); + auto allocator = std::make_unique>(config.GetNumberOfSlots(), + LogRecord{config.GetSlotSizeInBytes()}); + + * below. */ + + + + + return std::make_unique(std::move(message_builder), + STDOUT_FILENO, + std::move(allocator), + bmw::os::FcntlImpl::Default(memory_resource), + bmw::os::Unistd::Default(memory_resource)); + + + +} + +#ifdef __QNXNTO__ +std::unique_ptr CreateSystemBackend(const Configuration& config, amp::pmr::memory_resource* memory_resource) +{ + return std::make_unique(config.GetNumberOfSlots(), + LogRecord{config.GetSlotSizeInBytes()}, + config.GetAppId(), + bmw::os::qnx::Slog2Impl::Default(memory_resource)); +} +#endif + + +std::unique_ptr CreateFileLoggingBackend(const Configuration& config, + amp::pmr::memory_resource* memory_resource, + amp::pmr::unique_ptr fcntl_instance) noexcept + +{ + const std::string file_name{std::string(config.GetLogFilePath().data(), config.GetLogFilePath().size()) + "/" + + std::string{config.GetAppId().data(), config.GetAppId().size()} + ".dlt"}; + + // NOLINTBEGIN(bmw-banned-function): FileLoggingBackend is disabled in production. Argumentation: + const auto descriptor_result = fcntl_instance->open( + file_name.data(), + bmw::os::Fcntl::Open::kWriteOnly | bmw::os::Fcntl::Open::kCreate | bmw::os::Fcntl::Open::kCloseOnExec, + bmw::os::Stat::Mode::kReadUser | bmw::os::Stat::Mode::kWriteUser | bmw::os::Stat::Mode::kReadGroup | + bmw::os::Stat::Mode::kReadOthers); + // NOLINTEND(bmw-banned-function): see above for detailed explanation + + std::int32_t descriptor{0}; + + if (descriptor_result.has_value()) + + { + descriptor = descriptor_result.value(); + } + else + { + + ReportInitializationError(Error::kLogFileCreationFailed, descriptor_result.error().ToString()); + + return nullptr; + } + + + auto message_builder = std::make_unique(config.GetEcuId()); + auto allocator = std::make_unique>(config.GetNumberOfSlots(), + LogRecord{config.GetSlotSizeInBytes()}); + + + return std::make_unique(std::move(message_builder), + descriptor, + std::move(allocator), + bmw::os::Fcntl::Default(memory_resource), + bmw::os::Unistd::Default(memory_resource)); +} + +} // namespace + + + +std::unique_ptr RecorderFactory::CreateRecorderFromLogMode( + const LogMode& log_mode, + const Configuration& config, + amp::pmr::unique_ptr fcntl_instance, + amp::pmr::memory_resource* memory_resource) const noexcept + + +{ + if (memory_resource == nullptr) + { + ReportInitializationError(bmw::mw::log::detail::Error::kMemoryResourceError); + return CreateStub(); + } + + switch (log_mode) + { + case LogMode::kRemote: + { + return GetRemoteRecorder(config, memory_resource); + } + case LogMode::kFile: + { + return GetFileRecorder(config, std::move(fcntl_instance), memory_resource); + } + case LogMode::kConsole: + { + return GetConsoleRecorder(config, memory_resource); + } + case LogMode::kSystem: + { +#ifdef __QNXNTO__ + return GetSystemRecorder(config, memory_resource); +#else + ReportInitializationError(Error::kRecorderFactoryUnsupportedLogMode); + return std::make_unique(); +#endif + } + case LogMode::kInvalid: // Intentional fall-through + default: + { + ReportInitializationError(Error::kRecorderFactoryUnsupportedLogMode); + return std::make_unique(); + } + } +} + +std::unique_ptr RecorderFactory::GetRemoteRecorder(const Configuration& config, + amp::pmr::memory_resource* memory_resource) noexcept +{ + auto message_client_factory = std::make_unique( + config, + std::make_unique(), + MsgClientUtils{bmw::os::Unistd::Default(memory_resource), + bmw::os::Pthread::Default(memory_resource), + amp::pmr::make_unique(memory_resource)}); + + + WriterFactory::OsalInstances writer_factory_osal = {bmw::os::Fcntl::Default(memory_resource), + bmw::os::Unistd::Default(memory_resource), + bmw::os::Mman::Default(memory_resource), + bmw::os::Stat::Default(memory_resource), + bmw::os::Stdlib::Default(memory_resource)}; + + return std::make_unique( + std::make_unique(config.GetNumberOfSlots(), + LogRecord{config.GetSlotSizeInBytes()}, + *message_client_factory, + config, + WriterFactory{std::move(writer_factory_osal)}), + config); + + +} + +std::unique_ptr RecorderFactory::GetFileRecorder(const Configuration& config, + amp::pmr::unique_ptr fcntl_instance, + amp::pmr::memory_resource* memory_resource) noexcept +{ + auto backend = CreateFileLoggingBackend(config, memory_resource, std::move(fcntl_instance)); + if (backend == nullptr) + { + return std::make_unique(); + } + + + return std::make_unique(config, std::move(backend)); + + +} + +std::unique_ptr RecorderFactory::GetConsoleRecorder(const Configuration& config, + amp::pmr::memory_resource* memory_resource) noexcept +{ + auto backend = CreateConsoleLoggingBackend(config, memory_resource); + constexpr bool check_log_level_for_console = true; + + + return std::make_unique(config, std::move(backend), check_log_level_for_console); + + +} + +#ifdef __QNXNTO__ +std::unique_ptr RecorderFactory::GetSystemRecorder(const Configuration& config, + amp::pmr::memory_resource* memory_resource) noexcept +{ + auto backend = CreateSystemBackend(config, memory_resource); + constexpr bool check_log_level_for_console = false; + return std::make_unique(config, std::move(backend), check_log_level_for_console); +} +#endif + + +std::unique_ptr RecorderFactory::CreateFromConfiguration( + const std::unique_ptr config_reader, + amp::pmr::memory_resource* memory_resource) const noexcept + +{ + if (memory_resource == nullptr) + { + ReportInitializationError(bmw::mw::log::detail::Error::kMemoryResourceError); + return CreateStub(); + } + const auto result = config_reader->ReadConfig(); + + + if (!result.has_value()) + + { + ReportInitializationError(result.error(), "Failed to load configuration files. Fallback to console logging."); + return CreateWithConsoleLoggingOnly(memory_resource); + } + + std::vector> recorders; + + /* The lambda will be executed within this stack. Thus, all references are still valid */ + amp::ignore = + std::for_each(result->GetLogMode().begin(), + result->GetLogMode().end(), + [this, &result, &recorders, &memory_resource](const auto& log_mode) noexcept { + amp::ignore = recorders.emplace_back(CreateRecorderFromLogMode( + log_mode, result.value(), bmw::os::Fcntl::Default(memory_resource), memory_resource)); + }); + + + if (recorders.empty()) + { + ReportInitializationError(Error::kNoLogModeSpecified); + return std::make_unique(); + } + + if (recorders.size() == 1U) + { + return std::move(recorders[0]); + } + + + + // Composite recorder is needed iff there are more than one activate recorder. + return std::make_unique(std::move(recorders)); + + +} + + +std::unique_ptr RecorderFactory::CreateWithConsoleLoggingOnly( + amp::pmr::memory_resource* memory_resource) const noexcept + +{ + if (memory_resource == nullptr) + { + ReportInitializationError(bmw::mw::log::detail::Error::kMemoryResourceError); + return CreateStub(); + } + const Configuration config; // using all default values + auto backend = CreateConsoleLoggingBackend(config, memory_resource); + constexpr bool check_log_level_for_console = false; + + + return std::make_unique(config, std::move(backend), check_log_level_for_console); + + +} + + +std::unique_ptr RecorderFactory::CreateStub() const noexcept + +{ + return std::make_unique(); +} + + +std::unique_ptr CreateRecorderFactory() noexcept + +{ + return std::make_unique(); +} + + +std::unique_ptr RecorderFactory::CreateFromConfiguration( + amp::pmr::memory_resource* memory_resource) const noexcept + +{ + if (memory_resource == nullptr) + { + ReportInitializationError(bmw::mw::log::detail::Error::kMemoryResourceError); + return CreateStub(); + } + + + return CreateFromConfiguration(std::make_unique(std::make_unique( + bmw::os::Path::Default(memory_resource), + bmw::os::Stdlib::Default(memory_resource), + bmw::os::Unistd::Default(memory_resource))), + memory_resource); + +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/recorder_factory.h b/mw/log/detail/recorder_factory.h new file mode 100644 index 0000000..7615634 --- /dev/null +++ b/mw/log/detail/recorder_factory.h @@ -0,0 +1,75 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_RECORDER_FACTORY_H +#define PLATFORM_AAS_MW_LOG_DETAIL_RECORDER_FACTORY_H + +#include "platform/aas/lib/os/fcntl_impl.h" +#include "platform/aas/mw/log/configuration/target_config_reader.h" +#include "platform/aas/mw/log/irecorder_factory.h" +#include "platform/aas/mw/log/recorder.h" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +class RecorderFactory : public IRecorderFactory +{ + public: + RecorderFactory() noexcept = default; + + std::unique_ptr CreateFromConfiguration( + amp::pmr::memory_resource* memory_resource) const noexcept override; + std::unique_ptr CreateFromConfiguration(const std::unique_ptr config_reader, + amp::pmr::memory_resource* memory_resource) const noexcept; + + std::unique_ptr CreateWithConsoleLoggingOnly( + amp::pmr::memory_resource* memory_resource) const noexcept override; + + std::unique_ptr CreateStub() const noexcept override; + + std::unique_ptr CreateRecorderFromLogMode( + const LogMode& log_mode, + const Configuration& config, + amp::pmr::unique_ptr fcntl_instance = + bmw::os::FcntlImpl::Default(amp::pmr::get_default_resource()), + amp::pmr::memory_resource* memory_resource = amp::pmr::get_default_resource()) const noexcept; + + private: + static std::unique_ptr GetRemoteRecorder(const Configuration& config, + amp::pmr::memory_resource* memory_resource) noexcept; + static std::unique_ptr GetFileRecorder(const Configuration& config, + amp::pmr::unique_ptr fcntl_instance, + amp::pmr::memory_resource* memory_resource) noexcept; + static std::unique_ptr GetConsoleRecorder(const Configuration& config, + amp::pmr::memory_resource* memory_resource) noexcept; +#ifdef __QNXNTO__ + static std::unique_ptr GetSystemRecorder(const Configuration& config, + amp::pmr::memory_resource* memory_resource) noexcept; +#endif +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_RECORDER_FACTORY_H diff --git a/mw/log/detail/recorder_factory_test.cpp b/mw/log/detail/recorder_factory_test.cpp new file mode 100644 index 0000000..7a73fe3 --- /dev/null +++ b/mw/log/detail/recorder_factory_test.cpp @@ -0,0 +1,348 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/recorder_factory.h" + +#include "platform/aas/mw/log/configuration/target_config_reader_mock.h" +#include "platform/aas/mw/log/detail/composite_recorder.h" +#include "platform/aas/mw/log/detail/data_router/data_router_recorder.h" +#include "platform/aas/mw/log/detail/empty_recorder.h" +#include "platform/aas/mw/log/detail/error.h" +#include "platform/aas/mw/log/detail/file_logging/file_recorder.h" +#include "platform/aas/mw/log/detail/file_logging/text_recorder.h" + +#include "platform/aas/lib/os/mocklib/fcntl_mock.h" + +#include "gtest/gtest.h" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +using ::testing::_; +using ::testing::Return; + +template +bool IsRecorderOfType(const std::unique_ptr& recorder) noexcept +{ + static_assert(std::is_base_of::value, + "Concrete recorder shall be derived from Recorder base class"); + + return dynamic_cast(recorder.get()) != nullptr; +} + +template +bool ContainsRecorderOfType(const CompositeRecorder& composite) noexcept +{ + bool return_value = false; + for (const auto& recorder : composite.GetRecorders()) + { + if (IsRecorderOfType(recorder)) + { + return_value = true; + break; + } + } + return return_value; +} + +class RecorderFactoryConfigFixture : public ::testing::Test +{ + public: + void SetUp() override { memory_resource_ = amp::pmr::get_default_resource(); } + void TearDown() override {} + + std::unique_ptr CreateFromConfiguration() noexcept + { + auto config_reader_mock = std::make_unique(); + ON_CALL(*config_reader_mock, ReadConfig).WillByDefault(testing::Invoke([&]() { return config_result_; })); + return RecorderFactory{}.CreateFromConfiguration(std::move(config_reader_mock), + amp::pmr::get_default_resource()); + } + + void SetTargetConfigReaderResult(bmw::Result result) noexcept { config_result_ = result; } + + void SetConfigurationWithLogMode(const std::unordered_set& log_modes, + Configuration config = Configuration{}) noexcept + { + config.SetLogMode(log_modes); + SetTargetConfigReaderResult(config); + } + + protected: + bmw::Result config_result_; + amp::pmr::memory_resource* memory_resource_ = nullptr; +}; + +TEST_F(RecorderFactoryConfigFixture, ConfigurationMemoryErrorShallReturnEmptyRecorder) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "RecorderFactory will return empty recorder in case of configuration error."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + amp::pmr::memory_resource* null_memory_resource = nullptr; + auto recorder = RecorderFactory{}.CreateFromConfiguration(null_memory_resource); + EXPECT_TRUE(IsRecorderOfType(recorder)); +} + +TEST_F(RecorderFactoryConfigFixture, ConfigurationWithConfigReaderMemoryErrorShallReturnEmptyRecorder) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "RecorderFactory will return empty recorder in case of configuration error."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + auto config_reader_mock = std::make_unique(); + amp::pmr::memory_resource* null_memory_resource = nullptr; + auto recorder = RecorderFactory{}.CreateFromConfiguration(std::move(config_reader_mock), null_memory_resource); + EXPECT_TRUE(IsRecorderOfType(recorder)); +} + +TEST_F(RecorderFactoryConfigFixture, CreateConsoleLoggingOnlyMemoryErrorShallReturnEmptyRecorder) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Create Console Logging Recorder will return empty recorder in case of memory error."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + amp::pmr::memory_resource* null_memory_resource = nullptr; + auto recorder = RecorderFactory{}.CreateWithConsoleLoggingOnly(null_memory_resource); + EXPECT_TRUE(IsRecorderOfType(recorder)); +} + +TEST_F(RecorderFactoryConfigFixture, CreateRecorderFromLogModeMemoryErrorShallReturnEmptyRecorder) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Recorder created using LogMode will return empty recorder in case of memory error."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + amp::pmr::memory_resource* null_memory_resource = nullptr; + Configuration config{}; + auto fcntl_mock = amp::pmr::make_unique(memory_resource_); + auto recorder = RecorderFactory{}.CreateRecorderFromLogMode( + LogMode::kFile, config, std::move(fcntl_mock), null_memory_resource); + EXPECT_TRUE(IsRecorderOfType(recorder)); +} + +TEST_F(RecorderFactoryConfigFixture, ConfigurationErrorShallFallbackToConsoleLogging) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "RecorderFactory will return text recorder in case of configuration error."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + SetTargetConfigReaderResult(bmw::MakeUnexpected(Error::kConfigurationFilesNotFound)); + auto recorder = CreateFromConfiguration(); + EXPECT_TRUE(IsRecorderOfType(recorder)); +} + +TEST_F(RecorderFactoryConfigFixture, NoRecorderConfiguredShallReturnEmptyRecorder) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "RecorderFactory can return empty recorder if no recorder is configured."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // TargetConfigReader shall return a config with no active recorders. + SetConfigurationWithLogMode({}); + + auto recorder = CreateFromConfiguration(); + EXPECT_TRUE(IsRecorderOfType(recorder)); +} + +TEST_F(RecorderFactoryConfigFixture, RemoteConfiguredShallReturnDataRouterRecorder) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "RecorderFactory can create DataRouterRecorder if remote is configured."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + SetConfigurationWithLogMode({LogMode::kRemote}); + auto recorder = CreateFromConfiguration(); + EXPECT_TRUE(IsRecorderOfType(recorder)); +} + +TEST_F(RecorderFactoryConfigFixture, FileConfiguredShallReturnFileRecorder) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "RecorderFactory can create FileRecorder if file is configured."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + SetConfigurationWithLogMode({LogMode::kFile}); + auto recorder = CreateFromConfiguration(); + EXPECT_TRUE(IsRecorderOfType(recorder)); +} + +TEST_F(RecorderFactoryConfigFixture, FileConfiguredShallReturnEmptyRecorderWithInvalidFile) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "RecorderFactory can create FileRecorder if file is configured."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + Configuration config; + constexpr auto kInvalidPath = "!@#"; + config.SetLogFilePath(kInvalidPath); + SetConfigurationWithLogMode({LogMode::kFile}, config); + auto recorder = CreateFromConfiguration(); + EXPECT_TRUE(IsRecorderOfType(recorder)); +} + +TEST_F(RecorderFactoryConfigFixture, FileConfigurionShallCallFileCreationAndReturnFileRecorder) +{ + RecordProperty("Requirement", ", 7"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "RecorderFactory can create FileRecorder and fileCreation will be called if file is configured. The " + "component shall set the FD_CLOEXEC (or O_CLOEXEC) flag on all the file descriptor it owns"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + auto fcntl_mock = amp::pmr::make_unique(memory_resource_); + auto fcntl_mock_raw_ptr = fcntl_mock.get(); + std::int32_t file_descriptor = 3; + + const bmw::os::Fcntl::Open open_flags = + bmw::os::Fcntl::Open::kWriteOnly | bmw::os::Fcntl::Open::kCreate | bmw::os::Fcntl::Open::kCloseOnExec; + const bmw::os::Stat::Mode access_flags = bmw::os::Stat::Mode::kReadUser | bmw::os::Stat::Mode::kWriteUser | + bmw::os::Stat::Mode::kReadGroup | bmw::os::Stat::Mode::kReadOthers; + + EXPECT_CALL(*fcntl_mock_raw_ptr, open(_, open_flags, access_flags)).Times(1).WillOnce(Return(file_descriptor)); + + Configuration config; + + auto recorder = + RecorderFactory{}.CreateRecorderFromLogMode(LogMode::kFile, config, std::move(fcntl_mock), memory_resource_); + + EXPECT_TRUE(IsRecorderOfType(recorder)); +} + +TEST_F(RecorderFactoryConfigFixture, ConsoleConfiguredShallReturnConsoleRecorder) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "RecorderFactory can create ConsoleRecorder if console is configured."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + SetConfigurationWithLogMode({LogMode::kConsole}); + auto recorder = CreateFromConfiguration(); + EXPECT_TRUE(IsRecorderOfType(recorder)); +} + +TEST_F(RecorderFactoryConfigFixture, InvalidLogModeShallReturnEmptyRecorder) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "RecorderFactory will create EmptyRecorder in case of invalid log mode."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + SetConfigurationWithLogMode({LogMode::kInvalid}); + auto recorder = CreateFromConfiguration(); + EXPECT_TRUE(IsRecorderOfType(recorder)); +} + +TEST_F(RecorderFactoryConfigFixture, MultipleLogModesShallReturnCompositeRecorder) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "RecorderFactory shall create CompositeRecorder in case of multiple log mode is configured."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + SetConfigurationWithLogMode({LogMode::kFile, LogMode::kConsole, LogMode::kRemote}); + auto recorder = CreateFromConfiguration(); + ASSERT_TRUE(IsRecorderOfType(recorder)); + const auto& composite_recorder = *dynamic_cast(recorder.get()); + + EXPECT_EQ(composite_recorder.GetRecorders().size(), config_result_->GetLogMode().size()); + EXPECT_TRUE(ContainsRecorderOfType(composite_recorder)); + EXPECT_TRUE(ContainsRecorderOfType(composite_recorder)); + EXPECT_TRUE(ContainsRecorderOfType(composite_recorder)); +} + +TEST_F(RecorderFactoryConfigFixture, CreateWithConsoleLoggingOnlyShallReturnConsoleRecorder) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of creating a specific logging."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + auto recorder = RecorderFactory{}.CreateWithConsoleLoggingOnly(amp::pmr::get_default_resource()); + + EXPECT_TRUE(IsRecorderOfType(recorder)); +} + +TEST_F(RecorderFactoryConfigFixture, CreateStubShallReturnEmptyRecorder) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "RecorderFactory shall create EmptyRecorder in case of using CreateStub."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + auto recorder = RecorderFactory{}.CreateStub(); + + EXPECT_TRUE(IsRecorderOfType(recorder)); +} + +TEST_F(RecorderFactoryConfigFixture, SystemConfiguredShallReturnSlogRecorder) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("Description", + "The system logger backend shall be enabled if and only if the log mode contains 'kSystem'"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + SetConfigurationWithLogMode({LogMode::kSystem}); + auto recorder = CreateFromConfiguration(); + +#if defined(__QNXNTO__) + // Console recorder shall be reused for slog backend. For slogger we also need to output ASCII like on the console. + EXPECT_TRUE(IsRecorderOfType(recorder)); +#else + EXPECT_TRUE(IsRecorderOfType(recorder)); +#endif //__QNXNTO__ +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/slog/BUILD b/mw/log/detail/slog/BUILD new file mode 100644 index 0000000..e83743a --- /dev/null +++ b/mw/log/detail/slog/BUILD @@ -0,0 +1,84 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//platform/aas/mw:common_features.bzl", "COMPILER_WARNING_FEATURES") +load("//platform/aas/tools/itf/bazel_gen:itf_gen.bzl", "py_unittest_qnx_test") + +cc_library( + name = "slog_backend", + srcs = [ + "slog_backend.cpp", + ], + hdrs = [ + "slog_backend.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + target_compatible_with = ["@platforms//os:qnx"], + visibility = ["//platform/aas/mw/log:__subpackages__"], + deps = [ + "//platform/aas/lib/os/qnx:slog2", + "//platform/aas/mw/log/detail:backend_interface", + "//platform/aas/mw/log/detail:circular_allocator", + "//platform/aas/mw/log/detail:initialization_reporter", + ], +) + +cc_library( + name = "slog", + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + target_compatible_with = ["@platforms//os:qnx"], + visibility = ["//platform/aas/mw/log:__subpackages__"], + deps = [":slog_backend"], +) + +py_unittest_qnx_test( + name = "unit_tests_qnx", + test_cases = [ + ":slog_backend_test", + ], + visibility = ["//platform/aas/mw/log/detail:__pkg__"], +) + +test_suite( + name = "unit_tests", + tests = [], + visibility = ["//visibility:public"], +) + +cc_test( + name = "slog_backend_test", + srcs = [ + "slog_backend_test.cpp", + ], + features = [ + "aborts_upon_exception", + ], + tags = [ + "manual", + "unit", + ], + deps = [ + "//platform/aas/mw/log/configuration", + "//third_party/googletest", + "//third_party/googletest:main", + "@amp//:amp_test_support", + ] + select({ + "@platforms//os:linux": [], + "@platforms//os:qnx": [ + ":slog", + "//platform/aas/lib/os/mocklib/qnx:slog2_mock", + ], + }), +) diff --git a/mw/log/detail/slog/slog_backend.cpp b/mw/log/detail/slog/slog_backend.cpp new file mode 100644 index 0000000..f21312e --- /dev/null +++ b/mw/log/detail/slog/slog_backend.cpp @@ -0,0 +1,212 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/slog/slog_backend.h" + +#include "platform/aas/mw/log/detail/error.h" +#include "platform/aas/mw/log/detail/initialization_reporter.h" + +#include +#include +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +namespace +{ +constexpr auto SLOG_BUFFER_DEFAULT = 0; + +constexpr auto SLOG_VERBOSITY_DEFAULT = SLOG2_DEBUG2; + + +std::size_t CheckTheMaxCapacity(const std::size_t capacity) noexcept +{ + const auto is_within_max_capacity = (capacity <= std::numeric_limits::max()); + if (is_within_max_capacity) + { + return capacity; + } + else + { + return static_cast(std::numeric_limits::max()); + } +} + +enum class SlogLogLevel : std::uint8_t +{ + kDebug2 = SLOG2_DEBUG2, + kDebug1 = SLOG2_DEBUG1, + kInfo = SLOG2_INFO, + kWarning = SLOG2_WARNING, + kError = SLOG2_ERROR, + kCritical = SLOG2_CRITICAL, + kInvalid = SLOG2_INVALID_VERBOSITY +}; + +constexpr SlogLogLevel ConvertMwLogLevelToSlogLevel(const LogLevel level) +{ + SlogLogLevel slog_log_level = SlogLogLevel::kInvalid; + switch (level) + { + case LogLevel::kVerbose: + slog_log_level = SlogLogLevel::kDebug2; + break; + case LogLevel::kDebug: + slog_log_level = SlogLogLevel::kDebug1; + break; + case LogLevel::kInfo: + slog_log_level = SlogLogLevel::kInfo; + break; + case LogLevel::kWarn: + slog_log_level = SlogLogLevel::kWarning; + break; + case LogLevel::kError: + slog_log_level = SlogLogLevel::kError; + break; + case LogLevel::kFatal: + slog_log_level = SlogLogLevel::kCritical; + break; + case LogLevel::kOff: + default: + slog_log_level = SlogLogLevel::kInvalid; + break; + } + return slog_log_level; +} + +constexpr SlogLogLevel ToSloggerLogLevel(const LogLevel log_level) noexcept +{ + if (log_level <= GetMaxLogLevelValue()) + { + return ConvertMwLogLevelToSlogLevel(log_level); + } + else + { + return SlogLogLevel::kInvalid; + } +} + +} // namespace + +SlogBackend::SlogBackend( + const std::size_t number_of_slots, + const LogRecord& initial_slot_value, + const amp::string_view app_id, + amp::pmr::unique_ptr slog2_instance /* = bmw::os::qnx::Slog2() */) noexcept + : Backend::Backend(), + app_id_{app_id.data(), app_id.size()}, + buffer_{CheckTheMaxCapacity(number_of_slots), initial_slot_value}, + slog_buffer_{}, + slog_buffer_config_{}, + slog2_instance_{std::move(slog2_instance)} +{ + Init(static_cast(SLOG_VERBOSITY_DEFAULT)); +} + +amp::optional SlogBackend::ReserveSlot() noexcept +{ + const auto& slot = buffer_.AcquireSlotToWrite(); + if (slot.has_value()) + { + if (slot.value() < std::numeric_limits::max()) + { + // CircularAllocator has capacity limited by CheckFoxMaxCapacity thus the cast is valid: + // We intentionally static cast to SlotIndex(uint8_t) to limit memory allocations + // to the required levels during startup, since there is no need to support slots greater + // than uint8 as per the current system needs. + // + return SlotHandle{static_cast(slot.value())}; + } + } + return {}; +} + +LogRecord& SlogBackend::GetLogRecord(const SlotHandle& slot) noexcept +{ + // static cast from std::uint8_t to std::size_t + return buffer_.GetUnderlyingBufferFor(static_cast(slot.GetSlotOfSelectedRecorder())); +} + +void SlogBackend::FlushSlot(const SlotHandle& slot) noexcept +{ + // static cast from std::uint8_t to std::size_t + auto& log_entry = + buffer_.GetUnderlyingBufferFor(static_cast(slot.GetSlotOfSelectedRecorder())).getLogEntry(); + + constexpr std::size_t max_id_length{4U}; + + // Cast appid length to int32 without overflow. + const std::int32_t app_id_length = static_cast(std::min(max_id_length, app_id_.size())); + + // Cast context length to int32 without overflow. + const std::int32_t ctx_id_length = + static_cast(std::min(max_id_length, log_entry.ctx_id.GetStringView().size())); + + // Cast payload size to int32_t without overflow. + const std::int32_t payload_length = static_cast( + std::min(static_cast(std::numeric_limits::max()), log_entry.payload.size())); + +// We need to support Linux variant for testing. +// +#if defined __QNX__ + const auto slog2 = log_entry.slog2_code; +// +#else + const auto slog2 = 0; +// +#endif + // Log message with appid and ctxid. + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) no available alternative for slog2f + amp::ignore = slog2_instance_->slog2f(slog_buffer_, + slog2, + static_cast(ToSloggerLogLevel(log_entry.log_level)), + "%.*s,%.*s: %.*s", + app_id_length, + app_id_.c_str(), + ctx_id_length, + log_entry.ctx_id.GetStringView().data(), + payload_length, + log_entry.payload.data()); + + buffer_.ReleaseSlot(static_cast(slot.GetSlotOfSelectedRecorder())); +} + +void SlogBackend::Init(const std::uint8_t verbosity) noexcept +{ + slog_buffer_config_.num_buffers = 1; + slog_buffer_config_.buffer_set_name = app_id_.c_str(); + slog_buffer_config_.verbosity_level = verbosity; + slog_buffer_config_.buffer_config[SLOG_BUFFER_DEFAULT].buffer_name = app_id_.c_str(); + slog_buffer_config_.buffer_config[SLOG_BUFFER_DEFAULT].num_pages = 16; // 16*4kB = 64kB + + const auto result = slog2_instance_->slog2_register(&slog_buffer_config_, &slog_buffer_, 0U); + if (result.has_value() == false) + { + const auto underlying_error = result.error().ToStringContainer(result.error()); + ReportInitializationError(Error::kSloggerError, underlying_error.data()); + } +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/slog/slog_backend.h b/mw/log/detail/slog/slog_backend.h new file mode 100644 index 0000000..551146e --- /dev/null +++ b/mw/log/detail/slog/slog_backend.h @@ -0,0 +1,61 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_SLOG_SLOG_BACKEND_H +#define PLATFORM_AAS_MW_LOG_DETAIL_SLOG_SLOG_BACKEND_H + +#include "platform/aas/lib/os/qnx/slog2_impl.h" +#include "platform/aas/mw/log/detail/backend.h" +#include "platform/aas/mw/log/detail/circular_allocator.h" + +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +class SlogBackend final : public Backend +{ + public: + explicit SlogBackend(const std::size_t number_of_slots, + const LogRecord& initial_slot_value, + const amp::string_view app_id, + amp::pmr::unique_ptr slog2_instance) noexcept; + + amp::optional ReserveSlot() noexcept override; + void FlushSlot(const SlotHandle& slot) noexcept override; + LogRecord& GetLogRecord(const SlotHandle& slot) noexcept override; + + private: + void Init(const std::uint8_t verbosity) noexcept; + + std::string app_id_; + CircularAllocator buffer_; + slog2_buffer_t slog_buffer_; + slog2_buffer_set_config_t slog_buffer_config_; + amp::pmr::unique_ptr slog2_instance_; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_SLOG_SLOG_BACKEND_H diff --git a/mw/log/detail/slog/slog_backend_test.cpp b/mw/log/detail/slog/slog_backend_test.cpp new file mode 100644 index 0000000..404028e --- /dev/null +++ b/mw/log/detail/slog/slog_backend_test.cpp @@ -0,0 +1,332 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "gtest/gtest.h" + +#include "platform/aas/lib/os/mocklib/qnx/mock_slog2.h" +#include "platform/aas/mw/log/configuration/configuration.h" +#include "platform/aas/mw/log/detail/slog/slog_backend.h" + +#include "amp_assert_support.hpp" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +using ::testing::_; +using ::testing::Return; +using ::testing::StrEq; + +const std::string kDefaultApp{"a1"}; +const std::string kDefaultContext{"c1"}; +const std::string kDefaultMessage{"default message"}; +const std::uint16_t kDefaultCode{0}; + +struct SlogBackendFixture : ::testing::Test +{ + void SetUp() override + { + slog2_mock_ = amp::pmr::make_unique(amp::pmr::get_default_resource()); + slog2_mock_raw_ptr_ = slog2_mock_.get(); + } + + protected: + void SimulateLogging(LogLevel log_level, + const std::string& app_id = kDefaultApp, + const std::string& ctx_id = kDefaultContext, + const std::string& message = kDefaultMessage, + const std::uint16_t& code = kDefaultCode) + { + SlogBackend backend(config_.GetNumberOfSlots(), log_record_, app_id, std::move(slog2_mock_)); + + auto slot = backend.ReserveSlot(); + EXPECT_TRUE(slot.has_value()); + + auto& payload = backend.GetLogRecord(slot.value()); + auto& log_entry = payload.getLogEntry(); + log_entry.ctx_id = LoggingIdentifier(amp::string_view(ctx_id)); + log_entry.log_level = log_level; + log_entry.payload = ByteVector(message.begin(), message.end()); + log_entry.slog2_code = code; + + backend.FlushSlot(slot.value()); + } + + LogRecord log_record_{}; + Configuration config_{}; + amp::pmr::unique_ptr slog2_mock_{}; + bmw::os::qnx::MockSlog2* slog2_mock_raw_ptr_; +}; + +TEST_F(SlogBackendFixture, SlogRegister) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("Description", "Verifies normal slog registering."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_CALL(*slog2_mock_raw_ptr_, slog2_register(_, _, _)); + + SlogBackend unit(config_.GetNumberOfSlots(), log_record_, config_.GetAppId(), std::move(slog2_mock_)); +} + +TEST_F(SlogBackendFixture, SlogRegisterWithCapacityBiggerThanTheMaximum) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("Description", "Verifies slog registering with slots' capacity bigger than the maximum."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + auto capacity = std::numeric_limits::max() + 1; + config_.SetNumberOfSlots(capacity); + EXPECT_CALL(*slog2_mock_raw_ptr_, slog2_register(_, _, _)); + + SlogBackend unit(config_.GetNumberOfSlots(), log_record_, config_.GetAppId(), std::move(slog2_mock_)); +} + +TEST_F(SlogBackendFixture, SlogRegisterShouldHandleError) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("Description", "Verifies slog registering in case of returning an error."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_CALL(*slog2_mock_raw_ptr_, slog2_register(_, _, _)) + .Times(1) + .WillOnce(Return(amp::make_unexpected(bmw::os::Error::createFromErrno()))); + + SlogBackend unit(config_.GetNumberOfSlots(), log_record_, config_.GetAppId(), std::move(slog2_mock_)); +} + +TEST_F(SlogBackendFixture, ReserveSlotShouldAcquireSlot) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("Description", "Verifies the ability of reserving slot."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_CALL(*slog2_mock_raw_ptr_, slog2_register(_, _, _)); + + SlogBackend unit(config_.GetNumberOfSlots(), log_record_, config_.GetAppId(), std::move(slog2_mock_)); + + auto slot = unit.ReserveSlot(); + EXPECT_TRUE(slot.has_value()); +} + +TEST_F(SlogBackendFixture, LevelOffLog) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("Description", "Verbosity shall be invaild in case of disabling the logging."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_CALL(*slog2_mock_raw_ptr_, slog2_register(_, _, _)).Times(1).WillOnce(Return(0)); + EXPECT_CALL(*slog2_mock_raw_ptr_, MockedSlog2f(_, _, SLOG2_INVALID_VERBOSITY, _)).Times(1); + + SimulateLogging(LogLevel::kOff); +} + +TEST_F(SlogBackendFixture, FatalLog) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("Description", "Verifies the ability of logging fatal message."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_CALL(*slog2_mock_raw_ptr_, slog2_register(_, _, _)).Times(1).WillOnce(Return(0)); + EXPECT_CALL(*slog2_mock_raw_ptr_, MockedSlog2f(_, _, SLOG2_CRITICAL, _)).Times(1); + + SimulateLogging(LogLevel::kFatal); +} + +TEST_F(SlogBackendFixture, ErrorLog) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("Description", "Verifies the ability of logging error message."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_CALL(*slog2_mock_raw_ptr_, slog2_register(_, _, _)).Times(1).WillOnce(Return(0)); + EXPECT_CALL(*slog2_mock_raw_ptr_, MockedSlog2f(_, _, SLOG2_ERROR, _)).Times(1); + + SimulateLogging(LogLevel::kError); +} + +TEST_F(SlogBackendFixture, WarningLog) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("Description", "Verifies the ability of logging waring message."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_CALL(*slog2_mock_raw_ptr_, slog2_register(_, _, _)).Times(1).WillOnce(Return(0)); + EXPECT_CALL(*slog2_mock_raw_ptr_, MockedSlog2f(_, _, SLOG2_WARNING, _)).Times(1); + + SimulateLogging(LogLevel::kWarn); +} + +TEST_F(SlogBackendFixture, InfoLog) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("Description", "Verifies the ability of logging info message."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_CALL(*slog2_mock_raw_ptr_, slog2_register(_, _, _)).Times(1).WillOnce(Return(0)); + EXPECT_CALL(*slog2_mock_raw_ptr_, MockedSlog2f(_, _, SLOG2_INFO, _)).Times(1); + + SimulateLogging(LogLevel::kInfo); +} + +TEST_F(SlogBackendFixture, DebugLog) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("Description", "Verifies the ability of logging debug message."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_CALL(*slog2_mock_raw_ptr_, slog2_register(_, _, _)).Times(1).WillOnce(Return(0)); + EXPECT_CALL(*slog2_mock_raw_ptr_, MockedSlog2f(_, _, SLOG2_DEBUG1, _)).Times(1); + + SimulateLogging(LogLevel::kDebug); +} + +TEST_F(SlogBackendFixture, VerboseLog) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("Description", "Verifies the ability of logging verbose message."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_CALL(*slog2_mock_raw_ptr_, slog2_register(_, _, _)).Times(1).WillOnce(Return(0)); + EXPECT_CALL(*slog2_mock_raw_ptr_, MockedSlog2f(_, _, SLOG2_DEBUG2, _)).Times(1); + + SimulateLogging(LogLevel::kVerbose); +} + +TEST_F(SlogBackendFixture, DisableTheLog) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("Description", "Verifies the ability of disabling the logging."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_CALL(*slog2_mock_raw_ptr_, slog2_register(_, _, _)).Times(1).WillOnce(Return(0)); + + SimulateLogging(LogLevel::kOff); +} + +TEST_F(SlogBackendFixture, MessageShouldContainAppCtxPayload) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("Description", "Verifies lgo message with application and context payload."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_CALL(*slog2_mock_raw_ptr_, slog2_register(_, _, _)).Times(1).WillOnce(Return(0)); + EXPECT_CALL(*slog2_mock_raw_ptr_, MockedSlog2f(_, _, SLOG2_DEBUG2, StrEq("MyAp,MyCt: Hello World"))).Times(1); + + SimulateLogging(LogLevel::kVerbose, "MyAp", "MyCt", "Hello World"); +} + +TEST_F(SlogBackendFixture, BackendShouldHandleEmptyPayload) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("Description", "Verifies the ability of hte backend of handling empty payload."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_CALL(*slog2_mock_raw_ptr_, slog2_register(_, _, _)).Times(1).WillOnce(Return(0)); + EXPECT_CALL(*slog2_mock_raw_ptr_, MockedSlog2f(_, _, SLOG2_DEBUG2, StrEq(",: "))).Times(1); + + SimulateLogging(LogLevel::kVerbose, "", "", ""); +} + +TEST_F(SlogBackendFixture, LongIdentifiersShouldBeCropped) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("Description", + "Verifies that the application or context IDs should be cropped if it exceeds 4 characters length."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_CALL(*slog2_mock_raw_ptr_, slog2_register(_, _, _)).Times(1).WillOnce(Return(0)); + EXPECT_CALL(*slog2_mock_raw_ptr_, MockedSlog2f(_, _, SLOG2_DEBUG2, StrEq("1234,4567: "))).Times(1); + + SimulateLogging(LogLevel::kVerbose, "12345", "45678", ""); +} + +TEST_F(SlogBackendFixture, Slog2CodeShouldBeForwarded) +{ + RecordProperty("Description", "Verify if slog2 code is forwarded to slog2."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_CALL(*slog2_mock_raw_ptr_, slog2_register(_, _, _)).Times(1).WillOnce(Return(0)); + EXPECT_CALL(*slog2_mock_raw_ptr_, MockedSlog2f(_, 100, SLOG2_DEBUG2, StrEq("MyAp,MyCt: Slog message"))).Times(1); + + SimulateLogging(LogLevel::kVerbose, "MyAp", "MyCt", "Slog message", 100); +} + +TEST_F(SlogBackendFixture, NoSlotAvailableShouldReturnEmptyHandle) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("Description", "Verifies returning empty handler in case of no available slots."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + SlogBackend backend(config_.GetNumberOfSlots(), log_record_, config_.GetAppId(), std::move(slog2_mock_)); + + for (std::size_t i = 0; i < config_.GetNumberOfSlots(); ++i) + { + EXPECT_TRUE(backend.ReserveSlot().has_value()); + } + + EXPECT_FALSE(backend.ReserveSlot().has_value()); +} + +TEST_F(SlogBackendFixture, TooMuchSlotsRequestedShallBeTruncated) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("Description", "Verifies requesting too much slots shall be truncated."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + const auto kMaxSlotCount = std::numeric_limits::max(); + const std::size_t kSlotNumberOverflow = static_cast(kMaxSlotCount) + 2UL; + + SlogBackend backend(kSlotNumberOverflow, log_record_, config_.GetAppId(), std::move(slog2_mock_)); + + for (std::size_t i = 0; i < kMaxSlotCount; ++i) + { + EXPECT_TRUE(backend.ReserveSlot().has_value()); + } + + EXPECT_FALSE(backend.ReserveSlot().has_value()); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/statistics_reporter.cpp b/mw/log/detail/statistics_reporter.cpp new file mode 100644 index 0000000..291c868 --- /dev/null +++ b/mw/log/detail/statistics_reporter.cpp @@ -0,0 +1,131 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#include "platform/aas/mw/log/detail/statistics_reporter.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +namespace +{ + +bool IsReportOverdue(const std::chrono::steady_clock::time_point& now, + const std::int64_t& last_report_time_nsec, + const std::chrono::seconds& report_interval) noexcept +{ + if (std::chrono::duration_cast( + now - std::chrono::steady_clock::time_point{std::chrono::nanoseconds{last_report_time_nsec}}) >= + report_interval) + { + return true; + } + return false; +} + +void ReportStatisticsViaRecorder(Recorder& recorder, + const std::size_t& no_slot_available_counter, + const std::size_t& message_too_long_counter, + const std::size_t& number_of_slots, + const std::size_t& slot_size_bytes) noexcept +{ + LogLevel log_level = LogLevel::kInfo; + if ((no_slot_available_counter > 0) || (message_too_long_counter > 0)) + { + log_level = LogLevel::kWarn; + } + + const auto slot = recorder.StartRecord("STAT", log_level); + + if (slot.has_value() == false) + { + return; + } + recorder.Log(slot.value(), amp::string_view{"mw::log statistics: number_of_slots="}); + recorder.Log(slot.value(), number_of_slots); + recorder.Log(slot.value(), amp::string_view{", slot_size_bytes="}); + recorder.Log(slot.value(), slot_size_bytes); + recorder.Log(slot.value(), amp::string_view{", no_slot_available_counter="}); + recorder.Log(slot.value(), no_slot_available_counter); + recorder.Log(slot.value(), amp::string_view{", message_too_long_counter="}); + recorder.Log(slot.value(), message_too_long_counter); + recorder.StopRecord(slot.value()); +} + +} // namespace + + +StatisticsReporter::StatisticsReporter(Recorder& recorder, + const std::chrono::seconds report_interval, + const std::size_t number_of_slots, + const std::size_t slot_size_bytes) noexcept + : IStatisticsReporter{}, + recorder_{recorder}, + report_interval_{report_interval}, + number_of_slots_{number_of_slots}, + slot_size_bytes_{slot_size_bytes}, + no_slot_available_counter_{}, + message_too_long_counter_{}, + last_report_time_point_nanoseconds_{}, + currently_reporting_{} +{ +} + + +void StatisticsReporter::IncrementNoSlotAvailable() noexcept +{ + no_slot_available_counter_++; +} + +void StatisticsReporter::IncrementMessageTooLong() noexcept +{ + message_too_long_counter_++; +} + +void StatisticsReporter::Update(const std::chrono::steady_clock::time_point& now) noexcept +{ + if (IsReportOverdue(now, last_report_time_point_nanoseconds_.load(), report_interval_) == false) + { + return; + } + + // Try to acquire the reporting state + bool current_reporting_expected_false = false; + if (currently_reporting_.compare_exchange_weak(current_reporting_expected_false, true) == false) + { + // Give up because another thread is already reporting. + return; + } + + ReportStatisticsViaRecorder(recorder_, + no_slot_available_counter_.load(), + message_too_long_counter_.load(), + number_of_slots_, + slot_size_bytes_); + last_report_time_point_nanoseconds_ = std::chrono::nanoseconds{now.time_since_epoch()}.count(); + + // Release reporting state. + currently_reporting_ = false; +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/statistics_reporter.h b/mw/log/detail/statistics_reporter.h new file mode 100644 index 0000000..dab15ad --- /dev/null +++ b/mw/log/detail/statistics_reporter.h @@ -0,0 +1,63 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_DATA_ROUTER_STATISTICS_REPORTER_H +#define PLATFORM_AAS_MW_LOG_DETAIL_DATA_ROUTER_STATISTICS_REPORTER_H + +#include "platform/aas/mw/log/recorder.h" + +#include "platform/aas/mw/log/detail/istatistics_reporter.h" + +#include + +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +class StatisticsReporter final : public IStatisticsReporter +{ + public: + explicit StatisticsReporter(Recorder&, + const std::chrono::seconds report_interval, + const std::size_t number_of_slots, + const std::size_t slot_size_bytes) noexcept; + void IncrementNoSlotAvailable() noexcept override; + void IncrementMessageTooLong() noexcept override; + void Update(const std::chrono::steady_clock::time_point& now) noexcept override; + + private: + Recorder& recorder_; + std::chrono::seconds report_interval_; + std::size_t number_of_slots_; + std::size_t slot_size_bytes_; + std::atomic no_slot_available_counter_; + std::atomic message_too_long_counter_; + std::atomic last_report_time_point_nanoseconds_; + std::atomic_bool currently_reporting_; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_DATA_ROUTER_STATISTICS_REPORTER_H diff --git a/mw/log/detail/statistics_reporter_test.cpp b/mw/log/detail/statistics_reporter_test.cpp new file mode 100644 index 0000000..3bf9149 --- /dev/null +++ b/mw/log/detail/statistics_reporter_test.cpp @@ -0,0 +1,151 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/statistics_reporter.h" + +#include "platform/aas/mw/log/recorder_mock.h" + +#include "gtest/gtest.h" + +using testing::_; +using testing::InSequence; +using testing::Return; + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +const SlotHandle kSlot{}; +const std::chrono::seconds kReportInterval{1}; +const std::size_t kNumberOfSlots{7}; +const std::size_t kSlotSizeBytes{1024}; +const std::chrono::steady_clock::time_point kInitialTime{}; +const std::chrono::steady_clock::time_point kOverdueTime = kInitialTime + kReportInterval; +const LogLevel kZeroErrorsLogLevel{LogLevel::kInfo}; +const LogLevel kNonZeroErrorsLogLevel{LogLevel::kWarn}; + +struct StatisticsReporterFixture : public ::testing::Test +{ + void ExpectNoReport() { EXPECT_CALL(recorder_mock_, StartRecord(_, _)).Times(0); } + + void ExpectReport(const std::size_t expected_no_slot_available_counter, + const std::size_t expected_message_too_long_counter, + const LogLevel expected_log_level) + { + InSequence in_sequence{}; + EXPECT_CALL(recorder_mock_, StartRecord(_, expected_log_level)).Times(1).WillOnce(Return(kSlot)); + EXPECT_CALL(recorder_mock_, LogUint64(kSlot, kNumberOfSlots)); + EXPECT_CALL(recorder_mock_, LogUint64(kSlot, kSlotSizeBytes)); + EXPECT_CALL(recorder_mock_, LogUint64(kSlot, expected_no_slot_available_counter)); + EXPECT_CALL(recorder_mock_, LogUint64(kSlot, expected_message_too_long_counter)); + EXPECT_CALL(recorder_mock_, StopRecord(kSlot)).Times(1); + } + + RecorderMock recorder_mock_{}; + StatisticsReporter unit_{recorder_mock_, kReportInterval, kNumberOfSlots, kSlotSizeBytes}; +}; + +TEST_F(StatisticsReporterFixture, UpdateShallReportIfOverDue) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Update function will report statistics if report interval elapsed."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + const std::size_t expected_no_slot_available_counter = 0; + const std::size_t expected_message_too_long_counter = 0; + ExpectReport(expected_no_slot_available_counter, expected_message_too_long_counter, kZeroErrorsLogLevel); + unit_.Update(kOverdueTime); +} + +TEST_F(StatisticsReporterFixture, UpdateShallReportWarningIfOverDueAndErrors) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Update function will report warning in case of overdue error."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + const std::size_t expected_no_slot_available_counter = 2; + const std::size_t expected_message_too_long_counter = 1; + ExpectReport(expected_no_slot_available_counter, expected_message_too_long_counter, kNonZeroErrorsLogLevel); + + unit_.IncrementNoSlotAvailable(); + unit_.IncrementNoSlotAvailable(); + unit_.IncrementMessageTooLong(); + + unit_.Update(kOverdueTime); +} + +TEST_F(StatisticsReporterFixture, UpdateShallReportWarningMessageIsTooooLong) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Update function will report warning in case of error."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_CALL(recorder_mock_, StopRecord(kSlot)).Times(0); + + unit_.IncrementMessageTooLong(); + unit_.Update(kOverdueTime); +} + +TEST_F(StatisticsReporterFixture, UpdateShallGiveUpIfNotYetTimeToReport) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "Update function will not report if the time has not passed yet. In that case no output is expected"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + InSequence in_sequence{}; + EXPECT_CALL(recorder_mock_, StartRecord(_, _)).Times(0); + + unit_.Update(kInitialTime); +} + +TEST_F(StatisticsReporterFixture, UpdateShallGiveUpIfAlreadyReporting) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Update function will not report if it is already reporting."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + InSequence in_sequence{}; + EXPECT_CALL(recorder_mock_, StartRecord(_, _)).Times(1).WillOnce([this](auto, auto) { + EXPECT_CALL(recorder_mock_, StartRecord(_, _)).Times(0); + unit_.Update(kOverdueTime); + return kSlot; + }); + + unit_.Update(kOverdueTime); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/test/data/App1.secpol b/mw/log/detail/test/data/App1.secpol new file mode 100644 index 0000000..ea625c6 --- /dev/null +++ b/mw/log/detail/test/data/App1.secpol @@ -0,0 +1,14 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + + +type App1_t; +allow App1_t self:ability {}; diff --git a/mw/log/detail/thread_local_guard.cpp b/mw/log/detail/thread_local_guard.cpp new file mode 100644 index 0000000..59edbb2 --- /dev/null +++ b/mw/log/detail/thread_local_guard.cpp @@ -0,0 +1,59 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/thread_local_guard.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +namespace +{ + +bool& GetThreadLocalFlagFile() noexcept +{ + thread_local bool is_in_logging_stack{false}; + + // Returning address of thread_local variable is ok here because it can + // only be accessed from the same thread + // + return is_in_logging_stack; +} + +} // namespace + +ThreadLocalGuard::ThreadLocalGuard() +{ + GetThreadLocalFlagFile() = true; +} + +ThreadLocalGuard::~ThreadLocalGuard() +{ + GetThreadLocalFlagFile() = false; +} + +bool ThreadLocalGuard::IsWithingLogging() noexcept +{ + return GetThreadLocalFlagFile(); +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/thread_local_guard.h b/mw/log/detail/thread_local_guard.h new file mode 100644 index 0000000..7c99202 --- /dev/null +++ b/mw/log/detail/thread_local_guard.h @@ -0,0 +1,58 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_THREAD_LOCAL_GUARD_H +#define PLATFORM_AAS_MW_LOG_DETAIL_THREAD_LOCAL_GUARD_H + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +/// \brief RAII Pattern to manipulate thread-local variable, to indicate if we are within logging stack or not +/// +/// \details Whenever we call a complex recorder, it could happen that this complex recorder uses common libraries that +/// on the other side again use logging. This would lead to a recursive call stack - ending in a crash of an +/// application. The idea is the following, that we figure if a logging command has been called within a complex +/// recorder. We can do this by relying on the stack - meaning, we have to-do this per thread, since each thread has a +/// custom stack. For that, we utilize a thread-local variable. For each call to a recorder, we mark this as true, once +/// the call left, we mark it as false. This way we are also able to support recursive calls to logging within the user +/// space. Whenever we then are logging in the logging stack, we need to fallback to a simpler recorder (e.g. console) +/// which does not rely on any common libraries (like e.g. lib/memory/shared). +class ThreadLocalGuard +{ + public: + explicit ThreadLocalGuard(); + + ~ThreadLocalGuard(); + + static bool IsWithingLogging() noexcept; + + ThreadLocalGuard(ThreadLocalGuard&) = delete; + ThreadLocalGuard(ThreadLocalGuard&&) = delete; + + ThreadLocalGuard& operator=(ThreadLocalGuard&) = delete; + ThreadLocalGuard& operator=(ThreadLocalGuard&&) = delete; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_THREAD_LOCAL_GUARD_H diff --git a/mw/log/detail/thread_local_guard_test.cpp b/mw/log/detail/thread_local_guard_test.cpp new file mode 100644 index 0000000..c1d012d --- /dev/null +++ b/mw/log/detail/thread_local_guard_test.cpp @@ -0,0 +1,90 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/thread_local_guard.h" + +#include "platform/aas/lib/concurrency/notification.h" + +#include "gtest/gtest.h" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +TEST(ThreadLocalGuardTest, ByDefaultNotWithinLoggingStack) +{ + // Given that nothing has happened so far + // When querying if we are in the logging stack + // Then we should be not. + EXPECT_FALSE(ThreadLocalGuard::IsWithingLogging()); +} + +TEST(ThreadLocalGuardTest, OnConstructionInLoggingStack) +{ + // Given the ThreadLocalGuard has been initialized + ThreadLocalGuard unit{}; + + // When querying if we are in the logging stack + // Then we should be. + EXPECT_TRUE(ThreadLocalGuard::IsWithingLogging()); +} + +TEST(ThreadLocalGuardTest, OnDenstructionNotInLoggingStack) +{ + // Given the ThreadLocalGuard has been constructed and destructed + { + ThreadLocalGuard unit{}; + } + + // When querying if we are in the logging stack + // Then we should be not. + EXPECT_FALSE(ThreadLocalGuard::IsWithingLogging()); +} + +TEST(ThreadLocalGuardTest, DifferentThreadDifferentResult) +{ + // Given in one thread the ThreadLocalGuard has been initialized + ThreadLocalGuard unit{}; + + concurrency::Notification notification{}; + std::thread other_thread{[¬ification]() { + // When checking in another thread + // Then this is not affected + EXPECT_FALSE(ThreadLocalGuard::IsWithingLogging()); + notification.notify(); + }}; + + notification.waitWithAbort({}); + + // When checking in the same thread + // Then this is affected + EXPECT_TRUE(ThreadLocalGuard::IsWithingLogging()); + + other_thread.join(); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/verbose_payload.cpp b/mw/log/detail/verbose_payload.cpp new file mode 100644 index 0000000..40a1794 --- /dev/null +++ b/mw/log/detail/verbose_payload.cpp @@ -0,0 +1,194 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/verbose_payload.h" + +#include "amp_assert.hpp" +#include "amp_utility.hpp" + +#include +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +namespace +{ +class ReservedData +{ + public: + ReservedData(ByteVector& buffer, const std::size_t reserve_size) noexcept + : buffer_(buffer), old_size_(buffer.size()), total_used_{0}, reserved_{0} + { + const std::size_t max_possible_length_to_copy = buffer_.capacity() - old_size_; + + if ((reserve_size == 0) || (reserve_size > max_possible_length_to_copy)) + { + reserved_ = max_possible_length_to_copy; + } + else + { + reserved_ = reserve_size; + } + // casting constant value to more capable type: + constexpr auto max_signed_type_size = static_cast(std::numeric_limits::max()); + // limit value to maximum positive value of signed integer because data types with both signed and unsigned + // size types are mixed + reserved_ = std::min(max_signed_type_size, reserved_); + buffer_.resize(old_size_ + reserved_); + } + ReservedData(ReservedData&&) noexcept = delete; + ReservedData(const ReservedData&) noexcept = delete; + ReservedData& operator=(ReservedData&&) noexcept = delete; + ReservedData& operator=(const ReservedData&) noexcept = delete; + + ~ReservedData() + { + buffer_.resize(old_size_ + total_used_); // resize to fit only used data + } + + void IncreaseUsed(std::size_t size) noexcept + { + const auto space_left = buffer_.capacity() - total_used_ - old_size_; + const auto use_size = std::min(size, space_left); + total_used_ += use_size; + } + + amp::span GetData() const noexcept + { + const auto buffer_index = old_size_ + total_used_; + if (buffer_index < buffer_.size()) + { + // reserved_ is limited at construction time and total_used_ is derived from it and shall be always + // smaller: + return {&buffer_[buffer_index], + static_cast(reserved_) - static_cast(total_used_)}; + } + else + { + return {nullptr, 0UL}; + } + } + + private: + ByteVector& buffer_; + const std::size_t old_size_; + std::size_t total_used_; + std::size_t reserved_; +}; +} // anonymous namespace + +VerbosePayload::VerbosePayload(const std::size_t max_size, ByteVector& buffer) noexcept : buffer_{buffer} +{ + buffer_.get().reserve(max_size); +} + +void VerbosePayload::Put(const Byte* const data, const std::size_t length) noexcept +{ + if (length == 0) + { + return; + } + + // data == nullptr is only problematic if length != 0 + const auto isPointerValid = data != nullptr; + + + if (isPointerValid == false) + { + return; + } + + + + amp::ignore = this->Put( + + [data](amp::span dst_data) { + + if (dst_data.size() > 0) + { + const amp::span data_span{data, dst_data.size()}; + amp::ignore = std::copy(data_span.begin(), data_span.end(), dst_data.begin()); + } + return dst_data.size(); + }, + length); +} + + +/* False positive:Function passes non-const reference to member variable to non-const object. */ +std::size_t VerbosePayload::Put(const ReserveCallback callback, const std::size_t reserve_size) noexcept + +{ + ReservedData reserved_data(buffer_.get(), reserve_size); + const auto data = reserved_data.GetData(); + + // Execute user provided callback which should fill 'data' buffer and returned used space + const auto new_written_data_size = callback(data); + reserved_data.IncreaseUsed(new_written_data_size); + return new_written_data_size; +} + +amp::span VerbosePayload::GetSpan() const noexcept +{ + // Checking for sign overflow. Limit data to maximum possible span size: + using size_type = amp::span::size_type; + constexpr auto max_size = std::numeric_limits::max(); + + // Suppress false positive issue: + + const auto span_size = static_cast(std::min(static_cast(max_size), buffer_.get().size())); + + + + // reinterpret_cast due to handling raw data: + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) justification provided + // reinterpret_cast due to handling raw data + // + return {reinterpret_cast(buffer_.get().data()), span_size}; + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) justification provided + +} + +void VerbosePayload::Reset() const noexcept +{ + buffer_.get().resize(0); +} + +bool VerbosePayload::WillOverflow(const std::size_t length) const noexcept +{ + return length > RemainingCapacity(); +} + +std::size_t VerbosePayload::RemainingCapacity() const noexcept +{ + return buffer_.get().capacity() - buffer_.get().size(); +} + +void VerbosePayload::SetBuffer(ByteVector& buffer) noexcept +{ + buffer_ = buffer; +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/verbose_payload.h b/mw/log/detail/verbose_payload.h new file mode 100644 index 0000000..cb0abf8 --- /dev/null +++ b/mw/log/detail/verbose_payload.h @@ -0,0 +1,103 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_VERBOSE_PAYLOAD_H +#define PLATFORM_AAS_MW_LOG_DETAIL_VERBOSE_PAYLOAD_H + +#include +#include + +#include +#include +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + + +using Byte = char; +using ByteVector = std::vector; + + +using ReserveCallback = amp::callback)>; + +/// \brief Abstracts the usage of our underlying buffer for memory safety reasons. +/// +/// \details We want to be sure that our buffer only allocates memory on construction. +/// By providing our wrapper around it we can ensure this. Another nice point is that we can encapsulate memory +/// handling. Meaning that an overflow of our buffer can be mitigated at one central place. +class VerbosePayload final +{ + public: + /// \brief Constructs payload, by resizing buffer to max_size + /// + /// \param max_size The maximum size of memory that will be allocated + /// \param buffer The buffer that shall be manipulated + explicit VerbosePayload(const std::size_t max_size, ByteVector& buffer) noexcept; + + /// \brief Stores data in the underlying buffer, while taking care of memory safety + /// + /// \param data The pointer to data that shall be stored in the buffer + /// \param length The length of data that shall be stored in the buffer + /// + /// \details If the buffer is full, then this function will not continue to fill it. Meaning, any data will be cut + /// of at the maximum edge of the buffer. Otherwise the data will be appended. + void Put(const Byte* const data, const std::size_t length) noexcept; + + /// \brief Stores data in the underlying buffer, while taking care of some memory safety + /// + /// \param callback function with signature std::size(*f)(amp::callback) used to fill provided buffer + /// \param reserve_size reserve size for incoming data. When size zero is specified maximum allowed space is + /// reserved. This is default behaviour + /// + /// \details User code must be careful not to exceed provided buffer capacity. + /// Before return buffer is resized to fit the actual data use + /// \return Number of bytes written + std::size_t Put(const ReserveCallback callback, const std::size_t reserve_size = 0) noexcept; + + amp::span GetSpan() const noexcept; + + /// \brief Clear buffer for next cycle operation + /// + void Reset() const noexcept; + + /// \brief Check if provided length fits into payload + /// + /// \param length The length which shall be checked + /// \return true if length fits not in payload, false otherwise + bool WillOverflow(const std::size_t length) const noexcept; + + /// \brief Return the number of remaining bytes that shall fit in the buffer. + std::size_t RemainingCapacity() const noexcept; + + void SetBuffer(ByteVector&) noexcept; + + private: + /// Use reference_wrapper to enable buffer rebinding in SetBuffer(). + std::reference_wrapper buffer_; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_DETAIL_VERBOSE_PAYLOAD_H diff --git a/mw/log/detail/verbose_payload_test.cpp b/mw/log/detail/verbose_payload_test.cpp new file mode 100644 index 0000000..1eeecef --- /dev/null +++ b/mw/log/detail/verbose_payload_test.cpp @@ -0,0 +1,232 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/verbose_payload.h" + +#include "gtest/gtest.h" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +class VerbosePayloadFixture : public ::testing::Test +{ + public: + VerbosePayload constructUnitWithSize(const std::size_t size) { return VerbosePayload{size, buffer_}; } + + ByteVector buffer_{}; +}; + +using VerbosePayloadFixtureDeathTest = VerbosePayloadFixture; + +TEST_F(VerbosePayloadFixture, SinglePutStoresMemoryCorrect) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Put function stores data in correct order in the payload (buffer) if called once."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given an empty verbose payload with enough space + VerbosePayload unit = constructUnitWithSize(10); + + // When adding some data into that buffer + constexpr auto some_text = "Hello"; + unit.Put(some_text, 6); + + // Then the data is stored in the right order + ASSERT_EQ(buffer_.at(0), 'H'); + ASSERT_EQ(buffer_.at(1), 'e'); + ASSERT_EQ(buffer_.at(2), 'l'); + ASSERT_EQ(buffer_.at(3), 'l'); + ASSERT_EQ(buffer_.at(4), 'o'); + ASSERT_EQ(buffer_.at(5), '\0'); +} + +TEST_F(VerbosePayloadFixture, MultiplePutStoresMemoryCorrectly) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Put function stores data in correct order in the payload (buffer) if called multiple times."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given an already filled buffer with enough space + VerbosePayload unit = constructUnitWithSize(20); + unit.Put("Hello", 6); + + // When adding some more data into that buffer + constexpr auto some_text = "Next"; + unit.Put(some_text, 5); + + // Then the previous data is not corrupted and the new one is appended + // Please be advised that on purpose we want raw memory handling, meaning + // the duplicated null-termination is correct. + ASSERT_EQ(buffer_.at(5), '\0'); + ASSERT_EQ(buffer_.at(6), 'N'); + ASSERT_EQ(buffer_.at(7), 'e'); + ASSERT_EQ(buffer_.at(8), 'x'); + ASSERT_EQ(buffer_.at(9), 't'); + ASSERT_EQ(buffer_.at(10), '\0'); +} + +TEST_F(VerbosePayloadFixture, PutZeroSize) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Put function handles zero size data requests gracefully"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given an already filled buffer with enough space + VerbosePayload unit = constructUnitWithSize(20); + unit.Put("Hello", 0); + + ASSERT_EQ(buffer_.size(), 0); +} + +TEST_F(VerbosePayloadFixture, PutStopsAtMaximumSize) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Put function continues to fill data to maximum size and any additional data will not be filled."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given an empty verbose payload with to few space + VerbosePayload unit = constructUnitWithSize(3); + + // When adding some data into that buffer + constexpr auto some_text = "Hello"; + unit.Put(some_text, 6); + + // Then the data is stored in the right order, with maximum size of the buffer + ASSERT_EQ(buffer_.at(0), 'H'); + ASSERT_EQ(buffer_.at(1), 'e'); + ASSERT_EQ(buffer_.at(2), 'l'); + ASSERT_EQ(buffer_.size(), 3); +} + +TEST_F(VerbosePayloadFixtureDeathTest, AssertForInvalidPointerKicksIn) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Put function will exit with failure in case of invalid data pointer."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given an empty buffer + VerbosePayload unit = constructUnitWithSize(0); + + // When trying to write data from a nullptr + // Then an assertion protects from wrong behavior + ASSERT_NO_FATAL_FAILURE(unit.Put(nullptr, 5)); +} + +TEST_F(VerbosePayloadFixture, EmptyBufferHasNoWrongBehavior) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Put function will not fill any data in case of empty buffer and will return successfully."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given an empty verbose payload with no space + VerbosePayload unit = constructUnitWithSize(0); + + // When adding some data into that buffer + constexpr auto some_text = "Hello"; + unit.Put(some_text, 6); + + // Then no data is stored + ASSERT_EQ(buffer_.size(), 0); +} + +TEST_F(VerbosePayloadFixture, SizeFitsInPayload) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies size fits in payload."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given an empty verbose payload with enough space + VerbosePayload unit = constructUnitWithSize(5); + + // When checking if a fitting capacity is requested + // Then WillOverflow returns false + ASSERT_FALSE(unit.WillOverflow(5)); +} + +TEST_F(VerbosePayloadFixture, SizeFitsNotInPayload) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies size does not fit in payload."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given an empty verbose payload with enough space + VerbosePayload unit = constructUnitWithSize(5); + + // When checking if a fitting capacity is requested + // Then WillOverflow returns false + ASSERT_TRUE(unit.WillOverflow(6)); +} + +TEST_F(VerbosePayloadFixture, SetBufferShallRebindReference) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "Verifies setting new buffer for VerbosePayload will update the pointer to the new buffer and discards " + "the old one."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given VerbosePayload is constructed with a different buffer. + constexpr std::size_t kOldBufferSize = 5U; + VerbosePayload unit = constructUnitWithSize(kOldBufferSize); + + // When assigning another buffer... + constexpr std::size_t kNewBufferSize = 7U; + ByteVector new_buffer{}; + new_buffer.reserve(kNewBufferSize); + unit.SetBuffer(new_buffer); + + // ...check that the new buffer is used: + ASSERT_EQ(unit.RemainingCapacity(), kNewBufferSize); + + // ...and that the old buffer is unchanged: + ASSERT_EQ(buffer_.capacity(), kOldBufferSize); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/wait_free_producer_queue/BUILD b/mw/log/detail/wait_free_producer_queue/BUILD new file mode 100644 index 0000000..420d09e --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/BUILD @@ -0,0 +1,83 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//platform/aas/mw:common_features.bzl", "COMPILER_WARNING_FEATURES") +load("//platform/aas/tools/itf/bazel_gen:itf_gen.bzl", "py_unittest_qnx_test") + +cc_library( + name = "wait_free_producer_queue", + srcs = [ + "alternating_control_block.cpp", + "alternating_reader.cpp", + "linear_control_block.cpp", + "linear_reader.cpp", + "wait_free_alternating_writer.cpp", + "wait_free_linear_writer.cpp", + ], + hdrs = [ + "alternating_control_block.h", + "alternating_reader.h", + "linear_control_block.h", + "linear_reader.h", + "wait_free_alternating_writer.h", + "wait_free_linear_writer.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = ["//platform/aas/mw/log/detail/data_router:__subpackages__"], + deps = [ + "@amp", + ], +) + +cc_test( + name = "unit_tests", + srcs = [ + "alternating_control_block_test.cpp", + "linear_control_block_test.cpp", + "linear_reader_test.cpp", + "wait_free_alternating_writer_test.cpp", + "wait_free_linear_writer_test.cpp", + ], + features = COMPILER_WARNING_FEATURES + [ + "aborts_upon_exception", + ], + linkopts = select({ + "@platforms//os:linux": ["-latomic"], # needed for spp-host-clang + "//conditions:default": [], + }), + tags = ["unit"], + visibility = ["//platform/aas/mw/log/detail:__pkg__"], + deps = [ + ":wait_free_producer_queue", + "//platform/aas/mw/log/test/console_logging_environment", + "//third_party/googletest:main", + "//third_party/libatomic", + ], +) + +py_unittest_qnx_test( + name = "pkg_unit_tests_qnx", + test_cases = [ + ":unit_tests", + ], +) + +py_unittest_qnx_test( + name = "unit_tests_qnx", + tags = ["manual"], + test_suites = [ + ":pkg_unit_tests_qnx", + ], + visibility = ["//platform/aas/mw/log/detail:__pkg__"], +) diff --git a/mw/log/detail/wait_free_producer_queue/README.md b/mw/log/detail/wait_free_producer_queue/README.md new file mode 100644 index 0000000..d859358 --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/README.md @@ -0,0 +1,217 @@ + + + + +# Wait-free Multiple-Producer Single Consumer Queue + +- [Wait-free Multiple-Producer Single Consumer Queue](#wait-free-multiple-producer-single-consumer-queue) + - [Introduction](#introduction) + - [Usage](#usage) + - [Static Design](#static-design) + - [Detailed Design: Wait-free Linear Writer](#detailed-design-wait-free-linear-writer) + - [Detailed Design: Wait-Free Alternating Buffers](#detailed-design-wait-free-alternating-buffers) + +## Introduction + +This package provides a **wait-free** multiple-producer single consumer queue +implementation that is wait-free for multiple concurrent writer threads. We +support the following requirements tailored for the use case in `mw::log`: + +1. Wait-free for multiple producers: Writing for multiple writer threads shall + be lock-free and wait-free. A write operation shall have a deterministic + upper-bounded for runtime. The implementation shall not have cross-thread + mutual exclusion paths. +2. Single-consumer: The data structure supports only a single consumer thread. +3. Blocking consumer: Reading operation shall not be wait-free, and shall not + block active writers. +4. Data flow: Data shall be transported preserving first-in-first-out (FIFO) + order of a sequence of contiguous packets. +5. No overwriting: If the buffer is full, new data shall be dropped. Overwriting + of data shall be not supported. + +As algorithms with atomic data structures are easy to get wrong and hard to +verify, our implementation shall be based on a very minimalistic design. +Performance is only a secondary goal after correctness and safety. Thus we set +the following **non-goals** for the current implementation: + +1. Sequentially consistent atomic memory order: The implementation shall + initially use sequential consistency for all atomic operations for + simplicity. If needed, the memory ordering of atomic operation can be + relieved for performance improvement as a future step. +2. Cache line optimization and false sharing: The performance of the + implementation could be improved further by taking into account cache line + optimization and avoiding false sharing. We believe that the performance will + still be improved compared to a mutex-based approach and omit that aspect for + optional future work. + +## Usage + +Consider the following example for the writer / producer use case: + +```C++ +// Initialization: +void Initialize(){ + // Create two equal-sized linear buffers for writing. + std::vector buffer1(kBufferSize); + std::vector buffer2(kBufferSize); + bmw::mw::log::detail::AlternatingControlBlock control_block{}; + control_block.control_block_1.data = + amp::span(buffer1.data(), static_cast(buffer1.size())); + control_block.control_block_2.data = + amp::span(buffer2.data(), static_cast(buffer2.size())); + + // Initialize the writer given a control block: + bmw::mw::log::detail::WaitFreeAlternatingWriter writer{control_block}; + // ... +} + +void ProducerThread(bmw::mw::log::detail::WaitFreeAlternatingWriter& writer){ + // Acquire shall never block, but may return empty result if no capacity. + acquire_result = writer.Acquire(kAcquireLengthBytes); + + if (acquired_data.size() != kAcquireLength) + { + // Error handling: no space in buffer left + // ... + return; + } + + // Use data + ProduceData(acquire_result.value()); + + // Release data for reading. The writer shall always release acquired data. + writer.Release(acquire_result.value()); +} +``` + +The writer is initialized with a control block structure that references two +equal-sized linear buffers. The producer can rely on the non-blocking and +wait-free behavior of the `Acquire()` method. The producer shall adhere to the +contract to always `Release()` data that is ready for the consumer. In addition, +it has to consider error handling if `Acquire()` cannot allocate more space. + +For the consumer, consider the following example code: + +```C++ +void Consumer(amp::stop_token stop_token) { + bmw::mw::log::detail::AlternatingReader reader{control_block}; + while (stop_token.exit_requested() == false) + { + // Toggle between the two alternating linear buffers for concurrent reading and writing. + reader.Switch(); + + auto read_result = reader.Read(); + while (read_result.has_value()) + { + const auto packet = read_result.value(); + ConsumePacket(packet); + + read_result = reader.Read(); + } + + // At this point all data from the current linear buffer has been consumed, + // and we can toggle to the next buffer for reading. + } +} +``` + +The key method for the consumer-side is the `Switch()` method that toggles the +buffers used for reading and writing. The operation of the consumer is blocking, +i.e., it waits until (all) pending writes to the buffer have completed. After +`Switch()` returns, the data from one buffer can be safely read without need for +further synchronization. In parallel, the writer can concurrently write to the +other buffer. + +## Static Design + +The design is divided into data structures for linear and alternating mode. +The alternating mode is composed by reusing the linear entities: + +![Class diagram](/swh/ddad_platform/aas/mw/log/detail/wait_free_producer_queue/design/class_diagram.uxf?ref=e42bb784c8567256b2c8837cd7a771e6193fc9cb) + +For each mode, we extract the data members into POD structs that could be +allocated on shared memory. The data structs should not be accessed directly but +only through the corresponding writer- and reader-classes. + +The responsibility of the linear classes is to provide a simple foundation for +lock-free and wait-free writing. The alternating part enables a continuous +operation and decoupling the reader from the writers. This decomposition keeps +both aspects simple and safe to implement. + +## Detailed Design: Wait-free Linear Writer + +We first explain the linear writer that is the basic building block concurrent +wait-free writing. The buffer created by the writer consists of series of +chunks. Each chunk consists of a length, followed by the payload. The figure +below shows two producer threads writing concurrently and without mutex on the +linear buffer. The synchronization is based on the +[atomic_fetch_add](https://en.cppreference.com/w/c/atomic/atomic_fetch_add) +operation. The linear control block consists of two running atomic indices +`acquire_index` and `written_index`. A producer calls `Acquire()` which +atomically increases the `acquire_index`. The atomic properties guarantee that +the value returned by the atomic operation points to an offset in the linear +array that is exclusively reserved for the caller. For each acquired chunk, the +writer first writes the length of the payload. The length is followed by the +payload as written by the user. When the producer is done they call `Release()` +to finish the data for reading. This operation atomically adds the number of +written bytes to the `written_index`. Thus as soon as all pending writers are +finished we arrive at the equality of `written_index` and `acquire_index`. + +![Linear writer sequence](/swh/ddad_platform/aas/mw/log/detail/wait_free_producer_queue/design/wait_free_linear_buffer.uxf?ref=109d7fea89c8f7f627b62f9e78299f067241a6ec) + +The implementation shall prevent the overflow of the running indices. This +achieved by limiting the maximum allowed number of concurrent writers to some +theoretical limit (e.g. 64 threads). Once the limit is exceeded `Acquire()` will +return an empty value. We also limit the maximum size that could be allocated in +one `Acquire()` call. When the acquire exceeds a threshold of +`MaxNumberOfConcurrentWriters * MaxAcquireLength` the writer shall refuse to +write additional data in order to prevent a potential overflow. In addition, the +implementation always has to check if the `acquired_index` fits in the given +linear buffer with the total length requested by the caller. + +In addition, the implementation also needs to consider *failed acquisitions*. A +failed acquisition happens when a producer tries to acquire data, but does not +get the full amount of space as we reach the end of the buffer capacity. In such +a case, the writer shall only write the length into the buffer if there is +enough space. Then the reader identifies that a failed acquisition happened at +the end of the buffer by comparing the expected length with the actual size of +the buffer. If an out-of-bounds access would occur, the reader shall assume a +failed acquisition and drop the data. + +## Detailed Design: Wait-Free Alternating Buffers + +With the wait-free linear writer we have established the basis for concurrent +lock-free writing. However, we also need to enable continuous operation as the +reading of the data should also happen in parallel and without blocking the +producers. Enter the Wait-Free Alternating Buffers Writer concept: The +`AlternatingControlBlock` data structure is built on top of two +`LinearControlBlock`s. There is only a single atomic boolean variable in +addition that controls which linear buffer is currently for reading and which +one is for writing. + +The figure below shows the concurrent operation of a producer and consumer thread: + +![Alternating writer sequence](/swh/ddad_platform/aas/mw/log/detail/wait_free_producer_queue/design/wait_free_alternating_buffers.uxf?ref=109d7fea89c8f7f627b62f9e78299f067241a6ec) + +In the sequence diagram, the producer thread starts to acquire data on the +linear buffer 1. After that the consumer threads calls `Switch()` to toggle the +buffer that should be written to. Any future writer will use buffer 2 after the +atomic flag `active_for_writing` is updated. Still, buffer 1 is not yet ready +for reading as we first have to wait for pending writers to finish. The +producers increase an atomic counter at the beginning of `Acquire()`. In +`Release()` the counter is atomically decremented. Thus the consumer is able to +tell when all pending writers have finished writing to the current buffer. After +this point, the reader returns a span pointing to buffer 1 that can be safely +accessed then by the consumer. In parallel, the producer is concurrently writing +new data to buffer 2 that will be read by the consumer after the next call to +`Switch()`. diff --git a/mw/log/detail/wait_free_producer_queue/alternating_control_block.cpp b/mw/log/detail/wait_free_producer_queue/alternating_control_block.cpp new file mode 100644 index 0000000..09e6fd3 --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/alternating_control_block.cpp @@ -0,0 +1,65 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#include "platform/aas/mw/log/detail/wait_free_producer_queue/alternating_control_block.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +AlternatingControlBlockSelectId SelectLinearControlBlockId(std::uint32_t count) +{ + if (count % 2 == 0) + { + return AlternatingControlBlockSelectId::kBlockEven; + } + else + { + return AlternatingControlBlockSelectId::kBlockOdd; + } +} + +AlternatingControlBlockSelectId GetOppositeLinearControlBlock(const AlternatingControlBlockSelectId id) +{ + AlternatingControlBlockSelectId return_value{AlternatingControlBlockSelectId::kBlockEven}; + switch (id) + { + case AlternatingControlBlockSelectId::kBlockOdd: + return_value = AlternatingControlBlockSelectId::kBlockEven; + break; + case AlternatingControlBlockSelectId::kBlockEven: + return_value = AlternatingControlBlockSelectId::kBlockOdd; + break; + default: + break; + } + return return_value; +} + +AlternatingControlBlock& InitializeAlternatingControlBlock(AlternatingControlBlock& alternating_control_block) +{ + alternating_control_block.switch_count_points_active_for_writing = 1UL; + return alternating_control_block; +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/wait_free_producer_queue/alternating_control_block.h b/mw/log/detail/wait_free_producer_queue/alternating_control_block.h new file mode 100644 index 0000000..f10762d --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/alternating_control_block.h @@ -0,0 +1,84 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_WAIT_FREE_ALTERNATING_CONTROL_BLOCK_H +#define PLATFORM_AAS_MW_LOG_DETAIL_WAIT_FREE_ALTERNATING_CONTROL_BLOCK_H + +#include "platform/aas/mw/log/detail/wait_free_producer_queue/linear_control_block.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +// ----- COMMON_ARGUMENTATION ---- +// Maintaining compatibility and avoiding performance overhead outweighs POD Type (class) based design. The Struct +// is ONLY used internally under the namespace detail and ONLY for data_router sub-dir, it is NOT exposed publicly; +// this is additionally guaranteed by the build system(bazel) visibility. Moreover, the Type is simple and does not +// require invariance (interface OR custom behavior) as per the design. +// ------------------------------- + +struct AlternatingControlBlock +{ + // COMMON_ARGUMENTATION + // + LinearControlBlock control_block_even{}; + // COMMON_ARGUMENTATION + // + LinearControlBlock control_block_odd{}; + // switch count is used to select buffer active for writing. Odd value selects control_block_odd for writing, even + // value points control_block_even for writing. + // COMMON_ARGUMENTATION + // + std::atomic switch_count_points_active_for_writing{0UL}; +}; + +/// \brief Initializes AlternatingControlBlock to set reader and writer side of the buffers making a 0-index buffer +/// reserved for reader and 1-indexed buffer available for writer. Switch counter is set to 1 pointing to writer buffer. +AlternatingControlBlock& InitializeAlternatingControlBlock(AlternatingControlBlock& alternating_control_block); + +enum AlternatingControlBlockSelectId : std::uint8_t +{ + kBlockEven, + kBlockOdd, +}; + +// Template function is used to resolve return type as const or non-const depending on iput arguments type. +template , AlternatingControlBlock>::value, bool>> +auto& SelectLinearControlBlockReference(AlternatingControlBlockSelectId block_id, T& control) +{ + if (block_id == AlternatingControlBlockSelectId::kBlockEven) + { + return control.control_block_even; + } + else + { + return control.control_block_odd; + } +} + +AlternatingControlBlockSelectId GetOppositeLinearControlBlock(const AlternatingControlBlockSelectId id); +AlternatingControlBlockSelectId SelectLinearControlBlockId(std::uint32_t count); + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif diff --git a/mw/log/detail/wait_free_producer_queue/alternating_control_block_test.cpp b/mw/log/detail/wait_free_producer_queue/alternating_control_block_test.cpp new file mode 100644 index 0000000..1590612 --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/alternating_control_block_test.cpp @@ -0,0 +1,98 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/wait_free_producer_queue/alternating_control_block.h" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace test +{ + +namespace +{ + +TEST(AlternatingControlBlockTest, GettingOppositeBlockShallSucceed) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "The helper function shall check if the length and offset are valid"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_EQ(GetOppositeLinearControlBlock(AlternatingControlBlockSelectId::kBlockEven), + AlternatingControlBlockSelectId::kBlockOdd); + EXPECT_EQ(GetOppositeLinearControlBlock(AlternatingControlBlockSelectId::kBlockOdd), + AlternatingControlBlockSelectId::kBlockEven); +} + +TEST(AlternatingControlBlockTest, GettingBlockEvenAndOddBasedOnCoutnerValue) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "The helper function shall check if the length and offset are valid"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + EXPECT_EQ(SelectLinearControlBlockId(1UL), AlternatingControlBlockSelectId::kBlockOdd); + EXPECT_EQ(SelectLinearControlBlockId(3UL), AlternatingControlBlockSelectId::kBlockOdd); + + EXPECT_EQ(SelectLinearControlBlockId(0UL), AlternatingControlBlockSelectId::kBlockEven); + EXPECT_EQ(SelectLinearControlBlockId(2UL), AlternatingControlBlockSelectId::kBlockEven); +} + +TEST(AlternatingControlBlockTest, GettingReferenceBlock) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "The helper function shall check if the length and offset are valid"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + const AlternatingControlBlock const_block; + EXPECT_EQ(&SelectLinearControlBlockReference(AlternatingControlBlockSelectId::kBlockEven, const_block), + &const_block.control_block_even); + EXPECT_EQ(&SelectLinearControlBlockReference(AlternatingControlBlockSelectId::kBlockOdd, const_block), + &const_block.control_block_odd); +} + +TEST(AlternatingControlBlockTest, GettingReferenceConstBlock) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "The helper function shall check if the length and offset are valid"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + AlternatingControlBlock block; + EXPECT_EQ(&SelectLinearControlBlockReference(AlternatingControlBlockSelectId::kBlockEven, block), + &block.control_block_even); + EXPECT_EQ(&SelectLinearControlBlockReference(AlternatingControlBlockSelectId::kBlockOdd, block), + &block.control_block_odd); +} + +} // namespace + +} // namespace test +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/wait_free_producer_queue/alternating_reader.cpp b/mw/log/detail/wait_free_producer_queue/alternating_reader.cpp new file mode 100644 index 0000000..f1bf12b --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/alternating_reader.cpp @@ -0,0 +1,119 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/wait_free_producer_queue/alternating_reader.h" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +AlternatingReaderProxy::AlternatingReaderProxy(AlternatingControlBlock& dcb) noexcept + : alternating_control_block_(dcb), + previous_logging_ipc_counter_value_{dcb.switch_count_points_active_for_writing.load()} +{ +} + +AlternatingReadOnlyReader::AlternatingReadOnlyReader(const AlternatingControlBlock& dcb, + const amp::span buffer_even, + const amp::span buffer_odd) noexcept + : alternating_control_block_(dcb), reader_({}), buffer_even_{buffer_even}, buffer_odd_{buffer_odd} +{ +} + +LinearReader AlternatingReadOnlyReader::CreateLinearReader(std::uint32_t block_id_count) noexcept +{ + auto block_id = SelectLinearControlBlockId(block_id_count); + auto& block = SelectLinearControlBlockReference(block_id, alternating_control_block_); + + const auto written_bytes = block.written_index.load(); + + auto& buffer = block_id == AlternatingControlBlockSelectId::kBlockEven ? buffer_even_ : buffer_odd_; + return CreateLinearReaderFromDataAndLength(buffer, written_bytes); +} + +auto GetSplitBlocks(AlternatingControlBlockSelectId block_id_active_for_writing, + AlternatingControlBlock& alternating_control_block) +{ + return std::tuple{ReusedCleanupBlockReference{SelectLinearControlBlockReference( + GetOppositeLinearControlBlock(block_id_active_for_writing), alternating_control_block)}, + TerminatingBlockReference{ + SelectLinearControlBlockReference(block_id_active_for_writing, alternating_control_block)}}; +} + +/// Assumption: The Switch method shall not be called from a concurrent contexts i.e. it supports single consumer. +std::uint32_t AlternatingReaderProxy::Switch() noexcept +{ + const auto switch_count_points_active_for_writing = + alternating_control_block_.switch_count_points_active_for_writing.load(); + + // Sanity check: Relevant part of shared memory data context shall not be modified outside of this function: + // TODO: Handle or report error. This is most likely a fatal error + // AMP_ASSERT(previous_logging_ipc_counter_value_ == switch_count_points_active_for_writing); + + auto block_id_active_for_writing = SelectLinearControlBlockId(switch_count_points_active_for_writing); + + auto [restarting_control_block, terminating_control_block_intermediate] = + GetSplitBlocks(block_id_active_for_writing, alternating_control_block_); + + std::ignore = terminating_control_block_intermediate; + + // Reset counters for writing new data into restarting block. + const auto acquired_index = restarting_control_block.GetReusedCleanupBlock().acquired_index.exchange(0); + const auto written_index = restarting_control_block.GetReusedCleanupBlock().written_index.exchange(0); + // TODO: , handle or report error. Reader shall work on completely written, because ASIL-B clients shall + // not just abandon the work (exceptions at initialization must be considered) AMP_ASSERT(acquired_index == + // written_index); + std::ignore = acquired_index; + std::ignore = written_index; + + // Switch the active buffer for future writers. + const auto save_switch_count = alternating_control_block_.switch_count_points_active_for_writing.fetch_add(1UL); + + // TODO: Handle fatal error. No one else shall increment switch counter: + // AMP_ASSERT(save_switch_count == switch_count_points_active_for_writing); + + std::atomic_thread_fence(std::memory_order_release); + + // Writer switch may be incomplete. It is not yet safe to read the data in the buffer. + // It is left as reader responsibility to check if writers released buffer. + + previous_logging_ipc_counter_value_ = save_switch_count + 1; + return save_switch_count; +} + +bool AlternatingReadOnlyReader::IsBlockReleasedByWriters(const std::uint32_t block_id_count) const noexcept +{ + auto block_id = SelectLinearControlBlockId(block_id_count); + auto& block = SelectLinearControlBlockReference(block_id, alternating_control_block_); + + const bool result = + (block.number_of_writers.load() == 0) && (block.written_index.load() == block.acquired_index.load()); + if (result) + { + std::atomic_thread_fence(std::memory_order_acquire); + } + return result; +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/wait_free_producer_queue/alternating_reader.h b/mw/log/detail/wait_free_producer_queue/alternating_reader.h new file mode 100644 index 0000000..93f9c93 --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/alternating_reader.h @@ -0,0 +1,102 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_WAIT_FREE_ALTERNATING_READER_H +#define PLATFORM_AAS_MW_LOG_DETAIL_WAIT_FREE_ALTERNATING_READER_H + +#include "platform/aas/mw/log/detail/wait_free_producer_queue/alternating_control_block.h" +#include "platform/aas/mw/log/detail/wait_free_producer_queue/linear_reader.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +class AlternatingReadOnlyReader +{ + public: + explicit AlternatingReadOnlyReader(const AlternatingControlBlock& dcb, + const amp::span buffer_even, + const amp::span buffer_odd) noexcept; + + /// \brief Check if all the references to block pointed by block_id_count were dropped by the writers. + /// Returns false if at least one buffer is still referenced by writer, true otherwise. + bool IsBlockReleasedByWriters(const std::uint32_t block_id_count) const noexcept; + /// \brief Creates LinearReader which is based on span of data pointing to memory directly within Shared-memory + /// buffer. It must be synchronized by a user. Shall be called only after making sure that data is no longer being + /// modified by writers. + LinearReader CreateLinearReader(const std::uint32_t block_id_count) noexcept; + + private: + const AlternatingControlBlock& alternating_control_block_; + std::optional reader_; + const amp::span buffer_even_; + const amp::span buffer_odd_; +}; + +/// \brief Reader for two alternating linear buffers. +/// An instance of this class is not thread-safe and should only be used by a +/// single thread exclusively. +class AlternatingReaderProxy +{ + public: + explicit AlternatingReaderProxy(AlternatingControlBlock& dcb) noexcept; + + /// \brief Alternate the buffers for reading and writing. + /// Returns the value of counter before increment aka buffer acquried for reading. + std::uint32_t Switch() noexcept; + + private: + AlternatingControlBlock& alternating_control_block_; + std::uint32_t previous_logging_ipc_counter_value_; +}; + +// Wrapper structure used to enforce type checking: +template , LinearControlBlock>::value, bool>> +class ReusedCleanupBlockReference +{ + public: + explicit constexpr ReusedCleanupBlockReference(T& linear_control_block) + : reused_cleanup_block_{linear_control_block} + { + } + inline T& GetReusedCleanupBlock() { return reused_cleanup_block_; } + + private: + T& reused_cleanup_block_; +}; + +// Wrapper structure used to enforce type checking: +template , LinearControlBlock>::value, bool>> +class TerminatingBlockReference +{ + public: + explicit constexpr TerminatingBlockReference(T& linear_control_block) : terminating_block_{linear_control_block} {} + inline T& GetTerminatingBlock() { return terminating_block_; } + + private: + T& terminating_block_; +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif diff --git a/mw/log/detail/wait_free_producer_queue/design/class_diagram.uxf b/mw/log/detail/wait_free_producer_queue/design/class_diagram.uxf new file mode 100644 index 0000000..6aa556a --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/design/class_diagram.uxf @@ -0,0 +1,53 @@ +10Space for diagram notesUMLClass42080250120<<POD>> +LinearControlBlock +-- ++ data: span<byte> ++ acquired_index: atomic_int ++ written_index: atomic_int ++ number_of_writers: atomic_int + +bg=white +fontsize=14UMLClass120280410120WaitFreeLinearWriter +-- ++ WaitFreeLinearWriter(control_block: LinearControlBlock&) ++ Acquire(length): optional<AcquiredData > ++ Release(AcquiredData): void + + +bg=white +fontsize=14Relation45019050110lt=<- +uses10;10;10;90UMLClass580280340120LinearReader +-- ++ LinearReader(span<Byte>& data) ++ Read(): optional<span<Byte> > + + + + +bg=white +fontsize=14Relation60019050110lt=<- +uses10;10;10;90UMLClass400440300120<<POD>> +AlternatingControlBlock +-- ++ linear_control_block_1: LinearControlBlock ++ linear_control_block_2: LinearControlBlock + +bg=white +fontsize=14Relation54019030270lt=<<<<<-10;250;10;10UMLClass90620470120WaitFreeAlternatingWriter +-- ++ WaitFreeAlternatingWriter(control_block: AlternatingControlBlock&) ++ Acquire(length): optional<AcquiredData > ++ Release(AcquiredData): void + +bg=white +fontsize=14UMLClass610620440120AlternatingReader +-- ++ AlternatingReader(AlternatingControlBlock& data) ++ Read(): optional<span<Byte> > ++ Switch() + + +bg=white +fontsize=14Relation33039030250lt=<<<<<-10;230;10;10Relation4805505090lt=<- +uses10;10;10;70Relation6205505090lt=<- +uses10;10;10;70Relation81039030250lt=<<<<<-10;230;10;10 diff --git a/mw/log/detail/wait_free_producer_queue/design/wait_free_alternating_buffers.uxf b/mw/log/detail/wait_free_producer_queue/design/wait_free_alternating_buffers.uxf new file mode 100644 index 0000000..cc9504d --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/design/wait_free_alternating_buffers.uxf @@ -0,0 +1,152 @@ +10Space for diagram notesUMLGeneric26015022040_Producer Thread_ +(WaitFreeAlternatingWriter class) +bg=white +fontsize=14UMLGeneric70015017040_Consumer Thread_ +(AlternatingReader class) +bg=white +fontsize=14Relation3601803060lt=.10;10;10;40UMLObject17022037080Acquire(64 bytes): +-- +number_of_writers_1.increment() +-- +acquired_index_1 == 72 +number_of_writers_1 == 1 +bg=white +fontsize=14UMLObject3001057080AlternatingControlBlock +-- +acquired_index_1 == 0, written_index_1 == 0, number_of_writers_1 == 0 +acquired_index_2 == 0, written_index_2 == 0, number_of_writers_2 == 0 +active_for_writing == 1 +bg=white +fontsize=14Relation3608025090lt=<. +fontsize=1410;70;230;10Relation5808023090lt=<. +fontsize=14210;70;10;10Relation77018030100lt=.10;10;10;80UMLObject670260300260Switch(): + +-- + +Reset buffer 2 for writing +acquired_index_2.store(0) +written_index_2.store(0) + +-- + +Toggle future writers to use buffer 2 +active_for_writing.store(2) + +-- + +Block until pending writers finished on +buffer 1. +bg=white +fontsize=14Relation3602903070lt=.10;10;10;50UMLClass28046021060Release(64 bytes): +-- +written_index_1 == (8 + 64) +number_of_writers_1 == 0 +bg=white +fontsize=14UMLClass27034024090WriteDataProducer1(acquired_data) +-- +Writing on buffer 1, range [8:72) +bg=white +fontsize=14Relation3604203060lt=.10;10;10;40UMLClass7901120140140[0:8) +-- +Data: +16 +-- +Length of Packet 2 +bg=white +fontsize=14 +UMLClass640108016030Linear Buffer 2 Content: +bg=white +fontsize=14 +UMLClass9301120160140[8:24) +-- +Data: +Packet 2 +-- +Producer1, Packet 2 +bg=white +fontsize=14 +UMLClass6901120100140Index Range: +-- +Data: + +-- +Description +bg=white +fontsize=14 +UMLObject29090057080AlternatingControlBlock +-- +acquired_index_1 == 72, written_index_1 == 72, number_of_writers_1 == 0 +acquired_index_2 == 24, written_index_2 == 24, number_of_writers_2 == 0 +active_for_writing == 2 +bg=white +fontsize=14Relation3605103060lt=. +fontsize=14 +10;40;10;10Relation77077030150lt=<. +fontsize=1410;130;10;10UMLClass150108016030Linear Buffer 1 Content: +bg=white +fontsize=14 +UMLClass3101120140140[0:8) +-- +Data: +64 +-- +Length of Packet 1 +bg=white +fontsize=14 +UMLClass4501120160140[8:72) +-- +Data: +WriteDataProducer1() +-- +Producer 1, Packet 1 +bg=white +fontsize=14 +UMLClass2101120100140Index Range: +-- +Data: + +-- +Description +bg=white +fontsize=14 +Relation48050021040lt=<. +fontsize=14 +blocks until10;20;190;20Relation53024016050lt=<. +fontsize=14 +happens +after10;20;140;20UMLClass70057017080Read() +-- +buffer_for_reading == 1 +read_range == [0,72) +bg=white +fontsize=14Relation7705103080lt=<. +fontsize=1410;60;10;10UMLClass70070017080Process packet 1 on +buffer 1, range [8:72) + +bg=white +fontsize=14Relation7706403080lt=<. +fontsize=1410;60;10;10UMLObject20055037090Acquire(16 bytes): +-- +acquired_index_2 == 24 +number_of_writers_2 == 1 + +bg=white +fontsize=14Relation3608303090lt=<. +fontsize=14 +10;70;10;10UMLClass26067024090WriteDataProducer1(acquired_data) +-- +Writing on buffer 2, range [8:24) +bg=white +fontsize=14UMLClass28078021060Release(16 bytes): +-- +written_index_2 == 8 + 16 +number_of_writers_2 == 0 +bg=white +fontsize=14Relation3606303060lt=. +fontsize=14 +10;40;10;10Relation3607503050lt=. +fontsize=14 +10;30;10;10Relation49072023050lt=<.> +fontsize=14 +concurrent writing +and reading10;20;210;20 diff --git a/mw/log/detail/wait_free_producer_queue/design/wait_free_linear_buffer.uxf b/mw/log/detail/wait_free_producer_queue/design/wait_free_linear_buffer.uxf new file mode 100644 index 0000000..fb5ceff --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/design/wait_free_linear_buffer.uxf @@ -0,0 +1,101 @@ +10UMLGeneric10013019040_Producer Thread 1_ +(WaitFreeLinearWriter class) +bg=white +fontsize=14UMLGeneric52013019040_Producer Thread 2_ +(WaitFreeLinearWriter class) +bg=white +fontsize=14Relation1901603080lt=.10;10;10;60UMLObject4022032080Acquire(64 bytes): +-- +length_index = acquired_index.fetch_add(8 + 64) +-- +acquired_index == 72 +length_index == 0 +bg=white +fontsize=14UMLObject1303057040LinearControlBlock +-- +acquired_index == 0, written_index == 0 +bg=white +fontsize=14Relation1906025090lt=<. +fontsize=1410;70;230;10Relation4106023090lt=<. +fontsize=14210;70;10;10Relation60016030160lt=.10;10;10;140UMLObject50030030070Acquire(16 bytes): +-- +acquired_index == 96 +length_index == 72 +bg=white +fontsize=14Relation19029030140lt=.10;10;10;120Relation35028017040lt=<. +happens before +bg=white +fontsize=14150;20;10;20Relation6003603070lt=.10;10;10;50UMLClass11061021050Release(64 bytes): +-- +written_index == 24 + (8 + 64) +bg=white +fontsize=14UMLClass51054021070Release(16 bytes): +-- +written_index.fetch_add(8 + 16) +-- +written_index == 8 + 16 +bg=white +fontsize=14UMLClass49041024090WriteDataProducer2(acquired_data) +-- +Writing on range [80:96) +bg=white +fontsize=14UMLClass10041024090WriteDataProducer1(acquired_data) +-- +Writing on range [8:72) +bg=white +fontsize=14Relation33043018050lt=<.> +Concurrent +Writing +bg=white +fontsize=1410;20;160;20Relation6004903070lt=.10;10;10;50Relation19049030140lt=.10;10;10;120Relation31059022040lt=<. +happens before +bg=white +fontsize=1410;20;200;20UMLClass180830140140[0:8) +-- +Data: +64 +-- +Length of Producer1 +bg=white +fontsize=14 +group=1UMLClass3078016030Linear Buffer Content: +bg=white +fontsize=14UMLClass320830160140[8:72) +-- +Data: +WriteDataProducer1() +-- +Payload of Producer1 +bg=white +fontsize=14 +group=1UMLClass480830160140[72:80) +-- +Data: +16 +-- +Length of Payload 2 +bg=white +fontsize=14 +group=1UMLClass80830100140Index Range: +-- +Data: + +-- +Description +bg=white +fontsize=14 +group=1UMLClass640830160140[88:96) +-- +Data: +WriteDataProducer2() +-- +Payload of Producer2 +bg=white +fontsize=14 +group=1UMLObject12068057040LinearControlBlock +-- +acquired_index == written_index == 96 +bg=white +fontsize=14Relation1906503050lt=<. +fontsize=1410;30;10;10Relation60060030100lt=<. +fontsize=1410;80;10;10 diff --git a/mw/log/detail/wait_free_producer_queue/linear_control_block.cpp b/mw/log/detail/wait_free_producer_queue/linear_control_block.cpp new file mode 100644 index 0000000..1225089 --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/linear_control_block.cpp @@ -0,0 +1,58 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/wait_free_producer_queue/linear_control_block.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +/// \returns true if number_of_bytes fits in control_block at the offset. +bool DoBytesFitInRemainingCapacity(const amp::span& buffer, + const Length offset, + const Length number_of_bytes) noexcept +{ + const Length buffer_size = GetDataSizeAsLength(buffer); + + if (offset > buffer_size) + { + return false; + } + + const auto remaining_number_of_bytes_at_offset = buffer_size - offset; + + if (number_of_bytes > remaining_number_of_bytes_at_offset) + { + return false; + } + + return true; +} + +Length GetDataSizeAsLength(const amp::span& data) noexcept +{ + // Cast from non-negative signed size type to unsigned length is safe. + static_assert(sizeof(Length) >= sizeof(SpanLength), "Length shall contain positive values of span size type"); + return static_cast(data.size()); +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/wait_free_producer_queue/linear_control_block.h b/mw/log/detail/wait_free_producer_queue/linear_control_block.h new file mode 100644 index 0000000..5375c17 --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/linear_control_block.h @@ -0,0 +1,107 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_WAIT_FREE_PRODUCER_QUEUE_LINEAR_CONTROL_BLOCK_H +#define PLATFORM_AAS_MW_LOG_DETAIL_WAIT_FREE_PRODUCER_QUEUE_LINEAR_CONTROL_BLOCK_H + +#include "amp_span.hpp" + +#include +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +using Byte = char; +// Use hack to misslead KW +// using Byte = std::string::value_type; +using Length = std::uint64_t; +using SpanLength = amp::span::size_type; + +/// \brief Each entry in the buffer will consist of a length prefix followed by payload. +/// \returns the length of the prefix in bytes. +constexpr Length GetLengthOffsetBytes() +{ + return sizeof(Length); +} + +constexpr Length GetMaxLinearBufferLengthBytes() +{ + static_assert(sizeof(Length) >= sizeof(SpanLength), "Max of SpanLength should be contained in Length"); + return static_cast(std::numeric_limits::max()); +} + +constexpr Length GetMaxAcquireLengthBytes() +{ + // We need to define an upper bound to define guarantees for avoiding index overflows. + // Limit could be increased if needed, but for DLT v1 we need at least 64 K. + return 128UL * 1024UL * 1024UL; +} + +constexpr Length GetMaxNumberOfConcurrentWriters() +{ + return 64UL; +} + +constexpr Length GetMaxLinearBufferCapacityBytes() +{ + return std::numeric_limits::max() - + GetMaxNumberOfConcurrentWriters() * (GetMaxAcquireLengthBytes() + GetLengthOffsetBytes()); +} + +// ----- COMMON_ARGUMENTATION ---- +// Maintaining compatibility and avoiding performance overhead outweighs POD Type (class) based design. The Struct +// is ONLY used internally under the namespace detail and ONLY for data_router sub-dir, it is NOT exposed publicly; +// this is additionally guaranteed by the build system(bazel) visibility. Moreover, the Type is simple and does not +// require invariance (interface OR custom behavior) as per the design. +// ------------------------------ + +struct LinearControlBlock +{ + // COMMON_ARGUMENTATION + // + amp::span data{}; + // COMMON_ARGUMENTATION + // + std::atomic acquired_index{}; + // COMMON_ARGUMENTATION + // + std::atomic written_index{}; + // COMMON_ARGUMENTATION + // + std::atomic number_of_writers{}; +}; + +/// \returns true if number_of_bytes fits in control_block at the offset. +bool DoBytesFitInRemainingCapacity(const amp::span& buffer, + const Length offset, + const Length number_of_bytes) noexcept; + +/// \returns the size of the span casted to Length +Length GetDataSizeAsLength(const amp::span& data) noexcept; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif diff --git a/mw/log/detail/wait_free_producer_queue/linear_control_block_test.cpp b/mw/log/detail/wait_free_producer_queue/linear_control_block_test.cpp new file mode 100644 index 0000000..aff250b --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/linear_control_block_test.cpp @@ -0,0 +1,82 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +/// Only testing additional corner cases here, the Reader is mostly already tested through behavior tests in +/// wait_free_linear_writer.cpp. + +#include "platform/aas/mw/log/detail/wait_free_producer_queue/linear_control_block.h" + +#include "amp_utility.hpp" + +#include + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +namespace +{ + +TEST(LinearControlBlock, LengthExceedingMaxThresholdShouldReturnTruncated) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "The helper function shall check if the length and offset are valid"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + constexpr auto kBufferSize = 5ul; + std::vector buffer(kBufferSize); + auto data = amp::span(buffer.data(), + static_cast(buffer.size())); + + const auto invalid_offset = kBufferSize + 1ul; + constexpr auto kArbitraryBytesCount = 2ul; + + ASSERT_FALSE(DoBytesFitInRemainingCapacity(data, invalid_offset, kArbitraryBytesCount)); +} + +TEST(LinearControlBlockTests, BytesShallNotFitInRemainingCCapacityIfOffsetBiggerThanTheBufferSize) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "Verify that the bytes can't fit in the remaining capacity in case the offset is bigger than the buffer size."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // When offset is biggeer than the buffer size. + const amp::span buffer; + constexpr bmw::mw::log::detail::Length kOffsetBiggerThanBufferSize{10U}; + + constexpr bmw::mw::log::detail::Length kSingleByte{1U}; + + // Shall return false. + EXPECT_FALSE(bmw::mw::log::detail::DoBytesFitInRemainingCapacity(buffer, kOffsetBiggerThanBufferSize, kSingleByte)); +} + +} // namespace + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/wait_free_producer_queue/linear_reader.cpp b/mw/log/detail/wait_free_producer_queue/linear_reader.cpp new file mode 100644 index 0000000..de3e5d9 --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/linear_reader.cpp @@ -0,0 +1,102 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#include "platform/aas/mw/log/detail/wait_free_producer_queue/linear_reader.h" + +#include + +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +LinearReader::LinearReader(const amp::span& data) noexcept : data_{data}, read_index_{} {} + +std::optional> LinearReader::Read() noexcept +{ + const auto offset = read_index_; + + if (DoBytesFitInRemainingCapacity(data_, offset, GetLengthOffsetBytes()) == false) + { + return {}; + } + + // Cast is safe by bounds check above. + const auto offset_casted = static_cast(offset); + const auto length_span = data_.subspan(offset_casted, static_cast(GetLengthOffsetBytes())); + Length length{}; + + * out of bounds there. */ + // We need to convert void pointer to bytes for serialization purposes, no out of bounds there. + // + const auto dst_span = amp::span{static_cast(static_cast(&length)), sizeof(length)}; + + std::ignore = std::copy_n(length_span.begin(), GetLengthOffsetBytes(), dst_span.begin()); + + if (length > GetMaxAcquireLengthBytes()) + { + // Unexpected high length value, drop all remaining data. + read_index_ = GetDataSizeAsLength(data_); + return {}; + } + + read_index_ += length + GetLengthOffsetBytes(); + + if (DoBytesFitInRemainingCapacity(data_, offset, GetLengthOffsetBytes() + length) == false) + { + return {}; + } + + // Calculate the offset where the actual user payload lies behind the length prefix. + const Length payload_offset = offset + GetLengthOffsetBytes(); + + // static_casts are safe due to the bounds check above. + return data_.subspan(static_cast(payload_offset), static_cast(length)); +} + +LinearReader CreateLinearReaderFromControlBlock(const LinearControlBlock& control_block) noexcept +{ + return CreateLinearReaderFromDataAndLength(control_block.data, control_block.written_index.load()); +} + +Length LinearReader::GetSizeOfWholeDataBuffer() const noexcept +{ + return GetDataSizeAsLength(data_); +} + +LinearReader CreateLinearReaderFromDataAndLength(const amp::span& data, + const Length number_of_bytes_written) noexcept +{ + const Length data_length = GetDataSizeAsLength(data); + const auto number_of_bytes_to_read = std::min(number_of_bytes_written, data_length); + + // static_cast is safe due to min limitation about. + const auto number_of_bytes_to_read_casted = static_cast(number_of_bytes_to_read); + + const auto data_cropped = data.subspan(0, number_of_bytes_to_read_casted); + return LinearReader(data_cropped); +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/wait_free_producer_queue/linear_reader.h b/mw/log/detail/wait_free_producer_queue/linear_reader.h new file mode 100644 index 0000000..5926f10 --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/linear_reader.h @@ -0,0 +1,67 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_WAIT_FREE_PRODUCER_QUEUE_LINEAR_READER_H +#define PLATFORM_AAS_MW_LOG_DETAIL_WAIT_FREE_PRODUCER_QUEUE_LINEAR_READER_H + +#include "platform/aas/mw/log/detail/wait_free_producer_queue/linear_control_block.h" + +#include "amp_span.hpp" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +/// \brief Reader for a linear buffer. +/// The Reader instance itself is not thread-safe and should only be used after +/// the last writer has finished. +class LinearReader +{ + public: + explicit LinearReader(const amp::span& data) noexcept; + + /// \brief Try to read the next available data. + /// Returns empty if the data is not available. + std::optional> Read() noexcept; + /// \brief Get size of whole data span which means sum of length encoding headers and payload of each chunk + Length GetSizeOfWholeDataBuffer() const noexcept; + + + private: + + + /* Members are private () */ + amp::span data_; + Length read_index_; + +}; + +LinearReader CreateLinearReaderFromControlBlock(const LinearControlBlock&) noexcept; +LinearReader CreateLinearReaderFromDataAndLength(const amp::span& data, + const Length number_of_bytes_written) noexcept; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif diff --git a/mw/log/detail/wait_free_producer_queue/linear_reader_test.cpp b/mw/log/detail/wait_free_producer_queue/linear_reader_test.cpp new file mode 100644 index 0000000..c7d47e0 --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/linear_reader_test.cpp @@ -0,0 +1,50 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +/// Only testing additional corner cases here, the Reader is mostly already tested through behavior tests in +/// wait_free_linear_writer.cpp. + +#include "platform/aas/mw/log/detail/wait_free_producer_queue/linear_reader.h" + +#include "amp_utility.hpp" + +#include + +#include + +namespace +{ + +TEST(LinearReaderTests, LengthExceedingMaxThresholdShouldReturnEmpty) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "The reader shall check if the length is valid and drop values that would lead to out of bounds access."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + constexpr auto kBufferSize = bmw::mw::log::detail::GetLengthOffsetBytes() * 2u; + std::vector buffer(kBufferSize); + auto data = amp::span(buffer.data(), + static_cast(buffer.size())); + const auto invalid_length = bmw::mw::log::detail::GetMaxAcquireLengthBytes() + 1; + amp::ignore = memcpy(buffer.data(), &invalid_length, sizeof(invalid_length)); + + bmw::mw::log::detail::LinearReader reader(data); + ASSERT_FALSE(reader.Read().has_value()); +} + +} // namespace diff --git a/mw/log/detail/wait_free_producer_queue/wait_free_alternating_writer.cpp b/mw/log/detail/wait_free_producer_queue/wait_free_alternating_writer.cpp new file mode 100644 index 0000000..c83b653 --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/wait_free_alternating_writer.cpp @@ -0,0 +1,168 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/wait_free_producer_queue/wait_free_alternating_writer.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +namespace +{ + +void ReleaseBlock(AlternatingControlBlockSelectId block_id, AlternatingControlBlock& alternating_control_block) +{ + auto& block_ref = SelectLinearControlBlockReference(block_id, alternating_control_block); + std::ignore = --block_ref.number_of_writers; +} + +// For a given loaded switch counter value, the AcquireBlock increases the number_of_writers value +std::optional AcquireBlock(std::uint32_t loaded_switch_counter_value, + AlternatingControlBlock& alternating_control_block) +{ + const auto candidate_block_id_active_for_writing = SelectLinearControlBlockId(loaded_switch_counter_value); + auto& writing_block_reference = + SelectLinearControlBlockReference(candidate_block_id_active_for_writing, alternating_control_block); + + // Mark the try to acquire given block. Remember to release it when lost arbitration. + // This operation shall block reader to progress + writing_block_reference.number_of_writers.fetch_add(1, std::memory_order_acquire); + + const auto check_atomic_transition_valid_counter = + alternating_control_block.switch_count_points_active_for_writing.load(); + + const auto second_atomic_transition_counter_value = loaded_switch_counter_value + 1; + + if (check_atomic_transition_valid_counter == loaded_switch_counter_value) + { + // switch has not happened and the writer was able to acquire block. The block can be returned to the user. + return candidate_block_id_active_for_writing; + } + else if (check_atomic_transition_valid_counter == second_atomic_transition_counter_value) + // switch was incremented by one + { + // The switch happened before we were able to increment 'number_of_writers' and initial candidate block was + // reserved by the reader i.e. we lost arbitrage. We are currently blocking an increment for future, but only + // second after the next one which still leaves some room for error even after considering the assumption that + // reader should not send another Read Acquire Request until writer releases old buffer. Thanks to checking + // this condition, we were able to move the problem until second switch happens i.e. concurrent operations are + // safe within the scope of one Switch operation. To resolve the issue completely the atomic operation shall be + // changed in a way that reading the state would block reader from progressing until the operation is resolved. + // It can be done by first acquiring blindly both blocks, resolving the selection based on block pointer and + // reader reservation flags and releasing not selected block only after the selection process is finished. + const auto concurrently_changed_block_id_active_for_writing = + GetOppositeLinearControlBlock(candidate_block_id_active_for_writing); + auto& concurrently_changed_writing_block_reference = SelectLinearControlBlockReference( + concurrently_changed_block_id_active_for_writing, alternating_control_block); + + concurrently_changed_writing_block_reference.number_of_writers.fetch_add(1, std::memory_order_acquire); + + const auto second_check_atomic_transition_valid_counter = + alternating_control_block.switch_count_points_active_for_writing.load(); + + // Release a candidate after acquiring the other block to block possible progress of the reader. + writing_block_reference.number_of_writers.fetch_sub(1, std::memory_order_release); + + if (second_check_atomic_transition_valid_counter != second_atomic_transition_counter_value) + { + concurrently_changed_writing_block_reference.number_of_writers.fetch_sub(1, std::memory_order_release); + return std::nullopt; + } + + return concurrently_changed_block_id_active_for_writing; + } + + // Switch has happened more than once. This shall not happen as current writer still holds one block which was + // acquired by incrementing number_of_writers member. Abort performing functions as it is fatal error. + writing_block_reference.number_of_writers.fetch_sub(1, std::memory_order_release); + return std::nullopt; +} + +} // anonymous namespace + +WaitFreeAlternatingWriter::WaitFreeAlternatingWriter(AlternatingControlBlock& control_block) noexcept + : alternating_control_block_(control_block), + wait_free_writing_even_(alternating_control_block_.control_block_even), + wait_free_writing_odd_(alternating_control_block_.control_block_odd) +{ +} + +std::optional WaitFreeAlternatingWriter::AcquireLinearDataOnAcquiredBlock( + AlternatingControlBlockSelectId block_id_active_for_writing_value, + Length length) noexcept +{ + if (block_id_active_for_writing_value == AlternatingControlBlockSelectId::kBlockEven) + { + const auto acquired_linear_data = wait_free_writing_even_.Acquire(length); + if (acquired_linear_data.has_value()) + { + return AlternatingAcquiredData{acquired_linear_data->data, block_id_active_for_writing_value}; + } + } + else + { + const auto acquired_linear_data = wait_free_writing_odd_.Acquire(length); + if (acquired_linear_data.has_value()) + { + return AlternatingAcquiredData{acquired_linear_data->data, block_id_active_for_writing_value}; + } + } + return std::nullopt; +} + +// Causes the increment of 'number_of_writers' in selected block. Remember to decrement the value when releasing. +std::optional WaitFreeAlternatingWriter::Acquire(const Length length) noexcept +{ + const auto switch_count_points_active_for_writing = + alternating_control_block_.switch_count_points_active_for_writing.load(); + + const auto block_id_active_for_writing = + AcquireBlock(switch_count_points_active_for_writing, alternating_control_block_); + + if (!block_id_active_for_writing.has_value()) + { + return std::nullopt; + } + const auto block_id_active_for_writing_value = block_id_active_for_writing.value(); + + const auto acquired_data = AcquireLinearDataOnAcquiredBlock(block_id_active_for_writing_value, length); + + // Release Block as part of finishing the selection operation, block is still acquired by operation called on + // WaitFreeLinearWriter + ReleaseBlock(block_id_active_for_writing_value, alternating_control_block_); + + return acquired_data; +} + +void WaitFreeAlternatingWriter::Release(const AlternatingAcquiredData& acquired_data) noexcept +{ + if (acquired_data.control_block_id == AlternatingControlBlockSelectId::kBlockEven) + { + wait_free_writing_even_.Release(AcquiredData{acquired_data.data}); + } + else + { + wait_free_writing_odd_.Release(AcquiredData{acquired_data.data}); + } +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/wait_free_producer_queue/wait_free_alternating_writer.h b/mw/log/detail/wait_free_producer_queue/wait_free_alternating_writer.h new file mode 100644 index 0000000..f660d72 --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/wait_free_alternating_writer.h @@ -0,0 +1,85 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_WAIT_FREE_ALTERNATING_WRITER_H +#define PLATFORM_AAS_MW_LOG_DETAIL_WAIT_FREE_ALTERNATING_WRITER_H + +#include "platform/aas/mw/log/detail/wait_free_producer_queue/alternating_control_block.h" +#include "platform/aas/mw/log/detail/wait_free_producer_queue/wait_free_linear_writer.h" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +// ----- COMMON_ARGUMENTATION ---- +// Maintaining compatibility and avoiding performance overhead outweighs POD Type (class) based design. The Struct +// is ONLY used internally under the namespace detail and ONLY for data_router sub-dir, it is NOT exposed publicly; +// this is additionally guaranteed by the build system(bazel) visibility. Moreover, the Type is simple and does not +// require invariance (interface OR custom behavior) as per the design. +// ------------------------------ + +struct AlternatingAcquiredData +{ + // COMMON_ARGUMENTATION + // + amp::span data; + // NOLINTBEGIN(cppcoreguidelines-pro-type-member-init) COMMON_ARGUMENTATION provided in the struct header. + // COMMON_ARGUMENTATION + // + AlternatingControlBlockSelectId control_block_id{AlternatingControlBlockSelectId::kBlockEven}; + // NOLINTEND(cppcoreguidelines-pro-type-member-init) COMMON_ARGUMENTATION provided in the struct header. +}; + +/// \brief Wait-free writing to two alternating linear buffers. +/// Thread-safe for multiple writers. +class WaitFreeAlternatingWriter +{ + public: + explicit WaitFreeAlternatingWriter(AlternatingControlBlock& control_block) noexcept; + + /// \brief Try to acquire the length for writing. + /// Returns empty if there is not enough space available. + std::optional Acquire(const Length length) noexcept; + + /// \brief Release the acquired data. + void Release(const AlternatingAcquiredData& acquired_data) noexcept; + + + private: + + std::optional AcquireLinearDataOnAcquiredBlock( + AlternatingControlBlockSelectId block_id_active_for_writing_value, + Length length) noexcept; + + + AlternatingControlBlock& alternating_control_block_; + WaitFreeLinearWriter wait_free_writing_even_; + WaitFreeLinearWriter wait_free_writing_odd_; + +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif diff --git a/mw/log/detail/wait_free_producer_queue/wait_free_alternating_writer_test.cpp b/mw/log/detail/wait_free_producer_queue/wait_free_alternating_writer_test.cpp new file mode 100644 index 0000000..dc38111 --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/wait_free_alternating_writer_test.cpp @@ -0,0 +1,206 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#include "platform/aas/mw/log/detail/wait_free_producer_queue/wait_free_alternating_writer.h" +#include "platform/aas/mw/log/detail/wait_free_producer_queue/alternating_reader.h" + +#include + +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +namespace +{ + +TEST(WaitFreeAlternatingWriterTests, EnsureAtomicRequirements) +{ + RecordProperty("Requirement", ",,"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "The used atomic data types shall be lock free"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + bmw::mw::log::detail::AlternatingControlBlock control_block{}; + ASSERT_TRUE(control_block.switch_count_points_active_for_writing.is_lock_free()); +} + +TEST(WaitFreeAlternatingWriterTests, WriteBufferFullShouldReturnExpectedData) +{ + RecordProperty("Requirement", ",,"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Producing and consuming of logging data shall work concurrently and without cross-thread locks."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + constexpr auto kBufferSize = 10u * 64u * 1024u; + std::vector buffer_even(kBufferSize); + std::vector buffer_odd(kBufferSize); + bmw::mw::log::detail::AlternatingControlBlock control_block{}; + control_block.control_block_even.data = + amp::span(buffer_even.data(), static_cast(buffer_even.size())); + control_block.control_block_odd.data = + amp::span(buffer_odd.data(), static_cast(buffer_odd.size())); + + bmw::mw::log::detail::WaitFreeAlternatingWriter writer{InitializeAlternatingControlBlock(control_block)}; + + const auto kNumberOfWriterThreads = std::thread::hardware_concurrency(); + std::vector threads(kNumberOfWriterThreads); + + const auto kAcquireLength = kBufferSize / kNumberOfWriterThreads; + constexpr auto kNumberOfPacketsPerThread = 3; + + for (auto i = 0u; i < kNumberOfWriterThreads; i++) + { + threads[i] = std::thread([kAcquireLength, &writer]() { + for (auto packet_number = 0; packet_number < kNumberOfPacketsPerThread; packet_number++) + { + // Loop until we succeeded to reserve data on the buffer + std::optional acquire_result{}; + + while (acquire_result.has_value() == false) + { + acquire_result = writer.Acquire(kAcquireLength); + std::this_thread::sleep_for(10us); + } + + auto acquired_data = acquire_result.value().data; + if (acquired_data.size() != kAcquireLength) + { + std::abort(); + } + + // Write data into the complete acquired span. + acquired_data.data()[0] = static_cast(packet_number); + + writer.Release(acquire_result.value()); + } + }); + } + + bmw::mw::log::detail::AlternatingReaderProxy reader_proxy{control_block}; + bmw::mw::log::detail::AlternatingReadOnlyReader read_only_reader{control_block, buffer_even, buffer_odd}; + + std::vector number_of_packets_received(kNumberOfPacketsPerThread); + + bool all_packets_received = false; + while (all_packets_received == false) + { + // The responsibility of the switch state monitoring lies in the hands of the ProxyReader: + const auto acquired = reader_proxy.Switch(); + while (not read_only_reader.IsBlockReleasedByWriters(acquired)) + { + std::this_thread::sleep_for(10ms); + } + // Terminating block is no longer terminating as all the writers released the buffers. + // Buffer is now ready to setup the receiver and read data. + auto linear_reader = read_only_reader.CreateLinearReader(acquired); + auto read_result = linear_reader.Read(); + while (read_result.has_value()) + { + // cast to a larger type. + const auto packet_id = static_cast(*read_result.value().begin()); + number_of_packets_received[packet_id]++; + + read_result = linear_reader.Read(); + } + + all_packets_received = true; + for (const auto& counter : number_of_packets_received) + { + if (counter != kNumberOfWriterThreads) + { + all_packets_received = false; + break; + } + } + + std::this_thread::sleep_for(500ms); + } + + for (auto& thread : threads) + { + thread.join(); + } +} + +TEST(AlternatingReaderTest, EnsureSafeSwitchingToReadDataBuffer) +{ + RecordProperty("Requirement", ",,"); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "Verify the ability to safely switch in case of un-equally index for written_index and acquired_index."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + bmw::mw::log::detail::AlternatingControlBlock control_block{}; + + // Set non-equal values for written_index and acquired_index. + control_block.control_block_odd.written_index.store(1); + control_block.control_block_odd.acquired_index.store(0); + control_block.control_block_even.written_index.store(1); + control_block.control_block_even.acquired_index.store(0); + + std::ignore = bmw::mw::log::detail::InitializeAlternatingControlBlock(control_block); + + bmw::mw::log::detail::AlternatingReaderProxy reader{control_block}; + + auto thread = std::thread([&reader]() noexcept { + // Because we assign defferent values for acquired_index and written_index, the method shoukd sleep for some + // time without causing the test case to stuck. + EXPECT_NO_FATAL_FAILURE(reader.Switch()); + }); + + // Switch method has a delay in its condition, So. Let's wait some time as in the method itself to make sure the + // condition satisfied. + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + // reset the value again to be equally. + control_block.control_block_odd.written_index.store(0); + control_block.control_block_even.written_index.store(0); + + thread.join(); +} + +TEST(AlternatingReaderTest, EnsureSwitchingIncrementsInternalCounter) +{ + RecordProperty("Requirement", ",,"); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "Verify the ability to safely switch in case of un-equally index for written_index and acquired_index."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + bmw::mw::log::detail::AlternatingControlBlock control_block{}; + std::ignore = bmw::mw::log::detail::InitializeAlternatingControlBlock(control_block); + + bmw::mw::log::detail::AlternatingReaderProxy reader{control_block}; + bmw::mw::log::detail::WaitFreeAlternatingWriter writer{control_block}; + + EXPECT_EQ(control_block.switch_count_points_active_for_writing, 1UL); + EXPECT_NO_FATAL_FAILURE(reader.Switch()); + EXPECT_EQ(control_block.switch_count_points_active_for_writing, 2UL); + EXPECT_NO_FATAL_FAILURE(reader.Switch()); + EXPECT_EQ(control_block.switch_count_points_active_for_writing, 3UL); +} + +} // namespace diff --git a/mw/log/detail/wait_free_producer_queue/wait_free_linear_writer.cpp b/mw/log/detail/wait_free_producer_queue/wait_free_linear_writer.cpp new file mode 100644 index 0000000..10b4013 --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/wait_free_linear_writer.cpp @@ -0,0 +1,175 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#include "platform/aas/mw/log/detail/wait_free_producer_queue/wait_free_linear_writer.h" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +namespace +{ + +/// \brief We already incremented the atomic counter, but noted afterwards that +/// our payload does not fit anymore. In this case we attempt to write at least +/// the length to signal the reader that this was a failed acquisition. If even +/// the length does not fit anymore it is obvious to the reader that it was a +/// failed acquisition. +void TerminateBuffer(LinearControlBlock& control_block, const Length offset, const Length length) noexcept +{ + // Check if at least length fits in the remaining space. + if (DoBytesFitInRemainingCapacity(control_block.data, offset, GetLengthOffsetBytes())) + { + const auto length_span = control_block.data.subspan(static_cast(offset), + static_cast(GetLengthOffsetBytes())); + + // clang-format off + // we need to convert void pointer to bytes for serialization purposes, no out of bounds there + // + const auto src_span = amp::span{static_cast(static_cast(&length)), sizeof(length)}; + // clang-format on + + std::ignore = std::copy_n(src_span.begin(), GetLengthOffsetBytes(), length_span.begin()); + } + + // We must increment the written_index even for failed acquisition cases to ensure the condition + // written_index == acquired_index that allows the reader to determine if all writers have finished + // accessing the buffer. + // The reader shall be able to detect failed acquisition by bounds checking of the buffer size. + + // The summation will not exceed uint64 because: + // - GetLengthOffsetBytes() is returning sizeof(uint64) which equal to 8. + // - length is already validated in CheckAndGetAcquireOffset by ensuring it does not exceed GetMaxAcquireLengthBytes + // which within uint64. + // + amp::ignore = control_block.written_index.fetch_add(length + GetLengthOffsetBytes()); +} + +amp::optional CheckAndGetAcquireOffset(LinearControlBlock& control_block, + const Length length, + const Length writer_concurrency, + PreAcquireHook& pre_acquire_hook, + WaitFreeLinearWriter& writer) noexcept +{ + if (writer_concurrency > GetMaxNumberOfConcurrentWriters()) + { + // Too many writers. + return {}; + } + + if (length > GetMaxAcquireLengthBytes()) + { + // Not safe to increase + return {}; + } + + const auto total_acquired_length = length + GetLengthOffsetBytes(); + + // Check if it makes sense to increment the atomic counter, or if we are already full. + const auto old_offset = control_block.acquired_index.load(); + + // Avoid that the acquired_index could overflow. + if (old_offset >= GetMaxLinearBufferCapacityBytes()) + { + // Not safe to increase + return {}; + } + + if (DoBytesFitInRemainingCapacity(control_block.data, old_offset, total_acquired_length) == false) + { + // Already not enough space left. + return {}; + } + + pre_acquire_hook(writer); + + // We probably have enough space, attempt to acquire space on the buffer. + const auto offset = control_block.acquired_index.fetch_add(total_acquired_length); + + if (DoBytesFitInRemainingCapacity(control_block.data, offset, total_acquired_length) == false) + { + // Someone was faster, buffer is already full meanwhile. + TerminateBuffer(control_block, offset, length); + return {}; + } + + return offset; +} + +} // namespace + +WaitFreeLinearWriter::WaitFreeLinearWriter(LinearControlBlock& cb, PreAcquireHook pre_acquire_hook) noexcept + : control_block_(cb), pre_acquire_hook_{std::move(pre_acquire_hook)} +{ +} + +amp::optional WaitFreeLinearWriter::Acquire(const Length length) noexcept +{ + ++control_block_.number_of_writers; + const auto writer_concurrency = control_block_.number_of_writers.load(); + + const auto offset_result = + CheckAndGetAcquireOffset(control_block_, length, writer_concurrency, pre_acquire_hook_, *this); + + if (offset_result.has_value() == false) + { + control_block_.number_of_writers--; + return {}; + } + + const Length offset = offset_result.value(); + + // Copy length to the beginning of the acquired range. + const auto length_span = + control_block_.data.subspan(static_cast(offset), static_cast(GetLengthOffsetBytes())); + + // clang-format off + // we need to convert void pointer to bytes for serialization purposes, no out of bounds there + // + const auto src_span = amp::span{static_cast(static_cast(&length)), sizeof(length)}; + // clang-format on + + std::ignore = std::copy_n(src_span.begin(), GetLengthOffsetBytes(), length_span.begin()); + + // static_casts are safe by bounds checking in CheckAndGetAcquireOffset(). + const auto payload_offset = offset + GetLengthOffsetBytes(); + const auto payload_span = + control_block_.data.subspan(static_cast(payload_offset), static_cast(length)); + return AcquiredData{payload_span}; +} + +void WaitFreeLinearWriter::Release(const AcquiredData& acquired_data) noexcept +{ + // Fence is needed to ensure non atomic data is seen as written + // before the index is updated. + std::atomic_thread_fence(std::memory_order_release); + + amp::ignore = + control_block_.written_index.fetch_add(static_cast(acquired_data.data.size()) + GetLengthOffsetBytes()); + + control_block_.number_of_writers--; +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/detail/wait_free_producer_queue/wait_free_linear_writer.h b/mw/log/detail/wait_free_producer_queue/wait_free_linear_writer.h new file mode 100644 index 0000000..e0046e4 --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/wait_free_linear_writer.h @@ -0,0 +1,83 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_WAIT_FREE_LINEAR_WRITER_H +#define PLATFORM_AAS_MW_LOG_DETAIL_WAIT_FREE_LINEAR_WRITER_H + +#include "platform/aas/mw/log/detail/wait_free_producer_queue/linear_control_block.h" + +#include "amp_callback.hpp" +#include "amp_optional.hpp" +#include "amp_span.hpp" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +struct AcquiredData +{ + // Maintaining compatibility and avoiding performance overhead outweighs POD Type (class) based design. The Struct + // is ONLY used internally under the namespace detail and ONLY for data_router sub-dir, it is NOT exposed publicly; + // this is additionally guaranteed by the build system(bazel) visibility. Moreover, the Type is simple and does not + // require invariance (interface OR custom behavior) as per the design. + // + amp::span data; +}; + +class WaitFreeLinearWriter; +using PreAcquireHook = amp::callback; + +/// \brief Wait-free writing to a linear buffer. +/// Thread-safe for multiple writers. +/// No overwriting of data. +/// First in first out. +/// PreAcquireHook is used for testing certain call sequences, default constructed as no-op in production. +class WaitFreeLinearWriter +{ + public: + explicit WaitFreeLinearWriter(LinearControlBlock& cb, + PreAcquireHook pre_acquire_hook = std::move(empty_hook)) noexcept; + + /// \brief Try to acquire the length for writing. + /// Returns empty if there is not enough space available. + amp::optional Acquire(const Length length) noexcept; + + /// \brief Release the acquired data. + void Release(const AcquiredData& acquired_data) noexcept; + + private: + static inline void empty_hook(WaitFreeLinearWriter&) noexcept {} + + private: + + + LinearControlBlock& control_block_; + PreAcquireHook pre_acquire_hook_; + +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif diff --git a/mw/log/detail/wait_free_producer_queue/wait_free_linear_writer_test.cpp b/mw/log/detail/wait_free_producer_queue/wait_free_linear_writer_test.cpp new file mode 100644 index 0000000..c55e8ba --- /dev/null +++ b/mw/log/detail/wait_free_producer_queue/wait_free_linear_writer_test.cpp @@ -0,0 +1,365 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + + +#include "platform/aas/mw/log/detail/wait_free_producer_queue/wait_free_linear_writer.h" +#include "platform/aas/mw/log/detail/wait_free_producer_queue/linear_reader.h" + +#include + +#include +#include +#include +#include + +namespace +{ + +TEST(WaitFreeLinearWriter, EnsureAtomicRequirements) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "The used atomic data types shall be lock free"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + bmw::mw::log::detail::LinearControlBlock control_block{}; + ASSERT_TRUE(control_block.acquired_index.is_lock_free()); + ASSERT_TRUE(control_block.number_of_writers.is_lock_free()); + ASSERT_TRUE(control_block.written_index.is_lock_free()); +} + +TEST(WaitFreeLinearWriter, WriteBufferFullShouldReturnExpectedData) +{ + RecordProperty("Requirement", ", , , "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Returning the expected data if the write buffer is full."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + constexpr auto kBufferSize = 10u * 64u * 1024u; + std::vector buffer(kBufferSize); + bmw::mw::log::detail::LinearControlBlock control_block{}; + control_block.data = amp::span(buffer.data(), static_cast(buffer.size())); + + bmw::mw::log::detail::WaitFreeLinearWriter writer{control_block}; + + const auto kNumberOfWriterThreads = std::thread::hardware_concurrency(); + std::vector threads(kNumberOfWriterThreads); + + const auto kThreadAcquireFactor = kBufferSize / kNumberOfWriterThreads; + + for (auto i = 0u; i < kNumberOfWriterThreads; i++) + { + threads[i] = std::thread([i, kThreadAcquireFactor, &writer]() { + const auto kAcquireLength = kThreadAcquireFactor * i; + + const auto acquire_result = writer.Acquire(kAcquireLength); + + if (acquire_result.has_value() == false) + { + return; + } + + // Write data into the complete acquired span. + const auto acquired_data = acquire_result.value().data; + + if (acquired_data.size() != kAcquireLength) + { + std::abort(); + } + + for (auto payload_index = 0u; payload_index < acquired_data.size(); payload_index++) + { + acquired_data.data()[payload_index] = static_cast(payload_index); + } + + writer.Release(acquire_result.value()); + }); + } + + for (auto& thread : threads) + { + thread.join(); + } + + auto reader = bmw::mw::log::detail::CreateLinearReaderFromControlBlock(control_block); + + while (true) + { + const auto read_result = reader.Read(); + if (read_result.has_value() == false) + { + break; + } + + for (auto payload_index = 0u; payload_index < read_result.value().size(); payload_index++) + { + ASSERT_EQ(read_result.value().data()[payload_index], + static_cast(payload_index)); + } + } +} + +TEST(WaitFreeLinearWriter, WriterBigDataTest) +{ + RecordProperty("Requirement", ", , "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of writing big data."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + constexpr auto kBufferSize = 10u * 10u * 64u * 1024u; + std::vector buffer(kBufferSize); + bmw::mw::log::detail::LinearControlBlock control_block{}; + control_block.data = amp::span(buffer.data(), static_cast(buffer.size())); + + bmw::mw::log::detail::WaitFreeLinearWriter writer{control_block}; + + const auto kNumberOfWriterThreads = std::thread::hardware_concurrency(); + std::vector threads(kNumberOfWriterThreads); + + const auto kThreadAcquireFactor = kBufferSize / kNumberOfWriterThreads; + + for (auto i = 0u; i < kNumberOfWriterThreads; i++) + { + threads[i] = std::thread([i, kThreadAcquireFactor, &writer]() { + const auto kAcquireLength = kThreadAcquireFactor * i; + + const auto acquire_result = writer.Acquire(kAcquireLength); + + if (acquire_result.has_value() == false) + { + return; + } + + const auto acquired_data = acquire_result.value().data; + + if (acquired_data.size() != kAcquireLength) + { + std::abort(); + } + + // Only write at the beginning and the end as writing everywhere would be too slow. + if (acquired_data.size() >= 2) + { + *acquired_data.begin() = 1; + acquired_data.data()[acquired_data.size() - 1] = 2; + } + + writer.Release(acquire_result.value()); + }); + } + + for (auto& thread : threads) + { + thread.join(); + } + + auto reader = bmw::mw::log::detail::CreateLinearReaderFromControlBlock(control_block); + + while (true) + { + const auto read_result = reader.Read(); + if (read_result.has_value() == false) + { + break; + } + + // We don't check data here as this would be super slow under memcheck configuration. + if (read_result.value().size() >= 2) + { + ASSERT_EQ(read_result.value().data()[0], 1); + ASSERT_EQ(read_result.value().data()[read_result.value().size() - 1], 2); + } + } +} + +TEST(WaitFreeLinearWriter, TooManyConcurrentWriterShouldReturnEmpty) +{ + RecordProperty("Requirement", ", , "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Shall return empty in case of too many concurrent writers."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + constexpr auto kBufferSize = 10u * 10u * 64u * 1024u; + std::vector buffer(kBufferSize); + bmw::mw::log::detail::LinearControlBlock control_block{}; + control_block.data = amp::span(buffer.data(), static_cast(buffer.size())); + control_block.number_of_writers.store(bmw::mw::log::detail::GetMaxNumberOfConcurrentWriters()); + + bmw::mw::log::detail::WaitFreeLinearWriter writer{control_block}; + constexpr auto kArbitraryNumberOfBytes{42u}; + ASSERT_FALSE(writer.Acquire(kArbitraryNumberOfBytes).has_value()); +} + +TEST(WaitFreeLinearWriter, BufferSizeExceededShouldReturnEmpty) +{ + RecordProperty("Requirement", ", , "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Shall return empty if buffer size exceeded."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + constexpr auto kBufferSize = 10u * 10u * 64u * 1024u; + std::vector buffer(kBufferSize); + bmw::mw::log::detail::LinearControlBlock control_block{}; + control_block.data = amp::span(buffer.data(), static_cast(buffer.size())); + control_block.acquired_index.store(static_cast(control_block.data.size())); + + bmw::mw::log::detail::WaitFreeLinearWriter writer{control_block}; + constexpr auto kArbitraryNumberOfBytes{42u}; + ASSERT_FALSE(writer.Acquire(kArbitraryNumberOfBytes).has_value()); +} + +TEST(WaitFreeLinearWriter, BufferSizeExceededUpperLimitShouldReturnEmpty) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Acquire shall fail if buffer size exceeded upper limit."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + constexpr auto kBufferSize = 10u * 10u * 64u * 1024u; + std::vector buffer(kBufferSize); + bmw::mw::log::detail::LinearControlBlock control_block{}; + control_block.data = amp::span(buffer.data(), static_cast(buffer.size())); + control_block.acquired_index.store(bmw::mw::log::detail::GetMaxLinearBufferCapacityBytes()); + + bmw::mw::log::detail::WaitFreeLinearWriter writer{control_block}; + constexpr auto kArbitraryNumberOfBytes{42u}; + ASSERT_FALSE(writer.Acquire(kArbitraryNumberOfBytes).has_value()); +} + +TEST(WaitFreeLinearWriter, FailedAcquireShouldTerminateBuffer) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "If the linear buffer is full, acquire shall fail. In case of failed acquisition, the writer shall " + "at least write the length if sufficient space is available."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + constexpr auto kBufferSize = bmw::mw::log::detail::GetLengthOffsetBytes() * 2u; + std::vector buffer(kBufferSize); + bmw::mw::log::detail::LinearControlBlock control_block{}; + control_block.data = amp::span(buffer.data(), static_cast(buffer.size())); + + bmw::mw::log::detail::AcquiredData acquired_data{}; + + bmw::mw::log::detail::WaitFreeLinearWriter writer( + control_block, [i = 0, &acquired_data](bmw::mw::log::detail::WaitFreeLinearWriter& writer_callback) mutable { + // Simulate the case where another writer concurrently steals the capacity just before the current + // thread tries to reserve it. + if (i++ == 0) + { + auto acquire_result = writer_callback.Acquire(0u); + if (acquire_result.has_value() == false) + { + std::abort(); + } + acquired_data = acquire_result.value(); + } + }); + + ASSERT_FALSE(writer.Acquire(bmw::mw::log::detail::GetLengthOffsetBytes()).has_value()); + writer.Release(acquired_data); + + // acquired_index should be equal to the length of the first acquire plus the second acquire including overhead for + // length. + const auto expected_acquired_index = + bmw::mw::log::detail::GetLengthOffsetBytes() + 2 * bmw::mw::log::detail::GetLengthOffsetBytes(); + ASSERT_EQ(control_block.acquired_index, expected_acquired_index); + ASSERT_EQ(control_block.written_index, control_block.acquired_index); + + auto reader = bmw::mw::log::detail::CreateLinearReaderFromControlBlock(control_block); + + auto read_result = reader.Read(); + ASSERT_TRUE(read_result.has_value()); + ASSERT_EQ(read_result.value().size(), 0); + ASSERT_FALSE(reader.Read().has_value()); +} + +TEST(WaitFreeLinearWriter, FailedAcquireWithNoFreeSpaceShouldNotTerminateBuffer) +{ + RecordProperty("Requirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "If the linear buffer is full, acquire shall fail. In a failed acquisition case, the writer should " + "not write the length if there is no sufficient space left."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + constexpr auto kBufferSize = bmw::mw::log::detail::GetLengthOffsetBytes() * 2u; + std::vector buffer(kBufferSize); + bmw::mw::log::detail::LinearControlBlock control_block{}; + control_block.data = amp::span(buffer.data(), static_cast(buffer.size())); + + bmw::mw::log::detail::AcquiredData acquired_data{}; + + bmw::mw::log::detail::WaitFreeLinearWriter writer( + control_block, [i = 0, &acquired_data](bmw::mw::log::detail::WaitFreeLinearWriter& writer_callback) mutable { + // Simulate the case where another writer concurrently steals the capacity just before the current + // thread tries to reserve it. + if (i++ == 0) + { + auto acquire_result = writer_callback.Acquire(bmw::mw::log::detail::GetLengthOffsetBytes()); + if (acquire_result.has_value() == false) + { + std::abort(); + } + acquired_data = acquire_result.value(); + } + }); + + ASSERT_FALSE(writer.Acquire(bmw::mw::log::detail::GetLengthOffsetBytes()).has_value()); + writer.Release(acquired_data); + + // acquired_index should be equal to the length of the first acquire plus the second acquire including overhead for + // length. + const auto expected_acquired_index = + 2 * bmw::mw::log::detail::GetLengthOffsetBytes() + 2 * bmw::mw::log::detail::GetLengthOffsetBytes(); + ASSERT_EQ(control_block.acquired_index, expected_acquired_index); + ASSERT_EQ(control_block.written_index, control_block.acquired_index); + + auto reader = bmw::mw::log::detail::CreateLinearReaderFromControlBlock(control_block); + + auto read_result = reader.Read(); + ASSERT_TRUE(read_result.has_value()); + ASSERT_EQ(read_result.value().size(), bmw::mw::log::detail::GetLengthOffsetBytes()); + ASSERT_FALSE(reader.Read().has_value()); +} + +TEST(WaitFreeLinearWriter, AcquireMoreThanMaximumShouldFail) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Acquiring more than the supported threshold shall fail."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + constexpr auto kBufferSize = bmw::mw::log::detail::GetLengthOffsetBytes() * 2u; + std::vector buffer(kBufferSize); + bmw::mw::log::detail::LinearControlBlock control_block{}; + control_block.data = amp::span(buffer.data(), static_cast(buffer.size())); + + bmw::mw::log::detail::WaitFreeLinearWriter writer(control_block); + + ASSERT_FALSE(writer.Acquire(bmw::mw::log::detail::GetMaxAcquireLengthBytes() + 1).has_value()); +} + +} // namespace diff --git a/mw/log/detail/wait_free_stack/BUILD b/mw/log/detail/wait_free_stack/BUILD new file mode 100644 index 0000000..3a56473 --- /dev/null +++ b/mw/log/detail/wait_free_stack/BUILD @@ -0,0 +1,68 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("//platform/aas/mw:common_features.bzl", "COMPILER_WARNING_FEATURES") +load("//platform/aas/tools/itf/bazel_gen:itf_gen.bzl", "py_unittest_qnx_test") + +cc_library( + name = "wait_free_stack", + hdrs = [ + "wait_free_stack.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/ara/log:__subpackages__", + "//platform/aas/mw/log:__subpackages__", + ], + deps = ["@amp"], +) + +cc_test( + name = "unit_test", + srcs = ["wait_free_stack_test.cpp"], + features = COMPILER_WARNING_FEATURES + [ + "aborts_upon_exception", + ], + linkopts = select({ + "@platforms//os:linux": ["-latomic"], # needed for spp-host-clang + "//conditions:default": [], + }), + tags = ["unit"], + deps = [ + ":wait_free_stack", + "//third_party/googletest:main", + "//third_party/libatomic", + ], +) + +test_suite( + name = "unit_tests", + tests = ["unit_test"], + visibility = ["//platform/aas/mw/log/detail:__pkg__"], +) + +py_unittest_qnx_test( + name = "pkg_unit_tests_qnx", + test_cases = [ + ":unit_test", + ], +) + +py_unittest_qnx_test( + name = "unit_tests_qnx", + tags = ["manual"], + test_suites = [ + ":pkg_unit_tests_qnx", + ], + visibility = ["//platform/aas/mw/log/detail:__pkg__"], +) diff --git a/mw/log/detail/wait_free_stack/wait_free_stack.h b/mw/log/detail/wait_free_stack/wait_free_stack.h new file mode 100644 index 0000000..30e4114 --- /dev/null +++ b/mw/log/detail/wait_free_stack/wait_free_stack.h @@ -0,0 +1,124 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_DETAIL_WAIT_FREE_STACK_H +#define PLATFORM_AAS_MW_LOG_DETAIL_WAIT_FREE_STACK_H + +#include "amp_callback.hpp" +#include "amp_optional.hpp" +#include "amp_span.hpp" + +#include +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + +template +using FindPredicate = amp::callback; + +/// \brief Wait- and lock-free, push-only stack with fixed capacity. +template +class WaitFreeStack +{ + public: + explicit WaitFreeStack(const size_t max_number_of_elements) noexcept; + + /// \brief Inserts an element if capacity is left. + /// Returns a reference to the element in the stack if push was successful. + amp::optional> TryPush(Element&& element) noexcept; + + /// \brief returns the first matching element + /// Returns a reference to the element in the stack if element was found. + amp::optional> Find(const FindPredicate&) noexcept; + + using AtomicIndex = std::atomic_size_t; + using AtomicBool = std::atomic_bool; + + + private: + + + std::vector> elements_; + std::vector elements_written_; + AtomicIndex write_index_; + AtomicBool capacity_full_; + +}; + +template +WaitFreeStack::WaitFreeStack(const size_t max_number_of_elements) noexcept + : elements_(max_number_of_elements), + elements_written_(max_number_of_elements), + write_index_{}, + capacity_full_{false} +{ +} + +template +auto WaitFreeStack::TryPush(Element&& element) noexcept -> amp::optional> +{ + if (capacity_full_.load()) + { + return amp::nullopt; + } + + const auto current_write_index = write_index_.fetch_add(1); + + if (current_write_index >= elements_.size()) + { + capacity_full_.store(true); + return amp::nullopt; + } + + elements_[current_write_index] = std::forward(element); + + std::atomic_thread_fence(std::memory_order_release); + elements_written_[current_write_index].store(1); + + return elements_[current_write_index].value(); +} + +template +auto WaitFreeStack::Find(const FindPredicate& predicate) noexcept + -> amp::optional> +{ + for (auto i = 0UL; (i < elements_.size()) && (i <= write_index_.load()); i++) + { + if (elements_written_[i].load() != 0) + { + std::atomic_thread_fence(std::memory_order_acquire); + if (predicate(elements_[i].value()) == true) + { + return elements_[i].value(); + } + } + } + + return amp::nullopt; +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif diff --git a/mw/log/detail/wait_free_stack/wait_free_stack_test.cpp b/mw/log/detail/wait_free_stack/wait_free_stack_test.cpp new file mode 100644 index 0000000..81fd371 --- /dev/null +++ b/mw/log/detail/wait_free_stack/wait_free_stack_test.cpp @@ -0,0 +1,140 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/wait_free_stack/wait_free_stack.h" + +#include + +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +TEST(WaitFreeStack, AtomicShallBeLockFree) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Check atomic lock-free."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + ASSERT_TRUE(WaitFreeStack::AtomicIndex{}.is_lock_free()); + ASSERT_TRUE(WaitFreeStack::AtomicBool{}.is_lock_free()); +} + +TEST(WaitFreeStack, ConcurrentPushingAndReadingShouldReturnExpectedElements) +{ + RecordProperty("Requirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Ensures that WaitFreeStack shall be capable of performing multiple " + "concurrent write operations without endless loops and return the correct data."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + constexpr auto stack_size = 10UL; + WaitFreeStack stack{stack_size}; + + // Start writer threads + constexpr auto number_of_push_threads = 32UL; + std::vector push_threads; + for (auto i = 0UL; i < number_of_push_threads; ++i) + { + push_threads.emplace_back(std::thread([&stack, i]() { + const auto result = stack.TryPush(std::to_string(i)); + if (result.has_value() == false) + { + return; + } + + // Expect we get the same value as pushed. + if (result.value().get() != std::to_string(i)) + { + std::abort(); + } + + // Expect we find the same value as pushed. + if (stack.Find([i](const auto& item) { return item == std::to_string(i); }).value().get() != + std::to_string(i)) + { + std::abort(); + } + })); + } + + // Start reader threads + constexpr auto number_of_read_threads = 16UL; + std::vector> found_strings(number_of_read_threads); + std::vector read_threads; + for (auto thread_index = 0UL; thread_index < number_of_read_threads; thread_index++) + { + read_threads.emplace_back(std::thread([&stack, &found_strings, thread_index]() { + auto& thread_result = found_strings[thread_index]; + while (thread_result.size() < stack_size) + { + for (auto i = 0UL; i < number_of_push_threads; ++i) + { + if (std::find(thread_result.begin(), thread_result.end(), std::to_string(i)) != thread_result.end()) + { + continue; + } + + const auto result = stack.Find([i](const std::string& item) { return item == std::to_string(i); }); + if (result.has_value()) + { + thread_result.emplace_back(result.value()); + } + } + } + })); + } + + // Wait for all threads to terminate. + for (auto& thread : push_threads) + { + thread.join(); + } + for (auto& thread : read_threads) + { + thread.join(); + } + + // Check that all threads found the same elements. + for (auto thread_index = 0UL; thread_index < number_of_read_threads; thread_index++) + { + std::sort(found_strings[thread_index].begin(), found_strings[thread_index].end()); + } + for (auto i = 0UL; i < stack_size; ++i) + { + for (auto thread_index = 0UL; thread_index < number_of_read_threads; thread_index++) + { + ASSERT_EQ(found_strings[0UL][i], found_strings[thread_index][i]); + } + } +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/irecorder_factory.cpp b/mw/log/irecorder_factory.cpp new file mode 100644 index 0000000..9cb2f84 --- /dev/null +++ b/mw/log/irecorder_factory.cpp @@ -0,0 +1,15 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/irecorder_factory.h" diff --git a/mw/log/irecorder_factory.h b/mw/log/irecorder_factory.h new file mode 100644 index 0000000..cebac8c --- /dev/null +++ b/mw/log/irecorder_factory.h @@ -0,0 +1,71 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_IRECORDER_FACTORY_H +#define PLATFORM_AAS_MW_LOG_IRECORDER_FACTORY_H + +#include "platform/aas/mw/log/recorder.h" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ + + +/* False positive 'noexcept' is a keyword and not an identifier which could be hidden */ + +class IRecorderFactory +{ + public: + /// \brief Instantiates the recorder(s) according to the configuration files provides by the user + /// \details Depending on the configuration provided by the user it will instantiate a recorder for DLT, Console, + /// and/or File logging. + /// If no configuration can be found, it will fall back to CreateWithConsoleLoggingOnly(). + virtual std::unique_ptr CreateFromConfiguration( + amp::pmr::memory_resource* memory_resource) const noexcept = 0; + + /// \brief Instantiate a recorder that provides basic console logging. + /// \details This is for users that do not need or want to provide a logging configuration file. A typical use case + /// might be for example for unit or component testing. + virtual std::unique_ptr CreateWithConsoleLoggingOnly( + amp::pmr::memory_resource* memory_resource) const noexcept = 0; + + /// \brief Instantiates a stub recorder that drops all the logs. + /// \details For users that want to completely turn of logging for whatever reason. + virtual std::unique_ptr CreateStub() const noexcept = 0; + + IRecorderFactory() noexcept = default; + IRecorderFactory(IRecorderFactory&&) noexcept = delete; + IRecorderFactory(const IRecorderFactory&) noexcept = delete; + IRecorderFactory& operator=(IRecorderFactory&&) noexcept = delete; + IRecorderFactory& operator=(const IRecorderFactory&) noexcept = delete; + + virtual ~IRecorderFactory() = default; +}; + + + +namespace detail +{ +std::unique_ptr CreateRecorderFactory() noexcept; +} +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_IRECORDER_FACTORY_H diff --git a/mw/log/legacy_non_verbose_api/BUILD b/mw/log/legacy_non_verbose_api/BUILD new file mode 100644 index 0000000..a8c6233 --- /dev/null +++ b/mw/log/legacy_non_verbose_api/BUILD @@ -0,0 +1,86 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//platform/aas/mw:common_features.bzl", "COMPILER_WARNING_FEATURES") +load("//platform/aas/tools/itf/bazel_gen:itf_gen.bzl", "py_unittest_qnx_test") + +cc_library( + name = "legacy_non_verbose_api", + srcs = ["tracing.cpp"], + hdrs = ["tracing.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//platform/aas/mw/log:__subpackages__", + "//platform/aas/pas/logging:__pkg__", + ], + deps = [ + "//platform/aas/mw/log:frontend", + "//platform/aas/mw/log/configuration", + "//platform/aas/mw/log/configuration:nvconfig", + "//platform/aas/mw/log/detail/data_router/shared_memory:writer", + "//platform/aas/pas/logging:libtracing_legacy_public_headers", + "//third_party/static_reflection_with_serialization_pkg:serializer", + "@amp", + ], +) + +filegroup( + name = "unit_test_data", + testonly = True, + srcs = [ + "test/error-content-json-class-id.json", + "test/test-class-id.json", + ], +) + +cc_test( + name = "unit_test", + srcs = ["tracing_test.cpp"], + data = [":unit_test_data"], + features = COMPILER_WARNING_FEATURES + [ + "aborts_upon_exception", + ], + tags = ["unit"], + deps = [ + "//platform/aas/lib/os/mocklib:pthread_mock", + "//platform/aas/lib/os/mocklib:stat_mock", + "//platform/aas/lib/os/mocklib:stdlib_mock", + "//platform/aas/lib/os/mocklib:unistd_mock", + "//platform/aas/lib/os/utils/mocklib:signal_mock", + "//platform/aas/mw/com/message_passing:mock", + "//platform/aas/mw/log", + "//platform/aas/mw/log/detail/data_router/shared_memory:reader", + "//platform/aas/mw/log/test/console_logging_environment", + "//platform/aas/pas/logging:filetransfer_message_types", + "//third_party/googletest:main", + "@amp//:amp_test_support", + ], +) + +test_suite( + name = "unit_tests", + tests = [":unit_test"], + visibility = ["//platform/aas/mw/log:__pkg__"], +) + +py_unittest_qnx_test( + name = "unit_tests_qnx", + data_files = [ + ":unit_test_data", + ], + test_cases = [ + ":unit_test", + ], + visibility = ["//platform/aas/mw/log:__pkg__"], +) diff --git a/mw/log/legacy_non_verbose_api/test/error-content-json-class-id.json b/mw/log/legacy_non_verbose_api/test/error-content-json-class-id.json new file mode 100644 index 0000000..ea8af16 --- /dev/null +++ b/mw/log/legacy_non_verbose_api/test/error-content-json-class-id.json @@ -0,0 +1,72 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "adp::perception::CrocStateTraceable": { + "id": 8650814, + "ctxid": "CRoC", + "appid": "Plan", + "loglevel": 3 + }, + "adp::perception::obstacles::ObstaclesTraceable": { + "id": 8650815, + "ctxid": "OBSF", + "appid": "Plan", + "loglevel": 3 + }, + "adp::planning::awa::DebugData": { + "id": 8650816, + "ctxid": "AWA", + "appid": "Plan", + "loglevel": 2 + }, + "adp::planning::driving_tube::DiagnosticLogsData": { + "id": 8650817, + "ctxid": "DTNV", + "appid": "Plan", + "loglevel": 4 + }, + "bmw::logging::timesync::DltTimeSyncTimestamp": { + "id": 27, + "ctxid": "TSNC", + "appid": "STDF", + "loglevel": 2 + }, + "aas::logging::ReprocessingEvent": { + "id": 8650752, + "ctxid": "Repr", + "appid": "Plan", + "loglevel": 4 + }, + "adp::time::StdTimestamp": { + "id": 8650753, + "appid": "DR", + "loglevel": 4 + }, + "adp::logging::DynamicInsight": { + "id": 8650754, + "ctxid": "VIS", + "appid": "Plan", + "loglevel": 3 + }, + "bmw::sli::CommonStrategiesConfig": { + "id": 8458555, + "ctxid": "sli", + "appid": "Fasi", + "loglevel": 4 + }, + "bmw::sli::TsfBaseConfig": { + "id": 8458423, + "ctxid": "sli", + "appid": "Fasi", + "loglevel": 4 + } +} diff --git a/mw/log/legacy_non_verbose_api/test/test-class-id.json b/mw/log/legacy_non_verbose_api/test/test-class-id.json new file mode 100644 index 0000000..2952a28 --- /dev/null +++ b/mw/log/legacy_non_verbose_api/test/test-class-id.json @@ -0,0 +1,42 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "bmw::logging::PersistentLogFileEvent": { + "id": 301, + "ctxid": "PERL", + "appid": "DRC", + "loglevel": 1 + }, + "aas::logging::ReprocessingCycle": { + "id": 150, + "ctxid": "Repr", + "appid": "Plan", + "loglevel": 4 + }, + "poseng::logging::ReprocessingCycle": { + "id": 41000, + "ctxid": "Cycl", + "appid": "PE" + }, + "LogMark::stdframe": { + "id": 10, + "ctxid": "STDA", + "appid": "STDF", + "loglevel": 2 + }, + "bmw::mw::log::detail::LogEntry": { + "id": 10, + "ctxid": "STDA", + "appid": "STDF", + "loglevel": 2 + } +} diff --git a/mw/log/legacy_non_verbose_api/tracing.cpp b/mw/log/legacy_non_verbose_api/tracing.cpp new file mode 100644 index 0000000..4e5e7c9 --- /dev/null +++ b/mw/log/legacy_non_verbose_api/tracing.cpp @@ -0,0 +1,159 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/legacy_non_verbose_api/tracing.h" + +#include "platform/aas/lib/os/unistd.h" + +#include +#include + +namespace +{ + +template +// '<<' is not a left shift operator but an overload for logging the respective types. +// code analysis tools tend to assume otherwise hence a false positive in this usecase +// +auto operator<<(std::basic_ostream& os, amp::string_view sv) -> std::basic_ostream& +{ + os.write(sv.data(), static_cast(sv.size())); + return os; +} + +} // namespace + +namespace bmw +{ +namespace platform +{ + +logger::logger(const amp::optional& config, + const amp::optional& nv_config, + amp::optional&& writer) noexcept + + + : config_(config.has_value() ? config.value() : bmw::mw::log::detail::Configuration()), + nvconfig_(nv_config.has_value() ? nv_config.value() : bmw::mw::log::NvConfig()), + + + discard_operation_fallback_shm_data_{}, + discard_operation_fallback_shm_writer_{InitializeSharedData(discard_operation_fallback_shm_data_), + []() noexcept {}}, + appPrefix{} +{ + + if (writer.has_value()) + + { + shared_memory_writer_ = std::move(writer.value()); + } + constexpr auto idsize = bmw::mw::log::detail::LoggingIdentifier::kMaxLength; + + std::fill(appPrefix.begin(), appPrefix.end(), 0); + auto appPrefixIter = appPrefix.begin(); + static_assert(idsize < std::numeric_limits::max(), "Unsupported length!"); + std::advance(appPrefixIter, static_cast(idsize)); + appPrefixIter = std::copy(config_.GetEcuId().begin(), config_.GetEcuId().end(), appPrefixIter); + std::ignore = std::copy(config_.GetAppId().begin(), config_.GetAppId().end(), appPrefixIter); + + const auto readResult = nvconfig_.parseFromJson(); + if (readResult != bmw::mw::log::NvConfig::ReadResult::kOK) + { + std::cerr << "could not read message ID table for non-verbose DLT!"; + if (readResult == bmw::mw::log::NvConfig::ReadResult::kERROR_PARSE) + { + std::cerr << "cannot parse config\n"; + } + else + { + std::cerr << "incompatible content\n"; + } + } +} + +std::optional logger::GetLevelForContext(const std::string& name) const noexcept +{ + const bmw::mw::log::config::NvMsgDescriptor* const msg_desc = nvconfig_.getDltMsgDesc(name); + if (msg_desc != nullptr) + { + const auto ctxId = msg_desc->ctxid_; + const auto context_log_level_map = config_.GetContextLogLevel(); + const auto context = context_log_level_map.find(ctxId); + if (context != context_log_level_map.end()) + { + return static_cast(context->second); + } + } + return std::nullopt; +} + + +logger& logger::instance(const amp::optional& config, + const amp::optional& nv_config, + amp::optional writer) noexcept + +{ + if (*GetInjectedTestInstance() != nullptr) + { + return **GetInjectedTestInstance(); + } + + // It's a singleton by design hence cannot be made const + // + static logger logger_instance{config, nv_config, std::move(writer)}; + + return logger_instance; +} + +logger** logger::GetInjectedTestInstance() +{ + static logger* pointer{nullptr}; + return &pointer; +} + + + +void logger::InjectTestInstance(logger* const logger_ptr) +{ + *GetInjectedTestInstance() = logger_ptr; +} + + +const bmw::mw::log::detail::Configuration& logger::get_config() const +{ + return config_; +} + +const bmw::mw::log::NvConfig& logger::get_non_verbose_config() const +{ + return nvconfig_; +} + +bmw::mw::log::detail::SharedMemoryWriter& logger::GetSharedMemoryWriter() +{ + if (shared_memory_writer_.has_value()) + { + return shared_memory_writer_.value(); + } + // return a fallback that will discard any operations requested as a way of dealing with logging operation + // failures. This approach is used to avoid application abort. + // Using getter method and return the reference to avoid copy overhead. + // + return discard_operation_fallback_shm_writer_; +} + + +} // namespace platform +} // namespace bmw diff --git a/mw/log/legacy_non_verbose_api/tracing.h b/mw/log/legacy_non_verbose_api/tracing.h new file mode 100644 index 0000000..23520e1 --- /dev/null +++ b/mw/log/legacy_non_verbose_api/tracing.h @@ -0,0 +1,456 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef AAS_MW_LOG_LEGACY_NON_VERBOSE_API_TRACING_H_ +#define AAS_MW_LOG_LEGACY_NON_VERBOSE_API_TRACING_H_ + +/// \file This file contains the legacy API for non-verbose logging. +/// We only keep this file for legacy compatibility reasons. +/// Going forward a proper C++ API for mw::log shall be defined to replace this code. + +#include "serialization/for_logging.h" + +#include "platform/aas/lib/os/utils/high_resolution_steady_clock.h" +#include "platform/aas/mw/log/configuration/configuration.h" +#include "platform/aas/mw/log/configuration/nvconfig.h" +#include "platform/aas/mw/log/detail/data_router/shared_memory/writer_factory.h" +#include "platform/aas/mw/log/detail/logging_identifier.h" +#include "platform/aas/mw/log/runtime.h" + +#include "amp_utility.hpp" + +namespace bmw +{ +namespace platform +{ + +using timestamp_t = bmw::os::HighResolutionSteadyClock::time_point; +using msgsize_t = uint16_t; + +enum class LogLevel : uint8_t +{ + kOff = 0x00, + kFatal = 0x01, + kError = 0x02, + kWarn = 0x03, + kInfo = 0x04, + kDebug = 0x05, + kVerbose = 0x06 +}; + +class logger +{ + public: + using AppPrefix = std::array; + + static logger& instance(const amp::optional& config = amp::nullopt, + const amp::optional& nv_config = amp::nullopt, + amp::optional = amp::nullopt) noexcept; + + explicit logger(const amp::optional& config, + const amp::optional& nv_config, + amp::optional&&) noexcept; + + template + amp::optional RegisterType() noexcept + { + class TypeinfoWithPrefix + { + public: + explicit TypeinfoWithPrefix(const AppPrefix& appPref) : appPrefix_(appPref) {} + + std::size_t size() const + + { + // coverity issue solved with this comparision and to deeply analyze task we created issue. + // Issue: + if (::bmw::common::visitor::logger_type_info().size() > + std::numeric_limits::max() - appPrefix_.size()) + { + // If an overflow were to happen, then its safe to return + // maxpayload size and appPrefixsize. + return appPrefix_.size() + bmw::mw::log::detail::SharedMemoryWriter::GetMaxPayloadSize(); + } + + return appPrefix_.size() + ::bmw::common::visitor::logger_type_info().size(); + } + + + void copy(const amp::span data) const + + + { + using SpanSizeType = amp::span::size_type; + std::ignore = std::copy(appPrefix_.begin(), appPrefix_.end(), data.data()); + // static cast is allowed as size of the span is not expected to have a negative value + const auto size = static_cast(data.size()); + static_assert(sizeof(size) >= sizeof(SpanSizeType), + "Cast to unsigned of at least the same size is expected"); + // ----- COMMON_ARGUMENTATION ---- + // Pointer is needed to iterate char array. + // -------------------------------- + + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) COMMON_ARGUMENTATION. + ::bmw::common::visitor::logger_type_info().copy( + data.subspan(static_cast(appPrefix_.size())).data(), size - appPrefix_.size()); + + } + + + private: + + + const AppPrefix& appPrefix_; + + }; + + if (shared_memory_writer_.has_value()) + { + auto id = shared_memory_writer_.value().TryRegisterType(TypeinfoWithPrefix(appPrefix)); + if (true == id.has_value()) + { + return static_cast(id.value()); + } + } + + return {}; + } + + template + LogLevel get_type_level() const + { + auto log_level = LogLevel::kInfo; + const bmw::mw::log::config::NvMsgDescriptor* const msg_desc = + nvconfig_.getDltMsgDesc(::bmw::common::visitor::struct_visitable::name()); + if (msg_desc != nullptr) + { + auto message_descriptor_log_level = msg_desc->logLevel_; + // Check the range's value of the log level before casting it to the LogLevel enum. + if (message_descriptor_log_level <= bmw::mw::log::LogLevel::kVerbose) + { + log_level = static_cast(msg_desc->logLevel_); + } + } + return log_level; + } + + template + LogLevel get_type_threshold() const noexcept + { + return GetLevelForContext(::bmw::common::visitor::struct_visitable::name()).value_or(LogLevel::kVerbose); + } + + bmw::mw::log::detail::SharedMemoryWriter& GetSharedMemoryWriter(); + + const bmw::mw::log::detail::Configuration& get_config() const; + const bmw::mw::log::NvConfig& get_non_verbose_config() const; + + /// \brief Only for testing to inject an instance to intercept and check the behavior. + static void InjectTestInstance(logger* const logger_ptr); + + + private: + + static logger** GetInjectedTestInstance(); + std::optional GetLevelForContext(const std::string& name) const noexcept; + + + bmw::mw::log::detail::Configuration config_; + bmw::mw::log::NvConfig nvconfig_; + amp::optional shared_memory_writer_; + bmw::mw::log::detail::SharedData discard_operation_fallback_shm_data_; + bmw::mw::log::detail::SharedMemoryWriter discard_operation_fallback_shm_writer_; + AppPrefix appPrefix; + +}; + +template +class log_entry +{ + public: + static log_entry& instance() noexcept + { + // It's a singleton by design hence cannot be made const + // + static log_entry entry{}; + return entry; + } + + amp::optional RegisterTypeGetId() noexcept + { + const auto registered_id = logger::instance().RegisterType(); + if (registered_id.has_value()) + { + shared_memory_id_ = registered_id.value(); + } + return registered_id; + } + + template + void TrySerializeIntoSharedMemory(F serialize) noexcept + { + if (bmw::mw::log::detail::GetRegisterTypeToken() == shared_memory_id_) + { + if (!RegisterTypeGetId().has_value()) + { + return; + } + } + serialize(); + } + + + void TryWriteIntoSharedMemory(const T& t) noexcept + + { + TrySerializeIntoSharedMemory( + + [&t, this]() noexcept + + { + using s = ::bmw::common::visitor::logging_serializer; + logger::instance().GetSharedMemoryWriter().AllocAndWrite( + + [&t] + + (const auto data_span) { + return s::serialize(t, data_span.data(), bmw::mw::log::detail::GetDataSizeAsLength(data_span)); + }, + shared_memory_id_, + static_cast(s::serialize_size(t))); + }); + } + + + /// \public + /// \thread-safe + void log_at_time(timestamp_t timestamp, const T& t) + + { + TrySerializeIntoSharedMemory( + + [×tamp, &t, this]() + + { + using s = ::bmw::common::visitor::logging_serializer; + logger::instance().GetSharedMemoryWriter().AllocAndWrite( + timestamp, + shared_memory_id_, + s::serialize_size(t), + + [&t](const auto data_span) + + { + return s::serialize(t, data_span.data(), bmw::mw::log::detail::GetDataSizeAsLength(data_span)); + }); + }); + } + + log_entry() noexcept + : shared_memory_id_{bmw::mw::log::detail::GetRegisterTypeToken()}, + default_enabled_{false}, + level_enabled_{LogLevel::kVerbose} + { + amp::ignore = bmw::mw::log::detail::Runtime::GetRecorder(); + + + + + static_assert(sizeof(typename ::bmw::common::visitor::logging_serialized_descriptor::payload_type) <= + bmw::mw::log::detail::SharedMemoryWriter::GetMaxPayloadSize(), + "Serialized type too large"); + + level_enabled_ = logger::instance().get_type_threshold(); + default_enabled_ = level_enabled_ >= logger::instance().get_type_level(); + + amp::ignore = RegisterTypeGetId(); + } + + + + + /// \public + /// \thread-safe + void log_serialized(const char* data, const msgsize_t size) + + + { + TrySerializeIntoSharedMemory( + + [&data, &size, this]() + + { + logger::instance().GetSharedMemoryWriter().AllocAndWrite( + + [&data, &size](const auto data_span) + + { + const auto data_pointer = data_span.data(); + std::ignore = std::copy_n(data, size, data_pointer); + return size; + }, + shared_memory_id_, + size); + }); + } + + /// \public + /// \thread-safe + bool enabled() const { return default_enabled_; } + + /// \public + /// \thread-safe + bool enabled_at(LogLevel level) const { return level_enabled_ >= level; } + + + private: + + + + std::atomic shared_memory_id_; + + bool default_enabled_; + LogLevel level_enabled_; + +}; + +} // namespace platform +} // namespace bmw + +/// \public +/// \thread-safe +template +// Defined in global namespace, because it is global function, used in many files in different namespaces. +// +inline auto& LOG_ENTRY() +{ + using DecayedType = std::decay_t; + return bmw::platform::log_entry::instance(); +} + +/// \public +/// \thread-safe +template +// Defined in global namespace, because it is global function, used in many files in different namespaces. +// +inline void TRACE_LEVEL(bmw::platform::LogLevel level, const T& arg) +{ + auto& logger = LOG_ENTRY(); + if (logger.enabled_at(level)) + { + logger.TryWriteIntoSharedMemory(arg); + } +} + +/// \public +/// \thread-safe +template +// Defined in global namespace, because it is global function, used in many files in different namespaces. +// +inline void LOG_INTERNAL_LOGGER(const T& arg) +{ + auto& logger = LOG_ENTRY(); + if (logger.enabled()) + { + logger.TryWriteIntoSharedMemory(arg); + } +} + +/// \public +/// \thread-safe +template +// Defined in global namespace, because it is global function, used in many files in different namespaces. +// +inline void TRACE(const T& arg) +{ + LOG_INTERNAL_LOGGER(arg); +} + +/// \public +/// \thread-safe +template +// Defined in global namespace, because it is global function, used in many files in different namespaces. +// +inline void TRACE_VERBOSE(const T& arg) +{ + TRACE_LEVEL(bmw::platform::LogLevel::kVerbose, arg); +} + +/// \public +/// \thread-safe +template +// Defined in global namespace, because it is global function, used in many files in different namespaces. +// +inline void TRACE_DEBUG(const T& arg) +{ + TRACE_LEVEL(bmw::platform::LogLevel::kDebug, arg); +} + +/// \public +/// \thread-safe +template +// Defined in global namespace, because it is global function, used in many files in different namespaces. +// +inline void TRACE_INFO(const T& arg) +{ + TRACE_LEVEL(bmw::platform::LogLevel::kInfo, arg); +} + +/// \public +/// \thread-safe +template +// Defined in global namespace, because it is global function, used in many files in different namespaces. +// +inline void TRACE_WARNING(const T& arg) +{ + TRACE_LEVEL(bmw::platform::LogLevel::kWarn, arg); +} + +/// \public +/// \thread-safe +template +// Defined in global namespace, because it is global function, used in many files in different namespaces. +// +inline void TRACE_ERROR(const T& arg) +{ + TRACE_LEVEL(bmw::platform::LogLevel::kError, arg); +} + +/// \public +/// \thread-safe +template +// Defined in global namespace, because it is global function, used in many files in different namespaces. +// +inline void TRACE_FATAL(const T& arg) +{ + TRACE_LEVEL(bmw::platform::LogLevel::kFatal, arg); +} + +/// \public +/// \thread-safe +template +// Defined in global namespace, because it is global function, used in many files in different namespaces. +// +inline void TRACE_WARN(const T& arg) +{ + TRACE_LEVEL(bmw::platform::LogLevel::kWarn, arg); +} + +/// \public +/// \thread-safe +// Using preprocessor here because we define function macro to use it any where with name STRUCT_TRACEABLE +// +#define STRUCT_TRACEABLE(...) STRUCT_VISITABLE(__VA_ARGS__) + +#endif // AAS_MW_LOG_LEGACY_NON_VERBOSE_API_TRACING_H_ diff --git a/mw/log/legacy_non_verbose_api/tracing_test.cpp b/mw/log/legacy_non_verbose_api/tracing_test.cpp new file mode 100644 index 0000000..761a7c3 --- /dev/null +++ b/mw/log/legacy_non_verbose_api/tracing_test.cpp @@ -0,0 +1,297 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/detail/data_router/data_router_backend.h" + +#include "platform/aas/lib/os/mocklib/stat_mock.h" +#include "platform/aas/lib/os/mocklib/stdlib_mock.h" +#include "platform/aas/mw/log/detail/data_router/shared_memory/shared_memory_reader.h" +#include "platform/aas/mw/log/legacy_non_verbose_api/tracing.h" + +#include "platform/aas/pas/logging/include/filetransfer/filetransfer_message.h" +#include "platform/aas/pas/logging/include/filetransfer/filetransfer_message_trace.h" + +#include "serialization/for_logging.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +using ::testing::_; +using ::testing::AnyNumber; +using ::testing::Return; + +using SerializeNs = ::bmw::common::visitor::logging_serializer; +const std::string ERROR_CONTENT_1_PATH = + "platform/aas/mw/log/legacy_non_verbose_api/test/error-content-json-class-id.json"; +const std::string JSON_PATH = "platform/aas/mw/log/legacy_non_verbose_api/test/test-class-id.json"; +struct DatarouterMessageClientStub : DatarouterMessageClient +{ + void Run() override {} + void Shutdown() override {} +}; + +class DatarouterMessageClientStubFactory : public DatarouterMessageClientFactory +{ + public: + std::unique_ptr CreateOnce(const std::string&, const std::string&) override + { + return std::make_unique(); + } +}; + +class LoggerFixture : public ::testing::Test +{ + public: + void PrepareFixture(bmw::mw::log::NvConfig nv_config, uint64_t size = 1024UL) + { + auto kBufferSize = size; + buffer1_.resize(kBufferSize); + buffer2_.resize(kBufferSize); + shared_data_.control_block.control_block_even.data = {buffer1_.data(), static_cast(kBufferSize)}; + shared_data_.control_block.control_block_odd.data = {buffer2_.data(), static_cast(kBufferSize)}; + shared_data_.control_block.switch_count_points_active_for_writing = std::uint32_t{1}; + + AlternatingReadOnlyReader read_only_reader{shared_data_.control_block, + shared_data_.control_block.control_block_even.data, + shared_data_.control_block.control_block_odd.data}; + reader_ = std::make_unique(shared_data_, std::move(read_only_reader), []() noexcept {}); + + SharedMemoryWriter writer{shared_data_, []() noexcept {}}; + const amp::string_view kCtx{"STDA"}; + const ContextLogLevelMap context_log_level_map{{LoggingIdentifier{kCtx}, LogLevel::kError}}; + config_.SetContextLogLevel(context_log_level_map); + logger_ = std::make_unique(config_, nv_config, std::move(writer)); + ::bmw::platform::logger::InjectTestInstance(logger_.get()); + } + + void PrepareContextLogLevelFixture(bmw::mw::log::NvConfig nv_config, const amp::string_view ctxid) + { + SharedMemoryWriter writer{shared_data_, []() noexcept {}}; + const ContextLogLevelMap context_log_level_map{{LoggingIdentifier{ctxid}, LogLevel::kError}}; + config_.SetContextLogLevel(context_log_level_map); + logger_ = std::make_unique(config_, nv_config, std::move(writer)); + ::bmw::platform::logger::InjectTestInstance(logger_.get()); + } + + void TearDown() override { ::bmw::platform::logger::InjectTestInstance(nullptr); } + void SimulateLogging(const LogLevel logLevel = LogLevel::kError, + const std::string& context_id = "xxxx", + const std::string& app_id = "xxxx") + { + + const auto slot = unit_.ReserveSlot().value(); + + auto&& logRecord = unit_.GetLogRecord(slot); + auto& log_entry = logRecord.getLogEntry(); + + log_entry.app_id = LoggingIdentifier{app_id}; + log_entry.ctx_id = LoggingIdentifier{context_id}; + log_entry.log_level = logLevel; + log_entry.num_of_args = 5; + logRecord.getVerbosePayload().Put("xyz xyz", 7); + + unit_.FlushSlot(slot); + + const auto acquire_result = logger_->GetSharedMemoryWriter().ReadAcquire(); + config_ = logger_->get_config(); + + reader_->NotifyAcquisitionSetReader(acquire_result); + + reader_->Read([](const TypeRegistration&) noexcept {}, + [this](const SharedMemoryRecord& record) { + amp::ignore = SerializeNs::deserialize( + record.payload.data(), GetDataSizeAsLength(record.payload), header_); + }); + } + + Configuration config_{}; + std::unique_ptr logger_{}; + LogEntry header_{}; + + private: + SharedData shared_data_{}; + std::unique_ptr reader_{}; + DatarouterMessageClientStubFactory message_client_factory_{}; + + std::vector buffer1_{}; + std::vector buffer2_{}; + DataRouterBackend unit_{std::uint8_t{255UL}, LogRecord{}, message_client_factory_, config_, WriterFactory{{}}}; +}; + +TEST_F(LoggerFixture, WhenCreatingSharedMemoryWriterwithNotEnoughBufferSizeRegesteringNewTypeShallFail) +{ + RecordProperty("Requirement", ",,"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "When Creating Shared memory writer with not enough buffer size, registering new type shall fail"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + PrepareFixture(bmw::mw::log::NvConfig{JSON_PATH}, 1); + SimulateLogging(); +} + +TEST_F(LoggerFixture, WhenCreatingSharedMemoryWriterwithOneKiloBytesBufferSizeRegesteringNewTypeShallFail) +{ + RecordProperty("Requirement", ",,"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "When Creating Shared memory writer with not enough buffer size (1 kiloBytes), registering new type " + "shall fail"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + constexpr auto kBufferSize = 1024UL; + PrepareFixture(bmw::mw::log::NvConfig{JSON_PATH}, kBufferSize); + SimulateLogging(); +} + +TEST_F(LoggerFixture, WhenProvidingCorrectNvConfigGetTypeLevelAndThreshold) +{ + RecordProperty("Requirement", ","); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "The log message shall be disabled if the log level is above to the threshold."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + PrepareFixture(bmw::mw::log::NvConfig{JSON_PATH}); + EXPECT_EQ(bmw::platform::LogLevel::kError, logger_->get_type_level()); + EXPECT_EQ(bmw::platform::LogLevel::kError, logger_->get_type_threshold()); +} + +TEST_F(LoggerFixture, WhenProvidingNvConfigWithErrorShallGetErrorContent) +{ + RecordProperty("Requirement", ",,"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Unable to parse the JSON file due to error in the content."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + PrepareFixture(bmw::mw::log::NvConfig{ERROR_CONTENT_1_PATH}); + bmw::mw::log::NvConfig nv = logger_->get_non_verbose_config(); + EXPECT_EQ(bmw::mw::log::NvConfig::ReadResult::kERROR_CONTENT, nv.parseFromJson()); +} + +TEST(LoggerFallback, WhenProperWriterNotProvidedFailSafeFallbackShallBeReturned) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verify the ability of returning failsafe fallback in case of a wrong writer was provided."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + amp::optional writer{amp::nullopt}; + Configuration config_{}; + bmw::mw::log::NvConfig nv_config; + std::unique_ptr logger{}; + logger = std::make_unique(config_, nv_config, std::move(writer)); + + const auto acquire_result = logger->GetSharedMemoryWriter().ReadAcquire(); + EXPECT_EQ(acquire_result.acquired_buffer, 1UL); +} + +TEST(LoggerFallback, AllArgsNulloptShallReturnFailsafeFallback) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verify the ability of returning failsafe fallback in case of initialize logger with no arguments."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + std::unique_ptr logger{}; + logger = std::make_unique(amp::nullopt, amp::nullopt, amp::nullopt); + + const auto acquire_result = logger->GetSharedMemoryWriter().ReadAcquire(); + EXPECT_EQ(acquire_result.acquired_buffer, 1UL); + + constexpr Length kSmallRequest{1UL}; + + // AllocAndWrite shall discard operation by providing empty span: + logger->GetSharedMemoryWriter().AllocAndWrite( + [](const auto data_span) noexcept { + EXPECT_EQ(data_span.size(), 0); + return 0UL; + }, + TypeIdentifier{1UL}, + kSmallRequest); +} + +TEST_F(LoggerFixture, WhenProvidingWrongCtxIdWillLeadToVerboseLogLevelThreshold) +{ + RecordProperty("Requirement", ","); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "The log level should set to verbose when providing wrong ctx id."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + PrepareContextLogLevelFixture(bmw::mw::log::NvConfig{JSON_PATH}, "not supported ctx id"); + EXPECT_EQ(bmw::platform::LogLevel::kVerbose, logger_->get_type_threshold()); +} + +TEST_F(LoggerFixture, GetSharedMemoryWriterShallFailWhenThereIsNoSharedMemoryAllocatedUsingLoggerInstanceInitialization) +{ + RecordProperty("Requirement", ","); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "The code should terminate when getting the shared memory writer when there is no shared memory allocated. "); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + amp::optional shared_memory{}; + logger_ = + std::make_unique(config_, bmw::mw::log::NvConfig{JSON_PATH}, std::move(shared_memory)); + + const auto acquire_result = logger_->GetSharedMemoryWriter().ReadAcquire(); + EXPECT_EQ(acquire_result.acquired_buffer, 1UL); +} + +TEST_F( + LoggerFixture, + GetSharedMemoryWriterShallFailWhenThereIsNoSharedMemoryAllocatedUsingLoggerInstanceInitializationAndCallingRegisterType) +{ + RecordProperty("Requirement", ","); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "The code should terminate when getting the shared-memory writer if there is no shared memory " + "allocated during the RegisterType call."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + amp::optional shared_memory; + logger_ = + std::make_unique(config_, bmw::mw::log::NvConfig{JSON_PATH}, std::move(shared_memory)); + logger_->RegisterType(); + + const auto acquire_result = logger_->GetSharedMemoryWriter().ReadAcquire(); + EXPECT_EQ(acquire_result.acquired_buffer, 1UL); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/log_common.cpp b/mw/log/log_common.cpp new file mode 100644 index 0000000..cc0ffd8 --- /dev/null +++ b/mw/log/log_common.cpp @@ -0,0 +1,48 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/log_common.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +const PeriodSuffixMap PeriodToSuffix = {{typeid(std::atto::type), "as"}, + {typeid(std::femto::type), "fs"}, + {typeid(std::pico::type), "ps"}, + {typeid(std::nano::type), "ns"}, + {typeid(std::micro::type), "μs"}, + {typeid(std::milli::type), "ms"}, + {typeid(std::centi::type), "cs"}, + {typeid(std::deci::type), "ds"}, + {typeid(std::ratio<1>::type), "s"}, + {typeid(std::deca::type), "das"}, + {typeid(std::hecto::type), "hs"}, + {typeid(std::kilo::type), "ks"}, + {typeid(std::mega::type), "Ms"}, + {typeid(std::giga::type), "Gs"}, + {typeid(std::tera::type), "Ts"}, + {typeid(std::peta::type), "Ps"}, + {typeid(std::exa::type), "Es"}, + {typeid(std::ratio<60>::type), "min"}, + {typeid(std::ratio<3600>::type), "h"}}; +} // namespace detail + +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/log_common.h b/mw/log/log_common.h new file mode 100644 index 0000000..3b6f144 --- /dev/null +++ b/mw/log/log_common.h @@ -0,0 +1,53 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#ifndef PLATFORM_AAS_MW_LOG_COMMON_H +#define PLATFORM_AAS_MW_LOG_COMMON_H + +#include + +#include + +#include "amp_string_view.hpp" +#include +#include +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +namespace detail +{ +using PeriodSuffixMap = std::unordered_map; + +extern const PeriodSuffixMap PeriodToSuffix; +} // namespace detail + +template +amp::string_view DurationUnitSuffix() noexcept +{ + return detail::PeriodToSuffix.at(typeid(typename Period::type)); +} + +} // namespace log +} // namespace mw +} // namespace bmw + +#endif \ No newline at end of file diff --git a/mw/log/log_level.cpp b/mw/log/log_level.cpp new file mode 100644 index 0000000..c6adeb8 --- /dev/null +++ b/mw/log/log_level.cpp @@ -0,0 +1,50 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/log_level.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +mw::log::LogLevel GetLogLevelFromU8(std::uint8_t candidate_log_level) +{ + if (candidate_log_level <= static_cast(GetMaxLogLevelValue())) + { + return static_cast(candidate_log_level); + } + else + { + return LogLevel::kOff; + } +} + +std::optional TryGetLogLevelFromU8(std::uint8_t candidate_log_level) +{ + if (candidate_log_level <= static_cast(GetMaxLogLevelValue())) + { + return static_cast(candidate_log_level); + } + else + { + return std::nullopt; + } +} + +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/log_level.h b/mw/log/log_level.h new file mode 100644 index 0000000..73f699c --- /dev/null +++ b/mw/log/log_level.h @@ -0,0 +1,71 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_LOG_LEVEL_H +#define PLATFORM_AAS_MW_LOG_LOG_LEVEL_H + +// Be careful what you include here. Each additional header will be included in logging.h and thus exposed to the user. +// We need to try to keep the includes low to reduce the compile footprint of using this library. +#include +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +/// \brief Represents the severity of a log message +/// \public +/// +/// \details The severity of log messages will be used to filter if a message shall be further processed. This can be +/// used by an end-user to filter messages and reduce performance implications due to extensive logging. + + + +enum class LogLevel : std::uint8_t + + + +{ + kOff = 0x00, + kFatal = 0x01, + kError = 0x02, + kWarn = 0x03, + kInfo = 0x04, + kDebug = 0x05, + kVerbose = 0x06, +}; + +constexpr LogLevel GetMaxLogLevelValue() +{ + return std::max({LogLevel::kVerbose, + LogLevel::kInfo, + LogLevel::kWarn, + LogLevel::kError, + LogLevel::kFatal, + LogLevel::kDebug, + LogLevel::kOff}); +} + +mw::log::LogLevel GetLogLevelFromU8(std::uint8_t candidate_log_level); +std::optional TryGetLogLevelFromU8(std::uint8_t candidate_log_level); + +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_LOG_LEVEL_H diff --git a/mw/log/log_level_test.cpp b/mw/log/log_level_test.cpp new file mode 100644 index 0000000..dcc7cfc --- /dev/null +++ b/mw/log/log_level_test.cpp @@ -0,0 +1,55 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/log_level.h" + +#include "gtest/gtest.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +TEST(LogLevelTesting, EnsureMaxLevelCoversAllEnumCases) +{ + const bmw::mw::log::LogLevel max_log_level = GetMaxLogLevelValue(); + // Test conditions are intentionally put into switch to enforce covering all enum values: + switch (max_log_level) + { + case LogLevel::kVerbose: + EXPECT_EQ(LogLevel::kVerbose, max_log_level); + break; + case LogLevel::kDebug: + case LogLevel::kInfo: + case LogLevel::kWarn: + case LogLevel::kError: + case LogLevel::kFatal: + case LogLevel::kOff: + default: + FAIL(); + break; + } +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/log_mode.h b/mw/log/log_mode.h new file mode 100644 index 0000000..8b66ffa --- /dev/null +++ b/mw/log/log_mode.h @@ -0,0 +1,44 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_LOG_MODE_H +#define PLATFORM_AAS_MW_LOG_LOG_MODE_H + +#include + +namespace bmw +{ +namespace mw +{ + +/// +/// \brief Log mode. Flags, used to configure the sink for log messages. +/// \public +/// \details Flags can be combined. +/// +/// \Requirement{SWS_LOG_00019} +//// +enum class LogMode : uint8_t +{ + kRemote = 0x01, ///< Sent remotely + kFile = 0x02, ///< Save to file + kConsole = 0x04, ///< Forward to console, + kSystem = 0x08, ///< QNX: forward to slog, + kInvalid = 0xff ///< Invalid log mode, +}; + +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_LOG_LEVEL_H diff --git a/mw/log/log_stream.cpp b/mw/log/log_stream.cpp new file mode 100644 index 0000000..82c023a --- /dev/null +++ b/mw/log/log_stream.cpp @@ -0,0 +1,246 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/log_stream.h" + +#include "platform/aas/mw/log/detail/thread_local_guard.h" +#include "platform/aas/mw/log/recorder.h" + +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +namespace +{ + +const std::string kDefaultContext = "DFLT"; + +} // namespace + +LogStream::LogStream(Recorder& recorder, + Recorder& fallback_recorder, + const LogLevel log_level, + const amp::string_view context_id) noexcept + + : recorder_{recorder}, + fallback_recorder_{fallback_recorder}, + context_id_(context_id.data() == nullptr ? kDefaultContext : context_id), + log_level_{log_level} + +{ + // Construction fallback handled in log_stream_factory (using here CallRecorder, would give a false impression) + slot_ = recorder_.StartRecord(context_id_.GetStringView(), log_level_); +} + +LogStream::~LogStream() noexcept +{ + if (slot_.has_value()) + { + CallOnRecorder(&Recorder::StopRecord, slot_.value()); + } +} + +LogStream::LogStream(LogStream&& other) noexcept + : recorder_{other.recorder_}, + fallback_recorder_{other.fallback_recorder_}, + slot_{other.slot_}, + context_id_{other.context_id_}, + log_level_{other.log_level_} +{ + // Detach the moved-from log stream from the slot to ensure the slot is only owned by one LogStream. + other.slot_.reset(); + + other.context_id_ = detail::LoggingIdentifier{""}; + other.log_level_ = LogLevel::kOff; +} + +void LogStream::Flush() noexcept +{ + if (slot_.has_value()) + { + CallOnRecorder(&Recorder::StopRecord, slot_.value()); + } + slot_ = CallOnRecorder(&Recorder::StartRecord, context_id_.GetStringView(), log_level_); +} + +LogStream& LogStream::Log(const bool value) noexcept +{ + return LogWithRecorder(value); +} + +LogStream& LogStream::Log(const std::int8_t value) noexcept +{ + return LogWithRecorder(value); +} + +LogStream& LogStream::Log(const std::int16_t value) noexcept +{ + return LogWithRecorder(value); +} + +LogStream& LogStream::Log(const std::int32_t value) noexcept +{ + return LogWithRecorder(value); +} + +LogStream& LogStream::Log(const std::int64_t value) noexcept +{ + return LogWithRecorder(value); +} + +LogStream& LogStream::Log(const std::uint8_t value) noexcept +{ + return LogWithRecorder(value); +} + +LogStream& LogStream::Log(const std::uint16_t value) noexcept +{ + return LogWithRecorder(value); +} + +LogStream& LogStream::Log(const std::uint32_t value) noexcept +{ + return LogWithRecorder(value); +} + +LogStream& LogStream::Log(const std::uint64_t value) noexcept +{ + return LogWithRecorder(value); +} + +LogStream& LogStream::Log(const float value) noexcept +{ + return LogWithRecorder(value); +} + +LogStream& LogStream::Log(const double value) noexcept +{ + return LogWithRecorder(value); +} + +LogStream& LogStream::Log(const LogString value) noexcept +{ + if (value.Data() == nullptr) + { + return *this; + } + return LogWithRecorder(amp::string_view{value.Data(), value.Size()}); +} + +LogStream& LogStream::Log(const LogHex8& value) noexcept +{ + return LogWithRecorder(value); +} + +LogStream& LogStream::Log(const LogHex16& value) noexcept +{ + return LogWithRecorder(value); +} + +LogStream& LogStream::Log(const LogHex32& value) noexcept +{ + return LogWithRecorder(value); +} + +LogStream& LogStream::Log(const LogHex64& value) noexcept +{ + return LogWithRecorder(value); +} + +LogStream& LogStream::Log(const LogBin8& value) noexcept +{ + return LogWithRecorder(value); +} + +LogStream& LogStream::Log(const LogBin16& value) noexcept +{ + return LogWithRecorder(value); +} + + +/* False positive: LogStream shouldn't be considered as const. It's mandated by the ara::log API. */ +LogStream& LogStream::Log(const LogBin32& value) noexcept +{ + return LogWithRecorder(value); +} + +LogStream& LogStream::Log(const LogBin64& value) noexcept +{ + return LogWithRecorder(value); +} + +/* False positive: LogStream shouldn't be considered as const. It's mandated by the ara::log API. */ + +LogStream& LogStream::Log(const LogSlog2Message& value) noexcept +{ + return LogWithRecorder(value); +} + +template <> +// Log method for `LogRawBuffer` must be templated so that the overload for `LogString` always +// has higher precedence during overload resolution in case the parameter type is string-like. +// +LogStream& LogStream::Log(const LogRawBuffer& value) noexcept +{ + if (value.data() == nullptr) + { + return *this; + } + return LogWithRecorder(value); +} + +/* False positive: LogStream shouldn't be considered as const. It's mandated by the ara::log API. */ + +template +LogStream& LogStream::LogWithRecorder(const T value) noexcept +{ + if (slot_.has_value()) + { + CallOnRecorder(&Recorder::Log, slot_.value(), value); + } + return *this; +} + +// Magic function, to dispatch any recorder function to either the default recorder (if not in logging stack) or the +// fallback recorder if called within the logging stack. #templatemagic +template +// Checker overly strict, pointer of object not null and only existing functions called (ensure by typeset) +// NOLINTNEXTLINE(bmw-no-pointer-to-member): See above +ReturnValue LogStream::CallOnRecorder(ReturnValue (Recorder::*arbitrary_function)(ArgsOfFunction...) noexcept, + ArgsPassed&&... args) noexcept +{ + if (not detail::ThreadLocalGuard::IsWithingLogging()) + { + detail::ThreadLocalGuard guard{}; + // Checker overly strict, pointer of object not null and only existing functions called (ensure by typeset) + // NOLINTNEXTLINE(bmw-no-pointer-to-member): See above + return (recorder_.*arbitrary_function)(std::forward(args)...); + } + else + { + // Checker overly strict, pointer of object not null and only existing functions called (ensure by typeset) + // NOLINTNEXTLINE(bmw-no-pointer-to-member): See above + return (fallback_recorder_.*arbitrary_function)(std::forward(args)...); + } +} + +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/log_stream.h b/mw/log/log_stream.h new file mode 100644 index 0000000..9a38ed0 --- /dev/null +++ b/mw/log/log_stream.h @@ -0,0 +1,339 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_LOG_STREAM_H +#define PLATFORM_AAS_MW_LOG_LOG_STREAM_H + +// Be careful what you include here. Each additional header will be included in logging.h and thus exposed to the user. +// We need to try to keep the includes low to reduce the compile footprint of using this library. +#include "platform/aas/mw/log/detail/logging_identifier.h" +#include "platform/aas/mw/log/log_common.h" +#include "platform/aas/mw/log/log_level.h" +#include "platform/aas/mw/log/log_types.h" +#include "platform/aas/mw/log/slot_handle.h" + +#include "amp_optional.hpp" +#include "amp_string_view.hpp" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +// Forward declaration. +// +class Recorder; + +template +constexpr static bool IsCharPtrType() noexcept +{ + using PointerType = std::remove_reference_t; + if constexpr (std::is_pointer_v) + { + using ValueType = std::remove_pointer_t; + return std::is_same_v>; + } + return false; +} + +template +constexpr static bool IsCharArrayType() noexcept +{ + using ArrayType = amp::remove_cvref_t; + return std::is_array_v && std::is_same_v>; +} + +template +constexpr static bool IsLogRawBufferType() noexcept +{ + return std::is_same_v>; +} + +namespace detail +{ +class LogStreamFactory; + +template +struct TypesHolder +{ +}; + +template +constexpr static bool LogStreamSupports(TypesHolder /*unused*/) noexcept +{ + if constexpr (not(IsCharPtrType())) + { + using RequestedType = amp::remove_cvref_t; + return (std::is_convertible_v || ...); + } + else + { + return false; + } +} +} // namespace detail + +template +constexpr static bool LogStreamSupports() noexcept +{ + using SupportedTypes = detail::TypesHolder; + return detail::LogStreamSupports(SupportedTypes{}); +} + +// \brief User-Facing RAII class that manages a LogStream and can be used to log data. The log message will only be +// flushed upon destruction of this type. It is not possible to reuse one LogStream for multiple different log +// messages. Logging is a best effort operation, if it is not possible to log a message due to some reason, this class +// will not forward the respective messages. +// +// \details This class only supports the logging of the following basic data types: +// * bool +// * float +// * double +// * std::int8_t +// * std::int16_t +// * std::int32_t +// * std::int64_t +// * std::uint8_t +// * std::uint16_t +// * std::uint32_t +// * std::uint64_t +// * mw::log::LogBin8 +// * mw::log::LogBin16 +// * mw::log::LogBin32 +// * mw::log::LogBin64 +// * mw::log::LogHex8 +// * mw::log::LogHex16 +// * mw::log::LogHex32 +// * mw::log::LogHex64 +// * mw::log::LogString +// * mw::log::LogRawBuffer +// * std::string (via implicit conversion to mw::log::LogString) +// * std::string_view (via implicit conversion to mw::log::LogString) +// * amp::string_view (via implicit conversion to mw::log::LogString) +// * amp::pmr::string (via implicit conversion to mw::log::LogString) +// * ara::core::StringView (via implicit conversion to mw::log::LogString) +// * std::array<[const] char> (via implicit conversion to mw::log::LogString) +// * pointer to const char (deprecated) +// * C-style array of char +// * custom data struct LogSlog2Message, see struct description +// +// If a user wants to log a custom data type, he needs to extend the possibility as follows in the context of +// his data type: +// +// bmw::mw::LogStream& operator<<(bmw::mw::LogStream& stream, const YourAwesomeType& type) +// { +// stream << YourAwesomeType.getSomething(); // custom logic on how to represent your type as Log-Message +// return stream; +// } + +/* False positive: LogStream shouldn't be considered as const. It's mandated by the ara::log API. */ +/// \public +class LogStream final +{ + public: + /// \brief Stream operator which enables logging of supported data types. + /// \public + /// \note Only the types mentioned in class description are supported by default. + /// \details Similar to std::cout it is not safe to access this stream from multiple threads! + template + std::enable_if_t(), LogStream&> operator<<(const T& value) & noexcept + { + return Log(value); + } + + /// \brief Deprecated stream operator which enables logging of C-style string via character pointer. + /// \note The pointed-to character sequence must be null-terminated! + /// \details Similar to std::cout it is not safe to access this stream from multiple threads! + template + [[deprecated( + "SPP_DEPRECATION(performance): " + "Logging a plain character pointer is discouraged for performance reasons since that requires a length " + "determination of the pointed-to character sequence at runtime and results in a sequential memory scan. " + "Instead, since almost all string-like types are implicitly nothrow-convertible to `mw::log::LogString`, " + "these should be logged as-is and not via their underlying character pointer! If that's not possible, a " + "custom overload of `operator<<` for the particular user-defined type should be provided.")]] std:: + enable_if_t(), LogStream&> + operator<<(T char_ptr) & + { + if (char_ptr != nullptr) + { + const LogString value{char_ptr, LogString::TraitsType::length(char_ptr)}; + return Log(value); + } + return *this; + } + + /// \brief Stream operator which enables logging of a string literal (-> character array). + /// \public + /// \details Similar to std::cout it is not safe to access this stream from multiple threads! + template + // NOLINTNEXTLINE(modernize-avoid-c-arrays): required for logging string literals without array-to-pointer decay + LogStream& operator<<(const LogString::CharType (&array)[N]) noexcept + { + const LogString value{std::forward(array)}; + return Log(value); + } + + // \brief LogStream is move-construction only + LogStream(const LogStream&) noexcept = delete; + LogStream(LogStream&&) noexcept; + LogStream& operator=(const LogStream&) noexcept = delete; + LogStream& operator=(LogStream&&) noexcept = delete; + ~LogStream() noexcept; + + /// \brief Flushes the current buffer and prepares a new one. + /// \public + /// \details Calling mw::log::LogStream::Flush is only necessary if the mw::log::LogStream + /// object is going to be re-used within the same scope. Otherwise, if the + /// object goes out of scope (e.g. end of function block) then the flushing operation will + /// be done internally by the destructor. It is important to note that the mw::log::- + /// LogStream::Flush command does not empty the buffer, but it forwards the buffer’s + /// current contents to the Logging framework. + void Flush() noexcept; + + + private: + + + // We can't make the ctor public, the ctor intended to be private to avoid instance instantiation by the user + // but it is needed internally by LogStreamFactory + // + friend bmw::mw::log::detail::LogStreamFactory; + + LogStream(Recorder&, Recorder&, const LogLevel, const amp::string_view) noexcept; + + /// \brief Internal methods that perform logging of supported data types. + LogStream& Log(const bool) noexcept; + LogStream& Log(const float) noexcept; + LogStream& Log(const double) noexcept; + LogStream& Log(const LogString) noexcept; + LogStream& Log(const std::int8_t) noexcept; + LogStream& Log(const std::int16_t) noexcept; + LogStream& Log(const std::int32_t) noexcept; + LogStream& Log(const std::int64_t) noexcept; + LogStream& Log(const std::uint8_t) noexcept; + LogStream& Log(const std::uint16_t) noexcept; + LogStream& Log(const std::uint32_t) noexcept; + LogStream& Log(const std::uint64_t) noexcept; + LogStream& Log(const LogBin8& value) noexcept; + LogStream& Log(const LogBin16& value) noexcept; + LogStream& Log(const LogBin32& value) noexcept; + LogStream& Log(const LogBin64& value) noexcept; + LogStream& Log(const LogHex8& value) noexcept; + LogStream& Log(const LogHex16& value) noexcept; + LogStream& Log(const LogHex32& value) noexcept; + LogStream& Log(const LogHex64& value) noexcept; + LogStream& Log(const LogSlog2Message& value) noexcept; + + /// \note Log method for `LogRawBuffer` must be templated so that the overload for `LogString` always + /// has higher precedence during overload resolution in case the parameter type is string-like. + template + std::enable_if_t(), LogStream&> Log(const RawBufferType&) noexcept; + + /// \brief Internal method which forwards the logging operation to `LogStream`'s provided recorder(s). + template + LogStream& LogWithRecorder(const T value) noexcept; + + template + // Checker overly strict, pointer of object not null and only existing functions called (ensure by typeset) + // NOLINTNEXTLINE(bmw-no-pointer-to-member): See above + ReturnValue CallOnRecorder(ReturnValue (Recorder::*arbitrary_function)(ArgsOfFunction...) noexcept, + ArgsPassed&&... args) noexcept; + + + Recorder& recorder_; + Recorder& fallback_recorder_; + amp::optional slot_; + + detail::LoggingIdentifier context_id_; + LogLevel log_level_; + +}; + + +/// \brief Stream operator overload which enables logging to a `LogStream` rvalue. +/// \public +template + +std::enable_if_t()), LogStream&> operator<<(LogStream&& out, T&& value) noexcept +{ + return (out << std::forward(value)); +} + +/// @brief Writes chrono duration parameter as text into message. +/// \public +template +LogStream& operator<<(LogStream& out, const std::chrono::duration& value) noexcept +{ + return (out << value.count() << DurationUnitSuffix()); +} + +/// \brief Support for signed long long (transforms into int64_t on 64 bit platforms) +/// \public +template < + typename T = void, + typename std::enable_if<((!std::is_same::value) && (sizeof(long long) == sizeof(int64_t))), + T>::type* = nullptr> +inline LogStream& operator<<(LogStream& out, long long value) noexcept +{ + return (out << static_cast(value)); +} + +// Non-standard extensions +// To use these utilities please import the namespace, like this for example: +// `using ara::log::bmw_ext::operator<<;` +namespace bmw_ext +{ + +template ::value, bool> = true> + + +/// \public +LogStream& operator<<(LogStream& out, EnumerationT enumvalue) + +{ + return (out << +typename std::underlying_type::type(enumvalue)); +} +} // namespace bmw_ext + +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_LOG_STREAM_H diff --git a/mw/log/log_stream_factory.cpp b/mw/log/log_stream_factory.cpp new file mode 100644 index 0000000..bffc47a --- /dev/null +++ b/mw/log/log_stream_factory.cpp @@ -0,0 +1,37 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/log_stream_factory.h" + +#include "platform/aas/mw/log/detail/thread_local_guard.h" +#include "platform/aas/mw/log/runtime.h" + +bmw::mw::log::LogStream bmw::mw::log::detail::LogStreamFactory::GetStream(const LogLevel log_level, + const amp::string_view context_id) noexcept +{ + if (not ThreadLocalGuard::IsWithingLogging()) + { + ThreadLocalGuard guard{}; + // Unnamed object ok, since it will be moved out of this function + // NOLINTNEXTLINE(bmw-no-unnamed-temporary-objects): See above + return bmw::mw::log::LogStream{Runtime::GetRecorder(), Runtime::GetFallbackRecorder(), log_level, context_id}; + } + else + { + // Unnamed object ok, since it will be moved out of this function + // NOLINTNEXTLINE(bmw-no-unnamed-temporary-objects): See above + return bmw::mw::log::LogStream{ + Runtime::GetFallbackRecorder(), Runtime::GetFallbackRecorder(), log_level, context_id}; + } +} diff --git a/mw/log/log_stream_factory.h b/mw/log/log_stream_factory.h new file mode 100644 index 0000000..9f7c16f --- /dev/null +++ b/mw/log/log_stream_factory.h @@ -0,0 +1,56 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_LOG_STREAM_FACTORY_H +#define PLATFORM_AAS_MW_LOG_LOG_STREAM_FACTORY_H + +#include "platform/aas/mw/log/log_level.h" +#include "platform/aas/mw/log/log_stream.h" + +#include "amp_string_view.hpp" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +class LogStreamFactory +{ + public: + /// \brief Will create a `LogStream` based on currently set `Recorder` in the runtime. + /// This function shall not be used be the end-user. Please refer only to APIs outside the `detail` + /// namespace. + /// + /// \details This factory is tested within the LogStream unit tests. If no valid recorder is set, this function will + /// abort! + /// + /// \param log_level The LogLevel the created stream shall use + /// \param context_id The context id the created stream shall use + /// \return A newly created LogStream which can be used to LogData + + + static LogStream GetStream(const LogLevel, const amp::string_view context_id = "DFLT") noexcept; + + +}; + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_LOG_STREAM_FACTORY_H diff --git a/mw/log/log_stream_test.cpp b/mw/log/log_stream_test.cpp new file mode 100644 index 0000000..e674ec4 --- /dev/null +++ b/mw/log/log_stream_test.cpp @@ -0,0 +1,1072 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/log_stream.h" + +#include "platform/aas/mw/log/log_stream_factory.h" +#include "platform/aas/mw/log/recorder_mock.h" +#include "platform/aas/mw/log/runtime.h" +#include "platform/aas/mw/log/test/my_custom_lib/my_custom_type_mw_log.h" +#include "platform/aas/mw/log/test/sample_ara_core_logging/sample_ara_core_logging.h" + +#include "amsr/generic/generic_error_domain.h" +#include "ara/core/string_view.h" + +#include "gtest/gtest.h" + +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace +{ + +using bmw::mw::log::bmw_ext::operator<<; +using ::testing::_; +using ::testing::Return; +using namespace std::chrono_literals; +using ::testing::Types; + +const SlotHandle HANDLE{42}; + +TEST(LogStream, CorrectlyHandleStartStop) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability to start and stop stream."); + RecordProperty("TestType", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + RecorderMock recorder_mock_{}; + detail::Runtime::SetRecorder(&recorder_mock_); + + EXPECT_CALL(recorder_mock_, StartRecord(amp::string_view{"Bar"}, LogLevel::kError)).WillOnce(Return(HANDLE)); + EXPECT_CALL(recorder_mock_, StopRecord(HANDLE)).Times(1); + + detail::LogStreamFactory::GetStream(LogLevel::kError, "Bar"); +} + +bool OtherFunctionThatLogs() +{ + detail::LogStreamFactory::GetStream(LogLevel::kError) << false; + return true; +} + +TEST(LogStream, CanLogRecursive) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies that logging recursively calls the normal recorder"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + RecorderMock recorder_mock_{}; + detail::Runtime::SetRecorder(&recorder_mock_); + + EXPECT_CALL(recorder_mock_, StartRecord(amp::string_view{"DFLT"}, LogLevel::kError)).WillRepeatedly(Return(HANDLE)); + EXPECT_CALL(recorder_mock_, StopRecord(HANDLE)).Times(2); + + // Expecting that we log twice via the normal recorder + EXPECT_CALL(recorder_mock_, LogBool(HANDLE, _)).Times(2); + + // When logging a value recursively + detail::LogStreamFactory::GetStream(LogLevel::kError) << OtherFunctionThatLogs(); +} + +using DurationTypes = ::testing::Types; + +template +class DurationTest : public testing::Test +{ + public: + ~DurationTest() {} + + public: + LogStream Unit() { return detail::LogStreamFactory::GetStream(LogLevel::kError); } + DurationTest() + { + EXPECT_CALL(recorder_mock_, StartRecord(amp::string_view{"DFLT"}, _)).WillOnce(Return(HANDLE)); + EXPECT_CALL(recorder_mock_, StopRecord(HANDLE)).Times(1); + + detail::Runtime::SetRecorder(&recorder_mock_); + } + + RecorderMock recorder_mock_{}; +}; + +TYPED_TEST_SUITE_P(DurationTest); + +TYPED_TEST_P(DurationTest, insertion_operator_chrono_duration) +{ + this->RecordProperty("ParentRequirement", ", "); + this->RecordProperty("ASIL", "B"); + this->RecordProperty("Description", "ara log shall be able to log chrono duration with unit suffix"); + this->RecordProperty("TestType", "Requirements-based test"); + this->RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // changing representation to double to prevent amiguity between int*, float and double types + using d_TypeParam = std::chrono::duration; + d_TypeParam d = std::chrono::duration_cast(1min); + this->Unit() << d; +} + +REGISTER_TYPED_TEST_SUITE_P(DurationTest, insertion_operator_chrono_duration); + +INSTANTIATE_TYPED_TEST_SUITE_P(BMW, DurationTest, DurationTypes, ); + +TEST(LogStream, WhenTryToGetStreamWithEmptyStringViewShallReturnDfltStream) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Getting stream with empty string view shall return the default context id."); + RecordProperty("TestType", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + RecorderMock recorder_mock_{}; + detail::Runtime::SetRecorder(&recorder_mock_); + + EXPECT_CALL(recorder_mock_, StartRecord(amp::string_view{"DFLT"}, LogLevel::kError)).WillOnce(Return(HANDLE)); + EXPECT_CALL(recorder_mock_, StopRecord(HANDLE)).Times(1); + + detail::LogStreamFactory::GetStream(LogLevel::kError, amp::string_view{}); +} + +TEST(LogStream, TypeSupport) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the support for logging various types."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + auto ensure_that_log_stream_reports_support_for = [](auto value) { + using ValueType = decltype(value); + EXPECT_TRUE(LogStreamSupports()) + << "LogStream is expected to support type `" << typeid(ValueType).name() << "`"; + EXPECT_TRUE(LogStreamSupports()) + << "LogStream is expected to support type `" << typeid(ValueType).name() << "&`"; + EXPECT_TRUE(LogStreamSupports()) + << "LogStream is expected to support type `" << typeid(ValueType).name() << "&&`"; + EXPECT_TRUE(LogStreamSupports()) + << "LogStream is expected to support type `const " << typeid(ValueType).name() << "`"; + EXPECT_TRUE(LogStreamSupports()) + << "LogStream is expected to support type `const " << typeid(ValueType).name() << "&`"; + EXPECT_TRUE(LogStreamSupports()) + << "LogStream is expected to support type `const " << typeid(ValueType).name() << "&&`"; + }; + + ensure_that_log_stream_reports_support_for(bool{}); + ensure_that_log_stream_reports_support_for(float{}); + ensure_that_log_stream_reports_support_for(double{}); + ensure_that_log_stream_reports_support_for(std::int8_t{}); + ensure_that_log_stream_reports_support_for(std::int16_t{}); + ensure_that_log_stream_reports_support_for(std::int32_t{}); + ensure_that_log_stream_reports_support_for(std::int64_t{}); + ensure_that_log_stream_reports_support_for(std::uint8_t{}); + ensure_that_log_stream_reports_support_for(std::uint16_t{}); + ensure_that_log_stream_reports_support_for(std::uint32_t{}); + ensure_that_log_stream_reports_support_for(std::uint64_t{}); + ensure_that_log_stream_reports_support_for(LogBin8{{}}); + ensure_that_log_stream_reports_support_for(LogBin16{{}}); + ensure_that_log_stream_reports_support_for(LogBin32{{}}); + ensure_that_log_stream_reports_support_for(LogBin64{{}}); + ensure_that_log_stream_reports_support_for(LogHex8{{}}); + ensure_that_log_stream_reports_support_for(LogHex16{{}}); + ensure_that_log_stream_reports_support_for(LogHex32{{}}); + ensure_that_log_stream_reports_support_for(LogHex64{{}}); + ensure_that_log_stream_reports_support_for(std::string{}); + ensure_that_log_stream_reports_support_for(amp::pmr::string{}); + ensure_that_log_stream_reports_support_for(amp::string_view{}); + ensure_that_log_stream_reports_support_for(std::string_view{}); + ensure_that_log_stream_reports_support_for(ara::core::StringView{}); + ensure_that_log_stream_reports_support_for(std::array{}); + ensure_that_log_stream_reports_support_for(std::array{}); + ensure_that_log_stream_reports_support_for(mw::log::LogString{nullptr, 0U}); + ensure_that_log_stream_reports_support_for(mw::log::LogRawBuffer{nullptr, 0U}); + ensure_that_log_stream_reports_support_for(mw::log::LogSlog2Message{0U, amp::string_view{}}); + + struct MyCustomType + { + }; + EXPECT_FALSE(LogStreamSupports()); + EXPECT_FALSE(LogStreamSupports()); + EXPECT_FALSE(LogStreamSupports()); + EXPECT_FALSE(LogStreamSupports()); + EXPECT_FALSE(LogStreamSupports()); + EXPECT_FALSE(LogStreamSupports()); +} + +class LogStreamFixture : public ::testing::Test +{ + public: + LogStream Unit() { return detail::LogStreamFactory::GetStream(LogLevel::kError); } + LogStreamFixture() + { + EXPECT_CALL(recorder_mock_, StartRecord(amp::string_view{"DFLT"}, _)).WillOnce(Return(HANDLE)); + EXPECT_CALL(recorder_mock_, StopRecord(HANDLE)).Times(1); + + detail::Runtime::SetRecorder(&recorder_mock_); + } + + RecorderMock recorder_mock_{}; +}; + +TEST_F(LogStreamFixture, CanLogBool) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging boolean value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + auto constexpr value = true; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogBool(HANDLE, value)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogUint8) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging unsigned int8 value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + auto constexpr value = std::uint8_t{5}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogUint8(HANDLE, value)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogInt8) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging int8 value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + auto constexpr value = std::int8_t{5}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogInt8(HANDLE, value)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogUint16) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging unsigned int16 value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + auto constexpr value = std::uint16_t{5}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogUint16(HANDLE, value)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogInt16) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging int16 value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + auto constexpr value = std::int16_t{5}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogInt16(HANDLE, value)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogUint32) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging unsigned int32 value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + auto constexpr value = std::uint32_t{5}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogUint32(HANDLE, value)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogInt32) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging int32 value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + auto constexpr value = std::int32_t{5}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogInt32(HANDLE, value)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogUint64) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging unsigned int64 value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + auto constexpr value = std::uint64_t{5}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogUint64(HANDLE, value)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogInt64) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging int64 value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + auto constexpr value = std::int64_t{5}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogInt64(HANDLE, value)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogFloat) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging float value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + auto constexpr value = float{5.2F}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogFloat(HANDLE, value)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogDouble) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging double value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + auto constexpr value = double{5.2}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogDouble(HANDLE, value)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogAraStringView) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging ara::core::StringView value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + auto value = ara::core::StringView{"Foo"}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogStringView(HANDLE, amp::string_view{"Foo"})).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogAmpStringView) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging amp::string_view value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + auto value = amp::string_view{"Foo"}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogStringView(HANDLE, amp::string_view{"Foo"})).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogStdStringView) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging std::string_view value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + auto value = amp::string_view{"Foo"}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogStringView(HANDLE, amp::string_view{"Foo"})).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, WhenTryToLogEmptyAraStringViewShallNotLog) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies that in case of empty ara::core::StringView we shall expect no logs."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + auto value = ara::core::StringView{}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogStringView).Times(0); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, WhenTryToLogEmptyAmpStringViewShallNotLog) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies that in case of empty amp::string_view we shall expect no logs."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + auto value = amp::string_view{}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogStringView).Times(0); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, WhenTryToLogEmptyStdStringViewShallNotLog) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies that in case of empty std::string_view we shall expect no logs."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + auto value = std::string_view{}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogStringView).Times(0); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogConstStringReference) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging const string reference."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + const auto value = std::string{"Foo"}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogStringView(HANDLE, amp::string_view{value})).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogStdArrayOfChar) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging std::array value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Expecting that the expected value will be transferred to the correct log call + ::testing::InSequence in_sequence{}; + EXPECT_CALL(recorder_mock_, LogStringView(HANDLE, amp::string_view{"Test"})).Times(1); + EXPECT_CALL(recorder_mock_, LogStringView(HANDLE, amp::string_view{"Twice"})).Times(1); + + // When logging the value twice + Unit() << std::array{'T', 'e', 's', 't'} << std::array{'T', 'w', 'i', 'c', 'e'}; +} + +TEST_F(LogStreamFixture, CanLogCharArrayLiteral) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging char[] literal value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Expecting that the expected value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogStringView(HANDLE, amp::string_view{__func__})).Times(2); + + // When logging the value once directly and once via convenience method (which avoids array-to-pointer decay) + Unit() << __func__ << mw::log::LogStr(__func__); +} + +TEST_F(LogStreamFixture, CanLogPtrToNonConstChar) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging pointer to non-const char."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + std::array value{'F', 'o', 'o', '\0'}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogStringView(HANDLE, amp::string_view{value.data(), 3})).Times(1); + + // When logging the value + Unit() << value.data(); +} + +TEST_F(LogStreamFixture, CanLogStringLiteral) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging string literal value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + LogString::CharPtr value = "Foo"; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogStringView(HANDLE, amp::string_view{value})).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, WhenTryToLogEmptyStringLiteralShallNotLog) +{ + RecordProperty("ParentRequirement", ", "); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies that in case of empty string literal we shall expect no logs."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Given a value we want to log + LogString::CharPtr value = nullptr; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogStringView).Times(0); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, LogStreamMoveConstructorShallDetachMovedFromInstance) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of log value using a moved LogStream instance."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // When logging the value + auto log_stream_moved_from = Unit(); + auto log_stream_move_constructed = std::move(log_stream_moved_from); + + // Given a value we want to log + auto value = "Foo"; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogStringView(HANDLE, amp::string_view{value})).Times(1); + + log_stream_move_constructed << value; + // The fixture shall ensure that the StopRecord is only called once. +} + +TEST_F(LogStreamFixture, CanLogHex8) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging unsigned int8 in hexdecimal representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given a value we want to log + const LogHex8 value{0xFF}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogUint8(HANDLE, value.value)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogHex16) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging unsigned int16 in hexdecimal representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given a value we want to log + const LogHex16 value{0xFFFF}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogUint16(HANDLE, value.value)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogHex32) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging unsigned int32 in hexdecimal representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given a value we want to log + const LogHex32 value{0xFFFFFF}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogUint32(HANDLE, value.value)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogHex64) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging unsigned int64 in hexdecimal representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given a value we want to log + const LogHex64 value{0xFFFFFFFF}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogUint64(HANDLE, value.value)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogBin8) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging unsigned int8 in binary representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given a value we want to log + const LogBin8 value{0xFF}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogUint8(HANDLE, value.value)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogBin16) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging unsigned int16 in binary representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given a value we want to log + const LogBin16 value{0xFFFF}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogUint16(HANDLE, value.value)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogBin32) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging unsigned int32 in binary representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given a value we want to log + const LogBin32 value{0xFFFFFF}; + + EXPECT_CALL(recorder_mock_, LogUint32(HANDLE, value.value)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogBin64) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging unsigned int64 in binary representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given a value we want to log + const LogBin64 value{0xFFFFFFFF}; + + EXPECT_CALL(recorder_mock_, LogUint64(HANDLE, value.value)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogRawBuffer) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging LogRawBuffer."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given a value we want to log + char s2[]{"1234"}; + const LogRawBuffer value{s2, sizeof(s2)}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, Log_LogRawBuffer(HANDLE, value.data(), static_cast(value.size()))).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, WhenTryToLogEmptyRawBufferShallNotLog) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Logging empty LogRawBuffer shall not log."); + RecordProperty("TestType", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given a value we want to log + const LogRawBuffer value{nullptr, 1}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, Log_LogRawBuffer(HANDLE, value.data(), static_cast(value.size()))).Times(0); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogACustomType) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the ability of logging custom type (struct)."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + ::testing::InSequence in_sequence{}; + + const auto operator_string0 = "my_custom_type: int_field : "; + const auto operator_string1 = " , string_field : "; + // Given a custom object we want to log + const my::custom::type::MyCustomType value{12, "hello, world"}; + + // Expecting that the custom output operator overload will be called which will be transfer the contained values to + // the correct log calls + EXPECT_CALL(recorder_mock_, LogStringView(HANDLE, amp::string_view{operator_string0})).Times(1); + EXPECT_CALL(recorder_mock_, LogInt32(HANDLE, value.int_field)).Times(1); + EXPECT_CALL(recorder_mock_, LogStringView(HANDLE, amp::string_view{operator_string1})).Times(1); + EXPECT_CALL(recorder_mock_, + LogStringView(HANDLE, amp::string_view{value.string_field.c_str(), value.string_field.size()})) + .Times(1); + + // When logging the value + Unit() << value; +} + +TEST(LogStreamFlush, WhenFlushingLogStreamAfterLogUint8ShallBeAbleToLogBoolAgain) +{ + testing::InSequence seq; + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies teh ability of flushing LogStream used to log unsigned int8 and then use it again to log " + "boolean value."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given a value we want to log + constexpr auto value_uint8 = std::uint8_t{5}; + constexpr auto value_bool = true; + + RecorderMock recorder_mock_{}; + detail::Runtime::SetRecorder(&recorder_mock_); + + EXPECT_CALL(recorder_mock_, StartRecord(amp::string_view{"DFLT"}, _)).WillOnce(Return(HANDLE)); + + auto log_stream{detail::LogStreamFactory::GetStream(LogLevel::kError, amp::string_view("DFLT"))}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogUint8(HANDLE, value_uint8)).Times(1); + log_stream << value_uint8; + + EXPECT_CALL(recorder_mock_, StopRecord(HANDLE)).Times(1); + EXPECT_CALL(recorder_mock_, StartRecord(amp::string_view{"DFLT"}, _)).WillOnce(Return(HANDLE)); + + log_stream.Flush(); + + EXPECT_CALL(recorder_mock_, LogBool(HANDLE, value_bool)).Times(1); + log_stream << value_bool; + + EXPECT_CALL(recorder_mock_, StopRecord(HANDLE)).Times(1); +} + +TEST(LogStreamFlush, AvoidFormattingCallsWhenSlotIsNotAvailable) +{ + testing::InSequence seq; + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies the inability of logging formatting functions if no slots are reserved."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given a value we want to log + constexpr auto value_uint8 = std::uint8_t{5}; + constexpr auto value_bool = true; + + RecorderMock recorder_mock_{}; + detail::Runtime::SetRecorder(&recorder_mock_); + + EXPECT_CALL(recorder_mock_, StartRecord(amp::string_view{"DFLT"}, _)).WillOnce(Return(amp::nullopt)); + + auto log_stream{detail::LogStreamFactory::GetStream(LogLevel::kError, amp::string_view("DFLT"))}; + + // Without slot available formatting function shall not be called: + EXPECT_CALL(recorder_mock_, LogUint8(HANDLE, value_uint8)).Times(0); + log_stream << value_uint8; + + EXPECT_CALL(recorder_mock_, StopRecord(HANDLE)).Times(0); + EXPECT_CALL(recorder_mock_, StartRecord(amp::string_view{"DFLT"}, _)).WillOnce(Return(amp::nullopt)); + + log_stream.Flush(); + + // Without slot available formatting function shall not be called: + EXPECT_CALL(recorder_mock_, LogBool(HANDLE, value_bool)).Times(0); + log_stream << value_bool; + + // Nor is StopRecord function expected to be called + EXPECT_CALL(recorder_mock_, StopRecord(HANDLE)).Times(0); +} + +TEST(LogStreamFlush, WhenEmptyAppIdStringProvidedExpectDefaultOneReturned) +{ + testing::InSequence seq; + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "When empty app id is provided the default context id shall be returned."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Given a value we want to log + constexpr auto value_uint8 = std::uint8_t{5}; + + RecorderMock recorder_mock_{}; + detail::Runtime::SetRecorder(&recorder_mock_); + + EXPECT_CALL(recorder_mock_, StartRecord(amp::string_view{"DFLT"}, _)).WillOnce(Return(HANDLE)); + + // Here we provide empty AppId string: + auto log_stream{detail::LogStreamFactory::GetStream(LogLevel::kError, amp::string_view(nullptr, 0UL))}; + + // Standard formatting function shall be called: + EXPECT_CALL(recorder_mock_, LogUint8(HANDLE, value_uint8)); + log_stream << value_uint8; + + EXPECT_CALL(recorder_mock_, StopRecord(HANDLE)); +} + +enum class UColor : std::uint64_t +{ + red = 100U, + green = 200U, + blue = 300U +}; +enum class IColor : std::uint16_t +{ + red = 400, + green = 500, + blue = 600 +}; + +TEST_F(LogStreamFixture, CanLogAnEnumClassWithUnderlyingType) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging an enum class with underlying type (uint)."); + RecordProperty("TestingTechnique", "Internal-implementation-based test"); + + UColor value{UColor::green}; + + EXPECT_CALL(recorder_mock_, LogUint64(HANDLE, _)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogAnEnumClassWithoutUnderlyingType) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verify the ability of logging an enum class without underlying type (directly using int)."); + RecordProperty("TestingTechnique", "Internal-implementation-based test"); + + IColor value{IColor::green}; + + EXPECT_CALL(recorder_mock_, LogInt32(HANDLE, _)).Times(1); + + // When logging the value + Unit() << value; +} + +TEST_F(LogStreamFixture, CanLogAraCoreResultWithValue) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging ara::core::Result with int32 value."); + RecordProperty("TestingTechnique", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + ::testing::InSequence in_sequence{}; + + const auto operator_string = "Result value: "; + + // Given a result with value we want to log + const ara::core::Result result{123}; + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogStringView(HANDLE, amp::string_view{operator_string})).Times(1); + EXPECT_CALL(recorder_mock_, LogInt32(HANDLE, result.Value())).Times(1); + + // When logging the value + Unit() << result; +} + +TEST_F(LogStreamFixture, CanLogAraCoreResultWithError) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging ara::core::Result with error value."); + RecordProperty("TestingTechnique", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + ::testing::InSequence in_sequence{}; + + const auto operator_string = "Error message: "; + + // Given a result with value we want to log + const ara::core::Result result = + ara::core::Result::FromError(amsr::generic::GenErrc::kSystemPrivileges); + + // Expecting that this value will be transferred to the correct log call + EXPECT_CALL(recorder_mock_, LogStringView(HANDLE, amp::string_view{operator_string})).Times(1); + + const auto& msg = result.Error().UserMessage(); + amp::string_view expected_msg; + if (msg.size()) + { + expected_msg = {msg.data(), msg.size()}; + } + else + { + expected_msg = "{EMPTY}"; + } + EXPECT_CALL(recorder_mock_, LogStringView(HANDLE, expected_msg)).Times(1); + + // When logging the value + Unit() << result; +} + +TEST_F(LogStreamFixture, UsesFallbackRecorderWithinOtherRecorder) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies that the fallback recorder is used, if another recorder also uses logging"); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Expecting that we log once via the normal recorder + EXPECT_CALL(recorder_mock_, LogBool(HANDLE, true)).WillOnce([this](auto, auto logged_value) { + EXPECT_TRUE(logged_value); + + // When logging within a recorder + Unit() << false; + }); + + // When logging a value + Unit() << true; + + // Then the recorder_mock is only invoked once, not multiple times. +} + +} // namespace +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/log_types.h b/mw/log/log_types.h new file mode 100644 index 0000000..2c3eb59 --- /dev/null +++ b/mw/log/log_types.h @@ -0,0 +1,316 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#ifndef PLATFORM_AAS_MW_LOG_TYPES_H +#define PLATFORM_AAS_MW_LOG_TYPES_H + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +/// \brief Helper type to log an uint8 in hexadecimal representation. +/// \public +struct LogHex8 +{ + std::uint8_t value; +}; + +/// \brief Helper type to log an uint16 in hexadecimal representation. +/// \public +struct LogHex16 +{ + std::uint16_t value; +}; + +/// \brief Helper type to log an uint32 in hexadecimal representation. +/// \public +struct LogHex32 +{ + std::uint32_t value; +}; + +/// \brief Helper type to log an uint64 in hexadecimal representation. +/// \public +struct LogHex64 +{ + std::uint64_t value; +}; + +/// \brief Helper type to log an uint8 in binary representation. +/// \public +struct LogBin8 +{ + std::uint8_t value; +}; + +/// \brief Helper type to log an uint16 in binary representation. +/// \public +struct LogBin16 +{ + std::uint16_t value; +}; + +/// \brief Helper type to log an uint32 in binary representation. +/// \public +struct LogBin32 +{ + std::uint32_t value; +}; + +/// \brief Helper type to log an uint64 in binary representation. +/// \public +struct LogBin64 +{ + std::uint64_t value; +}; + +namespace detail +{ +/// @brief Used to obtain the iterator type of the type `Range`. +template +using IteratorType = decltype(std::begin(std::declval())); + +template ()), typename = R&> +using IteratorReferenceHelper = R; + +/// @brief Determines the reference type of `Iterator`. +template +using IteratorReferenceType = IteratorReferenceHelper; + +/// @brief Determines whether type `RangeType` is a range of `ElementType` and not an amp::span or array of such type. +template +struct IsNonSpanNonArrayRange : std::false_type +{ +}; +template +struct IsNonSpanNonArrayRange< + RangeType, + ElementType, + std::enable_if_t< + amp::is_iterable::value && not(amp::is_span::value) && + not(std::is_array::value) && + std::is_same>>>, + ElementType>::value>> : std::true_type +{ +}; +} // namespace detail + +/// \brief Helper type serving as view over string-like types. +/// \public +class LogString +{ + public: + using TraitsType = amp::string_view::traits_type; + using CharType = amp::string_view::value_type; + using CharPtr = std::add_pointer_t>; + + /// \brief Constructs `LogString` as view over character \p range;. + /// + /// This overload participates in overload resolution only if type `RangeType` is iterable via (const) `CharType` + /// and is not an amp::span or an array of type `CharType`. + /// + template , + std::enable_if_t::value, bool> = true> + // NOLINTNEXTLINE(google-explicit-constructor): intended here to allow implicit conversions from range of `CharType` + constexpr LogString(RangeType&& range) noexcept(noexcept(std::data(range)) && noexcept(std::size(range))) + : data_{std::data(range)}, size_{std::size(range)} + { + } + + /// \brief Constructs `LogString` as view over character \p range;. + /// + /// This overload participates in overload resolution only if type `RangeType` is iterable via (const) `CharType` + /// and is not an amp::span or an array of type `CharType`. + /// + template , + std::enable_if_t::value, bool> = true> + // NOLINTBEGIN(cppcoreguidelines-pro-type-member-init): false positive, this constructor delegates to another one + // NOLINTNEXTLINE(google-explicit-constructor): intended here to allow implicit conversions from range of `CharType` + constexpr LogString(std::reference_wrapper range_wrapper) noexcept(noexcept(LogString{ + range_wrapper.get()})) + : LogString(range_wrapper.get()) + // NOLINTEND(cppcoreguidelines-pro-type-member-init): see justification above + { + } + + /// \brief Constructs `LogString` as view over a bounded character array. + template + // NOLINTNEXTLINE(google-explicit-constructor, modernize-avoid-c-arrays): allow implicit conversion from char array + constexpr LogString(const CharType (&array)[N]) noexcept : data_{amp::data(array)}, size_{N - 1U} + { + static_assert(N > 0U, "character array must have at least 1 element"); + EnsureIsNullCharacter(array[N - 1U]); + } + + /// \brief Constructs `LogString` as view over a character sequence pointed-to via `str`. + constexpr LogString(CharPtr str, const std::size_t size) noexcept : data_{str}, size_{size} {} + + constexpr auto* Data() const noexcept { return data_; } + constexpr auto Size() const noexcept { return size_; } + + private: + /// \brief Performs assertion check whether a character value matches the null-character. + constexpr static void EnsureIsNullCharacter(const CharType character) noexcept + { + AMP_ASSERT_PRD_MESSAGE(character == '\0', "character array must be null-terminated"); + } + + CharPtr data_; + std::size_t size_; +}; + +/// \brief Helper type to log string and forward slog2 code. +/// \brief See QNX slog2f documentation. +/// \public +class LogSlog2Message +{ + public: + LogSlog2Message() = delete; + LogSlog2Message(const std::uint16_t code, const amp::string_view message) : slog_code_(code), message_(message) {} + + std::uint16_t GetCode() const { return slog_code_; } + + amp::string_view GetMessage() const { return message_; } + + private: + std::uint16_t slog_code_; + amp::string_view message_; +}; + +/// \brief Convenience method for logging character array +/// \public +/// \note Helps for example to avoid array-to-pointer decay when logging char[] literals or macros (such as __func__). +template +[[deprecated( + "SPP_DEPRECATION: Making use of `mw::log::LogStr()` is no longer required since `mw::log::LogStream` " + "meanwhile supports logging string literals natively via its `operator<<`.")]] constexpr LogString +// NOLINTNEXTLINE(modernize-avoid-c-arrays): required for avoiding implicit array-to-pointer decay in case of char array +LogStr(const LogString::CharType (&array)[N]) noexcept +{ + const LogString result{std::forward(array)}; + return result; +} + +/// \brief Helper type to log the raw bytes of a buffer +/// \public +/// \note Maximum supported size for DLT output is less than 64 KB. Bytes exceeding that limit will be cropped. +/// \note Recommended to split the output in chunks of 1400 Bytes to avoid IP fragmentation DLT packets. +using LogRawBuffer = + amp::span; + +/// \brief Create a LogRawBuffer from a scalar or array-of-scalars type instance +/// +/// \tparam T Type of the data referenced by the value to be dumped. +/// \param values The value that shall be dumped. +/// \return An instance of LogRawBuffer, whose lifetime is limited by that of the given input reference. +template ::value>> +inline LogRawBuffer MakeLogRawBuffer(const T& value) noexcept +{ + // ----- COMMON_ARGUMENTATION ---- + // We cast to a char to print the byte representation of the given span. Since we know that: + // - we do not violate the strict aliasing rules and + // - we do not try to interpret or modify the data + // the cast is acceptable. + // -------------------------------- + + + // COMMON_ARGUMENTATION. + // + return {reinterpret_cast( // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) COMMON_ARGUMENTATION + &value), + static_cast(sizeof(T))}; + +} + +/// \brief Create a LogRawBuffer from an amp::span referencing an array of scalars. +/// +/// \tparam T Type of the data referenced by the amp::span. Must be a scalar. +/// \param values The span referencing the array. +/// \return An instance of LogRawBuffer, whose lifetime is limited by that of the given input span. +template +inline LogRawBuffer MakeLogRawBuffer(const amp::span values) noexcept +{ + constexpr const bool IsEligible = std::is_scalar::value; + static_assert(IsEligible, "Only scalar types are allowed for dumping for now."); + + using SpanSizeType = typename amp::span::size_type; + + AMP_PRECONDITION_MESSAGE(values.size() >= SpanSizeType{0}, "amp::span with negative size refused."); + + + // ----- COMMON_ARGUMENTATION ---- + // We cast to a char to print the byte representation of the given span. Since we know that: + // - the memory is contiguous and + // - we do not violate the strict aliasing rules and + // - we do not try to interpret or modify the data + // the cast is acceptable. + // ------------------------------- + + + + /* other variants of the code would also require suppressions or were more complicated and not well-maintainable */ + const auto buffer_size{static_cast(static_cast(values.size()) * sizeof(T))}; + // COMMON_ARGUMENTATION. + // + return {reinterpret_cast( // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) COMMON_ARGUMENTATION + values.data()), + buffer_size}; + + +} + +/// \brief Create a LogRawBuffer from an std::vector referencing an array of scalars. +/// +/// \tparam T Type of the data contained in the std::vector. Must be a scalar. +/// \param values The vector of elements. +/// \return An instance of LogRawBuffer, whose lifetime is limited by that of the given input vector. +template +inline LogRawBuffer MakeLogRawBuffer(const std::vector& values) noexcept +{ + return MakeLogRawBuffer( + amp::span(values.data(), static_cast::size_type>(values.size()))); +} + +/// \brief Create a LogRawBuffer from an std::array consisting of scalars. +/// +/// \tparam T Type of the data contained in the std::array. Must be a scalar. +/// \param values The array of elements. +/// \return An instance of LogRawBuffer, whose lifetime is limited by that of the given input array. +template +inline LogRawBuffer MakeLogRawBuffer(const std::array& values) noexcept +{ + return MakeLogRawBuffer(amp::span(values.data(), static_cast::size_type>(N))); +} + +} // namespace log +} // namespace mw +} // namespace bmw + +#endif diff --git a/mw/log/log_types_test.cpp b/mw/log/log_types_test.cpp new file mode 100644 index 0000000..c2b698b --- /dev/null +++ b/mw/log/log_types_test.cpp @@ -0,0 +1,207 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/log_types.h" + +#include "ara/core/string.h" +#include "ara/core/string_view.h" + +#include + +#include + +#include +#include +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +namespace +{ + +TEST(LogStringTest, ConstructFromCharArray) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the detection of null-/non-null-terminated char array"); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // When constructing our LogString type from an empty char[] literal + const auto empty_log_str = mw::log::LogStr(""); + const auto empty_str = LogString{""}; + + // Then it must work appropriately + EXPECT_STREQ("", empty_log_str.Data()); + EXPECT_STREQ("", empty_str.Data()); + EXPECT_EQ(0U, empty_log_str.Size()); + EXPECT_EQ(0U, empty_str.Size()); + + // When constructing our LogString type from nullptr + const auto null_str = LogString{nullptr, 0U}; + + // Then it must work appropriately + EXPECT_EQ(nullptr, null_str.Data()); + EXPECT_EQ(0U, null_str.Size()); + + // Given a null-terminated array of characters + const char null_terminated[] = {'M', 'y', 'A', 'r', 'r', 'a', 'y', '\0'}; + + // When constructing our LogString type using that one + const auto log_str = mw::log::LogStr(null_terminated); + const auto str = LogString{null_terminated}; + + // Then it must work appropriately + EXPECT_STREQ("MyArray", log_str.Data()); + EXPECT_STREQ("MyArray", str.Data()); + EXPECT_EQ(7U, log_str.Size()); + EXPECT_EQ(7U, str.Size()); + + // Given a an array of characters which is not null-terminated at the end + const char non_null_terminated[] = {'M', 'y', '\0', 'A', 'r', 'r', 'a', 'y'}; + + // When constructing our LogString type using that one + const auto test_implicit_conversion_from = [](LogString param) { amp::ignore = param; }; + + // Then immediate terminating is expected + EXPECT_DEATH(test_implicit_conversion_from(non_null_terminated), ".*"); + EXPECT_DEATH(mw::log::LogStr(non_null_terminated), ".*"); +} + +TEST(LogStringTest, CanImplicitlyConvertFromStringLikeTypes) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of creating our LogString type view from string-like types"); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + constexpr auto kExpected = "MyString"; + + const auto test_implicit_conversion_from = [](LogString log_str) { EXPECT_STREQ(log_str.Data(), kExpected); }; + + const auto test = [&test_implicit_conversion_from](auto str) { + const auto& ref = str; + test_implicit_conversion_from(ref); + + std::reference_wrapper wrapper{ref}; + test_implicit_conversion_from(wrapper); + + test_implicit_conversion_from(str); + test_implicit_conversion_from(std::move(str)); + }; + + test_implicit_conversion_from("MyString"); + test(ara::core::StringView{kExpected}); + test(ara::core::String{kExpected}); + test(amp::pmr::string{kExpected}); + test(amp::string_view{kExpected}); + test(std::string_view{kExpected}); + test(std::string{kExpected}); +} + +TEST(MakeLogRawBufferTest, MakeBufferFromInteger) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of creating a buffer from integer"); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + const std::int32_t value{15}; + LogRawBuffer log_raw_buffer{MakeLogRawBuffer(value)}; + ASSERT_EQ(log_raw_buffer.size(), sizeof(value)); +#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + constexpr const char expected[]{0, 0, 0, 15}; +#else + constexpr const char expected[]{15, 0, 0, 0}; +#endif + EXPECT_TRUE(std::equal(log_raw_buffer.cbegin(), log_raw_buffer.cend(), std::begin(expected))); +} + +TEST(MakeLogRawBufferTest, MakeBufferFromIntegerStdArray) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of creating a buffer from array of integers."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + const std::array values{15, 16}; + LogRawBuffer log_raw_buffer{MakeLogRawBuffer(values)}; + ASSERT_EQ(log_raw_buffer.size(), sizeof(values)); +#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + constexpr const char expected[]{0, 0, 0, 15, 0, 0, 0, 16}; +#else + constexpr const char expected[]{15, 0, 0, 0, 16, 0, 0, 0}; +#endif + EXPECT_TRUE(std::equal(log_raw_buffer.cbegin(), log_raw_buffer.cend(), std::begin(expected))); +} + +TEST(MakeLogRawBufferTest, MakeBufferFromSpan) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of creating a buffer from amp::span."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + const std::int32_t values[]{15, 16}; + const amp::span span{values, 2}; + LogRawBuffer log_raw_buffer{MakeLogRawBuffer(span)}; + ASSERT_EQ(log_raw_buffer.size(), sizeof(values)); +#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + constexpr const char expected[]{0, 0, 0, 15, 0, 0, 0, 16}; +#else + constexpr const char expected[]{15, 0, 0, 0, 16, 0, 0, 0}; +#endif + EXPECT_TRUE(std::equal(log_raw_buffer.cbegin(), log_raw_buffer.cend(), std::begin(expected))); +} + +TEST(MakeLogRawBufferTest, MakeBufferFromVector) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of creating a buffer from vector of integers"); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + const std::vector values{15, 16}; + LogRawBuffer log_raw_buffer{MakeLogRawBuffer(values)}; + ASSERT_EQ(log_raw_buffer.size(), sizeof(std::int32_t) * values.size()); +#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + constexpr const char expected[]{0, 0, 0, 15, 0, 0, 0, 16}; +#else + constexpr const char expected[]{15, 0, 0, 0, 16, 0, 0, 0}; +#endif + EXPECT_TRUE(std::equal(log_raw_buffer.cbegin(), log_raw_buffer.cend(), std::begin(expected))); +} + +TEST(MakeLogRawBufferTest, DieOnNegativeSpanSize) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the in-ability of creating a negative sized span."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + const std::int32_t values[]{15, 16}; + const amp::span span{values, -2}; + EXPECT_DEATH(MakeLogRawBuffer(span), ".*"); +} + +} // namespace + +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/logger.cpp b/mw/log/logger.cpp new file mode 100644 index 0000000..224ab62 --- /dev/null +++ b/mw/log/logger.cpp @@ -0,0 +1,111 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/logger.h" + +#include "platform/aas/mw/log/log_stream_factory.h" +#include "platform/aas/mw/log/runtime.h" + +namespace +{ +const std::string kDefaultContext = "DFLT"; +} + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +Logger::Logger(const amp::string_view context) noexcept + : context_(context.data() == nullptr ? GetDefaultContextId() : context) +{ +} + + + +log::LogStream Logger::LogFatal() const noexcept +{ + return bmw::mw::log::detail::LogStreamFactory::GetStream(LogLevel::kFatal, context_.GetStringView()); +} + +log::LogStream Logger::LogError() const noexcept +{ + return bmw::mw::log::detail::LogStreamFactory::GetStream(LogLevel::kError, context_.GetStringView()); +} + +log::LogStream Logger::LogWarn() const noexcept +{ + return bmw::mw::log::detail::LogStreamFactory::GetStream(LogLevel::kWarn, context_.GetStringView()); +} + +log::LogStream Logger::LogInfo() const noexcept +{ + return bmw::mw::log::detail::LogStreamFactory::GetStream(LogLevel::kInfo, context_.GetStringView()); +} + +log::LogStream Logger::LogDebug() const noexcept +{ + return bmw::mw::log::detail::LogStreamFactory::GetStream(LogLevel::kDebug, context_.GetStringView()); +} + +log::LogStream Logger::LogVerbose() const noexcept +{ + return bmw::mw::log::detail::LogStreamFactory::GetStream(LogLevel::kVerbose, context_.GetStringView()); +} + +log::LogStream Logger::WithLevel( + const LogLevel log_level) const noexcept +{ + return bmw::mw::log::detail::LogStreamFactory::GetStream(log_level, context_.GetStringView()); +} + +bool Logger::IsLogEnabled(const LogLevel log_level) const noexcept +{ + return IsEnabled(log_level); +} + +bool Logger::IsEnabled(const LogLevel log_level) const noexcept +{ + return bmw::mw::log::detail::Runtime::GetRecorder().IsLogEnabled(log_level, context_.GetStringView()); +} + + +amp::string_view Logger::GetContext() const noexcept +{ + return context_.GetStringView(); +} + + + + +bmw::mw::log::Logger& CreateLogger(const amp::string_view context) noexcept +{ + return bmw::mw::log::detail::Runtime::GetLoggerContainer().GetLogger(context); +} + +bmw::mw::log::Logger& CreateLogger(const amp::string_view context_id, const amp::string_view) noexcept +{ + return CreateLogger(context_id); +} + +const std::string& GetDefaultContextId() noexcept +{ + return kDefaultContext; +} + +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/logger.h b/mw/log/logger.h new file mode 100644 index 0000000..f5ccdf5 --- /dev/null +++ b/mw/log/logger.h @@ -0,0 +1,144 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_LOGGER_H +#define PLATFORM_AAS_MW_LOG_LOGGER_H + +#include "platform/aas/mw/log/log_stream.h" + +#include + +#include "platform/aas/mw/log/detail/logging_identifier.h" + +namespace bmw + +{ +namespace mw +{ +namespace log +{ + + + + +/// \brief The logger creates LogStreams with a user-defined context. +/// Implicitly generated assignment operators are NOT thread-safe. +class Logger final +{ + + + public: + /// \brief Constructs a Logger object with a given context. All subsequent calls to a log statement will be logged + /// under the provided context. + /// \public + /// \thread-safe + explicit Logger(const amp::string_view context) noexcept; + + + /* Reusing identifiers allowed to keep interface consistent betweeen the member functions in this class with the + * free functions defined in logging.h. */ + /// \brief Creates a LogStream to log messages of criticality `Fatal` (highest). + /// \public + /// \thread-safe + /// + /// \details Fatal shall be used on errors that cannot be recovered and will lead to an overall failure in the + /// system. The message will be logged under the context that was provided on construction. \return LogStream which + /// can be used stream verbose logging messages (will be flushed on destruction) + log::LogStream LogFatal() const noexcept; + + /// \brief Creates a LogStream to log messages of criticality `Error` (2nd highest). + /// \public + /// \thread-safe + /// + /// \details Error shall be used on errors that can be recovered and would lead to an failure in the system. + /// The message will be logged under the context that was provided on construction. + /// \return LogStream which can be used stream verbose logging messages (will be flushed on destruction) + log::LogStream LogError() const noexcept; + + /// \brief Creates a LogStream to log messages of criticality `Warn` (3rd highest). + /// \public + /// \thread-safe + /// + /// \details Warnings shall be used on errors that might be no error but desired state or could to an error later + /// on. The message will be logged under the context that was provided on construction. \return LogStream which can + /// be used stream verbose logging messages (will be flushed on destruction) + log::LogStream LogWarn() const noexcept; + + /// \brief Creates a LogStream to log messages of criticality `Info` (4th highest). + /// \public + /// \thread-safe + /// + /// \details Infos shall be used on messages that are of interest to analyze issues and understand overall program + /// flow. The message will be logged under the context that was provided on construction. \return LogStream which + /// can be used stream verbose logging messages (will be flushed on destruction) + log::LogStream LogInfo() const noexcept; + + /// \brief Creates a LogStream to log messages of criticality `Debug` (5th highest). + /// \public + /// \thread-safe + /// + /// \details Debug shall be used on messages that are of interest to analyze issues in depth. + /// The message will be logged under the context that was provided on construction. + /// \return LogStream which can be used stream verbose logging messages (will be flushed on destruction) + log::LogStream LogDebug() const noexcept; + + /// \brief Creates a LogStream to log messages of criticality `Verbose` (lowest). + /// \public + /// \thread-safe + /// + /// \details Verbose shall be used on messages that are of interest to analyze issues in depth but lead to super + /// high bandwidth (e.g. sending every millisecond). The message will be logged under the context that was provided + /// on construction. + /// \return LogStream which can be used stream verbose logging messages (will be flushed on destruction) + log::LogStream LogVerbose() const noexcept; + + /// \brief Log a message where the log level is determined by an agurment. + /// \public + /// \thread-safe + /// \details See also AUTOSAR_SWS_LogAndTrace R20-11, Section 8.3.2.8 + log::LogStream WithLevel(const LogLevel log_level) const noexcept; + + /// \brief Check if the log level is enabled for the current context. + /// \public + /// \thread-safe + /// \details See also AUTOSAR_SWS_LogAndTrace R20-11, Section 8.3.2.7 + bool IsLogEnabled(const LogLevel) const noexcept; + + /// \brief Check if the log level is enabled for the current context. + /// \public + /// \thread-safe + /// \details See also AUTOSAR_SWS_LogAndTrace R20-11, Section 8.3.2.7 + bool IsEnabled(const LogLevel) const noexcept; + + + + amp::string_view GetContext() const noexcept; + + + private: + + + detail::LoggingIdentifier context_; + +}; + +bmw::mw::log::Logger& CreateLogger(const amp::string_view context) noexcept; +bmw::mw::log::Logger& CreateLogger(const amp::string_view context_id, const amp::string_view) noexcept; +const std::string& GetDefaultContextId() noexcept; + +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_LOGGER_H diff --git a/mw/log/logger_container.cpp b/mw/log/logger_container.cpp new file mode 100644 index 0000000..7e41392 --- /dev/null +++ b/mw/log/logger_container.cpp @@ -0,0 +1,80 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/logger_container.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +namespace +{ +constexpr std::size_t kMaxLoggersSize{32U}; +} + +LoggerContainer::LoggerContainer() : stack_{kMaxLoggersSize}, default_logger_{bmw::mw::log::GetDefaultContextId()} {} + +Logger& LoggerContainer::GetLogger(const amp::string_view context) noexcept +{ + auto logger = FindExistingLogger(context); + + if (logger.has_value()) + { + return logger.value(); + } + + return InsertNewLogger(context); + +} + +Logger& LoggerContainer::InsertNewLogger(const amp::string_view context) noexcept +{ + const auto result = stack_.TryPush(Logger{context}); + if (result.has_value()) + { + return result.value(); + } + // Returning address of non-static private class member is justified by + // design, that the logger object ownership stays within the Logging framework. + // https://www.autosar.org/fileadmin/standards/R20-11/AP/AUTOSAR_SWS_LogAndTrace.pdf section-8.2.1 + // + return default_logger_; +} + +amp::optional> LoggerContainer::FindExistingLogger( + const amp::string_view context) noexcept +{ + return stack_.Find([context](const Logger& logger) noexcept { return logger.GetContext() == context; }); +} + +size_t LoggerContainer::GetCapacity() const noexcept +{ + return kMaxLoggersSize; +} + +Logger& LoggerContainer::GetDefaultLogger() noexcept +{ + // Returning address of non-static private class member is justified by + // design, that the logger object ownership stays within the Logging framework. + // https://www.autosar.org/fileadmin/standards/R20-11/AP/AUTOSAR_SWS_LogAndTrace.pdf section-8.2.1 + // + return default_logger_; +} + +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/logger_container.h b/mw/log/logger_container.h new file mode 100644 index 0000000..28846a9 --- /dev/null +++ b/mw/log/logger_container.h @@ -0,0 +1,70 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_LOGGER_CONTAINER_H +#define PLATFORM_AAS_MW_LOG_LOGGER_CONTAINER_H + +#include "platform/aas/mw/log/detail/wait_free_stack/wait_free_stack.h" +#include "platform/aas/mw/log/logger.h" +#include "platform/aas/mw/log/slot_handle.h" +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ + + + + + + +class LoggerContainer final + + + + +{ + public: + explicit LoggerContainer(); + + Logger& GetLogger( + const amp::string_view context) noexcept; + + size_t GetCapacity() const noexcept; + + Logger& GetDefaultLogger() noexcept; + + + private: + + Logger& InsertNewLogger(const amp::string_view context) noexcept; + + amp::optional> FindExistingLogger(const amp::string_view context) noexcept; + + detail::WaitFreeStack stack_; + Logger default_logger_; + +}; + + + + +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_LOGGER_CONTAINER_H diff --git a/mw/log/logger_container_test.cpp b/mw/log/logger_container_test.cpp new file mode 100644 index 0000000..02113e2 --- /dev/null +++ b/mw/log/logger_container_test.cpp @@ -0,0 +1,140 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#include "platform/aas/mw/log/logger_container.h" +#include "platform/aas/mw/log/logger.h" + +#include "platform/aas/mw/log/logging.h" +#include "platform/aas/mw/log/recorder_mock.h" + +#include + +#include "gtest/gtest.h" +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +namespace +{ +const auto kContext1 = amp::string_view{"MYCT"}; +const auto kDefaultContext = amp::string_view{"DFLT"}; +} // namespace + +TEST(LoggerContainerTests, WhenRequestingNonExistingNewLoggerItShallBeInsertedAndReturnToCaller) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "Verifies the ability of requesting non-existing new logger shall be inserted and returned to the caller."); + RecordProperty("TestType", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + LoggerContainer unit; + EXPECT_EQ(unit.GetLogger(kContext1).GetContext(), kContext1); +} + +TEST(LoggerContainerTests, WhenGetingDefaultLoggerShallGetDLFTContextID) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies that when getting a default logger it shall get DFLT context id."); + RecordProperty("TestType", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + LoggerContainer unit; + EXPECT_EQ(unit.GetDefaultLogger().GetContext(), kDefaultContext); +} + +TEST(LoggerContainerTests, WhenRequestingAlreadyExistingLoggerShallBeReturnedWithoutInsertingNewLogger) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "Verifies that when requesting already existing logger shall be returned without inserting new logger."); + RecordProperty("TestType", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + LoggerContainer unit; + EXPECT_EQ(unit.GetLogger(kContext1).GetContext(), kContext1); + EXPECT_EQ(unit.GetLogger(kContext1).GetContext(), kContext1); +} + +TEST(LoggerContainerTests, WhenLoggerContainerIsFullShallGetDefaultContextWhenNewLoggerRequested) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "Verifies that when logger container is full shall get default context when new logger is requested."); + RecordProperty("TestType", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + // Expecting a log record of level verbose + LoggerContainer unit; + std::vector contexts_vector(unit.GetCapacity()); + + for (size_t i = 0; i < contexts_vector.size(); i++) + { + contexts_vector[i] = std::to_string(i); + } + // iterate to make the buffer full with 31 different context. + for (const auto& context : contexts_vector) + { + // filling the buffer with different contexts till overrun. + EXPECT_EQ(unit.GetLogger(context).GetContext(), amp::string_view{context}); + } + // buffer is full, default logger is returned. + EXPECT_EQ(unit.GetLogger(kContext1).GetContext(), kDefaultContext); + amp::string_view inserted_context = contexts_vector[0]; + EXPECT_EQ(unit.GetLogger(inserted_context).GetContext(), inserted_context); +} + +void LoggerRequester1(LoggerContainer& logger_container) +{ + EXPECT_EQ(logger_container.GetLogger(kContext1).GetContext(), kContext1); +} + +void LoggerRequester2(LoggerContainer& logger_container) +{ + EXPECT_EQ(logger_container.GetLogger(kContext1).GetContext(), kContext1); +} + +TEST(LoggerContainerTests, WhenTwoThreadsRequestSameLoggerShallBeOnlyOneExistingInLoggerContainer) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty( + "Description", + "Verifies that when two threads request the same logger it shall be only one existing in logger container."); + RecordProperty("TestType", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + LoggerContainer unit; + + std::thread t1(LoggerRequester1, std::ref(unit)); + t1.join(); + + std::thread t2(LoggerRequester2, std::ref(unit)); + t2.join(); + + // default logger exists + EXPECT_EQ(unit.GetLogger(kDefaultContext).GetContext(), kDefaultContext); +} + +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/logger_test.cpp b/mw/log/logger_test.cpp new file mode 100644 index 0000000..2ab1a34 --- /dev/null +++ b/mw/log/logger_test.cpp @@ -0,0 +1,222 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/logger.h" + +#include "platform/aas/mw/log/logging.h" +#include "platform/aas/mw/log/recorder_mock.h" + +#include + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace +{ + +using ::testing::_; +using ::testing::Return; + +const SlotHandle HANDLE{42}; +const auto CONTEXT = amp::string_view{"MYCT"}; +const auto CONTEXT_DESCRIPTION = amp::string_view{"Test context description"}; +const auto kDefaultContext = amp::string_view{"DFLT"}; + +class BasicLoggerFixture : public ::testing::Test +{ + public: + BasicLoggerFixture() { bmw::mw::log::SetLogRecorder(&recorder_mock_); } + + Logger unit_{CONTEXT}; + RecorderMock recorder_mock_{}; +}; + +class LoggerFixture : public BasicLoggerFixture +{ + public: + LoggerFixture() { EXPECT_CALL(recorder_mock_, StopRecord(HANDLE)).Times(1); } +}; + +TEST_F(LoggerFixture, CanLogVerboseWithContext) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging verbose message."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::Logger"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given nothing + // Expecting a log record of level verbose + EXPECT_CALL(recorder_mock_, StartRecord(CONTEXT, LogLevel::kVerbose)).WillOnce(Return(HANDLE)); + + // When logging at level verbose + unit_.LogVerbose() << 42; +} + +TEST_F(LoggerFixture, CanLogDebugWithContext) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging debug message."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::Logger"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given nothing + // Expecting a log record of level debug + EXPECT_CALL(recorder_mock_, StartRecord(CONTEXT, LogLevel::kDebug)).WillOnce(Return(HANDLE)); + + // When logging at level debug + unit_.LogDebug() << 42; +} + +TEST_F(LoggerFixture, CanLogInfoWithContext) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging info message."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::Logger"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given nothing + // Expecting a log record of level info + EXPECT_CALL(recorder_mock_, StartRecord(CONTEXT, LogLevel::kInfo)).WillOnce(Return(HANDLE)); + + // When logging at level info + unit_.LogInfo() << 42; +} + +TEST_F(LoggerFixture, CanLogWarnWithContext) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging warning message."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::Logger"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given nothing + // Expecting a log record of level warn + EXPECT_CALL(recorder_mock_, StartRecord(CONTEXT, LogLevel::kWarn)).WillOnce(Return(HANDLE)); + + // When logging at level warn + unit_.LogWarn() << 42; +} + +TEST_F(LoggerFixture, CanLogErrorWithContext) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging error message."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::Logger"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given nothing + // Expecting a log record of level error + EXPECT_CALL(recorder_mock_, StartRecord(CONTEXT, LogLevel::kError)).WillOnce(Return(HANDLE)); + + // When logging at level error + unit_.LogError() << 42; +} + +TEST_F(LoggerFixture, CanLogFatalWithContext) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging fatal message."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::Logger"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given nothing + // Expecting a log record of level fatal + EXPECT_CALL(recorder_mock_, StartRecord(CONTEXT, LogLevel::kFatal)).WillOnce(Return(HANDLE)); + + // When logging at level fatal + unit_.LogFatal() << 42; +} + +TEST_F(LoggerFixture, CheckThatWithLevelSetsCorrectLogLevel) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging warn message using WithLevel API."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::Logger"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given nothing + // Expecting a log record of level warn + EXPECT_CALL(recorder_mock_, StartRecord(CONTEXT, LogLevel::kWarn)).WillOnce(Return(HANDLE)); + + // When logging at level warn + unit_.WithLevel(bmw::mw::log::LogLevel::kWarn) << 42; +} + +TEST_F(BasicLoggerFixture, CheckThatIsLogEnabledReturnsCorrectValue) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify that IsLogEnabled API is returning the correct log level."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::Logger"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given IsLogEnabled is called with the expected context + EXPECT_CALL(recorder_mock_, IsLogEnabled(LogLevel::kWarn, CONTEXT)).WillOnce(Return(true)); + + // Expect that the correct value is returned. + EXPECT_TRUE(unit_.IsLogEnabled(LogLevel::kWarn)); +} + +TEST(CreateLoggerGetContext, CreateLoggerWithNeededContext) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verifies that GetContext shall return the right context."); + RecordProperty("TestType", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + Logger unit = CreateLogger(CONTEXT); + EXPECT_EQ(unit.GetContext(), CONTEXT); +} + +TEST(CreateLoggerGetContext, WhenCreateLoggerWIthEmptyContextShallReturnDefaultLogger) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies that creating a logger with empty context it shall return the default logger."); + RecordProperty("TestType", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + Logger unit = CreateLogger(amp::string_view{}); + EXPECT_EQ(unit.GetContext(), kDefaultContext); +} + +TEST(CreateLoggerGetContext, CreateLoggerPassingTwoArgs) +{ + RecordProperty("ParentRequirement", ""); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verifies that GetContext shall return the right context when instantiating the CreateLogger with " + "two arguments."); + RecordProperty("TestType", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + Logger unit = CreateLogger(CONTEXT, CONTEXT_DESCRIPTION); + EXPECT_EQ(unit.GetContext(), CONTEXT); +} + +} // namespace +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/logging.cpp b/mw/log/logging.cpp new file mode 100644 index 0000000..55d6180 --- /dev/null +++ b/mw/log/logging.cpp @@ -0,0 +1,90 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/logging.h" + +#include "platform/aas/mw/log/log_level.h" +#include "platform/aas/mw/log/log_stream_factory.h" +#include "platform/aas/mw/log/recorder.h" +#include "platform/aas/mw/log/runtime.h" + +bmw::mw::log::LogStream bmw::mw::log::LogFatal() noexcept +{ + return bmw::mw::log::detail::LogStreamFactory::GetStream(LogLevel::kFatal); +} + +bmw::mw::log::LogStream bmw::mw::log::LogError() noexcept +{ + return bmw::mw::log::detail::LogStreamFactory::GetStream(LogLevel::kError); +} + +bmw::mw::log::LogStream bmw::mw::log::LogWarn() noexcept +{ + return bmw::mw::log::detail::LogStreamFactory::GetStream(LogLevel::kWarn); +} + +bmw::mw::log::LogStream bmw::mw::log::LogInfo() noexcept +{ + return bmw::mw::log::detail::LogStreamFactory::GetStream(LogLevel::kInfo); +} + +bmw::mw::log::LogStream bmw::mw::log::LogDebug() noexcept +{ + return bmw::mw::log::detail::LogStreamFactory::GetStream(LogLevel::kDebug); +} + +bmw::mw::log::LogStream bmw::mw::log::LogVerbose() noexcept +{ + return bmw::mw::log::detail::LogStreamFactory::GetStream(LogLevel::kVerbose); +} + +bmw::mw::log::LogStream bmw::mw::log::LogFatal(const amp::string_view context_id) noexcept +{ + return bmw::mw::log::detail::LogStreamFactory::GetStream(LogLevel::kFatal, context_id); +} + +bmw::mw::log::LogStream bmw::mw::log::LogError(const amp::string_view context_id) noexcept +{ + return bmw::mw::log::detail::LogStreamFactory::GetStream(LogLevel::kError, context_id); +} + +bmw::mw::log::LogStream bmw::mw::log::LogWarn(const amp::string_view context_id) noexcept +{ + return bmw::mw::log::detail::LogStreamFactory::GetStream(LogLevel::kWarn, context_id); +} + +bmw::mw::log::LogStream bmw::mw::log::LogInfo(const amp::string_view context_id) noexcept +{ + return bmw::mw::log::detail::LogStreamFactory::GetStream(LogLevel::kInfo, context_id); +} + +bmw::mw::log::LogStream bmw::mw::log::LogDebug(const amp::string_view context_id) noexcept +{ + return bmw::mw::log::detail::LogStreamFactory::GetStream(LogLevel::kDebug, context_id); +} + +bmw::mw::log::LogStream bmw::mw::log::LogVerbose(const amp::string_view context_id) noexcept +{ + return bmw::mw::log::detail::LogStreamFactory::GetStream(LogLevel::kVerbose, context_id); +} + +bmw::mw::log::Recorder& bmw::mw::log::GetDefaultLogRecorder() noexcept +{ + return bmw::mw::log::detail::Runtime::GetRecorder(); +} + +void bmw::mw::log::SetLogRecorder(bmw::mw::log::Recorder* const recorder) noexcept +{ + bmw::mw::log::detail::Runtime::SetRecorder(recorder); +} diff --git a/mw/log/logging.h b/mw/log/logging.h new file mode 100644 index 0000000..ef755b1 --- /dev/null +++ b/mw/log/logging.h @@ -0,0 +1,163 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_LOGGING_H +#define PLATFORM_AAS_MW_LOG_LOGGING_H + +// Be careful what you include here. Each additional header will be exposed to the user. +// We need to try to keep the includes low to reduce the compile footprint of using this library. +#include "platform/aas/mw/log/log_stream.h" + +#include "amp_string_view.hpp" + + +namespace bmw + +{ +namespace mw +{ + +namespace log +{ +// We forward declare Recorder to reduce the include chain +class Recorder; + +// @todo: extract LogStream from `log` namespace. To make code more readable, since log::LogStream names logging twice. +using LogStream = log::LogStream; + +/// \brief Creates a LogStream to log messages of criticality `Fatal` (highest). +/// \public +/// \thread-safe +/// +/// \details Fatal shall be used on errors that cannot be recovered and will lead to an overall failure in the system. +/// Since no context id is provided, the default configured context id will be used. +/// \return LogStream which can be used stream verbose logging messages (will be flushed on destruction) +LogStream LogFatal() noexcept; + +/// \brief Creates a LogStream to log messages of criticality `Error` (2nd highest). +/// \public +/// \thread-safe +/// +/// \details Error shall be used on errors that can be recovered and would lead to an failure in the system. +/// Since no context id is provided, the default configured context id will be used. +/// \return LogStream which can be used stream verbose logging messages (will be flushed on destruction) +LogStream LogError() noexcept; + +/// \brief Creates a LogStream to log messages of criticality `Warn` (3rd highest). +/// \public +/// \thread-safe +/// +/// \details Warnings shall be used on errors that might be no error but desired state or could to an error later on. +/// Since no context id is provided, the default configured context id will be used. +/// \return LogStream which can be used stream verbose logging messages (will be flushed on destruction) +LogStream LogWarn() noexcept; + +/// \brief Creates a LogStream to log messages of criticality `Info` (4th highest). +/// \public +/// \thread-safe +/// +/// \details Infos shall be used on messages that are of interest to analyze issues and understand overall program flow. +/// Since no context id is provided, the default configured context id will be used. +/// \return LogStream which can be used stream verbose logging messages (will be flushed on destruction) +LogStream LogInfo() noexcept; + +/// \brief Creates a LogStream to log messages of criticality `Debug` (5th highest). +/// \public +/// \thread-safe +/// +/// \details Debug shall be used on messages that are of interest to analyze issues in depth. +/// Since no context id is provided, the default configured context id will be used. +/// \return LogStream which can be used stream verbose logging messages (will be flushed on destruction) +LogStream LogDebug() noexcept; + +/// \brief Creates a LogStream to log messages of criticality `Verbose` (lowest). +/// \public +/// \thread-safe +/// +/// \details Verbose shall be used on messages that are of interest to analyze issues in depth but lead to super high +/// bandwidth (e.g. sending every millisecond). Since no context id is provided, the default configured context id will +/// be used. +/// \return LogStream which can be used stream verbose logging messages (will be flushed on destruction) +LogStream LogVerbose() noexcept; + +/// \brief Creates a LogStream to log messages of criticality `Fatal` (highest). +/// \public +/// \thread-safe +/// +/// \details Fatal shall be used on errors that cannot be recovered and will lead to an overall failure in the system. +/// The provided context_id will be used (only first four byte). +/// \return LogStream which can be used stream verbose logging messages (will be flushed on destruction) +LogStream LogFatal(const amp::string_view context_id) noexcept; + +/// \brief Creates a LogStream to log messages of criticality `Error` (2nd highest). +/// \public +/// \thread-safe +/// +/// \details Error shall be used on errors that can be recovered and would lead to an failure in the system. +/// The provided context_id will be used (only first four byte). +/// \return LogStream which can be used stream verbose logging messages (will be flushed on destruction) +LogStream LogError(const amp::string_view context_id) noexcept; + +/// \brief Creates a LogStream to log messages of criticality `Warn` (3rd highest). +/// \public +/// \thread-safe +/// +/// \details Warnings shall be used on errors that might be no error but desired state or could to an error later on. +/// The provided context_id will be used (only first four byte). +/// \return LogStream which can be used stream verbose logging messages (will be flushed on destruction) +LogStream LogWarn(const amp::string_view context_id) noexcept; + +/// \brief Creates a LogStream to log messages of criticality `Info` (4th highest). +/// \public +/// \thread-safe +/// +/// \details Infos shall be used on messages that are of interest to analyze issues and understand overall program flow. +/// The provided context_id will be used (only first four byte). +/// \return LogStream which can be used stream verbose logging messages (will be flushed on destruction) +LogStream LogInfo(const amp::string_view context_id) noexcept; + +/// \brief Creates a LogStream to log messages of criticality `Debug` (5th highest). +/// \public +/// \thread-safe +/// +/// \details Debug shall be used on messages that are of interest to analyze issues in depth. +/// The provided context_id will be used (only first four byte). +/// \return LogStream which can be used stream verbose logging messages (will be flushed on destruction) +LogStream LogDebug(const amp::string_view context_id) noexcept; + +/// \brief Creates a LogStream to log messages of criticality `Verbose` (lowest). +/// \public +/// \thread-safe +/// +/// \details Verbose shall be used on messages that are of interest to analyze issues in depth but lead to super high +/// bandwidth (e.g. sending every millisecond). The provided context_id will be used (only first four byte). +/// \return LogStream which can be used stream verbose logging messages (will be flushed on destruction) +LogStream LogVerbose(const amp::string_view context_id) noexcept; + +/// \brief Can be used by the user to get the underlying Recorder (where logging messages will be stored) +/// +/// \details In a normal case the user does not want to use this API. It is only exposed e.g. for testing purposes +/// \return The currently process global configured Recorder +log::Recorder& GetDefaultLogRecorder() noexcept; + +/// \brief Set a given Recorder as the process global configured Recorder for all log messages +/// +/// \details In a normal case the user does not want to use this API. It is only exposed e.g. for testing purposes +void SetLogRecorder(bmw::mw::log::Recorder* const recorder) noexcept; + +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_LOGGING_H diff --git a/mw/log/logging_test.cpp b/mw/log/logging_test.cpp new file mode 100644 index 0000000..6f532bf --- /dev/null +++ b/mw/log/logging_test.cpp @@ -0,0 +1,265 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/logging.h" + +#include "platform/aas/mw/log/recorder_mock.h" +#include "platform/aas/mw/log/runtime.h" + +#include "amp_string_view.hpp" + +#include "gtest/gtest.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace +{ + +using ::testing::_; +using ::testing::Return; + +const SlotHandle HANDLE{42}; +const auto CONTEXT = amp::string_view{"MYCT"}; + +TEST(Logging, CanSetAndRetrieveDefaultRecorder) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of retrieving the default logger."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::SetLogRecorder"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given a Recorder + RecorderMock recorder_mock{}; + + // When setting it as global default recorder + bmw::mw::log::SetLogRecorder(&recorder_mock); + + // Then retrieving it will return the correct one + EXPECT_EQ(&recorder_mock, &GetDefaultLogRecorder()); +} + +class LoggingFixture : public ::testing::Test +{ + public: + LoggingFixture() + { + bmw::mw::log::SetLogRecorder(&recorder_mock_); + EXPECT_CALL(recorder_mock_, StopRecord(HANDLE)).Times(1); + } + + RecorderMock recorder_mock_{}; +}; + +TEST_F(LoggingFixture, CanLogVerboseWithoutContext) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging verbose message without context provided."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::LogVerbose"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given nothing + // Given nothing + // Expecting a log record of level verbose + EXPECT_CALL(recorder_mock_, StartRecord(_, LogLevel::kVerbose)).WillOnce(Return(HANDLE)); + + // When logging at level verbose + bmw::mw::log::LogVerbose() << 42; +} + +TEST_F(LoggingFixture, CanLogDebugWithoutContext) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging debug message without context provided."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::LogDebug"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given nothing + // Expecting a log record of level debug + EXPECT_CALL(recorder_mock_, StartRecord(_, LogLevel::kDebug)).WillOnce(Return(HANDLE)); + + // When logging at level debug + bmw::mw::log::LogDebug() << 42; +} + +TEST_F(LoggingFixture, CanLogInfoWithoutContext) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging info message without context provided."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::LogInfo"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given nothing + // Expecting a log record of level info + EXPECT_CALL(recorder_mock_, StartRecord(_, LogLevel::kInfo)).WillOnce(Return(HANDLE)); + + // When logging at level info + bmw::mw::log::LogInfo() << 42; +} + +TEST_F(LoggingFixture, CanLogWarnWithoutContext) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging warning message without context provided."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::LogWarn"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given nothing + // Expecting a log record of level warn + EXPECT_CALL(recorder_mock_, StartRecord(_, LogLevel::kWarn)).WillOnce(Return(HANDLE)); + + // When logging at level warn + bmw::mw::log::LogWarn() << 42; +} + +TEST_F(LoggingFixture, CanLogErrorWithoutContext) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging error message without context provided."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::LogError"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given nothing + // Expecting a log record of level error + EXPECT_CALL(recorder_mock_, StartRecord(_, LogLevel::kError)).WillOnce(Return(HANDLE)); + + // When logging at level error + bmw::mw::log::LogError() << 42; +} + +TEST_F(LoggingFixture, CanLogFatalWithoutContext) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging fatal message without context provided."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::LogFatal"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given nothing + // Expecting a log record of level fatal + EXPECT_CALL(recorder_mock_, StartRecord(_, LogLevel::kFatal)).WillOnce(Return(HANDLE)); + + // When logging at level fatal + bmw::mw::log::LogFatal() << 42; +} + +TEST_F(LoggingFixture, CanLogVerboseWitContext) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging verbose message with context provided."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::LogVerbose"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given nothing + // Expecting a log record of level verbose + EXPECT_CALL(recorder_mock_, StartRecord(CONTEXT, LogLevel::kVerbose)).WillOnce(Return(HANDLE)); + + // When logging at level verbose + bmw::mw::log::LogVerbose(CONTEXT) << 42; +} + +TEST_F(LoggingFixture, CanLogDebugWithContext) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging debug message with context provided."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::LogDebug"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given nothing + // Expecting a log record of level debug + EXPECT_CALL(recorder_mock_, StartRecord(CONTEXT, LogLevel::kDebug)).WillOnce(Return(HANDLE)); + + // When logging at level debug + bmw::mw::log::LogDebug(CONTEXT) << 42; +} + +TEST_F(LoggingFixture, CanLogInfoWithContext) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging info message with context provided."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::LogInfo"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given nothing + // Expecting a log record of level info + EXPECT_CALL(recorder_mock_, StartRecord(CONTEXT, LogLevel::kInfo)).WillOnce(Return(HANDLE)); + + // When logging at level info + bmw::mw::log::LogInfo(CONTEXT) << 42; +} + +TEST_F(LoggingFixture, CanLogWarnWithContext) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging warning message with context provided."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::LogWarn"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given nothing + // Expecting a log record of level warn + EXPECT_CALL(recorder_mock_, StartRecord(CONTEXT, LogLevel::kWarn)).WillOnce(Return(HANDLE)); + + // When logging at level warn + bmw::mw::log::LogWarn(CONTEXT) << 42; +} + +TEST_F(LoggingFixture, CanLogErrorWithContext) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging error message with context provided."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::LogError"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given nothing + // Expecting a log record of level error + EXPECT_CALL(recorder_mock_, StartRecord(CONTEXT, LogLevel::kError)).WillOnce(Return(HANDLE)); + + // When logging at level error + bmw::mw::log::LogError(CONTEXT) << 42; +} + +TEST_F(LoggingFixture, CanLogFatalWithContext) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of logging fatal message with context provided."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::LogFatal"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given nothing + // Expecting a log record of level fatal + EXPECT_CALL(recorder_mock_, StartRecord(CONTEXT, LogLevel::kFatal)).WillOnce(Return(HANDLE)); + + // When logging at level fatal + bmw::mw::log::LogFatal(CONTEXT) << 42; +} + +} // namespace +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/recorder.cpp b/mw/log/recorder.cpp new file mode 100644 index 0000000..0ef65b4 --- /dev/null +++ b/mw/log/recorder.cpp @@ -0,0 +1,30 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/recorder.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ + + +Recorder::~Recorder() = default; + + +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/recorder.h b/mw/log/recorder.h new file mode 100644 index 0000000..77b0b85 --- /dev/null +++ b/mw/log/recorder.h @@ -0,0 +1,130 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_RECORDER_H +#define PLATFORM_AAS_MW_LOG_RECORDER_H + +#include "platform/aas/mw/log/log_level.h" +#include "platform/aas/mw/log/log_types.h" +#include "platform/aas/mw/log/slot_handle.h" + +#include "amp_optional.hpp" +#include "amp_string_view.hpp" + +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +/// \brief Base class for any actual recorder implementation +/// +/// \details The main idea of an recorder to is to store logging information in a reserved memory slot. +/// As an interface it is the correct way to mock any actual recorder. +/// A Recorder will normally interact with a `Backend` and a `Formatter` to store streamed data in the right format into +/// correct memory slot. +/// +/// Please be advised, if you plan to extend our logging API with your personal complex type, this header is _not_ the +/// right place. The idea is that a `Recorder` only supports the basic types in C++. If you want to ensure that your +/// custom complex type is loggable, please provide a custom overload of the operator<<(LogStream). + + +class Recorder +{ + public: + /// \brief We don't support copy or move operations for our Recorder, since it shall act as single instance. + /// + /// An unattended copy or move could cause weired side-effect, since implementations should own storage (e.g. shared + /// memory). + Recorder() = default; + virtual ~Recorder(); + + + /* Does not recognize noexcept keyword and reports hidden specifier */ + Recorder(const Recorder&) noexcept = delete; + Recorder(Recorder&&) noexcept = delete; + Recorder& operator=(const Recorder&) noexcept = delete; + Recorder& operator=(Recorder&&) noexcept = delete; + + /// \brief Acquire a slot from a respective backend to enable streaming into it. + /// + /// \param context_id The context under which the message shall be identified. + /// \param log_level The log level under which the message shall be treated. + /// \return SlotHandle if able to be acquired, empty otherwise. + /// + /// \post A call to Log() with respective SlotHandle is possible. + virtual amp::optional StartRecord(const amp::string_view context_id, + const LogLevel log_level) noexcept = 0; + + /// \brief Indicate that a message has finished and free slot for next write + /// + /// \param slot The slot to be finished up + virtual void StopRecord(const SlotHandle& slot) noexcept = 0; + + /// In the following we specify all basic types that can be logged our Recorders. + /// The idea is that a slot must be provided, where the respective data will be stored. + + virtual void Log(const SlotHandle&, const bool data) noexcept = 0; + + virtual void Log(const SlotHandle&, const std::uint8_t) noexcept = 0; + virtual void Log(const SlotHandle&, const std::int8_t) noexcept = 0; + + virtual void Log(const SlotHandle&, const std::uint16_t) noexcept = 0; + virtual void Log(const SlotHandle&, const std::int16_t) noexcept = 0; + + virtual void Log(const SlotHandle&, const std::uint32_t) noexcept = 0; + virtual void Log(const SlotHandle&, const std::int32_t) noexcept = 0; + + virtual void Log(const SlotHandle&, const std::uint64_t) noexcept = 0; + virtual void Log(const SlotHandle&, const std::int64_t) noexcept = 0; + + virtual void Log(const SlotHandle&, const float) noexcept = 0; + virtual void Log(const SlotHandle&, const double) noexcept = 0; + + virtual void Log(const SlotHandle&, const amp::string_view) noexcept = 0; + + virtual void Log(const SlotHandle&, const LogHex8) noexcept = 0; + + virtual void Log(const SlotHandle&, const LogHex16) noexcept = 0; + + virtual void Log(const SlotHandle&, const LogHex32) noexcept = 0; + + virtual void Log(const SlotHandle&, const LogHex64) noexcept = 0; + + virtual void Log(const SlotHandle&, const LogBin8) noexcept = 0; + + virtual void Log(const SlotHandle&, const LogBin16) noexcept = 0; + + virtual void Log(const SlotHandle&, const LogBin32) noexcept = 0; + + virtual void Log(const SlotHandle&, const LogBin64) noexcept = 0; + + virtual void Log(const SlotHandle&, const LogRawBuffer) noexcept = 0; + + virtual void Log(const SlotHandle&, const LogSlog2Message) noexcept = 0; + + virtual bool IsLogEnabled(const LogLevel&, const amp::string_view context) const noexcept = 0; + +}; + + + +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_RECORDER_H diff --git a/mw/log/recorder_mock.h b/mw/log/recorder_mock.h new file mode 100644 index 0000000..d144f4e --- /dev/null +++ b/mw/log/recorder_mock.h @@ -0,0 +1,106 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_RECORDER_MOCK_H +#define PLATFORM_AAS_MW_LOG_RECORDER_MOCK_H + +#include "gmock/gmock.h" +#include "platform/aas/mw/log/recorder.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +class RecorderMock : public Recorder +{ + public: + MOCK_METHOD(amp::optional, + StartRecord, + (const amp::string_view context_id, const LogLevel log_level), + (override, noexcept)); + MOCK_METHOD(void, StopRecord, (const SlotHandle& slot), (override, noexcept)); + + MOCK_METHOD(void, LogBool, (const SlotHandle&, bool data), (noexcept)); + void Log(const SlotHandle& handle, bool value) noexcept override { LogBool(handle, value); } + + MOCK_METHOD(void, LogUint8, (const SlotHandle&, std::uint8_t), (noexcept)); + void Log(const SlotHandle& handle, std::uint8_t value) noexcept override { LogUint8(handle, value); } + MOCK_METHOD(void, LogInt8, (const SlotHandle&, std::int8_t), (noexcept)); + void Log(const SlotHandle& handle, std::int8_t value) noexcept override { LogInt8(handle, value); } + + MOCK_METHOD(void, LogUint16, (const SlotHandle&, std::uint16_t), (noexcept)); + void Log(const SlotHandle& handle, std::uint16_t value) noexcept override { LogUint16(handle, value); } + MOCK_METHOD(void, LogInt16, (const SlotHandle&, std::int16_t), (noexcept)); + void Log(const SlotHandle& handle, std::int16_t value) noexcept override { LogInt16(handle, value); } + + MOCK_METHOD(void, LogUint32, (const SlotHandle&, std::uint32_t), (noexcept)); + void Log(const SlotHandle& handle, std::uint32_t value) noexcept override { LogUint32(handle, value); } + MOCK_METHOD(void, LogInt32, (const SlotHandle&, std::int32_t), (noexcept)); + void Log(const SlotHandle& handle, std::int32_t value) noexcept override { LogInt32(handle, value); } + + MOCK_METHOD(void, LogUint64, (const SlotHandle&, std::uint64_t), (noexcept)); + void Log(const SlotHandle& handle, std::uint64_t value) noexcept override { LogUint64(handle, value); } + MOCK_METHOD(void, LogInt64, (const SlotHandle&, std::int64_t), (noexcept)); + void Log(const SlotHandle& handle, std::int64_t value) noexcept override { LogInt64(handle, value); } + + MOCK_METHOD(void, LogFloat, (const SlotHandle&, float), (noexcept)); + void Log(const SlotHandle& handle, float value) noexcept override { LogFloat(handle, value); } + MOCK_METHOD(void, LogDouble, (const SlotHandle&, double), (noexcept)); + void Log(const SlotHandle& handle, double value) noexcept override { LogDouble(handle, value); } + + MOCK_METHOD(void, LogStringView, (const SlotHandle&, amp::string_view), (noexcept)); + void Log(const SlotHandle& handle, amp::string_view value) noexcept override { LogStringView(handle, value); } + + void Log(const SlotHandle& handle, LogHex8 value) noexcept override { LogUint8(handle, value.value); } + + void Log(const SlotHandle& handle, LogHex16 value) noexcept override { LogUint16(handle, value.value); } + + void Log(const SlotHandle& handle, LogHex32 value) noexcept override { LogUint32(handle, value.value); } + + void Log(const SlotHandle& handle, LogHex64 value) noexcept override { LogUint64(handle, value.value); } + + void Log(const SlotHandle& handle, LogBin8 value) noexcept override { LogUint8(handle, value.value); } + + void Log(const SlotHandle& handle, LogBin16 value) noexcept override { LogUint16(handle, value.value); } + + void Log(const SlotHandle& handle, LogBin32 value) noexcept override { LogUint32(handle, value.value); } + + void Log(const SlotHandle& handle, LogBin64 value) noexcept override { LogUint64(handle, value.value); } + + MOCK_METHOD(void, Log_LogRawBuffer, (const SlotHandle&, const void*, const uint64_t), (noexcept)); + void Log(const SlotHandle& handle, LogRawBuffer value) noexcept override + { + Log_LogRawBuffer(handle, value.data(), static_cast(value.size())); + } + + MOCK_METHOD(void, Log_LogSlog2Message, (const SlotHandle&, const uint16_t, const LogString), (noexcept)); + void Log(const SlotHandle& handle, LogSlog2Message value) noexcept override + { + Log_LogSlog2Message(handle, value.GetCode(), value.GetMessage()); + } + + // TODO: To be uncommented once is resolved. + // MOCK_METHOD(void, LogSpan, (const SlotHandle&, const void*, const uint64_t), (noexcept)); + + MOCK_METHOD(bool, IsLogEnabled, (const LogLevel&, const amp::string_view context), (override, noexcept, const)); +}; + +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_RECORDER_MOCK_H diff --git a/mw/log/runtime.cpp b/mw/log/runtime.cpp new file mode 100644 index 0000000..f402555 --- /dev/null +++ b/mw/log/runtime.cpp @@ -0,0 +1,84 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/runtime.h" + +#include "platform/aas/mw/log/detail/thread_local_guard.h" +#include "platform/aas/mw/log/irecorder_factory.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ + + +Runtime& Runtime::Instance(Recorder* const recorder) noexcept + +{ + + static Runtime runtime{recorder}; + + return runtime; +} + +Runtime::Runtime(Recorder* const recorder) noexcept +{ + ThreadLocalGuard guard{}; + const auto recorder_factory = CreateRecorderFactory(); + if (recorder == nullptr) + { + default_recorder_ = recorder_factory->CreateFromConfiguration(amp::pmr::get_default_resource()); + } + else + { + default_recorder_ = recorder_factory->CreateWithConsoleLoggingOnly(amp::pmr::get_default_resource()); + recorder_instance_ = recorder; + } +} + +bmw::mw::log::Recorder& Runtime::GetRecorder() noexcept +{ + const auto& instance = Instance(nullptr); + if (instance.recorder_instance_ != nullptr) + { + return *instance.recorder_instance_; + } + return *instance.default_recorder_; +} + +bmw::mw::log::Recorder& Runtime::GetFallbackRecorder() noexcept +{ + static std::unique_ptr recorder{ + CreateRecorderFactory()->CreateWithConsoleLoggingOnly(amp::pmr::get_default_resource())}; + return *recorder; +} + +bmw::mw::log::LoggerContainer& Runtime::GetLoggerContainer() noexcept +{ + return Instance(nullptr).logger_container_instance_; +} + +void Runtime::SetRecorder(Recorder* const recorder) noexcept +{ + Instance(recorder).recorder_instance_ = recorder; +} + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/runtime.h b/mw/log/runtime.h new file mode 100644 index 0000000..daaf519 --- /dev/null +++ b/mw/log/runtime.h @@ -0,0 +1,103 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_RUNTIME_H +#define PLATFORM_AAS_MW_LOG_RUNTIME_H + +#include "platform/aas/mw/log/logger_container.h" +#include "platform/aas/mw/log/recorder.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +namespace detail +{ + +class RecorderFactory; + +/// \brief The runtime is a singleton that is responsible for providing the usage of the current Recorder. Due to the +/// distributed nature of the logging library, we need a central place to know which logging infrastructure to use. This +/// enables us logging distributed within our code base without the necessity to inject it everywhere. +/// +/// \details We thought about not providing such a runtime and instead injecting the necessary logging recorder +/// everywhere. The main reason against this approach are: +/// 1) cluttering of dependencies. We need logging everywhere, so it would be a big dependency mess +/// 2) We have currently logging in a distributed manner, refactoring to an injecting pattern would require tremendous +/// efforts in the overall codebase + +class Runtime final +{ + public: + /// \brief Implements a singleton, so no copying or moving + Runtime(const Runtime&) noexcept = delete; + Runtime(Runtime&&) noexcept = delete; + Runtime& operator=(const Runtime&) noexcept = delete; + Runtime& operator=(Runtime&&) noexcept = delete; + + ~Runtime() = default; + /// \brief Getter function for the currently configured recorder + /// + /// \details Be careful, you cannot use GetRecorder() and SetRecorder() in a thread-safe manner. We decided against + /// a synchronisation primitive since in production code has no real use-case for usage of SetRecorder() and we want + /// to avoid the cost of the synchronization for production code if race conditions only exists for testing. This + /// also means that it is safe to invoke `GetRecorder()` concurrently. + /// \return pointer to either the default recorder or the last set recorder + static Recorder& GetRecorder() noexcept; + + /// \brief Inject a Recorder into the logging framework + /// + /// \param recorder The recorder that from now shall be used by any logging call + /// + /// \details Be careful, you cannot use GetRecorder() and SetRecorder() in a thread-safe manner. We decided against + /// a synchronisation primitive since in production code no real use-case for SetRecorder() exists and we want to + /// avoid the cost of the synchronization for production code if only exists for testing. + /// + /// Be advised that it is the responsibility of the user of this API, to ensure that the set recorder is alive as + /// long as any other functionality might invoke logging statements! + static void SetRecorder(Recorder* const recorder) noexcept; + + static Recorder& GetFallbackRecorder() noexcept; + + static bmw::mw::log::LoggerContainer& GetLoggerContainer() noexcept; + + + private: + + explicit Runtime(Recorder* const recorder) noexcept; + static Runtime& Instance(Recorder* const recorder) noexcept; + + /* False positive. There is no initialization list in constructor definition. */ + + LoggerContainer logger_container_instance_{}; + + Recorder* recorder_instance_ = nullptr; + std::unique_ptr default_recorder_ = nullptr; + std::unique_ptr fallback_recorder_ = nullptr; + + /* False positive. There is no initialization list in constructor definition. */ + +}; + + + +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_RUNTIME_H diff --git a/mw/log/runtime_test.cpp b/mw/log/runtime_test.cpp new file mode 100644 index 0000000..5b5390d --- /dev/null +++ b/mw/log/runtime_test.cpp @@ -0,0 +1,153 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/runtime.h" + +#include "platform/aas/mw/log/detail/empty_recorder.h" +#include "platform/aas/mw/log/detail/file_logging/text_recorder.h" +#include "platform/aas/mw/log/recorder_mock.h" + +#include "gtest/gtest.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +template +bool IsRecorderOfType(const Recorder& recorder) noexcept +{ + static_assert(std::is_base_of::value, + "Concrete recorder shall be derived from Recorder base class"); + + return dynamic_cast(&recorder) != nullptr; +} + +class RuntimeFixture : public ::testing::Test +{ + public: + RecorderMock recorder_mock_{}; +}; + +TEST_F(RuntimeFixture, CanSetALoggingBackend) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability to set backend logging without exception."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given an empty process + + // When setting the recorder for e.g. testing purposes + Runtime::SetRecorder(&recorder_mock_); + + // Then no exception happens (API test) +} + +TEST_F(RuntimeFixture, CanRetrieveSetRecorder) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability to get the recorder properly."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given the recorder was already set + Runtime::SetRecorder(&recorder_mock_); + + // When trying to read the current recorder + // Then it is the one that was previously stored + EXPECT_EQ(&recorder_mock_, &Runtime::GetRecorder()); +} + +TEST_F(RuntimeFixture, CanRetrieveFallbackRecorder) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability to get a fallback recorder."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given the runtime was initialized + + // When trying to read the fallback recorder + const auto& recorder = Runtime::GetFallbackRecorder(); + + // Then we receive a text recorder + EXPECT_TRUE(IsRecorderOfType(recorder)); +} + +TEST_F(RuntimeFixture, DefaultRecorderShallBeReturned) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verify the ability of returning the default recorder in case of null recorder is set."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Given the unset recorder: + Runtime::SetRecorder(&recorder_mock_); + auto& recorder = Runtime::GetRecorder(); + Runtime::SetRecorder(nullptr); + + // When trying to read the current recorder + // It shall be of type TextRecorder + EXPECT_TRUE(IsRecorderOfType(Runtime::GetRecorder())); + + // Revert previously stored recorder: + Runtime::SetRecorder(&recorder); +} + +TEST_F(RuntimeFixture, WithLoggerContainerHasFreeCapacityExpectedThatNewLoggerContainsCorrectContext) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Verify if logger container has capacity will create this logger if it's not available."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + const amp::string_view context{"ctx"}; + EXPECT_EQ(context, Runtime::GetLoggerContainer().GetLogger(context).GetContext()); +} + +TEST(RuntimeTest, RuntimeInitializationWithPointer) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "This suite only exists to test the second branch of the runtime initialization. Since this is " + "static state we need a separate binary for this."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // This suite only exists to test the second branch of the runtime initialization. + // Since this is static state we need a separate binary for this. + EmptyRecorder r; + Runtime::SetRecorder(&r); + EXPECT_EQ(&Runtime::GetRecorder(), &r); + Runtime::SetRecorder(nullptr); + // Even after resetting the recorder GetRecorder() shall return a valid reference to a stub recorder. + // We enforce checking this by calling an arbitrary method on the reference. + // Address sanitizer and valgrind would detect a memory error if the implementation is faulty. + Runtime::GetRecorder().IsLogEnabled(LogLevel::kVerbose, ""); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/runtime_test_without_pointer_initialization.cpp b/mw/log/runtime_test_without_pointer_initialization.cpp new file mode 100644 index 0000000..2bf837c --- /dev/null +++ b/mw/log/runtime_test_without_pointer_initialization.cpp @@ -0,0 +1,54 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/runtime.h" + +#include "platform/aas/mw/log/detail/empty_recorder.h" +#include "platform/aas/mw/log/recorder_mock.h" + +#include "gtest/gtest.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace detail +{ +namespace +{ + +TEST(RuntimeTest, RuntimeInitializationWithoutPointer) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "This suite only exists to test the first branch of the runtime initialization. Since this is " + "static state we need a separate binary for this."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + // Do not add additional tests here, but in runtime_test.cpp. + auto& recorder = Runtime::GetRecorder(); + // GetRecorder() shall always return a valid reference to a recorder. + // We enforce checking this by calling an arbitrary method on the reference. + // Address sanitizer and valgrind would detect a memory error if the implementation is faulty. + recorder.IsLogEnabled(LogLevel::kVerbose, ""); +} + +} // namespace +} // namespace detail +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/slot_handle.cpp b/mw/log/slot_handle.cpp new file mode 100644 index 0000000..3de5bd2 --- /dev/null +++ b/mw/log/slot_handle.cpp @@ -0,0 +1,112 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/slot_handle.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +namespace +{ +bool IsRecorderValid(const SlotHandle::RecorderIdentifier recorder) noexcept +{ + return recorder.value <= SlotHandle::kMaxRecorders; +} +} // namespace + +SlotHandle::SlotHandle(const SlotIndex slot) noexcept +{ + SetSlot(slot, RecorderIdentifier{}); +} + +SlotIndex SlotHandle::GetSlotOfSelectedRecorder() const noexcept +{ + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index) bound already checked + return recorder_to_slot_[selected_recorder_.value]; +} + +SlotIndex SlotHandle::GetSlot(const RecorderIdentifier recorder) const noexcept +{ + if (!IsRecorderValid(recorder)) + { + return {}; + } + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index) bound already checked + return recorder_to_slot_[recorder.value]; +} + +void SlotHandle::SetSlot(const SlotIndex slot, const RecorderIdentifier recorder) noexcept +{ + if (!IsRecorderValid(recorder)) + { + return; + } + + recorder_slot_available_[recorder.value] = true; + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index) bound already checked + recorder_to_slot_[recorder.value] = slot; +} + +SlotHandle::RecorderIdentifier SlotHandle::GetSelectedRecorder() const noexcept +{ + return selected_recorder_; +} + +void SlotHandle::SetSelectedRecorder(const RecorderIdentifier recorder) noexcept +{ + if (!IsRecorderValid(recorder)) + { + return; + } + + selected_recorder_ = recorder; +} + +bool SlotHandle::IsRecorderActive(const RecorderIdentifier recorder) const noexcept +{ + if (!IsRecorderValid(recorder)) + { + return false; + } + + return recorder_slot_available_[recorder.value]; +} + +bool operator==(const SlotHandle& l_value, const SlotHandle& r_value) noexcept +{ + + return (((l_value.selected_recorder_ == r_value.selected_recorder_) && + (std::equal(l_value.recorder_to_slot_.begin(), + l_value.recorder_to_slot_.end(), + r_value.recorder_to_slot_.begin()))) == true); + +} + +bool operator!=(const SlotHandle& l_value, const SlotHandle& r_value) noexcept +{ + return !(l_value == r_value); +} + +bool operator==(const SlotHandle::RecorderIdentifier& l_value, const SlotHandle::RecorderIdentifier& r_value) noexcept +{ + return l_value.value == r_value.value; +} + +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/slot_handle.h b/mw/log/slot_handle.h new file mode 100644 index 0000000..7213e25 --- /dev/null +++ b/mw/log/slot_handle.h @@ -0,0 +1,97 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_SLOT_HANDLE_H +#define PLATFORM_AAS_MW_LOG_SLOT_HANDLE_H + +// Be careful what you include here. Each additional header will be included in logging.h and thus exposed to the user. +// We need to try to keep the includes low to reduce the compile footprint of using this library. +#include +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +using SlotIndex = std::uint8_t; + +/// \brief Represents an identifier that is owned by a `LogStream` to identify the correct memory slot where the +/// streamed message shall be written. +/// +/// \details We do not use a direct pointer to the memory, since this would expose implementation details. Further, the +/// user shall not be able to directly manipulate the memory, since the format of the memory must be well defined. For +/// more information visit the concept of an `Formatter`. +/// +/// The recorder identifier exists to enable multiple active recorders at the same time. For more details see also the +/// CompositeRecorder class. +class SlotHandle final +{ + public: + /// \brief Value to identify the destination recorder. + struct RecorderIdentifier + { + std::size_t value{}; + }; + + SlotHandle() noexcept = default; + explicit SlotHandle(const SlotIndex) noexcept; + + /// \brief Returns the slot identifier corresponding to the currently selected recorder. + SlotIndex GetSlotOfSelectedRecorder() const noexcept; + + /// \brief Gets the slot of the corresponding recorder identifier. + /// \details On invalid recorder returns zero. + SlotIndex GetSlot(const RecorderIdentifier) const noexcept; + + /// \brief Sets the slot of the according recorder identifier. + /// \details Iff recorder value is invalid, the call will be discarded. + void SetSlot(const SlotIndex, const RecorderIdentifier recorder = {{}}) noexcept; + + /// \brief Get the currently selected recorder. + RecorderIdentifier GetSelectedRecorder() const noexcept; + + /// \brief Set the selected recorder. + /// \details Invalid recorder value will be ignored. + void SetSelectedRecorder(const RecorderIdentifier) noexcept; + + /// \brief Returns true if a slot in the corresponding recorder was available and reserved. + bool IsRecorderActive(const RecorderIdentifier) const noexcept; + + friend bool operator==(const SlotHandle& l_value, const SlotHandle& r_value) noexcept; + friend bool operator!=(const SlotHandle& l_value, const SlotHandle& r_value) noexcept; + + /// \brief Maximum number of recorders supported at the same time. + static constexpr std::size_t kMaxRecorders{4}; + + + private: + + + std::array recorder_to_slot_{}; + std::bitset recorder_slot_available_{}; + RecorderIdentifier selected_recorder_{}; + +}; + +bool operator==(const SlotHandle::RecorderIdentifier& l_value, const SlotHandle::RecorderIdentifier& r_value) noexcept; + +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_SLOT_HANDLE_H diff --git a/mw/log/slot_handle_test.cpp b/mw/log/slot_handle_test.cpp new file mode 100644 index 0000000..f686566 --- /dev/null +++ b/mw/log/slot_handle_test.cpp @@ -0,0 +1,244 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/slot_handle.h" + +#include "gtest/gtest.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace +{ + +const SlotHandle::RecorderIdentifier kInvalidRecorder{255}; +const SlotIndex kSlotValue{3}; +const SlotHandle::RecorderIdentifier kRecorderValue{1}; + +void ExpectAllSlotsUnassigned(const SlotHandle& handle) +{ + for (SlotIndex i = 0; i < SlotHandle::kMaxRecorders; ++i) + { + EXPECT_EQ(handle.GetSlot(SlotHandle::RecorderIdentifier{i}), SlotIndex{}); + EXPECT_EQ(handle.IsRecorderActive(SlotHandle::RecorderIdentifier{i}), false); + } +} + +TEST(SlotHandle, SlotHandleShallDefaultInitializeToZero) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Check the default initialization of SlotHandle instance."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::SlotHandle"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + SlotHandle handle{}; + EXPECT_EQ(handle.GetSelectedRecorder(), SlotHandle::RecorderIdentifier{0}); + EXPECT_EQ(handle.GetSlotOfSelectedRecorder(), SlotIndex{0}); + ExpectAllSlotsUnassigned(handle); +} + +TEST(SlotHandle, GetSlotOfSelectedRecorderShallReturnCorrectSlot) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Check Set/Get slot method."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::SlotHandle"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + SlotHandle handle{}; + handle.SetSlot(kSlotValue, kRecorderValue); + handle.SetSelectedRecorder(kRecorderValue); + EXPECT_EQ(handle.GetSlotOfSelectedRecorder(), kSlotValue); +} + +TEST(SlotHandle, GetSlotShallReturnCorrectValue) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Check getting the slot whose Set via constructor."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::SlotHandle"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + SlotHandle handle{kSlotValue}; + EXPECT_EQ(handle.GetSlot(SlotHandle::RecorderIdentifier{0}), kSlotValue); +} + +TEST(SlotHandle, GetSlotShallReturnZeroOnIncorrectValue) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Check the in-ability of getting the slot in case of invalid recorder identifier value."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::SlotHandle"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + SlotHandle handle{kSlotValue}; + EXPECT_EQ(handle.GetSlot(kInvalidRecorder), 0); +} + +TEST(SlotHandle, SetSlotShallSetCorrectValue) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Check the ability of seting slot properly."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::SlotHandle"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + SlotHandle handle{kSlotValue}; + handle.SetSlot(kSlotValue, kRecorderValue); + EXPECT_EQ(handle.GetSlot(kRecorderValue), kSlotValue); +} + +TEST(SlotHandle, SetSlotShallDiscardInvalidRecorder) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the in-ability of setting invalid recorder identifier value."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::SlotHandle"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + SlotHandle handle{}; + handle.SetSlot(kSlotValue, kInvalidRecorder); + ExpectAllSlotsUnassigned(handle); +} + +TEST(SlotHandle, SetSelectedRecorderShallReturnCorrectValue) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of setting selected recorder."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::SlotHandle"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + SlotHandle handle{}; + handle.SetSelectedRecorder(kRecorderValue); + EXPECT_EQ(handle.GetSelectedRecorder(), kRecorderValue); +} + +TEST(SlotHandle, SetSelectedRecorderShallIgnoreInvalidValue) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of SetSelectedRecorder to ignore invalid recorder."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::SlotHandle"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + SlotHandle handle{}; + handle.SetSelectedRecorder(kRecorderValue); + handle.SetSelectedRecorder(kInvalidRecorder); + EXPECT_EQ(handle.GetSelectedRecorder(), kRecorderValue); +} + +TEST(SlotHandle, GetSlotAvailableShallReturnTrueOnAssigned) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of checking the availability of specific slot."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::SlotHandle"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + SlotHandle handle{kSlotValue}; + EXPECT_EQ(handle.IsRecorderActive(SlotHandle::RecorderIdentifier{}), true); +} + +TEST(SlotHandle, GetSlotAvailableShallReturnFalseOnInvalidRecorder) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of checking the availability of specific slot."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::SlotHandle"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + SlotHandle handle{}; + EXPECT_EQ(handle.IsRecorderActive(kInvalidRecorder), false); +} + +TEST(SlotHandle, ShallBeEqualIffSelectedRecorderAndSlotsAreEqual) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Check the equality of SlotHandle instances."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::SlotHandle"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + SlotHandle rhs{}; + SlotHandle lhs{}; + for (SlotIndex i = 0; i < SlotHandle::kMaxRecorders; i++) + { + rhs.SetSlot(i, SlotHandle::RecorderIdentifier{i}); + lhs.SetSlot(i, SlotHandle::RecorderIdentifier{i}); + } + + rhs.SetSelectedRecorder(kRecorderValue); + lhs.SetSelectedRecorder(kRecorderValue); + + EXPECT_EQ(lhs, rhs); +} + +TEST(SlotHandle, ShallBeUnequalIfSelectedRecorderUnequal) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Check the inequality of SlotHandle instances."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::SlotHandle"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + SlotHandle rhs{}; + SlotHandle lhs{}; + + lhs.SetSelectedRecorder(kRecorderValue); + + EXPECT_NE(lhs, rhs); +} + +TEST(SlotHandle, ShallBeUnequalIfAnySlotUnequal) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Check the inequality of SlotHandle instances."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::SlotHandle"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + SlotHandle rhs{}; + SlotHandle lhs{}; + + lhs.SetSlot(kSlotValue, kRecorderValue); + + EXPECT_NE(lhs, rhs); +} + +TEST(SlotHandle, ShallBeUnequalIfAnySlotUnequalAssigned) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Check the inequality of SlotHandle instances."); + RecordProperty("TestType", "Interface test"); + RecordProperty("Verifies", "::bmw::mw::log::SlotHandle"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + SlotHandle rhs{}; + SlotHandle lhs{}; + + lhs.SetSlot(kSlotValue, {}); + + EXPECT_NE(lhs, rhs); +} + +} // namespace +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/test/BUILD b/mw/log/test/BUILD new file mode 100644 index 0000000..a0c4cc2 --- /dev/null +++ b/mw/log/test/BUILD @@ -0,0 +1,127 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//platform/aas/bazel/packaging:adaptive_application.bzl", "pkg_adaptive_application") +load("//platform/aas/tools/itf/bazel_gen:itf_gen.bzl", "py_unittest_qnx_test") + +_compiler_warning_features = [ + "additional_warnings", + "strict_warnings", + "treat_warnings_as_errors", +] + +pkg_adaptive_application( + name = "mw_log_logging_app", + testonly = True, + bins = ["//platform/aas/mw/log/test/logging_app:mw_log_logging_app"], + etc_deps = [], + etcs = [ + "//platform/aas/mw/log/test/logging_app:logging_cfg", + ], + visibility = ["//platform/aas/mw/log/test:__subpackages__"], +) + +# Since //common shall not depend on platform (Since it is in another stand alone repo +# "static_reflection_with_serialization"), and because this new repo is not ready to build +# and run the unit tests on QNX, we have to integrate the qnx unit tests here. Kindly check +# visitor unit tests. +filegroup( + name = "visitor_unit_tests_fg", + testonly = True, + srcs = [":visitorUT"], +) + +cc_test( + name = "visitorUT", + srcs = [ + "@static_reflection_with_serialization//common/visitor:test/ut/test_detail.cpp", + "@static_reflection_with_serialization//common/visitor:test/ut/test_struct_visitor.cpp", + "@static_reflection_with_serialization//common/visitor:test/ut/test_visitor.cpp", + ], + features = _compiler_warning_features, + tags = ["unit"], + deps = [ + "//third_party/c++stdlib", + "@googletest//:gtest_main", + "@static_reflection_with_serialization//common/visitor:visitor_target", + ], +) + +# serializer unit tests. +filegroup( + name = "serializer_unit_tests_fg", + testonly = True, + srcs = [ + ":VisitorTypeTraitsUT", + ":serializerUT", + ":sizeVisitorUT", + ], +) + +cc_test( + name = "serializerUT", + srcs = [ + "@static_reflection_with_serialization//common/serialization:test/ut/test_serializer_visitor.cpp", + "@static_reflection_with_serialization//common/serialization:test/ut/test_skip_deserialize.cpp", + "@static_reflection_with_serialization//common/serialization:test/ut/visitor_test_types.h", + ], + features = _compiler_warning_features + [ + "aborts_upon_exception", + ], + tags = ["unit"], + deps = [ + "@googletest//:gtest_main", + "@static_reflection_with_serialization//common/serialization:serializer_target", + ], +) + +cc_test( + name = "sizeVisitorUT", + srcs = [ + "@static_reflection_with_serialization//common/serialization:test/ut/test_size_visitor.cpp", + "@static_reflection_with_serialization//common/serialization:test/ut/visitor_test_types.h", + ], + features = _compiler_warning_features + [ + "aborts_upon_exception", + ], + tags = ["unit"], + deps = [ + "@googletest//:gtest_main", + "@static_reflection_with_serialization//common/serialization:serializer_target", + ], +) + +cc_test( + name = "VisitorTypeTraitsUT", + srcs = [ + "@static_reflection_with_serialization//common/serialization:test/ut/test_visitor_type_traits.cpp", + "@static_reflection_with_serialization//common/serialization:test/ut/visitor_test_types.h", + ], + features = _compiler_warning_features + [ + "aborts_upon_exception", + ], + tags = ["unit"], + deps = [ + "@googletest//:gtest_main", + "@static_reflection_with_serialization//common/serialization:serializer_target", + ], +) + +py_unittest_qnx_test( + name = "serializer_unit_tests_qnx", + test_cases = [ + ":visitor_unit_tests_fg", + ":serializer_unit_tests_fg", + ], + visibility = ["//platform/aas:__subpackages__"], +) diff --git a/mw/log/test/console_logging_environment/BUILD b/mw/log/test/console_logging_environment/BUILD new file mode 100644 index 0000000..d34a048 --- /dev/null +++ b/mw/log/test/console_logging_environment/BUILD @@ -0,0 +1,41 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# You can depend on this if you don't need further customization of the main() for gtest. +cc_library( + name = "console_logging_environment", + testonly = True, + srcs = [ + "main.cpp", + ], + visibility = ["//visibility:public"], + deps = [ + ":console_logging_environment_lib", + ], +) + +# You can depend on this if you need a customized main() for gtest. +cc_library( + name = "console_logging_environment_lib", + testonly = True, + srcs = [ + "console_logging_environment.cpp", + ], + hdrs = [ + "console_logging_environment.h", + ], + deps = [ + "//platform/aas/mw/log", + "//third_party/googletest", + ], +) diff --git a/mw/log/test/console_logging_environment/console_logging_environment.cpp b/mw/log/test/console_logging_environment/console_logging_environment.cpp new file mode 100644 index 0000000..9383cf2 --- /dev/null +++ b/mw/log/test/console_logging_environment/console_logging_environment.cpp @@ -0,0 +1,44 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/test/console_logging_environment/console_logging_environment.h" +#include "platform/aas/mw/log/detail/recorder_factory.h" +#include "platform/aas/mw/log/runtime.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +void ConsoleLoggingEnvironment::SetUp() +{ + bmw::mw::log::detail::Configuration config{}; + config.SetLogMode({bmw::mw::LogMode::kConsole}); + config.SetDefaultConsoleLogLevel(bmw::mw::log::LogLevel::kVerbose); + recorder_ = bmw::mw::log::detail::RecorderFactory().CreateRecorderFromLogMode(bmw::mw::LogMode::kConsole, config); + + bmw::mw::log::detail::Runtime::SetRecorder(recorder_.get()); +} + +void ConsoleLoggingEnvironment::TearDown() +{ + bmw::mw::log::detail::Runtime::SetRecorder(nullptr); + recorder_.reset(); +} + +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/test/console_logging_environment/console_logging_environment.h b/mw/log/test/console_logging_environment/console_logging_environment.h new file mode 100644 index 0000000..919b52e --- /dev/null +++ b/mw/log/test/console_logging_environment/console_logging_environment.h @@ -0,0 +1,44 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef BMW_PLATFORM_MW_LOG_CONSOLE_LOGGING_ENVIRONMENT_H +#define BMW_PLATFORM_MW_LOG_CONSOLE_LOGGING_ENVIRONMENT_H + +#include "gtest/gtest.h" + +#include "platform/aas/mw/log/recorder.h" + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +class ConsoleLoggingEnvironment : public ::testing::Environment +{ + public: + void SetUp() override; + + void TearDown() override; + + private: + std::unique_ptr recorder_{nullptr}; +}; + +} // namespace log +} // namespace mw +} // namespace bmw + +#endif diff --git a/mw/log/test/console_logging_environment/main.cpp b/mw/log/test/console_logging_environment/main.cpp new file mode 100644 index 0000000..ce2b247 --- /dev/null +++ b/mw/log/test/console_logging_environment/main.cpp @@ -0,0 +1,29 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#include "gtest/gtest.h" + +#include "platform/aas/mw/log/test/console_logging_environment/console_logging_environment.h" + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + + // Googletest will delete all registered global environments and thus we must use new() here. + auto console_log_environment = new bmw::mw::log::ConsoleLoggingEnvironment(); + AddGlobalTestEnvironment(console_log_environment); + + return RUN_ALL_TESTS(); +} diff --git a/mw/log/test/dlt_load_generator/BUILD b/mw/log/test/dlt_load_generator/BUILD new file mode 100644 index 0000000..a7f66c9 --- /dev/null +++ b/mw/log/test/dlt_load_generator/BUILD @@ -0,0 +1,53 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//platform/aas/bazel/packaging:adaptive_application.bzl", "pkg_adaptive_application") +load("//platform/aas/mw:common_features.bzl", "COMPILER_WARNING_FEATURES") + +cc_binary( + name = "dlt_load_generator", + srcs = [ + "dlt_load_generator.cpp", + ], + defines = ["VERBOSE_LOGGING=0"], + features = COMPILER_WARNING_FEATURES, + deps = [ + "//platform/aas/mw/log", + ], +) + +# Config for logging app +filegroup( + name = "logging_cfg", + srcs = [ + "etc/logging.json", + ], + visibility = [ + "//ecu/xpad/xpad-shared/config:__subpackages__", + "//platform/aas/mw/log/test:__pkg__", + ], +) + +pkg_adaptive_application( + name = "pkg", + application_name = "dlt_load_generator", + bins = [":dlt_load_generator"], + etc_deps = [], + etcs = [ + ":logging_cfg", + ], + visibility = [ + "//ecu/xpad/xpad-shared/packaging/swe/swfl_platform:__subpackages__", + "//platform/aas/mw/log/test:__subpackages__", + ], +) diff --git a/mw/log/test/dlt_load_generator/README.md b/mw/log/test/dlt_load_generator/README.md new file mode 100644 index 0000000..230f287 --- /dev/null +++ b/mw/log/test/dlt_load_generator/README.md @@ -0,0 +1,80 @@ + + + + +# DLT load generator program + +Simple program to stimulate datarouter with a target bandwidth in case of verbose or non-verbose logging. + +## Usage + +1. Choose verbose logging by setting flag VERBOSE_LOGGING=1 in dlt_load_generator binary bazel rule and by default it's non verbose. + +2. Build the package and upload it to the running target in case of verbose logging. + +```bash +bazel build --config=ipnext_arm64_qnx //platform/aas/mw/log/test/dlt_load_generator:dlt_load_generator-pkg +scp bazel-bin/platform/aas/mw/log/test/dlt_load_generator/dlt_load_generator-release-pkg.tar target:/persistent +``` + +3. *In case of non verbose* +- Choose no_verbose logging by setting flag VERBOSE_LOGGING=0 in dlt_load_generator binary bazel rule. + +- Put non_verbose_golden_app_ids.json under ecu/xpad/xpad-shared/config/common/pas/logging +- Add the following in ddad/ecu/xpad/xpad-shared/config/common/pas/logging/BUILD + +``` +filegroup( + name = "non_verbose_app_ids", + srcs = ["non_verbose_golden_app_ids.json"], + visibility = ["//visibility:public"], +) +``` +- Adjust common_fixed_ids in ddad/ecu/xpad/xpad-shared/config/common/pas/logging/BUILD to have non_verbose_app_ids.json + +- Under ecu/xpad/xpad-shared/packaging/swe/swfl_platform/BUILD edit platform_apps_common to have +"//platform/aas/mw/log/test/non_verbose_dlt_load_app:pkg", + +- Disable running of EM from startup scripts ecu/xpad/xpad-shared/config/os/fs/init/usr/sbin/bmw_startup.sh + + +- Build IPNext image + +```bash +bazel build --config=ipnext_arm64_qnx --config=ad_gen_25 //ecu/xpad/xpad-shared/packaging/image:IPNext_HLOS +``` + +4. Restart datarouter to ensure other processes are not interfering with the test in case of verbose logging and for non verbose logging just run the data router since EM is disabled to have free cross interference environment. + +```bash +ssh target +slay datarouter +cd /bmw/platform/opt/datarouter && on -u 1038:1036 -A nonroot,allow,pathspace ./bin/datarouter -T datarouter_t --no_adaptive_runtime & +``` + +5. In a separate terminal start the DLT load program to stimulate the required bandwidth. + +```bash +ssh target +cd /persistent +tar -xvf dlt_load_generator-release-pkg.tar +cd opt/dlt_load_generator +./bin/dlt_load_generator 10 # Stimulate with 10 MB/s DLT payload +``` + +6. Measure memory and cpu usages + +```bash +showmem -p proc_id +top +``` diff --git a/mw/log/test/dlt_load_generator/dlt_load_generator.cpp b/mw/log/test/dlt_load_generator/dlt_load_generator.cpp new file mode 100644 index 0000000..4b14c16 --- /dev/null +++ b/mw/log/test/dlt_load_generator/dlt_load_generator.cpp @@ -0,0 +1,128 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#include "platform/aas/mw/log/logger.h" + +#include +#include +#include +#include +#if VERBOSE_LOGGING +#else +#include "platform/aas/mw/log/legacy_non_verbose_api/tracing.h" +#endif + +using namespace std::chrono_literals; + +namespace +{ +constexpr auto kChunkDuration = 100ms; +constexpr auto kDltMessagePayloadSizeBytes = 1400; +constexpr auto kStimulationDurationSeconds = 60s; +} // namespace + +#if VERBOSE_LOGGING +#else + +namespace bmw +{ +namespace logging +{ +namespace internal +{ +struct NonVerboseMessage +{ + std::uint8_t array[kDltMessagePayloadSizeBytes]; +}; +STRUCT_TRACEABLE(bmw::logging::internal::NonVerboseMessage, array) + +} // namespace internal +} // namespace logging +} // namespace bmw + +#endif + +int main(int argc, char** argv) +{ + + if (argc != 2) + { + std::cout << "Usage: bin/load_test \nWill stress Datarouter with the target load for " + << kStimulationDurationSeconds.count() << " seconds.\n"; + return EXIT_FAILURE; + } + + const auto target_dlt_load_mb_per_sec = std::strtol(argv[1], nullptr, 10); + + if (target_dlt_load_mb_per_sec <= 0) + { + std::cout << "Target load must be greater than 0" << std::endl; + } +#if VERBOSE_LOGGING + auto& logger = bmw::mw::log::CreateLogger("LOAD"); +#else + bmw::logging::internal::NonVerboseMessage entry; +#endif + + constexpr auto mega_byte_to_byte = 1024 * 1024; + constexpr auto second_to_milliseconds = 1000; + + const auto number_of_messages_per_chunk = + ((mega_byte_to_byte * target_dlt_load_mb_per_sec)) / + (kDltMessagePayloadSizeBytes * (second_to_milliseconds / kChunkDuration.count())); + + std::cout << "Sending " << number_of_messages_per_chunk << " DLT messages with a payload size of " + << kDltMessagePayloadSizeBytes << " bytes in each " << kChunkDuration.count() + << " milliseconds interval to simulate a load of " << target_dlt_load_mb_per_sec << " MB/s for " + << kStimulationDurationSeconds.count() << " seconds." << std::endl; + +// Warm up the logging infrastructure, i.e., wait for Datarouter to connect to the client. +#if VERBOSE_LOGGING + logger.LogFatal() << "Starting with target_dlt_load_mb_per_sec = " << target_dlt_load_mb_per_sec; +#else + TRACE(entry); +#endif + std::this_thread::sleep_for(200ms); + +#if VERBOSE_LOGGING + std::vector log_message_buffer(kDltMessagePayloadSizeBytes, 'a'); + bmw::mw::log::LogRawBuffer log_message{log_message_buffer.data(), + static_cast(log_message_buffer.size())}; +#endif + const auto start_time = std::chrono::steady_clock::now(); + for (;;) + { + const auto elapsed_time = std::chrono::steady_clock::now() - start_time; + if (elapsed_time >= kStimulationDurationSeconds) + { + break; + } + + const auto chunk_start_time = std::chrono::steady_clock::now(); + + for (auto i = 0; i < number_of_messages_per_chunk; ++i) + { +#if VERBOSE_LOGGING + logger.LogFatal() << log_message; +#else + TRACE(entry); +#endif + } + + std::this_thread::sleep_until(chunk_start_time + kChunkDuration); + } + + return EXIT_SUCCESS; +} diff --git a/mw/log/test/dlt_load_generator/etc/logging.json b/mw/log/test/dlt_load_generator/etc/logging.json new file mode 100644 index 0000000..6036583 --- /dev/null +++ b/mw/log/test/dlt_load_generator/etc/logging.json @@ -0,0 +1,21 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "appId": "LOAD", + "appDesc": "DLT load generator", + "logMode": "kRemote", + "logLevel": "kVerbose", + "logLevelThresholdConsole": "kVerbose", + "numberOfSlots": 10000, + "slotSize": 1500, + "ringBufferSize": 100097152 +} diff --git a/mw/log/test/dlt_load_generator/etc/non_verbose_golden_app_ids.json b/mw/log/test/dlt_load_generator/etc/non_verbose_golden_app_ids.json new file mode 100644 index 0000000..f250a9d --- /dev/null +++ b/mw/log/test/dlt_load_generator/etc/non_verbose_golden_app_ids.json @@ -0,0 +1,14 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "bmw::logging::internal::NonVerboseMessage": {"id": 7777, "ctxid": "LOAD", "appid": "LOAD"} +} diff --git a/mw/log/test/logging_app/BUILD b/mw/log/test/logging_app/BUILD new file mode 100644 index 0000000..68b3f49 --- /dev/null +++ b/mw/log/test/logging_app/BUILD @@ -0,0 +1,46 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//platform/aas/mw:common_features.bzl", "COMPILER_WARNING_FEATURES") + +# Config for logging app +filegroup( + name = "logging_cfg", + srcs = [ + "etc/logging.json", + ], + visibility = [ + "//ecu/xpad/xpad-shared/config:__subpackages__", + "//platform/aas/mw/log/test:__pkg__", + ], +) + +# Mw Log Logging Adaptive Application +cc_binary( + name = "mw_log_logging_app", + testonly = True, + srcs = [ + "main.cpp", + ], + defines = ["USE_MIDDLEWARE_LOGGING=1"], + features = COMPILER_WARNING_FEATURES, + visibility = [ + "//ecu/xpad/xpad-shared/config:__subpackages__", + "//platform/aas/mw/log/test:__pkg__", + ], + deps = [ + "//platform/aas/mw/log:frontend", + "//platform/aas/mw/log/detail:recorder_factory", + "//platform/aas/test/pas/logging/utils:logging_app_test_template", + ], +) diff --git a/mw/log/test/logging_app/etc/logging.json b/mw/log/test/logging_app/etc/logging.json new file mode 100644 index 0000000..d9035b5 --- /dev/null +++ b/mw/log/test/logging_app/etc/logging.json @@ -0,0 +1,19 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "appId": "EXA", + "appDesc": "Example App", + "logMode": "kConsole|kRemote|kFile", + "logLevel": "kVerbose", + "logLevelThresholdConsole": "kVerbose" +} + diff --git a/mw/log/test/logging_app/main.cpp b/mw/log/test/logging_app/main.cpp new file mode 100644 index 0000000..897e07c --- /dev/null +++ b/mw/log/test/logging_app/main.cpp @@ -0,0 +1,62 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/test/pas/logging/utils/src/logging_app.h" + +#include "platform/aas/mw/lifecycle/runapplication.h" + +#if !defined(USE_MIDDLEWARE_LOGGING) +#error "Define logging framework to be tested" +#endif + +#if USE_MIDDLEWARE_LOGGING +#include "platform/aas/mw/log/logging.h" +namespace logUnderTest = bmw::mw::log; +#else +#include "ara/log/logging.h" +namespace logUnderTest = ara::log; +#endif + +namespace +{ +using TypeFunction = logUnderTest::LogStream (&)(void); +} + + + +int main(std::int32_t argc, const char* argv[]) + + +{ +#if defined(__GNUC__) && not defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wuseless-cast" // Cast is used when functions have overloads provided by mw::log. + // Bigger harm would be to use symbols to avoid cast in other cases. +#endif // #if defined(__GNUC__) && not defined(__clang__) + + + return bmw::mw::lifecycle::run_application>( + argc, + argv, + static_cast(logUnderTest::LogInfo), + static_cast(logUnderTest::LogDebug), + static_cast(logUnderTest::LogWarn), + static_cast(logUnderTest::LogError), + static_cast(logUnderTest::LogFatal)); + + +#if defined(__GNUC__) && not defined(__clang__) +#pragma GCC diagnostic pop +#endif // #if defined(__GNUC__) && not defined(__clang__) +} diff --git a/mw/log/test/my_custom_lib/BUILD b/mw/log/test/my_custom_lib/BUILD new file mode 100644 index 0000000..60e1dfe --- /dev/null +++ b/mw/log/test/my_custom_lib/BUILD @@ -0,0 +1,53 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//platform/aas/mw:common_features.bzl", "COMPILER_WARNING_FEATURES") + +cc_library( + name = "my_custom_type_mw_log", + testonly = True, + srcs = [ + "my_custom_type_mw_log.cpp", + ], + hdrs = [ + "my_custom_type.h", + "my_custom_type_mw_log.h", + ], + features = COMPILER_WARNING_FEATURES, + visibility = [ + "//platform/aas/mw/log:__pkg__", + "//platform/aas/test/pas/logging/utils:__pkg__", + ], + deps = [ + "//platform/aas/mw/log:log_stream", + ], +) + +cc_library( + name = "my_custom_type_ara_log", + testonly = True, + srcs = [ + "my_custom_type_ara_log.cpp", + ], + hdrs = [ + "my_custom_type.h", + "my_custom_type_ara_log.h", + ], + features = COMPILER_WARNING_FEATURES, + visibility = [ + "//platform/aas/test/pas/logging/utils:__pkg__", + ], + deps = [ + "//platform/aas/ara/log", + ], +) diff --git a/mw/log/test/my_custom_lib/my_custom_type.h b/mw/log/test/my_custom_lib/my_custom_type.h new file mode 100644 index 0000000..2781aab --- /dev/null +++ b/mw/log/test/my_custom_lib/my_custom_type.h @@ -0,0 +1,37 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_TEST_MY_CUSTOM_LIB_MY_CUSTOM_TYPE_H +#define PLATFORM_AAS_MW_LOG_TEST_MY_CUSTOM_LIB_MY_CUSTOM_TYPE_H + +#include + +namespace my +{ +namespace custom +{ +namespace type +{ + +struct MyCustomType +{ + std::int32_t int_field; + std::string string_field; +}; + +} // namespace type +} // namespace custom +} // namespace my + +#endif // PLATFORM_AAS_MW_LOG_TEST_MY_CUSTOM_LIB_MY_CUSTOM_TYPE_H diff --git a/mw/log/test/my_custom_lib/my_custom_type_ara_log.cpp b/mw/log/test/my_custom_lib/my_custom_type_ara_log.cpp new file mode 100644 index 0000000..a5d3062 --- /dev/null +++ b/mw/log/test/my_custom_lib/my_custom_type_ara_log.cpp @@ -0,0 +1,37 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#include "my_custom_type_ara_log.h" + +namespace my +{ +namespace custom +{ +namespace type +{ + + +ara::log::LogStream& operator<<(ara::log::LogStream& log_stream, + const my::custom::type::MyCustomType& my_custom_type) noexcept +{ + log_stream << "my_custom_type: int_field : " << my_custom_type.int_field + << " , string_field : " << my_custom_type.string_field; + return log_stream; +} + + +} // namespace type +} // namespace custom +} // namespace my diff --git a/mw/log/test/my_custom_lib/my_custom_type_ara_log.h b/mw/log/test/my_custom_lib/my_custom_type_ara_log.h new file mode 100644 index 0000000..d6aca0f --- /dev/null +++ b/mw/log/test/my_custom_lib/my_custom_type_ara_log.h @@ -0,0 +1,35 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_TEST_MY_CUSTOM_LIB_MY_CUSTOM_TYPE_ARA_LOG_H +#define PLATFORM_AAS_MW_LOG_TEST_MY_CUSTOM_LIB_MY_CUSTOM_TYPE_ARA_LOG_H + +#include "platform/aas/ara/log/inc/ara/log/logstream.h" +#include "platform/aas/mw/log/test/my_custom_lib/my_custom_type.h" + +namespace my +{ +namespace custom +{ +namespace type +{ + +ara::log::LogStream& operator<<(ara::log::LogStream& log_stream, + const my::custom::type::MyCustomType& my_custom_type) noexcept; + +} // namespace type +} // namespace custom +} // namespace my + +#endif // PLATFORM_AAS_MW_LOG_TEST_MY_CUSTOM_LIB_MY_CUSTOM_TYPE_ARA_LOG_H diff --git a/mw/log/test/my_custom_lib/my_custom_type_mw_log.cpp b/mw/log/test/my_custom_lib/my_custom_type_mw_log.cpp new file mode 100644 index 0000000..6a54d02 --- /dev/null +++ b/mw/log/test/my_custom_lib/my_custom_type_mw_log.cpp @@ -0,0 +1,37 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + + +#include "my_custom_type_mw_log.h" + +namespace my +{ +namespace custom +{ +namespace type +{ + + +bmw::mw::log::LogStream& operator<<(bmw::mw::log::LogStream& log_stream, + const my::custom::type::MyCustomType& my_custom_type) noexcept +{ + log_stream << "my_custom_type: int_field : " << my_custom_type.int_field + << " , string_field : " << my_custom_type.string_field; + return log_stream; +} + + +} // namespace type +} // namespace custom +} // namespace my diff --git a/mw/log/test/my_custom_lib/my_custom_type_mw_log.h b/mw/log/test/my_custom_lib/my_custom_type_mw_log.h new file mode 100644 index 0000000..70cfe25 --- /dev/null +++ b/mw/log/test/my_custom_lib/my_custom_type_mw_log.h @@ -0,0 +1,35 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_TEST_MY_CUSTOM_LIB_MY_CUSTOM_TYPE_MW_LOG_H +#define PLATFORM_AAS_MW_LOG_TEST_MY_CUSTOM_LIB_MY_CUSTOM_TYPE_MW_LOG_H + +#include "platform/aas/mw/log/log_stream.h" +#include "platform/aas/mw/log/test/my_custom_lib/my_custom_type.h" + +namespace my +{ +namespace custom +{ +namespace type +{ + +bmw::mw::log::LogStream& operator<<(bmw::mw::log::LogStream& log_stream, + const my::custom::type::MyCustomType& my_custom_type) noexcept; + +} // namespace type +} // namespace custom +} // namespace my + +#endif // PLATFORM_AAS_MW_LOG_TEST_MY_CUSTOM_LIB_MY_CUSTOM_TYPE_MW_LOG_H diff --git a/mw/log/test/sample_ara_core_logging/BUILD b/mw/log/test/sample_ara_core_logging/BUILD new file mode 100644 index 0000000..bd8e4c8 --- /dev/null +++ b/mw/log/test/sample_ara_core_logging/BUILD @@ -0,0 +1,30 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//platform/aas/mw:common_features.bzl", "COMPILER_WARNING_FEATURES") + +cc_library( + name = "sample_ara_core_logging", + testonly = True, + hdrs = [ + "sample_ara_core_logging.h", + ], + features = COMPILER_WARNING_FEATURES, + visibility = [ + "//platform/aas/mw/log:__pkg__", + ], + deps = [ + "//platform/aas/ara/core", + "//platform/aas/mw/log:log_stream", + ], +) diff --git a/mw/log/test/sample_ara_core_logging/sample_ara_core_logging.h b/mw/log/test/sample_ara_core_logging/sample_ara_core_logging.h new file mode 100644 index 0000000..c9aca58 --- /dev/null +++ b/mw/log/test/sample_ara_core_logging/sample_ara_core_logging.h @@ -0,0 +1,56 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_LOG_TEST_SAMPLE_ARA_CORE_LOGGING_SAMPLE_ARA_CORE_LOGGING_H +#define PLATFORM_AAS_MW_LOG_TEST_SAMPLE_ARA_CORE_LOGGING_SAMPLE_ARA_CORE_LOGGING_H + +#include "platform/aas/mw/log/log_stream.h" + +#include +#include + +namespace ara +{ +namespace core +{ + +template +bmw::mw::log::LogStream& operator<<(bmw::mw::log::LogStream& log_stream, const ara::core::Result& result) noexcept +{ + if (result.HasValue()) + { + log_stream << "Result value: " << result.Value(); + } + else + { + log_stream << "Error message: "; + + const auto& msg = result.Error().UserMessage(); + if (msg.size()) + { + log_stream << amp::string_view{msg.data(), msg.size()}; + } + else + { + log_stream << "{EMPTY}"; + } + } + + return log_stream; +} + +} // namespace core +} // namespace ara + +#endif // PLATFORM_AAS_MW_LOG_TEST_SAMPLE_ARA_CORE_LOGGING_SAMPLE_ARA_CORE_LOGGING_H diff --git a/mw/log/test/sctf/BUILD b/mw/log/test/sctf/BUILD new file mode 100644 index 0000000..80fef7c --- /dev/null +++ b/mw/log/test/sctf/BUILD @@ -0,0 +1,31 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//platform/aas/tools/sctf/bazel_gen:sctf_gen.bzl", "py_sctf_test") + +py_sctf_test( + name = "test_mw_log", + srcs = [ + "sct/__init__.py", + "sct/test_mw_log.py", + ], + data = [ + "//platform/aas/mw/log/test:mw_log_logging_app-pkg.tar", + ], + flaky = False, + module_root = "platform/aas/mw/log/test/sctf/sct", + deps = [ + "//platform/aas/tools/logging:test_logging", + "//platform/aas/tools/perf_report", + ], +) diff --git a/mw/log/test/sctf/etc/logging.json b/mw/log/test/sctf/etc/logging.json new file mode 100644 index 0000000..daa8330 --- /dev/null +++ b/mw/log/test/sctf/etc/logging.json @@ -0,0 +1,19 @@ +// *******************************************************************************> +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// SPDX-License-Identifier: Apache-2.0 # +// ******************************************************************************* + + +{ + "appId": "EXA", + "appDesc": "Example App", + "logMode": "kRemote|kConsole", + "logLevel": "kInfo", + "logLevelThresholdConsole": "kInfo" + } + diff --git a/mw/log/test/sctf/sct/__init__.py b/mw/log/test/sctf/sct/__init__.py new file mode 100644 index 0000000..1022998 --- /dev/null +++ b/mw/log/test/sctf/sct/__init__.py @@ -0,0 +1,13 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + diff --git a/mw/log/test/sctf/sct/test_mw_log.py b/mw/log/test/sctf/sct/test_mw_log.py new file mode 100644 index 0000000..271cb6c --- /dev/null +++ b/mw/log/test/sctf/sct/test_mw_log.py @@ -0,0 +1,124 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + + +"""Logging sctf test""" +# pylint: disable=global-statement +# pylint: disable=import-error +# pylint: disable=unused-argument +import logging +import pytest +import test_logging.test_log_basic_acceptance as basic_acceptance_test +from sctf import run + +LOGGER = logging.getLogger(__name__) + +LOGGING_APP_ID = "EXA" + +MW_LOG_FILE_NAME = LOGGING_APP_ID + ".dlt" + +VALUES_TO_CHECK_MW_LOG_REMOTE = [ + f"{LOGGING_APP_ID} DFLT log info verbose 2 Logging Application DoLogging", + f"{LOGGING_APP_ID} DFLT log info verbose 2 val_bool True", + f"{LOGGING_APP_ID} DFLT log debug verbose 8 val_uint8t 123 val_uint16t 1234 val_uint32t 12345 val_uint64t 123456", + f"{LOGGING_APP_ID} DFLT log warn verbose 8 val_uint8tmax 255 val_uint16tmax 65535 val_uint32tmax 4294967295 val_uint64tmax 18446744073709551615", + f"{LOGGING_APP_ID} DFLT log fatal verbose 8 val_int8t -34 val_int16t -14576 val_int32t -2147483640 val_int64t -9223372036854775700", + f"{LOGGING_APP_ID} DFLT log warn verbose 8 val_int8tmax 127 val_int16tmax 32767 val_int32tmax 2147483647 val_int64tmax 9223372036854775807", + f"{LOGGING_APP_ID} DFLT log debug verbose 8 val_uint8t 123 val_uint16t 1234 val_uint32t 12345 val_uint64t 123456", + f"{LOGGING_APP_ID} DFLT log warn verbose 8 val_int8tmin -128 val_int16tmin -32768 val_int32tmin -2147483648 val_int64tmin -9223372036854775808", + f"{LOGGING_APP_ID} DFLT log error verbose 8 val_int8tminplusint8t -94 val_int16tminplusint16t -18192 val_int32tminplusint32t -8 " + f"val_int64tminplusint64t -108", + f"{LOGGING_APP_ID} DFLT log fatal verbose 2 val_string Logging", + f"{LOGGING_APP_ID} DFLT log fatal verbose 2 val_double 93454.6", + # Hex and bin values are displayed as decimal because of python-dlt limitations. TODO under + f"{LOGGING_APP_ID} DFLT log info verbose 8 log_hex_8 10 log_hex_16 9876 log_hex_32 543210987 log_hex_64 654321098765432109", + f"{LOGGING_APP_ID} DFLT log warn verbose 8 log_bin_8 8 log_bin_16 9012 log_bin_32 3456789012 log_bin_64 3456789012345678901", + f"{LOGGING_APP_ID} DFLT log fatal verbose 2 log_raw_buffer b\'raw\'", + f"{LOGGING_APP_ID} DFLT log fatal verbose 2 log_slog2_message slog2_message", +] + +VALUES_TO_CHECK_MW_LOG_CONSOLE = [ + f"{LOGGING_APP_ID} DFLT log info verbose 2 Logging Application DoLogging", + f"{LOGGING_APP_ID} DFLT log info verbose 2 val_bool True", + f"{LOGGING_APP_ID} DFLT log debug verbose 8 val_uint8t 123 val_uint16t 1234 val_uint32t 12345 val_uint64t 123456", + f"{LOGGING_APP_ID} DFLT log warn verbose 8 val_uint8tmax 255 val_uint16tmax 65535 val_uint32tmax 4294967295 val_uint64tmax 18446744073709551615", + f"{LOGGING_APP_ID} DFLT log fatal verbose 8 val_int8t -34 val_int16t -14576 val_int32t -2147483640 val_int64t -9223372036854775700", + f"{LOGGING_APP_ID} DFLT log warn verbose 8 val_int8tmax 127 val_int16tmax 32767 val_int32tmax 2147483647 val_int64tmax 9223372036854775807", + f"{LOGGING_APP_ID} DFLT log debug verbose 8 val_uint8t 123 val_uint16t 1234 val_uint32t 12345 val_uint64t 123456", + f"{LOGGING_APP_ID} DFLT log warn verbose 8 val_int8tmin -128 val_int16tmin -32768 val_int32tmin -2147483648 val_int64tmin -9223372036854775808", + f"{LOGGING_APP_ID} DFLT log error verbose 8 val_int8tminplusint8t -94 val_int16tminplusint16t -18192 val_int32tminplusint32t -8 " + f"val_int64tminplusint64t -108", + f"{LOGGING_APP_ID} DFLT log fatal verbose 2 val_string Logging", + f"{LOGGING_APP_ID} DFLT log fatal verbose 2 val_double 93454.6", + # Hex and bin values are displayed as decimal because of python-dlt limitations. TODO under + f"{LOGGING_APP_ID} DFLT log info verbose 8 log_hex_8 10 log_hex_16 9876 log_hex_32 543210987 log_hex_64 654321098765432109", + f"{LOGGING_APP_ID} DFLT log warn verbose 8 log_bin_8 8 log_bin_16 9012 log_bin_32 3456789012 log_bin_64 3456789012345678901", + f"{LOGGING_APP_ID} DFLT log fatal verbose 2 log_raw_buffer 726177", + f"{LOGGING_APP_ID} DFLT log fatal verbose 2 log_slog2_message slog2_message", +] + +VALUES_TO_CHECK_MW_LOG_FILE = [ + f"{LOGGING_APP_ID} DFLT log info verbose 2 Logging Application DoLogging", + f"{LOGGING_APP_ID} DFLT log info verbose 2 val_bool True", + f"{LOGGING_APP_ID} DFLT log debug verbose 8 val_uint8t 123 val_uint16t 1234 val_uint32t 12345 val_uint64t 123456", + f"{LOGGING_APP_ID} DFLT log warn verbose 8 val_uint8tmax 255 val_uint16tmax 65535 val_uint32tmax 4294967295 val_uint64tmax 18446744073709551615", + f"{LOGGING_APP_ID} DFLT log fatal verbose 8 val_int8t -34 val_int16t -14576 val_int32t -2147483640 val_int64t -9223372036854775700", + f"{LOGGING_APP_ID} DFLT log warn verbose 8 val_int8tmax 127 val_int16tmax 32767 val_int32tmax 2147483647 val_int64tmax 9223372036854775807", + f"{LOGGING_APP_ID} DFLT log debug verbose 8 val_uint8t 123 val_uint16t 1234 val_uint32t 12345 val_uint64t 123456", + f"{LOGGING_APP_ID} DFLT log warn verbose 8 val_int8tmin -128 val_int16tmin -32768 val_int32tmin -2147483648 val_int64tmin -9223372036854775808", + f"{LOGGING_APP_ID} DFLT log error verbose 8 val_int8tminplusint8t -94 val_int16tminplusint16t -18192 val_int32tminplusint32t -8 " + f"val_int64tminplusint64t -108", + f"{LOGGING_APP_ID} DFLT log fatal verbose 2 val_string Logging", + f"{LOGGING_APP_ID} DFLT log fatal verbose 2 val_double 93454.6", + f"{LOGGING_APP_ID} DFLT log info verbose 8 log_hex_8 10 log_hex_16 9876 log_hex_32 543210987 log_hex_64 654321098765432109", + f"{LOGGING_APP_ID} DFLT log warn verbose 8 log_bin_8 8 log_bin_16 9012 log_bin_32 3456789012 log_bin_64 3456789012345678901", + f"{LOGGING_APP_ID} DFLT log fatal verbose 2 log_raw_buffer b'raw'", + f"{LOGGING_APP_ID} DFLT log fatal verbose 2 log_slog2_message slog2_message", +] + +@pytest.fixture() +def mw_log_logging_fixture(environment): + mw_log_logging = basic_acceptance_test.LoggingApp(environment, "mw_log_logging_app") + mw_log_logging.generate_exec_config() + yield mw_log_logging + +@pytest.mark.metadata( + description=("mw::log shall implement Specification of Log and Trace for Adaptive Platform"), + verifies=[1633236, 1633144, 1633238], + testingTechnique="Interface test", + derivationTechnique="Analysis of requirements") +def test_mw_log_remote_logging(mw_log_logging_fixture, adaptive_environment_fixture): + basic_acceptance_test.body_test_case_remote_logging(mw_log_logging_fixture, adaptive_environment_fixture, VALUES_TO_CHECK_MW_LOG_REMOTE, LOGGING_APP_ID) + + +@pytest.mark.metadata( + description=("mw::log shall implement Specification of Log and Trace for Adaptive Platform"), + verifies=[1633236, 1633144, 1633238], + testingTechnique="Interface test", + derivationTechnique="Analysis of requirements") +def test_mw_log_console_logging(mw_log_logging_fixture, adaptive_environment_fixture): + basic_acceptance_test.body_test_case_console_logging(mw_log_logging_fixture, adaptive_environment_fixture, VALUES_TO_CHECK_MW_LOG_CONSOLE, LOGGING_APP_ID) + + +@pytest.mark.metadata( + description=("mw::log shall implement Specification of Log and Trace for Adaptive Platform"), + verifies=[1633236, 1633144, 1633238], + testingTechnique="Interface test", + derivationTechnique="Analysis of requirements") +def test_mw_log_file_logging(mw_log_logging_fixture, adaptive_environment_fixture): + basic_acceptance_test.body_test_case_file_logging( + mw_log_logging_fixture, adaptive_environment_fixture, VALUES_TO_CHECK_MW_LOG_FILE, LOGGING_APP_ID, MW_LOG_FILE_NAME) + + +if __name__ == '__main__': + run(__file__) diff --git a/mw/log/test/stream_capture/BUILD b/mw/log/test/stream_capture/BUILD new file mode 100644 index 0000000..407af87 --- /dev/null +++ b/mw/log/test/stream_capture/BUILD @@ -0,0 +1,40 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//platform/aas/mw:common_features.bzl", "COMPILER_WARNING_FEATURES") + +cc_library( + name = "stream_capture", + testonly = True, + srcs = [ + "stream_capture.cpp", + ], + hdrs = [ + "stream_capture.h", + ], + features = COMPILER_WARNING_FEATURES, + visibility = ["//platform/aas:__subpackages__"], +) + +cc_test( + name = "unit_test", + srcs = [ + "stream_capture_test.cpp", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["unit"], + deps = [ + ":stream_capture", + "//third_party/googletest:main", + ], +) diff --git a/mw/log/test/stream_capture/stream_capture.cpp b/mw/log/test/stream_capture/stream_capture.cpp new file mode 100644 index 0000000..fae2353 --- /dev/null +++ b/mw/log/test/stream_capture/stream_capture.cpp @@ -0,0 +1,110 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/test/stream_capture/stream_capture.h" +#include +#include +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +void StreamCapture::StartCapturingStream(FILE* stream) +{ + old_stream_ = dup(fileno(stream)); + if (old_stream_ == -1) + { + std::cerr << "dup call failed" << std::endl; + exit(-1); + } + + file_name_ = "test_" + std::to_string(rand()) + ".txt"; + std::FILE* fileW = std::fopen(file_name_.c_str(), "w"); + if (!fileW) + { + std::cerr << "Failed to open file" << std::endl; + exit(-1); + } + + fflush(stream); + if (dup2(fileno(fileW), fileno(stream)) == -1) + { + std::cerr << "dup call failed" << std::endl; + exit(-1); + } + capturing_ = true; + fclose(fileW); +} + +std::string StreamCapture::FetchCapturedStream() +{ + if (!capturing_) + { + std::cerr << "Capture not started" << std::endl; + exit(-1); + } + + auto stream = getStreamFile(); + fflush(stream); + if (dup2(old_stream_, fileno(stream) == -1)) + { + std::cerr << "dup call failed" << std::endl; + exit(-1); + } + close(old_stream_); + std::FILE* file = std::fopen(file_name_.c_str(), "r"); + + if (!file) + { + std::cerr << "Failed to open file" << std::endl; + exit(-1); + } + + char ch = static_cast(getc(file)); + std::string str; + while (ch != EOF) + { + str.push_back(ch); + ch = static_cast(getc(file)); + } + + fclose(file); + std::remove(file_name_.c_str()); + return str; +} + +void StreamCapture::StartCapturingStdout() +{ + StartCapturingStream(stdout); + stream_type_ = StreamType::STDOUT; +} + +void StreamCapture::StartCapturingStderr() +{ + StartCapturingStream(stderr); + stream_type_ = StreamType::STDERR; +} + +FILE* StreamCapture::getStreamFile() +{ + return stream_type_ == StreamType::STDOUT ? stdout : stderr; +} + +} // namespace log +} // namespace mw +} // namespace bmw diff --git a/mw/log/test/stream_capture/stream_capture.h b/mw/log/test/stream_capture/stream_capture.h new file mode 100644 index 0000000..cd1f5c4 --- /dev/null +++ b/mw/log/test/stream_capture/stream_capture.h @@ -0,0 +1,55 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#ifndef PLATFORM_AAS_MW_STREAM_CAPTURE_H +#define PLATFORM_AAS_MW_STREAM_CAPTURE_H + +#include +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ + +class StreamCapture +{ + public: + void StartCapturingStdout(); + void StartCapturingStderr(); + std::string FetchCapturedStream(); + + private: + void StartCapturingStream(FILE* stream); + FILE* getStreamFile(); + + int old_stream_; + bool capturing_ = false; + std::string file_name_; + enum class StreamType : std::uint8_t + { + STDOUT = 0, + STDERR + }; + StreamType stream_type_; +}; + +} // namespace log +} // namespace mw +} // namespace bmw + +#endif // PLATFORM_AAS_MW_LOG_LOGGER_H diff --git a/mw/log/test/stream_capture/stream_capture_test.cpp b/mw/log/test/stream_capture/stream_capture_test.cpp new file mode 100644 index 0000000..1a3338f --- /dev/null +++ b/mw/log/test/stream_capture/stream_capture_test.cpp @@ -0,0 +1,74 @@ +/******************************************************************************** +* Copyright (c) 2025 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License Version 2.0 which is available at +* https://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + + +#include "platform/aas/mw/log/test/stream_capture/stream_capture.h" +#include +#include + +namespace bmw +{ +namespace mw +{ +namespace log +{ +namespace +{ + +class StreamCaptureFixture : public ::testing::Test +{ +}; + +TEST_F(StreamCaptureFixture, StdoutIsCaptured) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of capturing the standard output"); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + StreamCapture stc; + stc.StartCapturingStdout(); + std::string test_str = "hello world!!"; + std::cout << test_str; + EXPECT_EQ(stc.FetchCapturedStream(), test_str); +} + +TEST_F(StreamCaptureFixture, StderrIsCaptured) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the ability of capturing the standard error"); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + StreamCapture stc; + stc.StartCapturingStderr(); + std::string test_str = "hello world!!"; + std::cerr << test_str; + EXPECT_EQ(stc.FetchCapturedStream(), test_str); +} + +TEST_F(StreamCaptureFixture, ExitIsCalledIfFetchIsCalledBeforeCapturing) +{ + RecordProperty("ASIL", "B"); + RecordProperty("Description", "Verify the in-ability of fetching a stream before capturing it."); + RecordProperty("TestType", "Interface test"); + RecordProperty("DerivationTechnique", "Generation and analysis of equivalence classes"); + + StreamCapture stc; + EXPECT_DEATH(stc.FetchCapturedStream(), "Capture not started"); +} + +} // namespace +} // namespace log +} // namespace mw +} // namespace bmw