Merge pull request #409 from karlseguin/unittest_build

Add a new unittest build step
This commit is contained in:
Pierre Tachoire
2025-02-10 08:45:24 +01:00
committed by GitHub
9 changed files with 441 additions and 57 deletions

View File

@@ -3,6 +3,8 @@
ZIG := zig ZIG := zig
BC := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) BC := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
# option test filter make unittest F="server"
F=
# OS and ARCH # OS and ARCH
kernel = $(shell uname -ms) kernel = $(shell uname -ms)
@@ -42,7 +44,7 @@ help:
# $(ZIG) commands # $(ZIG) commands
# ------------ # ------------
.PHONY: build build-dev run run-release shell test bench download-zig wpt .PHONY: build build-dev run run-release shell test bench download-zig wpt unittest
zig_version = $(shell grep 'recommended_zig_version = "' "vendor/zig-js-runtime/build.zig" | cut -d'"' -f2) zig_version = $(shell grep 'recommended_zig_version = "' "vendor/zig-js-runtime/build.zig" | cut -d'"' -f2)
@@ -91,6 +93,9 @@ test:
@$(ZIG) build test -Dengine=v8 || (printf "\e[33mTest ERROR\e[0m\n"; exit 1;) @$(ZIG) build test -Dengine=v8 || (printf "\e[33mTest ERROR\e[0m\n"; exit 1;)
@printf "\e[33mTest OK\e[0m\n" @printf "\e[33mTest OK\e[0m\n"
unittest:
@TEST_FILTER='${F}' $(ZIG) build unittest -freference-trace --summary all
# Install and build required dependencies commands # Install and build required dependencies commands
# ------------ # ------------
.PHONY: install-submodule .PHONY: install-submodule

View File

@@ -98,8 +98,8 @@ pub fn build(b: *std.Build) !void {
// compile // compile
const tests = b.addTest(.{ const tests = b.addTest(.{
.root_source_file = b.path("src/run_tests.zig"), .root_source_file = b.path("src/main_tests.zig"),
.test_runner = b.path("src/test_runner.zig"), .test_runner = b.path("src/main_tests.zig"),
.target = target, .target = target,
.optimize = mode, .optimize = mode,
}); });
@@ -119,6 +119,27 @@ pub fn build(b: *std.Build) !void {
const test_step = b.step("test", "Run unit tests"); const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_tests.step); test_step.dependOn(&run_tests.step);
// unittest
// ----
// compile
const unit_tests = b.addTest(.{
.root_source_file = b.path("src/unit_tests.zig"),
.test_runner = b.path("src/unit_tests.zig"),
.target = target,
.optimize = mode,
});
try common(b, unit_tests, options);
const run_unit_tests = b.addRunArtifact(unit_tests);
if (b.args) |args| {
run_unit_tests.addArgs(args);
}
// step
const unit_test_step = b.step("unittest", "Run unit tests");
unit_test_step.dependOn(&run_unit_tests.step);
// wpt // wpt
// ----- // -----

View File

@@ -79,13 +79,19 @@ pub const Loader = struct {
} }
}; };
test "basic url get" { test "loader: get" {
const alloc = std.testing.allocator; const alloc = std.testing.allocator;
var loader = Loader.init(alloc); var loader = Loader.init(alloc);
defer loader.deinit(); defer loader.deinit();
var result = try loader.get(alloc, "https://en.wikipedia.org/wiki/Main_Page"); const uri = try std.Uri.parse("http://localhost:9582/loader");
var result = try loader.get(alloc, uri);
defer result.deinit(); defer result.deinit();
try std.testing.expect(result.req.response.status == std.http.Status.ok); try std.testing.expectEqual(.ok, result.req.response.status);
var res: [128]u8 = undefined;
const size = try result.req.readAll(&res);
try std.testing.expectEqual(6, size);
try std.testing.expectEqualStrings("Hello!", res[0..6]);
} }

View File

@@ -15,19 +15,39 @@ pub const U32Iterator = struct {
done: bool, done: bool,
}; };
pub fn _next(self: *U32Iterator) !Return { pub fn _next(self: *U32Iterator) Return {
const i = self.index; const i = self.index;
if (i >= self.length) { if (i >= self.length) {
return Return{ return .{
.value = 0, .value = 0,
.done = true, .done = true,
}; };
} }
self.index += 1; self.index = i + 1;
return Return{ return .{
.value = i, .value = i,
.done = false, .done = false,
}; };
} }
}; };
const testing = std.testing;
test "U32Iterator" {
const Return = U32Iterator.Return;
{
var it = U32Iterator{ .length = 0 };
try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
}
{
var it = U32Iterator{ .length = 3 };
try testing.expectEqual(Return{ .value = 0, .done = false }, it._next());
try testing.expectEqual(Return{ .value = 1, .done = false }, it._next());
try testing.expectEqual(Return{ .value = 2, .done = false }, it._next());
try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
}
}

