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());
}