Native PostgreSQL driver / client for Zig
1// This is for the Zig 0.16.
2
3// See https://gist.github.com/karlseguin/c6bea5b35e4e8d26af6f81c22cb5d76b/eb15512d6ae49663fa9df6c7a9725b20dab43edd
4// for a version that workson Zig 0.15.2.
5
6// See https://gist.github.com/karlseguin/c6bea5b35e4e8d26af6f81c22cb5d76b/1f317ebc9cd09bc50fd5591d09c34255e15d1d85
7// for a version that workson Zig 0.14.1.
8
9// in your build.zig, you can specify a custom test runner:
10// const tests = b.addTest(.{
11// .root_module = $MODULE_BEING_TESTED,
12// .test_runner = .{ .path = b.path("test_runner.zig"), .mode = .simple },
13// });
14
15// in your build.zig, you can specify a custom test runner:
16// const tests = b.addTest(.{
17// .root_module = $MODULE_BEING_TESTED,
18// .test_runner = .{ .path = b.path("test_runner.zig"), .mode = .simple },
19// });
20
21const std = @import("std");
22const Io = std.Io;
23const builtin = @import("builtin");
24
25const Allocator = std.mem.Allocator;
26
27const BORDER = "=" ** 80;
28
29// use in custom panic handler
30var current_test: ?[]const u8 = null;
31
32pub fn main(init: std.process.Init) !void {
33 var mem: [8192]u8 = undefined;
34 var fba = std.heap.FixedBufferAllocator.init(&mem);
35
36 const allocator = fba.allocator();
37
38 const env = Env.init(init.environ_map);
39
40 std.testing.io_instance = .init(init.gpa, .{
41 .argv0 = .init(init.minimal.args),
42 .environ = init.minimal.environ,
43 });
44 defer std.testing.io_instance.deinit();
45
46 const io = std.testing.io;
47
48 var slowest = SlowTracker.init(io, allocator, 5);
49 defer slowest.deinit();
50
51 var pass: usize = 0;
52 var fail: usize = 0;
53 var skip: usize = 0;
54 var leak: usize = 0;
55
56 Printer.fmt("\r\x1b[0K", .{}); // beginning of line and clear to end of line
57
58 for (builtin.test_functions) |t| {
59 if (isSetup(t)) {
60 t.func() catch |err| {
61 Printer.status(.fail, "\nsetup \"{s}\" failed: {}\n", .{ t.name, err });
62 return err;
63 };
64 }
65 }
66
67 for (builtin.test_functions) |t| {
68 if (isSetup(t) or isTeardown(t)) {
69 continue;
70 }
71
72 var status = Status.pass;
73 slowest.startTiming(io);
74
75 const is_unnamed_test = isUnnamed(t);
76 if (env.filter) |f| {
77 if (!is_unnamed_test and std.mem.indexOf(u8, t.name, f) == null) {
78 continue;
79 }
80 }
81
82 const friendly_name = blk: {
83 const name = t.name;
84 var it = std.mem.splitScalar(u8, name, '.');
85 while (it.next()) |value| {
86 if (std.mem.eql(u8, value, "test")) {
87 const rest = it.rest();
88 break :blk if (rest.len > 0) rest else name;
89 }
90 }
91 break :blk name;
92 };
93
94 current_test = friendly_name;
95 std.testing.allocator_instance = .{};
96 const result = t.func();
97 current_test = null;
98
99 const ns_taken = slowest.endTiming(io, friendly_name);
100
101 if (std.testing.allocator_instance.deinit() == .leak) {
102 leak += 1;
103 Printer.status(.fail, "\n{s}\n\"{s}\" - Memory Leak\n{s}\n", .{ BORDER, friendly_name, BORDER });
104 }
105
106 if (result) |_| {
107 pass += 1;
108 } else |err| switch (err) {
109 error.SkipZigTest => {
110 skip += 1;
111 status = .skip;
112 },
113 else => {
114 status = .fail;
115 fail += 1;
116 Printer.status(.fail, "\n{s}\n\"{s}\" - {s}\n{s}\n", .{ BORDER, friendly_name, @errorName(err), BORDER });
117 if (@errorReturnTrace()) |trace| {
118 std.debug.dumpErrorReturnTrace(trace);
119 }
120 if (env.fail_first) {
121 break;
122 }
123 },
124 }
125
126 if (env.verbose) {
127 const ms = @as(f64, @floatFromInt(ns_taken)) / 1_000_000.0;
128 Printer.status(status, "{s} ({d:.2}ms)\n", .{ friendly_name, ms });
129 } else {
130 Printer.status(status, ".", .{});
131 }
132 }
133
134 for (builtin.test_functions) |t| {
135 if (isTeardown(t)) {
136 t.func() catch |err| {
137 Printer.status(.fail, "\nteardown \"{s}\" failed: {}\n", .{ t.name, err });
138 return err;
139 };
140 }
141 }
142
143 const total_tests = pass + fail;
144 const status = if (fail == 0) Status.pass else Status.fail;
145 Printer.status(status, "\n{d} of {d} test{s} passed\n", .{ pass, total_tests, if (total_tests != 1) "s" else "" });
146 if (skip > 0) {
147 Printer.status(.skip, "{d} test{s} skipped\n", .{ skip, if (skip != 1) "s" else "" });
148 }
149 if (leak > 0) {
150 Printer.status(.fail, "{d} test{s} leaked\n", .{ leak, if (leak != 1) "s" else "" });
151 }
152 Printer.fmt("\n", .{});
153 try slowest.display();
154 Printer.fmt("\n", .{});
155 std.process.exit(if (fail == 0) 0 else 1);
156}
157
158const Printer = struct {
159 fn fmt(comptime format: []const u8, args: anytype) void {
160 std.debug.print(format, args);
161 }
162
163 fn status(s: Status, comptime format: []const u8, args: anytype) void {
164 switch (s) {
165 .pass => std.debug.print("\x1b[32m", .{}),
166 .fail => std.debug.print("\x1b[31m", .{}),
167 .skip => std.debug.print("\x1b[33m", .{}),
168 else => {},
169 }
170 std.debug.print(format ++ "\x1b[0m", args);
171 }
172};
173
174const Status = enum {
175 pass,
176 fail,
177 skip,
178 text,
179};
180
181const SlowTracker = struct {
182 max: usize,
183 slowest: SlowestQueue,
184 start: Io.Timestamp,
185 allocator: Allocator,
186
187 const SlowestQueue = std.PriorityDequeue(TestInfo, void, compareTiming);
188
189 fn init(io: Io, allocator: Allocator, count: u32) SlowTracker {
190 const timestamp = Io.Clock.awake.now(io);
191 var slowest: SlowestQueue = .empty;
192 slowest.ensureTotalCapacity(allocator, count) catch @panic("OOM");
193 return .{
194 .max = count,
195 .start = timestamp,
196 .slowest = slowest,
197 .allocator = allocator,
198 };
199 }
200
201 const TestInfo = struct {
202 ns: u64,
203 name: []const u8,
204 };
205
206 fn deinit(self: *SlowTracker) void {
207 self.slowest.deinit(self.allocator);
208 }
209
210 fn startTiming(self: *SlowTracker, io: Io) void {
211 self.start = Io.Clock.awake.now(io);
212 }
213
214 fn endTiming(self: *SlowTracker, io: Io, test_name: []const u8) u64 {
215 const timestamp = Io.Clock.awake.now(io);
216 const start = self.start;
217 self.start = timestamp;
218 const ns: u64 = @intCast(start.durationTo(timestamp).toNanoseconds());
219
220 var slowest = &self.slowest;
221
222 if (slowest.count() < self.max) {
223 // Capacity is fixed to the # of slow tests we want to track
224 // If we've tracked fewer tests than this capacity, than always add
225 slowest.push(self.allocator, TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing");
226 return ns;
227 }
228
229 {
230 // Optimization to avoid shifting the dequeue for the common case
231 // where the test isn't one of our slowest.
232 const fastest_of_the_slow = slowest.peekMin() orelse unreachable;
233 if (fastest_of_the_slow.ns > ns) {
234 // the test was faster than our fastest slow test, don't add
235 return ns;
236 }
237 }
238
239 // the previous fastest of our slow tests, has been pushed off.
240 _ = slowest.popMin();
241 slowest.push(self.allocator, TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing");
242 return ns;
243 }
244
245 fn display(self: *SlowTracker) !void {
246 var slowest = self.slowest;
247 const count = slowest.count();
248 Printer.fmt("Slowest {d} test{s}: \n", .{ count, if (count != 1) "s" else "" });
249 while (slowest.popMin()) |info| {
250 const ms = @as(f64, @floatFromInt(info.ns)) / 1_000_000.0;
251 Printer.fmt(" {d:.2}ms\t{s}\n", .{ ms, info.name });
252 }
253 }
254
255 fn compareTiming(context: void, a: TestInfo, b: TestInfo) std.math.Order {
256 _ = context;
257 return std.math.order(a.ns, b.ns);
258 }
259};
260
261const Env = struct {
262 verbose: bool,
263 fail_first: bool,
264 filter: ?[]const u8,
265
266 fn init(map: *const std.process.Environ.Map) Env {
267 return .{
268 .verbose = readEnvBool(map, "TEST_VERBOSE", true),
269 .fail_first = readEnvBool(map, "TEST_FAIL_FIRST", false),
270 .filter = readEnv(map, "TEST_FILTER"),
271 };
272 }
273
274 fn readEnv(map: *const std.process.Environ.Map, key: []const u8) ?[]const u8 {
275 return map.get(key);
276 }
277
278 fn readEnvBool(map: *const std.process.Environ.Map, key: []const u8, deflt: bool) bool {
279 const value = readEnv(map, key) orelse return deflt;
280 return std.ascii.eqlIgnoreCase(value, "true");
281 }
282};
283
284pub const panic = std.debug.FullPanic(struct {
285 pub fn panicFn(msg: []const u8, first_trace_addr: ?usize) noreturn {
286 if (current_test) |ct| {
287 std.debug.print("\x1b[31m{s}\npanic running \"{s}\"\n{s}\x1b[0m\n", .{ BORDER, ct, BORDER });
288 }
289 std.debug.defaultPanic(msg, first_trace_addr);
290 }
291}.panicFn);
292
293fn isUnnamed(t: std.builtin.TestFn) bool {
294 const marker = ".test_";
295 const test_name = t.name;
296 const index = std.mem.indexOf(u8, test_name, marker) orelse return false;
297 _ = std.fmt.parseInt(u32, test_name[index + marker.len ..], 10) catch return false;
298 return true;
299}
300
301fn isSetup(t: std.builtin.TestFn) bool {
302 return std.mem.endsWith(u8, t.name, "tests:beforeAll");
303}
304
305fn isTeardown(t: std.builtin.TestFn) bool {
306 return std.mem.endsWith(u8, t.name, "tests:afterAll");
307}