From bee84247820f7a06e426a62144720fe4da7d8bf0 Mon Sep 17 00:00:00 2001 From: portersky <24420859+portersky@users.noreply.github.com> Date: Sat, 9 May 2026 20:32:55 +0200 Subject: [PATCH] Inital commit --- .editorconfig | 14 +++++ .gitignore | 15 +++++ AGENTS.md | 135 +++++++++++++++++++++++++++++++++++++++++++ CMakeLists.txt | 26 +++++++++ README.md | 125 +++++++++++++++++++++++++++++++++++++++ ctdd/CMakeLists.txt | 13 +++++ ctdd/logger.c | 6 ++ ctdd/logger.h | 3 + ctdd/report.c | 9 +++ ctdd/report.h | 4 ++ ctdd/str.c | 27 +++++++++ ctdd/str.h | 7 +++ deps/FindCMock.cmake | 92 +++++++++++++++++++++++++++++ deps/FindUnity.cmake | 58 +++++++++++++++++++ deps/Flags.cmake | 27 +++++++++ deps/IDE.cmake | 17 ++++++ deps/Platform.cmake | 64 ++++++++++++++++++++ main.c | 19 ++++++ tests/CMakeLists.txt | 46 +++++++++++++++ tests/test_report.c | 35 +++++++++++ tests/test_str.c | 76 ++++++++++++++++++++++++ 21 files changed, 818 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 ctdd/CMakeLists.txt create mode 100644 ctdd/logger.c create mode 100644 ctdd/logger.h create mode 100644 ctdd/report.c create mode 100644 ctdd/report.h create mode 100644 ctdd/str.c create mode 100644 ctdd/str.h create mode 100644 deps/FindCMock.cmake create mode 100644 deps/FindUnity.cmake create mode 100644 deps/Flags.cmake create mode 100644 deps/IDE.cmake create mode 100644 deps/Platform.cmake create mode 100644 main.c create mode 100644 tests/CMakeLists.txt create mode 100644 tests/test_report.c create mode 100644 tests/test_str.c diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..cc998f7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +[*] +end_of_line = LF +charset = utf-8 +indent_style = space +indent_size = 4 +insert_final_newline = true + +[*.{yml,json,lua,md,html}] +charset = utf-8 +indent_style = space +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..71d8e2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# CMake +build +build-* +cmake-build-* + +# Editors +.vscode +.idea +.ccls +.ccls-cache +.cache +compile_commands.json + +# macOS +.DS_Store diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7358b25 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,135 @@ +# AGENTS.md + +## Project Overview + +`cuber` is an OpenGL 3D renderer with multiple scenes. + +## Build System + +- **Generator:** Ninja +- **CMake minimum:** 3.21 +- **C standard:** C23 + +### Commands + +```sh +cmake -S . -B build -G Ninja +ninja -C build # build +ninja -C build test # run tests +./build/main # run (Linux/macOS) +./build/main.exe # run (Windows) +``` + +### Dependencies + +Dependencies are managed via custom `Find*.cmake` scripts in `deps/`. +These scripts use `FetchContent` under the hood to download and build +libraries automatically. + +To add a new dependency: + +1. Add the corresponding `Find.cmake` to `deps/` +2. Add `find_package( REQUIRED)` to `CMakeLists.txt` +3. Link with `::` in `target_link_libraries()` + +### Static Libraries + +The project is split into static libraries: + +- **`cbt_opengl`** — OpenGL abstraction and window management (window, + context, buffer, texture, vao, shader, descriptor) +- **`cbt_scene`** — Base scene class +- **`scenes_cube`** — Spinning cube scene implementation +- **`scenes_sphere`** — Cube-to-sphere mapped mesh with diffuse lighting + +### CMake Module Path + +`deps/` is added to `CMAKE_MODULE_PATH` so `find_package()` resolves +to the custom scripts instead of system-installed packages. + +## Coding Conventions + +- **Language:** C23 +- **Trailing return type** for function signatures (e.g. `auto fn() -> void`) +- **4-space indentation** +- **No semicolons after closing braces** for namespaces/classes +- `auto` for obvious types (e.g. `auto main(...) -> int`) +- **East const** (e.g. `char const*` not `const char*`) +- **Trailing return type** for all function definitions, including + operators (e.g. `auto operator=(T&&) noexcept -> T&`) +- **Public members first** in class declarations, private members at the + bottom +- `<>` includes only for system headers (std, OS, etc.) +- `""` includes for third-party dependencies (e.g. `fmt`, + `nlohmann/json`) +- **Naming:** `snake_case` for variables, functions, and classes +- **Naming:** `SCREAMING_SNAKE_CASE` only for macros and constants +- Include order: + 1. C++ standard library headers (``, ``, etc.) + 2. *(blank line)* + 3. C standard library headers (``, ``, etc.) + 4. *(blank line)* + 5. OS-specific headers (Windows API, POSIX, etc.) + 6. *(blank line)* + 7. Third-party dependencies (`"fmt/core.h"`, etc.) + 8. *(blank line)* + 9. Local/project headers + +## Shell Scripts + +- Always use `#!/bin/sh` shebang for shell scripts +- Scripts must be POSIX compliant (no bashisms) +- When providing commands to users: + - Windows/PowerShell: use `` ` `` for line continuation + - Unix/Linux/macOS: use `\` for line continuation + +## Commit Messages + +- Follow the 50/72 rule: + - Subject line: max 50 characters + - Body lines: wrapped at 72 characters +- Use conventional commit prefixes (`feat:`, `fix:`, `docs:`, `chore:`, + etc.) +- Separate subject from body with a blank line + +Example: + +``` +feat: add stopwatch timer + +Replace Hello World with a live stopwatch that prints elapsed time +in HH:MM:SS.mmm format, updating every 10ms with color output. +``` + +## Documentation (Markdown) + +- Wrap normal text and lists at **max 80 columns** (for readability in + terminals and editors). +- **Exceptions**: Tables and code blocks (```` ``` ````) can exceed 80 + columns when formatting requires it (e.g. trees, alignment). +- Use standard Markdown: `**bold**`, `` `inline code` ``, `##` headings, + `-` or numbered lists, fenced code blocks with language hints + (```` ```cpp ````, ```` ```sh ````). +- Keep examples concise, up-to-date, and self-documenting. +- This file (`AGENTS.md`) follows its own rules. + +## Source Layout + +```text +ctdd/ + str.h / str.c Pure string utilities (no dependencies) + report.h / report.c Formats a value and calls log_message() + logger.h / logger.c Real log_message — printf to stdout +main.c Entry point +tests/ + test_str.c Unity state-based tests for ctdd/str + test_report.c Interaction-based tests using CMock +deps/ + FindUnity.cmake Fetches Unity v2.6.1 via ZIP + FindCMock.cmake Fetches CMock v2.6.0 via ZIP +``` + +## Platform Support + +The project supports Windows, Linux, Emscripten, and Android via +`Platform.cmake` and `Flags.cmake` in `deps/`. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..25c4b2f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.21) +project(ctdd VERSION 0.1.0) + +# CMake configuration +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/deps") + +# Platform flags +include(Platform) +include(Flags) + +# Example Library +add_subdirectory(ctdd) + +# Main executable +add_executable(main main.c) +target_include_directories(main PRIVATE .) +target_compile_features(main PRIVATE c_std_23) +target_link_libraries(main PRIVATE ctdd_str ctdd_report ctdd_logger) + +# Testing +enable_testing() +add_subdirectory(tests) + +# IDE configuration +include(IDE) diff --git a/README.md b/README.md new file mode 100644 index 0000000..f624602 --- /dev/null +++ b/README.md @@ -0,0 +1,125 @@ +# ctdd + +A C23 project wired for test-driven development using +[Unity](https://github.com/ThrowTheSwitch/Unity) and +[CMock](https://github.com/ThrowTheSwitch/CMock). + +All dependencies are fetched automatically via CMake `FetchContent` — no +manual installation required beyond the tools listed below. + +## Requirements + +| Tool | Purpose | +| ------------------- | --------------------- | +| CMake ≥ 3.21 | Build system | +| Ninja | Build backend | +| C23 compiler | GCC 14+, Clang 18+ | +| Ruby ≥ 3.0 | CMock mock generation | + +## Build + +```sh +cmake -S . -B build -G Ninja +ninja -C build +``` + +## Test + +```sh +ninja -C build test # summary output +ctest --test-dir build -V # verbose (shows each test name) +``` + +Run a single suite directly for the clearest output: + +```sh +./build/tests/test_str.exe +./build/tests/test_report.exe +``` + +## Run + +```sh +./build/main.exe # Windows +./build/main # Linux / macOS +``` + + +## Adding a new module (TDD workflow) + +### 1. Write the header + +```c +// ctdd/counter.h +#pragma once +int counter_increment(int value); +``` + +### 2. Write a failing test + +```c +// tests/test_counter.c +#include "unity.h" +#include "ctdd/counter.h" + +void setUp(void) {} +void tearDown(void) {} + +void test_increment_adds_one(void) { + TEST_ASSERT_EQUAL_INT(2, counter_increment(1)); +} + +int main(void) { + UNITY_BEGIN(); + RUN_TEST(test_increment_adds_one); + return UNITY_END(); +} +``` + +### 3. Register the test in `tests/CMakeLists.txt` + +```cmake +add_executable(test_counter test_counter.c) +target_include_directories(test_counter PRIVATE "${CMAKE_SOURCE_DIR}") +target_link_libraries(test_counter PRIVATE ctdd_counter Unity::Unity) +target_compile_features(test_counter PRIVATE c_std_23) +add_test(NAME test_counter COMMAND test_counter) +``` + +### 4. Add a stub, confirm RED, then implement GREEN + +Stub `ctdd/counter.c` with `return 0;`, run tests to see the failure, +then implement the real logic and confirm they pass. + +## Mocking a dependency + +If your module calls an external function (e.g. `log_message`), generate +a mock from its header and add it to the test target: + +```cmake +# tests/CMakeLists.txt +add_executable(test_mymodule test_mymodule.c) +target_link_libraries(test_mymodule PRIVATE ctdd_mymodule Unity::Unity CMock::CMock) +target_compile_features(test_mymodule PRIVATE c_std_23) +cmock_generate_mock(test_mymodule "${CMAKE_SOURCE_DIR}/ctdd/logger.h") +add_test(NAME test_mymodule COMMAND test_mymodule) +``` + +CMock generates `Mocklogger.h` into the build directory. Include it and +use the generated API in your test: + +```c +#include "Mocklogger.h" + +void setUp(void) { Mocklogger_Init(); } +void tearDown(void) { Mocklogger_Verify(); Mocklogger_Destroy(); } + +void test_something_logs(void) { + log_message_Expect("expected string"); + my_function_under_test(); +} +``` + +The `_Expect` call records the expected argument. CMock verifies the +actual argument matches at call time using string comparison, and +`Mocklogger_Verify` confirms the expected number of calls were made. diff --git a/ctdd/CMakeLists.txt b/ctdd/CMakeLists.txt new file mode 100644 index 0000000..23dd818 --- /dev/null +++ b/ctdd/CMakeLists.txt @@ -0,0 +1,13 @@ +add_library(ctdd_str STATIC str.c) +target_include_directories(ctdd_str PUBLIC "${CMAKE_SOURCE_DIR}") +target_compile_features(ctdd_str PRIVATE c_std_23) + +# Reporter — calls log_message(); symbol resolved by the final binary +add_library(ctdd_report STATIC report.c) +target_include_directories(ctdd_report PUBLIC "${CMAKE_SOURCE_DIR}") +target_compile_features(ctdd_report PRIVATE c_std_23) + +# Real log_message implementation — linked into production binaries only +add_library(ctdd_logger STATIC logger.c) +target_include_directories(ctdd_logger PUBLIC "${CMAKE_SOURCE_DIR}") +target_compile_features(ctdd_logger PRIVATE c_std_23) diff --git a/ctdd/logger.c b/ctdd/logger.c new file mode 100644 index 0000000..5ec1106 --- /dev/null +++ b/ctdd/logger.c @@ -0,0 +1,6 @@ +#include "ctdd/logger.h" +#include + +void log_message(char const* msg) { + printf("[LOG] %s\n", msg); +} diff --git a/ctdd/logger.h b/ctdd/logger.h new file mode 100644 index 0000000..919a6e2 --- /dev/null +++ b/ctdd/logger.h @@ -0,0 +1,3 @@ +#pragma once + +void log_message(char const* msg); diff --git a/ctdd/report.c b/ctdd/report.c new file mode 100644 index 0000000..4232430 --- /dev/null +++ b/ctdd/report.c @@ -0,0 +1,9 @@ +#include "ctdd/report.h" +#include "ctdd/logger.h" +#include + +void report_value(char const* label, int value) { + char buf[256]; + snprintf(buf, sizeof(buf), "%s: %d", label, value); + log_message(buf); +} diff --git a/ctdd/report.h b/ctdd/report.h new file mode 100644 index 0000000..fa4804b --- /dev/null +++ b/ctdd/report.h @@ -0,0 +1,4 @@ +#pragma once + +/* Formats "label: value" and forwards it to log_message(). */ +void report_value(char const* label, int value); diff --git a/ctdd/str.c b/ctdd/str.c new file mode 100644 index 0000000..84d059b --- /dev/null +++ b/ctdd/str.c @@ -0,0 +1,27 @@ +#include "ctdd/str.h" +#include +#include + +int str_starts_with(char const* s, char const* prefix) { + return strncmp(s, prefix, strlen(prefix)) == 0; +} + +int str_ends_with(char const* s, char const* suffix) { + size_t sl = strlen(s); + size_t xl = strlen(suffix); + if (xl > sl) return 0; + return strcmp(s + sl - xl, suffix) == 0; +} + +int str_count(char const* s, char c) { + int n = 0; + for (; *s; s++) if (*s == c) n++; + return n; +} + +void str_upper(char* dst, char const* src, size_t n) { + size_t i; + for (i = 0; i < n - 1 && src[i]; i++) + dst[i] = (char)toupper((unsigned char)src[i]); + dst[i] = '\0'; +} diff --git a/ctdd/str.h b/ctdd/str.h new file mode 100644 index 0000000..05f747d --- /dev/null +++ b/ctdd/str.h @@ -0,0 +1,7 @@ +#pragma once +#include + +int str_starts_with(char const* s, char const* prefix); +int str_ends_with(char const* s, char const* suffix); +int str_count(char const* s, char c); +void str_upper(char* dst, char const* src, size_t n); diff --git a/deps/FindCMock.cmake b/deps/FindCMock.cmake new file mode 100644 index 0000000..6988bb7 --- /dev/null +++ b/deps/FindCMock.cmake @@ -0,0 +1,92 @@ +# ============================================================================== +# Find CMock +# ============================================================================== +# This module fetches the CMock mocking framework (depends on Unity). +# +# Targets provided: +# CMock::CMock - The CMock library target +# +# Variables set: +# CMock_FOUND - TRUE if CMock is available +# CMock_LIBRARIES - The CMock library target (CMock::CMock) +# CMock_INCLUDE_DIR - Include directories for CMock +# CMock_VERSION - Version of CMock (if available) +# +# Generator variables (set when Ruby is found): +# CMOCK_SCRIPT - Path to lib/cmock.rb +# RUBY_EXECUTABLE - Path to ruby interpreter +# ============================================================================== + +if (DEFINED _FINDCMOCK_INCLUDED) + return() +endif() +set(_FINDCMOCK_INCLUDED TRUE) + +find_package(Unity REQUIRED) + +if (DEFINED CMock_FIND_VERSION AND NOT CMock_FIND_VERSION STREQUAL "") + set(CMOCK_VERSION "${CMock_FIND_VERSION}") +else() + set(CMOCK_VERSION "2.6.0") +endif() + +message(STATUS "Fetching CMock ${CMOCK_VERSION}") + +include(FetchContent) + +FetchContent_Declare( + cmock + URL https://github.com/ThrowTheSwitch/CMock/archive/refs/tags/v${CMOCK_VERSION}.zip + DOWNLOAD_EXTRACT_TIMESTAMP TRUE +) + +# CMock uses Meson — bypass its build system and compile src/cmock.c directly. +# FetchContent_MakeAvailable cannot be used here (no CMakeLists.txt in CMock), +# so we call FetchContent_Populate directly and opt into the old policy. +cmake_policy(PUSH) +cmake_policy(SET CMP0169 OLD) +FetchContent_GetProperties(cmock) +if (NOT cmock_POPULATED) + FetchContent_Populate(cmock) +endif() +cmake_policy(POP) + +# The Ruby generator expects vendor/unity/auto/type_sanitizer.rb — populate +# it from the Unity source we already have rather than needing a git submodule +set(_cmock_vendor_auto "${cmock_SOURCE_DIR}/vendor/unity/auto") +if (NOT EXISTS "${_cmock_vendor_auto}/type_sanitizer.rb") + file(MAKE_DIRECTORY "${_cmock_vendor_auto}") + file(COPY "${unity_SOURCE_DIR}/auto/" DESTINATION "${_cmock_vendor_auto}") +endif() + +if (NOT TARGET cmock) + add_library(cmock STATIC "${cmock_SOURCE_DIR}/src/cmock.c") + target_include_directories(cmock PUBLIC + "${cmock_SOURCE_DIR}/src" + "${unity_SOURCE_DIR}/src" + ) + target_link_libraries(cmock PUBLIC unity) +endif() + +if (NOT TARGET CMock::CMock) + add_library(CMock::CMock ALIAS cmock) +endif() + +set(CMock_FOUND TRUE) +set(CMock_LIBRARIES CMock::CMock) +set(CMock_VERSION "${CMOCK_VERSION}") +set(CMock_INCLUDE_DIR "${cmock_SOURCE_DIR}/src") +set(CMOCK_SCRIPT "${cmock_SOURCE_DIR}/lib/cmock.rb" CACHE FILEPATH "Path to CMock Ruby generator") + +if (CMock_INCLUDE_DIR AND TARGET cmock) + set_target_properties(cmock PROPERTIES + INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${CMock_INCLUDE_DIR}" + ) +endif() + +find_program(RUBY_EXECUTABLE ruby) +if (NOT RUBY_EXECUTABLE) + message(WARNING "Ruby not found — CMock code generation unavailable") +endif() + +set(CMOCK_LICENSE_FILE "${cmock_SOURCE_DIR}/LICENSE.txt" CACHE FILEPATH "Path to CMock license file") diff --git a/deps/FindUnity.cmake b/deps/FindUnity.cmake new file mode 100644 index 0000000..26907d0 --- /dev/null +++ b/deps/FindUnity.cmake @@ -0,0 +1,58 @@ +# ============================================================================== +# Find Unity +# ============================================================================== +# This module fetches the Unity unit testing framework. +# +# Targets provided: +# Unity::Unity - The Unity library target +# +# Variables set: +# Unity_FOUND - TRUE if Unity is available +# Unity_LIBRARIES - The Unity library target (Unity::Unity) +# Unity_INCLUDE_DIR - Include directories for Unity +# Unity_VERSION - Version of Unity (if available) +# ============================================================================== + +if (DEFINED _FINDUNITY_INCLUDED) + return() +endif() +set(_FINDUNITY_INCLUDED TRUE) + +if (DEFINED Unity_FIND_VERSION AND NOT Unity_FIND_VERSION STREQUAL "") + set(UNITY_VERSION "${Unity_FIND_VERSION}") +else() + set(UNITY_VERSION "2.6.1") +endif() + +message(STATUS "Fetching Unity ${UNITY_VERSION}") + +include(FetchContent) + +FetchContent_Declare( + unity + URL https://github.com/ThrowTheSwitch/Unity/archive/refs/tags/v${UNITY_VERSION}.zip + DOWNLOAD_EXTRACT_TIMESTAMP TRUE +) + +FetchContent_MakeAvailable(unity) + +if (NOT TARGET Unity::Unity) + if (TARGET unity) + add_library(Unity::Unity ALIAS unity) + else() + message(FATAL_ERROR "Could not fetch Unity; no target unity or Unity::Unity available") + endif() +endif() + +set(Unity_FOUND TRUE) +set(Unity_LIBRARIES Unity::Unity) +set(Unity_VERSION "${UNITY_VERSION}") +set(Unity_INCLUDE_DIR "${unity_SOURCE_DIR}/src") + +if (Unity_INCLUDE_DIR AND TARGET unity) + set_target_properties(unity PROPERTIES + INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${Unity_INCLUDE_DIR}" + ) +endif() + +set(UNITY_LICENSE_FILE "${unity_SOURCE_DIR}/LICENSE.txt" CACHE FILEPATH "Path to Unity license file") diff --git a/deps/Flags.cmake b/deps/Flags.cmake new file mode 100644 index 0000000..28a12cd --- /dev/null +++ b/deps/Flags.cmake @@ -0,0 +1,27 @@ +# ============================================================================== +# Compiler Flags +# ============================================================================== +# Sets BASE_OPTIONS (warning flags) and BASE_DEFINITIONS. +# Apply per-target via target_compile_options / target_compile_definitions +# to avoid polluting fetched dependencies. +# +# Requires: Platform.cmake (for IS_CLANG_OR_GCC / IS_MSVC) +# ============================================================================== + +set(BASE_DEFINITIONS "") +set(BASE_LIBRARIES "") + +set(BASE_OPTIONS "") +if (IS_CLANG_OR_GCC) + set(BASE_OPTIONS + "-Wall" + "-Wextra" + "-Werror" + ) +elseif (IS_MSVC) + set(BASE_OPTIONS + "/W4" + "/WX" + "/utf-8" + ) +endif() diff --git a/deps/IDE.cmake b/deps/IDE.cmake new file mode 100644 index 0000000..1577f6c --- /dev/null +++ b/deps/IDE.cmake @@ -0,0 +1,17 @@ +# ============================================================================== +# IDE Integration +# ============================================================================== +# Groups dependency targets into folders for Visual Studio / Xcode. +# Has no effect on the build. +# ============================================================================== + +function(set_target_folder target folder) + if (TARGET ${target}) + set_target_properties(${target} PROPERTIES FOLDER ${folder}) + endif() +endfunction() + +if (CMAKE_GENERATOR MATCHES "Visual Studio" OR CMAKE_GENERATOR MATCHES "Xcode") + set_target_folder(unity deps) + set_target_folder(cmock deps) +endif() diff --git a/deps/Platform.cmake b/deps/Platform.cmake new file mode 100644 index 0000000..5a5fdcb --- /dev/null +++ b/deps/Platform.cmake @@ -0,0 +1,64 @@ +# ============================================================================== +# Platform Detection +# ============================================================================== +# This module detects the current platform and compiler, setting IS_* variables +# that can be used throughout the build system for conditional logic. +# +# Compiler flags set: +# IS_CLANG_OR_GCC - TRUE if using Clang or GCC compiler +# IS_MSVC - TRUE if using Microsoft Visual C++ compiler +# +# Platform flags set: +# IS_WINDOWS - TRUE if building for Windows +# IS_LINUX - TRUE if building for Linux +# IS_MACOS - TRUE if building for macOS +# IS_IOS - TRUE if building for iOS +# IS_ANDROID - TRUE if building for Android +# IS_EMSCRIPTEN - TRUE if building for WebAssembly via Emscripten +# ============================================================================== + +# ------------------------------------------------------------------------------ +# Compiler Detection +# ------------------------------------------------------------------------------ +set(IS_CLANG_OR_GCC FALSE) +set(IS_MSVC FALSE) + +if(MSVC) + set(IS_MSVC TRUE) +elseif(CMAKE_C_COMPILER_ID MATCHES "Clang|GNU") + set(IS_CLANG_OR_GCC TRUE) +endif() + +# ------------------------------------------------------------------------------ +# Platform Detection +# ------------------------------------------------------------------------------ +set(IS_WINDOWS FALSE) +set(IS_LINUX FALSE) +set(IS_MACOS FALSE) +set(IS_IOS FALSE) +set(IS_ANDROID FALSE) +set(IS_EMSCRIPTEN FALSE) + +if(EMSCRIPTEN) + message(STATUS "Platform: Emscripten") + set(IS_EMSCRIPTEN TRUE) +elseif(ANDROID) + message(STATUS "Platform: Android") + set(IS_ANDROID TRUE) +elseif(APPLE) + if(IOS) + message(STATUS "Platform: iOS") + set(IS_IOS TRUE) + else() + message(STATUS "Platform: macOS") + set(IS_MACOS TRUE) + endif() +elseif(WIN32) + message(STATUS "Platform: Windows") + set(IS_WINDOWS TRUE) +elseif(UNIX) + message(STATUS "Platform: Linux") + set(IS_LINUX TRUE) +else() + message(FATAL_ERROR "Unknown platform!") +endif() diff --git a/main.c b/main.c new file mode 100644 index 0000000..528b65c --- /dev/null +++ b/main.c @@ -0,0 +1,19 @@ +#include "ctdd/str.h" +#include "ctdd/report.h" +#include "ctdd/logger.h" + +int main([[maybe_unused]]int argc, [[maybe_unused]]char const* argv[]) { + char const* words[] = { "hello", "world", "ctdd", "tdd" }; + int n = 4, tdd_words = 0; + for (int i = 0; i < n; i++) { + if (str_ends_with(words[i], "tdd")) + tdd_words++; + } + report_value("words ending in 'tdd'", tdd_words); + + char upper[32]; + str_upper(upper, "hello, tdd", sizeof(upper)); + log_message(upper); + + return 0; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..87e5851 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,46 @@ +find_package(Unity REQUIRED) +find_package(CMock REQUIRED) + +set(MOCK_GEN_DIR "${CMAKE_CURRENT_BINARY_DIR}/mocks") +file(MAKE_DIRECTORY "${MOCK_GEN_DIR}") + +# Generate a CMock mock from a header and attach it to a target. +# Usage: cmock_generate_mock( ) +# CMock names generated files Mock.h/.c (capital M, no separator) +function(cmock_generate_mock target header) + if (NOT RUBY_EXECUTABLE) + message(FATAL_ERROR "Ruby is required for CMock generation") + endif() + get_filename_component(name "${header}" NAME_WE) + get_filename_component(header_dir "${header}" DIRECTORY) + set(mock_src "${MOCK_GEN_DIR}/Mock${name}.c") + set(mock_hdr "${MOCK_GEN_DIR}/Mock${name}.h") + add_custom_command( + OUTPUT "${mock_src}" "${mock_hdr}" + COMMAND "${RUBY_EXECUTABLE}" "${CMOCK_SCRIPT}" + "--mock_path=${MOCK_GEN_DIR}" + "${header}" + DEPENDS "${header}" + COMMENT "CMock: generating Mock${name}" + VERBATIM + ) + 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}") +endfunction() + +# str tests — pure functions, no mock needed +add_executable(test_str test_str.c) +target_include_directories(test_str PRIVATE "${CMAKE_SOURCE_DIR}") +target_link_libraries(test_str PRIVATE ctdd_str Unity::Unity) +target_compile_features(test_str PRIVATE c_std_23) +add_test(NAME test_str COMMAND test_str) + +# report tests — CMock-generated mock for log_message +add_executable(test_report test_report.c) +target_include_directories(test_report PRIVATE "${CMAKE_SOURCE_DIR}") +target_link_libraries(test_report PRIVATE ctdd_report Unity::Unity CMock::CMock) +target_compile_features(test_report PRIVATE c_std_23) +cmock_generate_mock(test_report "${CMAKE_SOURCE_DIR}/ctdd/logger.h") +add_test(NAME test_report COMMAND test_report) diff --git a/tests/test_report.c b/tests/test_report.c new file mode 100644 index 0000000..6f9a0a9 --- /dev/null +++ b/tests/test_report.c @@ -0,0 +1,35 @@ +#include "unity.h" +#include "ctdd/report.h" +#include "Mocklogger.h" + +void setUp(void) { Mocklogger_Init(); } +void tearDown(void) { Mocklogger_Verify(); Mocklogger_Destroy(); } + +void test_report_formats_label_and_value(void) { + log_message_Expect("count: 42"); + report_value("count", 42); +} + +void test_report_negative_value(void) { + log_message_Expect("score: -5"); + report_value("score", -5); +} + +void test_report_zero(void) { + log_message_Expect("total: 0"); + report_value("total", 0); +} + +void test_report_calls_log_exactly_once(void) { + log_message_Expect("x: 1"); + report_value("x", 1); +} + +int main(void) { + UNITY_BEGIN(); + RUN_TEST(test_report_formats_label_and_value); + RUN_TEST(test_report_negative_value); + RUN_TEST(test_report_zero); + RUN_TEST(test_report_calls_log_exactly_once); + return UNITY_END(); +} diff --git a/tests/test_str.c b/tests/test_str.c new file mode 100644 index 0000000..b0b3be3 --- /dev/null +++ b/tests/test_str.c @@ -0,0 +1,76 @@ +#include "unity.h" +#include "ctdd/str.h" + +void setUp(void) {} +void tearDown(void) {} + +void test_starts_with_match(void) { + TEST_ASSERT_TRUE(str_starts_with("hello world", "hello")); +} + +void test_starts_with_no_match(void) { + TEST_ASSERT_FALSE(str_starts_with("hello world", "world")); +} + +void test_starts_with_empty_prefix(void) { + TEST_ASSERT_TRUE(str_starts_with("hello", "")); +} + +void test_ends_with_match(void) { + TEST_ASSERT_TRUE(str_ends_with("hello world", "world")); +} + +void test_ends_with_no_match(void) { + TEST_ASSERT_FALSE(str_ends_with("hello world", "hello")); +} + +void test_ends_with_full_string(void) { + TEST_ASSERT_TRUE(str_ends_with("tdd", "tdd")); +} + +void test_count_multiple(void) { + TEST_ASSERT_EQUAL_INT(3, str_count("banana", 'a')); +} + +void test_count_none(void) { + TEST_ASSERT_EQUAL_INT(0, str_count("hello", 'z')); +} + +void test_count_single(void) { + TEST_ASSERT_EQUAL_INT(1, str_count("ctdd", 'c')); +} + +void test_upper_lowercase(void) { + char dst[16]; + str_upper(dst, "hello", sizeof(dst)); + TEST_ASSERT_EQUAL_STRING("HELLO", dst); +} + +void test_upper_mixed(void) { + char dst[16]; + str_upper(dst, "Hello World", sizeof(dst)); + TEST_ASSERT_EQUAL_STRING("HELLO WORLD", dst); +} + +void test_upper_already_upper(void) { + char dst[16]; + str_upper(dst, "TDD", sizeof(dst)); + TEST_ASSERT_EQUAL_STRING("TDD", dst); +} + +int main(void) { + UNITY_BEGIN(); + RUN_TEST(test_starts_with_match); + RUN_TEST(test_starts_with_no_match); + RUN_TEST(test_starts_with_empty_prefix); + RUN_TEST(test_ends_with_match); + RUN_TEST(test_ends_with_no_match); + RUN_TEST(test_ends_with_full_string); + RUN_TEST(test_count_multiple); + RUN_TEST(test_count_none); + RUN_TEST(test_count_single); + RUN_TEST(test_upper_lowercase); + RUN_TEST(test_upper_mixed); + RUN_TEST(test_upper_already_upper); + return UNITY_END(); +}