a73x

src/testtmp.zig

Ref:   Size: 4.5 KiB   History

//! Temp directories with SHORT paths, for tests that bind unix sockets.
//!
//! `std.testing.tmpDir` puts its directory under `.zig-cache/tmp`, so the path
//! is as long as wherever the repository is checked out — and a unix socket
//! caps at 108 bytes, so a deep checkout turns every socket-binding test into a
//! `NameTooLong` that surfaces from inside `std.net` and reads like a bug in
//! the code under test.
//!
//! Same shape as `std.testing.tmpDir`, plus `path()`. A directory here is ~21
//! characters wherever the repository lives.
const std = @import("std");

pub const TmpDir = struct {
    dir: std.fs.Dir,
    buf: [32]u8 = undefined,
    len: usize = 0,

    pub fn make() !TmpDir {
        var self: TmpDir = .{ .dir = undefined };
        while (true) {
            const p = try std.fmt.bufPrint(
                &self.buf,
                "/tmp/mux-t{x:0>12}",
                .{std.crypto.random.int(u48)},
            );
            self.len = p.len;
            std.fs.cwd().makeDir(p) catch |err| switch (err) {
                // Two tests in the same millisecond is ordinary; two that
                // drew the same 48 bits is not, but it costs one retry.
                error.PathAlreadyExists => continue,
                else => return err,
            };
            self.dir = try std.fs.cwd().openDir(p, .{ .iterate = true });
            return self;
        }
    }

    pub fn path(self: *const TmpDir) []const u8 {
        return self.buf[0..self.len];
    }

    /// Idempotent, because the natural way to use this is a `defer` plus an
    /// occasional early cleanup, and closing an already-closed directory
    /// aborts rather than complaining.
    pub fn cleanup(self: *TmpDir) void {
        if (self.len == 0) return;
        const p = self.path();
        self.dir.close();
        // Said out loud rather than swallowed: a `catch {}` makes a directory
        // that could not be removed look exactly like one that was. Non-fatal —
        // a passing test must not be failed by its own tidying — but not silent.
        std.fs.cwd().deleteTree(p) catch |err| {
            std.debug.print("testtmp: could not remove {s}: {t}\n", .{ p, err });
        };
        self.len = 0;
    }
};

/// A pid nothing holds: `true`, spawned and waited for. What the reapers ask
/// the OS about, made real rather than guessed from pid_max.
///
/// Found on PATH rather than spelled absolutely, because the two systems put
/// it in different places: `/bin/true` on Linux, `/usr/bin/true` on macOS.
/// The absolute Linux spelling failed to spawn on the Mac, and every reaper
/// test that asks for a dead pid failed with it.
pub fn deadPid() !std.posix.pid_t {
    var child = std.process.Child.init(&.{"true"}, std.testing.allocator);
    try child.spawn();
    // Read before the wait: `wait` sets `id` to undefined once the child
    // is reaped, and an undefined pid parsed out of a name is no pid.
    const pid = child.id;
    _ = try child.wait();
    return pid;
}

test "TmpDir: a path short enough to bind a socket in" {
    var tmp = try TmpDir.make();
    defer tmp.cleanup();

    // The number that matters. sun_path is 108 bytes including the
    // terminator, and this leaves room for a filename inside it.
    try std.testing.expect(tmp.path().len < 32);
    try std.testing.expect(std.mem.startsWith(u8, tmp.path(), "/tmp/"));

    // It really is a directory, and it really is writable.
    try tmp.dir.writeFile(.{ .sub_path = "probe", .data = "x" });

    // ...and a socket binds in it, which is the whole point. Built the way
    // the tests build theirs, so the length being tested is the real one.
    var buf: [128]u8 = undefined;
    const sock_path = try std.fmt.bufPrint(&buf, "{s}/probe.sock", .{tmp.path()});
    const addr = try std.net.Address.initUnix(sock_path);
    var server = try addr.listen(.{});
    server.deinit();

    const saved = try std.fmt.allocPrint(std.testing.allocator, "{s}", .{tmp.path()});
    defer std.testing.allocator.free(saved);
    tmp.cleanup();
    try std.testing.expectError(error.FileNotFound, std.fs.cwd().access(saved, .{}));
    // The `defer` above will call cleanup a second time. That has to be
    // harmless, or every test using this would have to choose between an
    // early cleanup and a defer — and closing a closed directory aborts.
}

// Forces semantic analysis of every pub decl under `zig build test`, so an
// unreferenced decl must at least compile (the silent-module-loss hazard,
// decisions.md). Pub decls only: std.meta.declarations sees nothing private.
test {
    std.testing.refAllDeclsRecursive(@This());
}