The FreeRADIUS server $Id: a4e7f8c94e8038b7627bcf52356b7ed15955c1d7 $
Loading...
Searching...
No Matches
bio_queue_tests.c
Go to the documentation of this file.
1/*
2 * This library is free software; you can redistribute it and/or
3 * modify it under the terms of the GNU Lesser General Public
4 * License as published by the Free Software Foundation; either
5 * version 2.1 of the License, or (at your option) any later version.
6 *
7 * This library is distributed in the hope that it will be useful,
8 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
10 * Lesser General Public License for more details.
11 *
12 * You should have received a copy of the GNU Lesser General Public
13 * License along with this library; if not, write to the Free Software
14 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
15 */
16
17/** Tests for the queue bio's shutdown and error handling
18 *
19 * @file src/lib/bio/test/bio_queue_tests.c
20 *
21 * @copyright 2026 Network RADIUS SAS (legal@networkradius.com)
22 */
23#include <freeradius-devel/util/test/acutest_common_init.h>
24#include <freeradius-devel/util/test/acutest_helpers.h>
25
26#include <freeradius-devel/bio/bio_priv.h>
27#include <freeradius-devel/bio/mem.h>
28#include <freeradius-devel/bio/null.h>
29#include <freeradius-devel/bio/pipe.h>
30#include <freeradius-devel/bio/queue.h>
31
32static int cancel_count;
33static int shutdown_count;
34
35static void cb_eof(fr_bio_t *bio) { (void) bio; }
36static int cb_shutdown(fr_bio_t *bio) { (void) bio; shutdown_count++; return 0; }
37static int cb_noop(fr_bio_t *bio) { (void) bio; return 0; }
38
39static void queue_cancel(fr_bio_t *bio, void *packet_ctx, void const *buffer, size_t size)
40{
41 (void) bio; (void) packet_ctx; (void) buffer; (void) size;
43}
44
47 .write_resume = cb_noop,
48 .read_blocked = cb_noop,
49 .write_blocked = cb_noop,
50 .eof = cb_eof,
51 .shutdown = cb_shutdown,
52};
53
54/*
55 * The pipe holds 1024 bytes, so the second write is partial, and the queue saves it.
56 */
57static uint8_t fill[1000];
58static uint8_t overflow[100];
59
60/** A stub transport which fails recoverably or fatally, as each test chooses.
61 *
62 * On a recoverable failure, the stub returns an error and changes nothing else. On a fatal failure,
63 * the stub first shuts the chain down, as the fd bio does. Only the failing bio decides whether a
64 * failure is recoverable or fatal.
65 */
66typedef struct {
68 bool fatal; //!< shut the chain down before failing
69} stub_t;
70
71static ssize_t stub_write_fail(fr_bio_t *bio, UNUSED void *packet_ctx, UNUSED void const *buffer, UNUSED size_t size)
72{
73 stub_t *stub = (stub_t *) bio;
74
75 if (stub->fatal) (void) fr_bio_shutdown(bio);
76
77 fr_strerror_const("stub write failure");
78 return fr_bio_error(GENERIC);
79}
80
81/** A transport which accepts one byte of each write, so the queue bio saves the rest of the packet.
82 */
83static ssize_t stub_write_one(UNUSED fr_bio_t *bio, UNUSED void *packet_ctx, UNUSED void const *buffer, UNUSED size_t size)
84{
85 return 1;
86}
87
88/** A transport which accepts every write in full.
89 */
90static ssize_t stub_write_all(UNUSED fr_bio_t *bio, UNUSED void *packet_ctx, UNUSED void const *buffer, size_t size)
91{
92 return size;
93}
94
95static fr_bio_t *stub_alloc(TALLOC_CTX *ctx)
96{
97 stub_t *stub;
98
99 stub = talloc_zero(ctx, stub_t);
100 if (!stub) return NULL;
101
102 stub->bio.read = fr_bio_null_read;
103 stub->bio.write = stub_write_fail;
104
105 return &stub->bio;
106}
107
108/** Leave one packet partly written in the queue.
109 */
111{
112 if (fr_bio_write(head, NULL, fill, sizeof(fill)) != sizeof(fill)) return false;
113
114 return (fr_bio_write(head, NULL, overflow, sizeof(overflow)) == sizeof(overflow));
115}
116
117/** The application installs its callbacks on a queue bio at the head of the chain.
118 *
119 * The queue's own shutdown routine must survive that, and still cancel the saved packet.
120 */
122{
123 TALLOC_CTX *ctx = talloc_init_const("test");
124 fr_bio_t *queue, *pipe;
125
127
128 pipe = fr_bio_pipe_alloc(ctx, &test_cb, 1024);
129 TEST_CHECK(pipe != NULL);
130 if (!pipe) goto done;
131
132 queue = fr_bio_queue_alloc(ctx, 4, NULL, NULL, queue_cancel, pipe);
133 TEST_CHECK(queue != NULL);
134 if (!queue) goto done;
135
136 fr_bio_cb_set(queue, &test_cb);
137
138 TEST_CHECK(queue_fill(queue));
139
140 TEST_CASE("the transport dies");
141 TEST_CHECK(fr_bio_shutdown(pipe) == 0);
142
143 TEST_CASE("the saved packet is cancelled");
145
146 TEST_CASE("the application is told about the shutdown");
148
149 TEST_CASE("writes report SHUTDOWN");
150 TEST_CHECK(fr_bio_write(queue, NULL, "x", 1) == fr_bio_error(SHUTDOWN));
151
152done:
153 talloc_free(ctx);
154}
155
156/** A queue bio in the middle of a chain is not the head, so only the private slot reaches it.
157 */
158static void test_shutdown_not_head(void)
159{
160 TALLOC_CTX *ctx = talloc_init_const("test");
161 fr_bio_t *head, *queue, *pipe;
162
164
165 pipe = fr_bio_pipe_alloc(ctx, &test_cb, 1024);
166 TEST_CHECK(pipe != NULL);
167 if (!pipe) goto done;
168
169 queue = fr_bio_queue_alloc(ctx, 4, NULL, NULL, queue_cancel, pipe);
170 TEST_CHECK(queue != NULL);
171 if (!queue) goto done;
172
173 /*
174 * A memory bio with no buffers passes writes straight through.
175 */
176 head = fr_bio_mem_alloc(ctx, 0, 0, queue);
177 TEST_CHECK(head != NULL);
178 if (!head) goto done;
179
181
183
184 TEST_CASE("the transport dies");
185 TEST_CHECK(fr_bio_shutdown(pipe) == 0);
186
187 TEST_CASE("the saved packet is cancelled");
189
190 TEST_CASE("the application is told about the shutdown");
192
193done:
194 talloc_free(ctx);
195}
196
197/** Build a queue in front of a stub, with one packet saved after a partial write.
198 */
199static fr_bio_t *test_queue_saved_alloc(TALLOC_CTX *ctx, fr_bio_t **stub_p)
200{
201 fr_bio_t *queue, *stub;
202
204
205 stub = stub_alloc(ctx);
206 if (!stub) return NULL;
207
208 queue = fr_bio_queue_alloc(ctx, 4, NULL, NULL, queue_cancel, stub);
209 if (!queue) return NULL;
210
211 fr_bio_cb_set(queue, &test_cb);
212
213 stub->write = stub_write_one;
214 if (fr_bio_write(queue, NULL, overflow, sizeof(overflow)) != sizeof(overflow)) return NULL;
215
216 *stub_p = stub;
217 return queue;
218}
219
220/** The queue bio passes a recoverable write error up to the application, and keeps working.
221 */
223{
224 TALLOC_CTX *ctx = talloc_init_const("test");
225 fr_bio_t *queue, *stub;
226
228
229 stub = stub_alloc(ctx);
230 TEST_CHECK(stub != NULL);
231 if (!stub) goto done;
232
233 queue = fr_bio_queue_alloc(ctx, 4, NULL, NULL, queue_cancel, stub);
234 TEST_CHECK(queue != NULL);
235 if (!queue) goto done;
236
237 fr_bio_cb_set(queue, &test_cb);
238
239 TEST_CASE("the write gets the error from the stub");
240 TEST_CHECK(fr_bio_write(queue, NULL, "x", 1) == fr_bio_error(GENERIC));
241
242 TEST_CASE("the queue bio does not shut the chain down");
244
245 TEST_CASE("the next write reaches the stub again");
246 stub->write = stub_write_all;
247 TEST_CHECK_RET((int) fr_bio_write(queue, NULL, "x", 1), 1);
248
249done:
250 talloc_free(ctx);
251}
252
253/** The queue bio passes a recoverable flush error up to the application, and keeps the saved packet.
254 */
256{
257 TALLOC_CTX *ctx = talloc_init_const("test");
258 fr_bio_t *queue, *stub = NULL;
259
260 queue = test_queue_saved_alloc(ctx, &stub);
261 TEST_CHECK(queue != NULL);
262 if (!queue) goto done;
263
264 TEST_CASE("the flush gets the error from the stub");
265 stub->write = stub_write_fail;
266 TEST_CHECK(fr_bio_write(queue, NULL, NULL, SIZE_MAX) == fr_bio_error(GENERIC));
267
268 TEST_CASE("the saved packet is not cancelled, and the chain is not shut down");
271
272 TEST_CASE("a later flush sends the saved packet");
273 stub->write = stub_write_all;
274 TEST_CHECK(fr_bio_write(queue, NULL, NULL, SIZE_MAX) > 0);
276
277done:
278 talloc_free(ctx);
279}
280
281/** When the next bio shuts the chain down on a fatal flush error, fr_bio_queue_shutdown() cancels the saved packet.
282 */
284{
285 TALLOC_CTX *ctx = talloc_init_const("test");
286 fr_bio_t *queue, *stub = NULL;
287
288 queue = test_queue_saved_alloc(ctx, &stub);
289 TEST_CHECK(queue != NULL);
290 if (!queue) goto done;
291
292 TEST_CASE("the flush gets the error from the stub");
293 ((stub_t *) stub)->fatal = true;
294 stub->write = stub_write_fail;
295 TEST_CHECK(fr_bio_write(queue, NULL, NULL, SIZE_MAX) == fr_bio_error(GENERIC));
296
297 TEST_CASE("fr_bio_queue_shutdown() cancels the saved packet, and the application is told");
300
301 TEST_CASE("later writes report SHUTDOWN");
302 TEST_CHECK(fr_bio_write(queue, NULL, "x", 1) == fr_bio_error(SHUTDOWN));
303
304done:
305 talloc_free(ctx);
306}
307
309 { "shutdown_keeps_app_callback", test_shutdown_keeps_app_callback },
310 { "shutdown_not_head", test_shutdown_not_head },
311 { "recoverable_write_passes_up", test_recoverable_write_passes_up },
312 { "recoverable_flush_passes_up", test_recoverable_flush_passes_up },
313 { "fatal_flush_from_next", test_fatal_flush_from_next },
315};
static int const char char buffer[256]
Definition acutest.h:641
#define TEST_CHECK(cond)
Definition acutest.h:101
#define TEST_CASE(name)
Definition acutest.h:203
#define TEST_TERMINATOR
Definition acutest.h:77
#define TEST_CHECK_RET(_got, _exp)
fr_bio_write_t _CONST write
write to the underlying bio
Definition base.h:117
static ssize_t fr_bio_write(fr_bio_t *bio, void *packet_ctx, void const *buffer, size_t size)
Write raw data to a bio.
Definition base.h:184
fr_bio_io_t read_resume
"unblocked" is too similar to "blocked"
Definition base.h:97
#define fr_bio_error(_x)
Definition base.h:200
fr_bio_t * fr_bio_queue_alloc(TALLOC_CTX *ctx, size_t max_saved, fr_bio_queue_saved_t saved, fr_bio_queue_callback_t sent, fr_bio_queue_callback_t cancel, fr_bio_t *next)
Allocate a packet-based bio.
Definition queue.c:351
static int packet_ctx
the tests pass only the address of packet_ctx
#define FR_BIO_COMMON
Common elements at the start of each private fr_bio_t.
Definition bio_priv.h:50
static void test_shutdown_keeps_app_callback(void)
The application installs its callbacks on a queue bio at the head of the chain.
TEST_LIST
static void test_recoverable_flush_passes_up(void)
The queue bio passes a recoverable flush error up to the application, and keeps the saved packet.
static void cb_eof(fr_bio_t *bio)
static fr_bio_cb_funcs_t test_cb
static ssize_t stub_write_one(UNUSED fr_bio_t *bio, UNUSED void *packet_ctx, UNUSED void const *buffer, UNUSED size_t size)
A transport which accepts one byte of each write, so the queue bio saves the rest of the packet.
static ssize_t stub_write_all(UNUSED fr_bio_t *bio, UNUSED void *packet_ctx, UNUSED void const *buffer, size_t size)
A transport which accepts every write in full.
static uint8_t fill[1000]
static void test_recoverable_write_passes_up(void)
The queue bio passes a recoverable write error up to the application, and keeps working.
bool fatal
shut the chain down before failing
static void queue_cancel(fr_bio_t *bio, void *packet_ctx, void const *buffer, size_t size)
static bool queue_fill(fr_bio_t *head)
Leave one packet partly written in the queue.
static int cb_shutdown(fr_bio_t *bio)
static void test_fatal_flush_from_next(void)
When the next bio shuts the chain down on a fatal flush error, fr_bio_queue_shutdown() cancels the sa...
static fr_bio_t * test_queue_saved_alloc(TALLOC_CTX *ctx, fr_bio_t **stub_p)
Build a queue in front of a stub, with one packet saved after a partial write.
static void test_shutdown_not_head(void)
A queue bio in the middle of a chain is not the head, so only the private slot reaches it.
static fr_bio_t * stub_alloc(TALLOC_CTX *ctx)
static ssize_t stub_write_fail(fr_bio_t *bio, UNUSED void *packet_ctx, UNUSED void const *buffer, UNUSED size_t size)
static int cancel_count
static int shutdown_count
static uint8_t overflow[100]
static int cb_noop(fr_bio_t *bio)
A stub transport which fails recoverably or fatally, as each test chooses.
#define UNUSED
Definition build.h:415
talloc_free(hp)
void fr_bio_cb_set(fr_bio_t *bio, fr_bio_cb_funcs_t const *cb)
Definition base.c:260
int fr_bio_shutdown(fr_bio_t *bio)
Shut down a set of BIOs.
Definition base.c:158
fr_bio_t * fr_bio_mem_alloc(TALLOC_CTX *ctx, size_t read_size, size_t write_size, fr_bio_t *next)
Allocate a memory buffer bio.
Definition mem.c:679
long int ssize_t
unsigned char uint8_t
ssize_t fr_bio_null_read(UNUSED fr_bio_t *bio, UNUSED void *packet_ctx, UNUSED void *buffer, UNUSED size_t size)
Always return 0 on read.
Definition null.c:31
fr_bio_t * fr_bio_pipe_alloc(TALLOC_CTX *ctx, fr_bio_cb_funcs_t *cb, size_t buffer_size)
Allocate a thread-safe pipe which can be used for both reads and writes.
Definition pipe.c:187
static bool done
Definition radclient.c:80
static TALLOC_CTX * talloc_init_const(char const *name)
Allocate a top level chunk with a constant name.
Definition talloc.h:127
static fr_slen_t head
Definition xlat.h:410
#define fr_strerror_const(_msg)
Definition strerror.h:223