test: add Logger implementation and test coverage

This commit is contained in:
user
2026-07-18 23:41:00 +04:00
parent 428d74aeb3
commit f3b0f449b1
6 changed files with 412 additions and 0 deletions
+10
View File
@@ -14,19 +14,24 @@ BUILD_DIR := build
RESULT_TEST_BIN := $(BUILD_DIR)/result_test RESULT_TEST_BIN := $(BUILD_DIR)/result_test
LOGGER_TEST_BIN := $(BUILD_DIR)/logger_test
SCOPED_FILE_TEST_BIN := $(BUILD_DIR)/scoped_file_test SCOPED_FILE_TEST_BIN := $(BUILD_DIR)/scoped_file_test
RESULT_TEST_SRC := tests/app/core/result_test.cpp 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 SCOPED_FILE_TEST_SRC := tests/helpers/scoped_file_test.cpp
LOGGER_SRC := logger/src/logger.cpp
.PHONY: all test clean .PHONY: all test clean
all: test all: test
test: $(RESULT_TEST_BIN) \ test: $(RESULT_TEST_BIN) \
$(LOGGER_TEST_BIN) \
$(SCOPED_FILE_TEST_BIN) $(SCOPED_FILE_TEST_BIN)
./$(RESULT_TEST_BIN) ./$(RESULT_TEST_BIN)
./$(LOGGER_TEST_BIN)
./$(SCOPED_FILE_TEST_BIN) ./$(SCOPED_FILE_TEST_BIN)
@@ -35,6 +40,11 @@ $(RESULT_TEST_BIN): $(RESULT_TEST_SRC)
$(CXX) $(CXXFLAGS) $< -o $@ $(LDFLAGS) $(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) $(SCOPED_FILE_TEST_BIN): $(SCOPED_FILE_TEST_SRC)
@mkdir -p $(BUILD_DIR) @mkdir -p $(BUILD_DIR)
$(CXX) $(CXXFLAGS) $< -o $@ $(LDFLAGS) $(CXX) $(CXXFLAGS) $< -o $@ $(LDFLAGS)
+7
View File
@@ -0,0 +1,7 @@
#pragma once
namespace logger {
enum class LogLevel { Debug = 0, Info = 1, Error = 2 };
} // namespace logger
+122
View File
@@ -0,0 +1,122 @@
#pragma once
#include <string>
#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
+29
View File
@@ -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
+121
View File
@@ -0,0 +1,121 @@
#include <cassert>
#include <fstream>
#include <string>
#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;
}
+123
View File
@@ -0,0 +1,123 @@
#include <cassert>
#include <filesystem>
#include <fstream>
#include <string>
#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;
}