mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 14:43:28 +00:00
Add a new unittest build step
Preserves all existing behavior (i.e. make test and zig build test are not changed in any way). The new 'unittest' only runs unit tests and is fast to build. It takes ~1.7 to build unittest, vs ~11.09 to build test. This is really the main goal, and hopefully any unit test which are (a) fast and (b) don't impact build times will be run here. The test runner is based on: https://gist.github.com/karlseguin/c6bea5b35e4e8d26af6f81c22cb5d76b It allow filtering, i.e. `make unittest F="parse query dup"`. 'unittest' does memory leak detection when tests use std.testing.allocator. Fixed a memory leak in url/query which was detected/reported with by the new 'unittest'. In order to avoid having 3 src/test_xyx.zig files, I merged the existing test_runner.zig and run_tests.zig into a single main_tests.zig. (this change is superfluous, but I thought it was cleaner this way. Happy to revert this).
This commit is contained in:
7
Makefile
7
Makefile
@@ -3,6 +3,8 @@
|
||||
|
||||
ZIG := zig
|
||||
BC := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
# option test filter make unittest F="server"
|
||||
F=
|
||||
|
||||
# OS and ARCH
|
||||
kernel = $(shell uname -ms)
|
||||
@@ -42,7 +44,7 @@ help:
|
||||
|
||||
# $(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)
|
||||
|
||||
@@ -91,6 +93,9 @@ test:
|
||||
@$(ZIG) build test -Dengine=v8 || (printf "\e[33mTest ERROR\e[0m\n"; exit 1;)
|
||||
@printf "\e[33mTest OK\e[0m\n"
|
||||
|
||||
unittest:
|
||||
@TEST_FILTER='${F}' $(ZIG) build unittest -freference-trace --summary all
|
||||
|
||||
# Install and build required dependencies commands
|
||||
# ------------
|
||||
.PHONY: install-submodule
|
||||
|
||||
25
build.zig
25
build.zig
@@ -98,8 +98,8 @@ pub fn build(b: *std.Build) !void {
|
||||
|
||||
// compile
|
||||
const tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/run_tests.zig"),
|
||||
.test_runner = b.path("src/test_runner.zig"),
|
||||
.root_source_file = b.path("src/main_tests.zig"),
|
||||
.test_runner = b.path("src/main_tests.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
});
|
||||
@@ -119,6 +119,27 @@ pub fn build(b: *std.Build) !void {
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
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
|
||||
// -----
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
337
src/unit_tests.zig
Normal file
337
src/unit_tests.zig
Normal file
@@ -0,0 +1,337 @@
|
||||
// Changed Jan 29, 2025 to accomodate latest Zig changes
|
||||
// See history if you're using an older version of Zig.
|
||||
|
||||
// in your build.zig, you can specify a custom test runner:
|
||||
// const tests = b.addTest(.{
|
||||
// .target = target,
|
||||
// .optimize = optimize,
|
||||
// .test_runner = .{ .path = b.path("test_runner.zig"), .mode = .simple }, // add this line
|
||||
// .root_source_file = b.path("src/main.zig"),
|
||||
// });
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
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 printer = Printer.init();
|
||||
printer.fmt("\r\x1b[0K", .{}); // beginning of line and clear to end of line
|
||||
|
||||
for (builtin.test_functions) |t| {
|
||||
if (isSetup(t)) {
|
||||
t.func() catch |err| {
|
||||
printer.status(.fail, "\nsetup \"{s}\" failed: {}\n", .{ t.name, err });
|
||||
return err;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (isSetup(t) or isTeardown(t)) {
|
||||
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, ".", .{});
|
||||
}
|
||||
}
|
||||
|
||||
for (builtin.test_functions) |t| {
|
||||
if (isTeardown(t)) {
|
||||
t.func() catch |err| {
|
||||
printer.status(.fail, "\nteardown \"{s}\" failed: {}\n", .{ t.name, err });
|
||||
return err;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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 isSetup(t: std.builtin.TestFn) bool {
|
||||
return std.mem.endsWith(u8, t.name, "tests:beforeAll");
|
||||
}
|
||||
|
||||
fn isTeardown(t: std.builtin.TestFn) bool {
|
||||
return std.mem.endsWith(u8, t.name, "tests:afterAll");
|
||||
}
|
||||
|
||||
test {
|
||||
const msgTest = @import("msg.zig");
|
||||
std.testing.refAllDecls(msgTest);
|
||||
|
||||
const dumpTest = @import("browser/dump.zig");
|
||||
std.testing.refAllDecls(dumpTest);
|
||||
|
||||
const mimeTest = @import("browser/mime.zig");
|
||||
std.testing.refAllDecls(mimeTest);
|
||||
|
||||
const cssTest = @import("css/css.zig");
|
||||
std.testing.refAllDecls(cssTest);
|
||||
|
||||
const cssParserTest = @import("css/parser.zig");
|
||||
std.testing.refAllDecls(cssParserTest);
|
||||
|
||||
const cssMatchTest = @import("css/match_test.zig");
|
||||
std.testing.refAllDecls(cssMatchTest);
|
||||
|
||||
const cssLibdomTest = @import("css/libdom_test.zig");
|
||||
std.testing.refAllDecls(cssLibdomTest);
|
||||
|
||||
const queryTest = @import("url/query.zig");
|
||||
std.testing.refAllDecls(queryTest);
|
||||
|
||||
std.testing.refAllDecls(@import("generate.zig"));
|
||||
std.testing.refAllDecls(@import("cdp/msg.zig"));
|
||||
}
|
||||
@@ -62,6 +62,7 @@ pub const Values = struct {
|
||||
// append by taking the ownership of the key and the value
|
||||
fn appendOwned(self: *Values, k: []const u8, v: []const u8) !void {
|
||||
if (self.map.getPtr(k)) |list| {
|
||||
self.alloc.free(k);
|
||||
return try list.append(self.alloc, v);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user