<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Threading-the-Needle on hereticles</title><link>https://icle.es/tags/threading-the-needle/</link><description>Recent content in Threading-the-Needle on hereticles</description><generator>Hugo</generator><language>en</language><lastBuildDate>Sun, 21 Jun 2026 17:14:49 +0100</lastBuildDate><atom:link href="https://icle.es/tags/threading-the-needle/index.xml" rel="self" type="application/rss+xml"/><item><title>Testing SSE in zig When EVERYTHING Blocks</title><link>https://icle.es/2026/06/21/testing-sse-in-zig-when-everything-blocks/</link><pubDate>Sun, 21 Jun 2026 17:14:49 +0100</pubDate><guid>https://icle.es/2026/06/21/testing-sse-in-zig-when-everything-blocks/</guid><description>&lt;p&gt;I rely on TDD heavily and really don&amp;rsquo;t like manual testing. So when I started
building SSE support into &lt;a href="https://icle.es/excursions/mantel.md"&gt;mantel&lt;/a&gt;, I wanted a proper
e2e test — connect a client, read the events, assert on them.&lt;/p&gt;
&lt;p&gt;Except, I ran into a problem.
&lt;a href="https://github.com/karlseguin/http.zig"&gt;http.zig&lt;/a&gt;&amp;rsquo;s test helpers don&amp;rsquo;t work
with SSE - they expect a request/response cycle that completes. And zig 0.16&amp;rsquo;s
new Io framework supports
&lt;a href="https://ziglang.org/download/0.16.0/release-notes.html#Batch"&gt;neither cancellation&lt;/a&gt;
&lt;a href="https://ziggit.dev/t/non-blocked-sockets-programming-in-0-16/14978"&gt;nor non-blocking reads&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So: everything blocks, and there&amp;rsquo;s no way to stop it.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I rely on TDD heavily and really don&rsquo;t like manual testing. So when I started
building SSE support into <a href="https://icle.es/excursions/mantel.md">mantel</a>, I wanted a proper
e2e test — connect a client, read the events, assert on them.</p>
<p>Except, I ran into a problem.
<a href="https://github.com/karlseguin/http.zig">http.zig</a>&rsquo;s test helpers don&rsquo;t work
with SSE - they expect a request/response cycle that completes. And zig 0.16&rsquo;s
new Io framework supports
<a href="https://ziglang.org/download/0.16.0/release-notes.html#Batch">neither cancellation</a>
<a href="https://ziggit.dev/t/non-blocked-sockets-programming-in-0-16/14978">nor non-blocking reads</a>.</p>
<p>So: everything blocks, and there&rsquo;s no way to stop it.</p>
<p>If I wasn&rsquo;t building this because it&rsquo;s fun, I&rsquo;d probably skip it. Actually, I&rsquo;d
just use go where testing this is easy.</p>
<p>I am not doing this for work, and this kind of &ldquo;threading the needle&rdquo; solution
finding is something I enjoy. Plus, I am writing zig because I love it.</p>
<p>I&rsquo;d actually decided to let it be and moved on, but in my sleep, a solution came
to me, and it worked.</p>
<h2 id="if-we-could-cancel">If We Could Cancel</h2>
<p>The normal testing pattern for this, for example in Go, would be something like:</p>
```
- Set up the server
- Connect the Client with a timeout
- Read from the client
```
<p>Here is a version of it from <a href="https://icle.es/endeavours/henge.md">henge</a>.</p>
```go
testServer := httptest.NewServer(router)
defer testServer.Close()

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
scanner := connectToDevice(ctx, testServer)
// we don't care about the initial event
scanner.Scan() // blocks until first event arrives
// ...
```
<p>You could do something similar if it didn&rsquo;t support timeout, but did support
non-blocking reads. You just wait until your timeout and see if the data was
there.</p>
<p>Unfortunately, on zig, <code>std.Io.Evented</code> — the non-blocking backend — is still a
work in progress.&quot;</p>
<h2 id="unblocking">(Un)Blocking</h2>
<p>The idea that came to me was simply that we could close the server stream
<em>before</em> we read as the client. In other words:</p>
```
- Start the Server in a new thread
- Connect Client, but don't read
- Close the server
- Read all bytes from client
  - Safe now because stream has been closed
```
<p><a href="https://icle.es/src/Server.zig">Server.zig</a></p>
```zig
test "e2e test" {
    const io = std.testing.io;
    const allocator = std.testing.allocator;

    var server = try init(io, allocator);
    defer server.deinit(allocator);

    var router = try server.https_srv.router(.{});
    router.get("/feed", handleFeed, .{});

    // start server async
    const thread = try server.https_srv.listenInNewThread();
    // The order of the following two lines is crucial
    defer thread.join();
    defer server.https_srv.stop();

    const conf_address = server.https_srv.config.address.ip.ip4;
    try httpz.testing.waitForPort(conf_address.port);

    const address = std.Io.net.IpAddress.parse("127.0.0.1", conf_address.port) catch unreachable;
    const stream = try address.connect(io, .{ .mode = .stream });
    defer stream.close(io);

    // send HTTP request first
    var write_buf: [256]u8 = undefined;
    var writer = stream.writer(io, &write_buf);
    const w = &writer.interface;
    w.writeAll("GET /feed HTTP/1.1\r\nHost: localhost\r\n\r\n") catch unreachable;
    w.flush() catch unreachable;

    // wait for feed to start
    while (!server.ctx.feed.active) {
        std.Io.sleep(io, std.Io.Duration.fromMilliseconds(100), std.Io.Clock.real) catch unreachable;
    }

    try server.ctx.feed.stopFeed();

    var read_buf: [1024]u8 = undefined;
    var reader = stream.reader(io, &read_buf);

    // drain the stream
    var body: std.ArrayList(u8) = .empty;
    defer body.deinit(allocator);
    try reader.interface.appendRemaining(allocator, &body, .unlimited);

    try std.testing.expect(std.mem.indexOf(u8, body.items, "a message") != null);
}
```
<p>And in <a href="https://icle.es/src/Feed.zig">Feed.zig</a>, we need to ensure the stream is closed
on de-activating the feed:</p>
```zig
fn handle(feed: *Feed, stream: std.Io.net.Stream) void {
    var buf: [4096]u8 = undefined;
    var writer = stream.writer(feed.io, &buf);
    const w = &writer.interface;
    w.writeAll("a message\n") catch unreachable;
    w.flush() catch unreachable;

    while (feed.active) {
        std.Io.sleep(feed.io, std.Io.Duration.fromMilliseconds(100), std.Io.Clock.real) catch unreachable;
    }

    stream.close(feed.io);
}
```
<p>Feed.zig also contains details about the active flag and how it&rsquo;s managed</p>
```zig
pub fn startFeed(feed: *Feed, res: *httpz.Response) !void {
    try feed.mutex.lock(feed.io);
    defer feed.mutex.unlock(feed.io);

    if (!feed.active) {
        try res.startEventStream(feed, Feed.handle);
        feed.active = true;
    }
}

pub fn stopFeed(feed: *Feed) !void {
    try feed.mutex.lock(feed.io);
    defer feed.mutex.unlock(feed.io);

    feed.active = false;
}
```
<p>In this test, we are just verifying we get back &ldquo;a message&rdquo; which is just a
placeholder. The intent is to prove the mechanism. The next step is to write an
actual event and verify it.</p>
<h3 id="gotchas">Gotchas</h3>
<p>There are many gotchas in this solution, particularly with ordering.</p>
<p>The server has to be closed before we join the thread, otherwise, it&rsquo;ll just
block.</p>
<p>We have to read all the content from the stream in one go with
<code>appendRemaining</code>, otherwise, it&rsquo;ll block. We could read in parts if we know
there is a minimum of that much content left in the stream — but that&rsquo;s tricky
at best.</p>
<p>We should wait until the Feed is <code>active</code> before de-activating it, or you&rsquo;ll end
up with race conditions.</p>
<p>You must drain the stream fully, otherwise you&rsquo;ll get errors.</p>
<h2 id="why-not-just-mock-it">Why Not Just Mock It?</h2>
<p>I could mock it, and I will add some mocked tests later. I wanted some
confidence of the end to end journey working well without having to run it and
test with curl manually.</p>
<p>Once I replace the placeholder with actual events, I&rsquo;ll not feel compelled to
test it manually each time to ensure it still works.</p>
]]></content:encoded></item></channel></rss>