diff --git a/Makefile b/Makefile index 4ba2ed8..7e419f1 100644 --- a/Makefile +++ b/Makefile @@ -14,19 +14,24 @@ BUILD_DIR := build RESULT_TEST_BIN := $(BUILD_DIR)/result_test +LOGGER_TEST_BIN := $(BUILD_DIR)/logger_test SCOPED_FILE_TEST_BIN := $(BUILD_DIR)/scoped_file_test RESULT_TEST_SRC := tests/app/core/result_test.cpp +LOGGER_TEST_SRC := tests/app/core/logger_test.cpp SCOPED_FILE_TEST_SRC := tests/helpers/scoped_file_test.cpp +LOGGER_SRC := logger/src/logger.cpp .PHONY: all test clean all: test test: $(RESULT_TEST_BIN) \ + $(LOGGER_TEST_BIN) \ $(SCOPED_FILE_TEST_BIN) ./$(RESULT_TEST_BIN) + ./$(LOGGER_TEST_BIN) ./$(SCOPED_FILE_TEST_BIN) @@ -35,6 +40,11 @@ $(RESULT_TEST_BIN): $(RESULT_TEST_SRC) $(CXX) $(CXXFLAGS) $< -o $@ $(LDFLAGS) +$(LOGGER_TEST_BIN): $(LOGGER_TEST_SRC) $(LOGGER_SRC) + @mkdir -p $(BUILD_DIR) + $(CXX) $(CXXFLAGS) $^ -o $@ $(LDFLAGS) + + $(SCOPED_FILE_TEST_BIN): $(SCOPED_FILE_TEST_SRC) @mkdir -p $(BUILD_DIR) $(CXX) $(CXXFLAGS) $< -o $@ $(LDFLAGS) diff --git a/logger/include/log_level.hpp b/logger/include/log_level.hpp new file mode 100644 index 0000000..cbcfea3 --- /dev/null +++ b/logger/include/log_level.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace logger { + +enum class LogLevel { Debug = 0, Info = 1, Error = 2 }; + +} // namespace logger diff --git a/logger/include/logger.hpp b/logger/include/logger.hpp new file mode 100644 index 0000000..949cf61 --- /dev/null +++ b/logger/include/logger.hpp @@ -0,0 +1,122 @@ +#pragma once + +#include + +#include "log_level.hpp" +#include "result.hpp" + +namespace logger { + +/** + * @brief Класс для ведения журналов (логирования) в файл. + * + * Logger предоставляет интерфейс для записи сообщений с различными + * уровнями важности. + * + * Объект Logger владеет своим ресурсом и не может быть скопирован или + * перемещён, чтобы исключить неоднозначность владения журналом. + * + * Методы with* используют неизменяемый подход: + * они создают новый экземпляр Logger с изменённой конфигурацией. + */ +class Logger { +public: + /** + * @brief Создаёт объект логгера. + * + * @param fileName Имя файла, в который будут записываться сообщения. + * @param defaultLevel Уровень логирования по умолчанию. + */ + Logger(const std::string &fileName, LogLevel defaultLevel); + + /** + * @brief Освобождает ресурсы, используемые логгером. + */ + ~Logger(); + + /** + * @brief Запрещает создание копии Logger. + * + * Logger управляет уникальным ресурсом и не может иметь + * несколько владельцев. + */ + Logger(const Logger &) = delete; + + /** + * @brief Запрещает копирующее присваивание. + */ + Logger &operator=(const Logger &) = delete; + + /** + * @brief Запрещает перемещение Logger. + */ + Logger(Logger &&) = delete; + + /** + * @brief Запрещает перемещающее присваивание. + */ + Logger &operator=(Logger &&) = delete; + + /** + * @brief Записывает сообщение в журнал. + * + * В случае успешной записи возвращается успешный Status. + * При ошибке открытия файла или записи возвращается Status + * с описанием ошибки. + * + * @param level Уровень важности сообщения. + * @param message Текст сообщения. + * + * @return Статус выполнения операции. + */ + core::Status writeLog(LogLevel level, const std::string &message); + + /** + * @brief Создаёт новый Logger с изменённым уровнем логирования. + * + * Текущий объект не изменяется. + * + * @param level Новый уровень логирования. + * + * @return Новый объект Logger. + */ + Logger withLogLevel(LogLevel level) const; + + /** + * @brief Создаёт новый Logger с изменённым именем файла. + * + * Текущий объект не изменяется. + * + * @param fileName Новое имя файла журнала. + * + * @return Новый объект Logger. + */ + Logger withFileName(const std::string &fileName) const; + + /** + * @brief Возвращает текущий уровень логирования. + * + * @return Уровень логирования. + */ + LogLevel getLogLevel() const; + + /** + * @brief Возвращает имя файла журнала. + * + * @return Имя файла. + */ + const std::string &getFileName() const; + +private: + /** + * @brief Имя файла журнала. + */ + std::string fileName_; + + /** + * @brief Уровень логирования по умолчанию. + */ + LogLevel defaultLevel_; +}; + +} // namespace logger diff --git a/logger/src/logger.cpp b/logger/src/logger.cpp new file mode 100644 index 0000000..78f7da0 --- /dev/null +++ b/logger/src/logger.cpp @@ -0,0 +1,29 @@ +// logger.cpp + +#include "logger.hpp" + +namespace logger { + +Logger::Logger(const std::string &fileName, LogLevel defaultLevel) + : fileName_(fileName), defaultLevel_(defaultLevel) {} + +Logger::~Logger() = default; + +core::Status Logger::writeLog(LogLevel /*level*/, + const std::string & /*message*/) { + return core::Status::Success(true); +} + +Logger Logger::withLogLevel(LogLevel level) const { + return Logger(fileName_, level); +} + +Logger Logger::withFileName(const std::string &fileName) const { + return Logger(fileName, defaultLevel_); +} + +LogLevel Logger::getLogLevel() const { return defaultLevel_; } + +const std::string &Logger::getFileName() const { return fileName_; } + +} // namespace logger diff --git a/tests/app/core/logger_test.cpp b/tests/app/core/logger_test.cpp new file mode 100644 index 0000000..cc5d8ee --- /dev/null +++ b/tests/app/core/logger_test.cpp @@ -0,0 +1,121 @@ +#include +#include +#include + +#include "logger.hpp" + +namespace { + +void testLoggerCreation() { + logger::Logger logger("test.log", logger::LogLevel::Info); + + assert(logger.getFileName() == "test.log"); + assert(logger.getLogLevel() == logger::LogLevel::Info); +} + +void testWithLogLevel() { + logger::Logger logger("test.log", logger::LogLevel::Info); + + auto debugLogger = logger.withLogLevel(logger::LogLevel::Debug); + + // Исходный объект не изменился + assert(logger.getLogLevel() == logger::LogLevel::Info); + + // Новый объект получил новый уровень + assert(debugLogger.getLogLevel() == logger::LogLevel::Debug); + + // Имя файла сохранилось + assert(debugLogger.getFileName() == "test.log"); +} + +void testWithFileName() { + logger::Logger logger("old.log", logger::LogLevel::Info); + + auto newLogger = logger.withFileName("new.log"); + + // Исходный объект не изменился + assert(logger.getFileName() == "old.log"); + + // Новый объект получил новый файл + assert(newLogger.getFileName() == "new.log"); + + // Уровень сохранился + assert(newLogger.getLogLevel() == logger::LogLevel::Info); +} + +void testWriteLogSuccess() { + const std::string fileName = "logger_test.log"; + + logger::Logger logger(fileName, logger::LogLevel::Info); + + auto result = logger.writeLog(logger::LogLevel::Info, "Application started"); + + assert(result.isSuccess()); + assert(result.getValue()); + + std::ifstream file(fileName); + + assert(file.is_open()); + + std::string content; + std::getline(file, content); + + assert(content.find("Application started") != std::string::npos); + + file.close(); + + std::remove(fileName.c_str()); +} + +void testWriteLogFailure() { + logger::Logger logger("/invalid/path/logger.log", logger::LogLevel::Info); + + auto result = logger.writeLog(logger::LogLevel::Info, "message"); + + assert(result.isFailure()); + assert(!result.getError().empty()); +} + +void testMultipleWrites() { + const std::string fileName = "multiple.log"; + + logger::Logger logger(fileName, logger::LogLevel::Info); + + assert(logger.writeLog(logger::LogLevel::Info, "first").isSuccess()); + + assert(logger.writeLog(logger::LogLevel::Error, "second").isSuccess()); + + std::ifstream file(fileName); + + assert(file.is_open()); + + std::string line1; + std::string line2; + + std::getline(file, line1); + std::getline(file, line2); + + assert(line1.find("first") != std::string::npos); + assert(line2.find("second") != std::string::npos); + + file.close(); + + std::remove(fileName.c_str()); +} + +void runTests() { + testLoggerCreation(); + testWithLogLevel(); + testWithFileName(); + testWriteLogSuccess(); + testWriteLogFailure(); + testMultipleWrites(); +} + +} // namespace + +int main() { + runTests(); + + return 0; +} diff --git a/tests/logger/logger_test.cpp b/tests/logger/logger_test.cpp new file mode 100644 index 0000000..04f5b39 --- /dev/null +++ b/tests/logger/logger_test.cpp @@ -0,0 +1,123 @@ +#include +#include +#include +#include + +#include "logger.hpp" +#include "scoped_file.hpp" + +namespace { + +void testLoggerCreation() { + logger::Logger logger("test.log", logger::LogLevel::Info); + + assert(logger.getFileName() == "test.log"); + assert(logger.getLogLevel() == logger::LogLevel::Info); +} + +void testWithLogLevel() { + logger::Logger logger("test.log", logger::LogLevel::Info); + + auto debugLogger = logger.withLogLevel(logger::LogLevel::Debug); + + assert(logger.getLogLevel() == logger::LogLevel::Info); + assert(debugLogger.getLogLevel() == logger::LogLevel::Debug); + + assert(debugLogger.getFileName() == "test.log"); +} + +void testWithFileName() { + logger::Logger logger("old.log", logger::LogLevel::Info); + + auto newLogger = logger.withFileName("new.log"); + + assert(logger.getFileName() == "old.log"); + assert(newLogger.getFileName() == "new.log"); + + assert(newLogger.getLogLevel() == logger::LogLevel::Info); +} + +void testWriteLogSuccess() { + ScopedFile file("logger_test.log"); + + file.throwIfExists(); + + logger::Logger logger(file.getFileName(), logger::LogLevel::Info); + + auto result = logger.writeLog(logger::LogLevel::Info, "Application started"); + + assert(result.isSuccess()); + assert(result.getValue()); + + std::ifstream input(file.getFileName()); + + assert(input.is_open()); + + std::string content; + std::getline(input, content); + + assert(content.find("Application started") != std::string::npos); +} + +void testWriteLogFailure() { + ScopedFile file("logger_test_directory"); + + file.throwIfExists(); + + std::filesystem::create_directory(file.getFileName()); + + logger::Logger logger(file.getFileName(), logger::LogLevel::Info); + + auto result = logger.writeLog(logger::LogLevel::Info, "message"); + + assert(result.isFailure()); + assert(!result.getError().empty()); + + std::filesystem::remove(file.getFileName()); +} + +void testMultipleWrites() { + ScopedFile file("multiple.log"); + + file.throwIfExists(); + + logger::Logger logger(file.getFileName(), logger::LogLevel::Info); + + auto first = logger.writeLog(logger::LogLevel::Info, "first message"); + + auto second = logger.writeLog(logger::LogLevel::Error, "second message"); + + assert(first.isSuccess()); + assert(second.isSuccess()); + + std::ifstream input(file.getFileName()); + + assert(input.is_open()); + + std::string firstLine; + std::string secondLine; + + std::getline(input, firstLine); + std::getline(input, secondLine); + + assert(firstLine.find("first message") != std::string::npos); + + assert(secondLine.find("second message") != std::string::npos); +} + +void runTests() { + testLoggerCreation(); + testWithLogLevel(); + testWithFileName(); + testWriteLogSuccess(); + testWriteLogFailure(); + testMultipleWrites(); +} + +} // namespace + +int main() { + runTests(); + + return 0; +}