View File

@@ -223,8 +223,14 @@ pub fn main() !void {
try parser.init(); try parser.init();
defer parser.deinit(); defer parser.deinit();
std.testing.allocator_instance = .{};
try test_fn.func(); try test_fn.func();
std.debug.print("{s}\tOK\n", .{test_fn.name});
if (std.testing.allocator_instance.deinit() == .leak) {
std.debug.print("======Memory Leak: {s}======\n", .{test_fn.name});
} else {
std.debug.print("{s}\tOK\n", .{test_fn.name});
}
} }
} }
} }

View File

@@ -149,20 +149,22 @@ pub const Bottle = struct {
} }
pub fn _setItem(self: *Bottle, k: []const u8, v: []const u8) !void { pub fn _setItem(self: *Bottle, k: []const u8, v: []const u8) !void {
const old = self.map.get(k); const gop = self.map.getOrPut(self.alloc, k) catch |e| {
if (old != null and std.mem.eql(u8, v, old.?)) return;
// owns k and v by copying them.
const kk = try self.alloc.dupe(u8, k);
errdefer self.alloc.free(kk);
const vv = try self.alloc.dupe(u8, v);
errdefer self.alloc.free(vv);
self.map.put(self.alloc, kk, vv) catch |e| {
log.debug("set item: {any}", .{e}); log.debug("set item: {any}", .{e});
return DOMError.QuotaExceeded; return DOMError.QuotaExceeded;
}; };
if (gop.found_existing == false) {
gop.key_ptr.* = try self.alloc.dupe(u8, k);
gop.value_ptr.* = try self.alloc.dupe(u8, v);
return;
}
if (std.mem.eql(u8, v, gop.value_ptr.*) == false) {
self.alloc.free(gop.value_ptr.*);
gop.value_ptr.* = try self.alloc.dupe(u8, v);
}
// > Broadcast this with key, oldValue, and value. // > Broadcast this with key, oldValue, and value.
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface // https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface
// //
@@ -175,8 +177,10 @@ pub const Bottle = struct {
} }
pub fn _removeItem(self: *Bottle, k: []const u8) !void { pub fn _removeItem(self: *Bottle, k: []const u8) !void {
const old = self.map.fetchRemove(k); if (self.map.fetchRemove(k)) |kv| {
if (old == null) return; self.alloc.free(kv.key);
self.alloc.free(kv.value);
}
// > Broadcast this with key, oldValue, and null. // > Broadcast this with key, oldValue, and null.
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface // https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface
@@ -235,14 +239,17 @@ test "storage bottle" {
var bottle = Bottle.init(std.testing.allocator); var bottle = Bottle.init(std.testing.allocator);
defer bottle.deinit(); defer bottle.deinit();
try std.testing.expect(0 == bottle.get_length()); try std.testing.expectEqual(0, bottle.get_length());
try std.testing.expect(null == bottle._getItem("foo")); try std.testing.expectEqual(null, bottle._getItem("foo"));
try bottle._setItem("foo", "bar"); try bottle._setItem("foo", "bar");
try std.testing.expect(std.mem.eql(u8, "bar", bottle._getItem("foo").?)); try std.testing.expectEqualStrings("bar", bottle._getItem("foo").?);
try bottle._setItem("foo", "other");
try std.testing.expectEqualStrings("other", bottle._getItem("foo").?);
try bottle._removeItem("foo"); try bottle._removeItem("foo");
try std.testing.expect(0 == bottle.get_length()); try std.testing.expectEqual(0, bottle.get_length());
try std.testing.expect(null == bottle._getItem("foo")); try std.testing.expectEqual(null, bottle._getItem("foo"));
} }

View File

@@ -1,29 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const tests = @import("run_tests.zig");
pub const Types = tests.Types;
pub const UserContext = tests.UserContext;
pub const IO = tests.IO;
pub fn main() !void {
try tests.main();
}

347
src/unit_tests.zig Normal file
View File

@@ -0,0 +1,347 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
pub const std_options = std.Options{
.http_disable_tls = true,
};
const BORDER = "=" ** 80;
// use in custom panic handler
var current_test: ?[]const u8 = null;
pub fn main() !void {
var mem: [8192]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&mem);
const allocator = fba.allocator();
const env = Env.init(allocator);
defer env.deinit(allocator);
var slowest = SlowTracker.init(allocator, 5);
defer slowest.deinit();
var pass: usize = 0;
var fail: usize = 0;
var skip: usize = 0;
var leak: usize = 0;
const address = try std.net.Address.parseIp("127.0.0.1", 9582);
var listener = try address.listen(.{ .reuse_address = true });
defer listener.deinit();
const http_thread = try std.Thread.spawn(.{}, serverHTTP, .{&listener});
defer http_thread.join();
const printer = Printer.init();
printer.fmt("\r\x1b[0K", .{}); // beginning of line and clear to end of line
for (builtin.test_functions) |t| {
if (std.mem.eql(u8, t.name, "unit_tests.test_0")) {
// don't display anything for this test
try t.func();
continue;
}
var status = Status.pass;
slowest.startTiming();
const is_unnamed_test = isUnnamed(t);
if (env.filter) |f| {
if (!is_unnamed_test and std.mem.indexOf(u8, t.name, f) == null) {
continue;
}
}
const friendly_name = blk: {
const name = t.name;
var it = std.mem.splitScalar(u8, name, '.');
while (it.next()) |value| {
if (std.mem.eql(u8, value, "test")) {
const rest = it.rest();
break :blk if (rest.len > 0) rest else name;
}
}
break :blk name;
};
current_test = friendly_name;
std.testing.allocator_instance = .{};
const result = t.func();
current_test = null;
const ns_taken = slowest.endTiming(friendly_name);
if (std.testing.allocator_instance.deinit() == .leak) {
leak += 1;
printer.status(.fail, "\n{s}\n\"{s}\" - Memory Leak\n{s}\n", .{ BORDER, friendly_name, BORDER });
}
if (result) |_| {
pass += 1;
} else |err| switch (err) {
error.SkipZigTest => {
skip += 1;
status = .skip;
},
else => {
status = .fail;
fail += 1;
printer.status(.fail, "\n{s}\n\"{s}\" - {s}\n{s}\n", .{ BORDER, friendly_name, @errorName(err), BORDER });
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
if (env.fail_first) {
break;
}
},
}
if (env.verbose) {
const ms = @as(f64, @floatFromInt(ns_taken)) / 1_000_000.0;
printer.status(status, "{s} ({d:.2}ms)\n", .{ friendly_name, ms });
} else {
printer.status(status, ".", .{});
}
}
const total_tests = pass + fail;
const status = if (fail == 0) Status.pass else Status.fail;
printer.status(status, "\n{d} of {d} test{s} passed\n", .{ pass, total_tests, if (total_tests != 1) "s" else "" });
if (skip > 0) {
printer.status(.skip, "{d} test{s} skipped\n", .{ skip, if (skip != 1) "s" else "" });
}
if (leak > 0) {
printer.status(.fail, "{d} test{s} leaked\n", .{ leak, if (leak != 1) "s" else "" });
}
printer.fmt("\n", .{});
try slowest.display(printer);
printer.fmt("\n", .{});
std.posix.exit(if (fail == 0) 0 else 1);
}
const Printer = struct {
out: std.fs.File.Writer,
fn init() Printer {
return .{
.out = std.io.getStdErr().writer(),
};
}
fn fmt(self: Printer, comptime format: []const u8, args: anytype) void {
std.fmt.format(self.out, format, args) catch unreachable;
}
fn status(self: Printer, s: Status, comptime format: []const u8, args: anytype) void {
const color = switch (s) {
.pass => "\x1b[32m",
.fail => "\x1b[31m",
.skip => "\x1b[33m",
else => "",
};
const out = self.out;
out.writeAll(color) catch @panic("writeAll failed?!");
std.fmt.format(out, format, args) catch @panic("std.fmt.format failed?!");
self.fmt("\x1b[0m", .{});
}
};
const Status = enum {
pass,
fail,
skip,
text,
};
const SlowTracker = struct {
const SlowestQueue = std.PriorityDequeue(TestInfo, void, compareTiming);
max: usize,
slowest: SlowestQueue,
timer: std.time.Timer,
fn init(allocator: Allocator, count: u32) SlowTracker {
const timer = std.time.Timer.start() catch @panic("failed to start timer");
var slowest = SlowestQueue.init(allocator, {});
slowest.ensureTotalCapacity(count) catch @panic("OOM");
return .{
.max = count,
.timer = timer,
.slowest = slowest,
};
}
const TestInfo = struct {
ns: u64,
name: []const u8,
};
fn deinit(self: SlowTracker) void {
self.slowest.deinit();
}
fn startTiming(self: *SlowTracker) void {
self.timer.reset();
}
fn endTiming(self: *SlowTracker, test_name: []const u8) u64 {
var timer = self.timer;
const ns = timer.lap();
var slowest = &self.slowest;
if (slowest.count() < self.max) {
// Capacity is fixed to the # of slow tests we want to track
// If we've tracked fewer tests than this capacity, than always add
slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing");
return ns;
}
{
// Optimization to avoid shifting the dequeue for the common case
// where the test isn't one of our slowest.
const fastest_of_the_slow = slowest.peekMin() orelse unreachable;
if (fastest_of_the_slow.ns > ns) {
// the test was faster than our fastest slow test, don't add
return ns;
}
}
// the previous fastest of our slow tests, has been pushed off.
_ = slowest.removeMin();
slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing");
return ns;
}
fn display(self: *SlowTracker, printer: Printer) !void {
var slowest = self.slowest;
const count = slowest.count();
printer.fmt("Slowest {d} test{s}: \n", .{ count, if (count != 1) "s" else "" });
while (slowest.removeMinOrNull()) |info| {
const ms = @as(f64, @floatFromInt(info.ns)) / 1_000_000.0;
printer.fmt(" {d:.2}ms\t{s}\n", .{ ms, info.name });
}
}
fn compareTiming(context: void, a: TestInfo, b: TestInfo) std.math.Order {
_ = context;
return std.math.order(a.ns, b.ns);
}
};
const Env = struct {
verbose: bool,
fail_first: bool,
filter: ?[]const u8,
fn init(allocator: Allocator) Env {
return .{
.verbose = readEnvBool(allocator, "TEST_VERBOSE", true),
.fail_first = readEnvBool(allocator, "TEST_FAIL_FIRST", false),
.filter = readEnv(allocator, "TEST_FILTER"),
};
}
fn deinit(self: Env, allocator: Allocator) void {
if (self.filter) |f| {
allocator.free(f);
}
}
fn readEnv(allocator: Allocator, key: []const u8) ?[]const u8 {
const v = std.process.getEnvVarOwned(allocator, key) catch |err| {
if (err == error.EnvironmentVariableNotFound) {
return null;
}
std.log.warn("failed to get env var {s} due to err {}", .{ key, err });
return null;
};
return v;
}
fn readEnvBool(allocator: Allocator, key: []const u8, deflt: bool) bool {
const value = readEnv(allocator, key) orelse return deflt;
defer allocator.free(value);
return std.ascii.eqlIgnoreCase(value, "true");
}
};
fn isUnnamed(t: std.builtin.TestFn) bool {
const marker = ".test_";
const test_name = t.name;
const index = std.mem.indexOf(u8, test_name, marker) orelse return false;
_ = std.fmt.parseInt(u32, test_name[index + marker.len ..], 10) catch return false;
return true;
}
fn serverHTTP(listener: *std.net.Server) !void {
var read_buffer: [1024]u8 = undefined;
ACCEPT: while (true) {
var conn = try listener.accept();
defer conn.stream.close();
var server = std.http.Server.init(conn, &read_buffer);
while (server.state == .ready) {
var request = server.receiveHead() catch |err| switch (err) {
error.HttpConnectionClosing => continue :ACCEPT,
else => {
std.debug.print("Test HTTP Server error: {}\n", .{err});
return err;
},
};
const path = request.head.target;
if (std.mem.eql(u8, path, "/loader")) {
try writeResponse(&request, .{
.body = "Hello!",
});
}
}
}
}
const Response = struct {
body: []const u8 = "",
status: std.http.Status = .ok,
};
fn writeResponse(req: *std.http.Server.Request, res: Response) !void {
try req.respond(res.body, .{ .status = res.status });
}
test {
std.testing.refAllDecls(@import("url/query.zig"));
std.testing.refAllDecls(@import("browser/dump.zig"));
std.testing.refAllDecls(@import("browser/loader.zig"));
std.testing.refAllDecls(@import("browser/mime.zig"));
std.testing.refAllDecls(@import("cdp/msg.zig"));
std.testing.refAllDecls(@import("css/css.zig"));
std.testing.refAllDecls(@import("css/libdom_test.zig"));
std.testing.refAllDecls(@import("css/match_test.zig"));
std.testing.refAllDecls(@import("css/parser.zig"));
std.testing.refAllDecls(@import("generate.zig"));
std.testing.refAllDecls(@import("http/Client.zig"));
std.testing.refAllDecls(@import("msg.zig"));
std.testing.refAllDecls(@import("storage/storage.zig"));
std.testing.refAllDecls(@import("iterator/iterator.zig"));
}

View File

@@ -62,6 +62,7 @@ pub const Values = struct {
// append by taking the ownership of the key and the value // append by taking the ownership of the key and the value
fn appendOwned(self: *Values, k: []const u8, v: []const u8) !void { fn appendOwned(self: *Values, k: []const u8, v: []const u8) !void {
if (self.map.getPtr(k)) |list| { if (self.map.getPtr(k)) |list| {
self.alloc.free(k);
return try list.append(self.alloc, v); return try list.append(self.alloc, v);
} }