From c726b022e8194a824551732c5e819f88ce5820b6 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 30 Jul 2026 14:53:46 +0800 Subject: [PATCH] tests: improve test output and test log handling 1 - TEST_VERBOSE is now off by default 2 - There's a afterEach callback that is automatically run after each tests, it: a - clears the log filters b - resets the test arena 3 - LogFilter replace with a - testing.silenceLog(&.{...scopes...}); to silence all logs for the given scopes. b - testing.expectLog(&.{...scopes}); to set log expectations, 1 per log. The goal here isn't so much to expect logs (though, you can do that), but rather to silence an expected # of logs, without silencing more. --- Makefile | 10 +---- src/SemanticTree.zig | 3 -- src/Server.zig | 6 +-- src/browser/Frame.zig | 10 ++--- src/browser/ImportMap.zig | 18 -------- src/browser/Mime.zig | 15 ------- src/browser/Page.zig | 3 +- src/browser/Runner.zig | 1 - src/browser/ScriptManager.zig | 2 - src/browser/ScriptManagerBase.zig | 2 - src/browser/SelectorPath.zig | 2 - src/browser/URL.zig | 17 ------- src/browser/data_url.zig | 8 ---- src/browser/dump.zig | 1 - src/browser/forms.zig | 11 ----- src/browser/frame/preload.zig | 2 - src/browser/webapi/EventTarget.zig | 3 +- src/browser/webapi/SharedWorker.zig | 3 +- src/browser/webapi/Worker.zig | 3 +- .../webapi/css/CSSStyleDeclaration.zig | 1 - src/browser/webapi/css/CSSStyleSheet.zig | 3 +- src/browser/webapi/element/Svg.zig | 1 + src/browser/webapi/element/html/Link.zig | 6 +-- src/browser/webapi/element/html/Script.zig | 4 +- src/browser/webapi/net/EventSource.zig | 6 +-- src/browser/webapi/net/WebSocket.zig | 1 + src/browser/webapi/net/body_init.zig | 4 -- src/browser/webapi/selector/Parser.zig | 2 - src/cdp/CDP.zig | 2 + src/cdp/domains/emulation.zig | 12 ++--- src/cdp/domains/network.zig | 15 +++---- src/cdp/domains/page.zig | 11 ++--- src/cdp/domains/runtime.zig | 3 +- src/cdp/testing.zig | 3 +- src/log.zig | 30 +++++++++++++ src/mcp/Server.zig | 1 - src/mcp/protocol.zig | 6 --- src/mcp/router.zig | 4 +- src/mcp/tools.zig | 37 --------------- src/network/cache/FsCache.zig | 4 +- src/network/http.zig | 3 +- src/script/Runtime.zig | 30 +------------ src/test_runner.zig | 29 ++++++++++-- src/testing.zig | 45 ++++++++++++------- 44 files changed, 127 insertions(+), 256 deletions(-) diff --git a/Makefile b/Makefile index 3bd9eb4ee..0ec21e8ab 100644 --- a/Makefile +++ b/Makefile @@ -119,16 +119,8 @@ run-debug: build-dev @printf "\033[36mRunning...\033[0m\n" @./zig-out/bin/lightpanda || (printf "\033[33mRun ERROR\033[0m\n"; exit 1;) -## Test - `grep` is used to filter out the huge compile command on build -ifeq ($(OS), macos) test: - @script -q /dev/null sh -c 'TEST_FILTER="${F}" $(ZIG) build $(ZIGFLAGS) test -freference-trace' 2>&1 \ - | grep --line-buffered -v "^/.*zig test -freference-trace" -else -test: - @script -qec 'TEST_FILTER="${F}" $(ZIG) build $(ZIGFLAGS) test -freference-trace' /dev/null 2>&1 \ - | grep --line-buffered -v "^/.*zig test -freference-trace" -endif + TEST_FILTER="${F}" $(ZIG) build $(ZIGFLAGS) test -freference-trace ## Run demo/runner end to end tests end2end: diff --git a/src/SemanticTree.zig b/src/SemanticTree.zig index 02a5f8f36..ad224bab9 100644 --- a/src/SemanticTree.zig +++ b/src/SemanticTree.zig @@ -730,8 +730,6 @@ pub fn getNodeDetails( const testing = @import("testing.zig"); test "SemanticTree backendDOMNodeId" { - defer testing.reset(); - var registry: CDPNode.Registry = .init(testing.allocator); defer registry.deinit(); @@ -756,7 +754,6 @@ test "SemanticTree backendDOMNodeId" { } test "SemanticTree max_depth" { - defer testing.reset(); var registry: CDPNode.Registry = .init(testing.allocator); defer registry.deinit(); diff --git a/src/Server.zig b/src/Server.zig index 7bd65a4fb..2d93c46c9 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -331,8 +331,7 @@ test "server: buildJSONVersionResponse" { } test "Client: http invalid request" { - const filter: testing.LogFilter = .init(&.{.cdp}); - defer filter.deinit(); + testing.silenceLog(&.{.cdp}); var c = try createTestClient(); defer c.deinit(); @@ -449,8 +448,7 @@ test "Client: read invalid websocket message" { } { - const filter: testing.LogFilter = .init(&.{.cdp}); - defer filter.deinit(); + testing.expectLog(&.{.cdp}); // length of message is 0, 0, 0, 0, 0, 16, 0, 1 i.e: 1024 * 1024 + 1 try assertWebSocketError(1009, &.{ 129, 255, 0, 0, 0, 0, 0, 16, 0, 1, 'm', 'a', 's', 'k' }); } diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index d4c3db33e..30bc1769e 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -3460,8 +3460,7 @@ test "Frame: urlBasename" { } test "WebApi: Frame" { - const filter: testing.LogFilter = .init(&.{.http}); - defer filter.deinit(); + testing.silenceLog(&.{.http}); try testing.htmlRunner("page", .{}); } @@ -3470,8 +3469,7 @@ test "WebApi: Frames" { } test "WebApi: Frame Blob" { - const filter: testing.LogFilter = .init(&.{ .frame, .browser, .js }); - defer filter.deinit(); + testing.silenceLog(&.{ .frame, .browser, .js }); try testing.htmlRunner("frames/blob", .{}); } @@ -3524,6 +3522,8 @@ test "Page: isSameOrigin" { } test "Frame: httpMetadata after navigation" { + testing.expectLog(&.{.http}); + const page = try testing.pageTest("page/meta.html", .{}); defer page.close(); @@ -3544,8 +3544,6 @@ test "Frame: httpMetadata 404" { } test "Frame: 401" { - defer testing.reset(); - var page = try testing.pageTest("401", .{}); defer page.close(); diff --git a/src/browser/ImportMap.zig b/src/browser/ImportMap.zig index a2c0ee7e7..355d28216 100644 --- a/src/browser/ImportMap.zig +++ b/src/browser/ImportMap.zig @@ -360,8 +360,6 @@ fn isSpecialUrl(url: []const u8) bool { const testing = @import("../testing.zig"); test "ImportMap: exact match" { - defer testing.reset(); - const im = try testParse( \\{ "imports": { "moment": "/node_modules/moment/index.js" } } , "https://example.com/app/index.html"); @@ -371,8 +369,6 @@ test "ImportMap: exact match" { } test "ImportMap: trailing slash prefix match" { - defer testing.reset(); - const im = try testParse( \\{ "imports": { "moment/": "/node_modules/moment/src/" } } , "https://example.com/app/index.html"); @@ -382,8 +378,6 @@ test "ImportMap: trailing slash prefix match" { } test "ImportMap: specificity — longest match wins" { - defer testing.reset(); - const im = try testParse( \\{ "imports": { \\ "a": "/1", @@ -413,8 +407,6 @@ test "ImportMap: specificity — longest match wins" { } test "ImportMap: scopes — most specific scope wins" { - defer testing.reset(); - const im = try testParse( \\{ \\ "imports": { "a": "/a-1.mjs", "b": "/b-1.mjs", "d": "/d-1.mjs" }, @@ -444,8 +436,6 @@ test "ImportMap: scopes — most specific scope wins" { } test "ImportMap: bare specifier with no match returns null" { - defer testing.reset(); - const im = try testParse( \\{ "imports": { "moment": "/m.js" } } , "https://example.com/app/index.html"); @@ -455,8 +445,6 @@ test "ImportMap: bare specifier with no match returns null" { } test "ImportMap: URL-like specifier falls back to itself" { - defer testing.reset(); - const im: ImportMap = .empty; const r = try testResolve(&im, "https://example.com/app.mjs", "./foo.js"); @@ -464,8 +452,6 @@ test "ImportMap: URL-like specifier falls back to itself" { } test "ImportMap: null entry throws (no fallback)" { - defer testing.reset(); - const im = try testParse( \\{ "imports": { "blocked": null } } , "https://example.com/app/index.html"); @@ -474,8 +460,6 @@ test "ImportMap: null entry throws (no fallback)" { } test "ImportMap: backtracking out of prefix throws" { - defer testing.reset(); - const im = try testParse( \\{ "imports": { "moment/": "/node_modules/moment/src/" } } , "https://example.com/app/index.html"); @@ -484,7 +468,6 @@ test "ImportMap: backtracking out of prefix throws" { } test "ImportMap: merge — first-wins on imports, new keys added" { - defer testing.reset(); const base: [:0]const u8 = "https://example.com/app/index.html"; var im = try testParse( @@ -504,7 +487,6 @@ test "ImportMap: merge — first-wins on imports, new keys added" { } test "ImportMap: merge — same-prefix scopes merge their imports" { - defer testing.reset(); const base: [:0]const u8 = "https://example.com/app/index.html"; var im = try testParse( diff --git a/src/browser/Mime.zig b/src/browser/Mime.zig index 6b5d85068..7bd941ab1 100644 --- a/src/browser/Mime.zig +++ b/src/browser/Mime.zig @@ -810,8 +810,6 @@ fn trimRight(s: []const u8) []const u8 { const testing = @import("../testing.zig"); test "Mime: invalid" { - defer testing.reset(); - const invalids = [_][]const u8{ "", "text", @@ -828,7 +826,6 @@ test "Mime: invalid" { } test "Mime: malformed parameters are ignored" { - defer testing.reset(); // These should all parse successfully as text/html with malformed params ignored const valid_with_malformed_params = [_][]const u8{ @@ -851,8 +848,6 @@ test "Mime: malformed parameters are ignored" { } test "Mime: parse common" { - defer testing.reset(); - try expect(.{ .content_type = .{ .text_xml = {} } }, "text/xml"); try expect(.{ .content_type = .{ .text_html = {} } }, "text/html"); try expect(.{ .content_type = .{ .text_plain = {} } }, "text/plain"); @@ -888,8 +883,6 @@ test "Mime: parse common" { } test "Mime: parse uncommon" { - defer testing.reset(); - const text_csv = Expectation{ .content_type = .{ .other = {} }, }; @@ -902,8 +895,6 @@ test "Mime: parse uncommon" { } test "Mime: parse charset" { - defer testing.reset(); - try expect(.{ .content_type = .{ .text_xml = {} }, .charset = "utf-8", @@ -936,7 +927,6 @@ test "Mime: parse charset" { } test "Mime: parse charset (WHATWG parameter semantics)" { - defer testing.reset(); // First charset wins (not last). try expect(.{ .content_type = .{ .text_html = {} }, .charset = "gbk" }, "text/html;charset=gbk;charset=utf-8"); @@ -959,8 +949,6 @@ test "Mime: parse charset (WHATWG parameter semantics)" { } test "Mime: isHTML" { - defer testing.reset(); - const assert = struct { fn assert(expected: bool, input: []const u8) !void { const mutable_input = try testing.arena_allocator.dupe(u8, input); @@ -977,8 +965,6 @@ test "Mime: isHTML" { } test "Mime: isXML" { - defer testing.reset(); - const assert = struct { fn assert(expected: bool, input: []const u8) !void { const mutable_input = try testing.arena_allocator.dupe(u8, input); @@ -1105,7 +1091,6 @@ fn expect(expected: Expectation, input: []const u8) !void { } test "Mime: serialize" { - defer testing.reset(); const arena = testing.arena_allocator; const expectSerialize = struct { diff --git a/src/browser/Page.zig b/src/browser/Page.zig index a0bbb3151..597c0ba47 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -407,7 +407,8 @@ fn appendFrameExecutions(frame: *Frame, origin: []const u8, arena: Allocator, li const testing = @import("../testing.zig"); test "Page: js_error_count" { - defer testing.reset(); + testing.expectLog(&.{ .js, .js, .js }); + // One uncaught top-level script exception, one uncaught timer-callback // exception. const page = try testing.pageTest("page_js_error.html", .{}); diff --git a/src/browser/Runner.zig b/src/browser/Runner.zig index 5c6edd94d..f07b748f6 100644 --- a/src/browser/Runner.zig +++ b/src/browser/Runner.zig @@ -474,7 +474,6 @@ test "Runner: waitForSelector timeout" { } test "Runner: waitForSelector" { - defer testing.reset(); const page = try testing.pageTest("runner/runner1.html", .{}); var runner = page.session.runner(.{}); diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 4c72e1036..952b82934 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -566,7 +566,6 @@ const PreloadedScript = struct { const testing = @import("../testing.zig"); test "ScriptManager: PreloadedScript.shutdownCallback drops a .loading preload" { - defer testing.reset(); const page = try testing.pageTest("mcp_nav.html", .{}); defer page.close(); @@ -598,7 +597,6 @@ test "ScriptManager: PreloadedScript.shutdownCallback drops a .loading preload" } test "ScriptManager: waitForPreload stops when teardown is pending" { - defer testing.reset(); const page = try testing.pageTest("mcp_nav.html", .{}); defer page.close(); diff --git a/src/browser/ScriptManagerBase.zig b/src/browser/ScriptManagerBase.zig index a2b0c1e3a..2069bfee7 100644 --- a/src/browser/ScriptManagerBase.zig +++ b/src/browser/ScriptManagerBase.zig @@ -1080,7 +1080,6 @@ pub const ImportedModule = struct { const testing = @import("../testing.zig"); test "ScriptManagerBase: shutdownCallback fails a .loading module" { - defer testing.reset(); const page = try testing.pageTest("mcp_nav.html", .{}); defer page.close(); const frame = page.frame().?; @@ -1115,7 +1114,6 @@ test "ScriptManagerBase: shutdownCallback fails a .loading module" { } test "ScriptManagerBase: waitForImport stops when teardown is pending" { - defer testing.reset(); const page = try testing.pageTest("mcp_nav.html", .{}); defer page.close(); const frame = page.frame().?; diff --git a/src/browser/SelectorPath.zig b/src/browser/SelectorPath.zig index 1750728be..0b2cb8515 100644 --- a/src/browser/SelectorPath.zig +++ b/src/browser/SelectorPath.zig @@ -213,8 +213,6 @@ fn isFirstMatch(self: SelectorPath, target: *Element, candidate: []const u8) boo const testing = @import("../testing.zig"); fn expectSelector(comptime selector: []const u8, comptime expected: []const u8) !void { - defer testing.reset(); - var page = try testing.pageTest("selector_path.html", .{}); defer page.close(); const frame = page.frame().?; diff --git a/src/browser/URL.zig b/src/browser/URL.zig index fe97add68..94d53d8df 100644 --- a/src/browser/URL.zig +++ b/src/browser/URL.zig @@ -692,8 +692,6 @@ test "URL: isCompleteHTTPUrl" { } test "URL: resolve regression (#1093)" { - defer testing.reset(); - const Case = struct { base: [:0]const u8, path: [:0]const u8, @@ -715,8 +713,6 @@ test "URL: resolve regression (#1093)" { } test "URL: resolve" { - defer testing.reset(); - const Case = struct { base: [:0]const u8, path: [:0]const u8, @@ -961,8 +957,6 @@ test "URL: resolve" { } test "URL: resolve strips tab and newline from input" { - defer testing.reset(); - const Case = struct { base: [:0]const u8, path: [:0]const u8, @@ -994,7 +988,6 @@ test "URL: resolve strips tab and newline from input" { } test "URL: resolve validates ASCII punycode (xn--) labels" { - defer testing.reset(); // Valid punycode is left untouched. const ok = try resolve(testing.arena_allocator, "https://example.com/", "https://xn--rksmrgs-5wao1o.se/x", .{}); @@ -1006,8 +999,6 @@ test "URL: resolve validates ASCII punycode (xn--) labels" { } test "URL: resolve with encoding" { - defer testing.reset(); - const Case = struct { base: [:0]const u8, path: [:0]const u8, @@ -1176,7 +1167,6 @@ test "URL: resolve with encoding" { } test "URL: eqlDocument" { - defer testing.reset(); { const url = "https://lightpanda.io/about"; try testing.expectEqual(true, eqlDocument(url, url)); @@ -1244,7 +1234,6 @@ test "URL: eqlDocument" { } test "URL: concatQueryString" { - defer testing.reset(); const arena = testing.arena_allocator; { @@ -1274,7 +1263,6 @@ test "URL: concatQueryString" { } test "URL: getRobotsUrl" { - defer testing.reset(); const arena = testing.arena_allocator; { @@ -1302,7 +1290,6 @@ test "URL: getRobotsUrl" { } test "URL: unescape" { - defer testing.reset(); const arena = testing.arena_allocator; { @@ -1437,8 +1424,6 @@ test "URL: setPathname percent-encodes" { } test "URL: getOrigin" { - defer testing.reset(); - const Case = struct { url: [:0]const u8, expected: ?[]const u8, @@ -1680,8 +1665,6 @@ test "URL: resolve path scheme" { } test "URL: resolveNavigation defaults a schemeless host to http (curl-like)" { - defer testing.reset(); - const Case = struct { url: [:0]const u8, expected: [:0]const u8, diff --git a/src/browser/data_url.zig b/src/browser/data_url.zig index 81e9deb24..fce62bb8f 100644 --- a/src/browser/data_url.zig +++ b/src/browser/data_url.zig @@ -72,28 +72,24 @@ fn base64Decode(arena: Allocator, input: []const u8) ![]const u8 { const testing = @import("../testing.zig"); test "data_url: plain text, default content-type" { - defer testing.reset(); const r = try parse(testing.arena_allocator, "data:,Hello%2C%20World"); try testing.expectString("text/plain;charset=US-ASCII", r.content_type); try testing.expectString("Hello, World", r.body); } test "data_url: explicit mediatype" { - defer testing.reset(); const r = try parse(testing.arena_allocator, "data:text/html,hi"); try testing.expectString("text/html", r.content_type); try testing.expectString("hi", r.body); } test "data_url: base64" { - defer testing.reset(); const r = try parse(testing.arena_allocator, "data:text/plain;base64,SGVsbG8="); try testing.expectString("text/plain", r.content_type); try testing.expectString("Hello", r.body); } test "data_url: base64 without padding decodes (forgiving)" { - defer testing.reset(); const r = try parse(testing.arena_allocator, "data:application/octet-stream;base64,SGVsbG8"); try testing.expectString("Hello", r.body); @@ -106,7 +102,6 @@ test "data_url: base64 without padding decodes (forgiving)" { } test "data_url: forgiving-base64 rejects misplaced/over-padding" { - defer testing.reset(); const arena = testing.arena_allocator; try std.testing.expectError(error.InvalidBase64, parse(arena, "data:;base64,abcd=")); // len % 4 == 1 try std.testing.expectError(error.InvalidBase64, parse(arena, "data:;base64,=")); @@ -115,18 +110,15 @@ test "data_url: forgiving-base64 rejects misplaced/over-padding" { } test "data_url: bare charset gets text/plain prefix" { - defer testing.reset(); const r = try parse(testing.arena_allocator, "data:;charset=utf-8,x"); try testing.expectString("text/plain;charset=utf-8", r.content_type); } test "data_url: empty body" { - defer testing.reset(); const r = try parse(testing.arena_allocator, "data:text/plain,"); try testing.expectString("", r.body); } test "data_url: missing comma is an error" { - defer testing.reset(); try std.testing.expectError(error.InvalidDataUrl, parse(testing.arena_allocator, "data:text/plain")); } diff --git a/src/browser/dump.zig b/src/browser/dump.zig index 394e12c92..fc197bea0 100644 --- a/src/browser/dump.zig +++ b/src/browser/dump.zig @@ -382,7 +382,6 @@ const testing = @import("../testing.zig"); // element), so reusing one frame across opts would leak that mutation // into later dumps. fn expectDump(opts: Opts, expected: []const u8) !void { - defer testing.reset(); var page = try testing.pageTest("dump.html", .{}); defer page.close(); diff --git a/src/browser/forms.zig b/src/browser/forms.zig index ce6de36b1..858382bfd 100644 --- a/src/browser/forms.zig +++ b/src/browser/forms.zig @@ -288,7 +288,6 @@ fn testForms(html: []const u8) ![]FormInfo { } test "browser.forms: login form" { - defer testing.reset(); const forms = try testForms( \\
\\ @@ -310,7 +309,6 @@ test "browser.forms: login form" { } test "browser.forms: form with select" { - defer testing.reset(); const forms = try testForms( \\ \\ @@ -345,7 +342,6 @@ test "browser.forms: form with textarea" { } test "browser.forms: empty form skipped" { - defer testing.reset(); const forms = try testForms( \\ \\

No fields here

@@ -357,7 +353,6 @@ test "browser.forms: empty form skipped" { } test "browser.forms: hidden inputs excluded" { - defer testing.reset(); const forms = try testForms( \\ \\ @@ -372,7 +367,6 @@ test "browser.forms: hidden inputs excluded" { } test "browser.forms: multiple forms" { - defer testing.reset(); const forms = try testForms( \\ \\ @@ -390,7 +384,6 @@ test "browser.forms: multiple forms" { } test "browser.forms: disabled fields flagged" { - defer testing.reset(); const forms = try testForms( \\ \\ @@ -406,7 +399,6 @@ test "browser.forms: disabled fields flagged" { } test "browser.forms: disabled fieldset" { - defer testing.reset(); const forms = try testForms( \\ \\
@@ -424,7 +416,6 @@ test "browser.forms: disabled fieldset" { } test "browser.forms: external field via form attribute" { - defer testing.reset(); const forms = try testForms( \\ \\ @@ -438,7 +429,6 @@ test "browser.forms: external field via form attribute" { } test "browser.forms: checkbox and radio return value attribute" { - defer testing.reset(); const forms = try testForms( \\ \\ @@ -456,7 +446,6 @@ test "browser.forms: checkbox and radio return value attribute" { } test "browser.forms: form without action or method" { - defer testing.reset(); const forms = try testForms( \\ \\ diff --git a/src/browser/frame/preload.zig b/src/browser/frame/preload.zig index 9bc80aeee..ca7ad8811 100644 --- a/src/browser/frame/preload.zig +++ b/src/browser/frame/preload.zig @@ -155,8 +155,6 @@ fn hasNonRemoteScheme(href: []const u8) bool { const testing = @import("../../testing.zig"); test "preload: prescan" { - defer testing.reset(); - const page = try testing.pageTest("mcp_nav.html", .{}); defer page.close(); const frame = page.frame().?; diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index 04c4a9085..3d6df52a5 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -290,8 +290,7 @@ pub const JsApi = struct { const testing = @import("../../testing.zig"); test "WebApi: EventTarget" { - const filter: testing.LogFilter = .init(&.{ .js, .event }); - defer filter.deinit(); + testing.silenceLog(&.{ .js, .event }); // we create thousands of these per frame. Nothing should bloat it. try testing.expectEqual(16, @sizeOf(EventTarget)); diff --git a/src/browser/webapi/SharedWorker.zig b/src/browser/webapi/SharedWorker.zig index a0063d27c..4e8e3e0cc 100644 --- a/src/browser/webapi/SharedWorker.zig +++ b/src/browser/webapi/SharedWorker.zig @@ -139,7 +139,6 @@ pub const JsApi = struct { const testing = @import("../../testing.zig"); test "WebApi: SharedWorker" { - const filter: testing.LogFilter = .init(&.{.http}); - defer filter.deinit(); + testing.silenceLog(&.{.http}); try testing.htmlRunner("shared_worker", .{ .timeout_ms = 8000 }); } diff --git a/src/browser/webapi/Worker.zig b/src/browser/webapi/Worker.zig index 941a24415..abcf64f2c 100644 --- a/src/browser/webapi/Worker.zig +++ b/src/browser/webapi/Worker.zig @@ -489,8 +489,7 @@ pub const JsApi = struct { const testing = @import("../../testing.zig"); test "WebApi: Worker" { - const filter: testing.LogFilter = .init(&.{.http}); - defer filter.deinit(); + testing.silenceLog(&.{.http}); // Worker tests chain a worker-script fetch with a dynamic-import fetch // and a cross-context postMessage. The default 2 s assertion budget can diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig index e05f9c221..80121f58d 100644 --- a/src/browser/webapi/css/CSSStyleDeclaration.zig +++ b/src/browser/webapi/css/CSSStyleDeclaration.zig @@ -946,7 +946,6 @@ test "normalizePropertyValue: collapse duplicate two-value shorthands" { } test "normalizePropertyValue: anchor() canonical order" { - defer testing.reset(); const cases = .{ // Dashed ident should come before keyword .{ "left", "anchor(left --foo)", "anchor(--foo left)" }, diff --git a/src/browser/webapi/css/CSSStyleSheet.zig b/src/browser/webapi/css/CSSStyleSheet.zig index 7e8350cc3..0793f37a5 100644 --- a/src/browser/webapi/css/CSSStyleSheet.zig +++ b/src/browser/webapi/css/CSSStyleSheet.zig @@ -216,8 +216,7 @@ pub const JsApi = struct { const testing = @import("../../../testing.zig"); test "WebApi: CSSStyleSheet" { - const filter: testing.LogFilter = .init(&.{.js}); - defer filter.deinit(); + testing.silenceLog(&.{.js}); try testing.htmlRunner("css/stylesheet.html", .{}); } diff --git a/src/browser/webapi/element/Svg.zig b/src/browser/webapi/element/Svg.zig index db0b0942c..08feef0f1 100644 --- a/src/browser/webapi/element/Svg.zig +++ b/src/browser/webapi/element/Svg.zig @@ -157,5 +157,6 @@ pub const JsApi = struct { const testing = @import("../../../testing.zig"); test "WebApi: Svg" { + testing.expectLog(&.{ .not_implemented, .not_implemented }); try testing.htmlRunner("element/svg", .{}); } diff --git a/src/browser/webapi/element/html/Link.zig b/src/browser/webapi/element/html/Link.zig index 688dc0188..8b306f05c 100644 --- a/src/browser/webapi/element/html/Link.zig +++ b/src/browser/webapi/element/html/Link.zig @@ -297,8 +297,7 @@ test "WebApi: HTML.Link" { } test "WebApi: HTML.Link external stylesheet" { - const filter: testing.LogFilter = .init(&.{.http}); - defer filter.deinit(); + testing.silenceLog(&.{.http}); try testing.htmlRunner("css/external_stylesheet.html", .{ .load_external_stylesheets = true }); } @@ -307,7 +306,6 @@ test "WebApi: HTML.Link external stylesheet" { // request gate during the sync window). Otherwise the deferred-script queue // never drains and the document is stuck at readyState "loading". test "WebApi: HTML.Link deferred script then external stylesheet" { - const filter: testing.LogFilter = .init(&.{.http}); - defer filter.deinit(); + testing.silenceLog(&.{.http}); try testing.htmlRunner("css/deferred_script_then_stylesheet.html", .{ .load_external_stylesheets = true }); } diff --git a/src/browser/webapi/element/html/Script.zig b/src/browser/webapi/element/html/Script.zig index c8e1a6d1e..cf8c79a66 100644 --- a/src/browser/webapi/element/html/Script.zig +++ b/src/browser/webapi/element/html/Script.zig @@ -194,7 +194,7 @@ pub const Build = struct { const testing = @import("../../../../testing.zig"); test "WebApi: Script" { - const filter: testing.LogFilter = .init(&.{.http}); - defer filter.deinit(); + testing.silenceLog(&.{.http}); + testing.expectLog(&.{ .js, .js }); try testing.htmlRunner("element/html/script", .{}); } diff --git a/src/browser/webapi/net/EventSource.zig b/src/browser/webapi/net/EventSource.zig index cfdb6c172..020be0863 100644 --- a/src/browser/webapi/net/EventSource.zig +++ b/src/browser/webapi/net/EventSource.zig @@ -650,13 +650,11 @@ pub const JsApi = struct { const testing = @import("../../../testing.zig"); test "WebApi: EventSource" { - const filter: testing.LogFilter = .init(&.{.http}); - defer filter.deinit(); + testing.silenceLog(&.{.http}); try testing.htmlRunner("net/eventsource.html", .{}); } test "WebApi: EventSource in worker" { - const filter: testing.LogFilter = .init(&.{.http}); - defer filter.deinit(); + testing.silenceLog(&.{.http}); try testing.htmlRunner("net/eventsource_worker.html", .{}); } diff --git a/src/browser/webapi/net/WebSocket.zig b/src/browser/webapi/net/WebSocket.zig index 72482a883..13213054a 100644 --- a/src/browser/webapi/net/WebSocket.zig +++ b/src/browser/webapi/net/WebSocket.zig @@ -1046,6 +1046,7 @@ pub const JsApi = struct { const testing = @import("../../../testing.zig"); test "WebApi: WebSocket" { + testing.expectLog(&.{.websocket}); try testing.htmlRunner("net/websocket.html", .{}); } diff --git a/src/browser/webapi/net/body_init.zig b/src/browser/webapi/net/body_init.zig index a095e1816..be318e166 100644 --- a/src/browser/webapi/net/body_init.zig +++ b/src/browser/webapi/net/body_init.zig @@ -136,14 +136,12 @@ pub fn stripUtf8Bom(bytes: []const u8) []const u8 { const testing = @import("../../../testing.zig"); test "BodyInit: bytes pass through with text/plain" { - defer testing.reset(); const r = try (BodyInit{ .bytes = "hello" }).extract(testing.arena_allocator); try testing.expectString("hello", r.bytes); try testing.expectString("text/plain;charset=UTF-8", r.content_type.?); } test "BodyInit: URLSearchParams emit urlencoded body + content-type" { - defer testing.reset(); const arena = try testing.test_app.arena_pool.acquire(.small, "body_init test"); defer arena.release(); @@ -158,7 +156,6 @@ test "BodyInit: URLSearchParams emit urlencoded body + content-type" { } test "BodyInit: FormData emits multipart with random boundary" { - defer testing.reset(); const arena = try testing.test_app.arena_pool.acquire(.small, "body_init test"); defer arena.release(); @@ -184,7 +181,6 @@ test "BodyInit: FormData emits multipart with random boundary" { } test "BodyInit: buffer source has no default Content-Type" { - defer testing.reset(); const r = try (BodyInit{ .buffer = .{ .values = "hello" } }).extract(testing.arena_allocator); try testing.expectString("hello", r.bytes); try testing.expectEqual(true, r.content_type == null); diff --git a/src/browser/webapi/selector/Parser.zig b/src/browser/webapi/selector/Parser.zig index f8cae8442..eb0be2d86 100644 --- a/src/browser/webapi/selector/Parser.zig +++ b/src/browser/webapi/selector/Parser.zig @@ -1666,7 +1666,6 @@ test "Selector: Parser.parseNthPattern" { } test "Selector: Parser.attributeValue" { - defer testing.reset(); const arena = testing.arena_allocator; // Unquoted identifier value (unchanged path). @@ -1759,7 +1758,6 @@ test "Selector: Parser.attributeValue" { } test "Selector: Parser.attributeName" { - defer testing.reset(); const arena = testing.arena_allocator; // Plain name (fast path). diff --git a/src/cdp/CDP.zig b/src/cdp/CDP.zig index 623c97023..7b6e09fff 100644 --- a/src/cdp/CDP.zig +++ b/src/cdp/CDP.zig @@ -1449,6 +1449,8 @@ test "cdp: disconnect latches so the worker keeps exiting" { } test "cdp: tick sends a close frame on pending terminate" { + testing.expectLog(&.{.cdp}); + var ctx = try testing.context(); defer ctx.deinit(); diff --git a/src/cdp/domains/emulation.zig b/src/cdp/domains/emulation.zig index 86262fc6f..56287d684 100644 --- a/src/cdp/domains/emulation.zig +++ b/src/cdp/domains/emulation.zig @@ -188,8 +188,7 @@ test "cdp.Emulation: setUserAgentOverride with valid user agent" { } test "cdp.Emulation: setUserAgentOverride ignores mozilla" { - const filter: testing.LogFilter = .init(&.{.not_implemented}); - defer filter.deinit(); + testing.silenceLog(&.{.not_implemented}); var ctx = try testing.context(); defer ctx.deinit(); @@ -206,8 +205,7 @@ test "cdp.Emulation: setUserAgentOverride ignores mozilla" { } test "cdp.Emulation: setUserAgentOverride ignores mozilla case insensitive" { - const filter: testing.LogFilter = .init(&.{.not_implemented}); - defer filter.deinit(); + testing.silenceLog(&.{.not_implemented}); var ctx = try testing.context(); defer ctx.deinit(); @@ -224,8 +222,7 @@ test "cdp.Emulation: setUserAgentOverride ignores mozilla case insensitive" { } test "cdp.Emulation: setUserAgentOverride rejects non-printable characters" { - const filter: testing.LogFilter = .init(&.{.not_implemented}); - defer filter.deinit(); + testing.silenceLog(&.{.not_implemented}); var ctx = try testing.context(); defer ctx.deinit(); @@ -241,8 +238,7 @@ test "cdp.Emulation: setUserAgentOverride rejects non-printable characters" { } test "cdp.Emulation: setUserAgentOverride with optional params" { - const filter: testing.LogFilter = .init(&.{.not_implemented}); - defer filter.deinit(); + testing.silenceLog(&.{.not_implemented}); var ctx = try testing.context(); defer ctx.deinit(); diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index deb77ffcd..011c9d1ac 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -652,8 +652,7 @@ test "cdp.network setExtraHTTPHeaders" { } test "cdp.network setExtraHTTPHeaders rejects non-printable User-Agent" { - const filter: testing.LogFilter = .init(&.{.not_implemented}); - defer filter.deinit(); + testing.silenceLog(&.{.not_implemented}); var ctx = try testing.context(); defer ctx.deinit(); @@ -674,8 +673,7 @@ test "cdp.network setExtraHTTPHeaders rejects non-printable User-Agent" { } test "cdp.network setExtraHTTPHeaders rejects a Mozilla User-Agent" { - const filter: testing.LogFilter = .init(&.{.not_implemented}); - defer filter.deinit(); + testing.silenceLog(&.{.not_implemented}); var ctx = try testing.context(); defer ctx.deinit(); @@ -709,8 +707,7 @@ test "cdp.network setExtraHTTPHeaders accepts valid User-Agent" { } test "cdp.network setExtraHTTPHeaders rejects a Mozilla User-Agent smuggled via a colon in the key" { - const filter: testing.LogFilter = .init(&.{.not_implemented}); - defer filter.deinit(); + testing.silenceLog(&.{.not_implemented}); var ctx = try testing.context(); defer ctx.deinit(); @@ -731,8 +728,7 @@ test "cdp.network setExtraHTTPHeaders rejects a Mozilla User-Agent smuggled via } test "cdp.network setExtraHTTPHeaders rejects a header that smuggles CRLF" { - const filter: testing.LogFilter = .init(&.{.not_implemented}); - defer filter.deinit(); + testing.silenceLog(&.{.not_implemented}); var ctx = try testing.context(); defer ctx.deinit(); @@ -989,8 +985,7 @@ test "cdp.Network: configured CDP ignores setCacheDisabled" { } test "cdp.Network: setBlockedURLs blocks requests with inspector reason" { - const filter: testing.LogFilter = .init(&.{.http}); - defer filter.deinit(); + testing.silenceLog(&.{.http}); var ctx = try testing.context(); defer ctx.deinit(); diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index f47b496db..8215990ee 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -1215,9 +1215,7 @@ test "cdp.frame: child navigation preserves node registry" { } test "cdp.frame: captureScreenshot" { - const LogFilter = @import("../../testing.zig").LogFilter; - const filter: LogFilter = .init(&.{.not_implemented}); - defer filter.deinit(); + testing.silenceLog(&.{.not_implemented}); var ctx = try testing.context(); defer ctx.deinit(); @@ -1235,9 +1233,7 @@ test "cdp.frame: captureScreenshot" { } test "cdp.frame: printToPDF" { - const LogFilter = @import("../../testing.zig").LogFilter; - const filter: LogFilter = .init(&.{.not_implemented}); - defer filter.deinit(); + testing.silenceLog(&.{.not_implemented}); var ctx = try testing.context(); defer ctx.deinit(); @@ -1536,8 +1532,7 @@ test "cdp.frame: navigate does not follow Location on a non-redirect 3xx" { } test "cdp.frame: navigate answers with errorText when the navigation fails" { - const filter: testing.LogFilter = .init(&.{.frame}); - defer filter.deinit(); + testing.silenceLog(&.{.frame}); // A root navigation that fails before commit (here: connection refused — // nothing listens on port 1) must still answer the Page.navigate command. diff --git a/src/cdp/domains/runtime.zig b/src/cdp/domains/runtime.zig index 35b34a745..bcd1dd268 100644 --- a/src/cdp/domains/runtime.zig +++ b/src/cdp/domains/runtime.zig @@ -169,8 +169,7 @@ pub fn consoleMessage(arena: Allocator, bc: *CDP.BrowserContext, event: *const N const testing = @import("../testing.zig"); test "cdp.runtime: consoleAPICalled type matches the console method" { - const filter: testing.LogFilter = .init(&.{.js}); - defer filter.deinit(); + testing.silenceLog(&.{.js}); // Wire types per the CDP protocol: console.log -> "log", // console.warn -> "warning" (not "warn"), console.info -> "info", diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index a83791441..8133f32bb 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -34,7 +34,8 @@ pub const expectError = base.expectError; pub const expectEqualSlices = base.expectEqualSlices; pub const pageTest = base.pageTest; pub const newString = base.newString; -pub const LogFilter = base.LogFilter; +pub const expectLog = base.expectLog; +pub const silenceLog = base.silenceLog; const TestContext = struct { read_at: usize = 0, diff --git a/src/log.zig b/src/log.zig index eb7aebb41..43852e02b 100644 --- a/src/log.zig +++ b/src/log.zig @@ -99,6 +99,28 @@ pub fn enabled(scope: Scope, level: Level) bool { return true; } +// The number of log lines each scope is expected to emit for the current test. +// A line from a scope with a pending count is consumed instead of written; see +// testing.expectLog. +var expected_logs: [num_scopes]u16 = @splat(0); + +// Registers one expected log line per entry in `scopes`. +pub fn expectLog(comptime scopes: []const Scope) void { + comptime std.debug.assert(IS_TEST); + inline for (scopes) |scope| { + expected_logs[@intFromEnum(scope)] += 1; + } +} + +// Called after each test. Returns the expectations that were never met. +pub fn resetTestState() [num_scopes]u16 { + std.debug.assert(IS_TEST); + const unmet = expected_logs; + expected_logs = @splat(0); + opts.scope_enabled = @splat(true); + return unmet; +} + // Ugliness to support complex debug parameters. Could add better support for // this directly in writeValue, but we [currently] only need this in one place // and I kind of don't want to encourage / make this easy. @@ -151,6 +173,14 @@ pub fn log(scope: Scope, level: Level, msg: []const u8, data: anytype) void { return; } + if (comptime IS_TEST) { + const expected = &expected_logs[@intFromEnum(scope)]; + if (expected.* > 0) { + expected.* -= 1; + return; + } + } + if (sink) |s| { var buf: [4096]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); diff --git a/src/mcp/Server.zig b/src/mcp/Server.zig index a25c59aa9..ca0d2735a 100644 --- a/src/mcp/Server.zig +++ b/src/mcp/Server.zig @@ -248,7 +248,6 @@ pub fn handleResourceRead(self: *Self, arena: std.mem.Allocator, req: protocol.R } test "MCP.Server - Integration: synchronous smoke test" { - defer testing.reset(); const allocator = testing.allocator; const app = testing.test_app; diff --git a/src/mcp/protocol.zig b/src/mcp/protocol.zig index 66c66e89b..232d07e62 100644 --- a/src/mcp/protocol.zig +++ b/src/mcp/protocol.zig @@ -186,7 +186,6 @@ pub const JsonEscapingWriter = struct { const testing = @import("../testing.zig"); test "MCP.protocol - request parsing" { - defer testing.reset(); const raw_json = \\{ \\ "jsonrpc": "2.0", @@ -222,7 +221,6 @@ test "MCP.protocol - request parsing" { } test "MCP.protocol - ping request parsing" { - defer testing.reset(); const raw_json = \\{ \\ "jsonrpc": "2.0", @@ -243,7 +241,6 @@ test "MCP.protocol - ping request parsing" { } test "MCP.protocol - response formatting" { - defer testing.reset(); const response = Response{ .id = .{ .integer = 42 }, .result = .{ .string = "success" }, @@ -257,7 +254,6 @@ test "MCP.protocol - response formatting" { } test "MCP.protocol - error formatting" { - defer testing.reset(); const response = Response{ .id = .{ .string = "abc" }, .@"error" = .{ @@ -274,7 +270,6 @@ test "MCP.protocol - error formatting" { } test "MCP.protocol - JsonEscapingWriter" { - defer testing.reset(); var aw: std.Io.Writer.Allocating = .init(testing.arena_allocator); defer aw.deinit(); @@ -288,7 +283,6 @@ test "MCP.protocol - JsonEscapingWriter" { } test "MCP.protocol - Tool serialization" { - defer testing.reset(); const t = Tool{ .name = "test", .inputSchema = minify( diff --git a/src/mcp/router.zig b/src/mcp/router.zig index 61f67fd24..b907b525b 100644 --- a/src/mcp/router.zig +++ b/src/mcp/router.zig @@ -123,7 +123,6 @@ const Server = @import("Server.zig"); const testing = @import("../testing.zig"); test "MCP.router - handleMessage - synchronous unit tests" { - defer testing.reset(); const allocator = testing.allocator; const app = testing.test_app; @@ -168,8 +167,7 @@ test "MCP.router - handleMessage - synchronous unit tests" { // 5. Parse error { - const filter: testing.LogFilter = .init(&.{.mcp}); - defer filter.deinit(); + testing.expectLog(&.{.mcp}); try handleMessage(server, aa, "invalid json"); try testing.expectJson("{\"jsonrpc\": \"2.0\", \"id\": null, \"error\": {\"code\": -32700}}", out_alloc.writer.buffered()); diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index 3ce55fc52..5b815077c 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -271,7 +271,6 @@ const router = @import("router.zig"); const testing = @import("../testing.zig"); test "MCP - evaluate error reporting" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -300,7 +299,6 @@ test "MCP - evaluate error reporting" { } test "MCP - evaluate: top-level return runs in an async wrapper" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -324,7 +322,6 @@ test "MCP - evaluate: top-level return runs in an async wrapper" { } test "MCP - evaluate: top-level await runs in an async wrapper" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -348,7 +345,6 @@ test "MCP - evaluate: top-level await runs in an async wrapper" { } test "MCP - evaluate: let declaration does not leak across calls" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -386,7 +382,6 @@ test "MCP - evaluate: let declaration does not leak across calls" { } test "MCP - evaluate: bare expression still returns its value" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -410,7 +405,6 @@ test "MCP - evaluate: bare expression still returns its value" { } test "MCP - evaluate: object return serializes as JSON" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -434,7 +428,6 @@ test "MCP - evaluate: object return serializes as JSON" { } test "MCP - evaluate: localStorage persists across navigations and is origin-scoped" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_actions.html", &out.writer); defer server.deinit(); @@ -524,7 +517,6 @@ test "MCP - evaluate: localStorage persists across navigations and is origin-sco } test "MCP - evaluate: save= value is readable via lp. in next evaluate" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -561,7 +553,6 @@ test "MCP - evaluate: save= value is readable via lp. in next evaluate" { } test "MCP - evaluate: save= a bare string round-trips without JSON.stringify" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -598,7 +589,6 @@ test "MCP - evaluate: save= a bare string round-trips without JSON.stringify" { } test "MCP - evaluate: lp.* mutations auto-sync between evaluates" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -635,7 +625,6 @@ test "MCP - evaluate: lp.* mutations auto-sync between evaluates" { } test "MCP - evaluate: lp.* survives navigation" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_actions.html", &out.writer); defer server.deinit(); @@ -686,7 +675,6 @@ test "MCP - evaluate: lp.* survives navigation" { } test "MCP - evaluate: delete lp. removes from bridge store" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -737,7 +725,6 @@ test "MCP - evaluate: delete lp. removes from bridge store" { } test "MCP - extract: save= exposes the result as lp." { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_actions.html", &out.writer); defer server.deinit(); @@ -777,7 +764,6 @@ test "MCP - extract: save= exposes the result as lp." { } test "MCP - evaluate: Promise.resolve return value is awaited" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -800,7 +786,6 @@ test "MCP - evaluate: Promise.resolve return value is awaited" { } test "MCP - evaluate: async IIFE resolves to returned value" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -823,7 +808,6 @@ test "MCP - evaluate: async IIFE resolves to returned value" { } test "MCP - evaluate: rejected Promise surfaces as is_error" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -845,7 +829,6 @@ test "MCP - evaluate: rejected Promise surfaces as is_error" { } test "MCP - evaluate: async IIFE without explicit return resolves to empty text" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -868,7 +851,6 @@ test "MCP - evaluate: async IIFE without explicit return resolves to empty text" } test "MCP - evaluate: lp.* mutations inside async IIFE survive to the next evaluate" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -905,7 +887,6 @@ test "MCP - evaluate: lp.* mutations inside async IIFE survive to the next evalu } test "MCP - save rejects unsafe path" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -918,7 +899,6 @@ test "MCP - save rejects unsafe path" { } test "MCP - save writes the script to disk" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -938,7 +918,6 @@ test "MCP - save writes the script to disk" { } test "MCP - tree rejects stale backendNodeId instead of dumping whole document" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -952,7 +931,6 @@ test "MCP - tree rejects stale backendNodeId instead of dumping whole document" } test "MCP - PascalCase argument keys from LLMs are normalized to canonical" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_actions.html", &out.writer); defer server.deinit(); @@ -967,7 +945,6 @@ test "MCP - PascalCase argument keys from LLMs are normalized to canonical" { } test "MCP - Actions: click, fill, scroll, hover, press, selectOption, setChecked" { - defer testing.reset(); const aa = testing.arena_allocator; var out: std.Io.Writer.Allocating = .init(aa); @@ -1105,7 +1082,6 @@ test "MCP - Actions: click, fill, scroll, hover, press, selectOption, setChecked // left the registry intact, and a second click on the same id dereferenced // a freed DOMNode. test "MCP - click that navigates clears node registry" { - defer testing.reset(); const aa = testing.arena_allocator; var out: std.Io.Writer.Allocating = .init(aa); @@ -1131,7 +1107,6 @@ test "MCP - click that navigates clears node registry" { } test "MCP - Actions by selector: hover, selectOption, setChecked" { - defer testing.reset(); const aa = testing.arena_allocator; var out: std.Io.Writer.Allocating = .init(aa); @@ -1200,7 +1175,6 @@ test "MCP - Actions by selector: hover, selectOption, setChecked" { } test "MCP - findElement" { - defer testing.reset(); const aa = testing.arena_allocator; var out: std.Io.Writer.Allocating = .init(aa); @@ -1245,7 +1219,6 @@ test "MCP - findElement" { } test "MCP - waitForSelector: existing element" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage( "http://localhost:9582/src/browser/tests/mcp_wait_for_selector.html", @@ -1263,7 +1236,6 @@ test "MCP - waitForSelector: existing element" { } test "MCP - waitForSelector: delayed element" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage( "http://localhost:9582/src/browser/tests/mcp_wait_for_selector.html", @@ -1281,7 +1253,6 @@ test "MCP - waitForSelector: delayed element" { } test "MCP - waitForSelector: timeout" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage( "http://localhost:9582/src/browser/tests/mcp_wait_for_selector.html", @@ -1302,7 +1273,6 @@ test "MCP - waitForSelector: timeout" { } test "MCP - markdown: full page, selector scope, maxBytes truncation" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_actions.html", &out.writer); defer server.deinit(); @@ -1331,7 +1301,6 @@ test "MCP - markdown: full page, selector scope, maxBytes truncation" { } test "MCP - html: full document, selector subtree, backendNodeId subtree" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_press_form.html", &out.writer); defer server.deinit(); @@ -1357,7 +1326,6 @@ test "MCP - html: full document, selector subtree, backendNodeId subtree" { } test "MCP - waitForScript: truthy returns, falsy times out" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -1377,7 +1345,6 @@ test "MCP - waitForScript: truthy returns, falsy times out" { } test "MCP - press Enter on form input triggers submit (lowercase alias)" { - defer testing.reset(); const aa = testing.arena_allocator; var out: std.Io.Writer.Allocating = .init(aa); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_press_form.html", &out.writer); @@ -1399,7 +1366,6 @@ test "MCP - press Enter on form input triggers submit (lowercase alias)" { } test "MCP - getCookies: defaults to current page, url filter, all flag" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_press_form.htm", &out.writer); defer server.deinit(); @@ -1439,7 +1405,6 @@ test "MCP - getCookies: defaults to current page, url filter, all flag" { } test "MCP - getCookies without a loaded page refuses instead of dumping the jar" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); var server = try Server.init(testing.allocator, testing.test_app, &out.writer); defer server.deinit(); @@ -1456,7 +1421,6 @@ test "MCP - getCookies without a loaded page refuses instead of dumping the jar" } test "MCP - waitForState with bad state surfaces rich error" { - defer testing.reset(); var out: std.Io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); @@ -1472,7 +1436,6 @@ test "MCP - waitForState with bad state surfaces rich error" { } test "MCP - sessions: new, list, attach isolation, close" { - defer testing.reset(); const aa = testing.arena_allocator; var out: std.Io.Writer.Allocating = .init(aa); var server = try Server.init(testing.allocator, testing.test_app, &out.writer); diff --git a/src/network/cache/FsCache.zig b/src/network/cache/FsCache.zig index 58d056716..69bb3fe58 100644 --- a/src/network/cache/FsCache.zig +++ b/src/network/cache/FsCache.zig @@ -554,9 +554,7 @@ test "FsCache: put override" { } test "FsCache: garbage file" { - const LogFilter = @import("../../testing.zig").LogFilter; - const filter: LogFilter = .init(&.{.cache}); - defer filter.deinit(); + @import("../../testing.zig").silenceLog(&.{.cache}); var setup = try setupCache(); defer { diff --git a/src/network/http.zig b/src/network/http.zig index bbcc7e746..be384566b 100644 --- a/src/network/http.zig +++ b/src/network/http.zig @@ -1029,8 +1029,7 @@ test "Headers.set adds a new header and preserves defaults" { } test "opensocketCallback: private IPv4 returns CURL_SOCKET_BAD" { - const lf: testing.LogFilter = .init(&.{.http}); - defer lf.deinit(); + testing.silenceLog(&.{.http}); const filter = IpFilter.init(true, null); var sa = makeSockAddrV4(.{ 127, 0, 0, 1 }); diff --git a/src/script/Runtime.zig b/src/script/Runtime.zig index 79948ad83..1f76d98b0 100644 --- a/src/script/Runtime.zig +++ b/src/script/Runtime.zig @@ -997,7 +997,6 @@ fn terminateRuntimeSoon(runtime: *Runtime) void { } test "agent script runtime: goto and evaluate dispatch through browser tools" { - defer testing.reset(); defer testing.test_session.closeAllPages(); var registry = CDPNode.Registry.init(testing.allocator); @@ -1019,8 +1018,6 @@ test "agent script runtime: goto and evaluate dispatch through browser tools" { } test "agent script runtime: Page must be called with new" { - defer testing.reset(); - var registry = CDPNode.Registry.init(testing.allocator); defer registry.deinit(); @@ -1032,8 +1029,6 @@ test "agent script runtime: Page must be called with new" { } test "agent script runtime: a method on an un-navigated page errors" { - defer testing.reset(); - var registry = CDPNode.Registry.init(testing.allocator); defer registry.deinit(); @@ -1048,7 +1043,6 @@ test "agent script runtime: a method on an un-navigated page errors" { } test "agent script runtime: page.close stales the handle" { - defer testing.reset(); defer testing.test_session.closeAllPages(); var registry = CDPNode.Registry.init(testing.allocator); @@ -1069,7 +1063,6 @@ test "agent script runtime: page.close stales the handle" { } test "agent script runtime: parallel gotos coexist and route per page" { - defer testing.reset(); defer testing.test_session.closeAllPages(); var registry = CDPNode.Registry.init(testing.allocator); @@ -1094,7 +1087,6 @@ test "agent script runtime: parallel gotos coexist and route per page" { } test "agent script runtime: goto resolves $LP_* placeholders" { - defer testing.reset(); defer testing.test_session.closeAllPages(); var registry = CDPNode.Registry.init(testing.allocator); @@ -1118,7 +1110,6 @@ extern fn setenv(name: [*:0]u8, value: [*:0]u8, override: c_int) c_int; extern fn unsetenv(name: [*:0]u8) c_int; test "agent script runtime: goto with invalid arguments rejects instead of crashing" { - defer testing.reset(); defer testing.test_session.closeAllPages(); var registry = CDPNode.Registry.init(testing.allocator); @@ -1138,7 +1129,6 @@ test "agent script runtime: goto with invalid arguments rejects instead of crash } test "agent script runtime: a tool-triggered navigation keeps the handle routable" { - defer testing.reset(); defer testing.test_session.closeAllPages(); var registry = CDPNode.Registry.init(testing.allocator); @@ -1159,7 +1149,6 @@ test "agent script runtime: a tool-triggered navigation keeps the handle routabl } test "agent script runtime: re-goto on the same page object replaces its page" { - defer testing.reset(); defer testing.test_session.closeAllPages(); var registry = CDPNode.Registry.init(testing.allocator); @@ -1178,10 +1167,8 @@ test "agent script runtime: re-goto on the same page object replaces its page" { } test "agent script runtime: a failed navigation rejects the goto promise" { - const filter: testing.LogFilter = .init(&.{.frame}); - defer filter.deinit(); + testing.silenceLog(&.{.frame}); - defer testing.reset(); defer testing.test_session.closeAllPages(); var registry = CDPNode.Registry.init(testing.allocator); @@ -1203,7 +1190,6 @@ test "agent script runtime: a failed navigation rejects the goto promise" { } test "agent script runtime: extract returns a JavaScript object" { - defer testing.reset(); defer testing.test_session.closeAllPages(); var registry = CDPNode.Registry.init(testing.allocator); @@ -1258,7 +1244,6 @@ test "agent script runtime: extract returns a JavaScript object" { } test "agent script runtime: extract tolerates list selectors that match nothing" { - defer testing.reset(); defer testing.test_session.closeAllPages(); var registry = CDPNode.Registry.init(testing.allocator); @@ -1287,7 +1272,6 @@ test "agent script runtime: extract tolerates list selectors that match nothing" } test "agent script runtime: strict-mode scripts can call primitives" { - defer testing.reset(); defer testing.test_session.closeAllPages(); var registry = CDPNode.Registry.init(testing.allocator); @@ -1306,8 +1290,6 @@ test "agent script runtime: strict-mode scripts can call primitives" { } test "agent script runtime: promise microtasks run to completion" { - defer testing.reset(); - var registry = CDPNode.Registry.init(testing.allocator); defer registry.deinit(); @@ -1326,7 +1308,6 @@ test "agent script runtime: promise microtasks run to completion" { } test "agent script runtime: primitives re-entered from argument callbacks stay isolated" { - defer testing.reset(); defer testing.test_session.closeAllPages(); var registry = CDPNode.Registry.init(testing.allocator); @@ -1354,7 +1335,6 @@ test "agent script runtime: primitives re-entered from argument callbacks stay i } test "agent script runtime: terminate interrupts local JavaScript" { - defer testing.reset(); defer testing.test_session.closeAllPages(); var registry = CDPNode.Registry.init(testing.allocator); @@ -1372,7 +1352,6 @@ test "agent script runtime: terminate interrupts local JavaScript" { } test "agent script runtime: agent variables persist and page globals are isolated" { - defer testing.reset(); defer testing.test_session.closeAllPages(); var registry = CDPNode.Registry.init(testing.allocator); @@ -1397,7 +1376,6 @@ test "agent script runtime: agent variables persist and page globals are isolate } test "agent script runtime: page evaluate cannot see agent primitives or bindings" { - defer testing.reset(); defer testing.test_session.closeAllPages(); var registry = CDPNode.Registry.init(testing.allocator); @@ -1417,8 +1395,6 @@ test "agent script runtime: page evaluate cannot see agent primitives or binding } test "agent script runtime: console is available in agent context" { - defer testing.reset(); - var registry = CDPNode.Registry.init(testing.allocator); defer registry.deinit(); @@ -1438,7 +1414,6 @@ test "agent script runtime: console is available in agent context" { } test "agent script runtime: tool errors throw and stop execution" { - defer testing.reset(); defer testing.test_session.closeAllPages(); var registry = CDPNode.Registry.init(testing.allocator); @@ -1465,7 +1440,6 @@ test "agent script runtime: tool errors throw and stop execution" { } test "agent script runtime: builtin argument marshalling (positional + options)" { - defer testing.reset(); defer testing.test_session.closeAllPages(); var registry = CDPNode.Registry.init(testing.allocator); @@ -1524,8 +1498,6 @@ test "agent script runtime: builtin argument marshalling (positional + options)" } test "agent script runtime: top-level await runs in an async wrapper" { - defer testing.reset(); - var registry = CDPNode.Registry.init(testing.allocator); defer registry.deinit(); diff --git a/src/test_runner.zig b/src/test_runner.zig index 083134136..b91ade5c4 100644 --- a/src/test_runner.zig +++ b/src/test_runner.zig @@ -90,7 +90,11 @@ const Runner = struct { Printer.fmt("\r\x1b[0K", .{}); // beginning of line and clear to end of line + var after_each: ?std.builtin.TestFn = null; for (builtin.test_functions) |t| { + if (isAfterEach(t)) { + after_each = t; + } if (isSetup(t)) { t.func() catch |err| { Printer.status(.fail, "\nsetup \"{s}\" failed: {}\n", .{ t.name, err }); @@ -104,7 +108,7 @@ const Runner = struct { const webapi_html_test_mode = self.env.filter == null and self.env.subfilter != null; for (builtin.test_functions) |t| { - if (isSetup(t) or isTeardown(t)) { + if (isSetup(t) or isTeardown(t) or isAfterEach(t)) { continue; } @@ -145,7 +149,16 @@ const Runner = struct { current_test = friendly_name; std.testing.allocator_instance = .{}; - const result = t.func(); + var result = t.func(); + if (after_each) |ae| { + // always runs, so that it can reset state, but it can only + // turn a passing test into a failing one. + ae.func() catch |err| { + if (result) |_| { + result = err; + } else |_| {} + }; + } current_test = null; if (webapi_html_test_mode and self.subtests.items.len == 0) { @@ -256,6 +269,12 @@ const Runner = struct { } }; +// When only part of a test runs, expectations about what the whole test logs +// can't be enforced. +pub fn hasSubfilter() bool { + return RUNNER.env.subfilter != null; +} + pub fn shouldRun(name: []const u8) bool { const sf = RUNNER.env.subfilter orelse return true; return std.mem.indexOf(u8, name, sf) != null; @@ -384,7 +403,7 @@ const Env = struct { .filter = filter, .subfilter = subfilter, .metrics = readEnvBool(map, "METRICS", false), - .verbose = readEnvBool(map, "TEST_VERBOSE", true), + .verbose = readEnvBool(map, "TEST_VERBOSE", false), .fail_first = readEnvBool(map, "TEST_FAIL_FIRST", false), }; } @@ -444,6 +463,10 @@ fn isTeardown(t: std.builtin.TestFn) bool { return std.mem.endsWith(u8, t.name, "tests:afterAll"); } +fn isAfterEach(t: std.builtin.TestFn) bool { + return std.mem.endsWith(u8, t.name, "tests:afterEach"); +} + pub const TrackingAllocator = struct { parent_allocator: Allocator, free_count: usize = 0, diff --git a/src/testing.zig b/src/testing.zig index 92dfc9855..ce3b15460 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -1042,24 +1042,37 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void { unreachable; } -/// LogFilter provides a scoped way to suppress specific log categories during tests. -/// This is useful for tests that trigger expected errors or warnings. -pub const LogFilter = struct { - old_filter: [log.num_scopes]bool, +/// Declares the log lines a test expects to emit: one entry per line, so +/// `&.{ .js, .js, .http }` covers two `js` lines and one `http` line. +pub const expectLog = log.expectLog; - /// Sets the log filter to suppress the specified scope(s). - /// Returns a LogFilter that should be deinitialized to restore previous filters. - pub fn init(comptime scopes: []const log.Scope) LogFilter { - comptime std.debug.assert(@TypeOf(scopes) == []const log.Scope); - const old_filter = log.opts.scope_enabled; - inline for (scopes) |scope| { - log.opts.scope_enabled[@intFromEnum(scope)] = false; +/// Suppresses every line from `scopes` for the rest of the test. +pub fn silenceLog(comptime scopes: []const log.Scope) void { + inline for (scopes) |scope| { + log.opts.scope_enabled[@intFromEnum(scope)] = false; + } +} + +test "tests:afterEach" { + defer reset(); + const unmet = log.resetTestState(); + if (@import("root").hasSubfilter()) { + // only part of the test ran, so expectations about what it logs + // don't hold. + return; + } + + var failed = false; + for (unmet, 0..) |count, i| { + if (count == 0) { + continue; } - return .{ .old_filter = old_filter }; + failed = true; + const scope: log.Scope = @enumFromInt(i); + std.debug.print("expected {d} more {s} log line(s)\n", .{ count, @tagName(scope) }); } - /// Restores the log filters to their previous state. - pub fn deinit(self: LogFilter) void { - log.opts.scope_enabled = self.old_filter; + if (failed) { + return error.UnmetLogExpectation; } -}; +}