feat: add log levels to logger

Replace log_message with log_debug/info/warn/err and a level filter
controlled by logger_set_level. Extract the printf sink into
log_write so CMock can intercept it in test_logger. Add 9 tests
covering emit and suppression behaviour per level.
This commit is contained in:
2026-05-09 20:43:32 +02:00
parent bee8424782
commit 701c644408
12 changed files with 128 additions and 15 deletions
+2
View File
@@ -91,6 +91,8 @@ to the custom scripts instead of system-installed packages.
- Use conventional commit prefixes (`feat:`, `fix:`, `docs:`, `chore:`, - Use conventional commit prefixes (`feat:`, `fix:`, `docs:`, `chore:`,
etc.) etc.)
- Separate subject from body with a blank line - Separate subject from body with a blank line
- Do **not** add yourself as a co-author (`Co-Authored-By:` trailers are
forbidden)
Example: Example:
+1 -1
View File
@@ -16,7 +16,7 @@ add_subdirectory(ctdd)
add_executable(main main.c) add_executable(main main.c)
target_include_directories(main PRIVATE .) target_include_directories(main PRIVATE .)
target_compile_features(main PRIVATE c_std_23) target_compile_features(main PRIVATE c_std_23)
target_link_libraries(main PRIVATE ctdd_str ctdd_report ctdd_logger) target_link_libraries(main PRIVATE ctdd_str ctdd_report ctdd_logger ctdd_log_write)
# Testing # Testing
enable_testing() enable_testing()
+7 -2
View File
@@ -2,12 +2,17 @@ add_library(ctdd_str STATIC str.c)
target_include_directories(ctdd_str PUBLIC "${CMAKE_SOURCE_DIR}") target_include_directories(ctdd_str PUBLIC "${CMAKE_SOURCE_DIR}")
target_compile_features(ctdd_str PRIVATE c_std_23) target_compile_features(ctdd_str PRIVATE c_std_23)
# Reporter — calls log_message(); symbol resolved by the final binary # Reporter — calls log_info(); symbol resolved by the final binary
add_library(ctdd_report STATIC report.c) add_library(ctdd_report STATIC report.c)
target_include_directories(ctdd_report PUBLIC "${CMAKE_SOURCE_DIR}") target_include_directories(ctdd_report PUBLIC "${CMAKE_SOURCE_DIR}")
target_compile_features(ctdd_report PRIVATE c_std_23) target_compile_features(ctdd_report PRIVATE c_std_23)
# Real log_message implementation — linked into production binaries only # Level-filtering logger — calls log_write(); symbol resolved by the final binary
add_library(ctdd_logger STATIC logger.c) add_library(ctdd_logger STATIC logger.c)
target_include_directories(ctdd_logger PUBLIC "${CMAKE_SOURCE_DIR}") target_include_directories(ctdd_logger PUBLIC "${CMAKE_SOURCE_DIR}")
target_compile_features(ctdd_logger PRIVATE c_std_23) target_compile_features(ctdd_logger PRIVATE c_std_23)
# Real log_write implementation — linked into production binaries only
add_library(ctdd_log_write STATIC log_write.c)
target_include_directories(ctdd_log_write PUBLIC "${CMAKE_SOURCE_DIR}")
target_compile_features(ctdd_log_write PRIVATE c_std_23)
+6
View File
@@ -0,0 +1,6 @@
#include "ctdd/log_write.h"
#include <stdio.h>
void log_write(char const* msg) {
printf("%s\n", msg);
}
+3
View File
@@ -0,0 +1,3 @@
#pragma once
void log_write(char const* msg);
+14 -2
View File
@@ -1,6 +1,18 @@
#include "ctdd/logger.h" #include "ctdd/logger.h"
#include "ctdd/log_write.h"
#include <stdio.h> #include <stdio.h>
void log_message(char const* msg) { static log_level s_level = LOG_DEBUG;
printf("[LOG] %s\n", msg);
void logger_set_level(log_level level) { s_level = level; }
static void emit(char const* prefix, char const* msg) {
char buf[512];
snprintf(buf, sizeof(buf), "[%s] %s", prefix, msg);
log_write(buf);
} }
void log_debug(char const* msg) { if (s_level <= LOG_DEBUG) emit("DEBUG", msg); }
void log_info(char const* msg) { if (s_level <= LOG_INFO) emit("INFO", msg); }
void log_warn(char const* msg) { if (s_level <= LOG_WARN) emit("WARN", msg); }
void log_err(char const* msg) { if (s_level <= LOG_ERROR) emit("ERROR", msg); }
+7 -1
View File
@@ -1,3 +1,9 @@
#pragma once #pragma once
void log_message(char const* msg); typedef enum { LOG_DEBUG = 0, LOG_INFO, LOG_WARN, LOG_ERROR, LOG_NONE } log_level;
void logger_set_level(log_level level);
void log_debug(char const* msg);
void log_info(char const* msg);
void log_warn(char const* msg);
void log_err(char const* msg);
+1 -1
View File
@@ -5,5 +5,5 @@
void report_value(char const* label, int value) { void report_value(char const* label, int value) {
char buf[256]; char buf[256];
snprintf(buf, sizeof(buf), "%s: %d", label, value); snprintf(buf, sizeof(buf), "%s: %d", label, value);
log_message(buf); log_info(buf);
} }
+1 -1
View File
@@ -13,7 +13,7 @@ int main([[maybe_unused]]int argc, [[maybe_unused]]char const* argv[]) {
char upper[32]; char upper[32];
str_upper(upper, "hello, tdd", sizeof(upper)); str_upper(upper, "hello, tdd", sizeof(upper));
log_message(upper); log_info(upper);
return 0; return 0;
} }
+9 -3
View File
@@ -25,8 +25,6 @@ function(cmock_generate_mock target header)
VERBATIM VERBATIM
) )
target_sources("${target}" PRIVATE "${mock_src}") target_sources("${target}" PRIVATE "${mock_src}")
# MOCK_GEN_DIR for the generated header; header_dir so the generated
# #include "logger.h" resolves without the ctdd/ prefix
target_include_directories("${target}" PRIVATE "${MOCK_GEN_DIR}" "${header_dir}") target_include_directories("${target}" PRIVATE "${MOCK_GEN_DIR}" "${header_dir}")
endfunction() endfunction()
@@ -37,10 +35,18 @@ target_link_libraries(test_str PRIVATE ctdd_str Unity::Unity)
target_compile_features(test_str PRIVATE c_std_23) target_compile_features(test_str PRIVATE c_std_23)
add_test(NAME test_str COMMAND test_str) add_test(NAME test_str COMMAND test_str)
# report tests — CMock-generated mock for log_message # report tests — mocks logger.h so log_info calls are intercepted
add_executable(test_report test_report.c) add_executable(test_report test_report.c)
target_include_directories(test_report PRIVATE "${CMAKE_SOURCE_DIR}") target_include_directories(test_report PRIVATE "${CMAKE_SOURCE_DIR}")
target_link_libraries(test_report PRIVATE ctdd_report Unity::Unity CMock::CMock) target_link_libraries(test_report PRIVATE ctdd_report Unity::Unity CMock::CMock)
target_compile_features(test_report PRIVATE c_std_23) target_compile_features(test_report PRIVATE c_std_23)
cmock_generate_mock(test_report "${CMAKE_SOURCE_DIR}/ctdd/logger.h") cmock_generate_mock(test_report "${CMAKE_SOURCE_DIR}/ctdd/logger.h")
add_test(NAME test_report COMMAND test_report) add_test(NAME test_report COMMAND test_report)
# logger tests — mocks log_write.h so output calls are intercepted
add_executable(test_logger test_logger.c)
target_include_directories(test_logger PRIVATE "${CMAKE_SOURCE_DIR}")
target_link_libraries(test_logger PRIVATE ctdd_logger Unity::Unity CMock::CMock)
target_compile_features(test_logger PRIVATE c_std_23)
cmock_generate_mock(test_logger "${CMAKE_SOURCE_DIR}/ctdd/log_write.h")
add_test(NAME test_logger COMMAND test_logger)
+73
View File
@@ -0,0 +1,73 @@
#include "unity.h"
#include "ctdd/logger.h"
#include "Mocklog_write.h"
void setUp(void) { Mocklog_write_Init(); logger_set_level(LOG_DEBUG); }
void tearDown(void) { Mocklog_write_Verify(); Mocklog_write_Destroy(); }
void test_log_debug_emits_at_debug_level(void) {
log_write_Expect("[DEBUG] hello");
log_debug("hello");
}
void test_log_info_emits_at_debug_level(void) {
log_write_Expect("[INFO] world");
log_info("world");
}
void test_log_warn_emits_at_warn_level(void) {
logger_set_level(LOG_WARN);
log_write_Expect("[WARN] alert");
log_warn("alert");
}
void test_log_err_emits_at_warn_level(void) {
logger_set_level(LOG_WARN);
log_write_Expect("[ERROR] fatal");
log_err("fatal");
}
void test_log_debug_suppressed_at_info_level(void) {
logger_set_level(LOG_INFO);
log_debug("silent");
}
void test_log_info_suppressed_at_warn_level(void) {
logger_set_level(LOG_WARN);
log_info("silent");
}
void test_log_warn_suppressed_at_error_level(void) {
logger_set_level(LOG_ERROR);
log_warn("silent");
}
void test_log_none_suppresses_all(void) {
logger_set_level(LOG_NONE);
log_debug("silent");
log_info("silent");
log_warn("silent");
log_err("silent");
}
void test_level_can_be_raised_then_lowered(void) {
logger_set_level(LOG_ERROR);
log_info("silent");
logger_set_level(LOG_DEBUG);
log_write_Expect("[INFO] now visible");
log_info("now visible");
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_log_debug_emits_at_debug_level);
RUN_TEST(test_log_info_emits_at_debug_level);
RUN_TEST(test_log_warn_emits_at_warn_level);
RUN_TEST(test_log_err_emits_at_warn_level);
RUN_TEST(test_log_debug_suppressed_at_info_level);
RUN_TEST(test_log_info_suppressed_at_warn_level);
RUN_TEST(test_log_warn_suppressed_at_error_level);
RUN_TEST(test_log_none_suppresses_all);
RUN_TEST(test_level_can_be_raised_then_lowered);
return UNITY_END();
}
+4 -4
View File
@@ -6,22 +6,22 @@ void setUp(void) { Mocklogger_Init(); }
void tearDown(void) { Mocklogger_Verify(); Mocklogger_Destroy(); } void tearDown(void) { Mocklogger_Verify(); Mocklogger_Destroy(); }
void test_report_formats_label_and_value(void) { void test_report_formats_label_and_value(void) {
log_message_Expect("count: 42"); log_info_Expect("count: 42");
report_value("count", 42); report_value("count", 42);
} }
void test_report_negative_value(void) { void test_report_negative_value(void) {
log_message_Expect("score: -5"); log_info_Expect("score: -5");
report_value("score", -5); report_value("score", -5);
} }
void test_report_zero(void) { void test_report_zero(void) {
log_message_Expect("total: 0"); log_info_Expect("total: 0");
report_value("total", 0); report_value("total", 0);
} }
void test_report_calls_log_exactly_once(void) { void test_report_calls_log_exactly_once(void) {
log_message_Expect("x: 1"); log_info_Expect("x: 1");
report_value("x", 1); report_value("x", 1);
} }