Aleph-w 3.0
A C++ Library for Data Structures and Algorithms
Loading...
Searching...
No Matches
ah_generator_test.cc
Go to the documentation of this file.
1/*
2 Aleph_w
3
4 Data structures & Algorithms
5 https://github.com/lrleon/Aleph-w
6
7 This file is part of Aleph-w library
8
9 Copyright (c) 2002-2026 Leandro Rabindranath Leon
10
11 Permission is hereby granted, free of charge, to any person obtaining a copy
12 of this software and associated documentation files (the "Software"), to deal
13 in the Software without restriction, including without limitation the rights
14 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 copies of the Software, and to permit persons to whom the Software is
16 furnished to do so, subject to the following conditions:
17
18 The above copyright notice and this permission notice shall be included in all
19 copies or substantial portions of the Software.
20
21 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27 SOFTWARE.
28*/
29
41#include <memory>
42#include <stdexcept>
43#include <string>
44#include <type_traits>
45#include <utility>
46#include <vector>
47
48#include <gtest/gtest.h>
49
50#include <ah-generator.H>
51
53
54namespace
55{
56Generator<int> countdown(int n)
57{
58 for (; n > 0; --n)
59 co_yield n;
60}
61
62Generator<int> empty_generator()
63{
64 if (false)
65 co_yield 0; // never reached; keeps this a coroutine
66}
67
68Generator<std::string> mixed_value_categories()
69{
70 co_yield "a"; // const char* -> std::string prvalue
71 co_yield std::string("b") + "b"; // rvalue temporary
72 std::string s = "c";
73 co_yield s; // lvalue
74}
75
76Generator<int> throwing_after(int n)
77{
78 for (int i = 0; i < n; ++i)
79 co_yield i;
80 throw std::runtime_error("generator failure");
81}
82
83// Instrumented element type: counts live instances so tests can prove no
84// leaks/double-destroys happen across suspension points.
85struct Probe
86{
87 static int live;
88 int value = 0;
89
90 explicit Probe(int v) : value(v) { ++live; }
91 Probe(const Probe &p) : value(p.value) { ++live; }
92 Probe(Probe &&p) noexcept : value(p.value) { ++live; }
93 Probe &operator=(const Probe &) = default;
94 Probe &operator=(Probe &&) noexcept = default;
95 ~Probe() { --live; }
96};
97
98int Probe::live = 0;
99
100Generator<Probe> probes(int n)
101{
102 for (int i = 0; i < n; ++i)
103 co_yield Probe(i);
104}
105
106Generator<std::unique_ptr<int>> unique_ptrs(int n)
107{
108 for (int i = 0; i < n; ++i)
109 co_yield std::make_unique<int>(i);
110}
111
112// A generator whose body loops over another generator, forwarding values —
113// exercises composition and nested coroutine-frame lifetime.
115{
116 for (int x : src)
117 co_yield x * 2;
118}
119} // namespace
120
121static_assert(
122 not std::is_invocable_v<decltype(&Generator<int>::begin), Generator<int>>,
123 "Generator::begin() must reject rvalue generators to avoid dangling iterators");
124static_assert(
125 std::is_invocable_v<decltype(&Generator<int>::begin), Generator<int> &>,
126 "Generator::begin() must still accept lvalue generators");
127
128TEST(Generator, BasicYieldAndRangeFor)
129{
130 int sum = 0;
131 int count = 0;
132 for (int i : countdown(5))
133 {
134 sum += i;
135 ++count;
136 }
137 EXPECT_EQ(sum, 15);
138 EXPECT_EQ(count, 5);
139}
140
141TEST(Generator, IterationOrderIsDescending)
142{
143 std::vector<int> seen;
144 for (int i : countdown(4))
145 seen.push_back(i);
146 EXPECT_EQ(seen, (std::vector<int>{4, 3, 2, 1}));
147}
148
149TEST(Generator, EmptyGeneratorYieldsNothing)
150{
151 int count = 0;
152 for (int i : empty_generator())
153 { (void) i; ++count; }
154 EXPECT_EQ(count, 0);
155}
156
157TEST(Generator, MixedValueCategoriesPreserveContent)
158{
159 std::vector<std::string> seen;
160 for (const std::string &s : mixed_value_categories())
161 seen.push_back(s);
162 EXPECT_EQ(seen, (std::vector<std::string>{"a", "bb", "c"}));
163}
164
165TEST(Generator, PartialConsumptionStopsEarly)
166{
167 int first = -1;
168 int count = 0;
169 for (int i : countdown(1000000))
170 {
171 if (count == 0)
172 first = i;
173 if (++count == 3)
174 break;
175 }
176 EXPECT_EQ(first, 1000000);
177 EXPECT_EQ(count, 3);
178}
179
180TEST(Generator, ManualIteratorProtocol)
181{
182 Generator<int> g = countdown(3);
183 auto it = g.begin();
184 ASSERT_NE(it, g.end());
185 EXPECT_EQ(*it, 3);
186 ++it;
187 ASSERT_NE(it, g.end());
188 EXPECT_EQ(*it, 2);
189 ++it;
190 ASSERT_NE(it, g.end());
191 EXPECT_EQ(*it, 1);
192 ++it;
193 EXPECT_EQ(it, g.end());
194}
195
196TEST(Generator, ResumingAfterExhaustionIsSafe)
197{
198 // Resuming a coroutine past its final suspend point is undefined
199 // behavior; both begin() and operator++() must detect exhaustion and
200 // avoid ever calling coroutine_handle::resume() on a done() coroutine.
201 Generator<int> g = countdown(1);
202
203 auto it = g.begin();
204 ASSERT_NE(it, g.end());
205 EXPECT_EQ(*it, 1);
206 ++it;
207 ASSERT_EQ(it, g.end());
208
209 // A second begin() on an already-exhausted generator must not resume the
210 // coroutine again; it should simply return end() again.
211 auto it2 = g.begin();
212 EXPECT_EQ(it2, g.end());
213
214 // Advancing an iterator that is already at end() must throw rather than
215 // resume a done() coroutine.
216 EXPECT_THROW(++it, std::domain_error);
217}
218
219TEST(Generator, DefaultConstructedGeneratorIsAnEmptyRange)
220{
221 // Generator() is documented as "empty/singular": no coroutine attached.
222 // begin() must treat that the same as an exhausted/never-yielding
223 // sequence (begin() == end(), zero iterations) rather than throwing —
224 // the usual empty-range/empty-container convention, and consistent with
225 // iterator::done() already tolerating a null handle.
227 EXPECT_EQ(g.begin(), g.end());
228
229 int count = 0;
230 for (int i : g)
231 { (void) i; ++count; }
232 EXPECT_EQ(count, 0);
233}
234
235TEST(Generator, MovedFromGeneratorIsAnEmptyRange)
236{
237 Generator<int> g = countdown(3);
238 Generator<int> moved = std::move(g);
239 // g is now moved-from ("empty/singular"): iterating it must not resume
240 // the coroutine moved's handle now owns.
241 EXPECT_EQ(g.begin(), g.end());
242}
243
244TEST(Generator, ExceptionPropagatesFromResume)
245{
246 std::vector<int> seen;
247 EXPECT_THROW(
248 {
249 for (int i : throwing_after(3))
250 seen.push_back(i);
251 },
252 std::runtime_error);
253 EXPECT_EQ(seen, (std::vector<int>{0, 1, 2}));
254}
255
256TEST(Generator, ExceptionMessageIsPreserved)
257{
258 try
259 {
260 for (int i : throwing_after(0))
261 (void) i;
262 FAIL() << "expected std::runtime_error";
263 }
264 catch (const std::runtime_error &e)
265 {
266 EXPECT_STREQ(e.what(), "generator failure");
267 }
268}
269
270TEST(Generator, ElementLifetimesAreBalancedAcrossSuspension)
271{
272 ASSERT_EQ(Probe::live, 0);
273 {
274 int sum = 0;
275 for (const Probe &p : probes(5))
276 {
277 // Exactly one live Probe at a time: the one currently yielded.
278 EXPECT_EQ(Probe::live, 1);
279 sum += p.value;
280 }
281 EXPECT_EQ(sum, 0 + 1 + 2 + 3 + 4);
282 }
283 EXPECT_EQ(Probe::live, 0);
284}
285
286TEST(Generator, ElementLifetimesAreBalancedOnEarlyDestruction)
287{
288 ASSERT_EQ(Probe::live, 0);
289 {
290 Generator<Probe> g = probes(1000);
291 auto it = g.begin();
292 EXPECT_EQ(Probe::live, 1);
293 (void) *it;
294 // g goes out of scope here, still holding one live Probe: the
295 // coroutine frame (and the Probe within it) must be destroyed.
296 }
297 EXPECT_EQ(Probe::live, 0);
298}
299
300TEST(Generator, MoveOnlyElementsAreSupported)
301{
302 std::vector<int> seen;
303 for (auto &p : unique_ptrs(4))
304 seen.push_back(*p);
305 EXPECT_EQ(seen, (std::vector<int>{0, 1, 2, 3}));
306}
307
308TEST(Generator, GeneratorItselfIsMoveOnly)
309{
310 static_assert(not std::is_copy_constructible_v<Generator<int>>);
311 static_assert(std::is_move_constructible_v<Generator<int>>);
312
313 Generator<int> g = countdown(2);
314 Generator<int> moved = std::move(g);
315 std::vector<int> seen;
316 for (int i : moved)
317 seen.push_back(i);
318 EXPECT_EQ(seen, (std::vector<int>{2, 1}));
319}
320
321TEST(Generator, CompositionForwardsThroughNestedFrame)
322{
323 std::vector<int> seen;
324 for (int i : doubled(countdown(3)))
325 seen.push_back(i);
326 EXPECT_EQ(seen, (std::vector<int>{6, 4, 2}));
327}
328
329TEST(Generator, MultipleIndependentInstancesDoNotInterfere)
330{
331 Generator<int> a = countdown(3);
332 Generator<int> b = countdown(5);
333
334 auto ita = a.begin();
335 auto itb = b.begin();
336 EXPECT_EQ(*ita, 3);
337 EXPECT_EQ(*itb, 5);
338 ++ita;
339 EXPECT_EQ(*ita, 2);
340 EXPECT_EQ(*itb, 5); // b unaffected by advancing a
341}
Lazy sequence type (Aleph::Generator<T>) built on C++20 coroutines.
size_t size_t int32_t value
Definition ca-c-api.h:116
Lazy, single-pass sequence of T values produced by a coroutine.
static std::default_sentinel_t end() noexcept
End sentinel, per the C++20 sentinel-based range model.
iterator begin() &
Begin iterating: resumes the coroutine to its first co_yield.
#define FAIL(msg)
#define TEST(name)