1const builtin = @import("builtin");
2
3const std = @import("std");
4const Io = std.Io;
5const net = std.Io.net;
6const mem = std.mem;
7const testing = std.testing;
8const Allocator = std.mem.Allocator;
9
10test "parse and render IP addresses at comptime" {
11 comptime {
12 const ipv6addr = net.IpAddress.parse("::1", 0) catch unreachable;
13 try testing.expectFmt("[::1]:0", "{f}", .{ipv6addr});
14
15 const ipv4addr = net.IpAddress.parse("127.0.0.1", 0) catch unreachable;
16 try testing.expectFmt("127.0.0.1:0", "{f}", .{ipv4addr});
17
18 try testing.expectError(error.ParseFailed, net.IpAddress.parse("::123.123.123.123", 0));
19 try testing.expectError(error.ParseFailed, net.IpAddress.parse("127.01.0.1", 0));
20 }
21}
22
23test "format IPv6 address with no zero runs" {
24 const addr = try net.IpAddress.parseIp6("2001:db8:1:2:3:4:5:6", 0);
25 try testing.expectFmt("[2001:db8:1:2:3:4:5:6]:0", "{f}", .{addr});
26}
27
28test "parse IPv6 addresses and check compressed form" {
29 try testing.expectFmt("[2001:db8::1:0:0:2]:0", "{f}", .{
30 try net.IpAddress.parseIp6("2001:0db8:0000:0000:0001:0000:0000:0002", 0),
31 });
32 try testing.expectFmt("[2001:db8::1:2]:0", "{f}", .{
33 try net.IpAddress.parseIp6("2001:0db8:0000:0000:0000:0000:0001:0002", 0),
34 });
35 try testing.expectFmt("[2001:db8:1:0:1::2]:0", "{f}", .{
36 try net.IpAddress.parseIp6("2001:0db8:0001:0000:0001:0000:0000:0002", 0),
37 });
38}
39
40test "parse IPv6 address, check raw bytes" {
41 const expected_raw: [16]u8 = .{
42 0x20, 0x01, 0x0d, 0xb8, // 2001:db8
43 0x00, 0x00, 0x00, 0x00, // :0000:0000
44 0x00, 0x01, 0x00, 0x00, // :0001:0000
45 0x00, 0x00, 0x00, 0x02, // :0000:0002
46 };
47 const addr = try net.IpAddress.parseIp6("2001:db8:0000:0000:0001:0000:0000:0002", 0);
48 try testing.expectEqualSlices(u8, &expected_raw, &addr.ip6.bytes);
49}
50
51test "parse and render IPv6 addresses" {
52 try testParseAndRenderIp6Address("FF01:0:0:0:0:0:0:FB", "ff01::fb");
53 try testParseAndRenderIp6Address("FF01::Fb", "ff01::fb");
54 try testParseAndRenderIp6Address("::1", "::1");
55 try testParseAndRenderIp6Address("::", "::");
56 try testParseAndRenderIp6Address("1::", "1::");
57 try testParseAndRenderIp6Address("2001:db8::", "2001:db8::");
58 try testParseAndRenderIp6Address("::1234:5678", "::1234:5678");
59 try testParseAndRenderIp6Address("2001:db8::1234:5678", "2001:db8::1234:5678");
60 try testParseAndRenderIp6Address("FF01::FB%1234", "ff01::fb%1234");
61 try testParseAndRenderIp6Address("::ffff:123.5.123.5", "::ffff:123.5.123.5");
62 try testParseAndRenderIp6Address("ff01::fb%12345678901234", "ff01::fb%12345678901234");
63}
64
65fn testParseAndRenderIp6Address(input: []const u8, expected_output: []const u8) !void {
66 var buffer: [100]u8 = undefined;
67 const parsed = net.Ip6Address.Unresolved.parse(input);
68 const actual_printed = try std.mem.print(&buffer, "{f}", .{parsed.success});
69 try testing.expectEqualStrings(expected_output, actual_printed);
70}
71
72test "IPv6 address parse failures" {
73 try testing.expectError(error.ParseFailed, net.IpAddress.parseIp6(":::", 0));
74
75 const Unresolved = net.Ip6Address.Unresolved;
76
77 try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 2 }, Unresolved.parse(":::"));
78 try testing.expectEqual(Unresolved.Parsed{ .overflow = 4 }, Unresolved.parse("FF001::FB"));
79 try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 9 }, Unresolved.parse("FF01::Fb:zig"));
80 try testing.expectEqual(Unresolved.Parsed{ .junk_after_end = 19 }, Unresolved.parse("FF01:0:0:0:0:0:0:FB:"));
81 try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("FF01:"));
82 try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 5 }, Unresolved.parse("::123.123.123.123"));
83 try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("1"));
84 try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("ff01::fb%"));
85}
86
87test "invalid but parseable IPv6 scope ids" {
88 if (builtin.os.tag != .linux and builtin.os.tag != .windows and comptime !builtin.os.tag.isDarwin()) return error.SkipZigTest;
89
90 const io = testing.io;
91
92 try testing.expectError(error.InterfaceNotFound, net.IpAddress.resolveIp6(io, "ff01::fb%123s45678901234", 0));
93}
94
95test "oversized IPv6 scope id" {
96 const long_scope: [256]u8 = @splat('a');
97 try testing.expectError(error.ParseFailed, net.IpAddress.resolveIp6(testing.io, "ff01::fb%" ++ &long_scope, 0));
98}
99
100test "parse and render IPv4 addresses" {
101 try testIp4ParseAndRender("0.0.0.0");
102 try testIp4ParseAndRender("255.255.255.255");
103 try testIp4ParseAndRender("1.2.3.4");
104 try testIp4ParseAndRender("123.255.0.91");
105 try testIp4ParseAndRender("127.0.0.1");
106
107 try testing.expectError(error.Overflow, net.IpAddress.parseIp4("256.0.0.1", 0));
108 try testing.expectError(error.InvalidCharacter, net.IpAddress.parseIp4("x.0.0.1", 0));
109 try testing.expectError(error.InvalidEnd, net.IpAddress.parseIp4("127.0.0.1.1", 0));
110 try testing.expectError(error.Incomplete, net.IpAddress.parseIp4("127.0.0.", 0));
111 try testing.expectError(error.InvalidCharacter, net.IpAddress.parseIp4("100..0.1", 0));
112 try testing.expectError(error.NonCanonical, net.IpAddress.parseIp4("127.01.0.1", 0));
113}
114
115fn testIp4ParseAndRender(text: []const u8) !void {
116 var buffer: [18]u8 = undefined;
117 const addr = try net.IpAddress.parseIp4(text, 0);
118 const rendered = try std.mem.print(&buffer, "{f}", .{addr});
119 const without_port = rendered[0 .. rendered.len - 2];
120 try testing.expectEqualStrings(text, without_port);
121}
122
123test "resolve DNS" {
124 const io = testing.io;
125
126 // Resolve localhost, this should not fail.
127 {
128 const localhost_v4 = try net.IpAddress.parse("127.0.0.1", 80);
129 const localhost_v6 = try net.IpAddress.parse("::1", 80);
130
131 var canonical_name_buffer: [net.HostName.max_len]u8 = undefined;
132 var results_buffer: [32]net.HostName.LookupResult = undefined;
133 var results: Io.Queue(net.HostName.LookupResult) = .init(&results_buffer);
134
135 net.HostName.lookup(try .init("localhost"), io, &results, .{
136 .port = 80,
137 .canonical_name_buffer = &canonical_name_buffer,
138 }) catch |err| switch (err) {
139 error.NetworkDown => return error.SkipZigTest,
140 else => |e| return e,
141 };
142
143 var addresses_found: usize = 0;
144 var found_canonical_name = false;
145
146 while (results.getOne(io)) |result| switch (result) {
147 .address => |address| {
148 if (address.eql(&localhost_v4) or address.eql(&localhost_v6))
149 addresses_found += 1;
150 },
151 // We can't test the actual string here because it might be "localhost.localdomain"
152 // or even the computer host name (as on Windows).
153 .canonical_name => found_canonical_name = true,
154 } else |err| switch (err) {
155 error.Closed => {},
156 error.Canceled => |e| return e,
157 }
158
159 try testing.expect(found_canonical_name);
160 try testing.expect(addresses_found != 0);
161 }
162
163 {
164 // The tests are required to work even when there is no Internet connection,
165 // so some of these errors we must accept and skip the test.
166 var canonical_name_buffer: [net.HostName.max_len]u8 = undefined;
167 var results_buffer: [16]net.HostName.LookupResult = undefined;
168 var results: Io.Queue(net.HostName.LookupResult) = .init(&results_buffer);
169
170 net.HostName.lookup(try .init("example.com"), io, &results, .{
171 .port = 80,
172 .canonical_name_buffer = &canonical_name_buffer,
173 }) catch |err| switch (err) {
174 error.UnknownHostName => return error.SkipZigTest,
175 error.NameServerFailure => return error.SkipZigTest,
176 else => |e| return e,
177 };
178
179 while (results.getOne(io)) |result| switch (result) {
180 .address => {},
181 .canonical_name => {},
182 } else |err| switch (err) {
183 error.Closed => {},
184 error.Canceled => |e| return e,
185 }
186 }
187}
188
189test "listen on a port, send bytes, receive bytes" {
190 if (true) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/31388
191
192 const io = testing.io;
193
194 // Try only the IPv4 variant as some CI builders have no IPv6 localhost
195 // configured.
196 const localhost: net.IpAddress = .{ .ip4 = .loopback(0) };
197
198 var server = localhost.listen(io, .{}) catch |err| switch (err) {
199 error.NetworkDown => return error.SkipZigTest,
200 else => |e| return e,
201 };
202 defer server.deinit(io);
203
204 const S = struct {
205 fn clientFn(server_address: net.IpAddress) !void {
206 var stream = try server_address.connect(io, .{ .mode = .stream });
207 defer stream.close(io);
208
209 var stream_writer = stream.writer(io, &.{});
210 try stream_writer.interface.writeAll("Hello world!");
211 }
212 };
213
214 var client_task = io.concurrent(S.clientFn, .{server.socket.address}) catch |err| switch (err) {
215 error.ConcurrencyUnavailable => return error.SkipZigTest,
216 };
217 defer client_task.cancel(io) catch {};
218
219 var stream = try server.accept(io);
220 defer stream.close(io);
221
222 var buf: [16]u8 = undefined;
223 var stream_reader = stream.reader(io, &.{});
224 const n = try stream_reader.interface.readSliceShort(&buf);
225
226 try testing.expectEqual(@as(usize, 12), n);
227 try testing.expectEqualStrings("Hello world!", buf[0..n]);
228
229 try client_task.await(io);
230}
231
232test "listen on an in use port" {
233 const io = testing.io;
234
235 const localhost: net.IpAddress = .{ .ip4 = .loopback(0) };
236
237 var server1 = localhost.listen(io, .{ .reuse_address = true }) catch |err| switch (err) {
238 error.NetworkDown => return error.SkipZigTest,
239 else => |e| return e,
240 };
241 defer server1.deinit(io);
242
243 var server2 = try server1.socket.address.listen(io, .{ .reuse_address = true });
244 defer server2.deinit(io);
245}
246
247test "listen on a unix socket, send bytes, receive bytes" {
248 if (builtin.os.tag == .windows) {
249 // https://codeberg.org/ziglang/zig/issues/31499
250 return error.SkipZigTest;
251 }
252
253 const io = testing.io;
254 const gpa = testing.allocator;
255
256 const socket_path = try generateFileName(gpa, io, "socket.unix");
257 defer gpa.free(socket_path);
258
259 const socket_addr = try net.UnixAddress.init(socket_path);
260 defer Io.Dir.cwd().deleteFile(io, socket_path) catch {};
261
262 var server = socket_addr.listen(io, .{}) catch |err| switch (err) {
263 error.AddressFamilyUnsupported => return error.SkipZigTest,
264 error.NetworkDown => return error.SkipZigTest,
265 else => |e| return e,
266 };
267 defer server.socket.close(io);
268
269 const S = struct {
270 fn clientFn(path: []const u8) !void {
271 const server_path: net.UnixAddress = try .init(path);
272 var stream = try server_path.connect(io);
273 defer stream.close(io);
274
275 var stream_writer = stream.writer(io, &.{});
276 try stream_writer.interface.writeAll("Hello world!");
277 }
278 };
279
280 var client_task = io.concurrent(S.clientFn, .{socket_path}) catch |err| switch (err) {
281 error.ConcurrencyUnavailable => return error.SkipZigTest,
282 };
283 defer client_task.cancel(io) catch {};
284
285 var stream = try server.accept(io);
286 defer stream.close(io);
287
288 var buf: [16]u8 = undefined;
289 var stream_reader = stream.reader(io, &.{});
290 const n = try stream_reader.interface.readSliceShort(&buf);
291
292 try testing.expectEqual(@as(usize, 12), n);
293 try testing.expectEqualStrings("Hello world!", buf[0..n]);
294
295 try client_task.await(io);
296}
297
298fn generateFileName(gpa: Allocator, io: Io, base_name: []const u8) ![]const u8 {
299 const random_bytes_count = 12;
300 const sub_path_len = comptime std.base64.url_safe.Encoder.calcSize(random_bytes_count);
301 var random_bytes: [12]u8 = undefined;
302 io.random(&random_bytes);
303 var sub_path: [sub_path_len]u8 = undefined;
304 _ = std.base64.url_safe.Encoder.encode(&sub_path, &random_bytes);
305 return std.fmt.allocPrint(gpa, "{s}-{s}", .{ sub_path[0..], base_name });
306}
307
308test "decompress compressed DNS name" {
309 const packet = &[_]u8{
310 // Header section
311 // ----------------------------------------------------------
312 0x00, 0x00, // ID: 0
313 0x81, 0x80, // Flags (QR:1, RCODE:0, etc.)
314 0x00, 0x01, // QDCOUNT: 1 (Question Count)
315 0x00, 0x01, // ANCOUNT: 1 (Answer Count)
316 0x00, 0x00, // NSCOUNT: 0
317 0x00, 0x00, // ARCOUNT: 0
318 // ----------------------------------------------------------
319
320 // Question section (12 byte offset)
321 // ----------------------------------------------------------
322 // QNAME: "www.ziglang.org"
323 0x03, 'w', 'w', 'w', // label
324 0x07, 'z', 'i', 'g', 'l', 'a', 'n', 'g', // label
325 0x03, 'o', 'r', 'g', // label
326 0x00, // null label
327 0x00, 0x01, // QTYPE: A
328 0x00, 0x01, // QCLASS: IN
329 // ----------------------------------------------------------
330
331 // Resource Record (Answer) (33 byte offset)
332 // ----------------------------------------------------------
333 0xc0, 0x0c, // NAME: pointer to 0x0c ("www.ziglang.org")
334 0x00, 0x05, // TYPE: CNAME
335 0x00, 0x01, // CLASS: IN
336 0x00, 0x00, 0x01, 0x00, // TTL: 256 (seconds)
337 0x00, 0x06, // RDLENGTH: 6 bytes
338
339 // RDATA (45 byte offset): "target.ziglang.org" (compressed)
340 0x06, 't', 'a', 'r', 'g', 'e', 't', // 6, "target"
341 0xc0, 0x10, // pointer (0xc0 flag) to 0x10 byte offset ("ziglang.org")
342 // ----------------------------------------------------------
343 };
344
345 // The start index of the CNAME RDATA is 45
346 const cname_data_index = 45;
347 var dest_buffer: [net.HostName.max_len]u8 = undefined;
348
349 const n_consumed, const result = try net.HostName.expand(
350 packet,
351 cname_data_index,
352 &dest_buffer,
353 );
354 try testing.expectEqual(packet.len - cname_data_index, n_consumed);
355 try testing.expectEqualStrings("target.ziglang.org", result.bytes);
356}
357
358test "cancel accept" {
359 const io = testing.io;
360 const localhost: net.IpAddress = .{ .ip4 = .loopback(0) };
361
362 var server = localhost.listen(io, .{}) catch |err| switch (err) {
363 error.NetworkDown => return error.SkipZigTest,
364 else => |e| return e,
365 };
366 defer server.deinit(io);
367
368 var accept = io.concurrent(std.Io.net.Server.accept, .{ &server, io }) catch |err| switch (err) {
369 error.ConcurrencyUnavailable => return error.SkipZigTest,
370 };
371 defer if (accept.cancel(io)) |stream| stream.close(io) else |_| {};
372
373 try io.sleep(.fromNanoseconds(1), .awake);
374}
375
376test "UDP send and receive" {
377 const io = testing.io;
378 const localhost: net.IpAddress = .{ .ip4 = .loopback(0) };
379
380 const recv_sock = localhost.bind(io, .{ .mode = .dgram }) catch |err| switch (err) {
381 error.NetworkDown => return error.SkipZigTest,
382 else => |e| return e,
383 };
384 defer recv_sock.close(io);
385 const send_sock = try localhost.bind(io, .{ .mode = .dgram });
386 defer send_sock.close(io);
387
388 const send_data: [3]u8 = .{ '1', '2', '3' };
389 try send_sock.send(io, &recv_sock.address, &send_data);
390
391 var recv_buf: [4]u8 = undefined;
392 const received = try recv_sock.receive(io, &recv_buf);
393 try testing.expect(received.from.eql(&send_sock.address));
394 try testing.expectEqualStrings(&send_data, received.data);
395}
396
397test "UDP sendTimeout and receiveTimeout" {
398 const io = testing.io;
399 const localhost: net.IpAddress = .{ .ip4 = .loopback(0) };
400
401 const recv_sock = localhost.bind(io, .{ .mode = .dgram }) catch |err| switch (err) {
402 error.NetworkDown => return error.SkipZigTest,
403 else => |e| return e,
404 };
405 defer recv_sock.close(io);
406 const send_sock = try localhost.bind(io, .{ .mode = .dgram });
407 defer send_sock.close(io);
408
409 const six_hours: Io.Timeout = .{ .duration = .{ .clock = .awake, .raw = .fromSeconds(21600) } };
410
411 const send_data: [3]u8 = .{ '1', '2', '3' };
412 send_sock.sendTimeout(io, &recv_sock.address, &send_data, six_hours) catch |err| switch (err) {
413 error.ConcurrencyUnavailable => return error.SkipZigTest,
414 else => |e| return e,
415 };
416
417 var recv_buf: [4]u8 = undefined;
418 const received = try recv_sock.receiveTimeout(io, &recv_buf, six_hours);
419 try testing.expect(received.from.eql(&send_sock.address));
420 try testing.expectEqualStrings(&send_data, received.data);
421
422 const short: Io.Timeout = .{ .duration = .{ .clock = .awake, .raw = .fromMicroseconds(123) } };
423 try testing.expectError(error.Timeout, recv_sock.receiveTimeout(io, &recv_buf, short));
424}
425
426test "UDP sendMany 2 and receive 2" {
427 const io = testing.io;
428 const localhost: net.IpAddress = .{ .ip4 = .loopback(0) };
429
430 const recv_sock = localhost.bind(io, .{ .mode = .dgram }) catch |err| switch (err) {
431 error.NetworkDown => return error.SkipZigTest,
432 else => |e| return e,
433 };
434 defer recv_sock.close(io);
435 const send_sock = try localhost.bind(io, .{ .mode = .dgram });
436 defer send_sock.close(io);
437
438 const send_data: [3]u8 = .{ '1', '2', '3' };
439 var send_msgs: [2]Io.net.OutgoingMessage = @splat(.{
440 .address = &recv_sock.address,
441 .data_ptr = &send_data,
442 .data_len = 3,
443 });
444 // note sendMany is deprecated, but should remain tested until removed
445 try send_sock.sendMany(io, &send_msgs, .{});
446 try testing.expectEqual(3, send_msgs[0].data_len);
447 try testing.expectEqual(3, send_msgs[1].data_len);
448
449 var recv_buf: [4]u8 = undefined;
450
451 {
452 const first = try recv_sock.receive(io, &recv_buf);
453 try testing.expect(first.from.eql(&send_sock.address));
454 try testing.expectEqualStrings(&send_data, first.data);
455 }
456
457 {
458 const second = try recv_sock.receive(io, &recv_buf);
459 try testing.expect(second.from.eql(&send_sock.address));
460 try testing.expectEqualStrings(&send_data, second.data);
461 }
462}
463
464test "UDP sendManyTimeout 1 recvManyTimeout 2" {
465 const io = testing.io;
466 const localhost: net.IpAddress = .{ .ip4 = .loopback(0) };
467
468 const recv_sock = localhost.bind(io, .{ .mode = .dgram }) catch |err| switch (err) {
469 error.NetworkDown => return error.SkipZigTest,
470 else => |e| return e,
471 };
472 defer recv_sock.close(io);
473 const send_sock = try localhost.bind(io, .{ .mode = .dgram });
474 defer send_sock.close(io);
475
476 const six_hours: Io.Timeout = .{ .duration = .{ .clock = .awake, .raw = .fromSeconds(21600) } };
477 const send_data: [3]u8 = .{ '1', '2', '3' };
478 var send_msg: Io.net.OutgoingMessage = .{
479 .address = &recv_sock.address,
480 .data_ptr = &send_data,
481 .data_len = 3,
482 };
483
484 const maybe_send_err, const send_count = send_sock.sendManyTimeout(io, (&send_msg)[0..1], .{}, six_hours);
485 if (maybe_send_err) |err| switch (err) {
486 error.ConcurrencyUnavailable => return error.SkipZigTest,
487 else => |e| return e,
488 };
489 try testing.expectEqual(1, send_count);
490 try testing.expectEqual(3, send_msg.data_len);
491
492 // This should complete as soon as the first message arrives, and not stall
493 // for the timeout waiting on the second one.
494 var recv_msgs: [2]net.IncomingMessage = @splat(.init);
495 var recv_buf: [10]u8 = undefined;
496 const maybe_recv_err, const recv_count = recv_sock.receiveManyTimeout(io, &recv_msgs, &recv_buf, .{}, six_hours);
497 if (maybe_recv_err) |err| return err;
498 try testing.expectEqual(1, recv_count);
499 try testing.expect(recv_msgs[0].from.eql(&send_sock.address));
500 try testing.expectEqualStrings(&send_data, recv_msgs[0].data);
501}
502
503fn testUdpSender(io: Io, send_sock: Io.net.Socket, send_data: []const u8, dest: Io.net.IpAddress) !void {
504 try io.sleep(.fromMilliseconds(10), .boot);
505 try send_sock.send(io, &dest, send_data);
506 try send_sock.send(io, &dest, send_data);
507 try io.sleep(.fromMilliseconds(10), .boot);
508 try send_sock.send(io, &dest, send_data);
509}
510
511test "UDP concurrency and timeouts" {
512 const io = testing.io;
513 const localhost: net.IpAddress = .{ .ip4 = .loopback(0) };
514
515 const recv_sock = localhost.bind(io, .{ .mode = .dgram }) catch |err| switch (err) {
516 error.NetworkDown => return error.SkipZigTest,
517 else => |e| return e,
518 };
519 defer recv_sock.close(io);
520 const send_sock = try localhost.bind(io, .{ .mode = .dgram });
521 defer send_sock.close(io);
522
523 const send_data: [3]u8 = .{ '1', '2', '3' };
524 var sender = io.async(testUdpSender, .{ io, send_sock, &send_data, recv_sock.address });
525 defer sender.cancel(io) catch {};
526
527 // Because the sender is async (may execute serially or concurrently) and
528 // it has some 10ms timing gaps, it's likely that there will be a variety
529 // of random behaviors (total iterations, messages per iteration) on the
530 // receive end of things related to the target and runner conditions. This
531 // should still suceed so long as all 3 packets arrive in reasonable time.
532 const six_hours: Io.Timeout = .{ .duration = .{ .clock = .awake, .raw = .fromSeconds(21600) } };
533 var received: usize = 0;
534 for (0..3) |_| {
535 var recv_msgs: [3]net.IncomingMessage = @splat(.init);
536 var recv_buf: [9]u8 = undefined;
537 const maybe_recv_err, const recv_count = recv_sock.receiveManyTimeout(io, &recv_msgs, &recv_buf, .{}, six_hours);
538 if (maybe_recv_err) |err| switch (err) {
539 error.ConcurrencyUnavailable => return error.SkipZigTest,
540 else => |e| return e,
541 };
542 received += recv_count;
543 try testing.expect(received <= 3);
544 for (0..recv_count) |i| {
545 const msg = recv_msgs[i];
546 try testing.expect(msg.from.eql(&send_sock.address));
547 try testing.expectEqualStrings(&send_data, msg.data);
548 }
549 if (received == 3) break;
550 }
551 try testing.expectEqual(3, received);
552 try sender.await(io); // ensure sender didn't fail
553}