Skip to content

Commit 55090d1

Browse files
committed
docs: fix verified API errors in Testing Facilities section
Audit of all 35 code snippets in 7.testing pages against actual headers revealed structural bugs that were verified by compiling and running each example. Fixes: * 7a thread_pool snippet: replace nonexistent <boost/capy/thread_pool.hpp> and pool.post(lambda) (which takes only `continuation&`) with the idiomatic run_async(pool.get_executor())(coroutine) pattern. * 7b/7c/7d "Putting It Together" snippets: add missing concept-header includes (read_stream, read_source, write_sink, buffer_source, buffer_sink) so the templated examples actually compile. * 7b/7c/7d "Putting It Together" snippets: fix a structural bug where mocks were constructed and asserted on outside the f.armed() lambda. With armed running the body multiple times for fault injection, the outside state was the last run's state, not the success run's. Move mock construction and state assertions inside the lambda following the canonical capy test pattern, with functions-under-test now returning std::error_code so callers can guard with `if(ec) co_return;`. * 7c handle_request: read_source::read returns cond::eof on partial fill; treat eof-with-data as success rather than bailing. * 7d buffer_sink basic example: remove the dead `if(bufs.empty()) co_return;` check. prepare() always returns a 1-element span for a non-empty input array. * All "Returns ...eof" prose and table descriptions: standardize on cond::eof spelling per error.hpp:32 ("Compare with cond::eof"), since cond::eof is the user-side comparison value while error::eof is the implementation's returned code. Both compare equal. * Mock constructor signatures in tables: add `explicit` qualifier to match the actual headers (read_stream, write_stream, read_source, write_sink, buffer_source, buffer_sink, fuse(error_code)). * Document write_some rollback semantics: write_stream::write_some and write_sink::write_some roll back on expected-data mismatch and return (test_failure, 0); call out the asymmetry with write_sink::write, which leaves the partial write in place. * Expand 7a's "armed() vs. inert()" subsection with a smoke-test-first pattern showing the same test body under both modes, plus prose explaining when each fits. * 7e bufgrind: add prose explaining why snippets use f.inert (bufgrind does not do I/O or consult a fuse, so a single pass is sufficient). All snippets verified by compiling and running them through the test suite during development.
1 parent 8b0ebd9 commit 55090d1

7 files changed

Lines changed: 160 additions & 98 deletions

File tree

doc/modules/ROOT/nav.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
** xref:6.streams/6d.buffer-concepts.adoc[Buffer Sources and Sinks]
3535
** xref:6.streams/6e.algorithms.adoc[Transfer Algorithms]
3636
** xref:6.streams/6f.isolation.adoc[Physical Isolation]
37-
* xref:7.testing/7.intro.adoc[Testing Facilities]
37+
* xref:7.testing/7.intro.adoc[Testing]
3838
** xref:7.testing/7a.drivers.adoc[Driving Tests]
3939
** xref:7.testing/7b.mock-streams.adoc[Mock Streams]
4040
** xref:7.testing/7c.mock-sources-sinks.adoc[Mock Sources and Sinks]

doc/modules/ROOT/pages/7.testing/7.intro.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// Official repository: https://github.com/cppalliance/capy
88
//
99

10-
= Testing Facilities
10+
= Testing
1111

1212
Real I/O is a poor foundation for unit tests. Network operations are slow,
1313
non-deterministic, and do not fail on demand -- so error-handling paths go

doc/modules/ROOT/pages/7.testing/7a.drivers.adoc

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,50 @@ void test_with_fuse()
156156
exception mode) and is the normal choice for exhaustive error coverage.
157157

