Add foundation logging facade
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
93
src/foundation/log.cpp
Normal 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
67
src/foundation/log.h
Normal 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;
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
82
tests/foundation/log_tests.cpp
Normal file
82
tests/foundation/log_tests.cpp
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user