summaryrefslogtreecommitdiff
path: root/tests/t_batch.c
diff options
context:
space:
mode:
authorDirk Engling <erdgeist@erdgeist.org>2026-10-25 22:30:04 +0100
committerDirk Engling <erdgeist@erdgeist.org>2026-10-25 22:30:04 +0100
commit0e3a2189f65354962fc71af41f5aa52261a782e8 (patch)
treeeae9ec0a8930ae7cc2de02b9ed1d797bfbc2e6e8 /tests/t_batch.c
parent3f7d4f4e8bc8cd9928fd0b1b280eb4c3f0ca9fa5 (diff)
Replace libowfat's iobatch with our own smaller (and probably only temporary) ot_batch
Diffstat (limited to 'tests/t_batch.c')
-rw-r--r--tests/t_batch.c348
1 files changed, 348 insertions, 0 deletions
diff --git a/tests/t_batch.c b/tests/t_batch.c
new file mode 100644
index 0000000..02743f1
--- /dev/null
+++ b/tests/t_batch.c
@@ -0,0 +1,348 @@
1/* This software was written by Dirk Engling <erdgeist@erdgeist.org>
2 It is considered beerware. Prost. Skol. Cheers or whatever.
3
4 $id$ */
5
6/* System */
7#include <fcntl.h>
8#include <poll.h>
9#include <stdlib.h>
10#include <string.h>
11#include <sys/socket.h>
12#include <unistd.h>
13
14/* Opentracker */
15#include "ot_batch.h"
16#include "test.h"
17
18/* The batch writes to a real socket pair, because what is worth pinning down
19 is what it does when the kernel takes only part of what it was offered. */
20
21static char *dup_of(const char *text) {
22 size_t len = strlen(text);
23 char *copy = malloc(len);
24
25 memcpy(copy, text, len);
26 return copy;
27}
28
29static void drain(int s, char *into, size_t len) {
30 size_t got = 0;
31
32 while (got < len) {
33 ssize_t r = read(s, into + got, len - got);
34
35 if (r <= 0)
36 break;
37 got += (size_t)r;
38 }
39 into[got] = 0;
40}
41
42static void case_queue_accounting(void) {
43 ot_batch batch;
44
45 memset(&batch, 0, sizeof(batch));
46
47 test_case("a zeroed batch is empty and sends nothing");
48 CHECK_INT(batch_pending(&batch), 0);
49 CHECK_INT(batch_send(-1, &batch), BATCH_DONE);
50
51 test_case("anything queued makes it pending");
52 CHECK_INT(batch_add(&batch, dup_of("hello "), 6), 0);
53 CHECK_INT(batch_add_copy(&batch, "world", 5), 0);
54 CHECK_INT(batch_pending(&batch), 1);
55 CHECK_INT(batch.count, 2);
56
57 batch_reset(&batch);
58 test_case("and a reset empties it");
59 CHECK_INT(batch_pending(&batch), 0);
60 CHECK_PTR(batch.entries, NULL);
61}
62
63static void case_send_in_one_go(void) {
64 ot_batch batch;
65 int sv[2];
66 char got[32];
67
68 memset(&batch, 0, sizeof(batch));
69 CHECK_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0);
70
71 test_case("several buffers arrive as one stream, in order");
72 CHECK_INT(batch_add(&batch, dup_of("opentracker"), 11), 0);
73 CHECK_INT(batch_add_copy(&batch, " says ", 6), 0);
74 CHECK_INT(batch_add(&batch, dup_of("hello"), 5), 0);
75 CHECK_INT(batch_send(sv[0], &batch), 22);
76 CHECK_INT(batch_pending(&batch), 0);
77
78 drain(sv[1], got, 22);
79 CHECK_STR(got, "opentracker says hello");
80
81 test_case("and an empty batch reports it has nothing left");
82 CHECK_INT(batch_send(sv[0], &batch), BATCH_DONE);
83
84 batch_reset(&batch);
85 close(sv[0]);
86 close(sv[1]);
87}
88
89/* Send everything the batch holds, reading the other end as we go so the
90 socket never stays full, and return what arrived. */
91static size_t pump(int writer, int reader, ot_batch *batch, char *into, size_t room) {
92 size_t got = 0;
93
94 while (batch_pending(batch) || got < room) {
95 ssize_t r;
96
97 if (batch_pending(batch)) {
98 ssize_t res = batch_send(writer, batch);
99
100 if (res == BATCH_FAILED)
101 break;
102 if (res > 0)
103 continue;
104 }
105
106 r = read(reader, into + got, room - got);
107 if (r <= 0)
108 break;
109 got += (size_t)r;
110 }
111 return got;
112}
113
114static void case_partial_write_resumes(void) {
115 const size_t size = 200000;
116 ot_batch batch;
117 int sv[2], sndbuf = 4096;
118 char *sent, *got;
119 size_t i, arrived;
120
121 memset(&batch, 0, sizeof(batch));
122 CHECK_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0);
123 setsockopt(sv[0], SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
124 fcntl(sv[0], F_SETFL, fcntl(sv[0], F_GETFL, 0) | O_NONBLOCK);
125
126 sent = malloc(size);
127 got = malloc(size);
128 for (i = 0; i < size; ++i)
129 sent[i] = (char)(i % 251);
130
131 test_case("a buffer the kernel takes in pieces arrives whole and in order");
132 CHECK_INT(batch_add(&batch, memcpy(malloc(size), sent, size), size), 0);
133 arrived = pump(sv[0], sv[1], &batch, got, size);
134 CHECK_INT(arrived, size);
135 CHECK_INT(batch_pending(&batch), 0);
136 CHECK_MEM(got, sent, size);
137
138 free(sent);
139 free(got);
140 batch_reset(&batch);
141 close(sv[0]);
142 close(sv[1]);
143}
144
145/* writev refuses more than IOV_MAX entries at once, so a long batch has to be
146 sent across several calls. */
147static void case_more_entries_than_one_writev(void) {
148 const size_t entries = 3000, each = 4;
149 ot_batch batch;
150 int sv[2], sndbuf = 4096;
151 char *sent, *got;
152 size_t i, arrived, refused = 0;
153
154 memset(&batch, 0, sizeof(batch));
155 CHECK_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0);
156 setsockopt(sv[0], SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
157 fcntl(sv[0], F_SETFL, fcntl(sv[0], F_GETFL, 0) | O_NONBLOCK);
158
159 sent = malloc(entries * each);
160 got = malloc(entries * each);
161
162 test_case("a batch of more entries than writev accepts still arrives whole");
163 for (i = 0; i < entries; ++i) {
164 char piece[4];
165
166 piece[0] = (char)(i), piece[1] = (char)(i >> 8), piece[2] = 'x', piece[3] = 'y';
167 memcpy(sent + i * each, piece, each);
168 if (batch_add(&batch, memcpy(malloc(each), piece, each), each))
169 ++refused;
170 }
171 CHECK_INT(refused, 0);
172 CHECK_INT(batch.count, entries);
173
174 arrived = pump(sv[0], sv[1], &batch, got, entries * each);
175 CHECK_INT(arrived, entries * each);
176 CHECK_MEM(got, sent, entries * each);
177
178 free(sent);
179 free(got);
180 batch_reset(&batch);
181 close(sv[0]);
182 close(sv[1]);
183}
184
185static void case_reset_frees_the_unsent(void) {
186 ot_batch batch;
187 int sv[2];
188
189 memset(&batch, 0, sizeof(batch));
190 CHECK_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0);
191
192 test_case("a batch torn down mid flight keeps no bytes outstanding");
193 CHECK_INT(batch_add(&batch, dup_of("first"), 5), 0);
194 CHECK_INT(batch_add(&batch, dup_of("second"), 6), 0);
195 CHECK_INT(batch_send(sv[0], &batch), 11);
196 CHECK_INT(batch_add(&batch, dup_of("never sent"), 10), 0);
197 CHECK_INT(batch_pending(&batch), 1);
198
199 /* Whether the unsent buffer is freed is for the sanitizer to say */
200 batch_reset(&batch);
201 CHECK_INT(batch_pending(&batch), 0);
202 CHECK_INT(batch.count, 0);
203
204 close(sv[0]);
205 close(sv[1]);
206}
207
208static void case_closed_socket_is_fatal(void) {
209 ot_batch batch;
210 int sv[2];
211
212 memset(&batch, 0, sizeof(batch));
213 CHECK_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0);
214 close(sv[1]);
215
216 test_case("writing to a socket whose peer is gone reports a dead connection");
217 CHECK_INT(batch_add(&batch, dup_of("nobody is listening"), 19), 0);
218 CHECK_INT(batch_send(sv[0], &batch), BATCH_FAILED);
219
220 batch_reset(&batch);
221 close(sv[0]);
222}
223
224static void case_vector_is_taken_whole(void) {
225 ot_batch batch;
226 struct iovec *vector = malloc(3 * sizeof(struct iovec));
227 int sv[2];
228 char got[32];
229
230 memset(&batch, 0, sizeof(batch));
231 CHECK_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0);
232
233 test_case("a whole vector is queued in one go, in order");
234 vector[0].iov_base = dup_of("one "), vector[0].iov_len = 4;
235 vector[1].iov_base = dup_of("two "), vector[1].iov_len = 4;
236 vector[2].iov_base = dup_of("three"), vector[2].iov_len = 5;
237 CHECK_INT(batch_add_vector(&batch, vector, 3), 0);
238 free(vector);
239 CHECK_INT(batch.count, 3);
240 CHECK_INT(batch_send(sv[0], &batch), 13);
241
242 drain(sv[1], got, 13);
243 CHECK_STR(got, "one two three");
244
245 batch_reset(&batch);
246 close(sv[0]);
247 close(sv[1]);
248}
249
250/* The entry array starts small and doubles, so crossing that boundary one
251 buffer at a time is worth its own case. */
252static void case_growing_one_entry_at_a_time(void) {
253 const size_t entries = 100;
254 ot_batch batch;
255 int sv[2];
256 char sent[400], got[401];
257 size_t i, refused = 0;
258
259 memset(&batch, 0, sizeof(batch));
260 CHECK_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0);
261
262 test_case("a batch grown one buffer at a time keeps them all, in order");
263 for (i = 0; i < entries; ++i) {
264 char piece[4];
265
266 piece[0] = (char)('a' + i % 26), piece[1] = (char)('0' + i % 10);
267 piece[2] = (char)('A' + i % 26), piece[3] = '.';
268 memcpy(sent + i * 4, piece, 4);
269 if (batch_add(&batch, memcpy(malloc(4), piece, 4), 4))
270 ++refused;
271 }
272 CHECK_INT(refused, 0);
273 CHECK_INT(batch.count, entries);
274 CHECK_INT(batch_send(sv[0], &batch), (ssize_t)(entries * 4));
275
276 drain(sv[1], got, entries * 4);
277 CHECK_MEM(got, sent, entries * 4);
278
279 batch_reset(&batch);
280 close(sv[0]);
281 close(sv[1]);
282}
283
284/* Appending while earlier entries are still on their way is what a chunked
285 transfer does. Rather than depend on when a kernel decides to block, this
286 takes entries off the front the way a completed send would and checks that
287 the array reuses the space instead of growing. */
288static void case_append_while_sending(void) {
289 const size_t per_round = 12, each = 8;
290 ot_batch batch;
291 int sv[2];
292 char expected[16], got[17];
293 size_t i, round, refused = 0, space_after_first = 0;
294
295 memset(&batch, 0, sizeof(batch));
296 CHECK_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0);
297
298 test_case("an array whose front has gone out is reused, not grown");
299 for (round = 0; round < 8; ++round) {
300 for (i = 0; i < per_round; ++i) {
301 /* Distinct per entry, so a move from the wrong index shows up */
302 char *piece = malloc(each);
303
304 memset(piece, (int)('a' + (round * per_round + i) % 26), each);
305 piece[0] = (char)('0' + i);
306 piece[1] = (char)('0' + round);
307 if (batch_add(&batch, piece, each))
308 ++refused;
309 }
310
311 /* All but the last entry have left and been freed */
312 while (batch.count - batch.first > 1)
313 free(batch.entries[batch.first++].iov_base);
314
315 if (!round)
316 space_after_first = batch.space;
317 }
318 CHECK_INT(refused, 0);
319 CHECK_INT(batch.space, space_after_first);
320
321 test_case("and what is still queued survives the move, in order");
322 CHECK_INT(batch.count - batch.first, 1);
323 CHECK_INT(batch_add(&batch, memset(malloc(each), 'Z', each), each), 0);
324 CHECK_INT(batch_send(sv[0], &batch), (ssize_t)(2 * each));
325
326 memset(expected, (int)('a' + (7 * per_round + per_round - 1) % 26), each);
327 expected[0] = (char)('0' + per_round - 1);
328 expected[1] = '7';
329 memset(expected + each, 'Z', each);
330 drain(sv[1], got, 2 * each);
331 CHECK_MEM(got, expected, 2 * each);
332
333 batch_reset(&batch);
334 close(sv[0]);
335 close(sv[1]);
336}
337
338void test_batch(void) {
339 case_queue_accounting();
340 case_send_in_one_go();
341 case_vector_is_taken_whole();
342 case_growing_one_entry_at_a_time();
343 case_append_while_sending();
344 case_partial_write_resumes();
345 case_more_entries_than_one_writev();
346 case_reset_frees_the_unsent();
347 case_closed_socket_is_fatal();
348}