From 8e400d0f1dc71d30251187783ffb965b23014bd4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Apr 2026 23:23:23 +0000 Subject: [PATCH 1/2] Restore test coverage improvements from PR #329 - Fix Split("", ',') crash: replace std::istringstream with find-based parsing to handle empty strings safely - Add TestThreadDebugger.cpp with 22 tests covering edge cases, null args, disabled mode, report smoke tests, and macro compilation - Add scripts/coverage-report.sh for per-subsystem coverage analysis with configurable thresholds and JSON output - Enhance CI coverage job: add game modules, verbose test output, per-subsystem analysis step, and enriched PR comment with threshold pass/fail table https://claude.ai/code/session_016pN7BUHr5SksZ43YFVxvFV --- .claude/index.md | 4 +- .github/badges/files.json | 2 +- .github/badges/loc-breakdown.json | 12 +- .github/badges/loc.json | 2 +- .github/copilot-instructions.md | 2 +- .github/prompts/build-test.prompt.md | 2 +- .github/prompts/copilot-instructions.md | 2 +- .github/workflows/build.yml | 36 ++- CLAUDE.md | 4 +- README.md | 6 +- SparkEngine/Source/Utils/StringUtils.h | 13 +- Tests/CMakeLists.txt | 1 + Tests/TestThreadDebugger.cpp | 336 ++++++++++++++++++++++++ scripts/coverage-report.sh | 165 ++++++++++++ wiki/Codebase-Statistics.md | 18 +- wiki/Home.md | 6 +- wiki/Testing.md | 3 +- 17 files changed, 571 insertions(+), 43 deletions(-) create mode 100644 Tests/TestThreadDebugger.cpp create mode 100755 scripts/coverage-report.sh diff --git a/.claude/index.md b/.claude/index.md index a2f8bc0ef..7cdaa6979 100644 --- a/.claude/index.md +++ b/.claude/index.md @@ -42,7 +42,7 @@ _Read this at every session start (after git sync). Each row links to a detailed - **Physics**: Jolt Physics (migrated from Bullet3). Use `EngineContext::Get()->GetPhysics()` - **Networking**: Enabled by default (`ENABLE_NETWORKING=ON`), UDP sockets, no external deps -- **Tests**: 338 test files, 4290 tests (all pass on native Linux except 1 pre-existing MMO test) +- **Tests**: 339 test files, 4312 tests (all pass on native Linux except 1 pre-existing MMO test) - **Editor**: 59 panels, all wired including GizmoSystem, CollaborativeEditSession, CinematicSequencer, TimeOfDay, AbilityEditor, TriggerEditor, ConditionEditor, DecalEditor - **Rendering**: All 12 former stubs now have .cpp implementations. 6 RHI backends (D3D11, D3D12, Vulkan, OpenGL, Metal, NullRHI) - **Post-processing**: 14 passes (Bloom, AutoExposure, Tonemapping, ColorGrading, FXAA, DOF, MotionBlur, Vignette, ChromaticAberration, FilmGrain, LensDistortion, LightShafts, LensFlare, Sharpen) @@ -50,7 +50,7 @@ _Read this at every session start (after git sync). Each row links to a detailed - **Game modules**: 10 (SparkGame, FPS, MMO, RPG, ARPG, RTS, Racing, Platformer, OpenWorld, VisualScript) - **Infrastructure**: JobSystem wired, DeferredDeletionQueue in RHI, collision layer filtering, EntityEventBus cleanup, archetype spawn overrides - **Gameplay**: TimeOfDaySystem, AI enemies in SparkGame, WeatherSystem integration -- **Codebase**: ~510K lines of C++ across 1574 source files, 117 wiki pages +- **Codebase**: ~511K lines of C++ across 1575 source files, 117 wiki pages ### Before Writing Code diff --git a/.github/badges/files.json b/.github/badges/files.json index 52a7aeea0..eb40ffffc 100644 --- a/.github/badges/files.json +++ b/.github/badges/files.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, "label": "source files", - "message": "1574", + "message": "1575", "color": "green" } diff --git a/.github/badges/loc-breakdown.json b/.github/badges/loc-breakdown.json index fceaf43ac..d5656feda 100644 --- a/.github/badges/loc-breakdown.json +++ b/.github/badges/loc-breakdown.json @@ -1,11 +1,11 @@ { "schemaVersion": 1, - "total": 510938, - "files": 1574, - "engine": 261074, + "total": 511257, + "files": 1575, + "engine": 261079, "editor": 85303, - "game": 57841, - "tests": 104329, + "game": 57813, + "tests": 104671, "tools": 2391, - "updated": "2026-04-06T14:24:43Z" + "updated": "2026-04-06T23:22:55Z" } diff --git a/.github/badges/loc.json b/.github/badges/loc.json index 02ae6ccdf..8a6df5665 100644 --- a/.github/badges/loc.json +++ b/.github/badges/loc.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "label": "C++ lines of code", - "message": "510938", + "message": "511257", "color": "blue", "namedLogo": "cplusplus", "logoColor": "white" diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6ee90ea09..280e6a048 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -40,7 +40,7 @@ SparkConsole/ ← External debug console app (named pipe communication) Shaders/HLSL/ ← DirectX shaders (PBR, post-processing, compute) Shaders/GLSL/ ← OpenGL shaders (experimental) -Tests/ ← 4290 unit tests across 338 files, CTest integration +Tests/ ← 4312 unit tests across 339 files, CTest integration Templates/ ← Game module templates Assets/ ← Demo scenes, models, scripts ``` diff --git a/.github/prompts/build-test.prompt.md b/.github/prompts/build-test.prompt.md index fdcc4c540..14be344a8 100644 --- a/.github/prompts/build-test.prompt.md +++ b/.github/prompts/build-test.prompt.md @@ -65,7 +65,7 @@ Builds on every push/PR: Windows MSVC + Linux GCC + Linux Clang (Debug + Release ## Testing -4290 unit tests across 338 files in `Tests/` with internal framework + CTest. +4312 unit tests across 339 files in `Tests/` with internal framework + CTest. ```bash cd build && ctest --output-on-failure # all tests diff --git a/.github/prompts/copilot-instructions.md b/.github/prompts/copilot-instructions.md index 09d5ecc51..9a83e4a13 100644 --- a/.github/prompts/copilot-instructions.md +++ b/.github/prompts/copilot-instructions.md @@ -40,7 +40,7 @@ SparkConsole/ ← External debug console app (named pipe communication) Shaders/HLSL/ ← DirectX shaders (PBR, post-processing, compute) Shaders/GLSL/ ← OpenGL shaders (experimental) -Tests/ ← 4290 unit tests across 338 files, CTest integration +Tests/ ← 4312 unit tests across 339 files, CTest integration Templates/ ← Game module templates Assets/ ← Demo scenes, models, scripts ``` diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1f9385c57..5cad758aa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -755,7 +755,7 @@ jobs: # Code Coverage — GCC with lcov # =========================================================================== coverage: - name: "Code Coverage (GCC)" + name: "Code Coverage (GCC + per-subsystem thresholds)" permissions: contents: read pull-requests: write @@ -780,7 +780,7 @@ jobs: -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ -DCMAKE_CXX_FLAGS="--coverage -fprofile-update=atomic" -DCMAKE_C_FLAGS="--coverage -fprofile-update=atomic" \ -DCMAKE_EXE_LINKER_FLAGS="--coverage" \ - -DBUILD_TESTS=ON + -DBUILD_TESTS=ON -DBUILD_GAME_MODULES=ON - name: Build env: CCACHE_COMPRESS: "true" @@ -789,12 +789,16 @@ jobs: - name: Test env: SPARK_TEST_EXCLUDE: "LoadTest_" - run: cd build && ./bin/SparkTests 2>&1 | tee ../test-output.log; tail -5 ../test-output.log + run: cd build && ./bin/SparkTests --verbose 2>&1 | tee ../test-output.log; tail -5 ../test-output.log - name: Generate coverage run: | lcov --capture --directory build --output-file coverage.info --ignore-errors mismatch,mismatch,gcov,negative --rc geninfo_unexecuted_blocks=1 lcov --remove coverage.info '/usr/*' '*/ThirdParty/*' '*/Tests/*' --output-file coverage.info --ignore-errors unused,negative lcov --list coverage.info --ignore-errors unused,negative 2>&1 | tee coverage-summary.txt + - name: Per-subsystem coverage analysis + run: | + chmod +x scripts/coverage-report.sh + scripts/coverage-report.sh coverage.info --json coverage.json 2>&1 | tee subsystem-coverage.txt || true - name: Extract error summary if: failure() run: | @@ -820,12 +824,25 @@ jobs: try { const summary = fs.readFileSync('coverage-summary.txt', 'utf8'); const lines = summary.split('\n'); - // Extract the total line (last non-empty line with a percentage) - const totalLine = lines.filter(l => /Total:/.test(l) || /\d+\.\d+%/.test(l)).pop() || ''; - let body = `## Code Coverage (GCC + lcov)\n\n`; - // Get the last 30 lines which contain the per-directory breakdown const table = lines.slice(-35).join('\n'); + let body = `## Code Coverage (GCC + lcov)\n\n`; body += `\`\`\`\n${table}\n\`\`\`\n`; + + // Append per-subsystem breakdown if available + try { + const json = JSON.parse(fs.readFileSync('coverage.json', 'utf8')); + body += `\n### Per-Subsystem Coverage\n\n`; + body += `| Subsystem | Lines | Hit | Coverage | Threshold | Status |\n`; + body += `|-----------|-------|-----|----------|-----------|--------|\n`; + for (const s of json.subsystems) { + const icon = s.pass ? ':white_check_mark:' : ':x:'; + body += `| ${s.subsystem} | ${s.lines} | ${s.hit} | ${s.coverage}% | ${s.threshold}% | ${icon} |\n`; + } + body += `\n**Total: ${json.total_coverage}%** (${json.total_hit}/${json.total_lines} lines)\n`; + } catch (e2) { + // subsystem data not available + } + await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, @@ -839,7 +856,10 @@ jobs: uses: actions/upload-artifact@v7 with: name: coverage-report - path: coverage.info + path: | + coverage.info + coverage.json + subsystem-coverage.txt retention-days: ${{ env.ARTIFACT_RETENTION_DAYS }} # =========================================================================== diff --git a/CLAUDE.md b/CLAUDE.md index ed3ef3b59..08b032370 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -134,7 +134,7 @@ GameModules/SparkGameVisualScript/Source/ — Visual script game module (DLL) SparkConsole/src/ — Standalone console application SparkShaderCompiler/src/ — Shader compilation tool SparkSDK/ — Public SDK/interface headers -Tests/ — 4290 unit tests across 338 files, CTest +Tests/ — 4312 unit tests across 339 files, CTest ``` NullRHIDevice automatically activates when no GPU backend is available — engine continues in headless mode. GLAD (OpenGL loader) and SDL2 are bundled in `ThirdParty/`. SDL2 requires `libgl-dev` before CMake configure on Linux. @@ -297,7 +297,7 @@ To reproduce CI failures locally, see `.claude/knowledge/ci-reproducible-builds. | `build-windows-vs2026` | windows-latest | MSVC v144 | Debug, Release | `continue-on-error` | | `build-linux-mingw-wine` | ubuntu-24.04 | MinGW-w64 + Wine | Release | `continue-on-error` | | `build-macos` | macos-latest | Apple Clang | Debug, Release | `continue-on-error` | -| `coverage` | ubuntu-24.04 | GCC | Debug | `--coverage` + lcov | +| `coverage` | ubuntu-24.04 | GCC | Debug | `--coverage` + lcov, per-subsystem thresholds | | `clang-tidy` | ubuntu-24.04 | Clang | Debug | `continue-on-error` | | `todo-count` | ubuntu-24.04 | — | — | threshold: 20 | diff --git a/README.md b/README.md index cb78b0eed..8dda16437 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ **Quality & Testing:** -[![Tests](https://img.shields.io/badge/tests-4290_cases-brightgreen)](https://github.com/Krilliac/SparkEngine/tree/Working/Tests) +[![Tests](https://img.shields.io/badge/tests-4312_cases-brightgreen)](https://github.com/Krilliac/SparkEngine/tree/Working/Tests) [![clang--format](https://img.shields.io/badge/style-clang--format-blue)](https://github.com/Krilliac/SparkEngine/blob/Working/.clang-format) [![clang--tidy](https://img.shields.io/badge/analysis-clang--tidy-blue)](https://github.com/Krilliac/SparkEngine/blob/Working/.clang-tidy) @@ -412,7 +412,7 @@ SparkEngine/ | |-- Scenes/ # Level/scene JSON files | |-- Scripts/ # AngelScript game scripts |-- Templates/ # Game module project templates -|-- Tests/ # 4290 unit tests across 338 files (CTest + 5 sanitizers) +|-- Tests/ # 4312 unit tests across 339 files (CTest + 5 sanitizers) |-- tools/ | |-- SparkBuild.exe # Pre-built SparkBuild binary | |-- update-sparkbuild.* # Manual update scripts (ps1/sh) @@ -457,7 +457,7 @@ The following libraries are included directly in the source tree: ## Tests -4290 unit tests across 338 test files covering all major engine systems, built with a lightweight internal test framework (no external test dependencies). Integrated with CMake's CTest. +4312 unit tests across 339 test files covering all major engine systems, built with a lightweight internal test framework (no external test dependencies). Integrated with CMake's CTest. ```bash # Build and run tests diff --git a/SparkEngine/Source/Utils/StringUtils.h b/SparkEngine/Source/Utils/StringUtils.h index d977c6e25..30c8a66dd 100644 --- a/SparkEngine/Source/Utils/StringUtils.h +++ b/SparkEngine/Source/Utils/StringUtils.h @@ -105,12 +105,17 @@ namespace Spark inline std::vector Split(const std::string& str, char delimiter) { std::vector tokens; - std::string token; - std::istringstream stream(str); - while (std::getline(stream, token, delimiter)) + if (str.empty()) + return tokens; + size_t start = 0; + size_t end = str.find(delimiter); + while (end != std::string::npos) { - tokens.push_back(token); + tokens.push_back(str.substr(start, end - start)); + start = end + 1; + end = str.find(delimiter, start); } + tokens.push_back(str.substr(start)); return tokens; } diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 0c558519a..c7b67fcf9 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -202,6 +202,7 @@ add_executable(SparkTests TestJobSystem.cpp TestOpaqueHandle.cpp TestSparkError.cpp + TestThreadDebugger.cpp TestCollisionSystem.cpp TestNetworkSecurity.cpp TestSubTickInput.cpp diff --git a/Tests/TestThreadDebugger.cpp b/Tests/TestThreadDebugger.cpp new file mode 100644 index 000000000..3aaacd99c --- /dev/null +++ b/Tests/TestThreadDebugger.cpp @@ -0,0 +1,336 @@ +// TestThreadDebugger.cpp - Extended tests for ThreadDebugger +// Covers edge cases, disabled mode, null args, reports, macros, and +// multi-thread scenarios not covered by TestDebugUtilities.cpp + +#include "TestFramework.h" +#include "Utils/ThreadDebugger.h" +#include + +static void ResetDebugger() +{ + auto& dbg = Spark::ThreadDebugger::GetInstance(); + dbg.Reset(); + dbg.SetEnabled(true); +} + +// ============================================================================= +// Thread lifecycle edge cases +// ============================================================================= + +TEST(ThreadDbg_MultipleThreadsActiveCount) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + dbg.RecordThreadStart(1, "A"); + dbg.RecordThreadStart(2, "B"); + dbg.RecordThreadStart(3, "C"); + + EXPECT_EQ(dbg.GetActiveThreadCount(), (uint32_t)3); + + dbg.RecordThreadEnd(2); + EXPECT_EQ(dbg.GetActiveThreadCount(), (uint32_t)2); + + auto all = dbg.GetAllThreads(); + EXPECT_EQ(all.size(), (size_t)3); +} + +TEST(ThreadDbg_NullNameHandled) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + dbg.RecordThreadStart(50, nullptr, nullptr, 0, nullptr); + + auto threads = dbg.GetAllThreads(); + EXPECT_EQ(threads.size(), (size_t)1); + EXPECT_EQ(threads[0].name, std::string("")); + EXPECT_EQ(threads[0].creationFile, std::string("")); + EXPECT_EQ(threads[0].creationFunction, std::string("")); +} + +TEST(ThreadDbg_SetNameUnknownThread) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + // No crash on unknown thread ID + dbg.SetThreadName(999, "Ghost"); + EXPECT_EQ(dbg.GetAllThreads().size(), (size_t)0); +} + +TEST(ThreadDbg_EndUnknownThread) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + // No crash on unknown thread ID + dbg.RecordThreadEnd(777); + EXPECT_EQ(dbg.GetAllThreads().size(), (size_t)0); +} + +TEST(ThreadDbg_CreationSiteInfo) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + dbg.RecordThreadStart(100, "Worker", "main.cpp", 42, "LaunchWorker"); + + auto threads = dbg.GetAllThreads(); + EXPECT_EQ(threads[0].creationFile, std::string("main.cpp")); + EXPECT_EQ(threads[0].creationLine, 42); + EXPECT_EQ(threads[0].creationFunction, std::string("LaunchWorker")); +} + +// ============================================================================= +// Mutex contention edge cases +// ============================================================================= + +TEST(ThreadDbg_ContentionStatsTopN) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + for (int i = 0; i < 5; ++i) + { + std::string name = "Mutex" + std::to_string(i); + dbg.RecordMutexWaitBegin(name.c_str()); + dbg.RecordMutexWaitEnd(name.c_str()); + } + + auto top2 = dbg.GetContentionStats(2); + EXPECT_EQ(top2.size(), (size_t)2); +} + +TEST(ThreadDbg_TotalContentionEvents) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + dbg.RecordMutexWaitBegin("A"); + dbg.RecordMutexWaitEnd("A"); + dbg.RecordMutexWaitBegin("B"); + dbg.RecordMutexWaitEnd("B"); + dbg.RecordMutexWaitBegin("A"); + dbg.RecordMutexWaitEnd("A"); + + EXPECT_EQ(dbg.GetTotalContentionEvents(), (uint64_t)3); +} + +TEST(ThreadDbg_WaitEndWithoutBeginIgnored) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + dbg.RecordMutexWaitEnd("NeverStarted"); + EXPECT_EQ(dbg.GetTotalContentionEvents(), (uint64_t)0); +} + +TEST(ThreadDbg_NullMutexNameIgnored) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + dbg.RecordMutexWaitBegin(nullptr); + dbg.RecordMutexWaitEnd(nullptr); + dbg.RecordMutexAcquire(nullptr, 1); + dbg.RecordMutexRelease(nullptr, 1); + + EXPECT_EQ(dbg.GetTotalContentionEvents(), (uint64_t)0); +} + +TEST(ThreadDbg_PeakWaitTimeTracked) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + dbg.RecordMutexWaitBegin("PeakTest"); + dbg.RecordMutexWaitEnd("PeakTest"); + + auto stats = dbg.GetContentionStats(); + EXPECT_EQ(stats.size(), (size_t)1); + EXPECT_TRUE(stats[0].peakWaitTimeMs >= 0.0f); + EXPECT_TRUE(stats[0].avgWaitTimeMs >= 0.0f); +} + +// ============================================================================= +// Lock-order tracking +// ============================================================================= + +TEST(ThreadDbg_NoFalsePositiveSameOrder) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + // Both threads acquire in same order — no warning + dbg.RecordMutexAcquire("X", 1); + dbg.RecordMutexAcquire("Y", 1); + dbg.RecordMutexRelease("Y", 1); + dbg.RecordMutexRelease("X", 1); + + dbg.RecordMutexAcquire("X", 2); + dbg.RecordMutexAcquire("Y", 2); + dbg.RecordMutexRelease("Y", 2); + dbg.RecordMutexRelease("X", 2); + + EXPECT_EQ(dbg.GetDeadlockWarnings().size(), (size_t)0); +} + +TEST(ThreadDbg_SingleMutexNoWarning) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + dbg.RecordMutexAcquire("OnlyOne", 1); + dbg.RecordMutexRelease("OnlyOne", 1); + + EXPECT_EQ(dbg.GetDeadlockWarnings().size(), (size_t)0); +} + +TEST(ThreadDbg_ReleaseUnheldMutex) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + // No crash + dbg.RecordMutexRelease("NotHeld", 42); + EXPECT_EQ(dbg.GetDeadlockWarnings().size(), (size_t)0); +} + +TEST(ThreadDbg_ThreadEndCleansHeldMutexes) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + dbg.RecordThreadStart(1, "T1"); + dbg.RecordMutexAcquire("L1", 1); + dbg.RecordMutexAcquire("L2", 1); + + // End thread without releasing — should clean up held mutexes + dbg.RecordThreadEnd(1); + EXPECT_EQ(dbg.GetActiveThreadCount(), (uint32_t)0); +} + +// ============================================================================= +// Enabled/disabled +// ============================================================================= + +TEST(ThreadDbg_DisabledSkipsAll) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + dbg.SetEnabled(false); + EXPECT_FALSE(dbg.IsEnabled()); + + dbg.RecordThreadStart(1, "Ignored"); + dbg.RecordMutexWaitBegin("Ignored"); + dbg.RecordMutexWaitEnd("Ignored"); + dbg.RecordMutexAcquire("Ignored", 1); + dbg.RecordMutexRelease("Ignored", 1); + dbg.RecordThreadEnd(1); + + EXPECT_EQ(dbg.GetAllThreads().size(), (size_t)0); + EXPECT_EQ(dbg.GetTotalContentionEvents(), (uint64_t)0); + + dbg.SetEnabled(true); +} + +TEST(ThreadDbg_DisabledSetThreadNameIgnored) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + dbg.RecordThreadStart(1, "Before"); + dbg.SetEnabled(false); + dbg.SetThreadName(1, "ShouldNotChange"); + dbg.SetEnabled(true); + + auto threads = dbg.GetAllThreads(); + EXPECT_EQ(threads[0].name, std::string("Before")); +} + +// ============================================================================= +// Reports (smoke tests — verify they don't crash) +// ============================================================================= + +TEST(ThreadDbg_PrintThreadReportNoCrash) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + dbg.RecordThreadStart(1, "Main", "engine.cpp", 10, "Start"); + dbg.RecordThreadStart(2, "Worker"); + dbg.RecordThreadEnd(2); + + dbg.PrintThreadReport(); + EXPECT_TRUE(true); +} + +TEST(ThreadDbg_PrintContentionReportNoCrash) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + dbg.RecordMutexWaitBegin("TestLock"); + dbg.RecordMutexWaitEnd("TestLock"); + + dbg.PrintContentionReport(); + EXPECT_TRUE(true); +} + +TEST(ThreadDbg_PrintSummaryNoCrash) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + dbg.RecordThreadStart(1, "Main"); + dbg.RecordMutexWaitBegin("Lock"); + dbg.RecordMutexWaitEnd("Lock"); + + dbg.PrintSummary(); + EXPECT_TRUE(true); +} + +TEST(ThreadDbg_PrintEmptyReportsNoCrash) +{ + ResetDebugger(); + auto& dbg = Spark::ThreadDebugger::GetInstance(); + + dbg.PrintThreadReport(); + dbg.PrintContentionReport(); + dbg.PrintSummary(); + EXPECT_TRUE(true); +} + +// ============================================================================= +// GetCurrentThreadId consistency +// ============================================================================= + +TEST(ThreadDbg_CurrentThreadIdConsistent) +{ + uint64_t id1 = Spark::ThreadDebugger::GetCurrentThreadId(); + uint64_t id2 = Spark::ThreadDebugger::GetCurrentThreadId(); + EXPECT_EQ(id1, id2); + EXPECT_TRUE(id1 != 0); +} + +// ============================================================================= +// Macros +// ============================================================================= + +TEST(ThreadDbg_MacrosCompile) +{ + ResetDebugger(); + + uint64_t tid = Spark::ThreadDebugger::GetCurrentThreadId(); + SPARK_TRACK_THREAD_START(tid, "MacroThread"); + SPARK_TRACK_MUTEX_WAIT("MacroMutex"); + SPARK_TRACK_MUTEX_ACQUIRED("MacroMutex"); + SPARK_TRACK_MUTEX_ACQUIRE("MacroMutex", tid); + SPARK_TRACK_MUTEX_RELEASE("MacroMutex", tid); + SPARK_PRINT_THREAD_REPORT(); + SPARK_TRACK_THREAD_END(tid); + + EXPECT_TRUE(true); +} diff --git a/scripts/coverage-report.sh b/scripts/coverage-report.sh new file mode 100755 index 000000000..a574b2c4e --- /dev/null +++ b/scripts/coverage-report.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +# coverage-report.sh — Per-subsystem coverage analysis from lcov data +# +# Usage: scripts/coverage-report.sh [--json coverage.json] +# +# Parses lcov coverage.info and reports per-subsystem line coverage. +# Optionally outputs JSON for CI consumption. + +set -euo pipefail + +COVERAGE_FILE="${1:-coverage.info}" +JSON_OUTPUT="" +shift || true + +while [[ $# -gt 0 ]]; do + case "$1" in + --json) + JSON_OUTPUT="$2" + shift 2 + ;; + *) + shift + ;; + esac +done + +if [[ ! -f "$COVERAGE_FILE" ]]; then + echo "Error: Coverage file not found: $COVERAGE_FILE" >&2 + exit 1 +fi + +# Define subsystems and their source paths +declare -A SUBSYSTEMS=( + ["Core"]="SparkEngine/Source/Core/" + ["Graphics"]="SparkEngine/Source/Graphics/" + ["Utils"]="SparkEngine/Source/Utils/" + ["Camera"]="SparkEngine/Source/Camera/" + ["ECS"]="SparkEngine/Source/Engine/ECS/" + ["AI"]="SparkEngine/Source/Engine/AI/" + ["Animation"]="SparkEngine/Source/Engine/Animation/" + ["Networking"]="SparkEngine/Source/Engine/Networking/" + ["Physics"]="SparkEngine/Source/Engine/Physics/" + ["Audio"]="SparkEngine/Source/Engine/Audio/" + ["Scripting"]="SparkEngine/Source/Engine/Scripting/" + ["Editor"]="SparkEditor/Source/" + ["GameModules"]="GameModules/" +) + +# Define per-subsystem thresholds (minimum acceptable line coverage %) +declare -A THRESHOLDS=( + ["Core"]=40 + ["Graphics"]=30 + ["Utils"]=60 + ["Camera"]=40 + ["ECS"]=40 + ["AI"]=35 + ["Animation"]=35 + ["Networking"]=35 + ["Physics"]=35 + ["Audio"]=30 + ["Scripting"]=30 + ["Editor"]=25 + ["GameModules"]=30 +) + +echo "==============================" +echo "Per-Subsystem Coverage Report" +echo "==============================" +printf "%-15s %8s %8s %8s %6s\n" "Subsystem" "Lines" "Hit" "Missed" "Cov%" +printf "%-15s %8s %8s %8s %6s\n" "---------" "-----" "---" "------" "----" + +total_lines=0 +total_hit=0 +all_pass=true +json_entries="" + +for subsystem in $(echo "${!SUBSYSTEMS[@]}" | tr ' ' '\n' | sort); do + path="${SUBSYSTEMS[$subsystem]}" + threshold="${THRESHOLDS[$subsystem]:-0}" + + # Extract coverage data for this subsystem from lcov info + lines=0 + hit=0 + + while IFS= read -r line; do + case "$line" in + SF:*"$path"*) + in_subsystem=true + ;; + SF:*) + in_subsystem=false + ;; + DA:*) + if [[ "${in_subsystem:-false}" == "true" ]]; then + count="${line##*,}" + ((lines++)) || true + if [[ "$count" -gt 0 ]] 2>/dev/null; then + ((hit++)) || true + fi + fi + ;; + esac + done < "$COVERAGE_FILE" + + missed=$((lines - hit)) + if [[ $lines -gt 0 ]]; then + pct=$(awk "BEGIN { printf \"%.1f\", ($hit / $lines) * 100 }") + else + pct="0.0" + fi + + # Check threshold + pass="true" + icon="OK" + if [[ $lines -gt 0 ]]; then + below=$(awk "BEGIN { print ($pct < $threshold) ? 1 : 0 }") + if [[ "$below" == "1" ]]; then + pass="false" + icon="FAIL" + all_pass=false + fi + fi + + printf "%-15s %8d %8d %8d %5s%% [%s >= %d%%]\n" \ + "$subsystem" "$lines" "$hit" "$missed" "$pct" "$icon" "$threshold" + + total_lines=$((total_lines + lines)) + total_hit=$((total_hit + hit)) + + if [[ -n "$json_entries" ]]; then + json_entries+="," + fi + json_entries+=$(printf '{"subsystem":"%s","lines":%d,"hit":%d,"coverage":%s,"threshold":%d,"pass":%s}' \ + "$subsystem" "$lines" "$hit" "$pct" "$threshold" "$pass") +done + +echo "---------------------------------------------" +total_missed=$((total_lines - total_hit)) +if [[ $total_lines -gt 0 ]]; then + total_pct=$(awk "BEGIN { printf \"%.1f\", ($total_hit / $total_lines) * 100 }") +else + total_pct="0.0" +fi +printf "%-15s %8d %8d %8d %5s%%\n" "TOTAL" "$total_lines" "$total_hit" "$total_missed" "$total_pct" + +# Output JSON if requested +if [[ -n "$JSON_OUTPUT" ]]; then + cat > "$JSON_OUTPUT" < diff --git a/wiki/Testing.md b/wiki/Testing.md index 28c8b8b6c..be0ffee03 100644 --- a/wiki/Testing.md +++ b/wiki/Testing.md @@ -517,7 +517,7 @@ SDL2 must be built with OpenGL/GLX support (install `libgl-dev` *before* buildin ## Test File Inventory -*337 test files, 4290+ test cases* +*338 test files, 4312+ test cases* | Test File | Test Cases | |-----------|------------| @@ -832,6 +832,7 @@ SDL2 must be built with OpenGL/GLX support (install `libgl-dev` *before* buildin | `TestTextureCompressor` | 9 | | `TestTextureZombiePool` | 6 | | `TestThirdPartyIntegration` | 22 | +| `TestThreadDebugger` | 22 | | `TestThreadSafeQueue` | 10 | | `TestTimeOfDaySystem` | 18 | | `TestTimerManager` | 11 | From e08579f886d3f752068fb31c226fff82b3db6771 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Apr 2026 23:24:52 +0000 Subject: [PATCH 2/2] Update badge timestamp https://claude.ai/code/session_016pN7BUHr5SksZ43YFVxvFV --- .github/badges/loc-breakdown.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/badges/loc-breakdown.json b/.github/badges/loc-breakdown.json index d5656feda..70def007b 100644 --- a/.github/badges/loc-breakdown.json +++ b/.github/badges/loc-breakdown.json @@ -7,5 +7,5 @@ "game": 57813, "tests": 104671, "tools": 2391, - "updated": "2026-04-06T23:22:55Z" + "updated": "2026-04-06T23:24:32Z" }