milestone 15: make a green run mean a real pass
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
//! and a log line emitted from the sink would recurse.
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const model = @import("../config/model.zig");
|
||||
|
||||
/// Two upstream failures with the same key inside this window produce one line.
|
||||
@@ -585,9 +586,25 @@ fn rotateStepsLocked() RotateError!void {
|
||||
try renameLocked(dir, p, first);
|
||||
}
|
||||
|
||||
const RotateFault = enum { none, fail_delete, fail_rename };
|
||||
|
||||
/// The two rotation steps fail only when the filesystem does, which no unit
|
||||
/// test can arrange on demand, so the failure paths are driven through this
|
||||
/// seam instead. The storage exists in a test build only, and
|
||||
/// `rotateFaultTripped` reduces to `false` everywhere else.
|
||||
const rotate_fault_seam = if (builtin.is_test) struct {
|
||||
var fault: RotateFault = .none;
|
||||
} else struct {};
|
||||
|
||||
fn rotateFaultTripped(comptime which: RotateFault) bool {
|
||||
if (!builtin.is_test) return false;
|
||||
return rotate_fault_seam.fault == which;
|
||||
}
|
||||
|
||||
/// A generation that does not exist yet is not a failure: the first rotations
|
||||
/// of a fresh log directory find nothing to delete.
|
||||
fn deleteLocked(dir: std.Io.Dir, p: []const u8) RotateError!void {
|
||||
if (rotateFaultTripped(.fail_delete)) return error.RotateFailed;
|
||||
dir.deleteFile(state.io, p) catch |err| switch (err) {
|
||||
error.FileNotFound => {},
|
||||
else => return error.RotateFailed,
|
||||
@@ -595,6 +612,7 @@ fn deleteLocked(dir: std.Io.Dir, p: []const u8) RotateError!void {
|
||||
}
|
||||
|
||||
fn renameLocked(dir: std.Io.Dir, from: []const u8, to: []const u8) RotateError!void {
|
||||
if (rotateFaultTripped(.fail_rename)) return error.RotateFailed;
|
||||
dir.rename(from, dir, to, state.io) catch |err| switch (err) {
|
||||
error.FileNotFound => {},
|
||||
else => return error.RotateFailed,
|
||||
@@ -605,10 +623,9 @@ fn renameLocked(dir: std.Io.Dir, from: []const u8, to: []const u8) RotateError!v
|
||||
// Tests
|
||||
//
|
||||
// The pure pieces only. `logFn` is never exercised: installing a sink under the
|
||||
// test runner would eat the harness's own output. File behaviour is S8's, and
|
||||
// the rotation failure paths (`rotateLocked` returning false, the pending
|
||||
// rotation that keeps the oversized file closed) stay uncovered: they need a
|
||||
// filesystem that fails a delete or a rename on demand.
|
||||
// test runner would eat the harness's own output. File behaviour is S8's. The
|
||||
// rotation failure paths need a filesystem that fails a delete or a rename on
|
||||
// demand, which `rotate_fault_seam` supplies.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const testing = std.testing;
|
||||
@@ -908,6 +925,72 @@ test "a failed open counts exactly one sink error" {
|
||||
try testing.expectEqual(@as(u64, 0), state.stats.lines_written);
|
||||
}
|
||||
|
||||
/// The shared body of the two rotation-failure tests: they differ only in which
|
||||
/// step is made to fail and in how many `max_files` it takes to reach it.
|
||||
///
|
||||
/// `state.io` is normally installed by `install`, which no test calls, so a real
|
||||
/// one is put in place for the duration: `rotateLocked` swaps cancel protection
|
||||
/// on it before the first step runs, and the `fail_rename` case deletes the
|
||||
/// oldest generation for real before it reaches the rename. That delete is why
|
||||
/// the path points into a fresh tmp directory rather than at any real log.
|
||||
fn expectRotationFailureCounted(fault: RotateFault, max_files: u8) !void {
|
||||
var threaded: std.Io.Threaded = .init(testing.allocator, .{});
|
||||
defer threaded.deinit();
|
||||
|
||||
var tmp = testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
var path_buf: [128]u8 = undefined;
|
||||
const p = try std.fmt.bufPrint(&path_buf, ".zig-cache/tmp/{s}/nxdns.log", .{tmp.sub_path});
|
||||
|
||||
var stderr_buf: [64]u8 = undefined;
|
||||
_ = std.debug.lockStderr(&stderr_buf);
|
||||
const saved_stats = state.stats;
|
||||
const saved_path_len = state.path_len;
|
||||
const saved_max_files = state.max_files;
|
||||
const saved_file = state.file;
|
||||
const saved_pending = state.rotate_pending;
|
||||
const saved_io = state.io;
|
||||
defer {
|
||||
rotate_fault_seam.fault = .none;
|
||||
state.stats = saved_stats;
|
||||
state.path_len = saved_path_len;
|
||||
state.max_files = saved_max_files;
|
||||
state.file = saved_file;
|
||||
state.rotate_pending = saved_pending;
|
||||
state.io = saved_io;
|
||||
std.debug.unlockStderr();
|
||||
}
|
||||
|
||||
state.stats = .{};
|
||||
state.io = threaded.io();
|
||||
state.path_len = p.len;
|
||||
@memcpy(state.path_buf[0..p.len], p);
|
||||
state.max_files = max_files;
|
||||
state.file = null;
|
||||
state.rotate_pending = true;
|
||||
rotate_fault_seam.fault = fault;
|
||||
|
||||
try testing.expect(!prepareFileLocked(64));
|
||||
try testing.expectEqual(@as(u64, 1), state.stats.sink_errors);
|
||||
try testing.expectEqual(@as(u64, 0), state.stats.rotations);
|
||||
// The oversized file stays closed and the rotation stays owed, so the next
|
||||
// line retries the rotation instead of appending past `max_bytes`.
|
||||
try testing.expect(state.rotate_pending);
|
||||
try testing.expectEqual(@as(?std.Io.File, null), state.file);
|
||||
}
|
||||
|
||||
test "a failed rotation delete counts exactly one sink error" {
|
||||
// `max_files` below 2 keeps no generations, so the whole rotation is the one
|
||||
// delete of the live path.
|
||||
try expectRotationFailureCounted(.fail_delete, 1);
|
||||
}
|
||||
|
||||
test "a failed rotation rename counts exactly one sink error" {
|
||||
// `max_files` of 2 keeps generation 1, so the steps are one delete of the
|
||||
// oldest generation followed by the rename of the live path onto it.
|
||||
try expectRotationFailureCounted(.fail_rename, 2);
|
||||
}
|
||||
|
||||
test "rotatedName appends the generation" {
|
||||
var buf: [max_rotated_path_bytes]u8 = undefined;
|
||||
try testing.expectEqualStrings(
|
||||
|
||||
Reference in New Issue
Block a user