Add foundation event dispatcher tests

This commit is contained in:
2026-06-01 08:28:57 +02:00
parent 3f5711773e
commit 6c435dafb7
9 changed files with 302 additions and 8 deletions

View File

@@ -69,6 +69,7 @@ add_custom_target(panopainter_validate_shaders
add_library(pp_foundation STATIC add_library(pp_foundation STATIC
src/foundation/binary_stream.cpp src/foundation/binary_stream.cpp
src/foundation/event.cpp
src/foundation/log.cpp src/foundation/log.cpp
src/foundation/parse.cpp src/foundation/parse.cpp
src/foundation/task_queue.cpp src/foundation/task_queue.cpp

View File

@@ -80,8 +80,8 @@ Known local toolchain state:
`platform-build` automation wrapper for `pp_foundation`, `pp_assets`, `platform-build` automation wrapper for `pp_foundation`, `pp_assets`,
`pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`, `pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`,
`pp_ui_core`, `pano_cli`, and their current headless test binaries, `pp_ui_core`, `pano_cli`, and their current headless test binaries,
including foundation logging/task queue coverage, PPI header, paint stroke including foundation event/logging/task queue coverage, PPI header, paint
sampling, and layout XML parse coverage. stroke sampling, and layout XML parse coverage.
- `panopainter_validate_shaders` validates the current combined GLSL shader - `panopainter_validate_shaders` validates the current combined GLSL shader
files for one vertex stage marker, one fragment stage marker, valid marker files for one vertex stage marker, one fragment stage marker, valid marker
order, and existing relative includes. order, and existing relative includes.

View File

@@ -304,9 +304,10 @@ Goal: split libraries while keeping current app behavior.
Status: started. `pp_foundation` exists with binary stream utilities and Status: started. `pp_foundation` exists with binary stream utilities and
boundary/overread tests. It also owns strict decimal `uint32` parsing used by boundary/overread tests. It also owns strict decimal `uint32` parsing used by
`pano_cli`, with rejection tests for empty, signed, mixed, and overflowing `pano_cli`, with rejection tests for empty, signed, mixed, and overflowing
input. A structured logging facade, bounded FIFO task queue, and deterministic input. A synchronous event dispatcher, structured logging facade, bounded FIFO
`TraceRecorder` now record component/name/thread/frame/stroke metadata with task queue, and deterministic `TraceRecorder` now record
filtering, capacity, and invalid-end tests. `pp_assets` has started with component/name/thread/frame/stroke metadata with filtering, capacity, and
invalid-end tests. `pp_assets` has started with
PNG/JPEG signature detection plus PPI header recognition, with PNG/JPEG signature detection plus PPI header recognition, with
corrupt/truncated/unsupported tests. corrupt/truncated/unsupported tests.
`pp_paint` has started with CPU reference math for the five current shader `pp_paint` has started with CPU reference math for the five current shader
@@ -524,7 +525,7 @@ Last verified on 2026-06-01:
```powershell ```powershell
cmake --preset windows-msvc-default cmake --preset windows-msvc-default
cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_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_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_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 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\test.ps1 -Preset desktop-fast -Configuration Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli
@@ -538,6 +539,7 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -P
Results: Results:
- `pp_foundation_binary_stream_tests` passed. - `pp_foundation_binary_stream_tests` passed.
- `pp_foundation_event_tests` passed.
- `pp_foundation_log_tests` passed. - `pp_foundation_log_tests` passed.
- `pp_foundation_parse_tests` passed. - `pp_foundation_parse_tests` passed.
- `pp_foundation_task_queue_tests` passed. - `pp_foundation_task_queue_tests` passed.

View File

@@ -1,7 +1,7 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[string[]]$Presets = @("android-arm64"), [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_log_tests", "pp_foundation_parse_tests", "pp_foundation_task_queue_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_event_tests", "pp_foundation_log_tests", "pp_foundation_parse_tests", "pp_foundation_task_queue_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" $ErrorActionPreference = "Stop"

View File

@@ -3,7 +3,7 @@ set -u
preset="${1:-android-arm64}" preset="${1:-android-arm64}"
shift || true 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_log_tests pp_foundation_parse_tests pp_foundation_task_queue_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_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_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)" start="$(date +%s)"
cmake --preset "$preset" cmake --preset "$preset"

97
src/foundation/event.cpp Normal file
View File

@@ -0,0 +1,97 @@
#include "foundation/event.h"
#include <algorithm>
namespace pp::foundation {
EventDispatcher::EventDispatcher(std::size_t max_subscriptions) noexcept
: max_subscriptions_(max_subscriptions)
{
subscriptions_.reserve(std::min(max_subscriptions_, max_event_subscriptions));
}
std::size_t EventDispatcher::size() const noexcept
{
return subscriptions_.size();
}
bool EventDispatcher::empty() const noexcept
{
return subscriptions_.empty();
}
std::size_t EventDispatcher::max_subscriptions() const noexcept
{
return max_subscriptions_;
}
Result<std::uint64_t> EventDispatcher::subscribe(std::uint32_t type, EventCallback callback, void* user_data)
{
if (max_subscriptions_ == 0U || max_subscriptions_ > max_event_subscriptions) {
return Result<std::uint64_t>::failure(
Status::out_of_range("event dispatcher capacity is outside the configured range"));
}
if (type == 0U) {
return Result<std::uint64_t>::failure(Status::invalid_argument("event type must not be zero"));
}
if (callback == nullptr) {
return Result<std::uint64_t>::failure(Status::invalid_argument("event callback must not be null"));
}
if (subscriptions_.size() >= max_subscriptions_) {
return Result<std::uint64_t>::failure(Status::out_of_range("event dispatcher is full"));
}
const auto id = next_subscription_id_++;
subscriptions_.push_back(EventSubscription {
.id = id,
.type = type,
.callback = callback,
.user_data = user_data,
});
return Result<std::uint64_t>::success(id);
}
Status EventDispatcher::unsubscribe(std::uint64_t subscription_id) noexcept
{
const auto found = std::find_if(
subscriptions_.begin(),
subscriptions_.end(),
[subscription_id](const EventSubscription& subscription) {
return subscription.id == subscription_id;
});
if (found == subscriptions_.end()) {
return Status::out_of_range("event subscription id was not found");
}
subscriptions_.erase(found);
return Status::success();
}
std::size_t EventDispatcher::publish(const Event& event) const noexcept
{
if (event.type == 0U) {
return 0;
}
std::size_t delivered = 0;
for (const auto& subscription : subscriptions_) {
if (subscription.type == event.type) {
subscription.callback(event, subscription.user_data);
++delivered;
}
}
return delivered;
}
void EventDispatcher::clear() noexcept
{
subscriptions_.clear();
}
}

48
src/foundation/event.h Normal file
View File

@@ -0,0 +1,48 @@
#pragma once
#include "foundation/result.h"
#include <cstddef>
#include <cstdint>
#include <vector>
namespace pp::foundation {
constexpr std::size_t max_event_subscriptions = 65536;
struct Event {
std::uint32_t type = 0;
std::uint64_t source_id = 0;
std::uint64_t frame_id = 0;
std::uint64_t payload_u64 = 0;
};
using EventCallback = void (*)(const Event& event, void* user_data) noexcept;
struct EventSubscription {
std::uint64_t id = 0;
std::uint32_t type = 0;
EventCallback callback = nullptr;
void* user_data = nullptr;
};
class EventDispatcher {
public:
explicit EventDispatcher(std::size_t max_subscriptions = max_event_subscriptions) noexcept;
[[nodiscard]] std::size_t size() const noexcept;
[[nodiscard]] bool empty() const noexcept;
[[nodiscard]] std::size_t max_subscriptions() const noexcept;
[[nodiscard]] Result<std::uint64_t> subscribe(std::uint32_t type, EventCallback callback, void* user_data);
[[nodiscard]] Status unsubscribe(std::uint64_t subscription_id) noexcept;
[[nodiscard]] std::size_t publish(const Event& event) const noexcept;
void clear() noexcept;
private:
std::size_t max_subscriptions_ = max_event_subscriptions;
std::uint64_t next_subscription_id_ = 1;
std::vector<EventSubscription> subscriptions_;
};
}

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 set_tests_properties(pp_foundation_binary_stream_tests PROPERTIES
LABELS "foundation;desktop-fast") LABELS "foundation;desktop-fast")
add_executable(pp_foundation_event_tests
foundation/event_tests.cpp)
target_link_libraries(pp_foundation_event_tests PRIVATE
pp_foundation
pp_test_harness)
add_test(NAME pp_foundation_event_tests COMMAND pp_foundation_event_tests)
set_tests_properties(pp_foundation_event_tests PROPERTIES
LABELS "foundation;desktop-fast")
add_executable(pp_foundation_log_tests add_executable(pp_foundation_log_tests
foundation/log_tests.cpp) foundation/log_tests.cpp)
target_link_libraries(pp_foundation_log_tests PRIVATE target_link_libraries(pp_foundation_log_tests PRIVATE

View File

@@ -0,0 +1,136 @@
#include "foundation/event.h"
#include "test_harness.h"
using pp::foundation::Event;
using pp::foundation::EventDispatcher;
using pp::foundation::StatusCode;
using pp::foundation::max_event_subscriptions;
namespace {
struct Receiver {
int count = 0;
std::uint64_t payload_sum = 0;
std::uint64_t last_source = 0;
};
void receive_event(const Event& event, void* user_data) noexcept
{
auto* receiver = static_cast<Receiver*>(user_data);
++receiver->count;
receiver->payload_sum += event.payload_u64;
receiver->last_source = event.source_id;
}
void subscribe_and_publish_matching_events(pp::tests::Harness& h)
{
EventDispatcher dispatcher(4);
Receiver receiver;
const auto subscription = dispatcher.subscribe(7, receive_event, &receiver);
PP_EXPECT(h, subscription.ok());
PP_EXPECT(h, subscription.value() == 1U);
PP_EXPECT(h, dispatcher.size() == 1U);
PP_EXPECT(h, dispatcher.max_subscriptions() == 4U);
const auto delivered = dispatcher.publish(Event {
.type = 7,
.source_id = 42,
.frame_id = 3,
.payload_u64 = 11,
});
PP_EXPECT(h, delivered == 1U);
PP_EXPECT(h, receiver.count == 1);
PP_EXPECT(h, receiver.payload_sum == 11U);
PP_EXPECT(h, receiver.last_source == 42U);
}
void ignores_non_matching_or_zero_events(pp::tests::Harness& h)
{
EventDispatcher dispatcher(4);
Receiver receiver;
PP_EXPECT(h, dispatcher.subscribe(2, receive_event, &receiver).ok());
PP_EXPECT(h, dispatcher.publish(Event { .type = 3, .payload_u64 = 1 }) == 0U);
PP_EXPECT(h, dispatcher.publish(Event { .type = 0, .payload_u64 = 1 }) == 0U);
PP_EXPECT(h, receiver.count == 0);
}
void preserves_subscription_order_and_unsubscribes(pp::tests::Harness& h)
{
EventDispatcher dispatcher(4);
Receiver first;
Receiver second;
const auto first_subscription = dispatcher.subscribe(9, receive_event, &first);
const auto second_subscription = dispatcher.subscribe(9, receive_event, &second);
PP_EXPECT(h, first_subscription.ok());
PP_EXPECT(h, second_subscription.ok());
PP_EXPECT(h, dispatcher.publish(Event { .type = 9, .payload_u64 = 5 }) == 2U);
PP_EXPECT(h, first.payload_sum == 5U);
PP_EXPECT(h, second.payload_sum == 5U);
PP_EXPECT(h, dispatcher.unsubscribe(first_subscription.value()).ok());
PP_EXPECT(h, dispatcher.publish(Event { .type = 9, .payload_u64 = 7 }) == 1U);
PP_EXPECT(h, first.payload_sum == 5U);
PP_EXPECT(h, second.payload_sum == 12U);
const auto missing = dispatcher.unsubscribe(first_subscription.value());
PP_EXPECT(h, !missing.ok());
PP_EXPECT(h, missing.code == StatusCode::out_of_range);
}
void rejects_invalid_subscriptions_and_capacity(pp::tests::Harness& h)
{
EventDispatcher dispatcher(1);
EventDispatcher zero_capacity(0);
EventDispatcher excessive_capacity(max_event_subscriptions + 1U);
Receiver receiver;
const auto zero_type = dispatcher.subscribe(0, receive_event, &receiver);
const auto null_callback = dispatcher.subscribe(1, nullptr, &receiver);
PP_EXPECT(h, !zero_type.ok());
PP_EXPECT(h, zero_type.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !null_callback.ok());
PP_EXPECT(h, null_callback.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, dispatcher.subscribe(1, receive_event, &receiver).ok());
const auto full = dispatcher.subscribe(2, receive_event, &receiver);
PP_EXPECT(h, !full.ok());
PP_EXPECT(h, full.status().code == StatusCode::out_of_range);
const auto zero_capacity_result = zero_capacity.subscribe(1, receive_event, &receiver);
const auto excessive_capacity_result = excessive_capacity.subscribe(1, receive_event, &receiver);
PP_EXPECT(h, !zero_capacity_result.ok());
PP_EXPECT(h, zero_capacity_result.status().code == StatusCode::out_of_range);
PP_EXPECT(h, !excessive_capacity_result.ok());
PP_EXPECT(h, excessive_capacity_result.status().code == StatusCode::out_of_range);
}
void clear_removes_all_subscriptions(pp::tests::Harness& h)
{
EventDispatcher dispatcher(4);
Receiver receiver;
PP_EXPECT(h, dispatcher.subscribe(1, receive_event, &receiver).ok());
PP_EXPECT(h, dispatcher.subscribe(2, receive_event, &receiver).ok());
dispatcher.clear();
PP_EXPECT(h, dispatcher.empty());
PP_EXPECT(h, dispatcher.publish(Event { .type = 1, .payload_u64 = 5 }) == 0U);
PP_EXPECT(h, receiver.count == 0);
}
}
int main()
{
pp::tests::Harness harness;
harness.run("subscribe_and_publish_matching_events", subscribe_and_publish_matching_events);
harness.run("ignores_non_matching_or_zero_events", ignores_non_matching_or_zero_events);
harness.run("preserves_subscription_order_and_unsubscribes", preserves_subscription_order_and_unsubscribes);
harness.run("rejects_invalid_subscriptions_and_capacity", rejects_invalid_subscriptions_and_capacity);
harness.run("clear_removes_all_subscriptions", clear_removes_all_subscriptions);
return harness.finish();
}