diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml new file mode 100644 index 0000000..7914bbd --- /dev/null +++ b/.github/workflows/test-unit.yml @@ -0,0 +1,19 @@ +name: Unit Tests + +on: + workflow_call: + +jobs: + unit-tests: + runs-on: ubuntu-latest # Or other OS if needed, e.g., windows-latest, macos-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up CMake + uses: jwlawson/actions-setup-cmake@v1.13 # Or any other reliable setup-cmake action + with: + cmake-version: "3.20.x" # Specify a version, or let it pick latest + + - name: Test + run: pwsh tests/unit/run_tests.ps1 diff --git a/.github/workflows/workflow-merge.yml b/.github/workflows/workflow-merge.yml index 6fe29a6..9103fb8 100644 --- a/.github/workflows/workflow-merge.yml +++ b/.github/workflows/workflow-merge.yml @@ -12,6 +12,9 @@ jobs: build: uses: ./.github/workflows/build.yml - test: + test-packages: needs: build uses: ./.github/workflows/test-packages.yml + + test-unit: + uses: ./.github/workflows/test-unit.yml diff --git a/.github/workflows/workflow-pr.yml b/.github/workflows/workflow-pr.yml index f3a20ad..7909008 100644 --- a/.github/workflows/workflow-pr.yml +++ b/.github/workflows/workflow-pr.yml @@ -12,6 +12,9 @@ jobs: build: uses: ./.github/workflows/build.yml - test: + test-packages: needs: build uses: ./.github/workflows/test-packages.yml + + test-unit: + uses: ./.github/workflows/test-unit.yml diff --git a/.gitignore b/.gitignore index 37a4d50..e3ee6de 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ install subprojects *.stackdump example.log +build_local_tests/ +Testing diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ad0022..1daff1a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,14 @@ target_include_directories( # building $) # when installed +include(CTest) +enable_testing() + +# ---------------------------------------------------------------------------- +# Testing +# ---------------------------------------------------------------------------- +add_subdirectory(tests/unit) + # ---------------------------------------------------------------------------- # Installing # ---------------------------------------------------------------------------- diff --git a/README.md b/README.md index 7efacb9..0adb836 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # microlog -[![latest](https://img.shields.io/github/v/tag/an-dr/microlog?filter=v*&label=latest)](https://github.com/an-dr/microlog/tags) +[![Latest](https://img.shields.io/github/v/tag/an-dr/microlog?style=flat&filter=v*&label=Release)](https://github.com/an-dr/microlog/tags) +[![Unit Tests](https://github.com/an-dr/microlog/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/an-dr/microlog/actions/workflows/unit-tests.yml) A simple customizable logging library. Features: diff --git a/microlog.code-workspace b/microlog.code-workspace index 59c7463..044d81c 100644 --- a/microlog.code-workspace +++ b/microlog.code-workspace @@ -8,7 +8,6 @@ "tasks": { "version": "2.0.0", "tasks": [ - { "label": "Clean", "type": "shell", @@ -73,6 +72,25 @@ "Build" ] }, + { + "label": "Run Tests", + "type": "shell", + "command": "powershell", + "args": [ + "-ExecutionPolicy", + "Bypass", + "-File", + "${workspaceFolder}\\tests\\unit\\run_tests.ps1" + ], + "group": { + "kind": "test", + "isDefault": false + }, + "problemMatcher": [], + "dependsOn": [ + "Build" + ] + }, ] }, "launch": { diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..2977be6 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -e # Exit immediately if a command exits with a non-zero status. + +BUILD_DIR="build_local_tests" + +echo "Configuring CMake..." +cmake -S . -B "${BUILD_DIR}" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_FLAGS="-Wall -Wextra -Werror" + +echo "Building project..." +cmake --build "${BUILD_DIR}" --config Debug + +echo "Changing to build directory: ${BUILD_DIR}" +cd "${BUILD_DIR}" + +echo "Running CTest..." +ctest -C Debug --output-on-failure + +echo "Tests completed." +cd .. # Go back to root + +exit 0 diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt new file mode 100644 index 0000000..6eac051 --- /dev/null +++ b/tests/unit/CMakeLists.txt @@ -0,0 +1,14 @@ +# CMakeLists.txt for microlog unit tests + +# Common sources and include directories +set(MICROLOG_SRC ../../src/ulog.c) +set(MICROLOG_INCLUDE_DIR ../../include) + +# --- Core Tests --- +add_executable(test_core) +target_sources(test_core PRIVATE ${MICROLOG_SRC} ut_callback.c ut_test_suite.c + test_core.c) +target_include_directories(test_core PRIVATE ${MICROLOG_INCLUDE_DIR}) +target_compile_definitions(test_core PRIVATE "ULOG_EXTRA_OUTPUTS=1" +)# Added for test_log_callback +add_test(NAME CoreTests COMMAND test_core) diff --git a/tests/unit/run_tests.ps1 b/tests/unit/run_tests.ps1 new file mode 100644 index 0000000..a4d5421 --- /dev/null +++ b/tests/unit/run_tests.ps1 @@ -0,0 +1,33 @@ +#!/usr/bin/env pwsh +# ************************************************************************* +# +# Copyright (c) 2025 Andrei Gramakov. All rights reserved. +# +# This file is licensed under the terms of the MIT license. +# For a copy, see: https://opensource.org/licenses/MIT +# +# site: https://agramakov.me +# e-mail: mail@agramakov.me +# +# ************************************************************************* + +$REPO_DIR = "$PSScriptRoot/../.." +$BUILD_DIR = "build_local_tests" + +pushd $REPO_DIR + +echo "Configuring CMake..." +cmake -S . -B "${BUILD_DIR}" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -G "Ninja" + +echo "Building project..." +cmake --build "${BUILD_DIR}" --config Debug + +echo "Changing to build directory: ${BUILD_DIR}" +cd "${BUILD_DIR}" + +echo "Running CTest..." +ctest -C Debug --output-on-failure + +echo "Tests completed." + +popd diff --git a/tests/unit/test_core.c b/tests/unit/test_core.c new file mode 100644 index 0000000..d1d36e9 --- /dev/null +++ b/tests/unit/test_core.c @@ -0,0 +1,97 @@ +#include +#include +#include +#include + +#include "ulog.h" +#include "ut_callback.h" +#include "ut_test_suite.h" + +void test_basic_logging_macros() { + log_trace("This is a TRACE message: %d", 123); + log_debug("This is a DEBUG message: %s", "test"); + log_info("This is an INFO message: %.2f", 1.23); + log_warn("This is a WARN message"); + log_error("This is an ERROR message: %x", 0xff); + log_fatal("This is a FATAL message"); + assert(ut_callback_get_message_count() == 6); + assert(strcmp(ut_callback_get_last_message(), "This is a FATAL message") == + 0); +} + +void test_ulog_set_level() { + ulog_set_level(LOG_INFO); + + log_trace("This TRACE should not be processed."); + assert(ut_callback_get_message_count() == 0); + log_debug("This DEBUG should not be processed."); + assert(ut_callback_get_message_count() == 0); + log_info("This INFO should be processed."); + assert(ut_callback_get_message_count() == 1); + log_warn("This WARN should be processed."); + assert(ut_callback_get_message_count() == 2); + log_error("This ERROR should be processed."); + assert(ut_callback_get_message_count() == 3); + log_fatal("This FATAL should be processed."); + assert(ut_callback_get_message_count() == 4); +} + +void test_ulog_set_quiet() { + ulog_set_quiet(true); + // This log_info will still trigger test_log_callback because ulog.quiet + // does not affect extra callbacks. + log_info( + "This message will trigger extra callbacks, stdout should be quiet."); + assert(ut_callback_get_message_count() == + 1); // Expect 1 because test_log_callback runs + + ulog_set_quiet(false); + // This log_info will also trigger test_log_callback. Stdout is not quiet. + log_info("This message will trigger extra callbacks, stdout is not quiet."); + assert(ut_callback_get_message_count() == + 2); // Expect 2 as test_log_callback runs again + + printf("Test: %s - Passed\n\n", __func__); + + // No ulog_remove_callback, so the callback stays registered. +} + +void setup() { + printf("Running setup...\n"); + // This function can be used to set up the environment before tests. + // For example, you might want to initialize the logger or reset states. + ulog_set_level(LOG_TRACE); // Set the default log level to TRACE + ulog_set_quiet(false); // Ensure quiet mode is off for tests + ut_callback_reset(); // Reset the callback state + printf("Setup complete.\n"); +} + +void setup_suite() { + printf("Running setup suite...\n"); + int callback_add_result = ulog_add_callback(ut_callback, NULL, LOG_TRACE); + if (callback_add_result != 0) { + fprintf(stderr, "Failed to add the primary test callback. This may " + "affect test results.\n"); + // Depending on microlog's behavior, this could be due to no slots + // (ULOG_EXTRA_OUTPUTS too small) + } + assert(callback_add_result == 0 && + "Failed to add test_log_callback for test_core.c"); + printf("Setup suite complete.\n"); +} + +int main() { + + TestSuite suite; + TestSuite_init(&suite, setup_suite, NULL, setup, NULL); + + printf("Starting unit tests for microlog core features...\n\n"); + + TestSuite_add_test(&suite, "Macros", test_basic_logging_macros); + TestSuite_add_test(&suite, "Levels", test_ulog_set_level); + TestSuite_add_test(&suite, "Quiet Mode", test_ulog_set_quiet); + TestSuite_run(&suite); + + printf("All core tests completed successfully!\n"); + return 0; +} diff --git a/tests/unit/ut_callback.c b/tests/unit/ut_callback.c new file mode 100644 index 0000000..e05c514 --- /dev/null +++ b/tests/unit/ut_callback.c @@ -0,0 +1,32 @@ +#include "ut_callback.h" +#include "ulog.h" + +static int processed_message_count = 0; +static char last_message_buffer[UT_LOG_BUFFER_SIZE] = {0}; + +// Custom log callback for tests +void ut_callback(ulog_Event *ev, void *arg) { + (void)arg; // Userdata is now 'arg', mark as unused if not used. + + processed_message_count++; + + // Format the message into our static buffer to "capture" it + // In a real scenario, be wary of buffer overflows if messages are long + // Note: ulog_Event has 'message' (format string) and 'message_format_args' + // (va_list) + vsnprintf(last_message_buffer, sizeof(last_message_buffer), ev->message, + ev->message_format_args); +} + +int ut_callback_get_message_count() { + return processed_message_count; +} + +char *ut_callback_get_last_message() { + return last_message_buffer; +} + +void ut_callback_reset() { + processed_message_count = 0; + last_message_buffer[0] = '\0'; // Clear the last message +} diff --git a/tests/unit/ut_callback.h b/tests/unit/ut_callback.h new file mode 100644 index 0000000..f27938f --- /dev/null +++ b/tests/unit/ut_callback.h @@ -0,0 +1,11 @@ +#pragma once + +#include "ulog.h" + +#define UT_LOG_BUFFER_SIZE 256 + +void ut_callback(ulog_Event *ev, void *arg); + +int ut_callback_get_message_count(); +char *ut_callback_get_last_message(); +void ut_callback_reset(); diff --git a/tests/unit/ut_test_suite.c b/tests/unit/ut_test_suite.c new file mode 100644 index 0000000..4a70e48 --- /dev/null +++ b/tests/unit/ut_test_suite.c @@ -0,0 +1,55 @@ +#include "ut_test_suite.h" +#include +#include + +#define COUNT_OF(x) (sizeof(x) / sizeof((x)[0])) + +void TestSuite_run(TestSuite *suite) { + if (suite->setup_suite) { + suite->setup_suite(); + } + + for (int i = 0; i < suite->test_count; i++) { + if (suite->setup_test) { + suite->setup_test(); + } + printf("[START] Test: %s\n", suite->test_names[i]); + if (suite->tests[i]) { + suite->tests[i](); + } + printf("[DONE ] Test: %s\n", suite->test_names[i]); + if (suite->teardown_test) { + suite->teardown_test(); + } + } + + if (suite->teardown_suite) { + suite->teardown_suite(); + } +} + +void TestSuite_add_test(TestSuite *suite, const char *test_name, + TestFunction test) { + int i = suite->test_count; + if (i < COUNT_OF(suite->tests)) { + suite->tests[i] = test; + + strncpy(suite->test_names[i], test_name, TEST_NAME_MAX_LENGTH - 1); + + // Ensure null-termination + suite->test_names[i][TEST_NAME_MAX_LENGTH - 1] = '\0'; + + // Increment the test count + suite->test_count++; + } +} + +void TestSuite_init(TestSuite *suite, void (*setup_suite)(), + void (*teardown_suite)(), void (*setup_test)(), + void (*teardown_test)()) { + suite->setup_suite = setup_suite; + suite->teardown_suite = teardown_suite; + suite->setup_test = setup_test; + suite->teardown_test = teardown_test; + suite->test_count = 0; +} diff --git a/tests/unit/ut_test_suite.h b/tests/unit/ut_test_suite.h new file mode 100644 index 0000000..e3a02a4 --- /dev/null +++ b/tests/unit/ut_test_suite.h @@ -0,0 +1,25 @@ +#pragma once + +#define TEST_NUM 32 +#define TEST_NAME_MAX_LENGTH 64 + +typedef void (*TestFunction)(); + +typedef struct { + void (*setup_suite)(); + void (*teardown_suite)(); + void (*setup_test)(); + void (*teardown_test)(); + int test_count; + TestFunction tests[TEST_NUM]; + char test_names[TEST_NUM][TEST_NAME_MAX_LENGTH]; +} TestSuite; + +void TestSuite_init(TestSuite *suite, void (*setup_suite)(), + void (*teardown_suite)(), void (*setup_test)(), + void (*teardown_test)()); + +void TestSuite_add_test(TestSuite *suite, const char *test_name, + TestFunction test); + +void TestSuite_run(TestSuite *suite);