From a7bb04f54bb0bdafa80265c82cf98a8a4ea6af6c Mon Sep 17 00:00:00 2001 From: omigamedev Date: Mon, 1 Jun 2026 08:20:58 +0200 Subject: [PATCH] Add foundation logging facade --- CMakeLists.txt | 1 + docs/modernization/build-inventory.md | 3 +- docs/modernization/roadmap.md | 8 ++- scripts/automation/platform-build.ps1 | 2 +- scripts/automation/platform-build.sh | 2 +- src/foundation/log.cpp | 93 +++++++++++++++++++++++++++ src/foundation/log.h | 67 +++++++++++++++++++ tests/CMakeLists.txt | 10 +++ tests/foundation/log_tests.cpp | 82 +++++++++++++++++++++++ 9 files changed, 262 insertions(+), 6 deletions(-) create mode 100644 src/foundation/log.cpp create mode 100644 src/foundation/log.h create mode 100644 tests/foundation/log_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 132b408..60990bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,7 @@ add_custom_target(panopainter_validate_shaders add_library(pp_foundation STATIC src/foundation/binary_stream.cpp + src/foundation/log.cpp src/foundation/parse.cpp src/foundation/trace.cpp) target_include_directories(pp_foundation diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index a5a3f4e..849b520 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -80,7 +80,8 @@ Known local toolchain state: `platform-build` automation wrapper for `pp_foundation`, `pp_assets`, `pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`, `pp_ui_core`, `pano_cli`, and their current headless test binaries, - including PPI header, paint stroke sampling, and layout XML parse coverage. + including foundation logging, PPI header, paint stroke sampling, and layout + XML parse coverage. - `panopainter_validate_shaders` validates the current combined GLSL shader files for one vertex stage marker, one fragment stage marker, valid marker order, and existing relative includes. diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 8586554..b467d3e 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -304,8 +304,9 @@ Goal: split libraries while keeping current app behavior. Status: started. `pp_foundation` exists with binary stream utilities and boundary/overread tests. It also owns strict decimal `uint32` parsing used by `pano_cli`, with rejection tests for empty, signed, mixed, and overflowing -input. A deterministic `TraceRecorder` now records component/name/thread/frame -and stroke timing spans with invalid-end tests. `pp_assets` has started with +input. A structured logging facade and deterministic `TraceRecorder` now +record component/name/thread/frame/stroke metadata with filtering and invalid-end +tests. `pp_assets` has started with PNG/JPEG signature detection plus PPI header recognition, with corrupt/truncated/unsupported tests. `pp_paint` has started with CPU reference math for the five current shader @@ -523,7 +524,7 @@ Last verified on 2026-06-01: ```powershell cmake --preset windows-msvc-default -cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pano_cli PanoPainter +cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pano_cli PanoPainter ctest --preset desktop-fast --build-config Debug powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli @@ -537,6 +538,7 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -P Results: - `pp_foundation_binary_stream_tests` passed. +- `pp_foundation_log_tests` passed. - `pp_foundation_parse_tests` passed. - `pp_foundation_trace_tests` passed. - `pp_assets_image_format_tests` passed. diff --git a/scripts/automation/platform-build.ps1 b/scripts/automation/platform-build.ps1 index 7b34425..f3239d1 100644 --- a/scripts/automation/platform-build.ps1 +++ b/scripts/automation/platform-build.ps1 @@ -1,7 +1,7 @@ [CmdletBinding()] param( [string[]]$Presets = @("android-arm64"), - [string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_core", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_parse_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_assets_ppi_header_tests", "pp_paint_blend_tests", "pp_paint_stroke_tests", "pp_document_tests", "pp_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_layout_value_tests", "pp_ui_core_layout_xml_tests") + [string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_core", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_log_tests", "pp_foundation_parse_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_assets_ppi_header_tests", "pp_paint_blend_tests", "pp_paint_stroke_tests", "pp_document_tests", "pp_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_layout_value_tests", "pp_ui_core_layout_xml_tests") ) $ErrorActionPreference = "Stop" diff --git a/scripts/automation/platform-build.sh b/scripts/automation/platform-build.sh index 4519970..1b219e7 100644 --- a/scripts/automation/platform-build.sh +++ b/scripts/automation/platform-build.sh @@ -3,7 +3,7 @@ set -u preset="${1:-android-arm64}" shift || true -targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_core pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests}" +targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_core pano_cli pp_foundation_binary_stream_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests}" start="$(date +%s)" cmake --preset "$preset" diff --git a/src/foundation/log.cpp b/src/foundation/log.cpp new file mode 100644 index 0000000..31bc07f --- /dev/null +++ b/src/foundation/log.cpp @@ -0,0 +1,93 @@ +#include "foundation/log.h" + +namespace pp::foundation { + +namespace { + +[[nodiscard]] bool should_write(LogLevel level, LogLevel min_level) noexcept +{ + return static_cast(level) >= static_cast(min_level); +} + +} + +Logger::Logger(ILogSink& sink) noexcept + : sink_(&sink) +{ +} + +void Logger::set_min_level(LogLevel level) noexcept +{ + min_level_ = level; +} + +LogLevel Logger::min_level() const noexcept +{ + return min_level_; +} + +Status Logger::write( + LogLevel level, + std::string_view component, + std::string_view message, + std::uint64_t frame_id, + std::uint64_t stroke_id, + std::uint64_t thread_id) noexcept +{ + if (component.empty()) { + return Status::invalid_argument("log component must not be empty"); + } + + if (message.empty()) { + return Status::invalid_argument("log message must not be empty"); + } + + if (!should_write(level, min_level_)) { + return Status::success(); + } + + sink_->write(LogRecord { + .level = level, + .component = std::string(component), + .message = std::string(message), + .frame_id = frame_id, + .stroke_id = stroke_id, + .thread_id = thread_id, + }); + return Status::success(); +} + +void MemoryLogSink::write(const LogRecord& record) noexcept +{ + records_.push_back(record); +} + +const std::vector& MemoryLogSink::records() const noexcept +{ + return records_; +} + +void MemoryLogSink::clear() noexcept +{ + records_.clear(); +} + +const char* log_level_name(LogLevel level) noexcept +{ + switch (level) { + case LogLevel::trace: + return "trace"; + case LogLevel::debug: + return "debug"; + case LogLevel::info: + return "info"; + case LogLevel::warning: + return "warning"; + case LogLevel::error: + return "error"; + } + + return "unknown"; +} + +} diff --git a/src/foundation/log.h b/src/foundation/log.h new file mode 100644 index 0000000..68e1a11 --- /dev/null +++ b/src/foundation/log.h @@ -0,0 +1,67 @@ +#pragma once + +#include "foundation/result.h" + +#include +#include +#include +#include + +namespace pp::foundation { + +enum class LogLevel : std::uint8_t { + trace, + debug, + info, + warning, + error, +}; + +struct LogRecord { + LogLevel level = LogLevel::info; + std::string component; + std::string message; + std::uint64_t frame_id = 0; + std::uint64_t stroke_id = 0; + std::uint64_t thread_id = 0; +}; + +class ILogSink { +public: + virtual ~ILogSink() = default; + virtual void write(const LogRecord& record) noexcept = 0; +}; + +class Logger { +public: + explicit Logger(ILogSink& sink) noexcept; + + void set_min_level(LogLevel level) noexcept; + [[nodiscard]] LogLevel min_level() const noexcept; + + [[nodiscard]] Status write( + LogLevel level, + std::string_view component, + std::string_view message, + std::uint64_t frame_id = 0, + std::uint64_t stroke_id = 0, + std::uint64_t thread_id = 0) noexcept; + +private: + ILogSink* sink_ = nullptr; + LogLevel min_level_ = LogLevel::trace; +}; + +class MemoryLogSink final : public ILogSink { +public: + void write(const LogRecord& record) noexcept override; + [[nodiscard]] const std::vector& records() const noexcept; + void clear() noexcept; + +private: + std::vector records_; +}; + +[[nodiscard]] const char* log_level_name(LogLevel level) noexcept; + +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 54c1857..9ae3545 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -16,6 +16,16 @@ add_test(NAME pp_foundation_binary_stream_tests COMMAND pp_foundation_binary_str set_tests_properties(pp_foundation_binary_stream_tests PROPERTIES LABELS "foundation;desktop-fast") +add_executable(pp_foundation_log_tests + foundation/log_tests.cpp) +target_link_libraries(pp_foundation_log_tests PRIVATE + pp_foundation + pp_test_harness) + +add_test(NAME pp_foundation_log_tests COMMAND pp_foundation_log_tests) +set_tests_properties(pp_foundation_log_tests PROPERTIES + LABELS "foundation;desktop-fast") + add_executable(pp_foundation_parse_tests foundation/parse_tests.cpp) target_link_libraries(pp_foundation_parse_tests PRIVATE diff --git a/tests/foundation/log_tests.cpp b/tests/foundation/log_tests.cpp new file mode 100644 index 0000000..e431561 --- /dev/null +++ b/tests/foundation/log_tests.cpp @@ -0,0 +1,82 @@ +#include "foundation/log.h" +#include "test_harness.h" + +#include + +using pp::foundation::LogLevel; +using pp::foundation::Logger; +using pp::foundation::MemoryLogSink; +using pp::foundation::StatusCode; +using pp::foundation::log_level_name; + +namespace { + +void writes_structured_records(pp::tests::Harness& h) +{ + MemoryLogSink sink; + Logger logger(sink); + + const auto status = logger.write(LogLevel::info, "paint", "stroke committed", 7, 11, 3); + + PP_EXPECT(h, status.ok()); + PP_EXPECT(h, sink.records().size() == 1U); + PP_EXPECT(h, sink.records()[0].level == LogLevel::info); + PP_EXPECT(h, sink.records()[0].component == std::string_view("paint")); + PP_EXPECT(h, sink.records()[0].message == std::string_view("stroke committed")); + PP_EXPECT(h, sink.records()[0].frame_id == 7U); + PP_EXPECT(h, sink.records()[0].stroke_id == 11U); + PP_EXPECT(h, sink.records()[0].thread_id == 3U); +} + +void filters_below_minimum_level(pp::tests::Harness& h) +{ + MemoryLogSink sink; + Logger logger(sink); + logger.set_min_level(LogLevel::warning); + + PP_EXPECT(h, logger.min_level() == LogLevel::warning); + PP_EXPECT(h, logger.write(LogLevel::debug, "ui", "layout pass").ok()); + PP_EXPECT(h, logger.write(LogLevel::warning, "ui", "slow layout").ok()); + PP_EXPECT(h, sink.records().size() == 1U); + PP_EXPECT(h, sink.records()[0].level == LogLevel::warning); +} + +void rejects_empty_component_or_message(pp::tests::Harness& h) +{ + MemoryLogSink sink; + Logger logger(sink); + + const auto empty_component = logger.write(LogLevel::error, "", "message"); + const auto empty_message = logger.write(LogLevel::error, "renderer", ""); + + PP_EXPECT(h, !empty_component.ok()); + PP_EXPECT(h, empty_component.code == StatusCode::invalid_argument); + PP_EXPECT(h, !empty_message.ok()); + PP_EXPECT(h, empty_message.code == StatusCode::invalid_argument); + PP_EXPECT(h, sink.records().empty()); +} + +void exposes_stable_level_names_and_clear(pp::tests::Harness& h) +{ + MemoryLogSink sink; + Logger logger(sink); + + PP_EXPECT(h, log_level_name(LogLevel::trace) == std::string_view("trace")); + PP_EXPECT(h, log_level_name(LogLevel::error) == std::string_view("error")); + PP_EXPECT(h, logger.write(LogLevel::info, "assets", "loaded").ok()); + PP_EXPECT(h, sink.records().size() == 1U); + sink.clear(); + PP_EXPECT(h, sink.records().empty()); +} + +} + +int main() +{ + pp::tests::Harness harness; + harness.run("writes_structured_records", writes_structured_records); + harness.run("filters_below_minimum_level", filters_below_minimum_level); + harness.run("rejects_empty_component_or_message", rejects_empty_component_or_message); + harness.run("exposes_stable_level_names_and_clear", exposes_stable_level_names_and_clear); + return harness.finish(); +}