Add foundation logging facade

This commit is contained in:
2026-06-01 08:20:58 +02:00
parent 6604f30ef3
commit a7bb04f54b
9 changed files with 262 additions and 6 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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"

View File

@@ -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"

93
src/foundation/log.cpp Normal file
View File

@@ -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<std::uint8_t>(level) >= static_cast<std::uint8_t>(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<LogRecord>& 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";
}
}

67
src/foundation/log.h Normal file
View File

@@ -0,0 +1,67 @@
#pragma once
#include "foundation/result.h"
#include <cstdint>
#include <string>
#include <string_view>
#include <vector>
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<LogRecord>& records() const noexcept;
void clear() noexcept;
private:
std::vector<LogRecord> records_;
};
[[nodiscard]] const char* log_level_name(LogLevel level) noexcept;
}

View File

@@ -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

View File

@@ -0,0 +1,82 @@
#include "foundation/log.h"
#include "test_harness.h"
#include <string_view>
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();
}