jetzig/src/test_runner.zig
Bob Farrell d27907a1c5 WIP
2024-11-09 17:13:32 +00:00

281 lines
9.4 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const jetzig = @import("jetzig");
pub const static = @import("static");
pub const std_options = std.Options{
.logFn = log,
};
pub const jetzig_options = @import("main").jetzig_options;
pub fn log(
comptime message_level: std.log.Level,
comptime scope: @Type(.enum_literal),
comptime format: []const u8,
args: anytype,
) void {
jetzig.testing.logger.log(message_level, scope, format, args);
}
const Test = struct {
name: []const u8,
function: TestFn,
module: ?[]const u8 = null,
leaked: bool = false,
result: Result = .success,
stack_trace_buf: [8192]u8 = undefined,
duration: usize = 0,
pub const TestFn = *const fn () anyerror!void;
pub const Result = union(enum) {
success: void,
failure: Failure,
skipped: void,
};
const Failure = struct {
err: anyerror,
trace: ?[]const u8,
};
const name_template = jetzig.colors.blue("{s}") ++ jetzig.colors.yellow("::") ++ "\"" ++ jetzig.colors.cyan("{s}") ++ "\" ";
pub fn init(test_fn: std.builtin.TestFn) Test {
return if (std.mem.indexOf(u8, test_fn.name, ".test.")) |index|
.{
.function = test_fn.func,
.module = test_fn.name[0..index],
.name = test_fn.name[index + ".test.".len ..],
}
else
.{ .function = test_fn.func, .name = test_fn.name };
}
pub fn run(self: *Test, allocator: std.mem.Allocator) !void {
std.testing.allocator_instance = .{};
const start = std.time.nanoTimestamp();
self.function() catch |err| {
switch (err) {
error.SkipZigTest => self.result = .skipped,
else => self.result = .{ .failure = .{
.err = err,
.trace = if (try self.formatStackTrace(@errorReturnTrace())) |trace|
try allocator.dupe(u8, trace)
else
null,
} },
}
};
self.duration = @intCast(std.time.nanoTimestamp() - start);
if (std.testing.allocator_instance.deinit() == .leak) self.leaked = true;
}
fn formatStackTrace(self: *Test, maybe_trace: ?*std.builtin.StackTrace) !?[]const u8 {
return if (maybe_trace) |trace| blk: {
var stream = std.io.fixedBufferStream(&self.stack_trace_buf);
const writer = stream.writer();
try trace.format("", .{}, writer);
break :blk stream.getWritten();
} else null;
}
pub fn print(self: Test, stream: anytype) !void {
const writer = stream.writer();
switch (self.result) {
.success => {
try self.printPassed(writer);
if (self.leaked) try self.printLeaked(writer);
try self.printDuration(writer);
},
.failure => |failure| {
try self.printFailure(failure, writer);
if (self.leaked) try self.printLeaked(writer);
},
.skipped => try self.printSkipped(writer),
}
try writer.writeByte('\n');
}
fn printPassed(self: Test, writer: anytype) !void {
try writer.print(
jetzig.colors.green("[PASS] ") ++ name_template,
.{ self.module orelse "tests", self.name },
);
}
fn printFailure(self: Test, failure: Failure, writer: anytype) !void {
try writer.print(
jetzig.colors.red("[FAIL] ") ++ name_template ++ jetzig.colors.yellow("({s})"),
.{ self.module orelse "tests", self.name, @errorName(failure.err) },
);
}
fn printFailureDetail(self: Test, index: usize, failure: Failure, writer: anytype) !void {
try writer.print("\n", .{});
const count = " FAILURE: ".len + (self.module orelse "tests").len + ":".len + self.name.len + 4;
try writer.writeAll(jetzig.colors.red(""));
for (0..count) |_| try writer.writeAll(jetzig.colors.red(""));
try writer.writeAll(jetzig.colors.red(""));
try writer.print(
jetzig.colors.red("\n│ FAILURE: ") ++ name_template ++ jetzig.colors.red("") ++ "\n",
.{ self.module orelse "tests", self.name },
);
try writer.writeAll(jetzig.colors.red(""));
for (0..count) |_| try writer.writeAll(jetzig.colors.red(""));
try writer.writeAll(jetzig.colors.red(""));
try writer.writeByte('\n');
const maybe_log_events = jetzig.testing.logger.logs.get(index);
if (maybe_log_events) |log_events| {
for (log_events.items) |log_event| try indent(log_event.output, jetzig.colors.red(""), writer);
}
if (failure.trace) |trace| {
try writer.writeAll(jetzig.colors.red("\n"));
try indent(trace, jetzig.colors.red(""), writer);
}
}
fn printSkipped(self: Test, writer: anytype) !void {
try writer.print(
"[" ++ jetzig.colors.yellow("SKIP") ++ "]" ++ name_template,
.{ self.module orelse "tests", self.name },
);
}
fn printLeaked(self: Test, writer: anytype) !void {
_ = self;
try writer.print("[" ++ jetzig.colors.red("LEAKED") ++ "] ", .{});
}
fn printDuration(self: Test, writer: anytype) !void {
var buf: [256]u8 = undefined;
try writer.print(
"[" ++ jetzig.colors.cyan("{s}") ++ "]",
.{try jetzig.colors.duration(&buf, @intCast(self.duration), true)},
);
}
};
pub fn main() !void {
const start = std.time.nanoTimestamp();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var tests = std.ArrayList(Test).init(allocator);
defer tests.deinit();
var mime_map = jetzig.http.mime.MimeMap.init(allocator);
try mime_map.build();
jetzig.testing.mime_map = &mime_map;
try std.io.getStdErr().writer().writeAll("\n[jetzig] Launching Test Runner...\n\n");
jetzig.testing.logger = jetzig.testing.Logger.init(allocator);
jetzig.testing.state = .ready;
for (builtin.test_functions, 0..) |test_function, index| {
jetzig.testing.logger.index = index;
var t = Test.init(test_function);
try t.run(allocator);
try t.print(std.io.getStdErr());
try tests.append(t);
}
try printSummary(tests.items, start);
}
fn printSummary(tests: []const Test, start: i128) !void {
var success: usize = 0;
var failure: usize = 0;
var leaked: usize = 0;
var skipped: usize = 0;
for (tests) |t| {
switch (t.result) {
.success => success += 1,
.failure => failure += 1,
.skipped => skipped += 1,
}
if (t.leaked) leaked += 1;
}
const tick = jetzig.colors.green("");
const cross = jetzig.colors.red("");
const writer = std.io.getStdErr().writer();
var total_duration_buf: [256]u8 = undefined;
const total_duration = try jetzig.colors.duration(
&total_duration_buf,
@intCast(std.time.nanoTimestamp() - start),
false,
);
for (tests, 0..) |t, index| {
switch (t.result) {
.success, .skipped => {},
.failure => |capture| try t.printFailureDetail(index, capture, writer),
}
}
try writer.print(
"\n {s}{s}{}" ++
"\n {s}{s}{}" ++
"\n {s}{}" ++
"\n " ++ jetzig.colors.cyan(" tests ") ++ "{}" ++
"\n " ++ jetzig.colors.cyan(" duration ") ++ "{s}" ++ "\n\n",
.{
if (failure == 0) tick else cross,
if (failure == 0) jetzig.colors.blue(" failed ") else jetzig.colors.red(" failed "),
failure,
if (leaked == 0) tick else cross,
if (leaked == 0) jetzig.colors.blue(" leaked ") else jetzig.colors.red(" leaked "),
leaked,
if (skipped == 0) jetzig.colors.blue(" skipped ") else jetzig.colors.yellow(" skipped "),
skipped,
success + failure,
total_duration,
},
);
if (failure == 0 and leaked == 0) {
try writer.print(jetzig.colors.green(" PASS ") ++ "\n", .{});
try writer.print(jetzig.colors.green(" ▔▔▔▔") ++ "\n", .{});
std.process.exit(0);
} else {
try writer.print(jetzig.colors.red(" FAIL ") ++ "\n", .{});
try writer.print(jetzig.colors.red(" ▔▔▔▔") ++ "\n", .{});
try writer.print("Server logs: " ++ jetzig.colors.cyan("log/test.log") ++ "\n\n", .{});
std.process.exit(1);
}
}
fn indent(message: []const u8, comptime indent_sequence: []const u8, writer: anytype) !void {
var it = std.mem.tokenizeScalar(u8, message, '\n');
var color: ?[]const u8 = null;
const escape = jetzig.colors.codes.escape;
while (it.next()) |line| {
try writer.print(indent_sequence ++ "{s}{s}\n", .{ color orelse "", line });
// Preserve last color used in previous line (including reset) in case indent changes color.
if (std.mem.lastIndexOf(u8, line, escape)) |index| {
inline for (std.meta.fields(@TypeOf(jetzig.colors.codes))) |field| {
const code = @field(jetzig.colors.codes, field.name);
if (std.mem.startsWith(u8, line[index..], escape ++ code)) {
color = escape ++ code;
}
}
}
}
}