158158
`inert()` runs the test body exactly once with no injection. Calls to
159-
`maybe_fail()` always return an empty error code and never throw. Use
160-
`inert()` when you want to verify the happy path is reachable, or when
161-
combining with `bufgrind` where only one run is needed.
159+
`maybe_fail()` always return an empty error code and never throw.
160+
161+
Use `inert()` for happy-path verification ("does this work when nothing
162+
fails?"). Use `armed()` for fault-tolerance verification ("does this
163+
handle a failure at every async step?"). A typical test suite pairs
164+
both -- `inert()` confirms the function works at all, then `armed()`
165+
confirms it handles every error site:
166+
167+
[source,cpp]
168+
----
169+
fuse f;
170+
171+
// Smoke test: happy path
172+
auto r1 = f.inert([&](fuse&) -> task<void> {
173+
read_stream rs(f);
174+
rs.provide("hello");
175+
176+
char buf[8];
177+
auto [ec, n] = co_await rs.read_some(make_buffer(buf));
178+
BOOST_TEST(!ec);
179+
BOOST_TEST(std::string_view(buf, n) == "hello");
180+
});
181+
BOOST_TEST(r1.success);
182+
183+
// Fault coverage: every error site
184+
auto r2 = f.armed([&](fuse&) -> task<void> {
185+
read_stream rs(f);
186+
rs.provide("hello");
187+
188+
char buf[8];
189+
auto [ec, n] = co_await rs.read_some(make_buffer(buf));
190+
if(ec)
191+
co_return; // fuse injected an error; exit gracefully
192+
BOOST_TEST(std::string_view(buf, n) == "hello");
193+
});
194+
BOOST_TEST(r2.success);
195+
----
196+
197+
The only difference is the `if(ec) co_return;` guard. In `inert()`,
198+
that guard is dead code (`maybe_fail()` never returns an error); in
199+
`armed()`, it is essential.
200+
201+
The only way to signal a test failure under `inert()` is to call
202+
`f.fail()` from inside the body:
162203

163204
[source,cpp]
164205
----
@@ -213,37 +254,37 @@ auto r = f.armed([&](fuse&) -> task<void> {
213254
BOOST_TEST(r.success);
214255
----
215256

216-
=== Dependency Injection
257+
=== Custom Fail Points
217258

218-
A `fuse` constructed outside of `armed()` or `inert()` is a no-op. Passing
219-
it to a service at construction time lets the same service be tested without
220-
any code changes:
259+
A type that holds a `fuse` reference can call `maybe_fail()` from its own
260+
methods to declare additional fail points beyond those built into the
261+
mocks. Outside `armed()` or `inert()` the call is a no-op (returns an
262+
empty error code immediately); inside `armed()` it participates in
263+
fault injection alongside every other site.
221264

222265
[source,cpp]
223266
----
224-
class DataService
267+
class widget
225268
{
226269
fuse& f_;
227270
public:
228-
explicit DataService(fuse& f) : f_(f) {}
271+
explicit widget(fuse& f) : f_(f) {}
229272
230-
std::error_code fetch()
273+
std::error_code process()
231274
{
232-
auto ec = f_.maybe_fail(); // no-op in production
275+
auto ec = f_.maybe_fail();
233276
if(ec)
234277
return ec;
235278
// ... actual work ...
236279
return {};
237280
}
238281
};
239282
240-
// Production: fuse is never armed, maybe_fail() is always a no-op
241283
fuse f;
242-
DataService svc(f);
243-
svc.fetch();
284+
widget w(f);
285+
w.process(); // maybe_fail() returns {}
244286
245-
// Tests: armed() injects failures through the same fuse
246-
auto r = f.armed([&](fuse&) { svc.fetch(); });
287+
auto r = f.armed([&](fuse&) { w.process(); }); // both branches exercised
247288
BOOST_TEST(r.success);
248289
----
249290

@@ -273,7 +314,7 @@ BOOST_TEST(r.success);
273314
| `fuse()`
274315
| Construct with the default error code (`error::test_failure`).
275316

276-
| `fuse(std::error_code ec)`
317+
| `explicit fuse(std::error_code ec)`
277318
| Construct with a custom error code delivered by `maybe_fail()`.
278319

279320
| `armed(fn) -> result`
@@ -330,17 +371,19 @@ Platform limits on the name length:
330371

331372
[source,cpp]
332373
----
374+
#include <boost/capy/ex/run_async.hpp>
375+
#include <boost/capy/ex/thread_pool.hpp>
376+
#include <boost/capy/task.hpp>
333377
#include <boost/capy/test/thread_name.hpp>
334-
#include <boost/capy/thread_pool.hpp>
335378
336379
using namespace boost::capy;
337380
338381
thread_pool pool(4);
339-
int worker = 0;
340-
pool.post([&] {
382+
run_async(pool.get_executor())([]() -> task<void> {
341383
set_current_thread_name("test-worker-0");
342384
// ... test work runs here; name appears in gdb thread list
343-
});
385+
co_return;
386+
}());
344387
pool.join();
345388
----
346389

doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ BOOST_TEST(r.success);
8282
=== EOF Behavior
8383

8484
When all provided data has been consumed, `read_some` returns
85-
`error::eof` with a byte count of zero. The stream does not
85+
`cond::eof` with a byte count of zero. The stream does not
8686
suspend; the result is available immediately.
8787

8888
[source,cpp]
@@ -102,7 +102,7 @@ auto r = f.inert([&](fuse&) -> task<void> {
102102
// Second read: EOF
103103
auto [ec2, n2] = co_await rs.read_some(
104104
mutable_buffer(buf, sizeof(buf)));
105-
BOOST_TEST(ec2 == error::eof);
105+
BOOST_TEST(ec2 == cond::eof);
106106
BOOST_TEST(n2 == 0);
107107
});
108108
BOOST_TEST(r.success);
@@ -112,7 +112,7 @@ BOOST_TEST(r.success);
112112
|===
113113
| Member | Description
114114

115-
| `read_stream(fuse f = {}, std::size_t max_read_size = std::size_t(-1))`
115+
| `explicit read_stream(fuse f = {}, std::size_t max_read_size = std::size_t(-1))`
116116
| Construct with an optional shared `fuse` and an optional per-read byte limit.
117117
When omitted, the fuse is inert and reads return all available data at once.
118118
Set `max_read_size` to simulate chunked network delivery.
@@ -123,7 +123,7 @@ BOOST_TEST(r.success);
123123

124124
| `read_some(MutableBufferSequence buffers)`
125125
| Partial read. Returns up to `max_read_size` bytes (or all available
126-
if no limit was set). Returns `error::eof` when the buffer is drained.
126+
if no limit was set). Returns `cond::eof` when the buffer is drained.
127127
Consults the fuse before every read.
128128

129129
| `available() -> std::size_t`
@@ -218,16 +218,16 @@ BOOST_TEST(r.success);
218218
|===
219219
| Member | Description
220220

221-
| `write_stream(fuse f = {}, std::size_t max_write_size = std::size_t(-1))`
221+
| `explicit write_stream(fuse f = {}, std::size_t max_write_size = std::size_t(-1))`
222222
| Construct with an optional shared `fuse` and an optional per-write byte limit.
223223
When omitted, the fuse is inert and writes accept all bytes at once.
224224
Set `max_write_size` to simulate chunked network delivery.
225225

226226
| `write_some(ConstBufferSequence buffers)`
227227
| Partial write. Appends up to `max_write_size` bytes to the internal
228-
buffer. Checks against expected data after appending; returns
229-
`error::test_failure` on mismatch. Consults the fuse before every
230-
write.
228+
buffer, then checks against the expected prefix. On mismatch, rolls
229+
back the appended bytes and returns `(error::test_failure, 0)`.
230+
Consults the fuse before every write.
231231

232232
| `data() -> std::string_view`
233233
| Return bytes written but not yet matched by `expect()`.
@@ -300,14 +300,14 @@ operation under test.
300300

301301
Calling `close()` on one end signals EOF to the peer. The peer drains
302302
any buffered data first; once the buffer is empty, subsequent
303-
`read_some` calls on the peer return `error::eof`. The peer may still
303+
`read_some` calls on the peer return `cond::eof`. The peer may still
304304
call `write_some` after receiving EOF.
305305

306306
When the fuse injects an error during `read_some` or `write_some`, the
307307
pair is automatically closed: the calling end returns the injected
308308
error, any suspended reader on the other end is resumed with
309-
`error::eof`, and all subsequent operations on both ends return
310-
`error::eof`.
309+
`cond::eof`, and all subsequent operations on both ends return
310+
`cond::eof`.
311311

312312
=== Thread Safety
313313

@@ -324,13 +324,13 @@ concurrent coroutines is undefined behavior.
324324

325325
| `read_some(MutableBufferSequence buffers)`
326326
| Partial read from the peer's outgoing data. Suspends if no data is
327-
available. Returns `error::eof` when the stream is closed or the peer
327+
available. Returns `cond::eof` when the stream is closed or the peer
328328
called `close()`. Consults the fuse before every read (unless
329329
draining after `close()`).
330330

331331
| `write_some(ConstBufferSequence buffers)`
332332
| Partial write into the peer's incoming buffer. Resumes a suspended
333-
peer reader if any. Returns `error::eof` if the stream is closed.
333+
peer reader if any. Returns `cond::eof` if the stream is closed.
334334
Consults the fuse before every write.
335335

336336
| `close()`
@@ -364,17 +364,19 @@ a different error-handling branch inside `read_line`.
364364

365365
[source,cpp]
366366
----
367-
#include <boost/capy/test/read_stream.hpp>
368-
#include <boost/capy/test/fuse.hpp>
369367
#include <boost/capy/buffers/make_buffer.hpp>
368+
#include <boost/capy/concept/read_stream.hpp>
370369
#include <boost/capy/task.hpp>
370+
#include <boost/capy/test/fuse.hpp>
371+
#include <boost/capy/test/read_stream.hpp>
371372
372373
using namespace boost::capy;
373374
using namespace boost::capy::test;
374375
375376
// Function under test: read until '\n' or EOF
376377
template<ReadStream S>
377-
task<std::string> read_line(S& stream)
378+
task<std::pair<std::error_code, std::string>>
379+
read_line(S& stream)
378380
{
379381
std::string line;
380382
char ch;
@@ -383,23 +385,23 @@ task<std::string> read_line(S& stream)
383385
auto [ec, n] = co_await stream.read_some(
384386
mutable_buffer(&ch, 1));
385387
if(ec)
386-
co_return line;
388+
co_return {ec, std::move(line)};
387389
if(ch == '\n')
388390
break;
389391
line += ch;
390392
}
391-
co_return line;
393+
co_return {std::error_code{}, std::move(line)};
392394
}
393395
394396
void test_read_line()
395397
{
396398
fuse f;
397-
read_stream rs(f);
398-
rs.provide("hello\n");
399-
400399
auto r = f.armed([&](fuse&) -> task<void> {
401-
auto line = co_await read_line(rs);
402-
if(line.empty())
400+
read_stream rs(f);
401+
rs.provide("hello\n");
402+
403+
auto [ec, line] = co_await read_line(rs);
404+
if(ec)
403405
co_return; // fuse injected an error; exit gracefully
404406
BOOST_TEST(line == "hello");
405407
});

0 commit comments

Comments
 (0)