Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
list_conformance.hpp
Go to the documentation of this file.
1#pragma once
2
3// ============================================================================
4// aria/testing/list_conformance.hpp
5// ----------------------------------------------------------------------------
6// Framework-level conformance suite for any type that satisfies
7// `aria::ListSource`. Pin the contracts spelled out in
8// `docs/list-diff-contract.md` (D-N) into machine-checkable form.
9//
10// Usage from a test TU:
11//
12// #include <doctest/doctest.h>
13// #include "aria/testing/list_conformance.hpp"
14//
15// TEST_CASE("ObservableList<int> conforms to ListSource D-N contract") {
16// aria::testing::run_list_source_conformance<aria::ObservableList<int>>(
17// [] { return std::make_shared<aria::ObservableList<int>>(); });
18// }
19//
20// The factory is required because some list types are non-default-
21// constructible (e.g. SortedList needs a comparator). The suite runs
22// every conformance section in fresh state.
23//
24// Per `docs/api-style.md` S-1, the entry points live in
25// `aria::testing::` and never under any implementation namespace.
26//
27// Coverage roadmap:
28//
29// D-1 / D-2 / D-3 indirectly verified through the structural tests
30// below (kind / index / item lifetime).
31// D-10 single-element ops emit one event.
32// D-11 batch ops emit ordered events with "as-seen"
33// index semantics.
34// D-12 Reset semantics.
35// D-13 ItemChanged probe (only when value_type satisfies
36// the on_changed concept).
37// D-20 / D-21 reentrancy probes (unsubscribe / subscribe inside
38// emit; throwing handlers do not break siblings).
39//
40// Derived-list-specific contracts (D-30 / D-31 / D-32) live in their
41// own dedicated tests; this generic suite focuses on the universal
42// contract.
43// ============================================================================
44
45#include <doctest/doctest.h>
46
47#include "aria/list_source.hpp"
49#include "aria/subscription.hpp"
50
51#include <atomic>
52#include <functional>
53#include <memory>
54#include <string>
55#include <utility>
56#include <vector>
57
58namespace aria::testing {
59
60namespace detail {
61
65template<typename T>
66struct EventLog {
67 struct Entry {
69 std::size_t index;
70 std::size_t from_index;
72 };
73
74 std::vector<Entry> events;
76
77 template<typename L>
78 explicit EventLog(L& list) {
79 sub = list.observe([this](const ListChange<T>& c) {
80 events.push_back(Entry{
81 c.kind,
82 c.index,
83 c.from_index,
84 c.item == nullptr,
85 });
86 });
87 }
88};
89
93template<typename L, typename Factory>
94[[nodiscard]] auto build(Factory& f) {
95 return f();
96}
97
98} // namespace detail
99
100// ---------------------------------------------------------------------------
101// D-10 -- single-element operations emit exactly one event.
102// ---------------------------------------------------------------------------
103
104template<typename L, typename Factory>
105void check_single_element_ops(Factory factory) {
106 using T = list_source_value_t<L>;
107
108 SUBCASE("D-10: push_back emits one Insert with index=size_before") {
109 auto list = factory();
110 detail::EventLog<T> log{*list};
111 list->push_back(std::make_shared<T>());
112 REQUIRE(log.events.size() == 1);
113 CHECK(log.events[0].kind == ListChangeKind::Insert);
114 CHECK(log.events[0].index == 0);
115 CHECK_FALSE(log.events[0].item_was_null);
116 }
117
118 SUBCASE("D-10: remove_at emits one Remove with valid item ptr") {
119 auto list = factory();
120 list->push_back(std::make_shared<T>());
121 list->push_back(std::make_shared<T>());
122 detail::EventLog<T> log{*list};
123 list->remove_at(0);
124 REQUIRE(log.events.size() == 1);
125 CHECK(log.events[0].kind == ListChangeKind::Remove);
126 CHECK(log.events[0].index == 0);
127 CHECK_FALSE(log.events[0].item_was_null); // D-3
128 }
129
130 SUBCASE("D-10: clear emits one Reset with item == nullptr") {
131 auto list = factory();
132 list->push_back(std::make_shared<T>());
133 list->push_back(std::make_shared<T>());
134 detail::EventLog<T> log{*list};
135 list->clear();
136 REQUIRE(log.events.size() == 1);
137 CHECK(log.events[0].kind == ListChangeKind::Reset);
138 CHECK(log.events[0].index == 0);
139 CHECK(log.events[0].item_was_null);
140 }
141}
142
143// ---------------------------------------------------------------------------
144// D-11 -- batch operations emit ordered events with "as-seen" indices.
145// ---------------------------------------------------------------------------
146
147template<typename L, typename Factory>
148void check_batch_ops(Factory factory) {
149 using T = list_source_value_t<L>;
150
151 SUBCASE("D-11: insert_range emits per-element Inserts in order") {
152 auto list = factory();
153 detail::EventLog<T> log{*list};
154 std::vector<std::shared_ptr<T>> in{
155 std::make_shared<T>(),
156 std::make_shared<T>(),
157 std::make_shared<T>(),
158 };
159 list->insert_range(0, in.begin(), in.end());
160 REQUIRE(log.events.size() == 3);
161 CHECK(log.events[0].kind == ListChangeKind::Insert);
162 CHECK(log.events[0].index == 0);
163 CHECK(log.events[1].kind == ListChangeKind::Insert);
164 CHECK(log.events[1].index == 1);
165 CHECK(log.events[2].kind == ListChangeKind::Insert);
166 CHECK(log.events[2].index == 2);
167 }
168
169 SUBCASE("D-11: remove_range reports each Remove with index = pos") {
170 // [A, B, C, D] -- remove_range(1, 2) drops B and C in order.
171 // Both events fire with index = 1 because the list shrinks.
172 auto list = factory();
173 for (int i = 0; i < 4; ++i) list->push_back(std::make_shared<T>());
174 detail::EventLog<T> log{*list};
175 list->remove_range(1, 2);
176 REQUIRE(log.events.size() == 2);
177 CHECK(log.events[0].kind == ListChangeKind::Remove);
178 CHECK(log.events[0].index == 1);
179 CHECK(log.events[1].kind == ListChangeKind::Remove);
180 CHECK(log.events[1].index == 1);
181 }
182}
183
184// ---------------------------------------------------------------------------
185// D-20 / D-21 -- reentrancy semantics.
186// ---------------------------------------------------------------------------
187
188template<typename L, typename Factory>
189void check_reentrancy(Factory factory) {
190 using T = list_source_value_t<L>;
191
192 SUBCASE("D-20: unsubscribe inside emit skips a pending observer") {
193 auto list = factory();
194 Subscription a, b;
195 int hits_a = 0, hits_b = 0;
196
197 a = list->observe([&](const ListChange<T>&) {
198 ++hits_a;
199 b.release(); // disconnect b mid-emit
200 });
201 b = list->observe([&](const ListChange<T>&) { ++hits_b; });
202
203 list->push_back(std::make_shared<T>());
204 // Cancellation invalidates b before its turn in the current fan-out.
205 CHECK(hits_a == 1);
206 CHECK(hits_b == 0);
207
208 list->push_back(std::make_shared<T>());
209 CHECK(hits_a == 2);
210 CHECK(hits_b == 0); // b was disconnected before this emit
211 }
212
213 SUBCASE("D-21: throwing handler does not break siblings") {
214 auto list = factory();
215 int after = 0;
216 auto sub_throw = list->observe([](const ListChange<T>&) {
217 throw std::runtime_error("naughty observer");
218 });
219 auto sub_count = list->observe([&](const ListChange<T>&) { ++after; });
220
221 list->push_back(std::make_shared<T>());
222 // The naughty handler raised; the framework swallowed it; the
223 // sibling still got its callback.
224 CHECK(after == 1);
225 }
226}
227
228// ---------------------------------------------------------------------------
229// Aggregator: run every conformance section.
230//
231// `Factory` :: () -> std::shared_ptr<L>
232// ---------------------------------------------------------------------------
233
234template<typename L, typename Factory>
235void run_list_source_conformance(Factory factory) {
236 static_assert(::aria::ListSource<L>,
237 "run_list_source_conformance<L>: L does not satisfy aria::ListSource. "
238 "See docs/list-diff-contract.md D-40.");
239
241 check_batch_ops<L>(factory);
242 check_reentrancy<L>(factory);
243}
244
245} // namespace aria::testing
RAII handle to a single subscription.
Definition subscription.hpp:44
void release() noexcept
Explicitly disconnect now (instead of at destruction).
Definition subscription.hpp:83
Definition list_conformance.hpp:60
auto build(Factory &f)
Helper: most generic factory accepts a closure producing a shared_ptr<L>.
Definition list_conformance.hpp:94
Definition list_conformance.hpp:58
void check_batch_ops(Factory factory)
Definition list_conformance.hpp:148
void check_reentrancy(Factory factory)
Definition list_conformance.hpp:189
void run_list_source_conformance(Factory factory)
Definition list_conformance.hpp:235
void check_single_element_ops(Factory factory)
Definition list_conformance.hpp:105
typename detail::list_source_value_impl< std::remove_cvref_t< L > >::type list_source_value_t
Element type for a list source.
Definition list_source.hpp:76
ListChangeKind
Definition list_change.hpp:10
@ Remove
Definition list_change.hpp:10
@ Reset
Definition list_change.hpp:10
@ Insert
Definition list_change.hpp:10
An owning event in a sequential list edit stream.
Definition list_change.hpp:16
std::size_t index
Definition list_change.hpp:19
std::shared_ptr< T > item
Definition list_change.hpp:20
std::size_t from_index
Definition list_change.hpp:21
ListChangeKind kind
Definition list_change.hpp:18
Definition list_conformance.hpp:67
bool item_was_null
Definition list_conformance.hpp:71
ListChangeKind kind
Definition list_conformance.hpp:68
std::size_t from_index
Definition list_conformance.hpp:70
std::size_t index
Definition list_conformance.hpp:69
Tiny event log used by every conformance probe.
Definition list_conformance.hpp:66
Subscription sub
Definition list_conformance.hpp:75
std::vector< Entry > events
Definition list_conformance.hpp:74
EventLog(L &list)
Definition list_conformance.hpp:78