diff --git a/score/json/examples/logging.json b/score/json/examples/logging.json index 53027f2c..ff0e1d76 100644 --- a/score/json/examples/logging.json +++ b/score/json/examples/logging.json @@ -1,6 +1,16 @@ + { - "appId": "JSON", - "appDesc": "JSON example programs", - "logMode" : "kConsole", - "logLevel": "kVerbose" + "ecuId": "ECU1", + "appId": "log", + "logLevel": "kError", + "logMode": "kConsole|kFile", + "logLevelThresholdConsole": "kInfo", + "logFilePath": "./", + "logFileSizePolicy": { + "enabled": false, + "maxFileSizeInBytes": 500, + "numberOfFiles": 2, + "overwriteExistingFiles": false, + "truncateOnRotation": false + } } diff --git a/score/mw/log/README.md b/score/mw/log/README.md index 4baec04b..8ca43fe4 100644 --- a/score/mw/log/README.md +++ b/score/mw/log/README.md @@ -102,6 +102,12 @@ in parallel * **logFilePath** -- used for file logging, if ```logMode``` includes ```kFile```, this is the directory, where the logfile ```appId.dlt``` will be put +* **logFileSizePolicy** -- an object that defines the rules for log file rotation. + * **enabled** (`true`/`false`): Enables or disables the file rotation/sizing feature. + * **maxFileSizeInBytes** (integer): The maximum size in bytes that a single log file can reach before rotation is triggered. + * **numberOfFiles** (integer): The total number of log files to use in the rotation cycle (e.g., `appId_1.dlt`, `appId_2.dlt`). + * **overwriteExistingFiles** (`true`/`false`): If `true`, allows the logger to overwrite an existing log file when it rotates back to it. If `false`, rotation will stop if the next file in the cycle already exists. + * **truncateOnRotation** (`true`/`false`): If `true`, the contents of the next log file in the cycle will be deleted (truncated) before new logs are written to it. * **logLevel** -- default value: LogLevel::kWarn, global log level threshold for the application * **logLevelThresholdConsole** -- if ```logMode``` includes ```kConsole```, diff --git a/score/mw/log/configuration/configuration.cpp b/score/mw/log/configuration/configuration.cpp index d478955a..4d75d448 100644 --- a/score/mw/log/configuration/configuration.cpp +++ b/score/mw/log/configuration/configuration.cpp @@ -208,6 +208,56 @@ void Configuration::SetDynamicDatarouterIdentifiers(const bool enable_dynamic_id { dynamic_datarouter_identifiers_ = enable_dynamic_identifiers; } +bool Configuration::IsCircularFileLogging() const noexcept +{ + return circular_file_logging_; +} + +void Configuration::SetCircularFileLogging(const bool circular_file_logging) noexcept +{ + circular_file_logging_ = circular_file_logging; +} + +std::size_t Configuration::GetMaxLogFileSizeBytes() const noexcept +{ + return max_log_file_size_bytes_; +} + +void Configuration::SetMaxLogFileSizeBytes(const std::size_t max_log_file_size_bytes) noexcept +{ + max_log_file_size_bytes_ = max_log_file_size_bytes; +} + +bool Configuration::IsOverwriteLogOnFull() const noexcept +{ + return overwrite_log_on_full_; +} + +void Configuration::SetOverwriteLogOnFull(const bool overwrite_log_on_full) noexcept +{ + // The JSON configuration key for this parameter is "overwriteLogOnFull". + overwrite_log_on_full_ = overwrite_log_on_full; +} + +std::size_t Configuration::GetNoOfLogFiles() const noexcept +{ + return no_of_log_files_; +} + +void Configuration::SetNoOfLogFiles(const std::size_t no_of_log_files) noexcept +{ + no_of_log_files_ = no_of_log_files; +} + +bool Configuration::IsTruncateOnRotation() const noexcept +{ + return truncate_on_rotation_; +} + +void Configuration::SetTruncateOnRotation(const bool truncate_on_rotation) noexcept +{ + truncate_on_rotation_ = truncate_on_rotation; +} } // namespace detail } // namespace log diff --git a/score/mw/log/configuration/configuration.h b/score/mw/log/configuration/configuration.h index 80f5be33..5bc4d8f4 100644 --- a/score/mw/log/configuration/configuration.h +++ b/score/mw/log/configuration/configuration.h @@ -84,6 +84,21 @@ class Configuration final bool GetDynamicDatarouterIdentifiers() const noexcept; void SetDynamicDatarouterIdentifiers(const bool enable_dynamic_identifiers) noexcept; + bool IsCircularFileLogging() const noexcept; + void SetCircularFileLogging(const bool) noexcept; + + std::size_t GetMaxLogFileSizeBytes() const noexcept; + void SetMaxLogFileSizeBytes(const std::size_t) noexcept; + + bool IsOverwriteLogOnFull() const noexcept; + void SetOverwriteLogOnFull(const bool) noexcept; + + std::size_t GetNoOfLogFiles() const noexcept; + void SetNoOfLogFiles(const std::size_t) noexcept; + + bool IsTruncateOnRotation() const noexcept; + void SetTruncateOnRotation(const bool) 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. @@ -138,6 +153,21 @@ class Configuration final /// \brief Toggle between dynamic datarouter identifiers. bool dynamic_datarouter_identifiers_{false}; + + /// \brief Enable circular file logging. + bool circular_file_logging_{false}; + + /// \brief Maximum log file size in bytes for circular file logging. + std::size_t max_log_file_size_bytes_{10485760}; // 10 MB default + + /// \brief Overwrite log file on full. + bool overwrite_log_on_full_{false}; + + /// \brief Number of log files to keep. + std::size_t no_of_log_files_{1}; + + /// \brief Truncate on rotation. + bool truncate_on_rotation_{false}; }; } // namespace detail diff --git a/score/mw/log/configuration/target_config_reader.cpp b/score/mw/log/configuration/target_config_reader.cpp index e5d300ab..f1331c0d 100644 --- a/score/mw/log/configuration/target_config_reader.cpp +++ b/score/mw/log/configuration/target_config_reader.cpp @@ -49,6 +49,13 @@ constexpr StringLiteral kSlotSizeBytesKey{"slotSizeBytes"}; constexpr StringLiteral kDatarouterUidKey{"datarouterUid"}; constexpr StringLiteral kDynamicDatarouterIdentifiersKey{"dynamicDatarouterIdentifiers"}; +constexpr StringLiteral kLogFileSizePolicyKey{"logFileSizePolicy"}; +constexpr StringLiteral kEnabledKey{"enabled"}; +constexpr StringLiteral kMaxFileSizeInBytesKey{"maxFileSizeInBytes"}; +constexpr StringLiteral kNumberOfFilesKey{"numberOfFiles"}; +constexpr StringLiteral kOverwriteExistingFilesKey{"overwriteExistingFiles"}; +constexpr StringLiteral kTruncateOnRotationKey{"truncateOnRotation"}; + // Suppress Coverity warning because: // 1. 'constexpr' cannot be used with std::unordered_map. // 2. Defining it as a local variable does not resolve the Coverity warning. @@ -429,6 +436,95 @@ score::ResultBlank ParseDynamicDatarouterIdentifiers(const score::json::Object& // clang-format on } +score::ResultBlank ParseCircularFileLogging(const score::json::Object& root, Configuration& config) noexcept +{ + const auto file_rotation_obj_result = GetElementAsRef(root, kLogFileSizePolicyKey); + if (!file_rotation_obj_result.has_value()) + { + return {}; // The whole fileRotation object is optional. + } + + // clang-format off + return GetElementAndThen( + file_rotation_obj_result.value().get(), + kEnabledKey, + [&config](auto value) noexcept { config.SetCircularFileLogging(value); } + ); + // clang-format on +} + +score::ResultBlank ParseMaxLogFileSizeBytes(const score::json::Object& root, Configuration& config) noexcept +{ + const auto file_rotation_obj_result = GetElementAsRef(root, kLogFileSizePolicyKey); + if (!file_rotation_obj_result.has_value()) + { + return {}; // The whole fileRotation object is optional. + } + + // Disabling clang-format to address Coverity warning: autosar_cpp14_a7_1_7_violation + // clang-format off + return GetElementAndThen( + file_rotation_obj_result.value().get(), + kMaxFileSizeInBytesKey, + [&config](auto value) noexcept { config.SetMaxLogFileSizeBytes(value); } + ); + // clang-format on +} + +score::ResultBlank ParseOverwriteLogOnFull(const score::json::Object& root, Configuration& config) noexcept +{ + const auto file_rotation_obj_result = GetElementAsRef(root, kLogFileSizePolicyKey); + if (!file_rotation_obj_result.has_value()) + { + return {}; // The whole fileRotation object is optional. + } + + // Disabling clang-format to address Coverity warning: autosar_cpp14_a7_1_7_violation + // clang-format off + return GetElementAndThen( + file_rotation_obj_result.value().get(), + kOverwriteExistingFilesKey, + [&config](auto value) noexcept { config.SetOverwriteLogOnFull(value); } + ); + // clang-format on +} + +score::ResultBlank ParseNoOfLogFiles(const score::json::Object& root, Configuration& config) noexcept +{ + const auto file_rotation_obj_result = GetElementAsRef(root, kLogFileSizePolicyKey); + if (!file_rotation_obj_result.has_value()) + { + return {}; // The whole fileRotation object is optional. + } + + // Disabling clang-format to address Coverity warning: autosar_cpp14_a7_1_7_violation + // clang-format off + return GetElementAndThen( + file_rotation_obj_result.value().get(), + kNumberOfFilesKey, + [&config](auto value) noexcept { config.SetNoOfLogFiles(value); } + ); + // clang-format on +} + +score::ResultBlank ParseTruncateOnRotation(const score::json::Object& root, Configuration& config) noexcept +{ + const auto file_rotation_obj_result = GetElementAsRef(root, kLogFileSizePolicyKey); + if (!file_rotation_obj_result.has_value()) + { + return {}; // The whole fileRotation object is optional. + } + + // Disabling clang-format to address Coverity warning: autosar_cpp14_a7_1_7_violation + // clang-format off + return GetElementAndThen( + file_rotation_obj_result.value().get(), + kTruncateOnRotationKey, + [&config](auto value) noexcept { config.SetTruncateOnRotation(value); } + ); + // clang-format on +} + void ParseConfigurationElements(const score::json::Object& root, const std::string& path, Configuration& config) noexcept { ReportOnError(ParseEcuId(root, config), path); @@ -446,6 +542,11 @@ void ParseConfigurationElements(const score::json::Object& root, const std::stri ReportOnError(ParseSlotSizeBytes(root, config), path); ReportOnError(ParseDatarouterUid(root, config), path); ReportOnError(ParseDynamicDatarouterIdentifiers(root, config), path); + ReportOnError(ParseCircularFileLogging(root, config), path); + ReportOnError(ParseMaxLogFileSizeBytes(root, config), path); + ReportOnError(ParseOverwriteLogOnFull(root, config), path); + ReportOnError(ParseNoOfLogFiles(root, config), path); + ReportOnError(ParseTruncateOnRotation(root, config), path); } // Suppress "AUTOSAR C++14 A15-5-3" rule findings: "The std::terminate() function shall not be called implicitly". diff --git a/score/mw/log/configuration/target_config_reader_test.cpp b/score/mw/log/configuration/target_config_reader_test.cpp index 89c59a29..b5d5d94d 100644 --- a/score/mw/log/configuration/target_config_reader_test.cpp +++ b/score/mw/log/configuration/target_config_reader_test.cpp @@ -516,6 +516,46 @@ TEST_F(TargetConfigReaderFixture, ConfigReaderShallFallBackToContextLogLevelDefa EXPECT_EQ(GetReader().ReadConfig()->GetContextLogLevel(), ContextLogLevelMap{}); } +TEST_F(TargetConfigReaderFixture, ConfigReaderShallParseFileRotationStructure) +{ + RecordProperty("Requirement", "N/A"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "TargetConfigReader shall parse the new nested fileRotation configuration structure correctly."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + // Create a temporary config file with the new structure + const std::string new_config_content = R"({ + "logFileSizePolicy": { + "enabled": true, + "maxFileSizeInBytes": 12345, + "numberOfFiles": 5, + "overwriteExistingFiles": true, + "truncateOnRotation": true + } + })"; + const std::string temp_file_path = "new_config.json"; + std::ofstream temp_file(temp_file_path); + temp_file << new_config_content; + temp_file.close(); + + SetConfigurationFiles({temp_file_path}); + + const auto config_result = GetReader().ReadConfig(); + ASSERT_TRUE(config_result.has_value()); + const auto& config = config_result.value(); + + EXPECT_TRUE(config.IsCircularFileLogging()); + EXPECT_EQ(config.GetMaxLogFileSizeBytes(), 12345); + EXPECT_EQ(config.GetNoOfLogFiles(), 5); + EXPECT_TRUE(config.IsOverwriteLogOnFull()); + EXPECT_TRUE(config.IsTruncateOnRotation()); + + // Clean up the temporary file + std::remove(temp_file_path.c_str()); +} + } // namespace } // namespace detail } // namespace log diff --git a/score/mw/log/detail/recorder_factory_stub.cpp b/score/mw/log/detail/recorder_factory_stub.cpp index 1c84b74c..e62140bc 100644 --- a/score/mw/log/detail/recorder_factory_stub.cpp +++ b/score/mw/log/detail/recorder_factory_stub.cpp @@ -42,9 +42,15 @@ std::unique_ptr CreateConsoleLoggingBackend(const Configuration& config LogRecord{config.GetSlotSizeInBytes()}); return std::make_unique(std::move(message_builder), STDOUT_FILENO, + std::string{}, std::move(allocator), score::os::FcntlImpl::Default(memory_resource), - score::os::Unistd::Default(memory_resource)); + score::os::Unistd::Default(memory_resource), + config.IsCircularFileLogging(), + false, // overwrite log on full + config.GetMaxLogFileSizeBytes(), + 1, // no of log files + false); // delete old log files } std::unique_ptr RecorderFactory::CreateFromConfiguration( diff --git a/score/mw/log/detail/text_recorder/BUILD b/score/mw/log/detail/text_recorder/BUILD index 6b50be3b..8a7f18dc 100644 --- a/score/mw/log/detail/text_recorder/BUILD +++ b/score/mw/log/detail/text_recorder/BUILD @@ -98,8 +98,11 @@ cc_library( "//visibility:public", ], deps = [ + "@score_baselibs//score/filesystem", "@score_baselibs//score/language/futurecpp", "@score_baselibs//score/mw/log/detail:types_and_errors", + "@score_baselibs//score/os", + "@score_baselibs//score/os:fcntl", "@score_baselibs//score/os:unistd", ], ) @@ -212,6 +215,7 @@ cc_test( deps = [ ":non_blocking_writer", "@score_baselibs//score/os/mocklib:unistd_mock", + "@score_baselibs//score/os/mocklib:fcntl_mock", #"@score_baselibs//score/mw/log/test/console_logging_environment", "@googletest//:gtest_main", ], diff --git a/score/mw/log/detail/text_recorder/console_recorder_factory.cpp b/score/mw/log/detail/text_recorder/console_recorder_factory.cpp index a9f96676..8fe87abd 100644 --- a/score/mw/log/detail/text_recorder/console_recorder_factory.cpp +++ b/score/mw/log/detail/text_recorder/console_recorder_factory.cpp @@ -40,9 +40,15 @@ std::unique_ptr ConsoleRecorderFactory::CreateConsoleLoggingBackend( return std::make_unique(std::move(message_builder), STDOUT_FILENO, + "", std::move(allocator), score::os::FcntlImpl::Default(memory_resource), - score::os::Unistd::Default(memory_resource)); + score::os::Unistd::Default(memory_resource), + false, + false, + 0, + 1, + false); } } // namespace detail diff --git a/score/mw/log/detail/text_recorder/file_output_backend.cpp b/score/mw/log/detail/text_recorder/file_output_backend.cpp index 3b6e0d77..06f9b443 100644 --- a/score/mw/log/detail/text_recorder/file_output_backend.cpp +++ b/score/mw/log/detail/text_recorder/file_output_backend.cpp @@ -25,21 +25,30 @@ namespace detail FileOutputBackend::FileOutputBackend(std::unique_ptr message_builder, const std::int32_t file_descriptor, + const std::string& file_path, std::unique_ptr> allocator, - score::cpp::pmr::unique_ptr fcntl_instance, - score::cpp::pmr::unique_ptr unistd) noexcept + score::cpp::pmr::unique_ptr fcntl, + score::cpp::pmr::unique_ptr unistd, + const bool circular_file_logging, + const bool overwrite_log_on_full, + const std::size_t max_log_file_size_bytes, + const std::size_t no_of_log_files, + const bool truncate_on_rotation) noexcept : Backend(), buffer_allocator_(std::move(allocator)), - slot_drainer_(std::move(message_builder), buffer_allocator_, file_descriptor, std::move(unistd)) + slot_drainer_(std::move(message_builder), + buffer_allocator_, + file_descriptor, + file_path, + std::move(unistd), + std::move(fcntl), + circular_file_logging, + overwrite_log_on_full, + max_log_file_size_bytes, + no_of_log_files, + truncate_on_rotation) + { - const auto flags = fcntl_instance->fcntl(file_descriptor, score::os::Fcntl::Command::kFileGetStatusFlags); - if (flags.has_value()) - { - std::ignore = fcntl_instance->fcntl( - file_descriptor, - score::os::Fcntl::Command::kFileSetStatusFlags, - flags.value() | score::os::Fcntl::Open::kNonBlocking | score::os::Fcntl::Open::kCloseOnExec); - } } score::cpp::optional FileOutputBackend::ReserveSlot() noexcept diff --git a/score/mw/log/detail/text_recorder/file_output_backend.h b/score/mw/log/detail/text_recorder/file_output_backend.h index 88fba5aa..9489f404 100644 --- a/score/mw/log/detail/text_recorder/file_output_backend.h +++ b/score/mw/log/detail/text_recorder/file_output_backend.h @@ -33,9 +33,15 @@ class FileOutputBackend final : public Backend public: FileOutputBackend(std::unique_ptr message_builder, const std::int32_t file_descriptor, + const std::string& file_path, std::unique_ptr> allocator, - score::cpp::pmr::unique_ptr fcntl_instance, - score::cpp::pmr::unique_ptr unistd) noexcept; + score::cpp::pmr::unique_ptr fcntl, + score::cpp::pmr::unique_ptr unistd, + const bool circular_file_logging, + const bool overwrite_log_on_full, + const std::size_t max_log_file_size_bytes, + const std::size_t no_of_log_files, + const bool truncate_on_rotation) 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. diff --git a/score/mw/log/detail/text_recorder/file_output_backend_test.cpp b/score/mw/log/detail/text_recorder/file_output_backend_test.cpp index 9e9968e9..ff7f1487 100644 --- a/score/mw/log/detail/text_recorder/file_output_backend_test.cpp +++ b/score/mw/log/detail/text_recorder/file_output_backend_test.cpp @@ -79,9 +79,15 @@ TEST_F(FileOutputBackendFixture, ReserveSlotShouldTriggerFlushing) auto unistd_mock = score::cpp::pmr::make_unique(memory_resource_); FileOutputBackend unit(std::move(message_builder), file_descriptor_, + std::string{}, std::move(allocator_), std::move(fcntl_mock), - std::move(unistd_mock)); + std::move(unistd_mock), + false, + false, + 0, + 1, + false); EXPECT_CALL(*raw_message_builder_mock_, GetNextSpan) .WillOnce(Return(OptionalSpan{})) @@ -105,9 +111,15 @@ TEST_F(FileOutputBackendFixture, FlushSlotShouldTriggerFlushing) const auto& slot_index = allocator_->AcquireSlotToWrite(); FileOutputBackend unit(std::move(message_builder), file_descriptor_, + std::string{}, std::move(allocator_), std::move(fcntl_mock), - std::move(unistd_mock)); + std::move(unistd_mock), + false, + false, + 0, + 1, + false); EXPECT_CALL(*raw_message_builder_mock_, GetNextSpan) .WillOnce(Return(OptionalSpan{})) // first unitialized @@ -138,9 +150,15 @@ TEST_F(FileOutputBackendFixture, DepletedAllocatorShouldCauseEmptyOptionalReturn FileOutputBackend unit(std::move(message_builder), file_descriptor_, + std::string{}, std::move(allocator_), std::move(fcntl_mock), - std::move(unistd_mock)); + std::move(unistd_mock), + false, + false, + 0, + 1, + false); EXPECT_CALL(*raw_message_builder_mock_, GetNextSpan) .WillOnce(Return(OptionalSpan{})) // first unitialized @@ -162,9 +180,15 @@ TEST_F(FileOutputBackendFixture, GetLogRecordReturnsObjectSameAsAllocatorWould) auto unistd_mock = score::cpp::pmr::make_unique(memory_resource_); FileOutputBackend unit(std::move(message_builder), file_descriptor_, + std::string{}, std::move(allocator_), std::move(fcntl_mock), - std::move(unistd_mock)); + std::move(unistd_mock), + false, + false, + 0, + 1, + false); EXPECT_CALL(*raw_message_builder_mock_, GetNextSpan).WillRepeatedly(Return(OptionalSpan{})); const auto slot = unit.ReserveSlot(); @@ -202,9 +226,15 @@ TEST_F(FileOutputBackendFixture, BackendConstructionShallCallNonBlockingFileSetu // Given construction FileOutputBackend unit(std::move(message_builder), file_descriptor_, + std::string{}, std::move(allocator_), std::move(fcntl_mock), - std::move(unistd_mock)); + std::move(unistd_mock), + false, + false, + 0, + 1, + false); } TEST_F(FileOutputBackendFixture, MissingFlagsShallSkipCallToSetupFile) @@ -229,9 +259,15 @@ TEST_F(FileOutputBackendFixture, MissingFlagsShallSkipCallToSetupFile) // Given construction FileOutputBackend unit(std::move(message_builder), file_descriptor_, + std::string{}, std::move(allocator_), std::move(fcntl_mock), - std::move(unistd_mock)); + std::move(unistd_mock), + false, + false, + 0, + 1, + false); } } // namespace diff --git a/score/mw/log/detail/text_recorder/non_blocking_writer.cpp b/score/mw/log/detail/text_recorder/non_blocking_writer.cpp index e7b0ae7d..13f120e8 100644 --- a/score/mw/log/detail/text_recorder/non_blocking_writer.cpp +++ b/score/mw/log/detail/text_recorder/non_blocking_writer.cpp @@ -11,11 +11,16 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ #include "score/mw/log/detail/text_recorder/non_blocking_writer.h" +#include "score/filesystem/path.h" +#include "score/os/fcntl.h" +#include "score/os/stat.h" +#include #if defined(__QNXNTO__) #include #endif // __QNXNTO__ + namespace score { namespace mw @@ -42,17 +47,125 @@ std::size_t NonBlockingWriter::GetMaxChunkSize() noexcept return kMaxChunkSizeSupportedByOs; } -NonBlockingWriter::NonBlockingWriter(const int32_t fileHandle, +NonBlockingWriter::NonBlockingWriter(const std::string& file_path, std::size_t max_chunk_size, - score::cpp::pmr::unique_ptr unistd) noexcept + score::cpp::pmr::unique_ptr unistd, + score::cpp::pmr::unique_ptr fcntl, + const bool circular_file_logging, + const bool overwrite_log_on_full, + const std::size_t max_log_file_size_bytes, + const std::size_t no_of_log_files, + const bool truncate_on_rotation) noexcept : unistd_{std::move(unistd)}, - file_handle_(fileHandle), + fcntl_{std::move(fcntl)}, number_of_flushed_bytes_{0U}, buffer_{nullptr, 0UL}, buffer_flushed_{Result::kWouldBlock}, - max_chunk_size_(std::min(max_chunk_size, GetMaxChunkSize())) + max_chunk_size_(std::min(max_chunk_size, GetMaxChunkSize())), + circular_file_logging_(circular_file_logging), + overwrite_log_on_full_(overwrite_log_on_full), + max_log_file_size_bytes_(max_log_file_size_bytes), + current_file_position_{0U}, + no_of_log_files_(no_of_log_files), + truncate_on_rotation_(truncate_on_rotation), + current_log_file_index_{1} +{ + const score::filesystem::Path path(file_path); + file_path_ = path.ParentPath().Native(); + file_extension_ = path.Extension().Native(); + file_name_ = path.Stem().Native(); + + std::string initial_file_path = file_path; + if (no_of_log_files_ > 1) + { + initial_file_path = file_path_ + "/" + file_name_ + "_" + std::to_string(current_log_file_index_) + file_extension_; + } + + auto open_flags = + score::os::Fcntl::Open::kReadWrite | score::os::Fcntl::Open::kCreate | score::os::Fcntl::Open::kCloseOnExec; + + // NOLINTBEGIN(score-banned-function): FileLoggingBackend is disabled in production. Argumentation: Ticket-75726 + const auto descriptor_result = fcntl_->open( + initial_file_path.data(), + open_flags, + score::os::Stat::Mode::kReadUser | score::os::Stat::Mode::kWriteUser | score::os::Stat::Mode::kReadGroup | + score::os::Stat::Mode::kReadOthers); + + if (descriptor_result.has_value()) + { + file_handle_ = descriptor_result.value(); + const auto flags = fcntl_->fcntl(file_handle_, score::os::Fcntl::Command::kFileGetStatusFlags); + if (flags.has_value()) + { + std::ignore = fcntl_->fcntl( + file_handle_, + score::os::Fcntl::Command::kFileSetStatusFlags, + flags.value() | score::os::Fcntl::Open::kNonBlocking | score::os::Fcntl::Open::kCloseOnExec); + } + } + if (circular_file_logging_ && max_log_file_size_bytes_ > 0 && file_handle_ != -1) + { + const auto file_size_or_error = unistd_->lseek(file_handle_, 0, SEEK_END); + if (file_size_or_error.has_value()) + { + current_file_position_ = static_cast(file_size_or_error.value()); + } + // If lseek fails, current_file_position_ remains 0, which is a safe fallback. + } +} + +NonBlockingWriter::NonBlockingWriter(const std::string& file_path, + std::int32_t file_descriptor, + std::size_t max_chunk_size, + score::cpp::pmr::unique_ptr unistd, + score::cpp::pmr::unique_ptr fcntl, + const bool circular_file_logging, + const bool overwrite_log_on_full, + const std::size_t max_log_file_size_bytes, + const std::size_t no_of_log_files, + const bool truncate_on_rotation) noexcept + : unistd_{std::move(unistd)}, + fcntl_{std::move(fcntl)}, + file_handle_{file_descriptor}, + number_of_flushed_bytes_{0U}, + buffer_{nullptr, 0UL}, + buffer_flushed_{Result::kWouldBlock}, + max_chunk_size_(std::min(max_chunk_size, GetMaxChunkSize())), + circular_file_logging_(circular_file_logging), + overwrite_log_on_full_(overwrite_log_on_full), + max_log_file_size_bytes_(max_log_file_size_bytes), + current_file_position_{0U}, + no_of_log_files_(no_of_log_files), + truncate_on_rotation_(truncate_on_rotation), + current_log_file_index_{1} { + const score::filesystem::Path path(file_path); + file_path_ = path.ParentPath().Native(); + file_extension_ = path.Extension().Native(); + file_name_ = path.Stem().Native(); + if (no_of_log_files_ > 1 && file_name_.size() > 2 && file_name_.substr(file_name_.size() - 2) == "_1") + { + file_name_ = file_name_.substr(0, file_name_.size() - 2); + } + const auto flags = fcntl_->fcntl(file_handle_, score::os::Fcntl::Command::kFileGetStatusFlags); + if (flags.has_value()) + { + std::ignore = fcntl_->fcntl( + file_handle_, + score::os::Fcntl::Command::kFileSetStatusFlags, + flags.value() | score::os::Fcntl::Open::kNonBlocking | score::os::Fcntl::Open::kCloseOnExec); + } + + if (circular_file_logging_ && max_log_file_size_bytes_ > 0 && file_handle_ != -1) + { + const auto file_size_or_error = unistd_->lseek(file_handle_, 0, SEEK_END); + if (file_size_or_error.has_value()) + { + current_file_position_ = static_cast(file_size_or_error.value()); + } + // If lseek fails, current_file_position_ remains 0, which is a safe fallback. + } } void NonBlockingWriter::SetSpan(const score::cpp::span& buffer) noexcept @@ -87,27 +200,279 @@ score::cpp::expected N score::cpp::expected NonBlockingWriter::InternalFlush(const uint64_t size_to_flush) noexcept { + if (file_handle_ == -1) + { + if (rotation_failed_) + { + return 0; // A rotation attempt already failed, do nothing. + } + RotateLogFile(); + if (file_handle_ == -1) + { + return 0; // Rotation failed, do nothing. + } + } + const auto buffer_size = static_cast(buffer_.size()); - if (number_of_flushed_bytes_ < buffer_size) - { - score::cpp::expected num_of_bytes_written{}; - // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) used on span which is an array - // NOLINTBEGIN(score-banned-function) it is among safety headers. - // Needs ptr to access score::cpp::span elements - // coverity[autosar_cpp14_m5_0_15_violation] - num_of_bytes_written = unistd_->write(file_handle_, &(buffer_.data()[number_of_flushed_bytes_]), size_to_flush); - // NOLINTEND(score-banned-function) it is among safety headers. - // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) used on span which is an array + if (number_of_flushed_bytes_ >= buffer_size) + { + return 0; // Nothing left to flush + } + + // --- Multi-file rotation logic --- + if (circular_file_logging_ && no_of_log_files_ > 1) + { + // If the file size is limited and the next write would exceed it, rotate the file. + if (max_log_file_size_bytes_ > 0 && (current_file_position_ + size_to_flush > max_log_file_size_bytes_)) + { + RotateLogFile(); + if (rotation_failed_) + { + return 0; // A rotation attempt just failed, do nothing. + } + } + + // After potential rotation, we write to the current file. + // We must still guard against a single write being larger than the max file size. + const uint64_t remaining_space = (max_log_file_size_bytes_ > 0) + ? (max_log_file_size_bytes_ - current_file_position_) + : size_to_flush; + const uint64_t write_size = std::min(size_to_flush, remaining_space); + + if (write_size == 0 && max_log_file_size_bytes_ > 0) + { + // This can happen if the file is exactly full. The next call will rotate. + return 0; + } + + auto num_of_bytes_written = + unistd_->write(file_handle_, &(buffer_.data()[number_of_flushed_bytes_]), write_size); + if (!num_of_bytes_written.has_value()) { return score::cpp::make_unexpected(num_of_bytes_written.error()); } + + const auto written_count = static_cast(num_of_bytes_written.value()); + number_of_flushed_bytes_ += written_count; + current_file_position_ += written_count; + return num_of_bytes_written.value(); + } + + // --- Fallback to simple write or single-file circular logging --- + + // If circular logging is not enabled, perform a simple write. + if (!circular_file_logging_ || max_log_file_size_bytes_ == 0) { + // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) + auto num_of_bytes_written = + unistd_->write(file_handle_, &(buffer_.data()[number_of_flushed_bytes_]), size_to_flush); + // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) + if (!num_of_bytes_written.has_value()) + { + return score::cpp::make_unexpected(num_of_bytes_written.error()); + } + const auto written_count = static_cast(num_of_bytes_written.value()); + number_of_flushed_bytes_ += written_count; + return num_of_bytes_written.value(); + } + + // --- Circular logging is enabled from this point on (and it's single-file mode) --- + + if (!overwrite_log_on_full_) + { + // CASE: Circular logging, NO overwrite. + if (current_file_position_ >= max_log_file_size_bytes_) + { + const std::string console_warning = "WARNING: Log file size limit reached. Logging has stopped.\n"; + unistd_->write(STDERR_FILENO, console_warning.c_str(), console_warning.length()); + return 0; // File is full, stop writing. + } + + const uint64_t remaining_space = max_log_file_size_bytes_ - current_file_position_; + const uint64_t write_size = std::min(size_to_flush, remaining_space); + + if (write_size == 0) { + return 0; + } + + // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) + auto num_of_bytes_written = + unistd_->write(file_handle_, &(buffer_.data()[number_of_flushed_bytes_]), write_size); + // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) + if (!num_of_bytes_written.has_value()) + { + return score::cpp::make_unexpected(num_of_bytes_written.error()); + } + + const auto written_count = static_cast(num_of_bytes_written.value()); + number_of_flushed_bytes_ += written_count; + current_file_position_ += written_count; + return num_of_bytes_written.value(); + } + else + { + // CASE: Circular logging, WITH overwrite. + if (current_file_position_ + size_to_flush <= max_log_file_size_bytes_) + { + // The write fits without wrapping. + // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) + auto num_of_bytes_written = + unistd_->write(file_handle_, &(buffer_.data()[number_of_flushed_bytes_]), size_to_flush); + // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) + if (!num_of_bytes_written.has_value()) + { + return score::cpp::make_unexpected(num_of_bytes_written.error()); + } + const auto written_count = static_cast(num_of_bytes_written.value()); + number_of_flushed_bytes_ += written_count; + current_file_position_ += written_count; + return num_of_bytes_written.value(); + } else { - number_of_flushed_bytes_ += static_cast(num_of_bytes_written.value()); + // The write crosses the boundary and needs to wrap. + const std::string console_warning = "WARNING: Log file size limit reached. Wrapping around.\n"; + unistd_->write(STDERR_FILENO, console_warning.c_str(), console_warning.length()); + + // Seek to the beginning of the file to overwrite. + const auto seek_result = unistd_->lseek(file_handle_, 0, SEEK_SET); + if (!seek_result.has_value()) + { + // If seek fails, we cannot proceed with the circular write. + rotation_failed_ = true; + return score::cpp::make_unexpected(seek_result.error()); + } + + const uint64_t write_size = std::min(size_to_flush, max_log_file_size_bytes_); + + // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) + auto num_of_bytes_written = + unistd_->write(file_handle_, &(buffer_.data()[number_of_flushed_bytes_]), write_size); + // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) + if (!num_of_bytes_written.has_value()) + { + return score::cpp::make_unexpected(num_of_bytes_written.error()); + } + + const auto written_count = static_cast(num_of_bytes_written.value()); + number_of_flushed_bytes_ += written_count; + current_file_position_ = written_count; + rotation_failed_ = false; // Success + return num_of_bytes_written.value(); + } + } +} + +void NonBlockingWriter::RotateLogFile() noexcept +{ + if (no_of_log_files_ <= 1) + { + return; + } + if (file_handle_ != -1) + { + unistd_->close(file_handle_); + file_handle_ = -1; // Invalidate handle immediately after closing. + } + + current_log_file_index_++; + if (current_log_file_index_ > no_of_log_files_) + { + current_log_file_index_ = 1; + } + + std::string next_file_path = file_path_ + "/" + file_name_ + "_" + std::to_string(current_log_file_index_) + file_extension_; + + // Check for file existence using the Unistd wrapper. + const auto access_result = unistd_->access(next_file_path.c_str(), score::os::Unistd::AccessMode::kExists); + if (!access_result.has_value() && access_result.error() != score::os::Error::Code::kNoSuchFileOrDirectory) + { + rotation_failed_ = true; + const std::string console_warning = "ERROR: Could not access next log file: " + next_file_path + "\n"; + unistd_->write(STDERR_FILENO, console_warning.c_str(), console_warning.length()); + return; + } + const bool file_exists = access_result.has_value(); + + if (file_exists && !overwrite_log_on_full_) + { + const std::string console_warning = "WARNING: Log file rotation stopped. File exists and overwrite is disabled.\n"; + unistd_->write(STDERR_FILENO, console_warning.c_str(), console_warning.length()); + current_file_position_ = 0; + rotation_failed_ = true; // Set failure flag + return; // Stop, leaving file_handle_ as -1. + } + + auto open_flags = + score::os::Fcntl::Open::kWriteOnly | score::os::Fcntl::Open::kCreate | score::os::Fcntl::Open::kCloseOnExec; + + // Only truncate if both overwrite and delete are enabled, matching user's expectation. + if (overwrite_log_on_full_ && truncate_on_rotation_) + { + open_flags |= score::os::Fcntl::Open::kTruncate; + } + + const auto descriptor_result = fcntl_->open( + next_file_path.data(), + open_flags, + score::os::Stat::Mode::kReadUser | score::os::Stat::Mode::kWriteUser | score::os::Stat::Mode::kReadGroup | + score::os::Stat::Mode::kReadOthers); + + if (descriptor_result.has_value()) + { + file_handle_ = descriptor_result.value(); + + const auto flags = fcntl_->fcntl(file_handle_, score::os::Fcntl::Command::kFileGetStatusFlags); + if (!flags.has_value()) + { + rotation_failed_ = true; + const std::string console_warning = "ERROR: Could not get flags for new log file: " + next_file_path + "\n"; + unistd_->write(STDERR_FILENO, console_warning.c_str(), console_warning.length()); + unistd_->close(file_handle_); + file_handle_ = -1; + return; + } + + auto fcntl_result = fcntl_->fcntl( + file_handle_, + score::os::Fcntl::Command::kFileSetStatusFlags, + flags.value() | score::os::Fcntl::Open::kNonBlocking | score::os::Fcntl::Open::kCloseOnExec); + + if (!fcntl_result.has_value()) + { + rotation_failed_ = true; + const std::string console_warning = "ERROR: Could not set flags for new log file: " + next_file_path + "\n"; + unistd_->write(STDERR_FILENO, console_warning.c_str(), console_warning.length()); + unistd_->close(file_handle_); + file_handle_ = -1; + return; } + + + if (file_exists && overwrite_log_on_full_ && !truncate_on_rotation_) + { + const auto seek_result = unistd_->lseek(file_handle_, 0, SEEK_SET); + if (!seek_result.has_value()) + { + rotation_failed_ = true; + const std::string console_warning = "ERROR: Could not seek to beginning of log file: " + next_file_path + "\n"; + unistd_->write(STDERR_FILENO, console_warning.c_str(), console_warning.length()); + unistd_->close(file_handle_); + file_handle_ = -1; + return; + } + } + rotation_failed_ = false; // Rotation fully succeeded } - return number_of_flushed_bytes_; + else + { + // Handle remains -1 from earlier. + const std::string console_warning = "ERROR: Could not open next log file: " + next_file_path + "\n"; + unistd_->write(STDERR_FILENO, console_warning.c_str(), console_warning.length()); + rotation_failed_ = true; // Set failure flag + } + + current_file_position_ = 0; } } // namespace detail diff --git a/score/mw/log/detail/text_recorder/non_blocking_writer.h b/score/mw/log/detail/text_recorder/non_blocking_writer.h index 10207692..ccd9b812 100644 --- a/score/mw/log/detail/text_recorder/non_blocking_writer.h +++ b/score/mw/log/detail/text_recorder/non_blocking_writer.h @@ -16,11 +16,15 @@ // 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 "score/os/unistd.h" +#include "score/os/fcntl.h" #include "score/mw/log/detail/error.h" #include +#include + #include #include +#include namespace score { @@ -44,30 +48,60 @@ class NonBlockingWriter final }; /// \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, + explicit NonBlockingWriter(const std::string& file_path, + std::size_t max_chunk_size, + score::cpp::pmr::unique_ptr unistd, + score::cpp::pmr::unique_ptr fcntl, + const bool circular_file_logging, + const bool overwrite_log_on_full, + const std::size_t max_log_file_size_bytes, + const std::size_t no_of_log_files, + const bool truncate_on_rotation) noexcept; + + explicit NonBlockingWriter(const std::string& file_path, + std::int32_t file_descriptor, std::size_t max_chunk_size, - score::cpp::pmr::unique_ptr unistd) noexcept; + score::cpp::pmr::unique_ptr unistd, + score::cpp::pmr::unique_ptr fcntl, + const bool circular_file_logging, + const bool overwrite_log_on_full, + const std::size_t max_log_file_size_bytes, + const std::size_t no_of_log_files, + const bool truncate_on_rotation) 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 score::cpp::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 score::cpp::span& buffer) noexcept; static std::size_t GetMaxChunkSize() noexcept; private: + void RotateLogFile() noexcept; score::cpp::expected InternalFlush(const uint64_t size_to_flush) noexcept; score::cpp::pmr::unique_ptr unistd_; + score::cpp::pmr::unique_ptr fcntl_; - std::int32_t file_handle_; // given file handle to write to it. + std::int32_t file_handle_{-1}; // 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. score::cpp::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_; + bool circular_file_logging_; + bool overwrite_log_on_full_; + std::size_t max_log_file_size_bytes_; + uint64_t current_file_position_; + std::size_t no_of_log_files_; + bool truncate_on_rotation_; + std::size_t current_log_file_index_{0}; + std::string file_path_; + std::string file_name_; + std::string file_extension_; + bool rotation_failed_{false}; }; } // namespace detail diff --git a/score/mw/log/detail/text_recorder/non_blocking_writer_test.cpp b/score/mw/log/detail/text_recorder/non_blocking_writer_test.cpp index b9a7f075..33c2d8a3 100644 --- a/score/mw/log/detail/text_recorder/non_blocking_writer_test.cpp +++ b/score/mw/log/detail/text_recorder/non_blocking_writer_test.cpp @@ -13,6 +13,7 @@ #include "score/mw/log/detail/text_recorder/non_blocking_writer.h" #include "score/os/mocklib/unistdmock.h" +#include "score/os/mocklib/fcntl_mock.h" #include "gtest/gtest.h" #include @@ -38,23 +39,104 @@ struct NonBlockingWriterTestFixture : ::testing::Test { auto unistd = score::cpp::pmr::make_unique(score::cpp::pmr::get_default_resource()); unistd_ = unistd.get(); - writer_ = std::make_unique(kFileDescriptor, max_chunk_size, std::move(unistd)); - // score::os::Unistd::set_testing_instance(unistd_); + auto fcntl = score::cpp::pmr::make_unique(score::cpp::pmr::get_default_resource()); + fcntl_ = fcntl.get(); + writer_ = std::make_unique("", max_chunk_size, std::move(unistd), std::move(fcntl), false, false, 0, 1, false); } void TearDown() override { writer_.reset(); unistd_ = nullptr; + fcntl_ = nullptr; } protected: std::unique_ptr writer_; score::os::UnistdMock* unistd_{}; + score::os::FcntlMock* fcntl_{}; std::int32_t kFileDescriptor{}; }; +TEST_F(NonBlockingWriterTestFixture, NonBlockingWriterWhenCircularLoggingExceedsMaxSize) +{ + RecordProperty("ParentRequirement", "SCR-861578"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "NonBlockingWrite wraps around when circular logging is enabled and max size is reached."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + auto unistd = score::cpp::pmr::make_unique(score::cpp::pmr::get_default_resource()); + unistd_ = unistd.get(); + auto fcntl = score::cpp::pmr::make_unique(score::cpp::pmr::get_default_resource()); + fcntl_ = fcntl.get(); + + EXPECT_CALL(*fcntl_, open(_, _, _)).WillOnce(Return(kFileDescriptor)); + EXPECT_CALL(*fcntl_, fcntl(kFileDescriptor, score::os::Fcntl::Command::kFileGetStatusFlags)).WillOnce(Return(score::os::Fcntl::Open{})); + EXPECT_CALL(*fcntl_, fcntl(kFileDescriptor, score::os::Fcntl::Command::kFileSetStatusFlags, _)).WillOnce(Return(score::cpp::expected_blank())); + EXPECT_CALL(*unistd_, lseek(kFileDescriptor, 0, SEEK_END)).WillOnce(Return(0)); + + writer_ = std::make_unique("", max_chunk_size, std::move(unistd), std::move(fcntl), true, true, max_chunk_size * 2, 1, false); + + std::array payload{}; + const score::cpp::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::kWouldBlock, writer_->FlushIntoFile().value()); + + EXPECT_CALL(*unistd_, write(STDERR_FILENO, _, _)).WillOnce(Return(0)); + EXPECT_CALL(*unistd_, lseek(kFileDescriptor, 0, SEEK_SET)).WillOnce(Return(0)); + EXPECT_CALL(*unistd_, write(kFileDescriptor, &(buffer.data()[2*max_chunk_size]), max_chunk_size)) + .WillOnce(Return(max_chunk_size)); + ASSERT_EQ(NonBlockingWriter::Result::kDone, writer_->FlushIntoFile().value()); +} + +TEST_F(NonBlockingWriterTestFixture, NonBlockingWriterWhenNonCircularLoggingExceedsMaxSize) +{ + RecordProperty("ParentRequirement", "SCR-86157TBD"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", "NonBlockingWrite does not wrap around when circular logging is disabled."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "Analysis of requirements"); + + auto unistd = score::cpp::pmr::make_unique(score::cpp::pmr::get_default_resource()); + unistd_ = unistd.get(); + auto fcntl = score::cpp::pmr::make_unique(score::cpp::pmr::get_default_resource()); + fcntl_ = fcntl.get(); + + EXPECT_CALL(*fcntl_, open(_, _, _)).WillOnce(Return(kFileDescriptor)); + EXPECT_CALL(*fcntl_, fcntl(kFileDescriptor, score::os::Fcntl::Command::kFileGetStatusFlags)).WillOnce(Return(score::os::Fcntl::Open{})); + EXPECT_CALL(*fcntl_, fcntl(kFileDescriptor, score::os::Fcntl::Command::kFileSetStatusFlags, _)).WillOnce(Return(score::cpp::expected_blank())); + + writer_ = std::make_unique("", max_chunk_size, std::move(unistd), std::move(fcntl), false, false, max_chunk_size * 2, 1, false); + + std::array payload{}; + const score::cpp::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::kWouldBlock, writer_->FlushIntoFile().value()); + + EXPECT_CALL(*unistd_, lseek(_, _, _)).Times(0); + EXPECT_CALL(*unistd_, write(kFileDescriptor, &(buffer.data()[2*max_chunk_size]), max_chunk_size)) + .WillOnce(Return(max_chunk_size)); + ASSERT_EQ(NonBlockingWriter::Result::kDone, writer_->FlushIntoFile().value()); +} + TEST_F(NonBlockingWriterTestFixture, NonBlockingWriterWhenFlushingTwiceMaxChunkSizeShallReturnTrue) { RecordProperty("ParentRequirement", "SCR-861578"); diff --git a/score/mw/log/detail/text_recorder/slot_drainer.cpp b/score/mw/log/detail/text_recorder/slot_drainer.cpp index c069a94e..65c58437 100644 --- a/score/mw/log/detail/text_recorder/slot_drainer.cpp +++ b/score/mw/log/detail/text_recorder/slot_drainer.cpp @@ -26,11 +26,27 @@ namespace detail SlotDrainer::SlotDrainer(std::unique_ptr message_builder, std::shared_ptr> allocator, const std::int32_t file_descriptor, + const std::string& file_path, score::cpp::pmr::unique_ptr unistd, + score::cpp::pmr::unique_ptr fcntl, + const bool circular_file_logging, + const bool overwrite_log_on_full, + const std::size_t max_log_file_size_bytes, + const std::size_t no_of_log_files, + const bool truncate_on_rotation, 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)), + non_blocking_writer_(file_path, + file_descriptor, + NonBlockingWriter::GetMaxChunkSize(), + std::move(unistd), + std::move(fcntl), + circular_file_logging, + overwrite_log_on_full, + max_log_file_size_bytes, + no_of_log_files, + truncate_on_rotation), limit_slots_in_one_cycle_(limit_slots_in_one_cycle) { } diff --git a/score/mw/log/detail/text_recorder/slot_drainer.h b/score/mw/log/detail/text_recorder/slot_drainer.h index 1bdfe5fb..34826756 100644 --- a/score/mw/log/detail/text_recorder/slot_drainer.h +++ b/score/mw/log/detail/text_recorder/slot_drainer.h @@ -46,7 +46,14 @@ class SlotDrainer SlotDrainer(std::unique_ptr message_builder, std::shared_ptr> allocator, const std::int32_t file_descriptor, + const std::string& file_path, score::cpp::pmr::unique_ptr unistd, + score::cpp::pmr::unique_ptr fcntl, + const bool circular_file_logging, + const bool overwrite_log_on_full, + const std::size_t max_log_file_size_bytes, + const std::size_t no_of_log_files, + const bool truncate_on_rotation, const std::size_t limit_slots_in_one_cycle = 32UL); SlotDrainer(SlotDrainer&&) noexcept = delete; diff --git a/score/mw/log/detail/text_recorder/slot_drainer_test.cpp b/score/mw/log/detail/text_recorder/slot_drainer_test.cpp index bc2edcc8..6b7b9148 100644 --- a/score/mw/log/detail/text_recorder/slot_drainer_test.cpp +++ b/score/mw/log/detail/text_recorder/slot_drainer_test.cpp @@ -13,6 +13,7 @@ #include "score/mw/log/detail/text_recorder/slot_drainer.h" #include "score/os/mocklib/unistdmock.h" +#include "score/os/mocklib/fcntl_mock.h" #include "score/mw/log/detail/error.h" #include "score/mw/log/detail/text_recorder/mock/message_builder_mock.h" @@ -48,6 +49,9 @@ class SlotDrainerFixture : public ::testing::Test unistd_ptr_ = score::cpp::pmr::make_unique(score::cpp::pmr::get_default_resource()); unistd_mock_ = unistd_ptr_.get(); + fcntl_ptr_ = score::cpp::pmr::make_unique(score::cpp::pmr::get_default_resource()); + fcntl_mock_ = fcntl_ptr_.get(); + allocator_ = std::make_unique>(pool_size_); message_builder_mock_ = std::make_unique(); @@ -65,6 +69,8 @@ class SlotDrainerFixture : public ::testing::Test score::cpp::pmr::unique_ptr<::score::os::UnistdMock> unistd_ptr_{}; ::score::os::UnistdMock* unistd_mock_{}; + score::cpp::pmr::unique_ptr<::score::os::FcntlMock> fcntl_ptr_{}; + ::score::os::FcntlMock* fcntl_mock_{}; std::unique_ptr message_builder = nullptr; std::shared_ptr> allocator_ = nullptr; std::int32_t file_descriptor_ = 23; // random number file descriptor @@ -80,8 +86,18 @@ TEST_F(SlotDrainerFixture, TestOneWriteFileFailurePath) 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); + SlotDrainer unit(std::move(message_builder), + allocator_, + file_descriptor_, + std::string{}, + std::move(unistd_ptr_), + std::move(fcntl_ptr_), + false, + false, + 0, + 1, + false, + kLimitSlotsInOneCycle); EXPECT_CALL(*raw_message_builder_mock_, GetNextSpan) .WillOnce(Return(OptionalSpan{})) // first unitialized @@ -109,8 +125,18 @@ TEST_F(SlotDrainerFixture, IncompleteWriteFileShouldMakeFlushSpansReturnWouldBlo 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); + SlotDrainer unit(std::move(message_builder), + allocator_, + file_descriptor_, + std::string{}, + std::move(unistd_ptr_), + std::move(fcntl_ptr_), + false, + false, + 0, + 1, + false, + kLimitSlotsInOneCycle); EXPECT_CALL(*raw_message_builder_mock_, GetNextSpan) .WillOnce(Return(OptionalSpan{})) // first unitialized @@ -143,8 +169,18 @@ TEST_F(SlotDrainerFixture, TestOneSlotOneSpan) 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); + SlotDrainer unit(std::move(message_builder), + allocator_, + file_descriptor_, + std::string{}, + std::move(unistd_ptr_), + std::move(fcntl_ptr_), + false, + false, + 0, + 1, + false, + kLimitSlotsInOneCycle); // Expect sequence of calls to check if any spans from possible previous slots are remaining: EXPECT_CALL(*raw_message_builder_mock_, GetNextSpan) @@ -183,10 +219,17 @@ TEST_F(SlotDrainerFixture, TestTooManySlotsForSingleCallShallNotBeAbleToFlushAll // 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); + allocator_, + file_descriptor_, + std::string{}, + std::move(unistd_ptr_), + std::move(fcntl_ptr_), + false, + false, + 0, + 1, + false, + kLimiNumberOfSlotsProcesssedInOneCall); // Given 3 slots queued due to 'would block' returns: EXPECT_CALL(*raw_message_builder_mock_, GetNextSpan) diff --git a/score/os/BUILD b/score/os/BUILD index 000ab760..f83e0fa3 100644 --- a/score/os/BUILD +++ b/score/os/BUILD @@ -907,6 +907,12 @@ cc_library( ], ) +cc_library( + name = "os", + hdrs = glob(["*.h"]), + visibility = ["//visibility:public"], +) + cc_library( name = "version", hdrs = [