Removes CDPT (generic CDP)

CDPT used to be a generic so that we could inject Browser, Session, Page and
Client. At some point, it [thankfully] became a generic only to inject Client.

This commit removes the generic and bakes the *Server.Client instance in CDP.
It uses a socketpair for testing.

BrowserContext is still generic, but that's generic for a very different reason
and, while I'd like to remove that generic too, it belongs in a different PR.
This commit is contained in:
Karl Seguin
2026-03-25 17:43:30 +08:00
parent d517488158
commit 0dd0495ab8
14 changed files with 456 additions and 396 deletions

View File

@@ -27,7 +27,7 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const log = @import("log.zig"); const log = @import("log.zig");
const App = @import("App.zig"); const App = @import("App.zig");
const Config = @import("Config.zig"); const Config = @import("Config.zig");
const CDP = @import("cdp/cdp.zig").CDP; const CDP = @import("cdp/CDP.zig");
const Net = @import("network/websocket.zig"); const Net = @import("network/websocket.zig");
const HttpClient = @import("browser/HttpClient.zig"); const HttpClient = @import("browser/HttpClient.zig");
@@ -212,7 +212,7 @@ pub const Client = struct {
http: *HttpClient, http: *HttpClient,
ws: Net.WsConnection, ws: Net.WsConnection,
fn init( pub fn init(
socket: posix.socket_t, socket: posix.socket_t,
allocator: Allocator, allocator: Allocator,
app: *App, app: *App,
@@ -250,7 +250,7 @@ pub const Client = struct {
self.ws.shutdown(); self.ws.shutdown();
} }
fn deinit(self: *Client) void { pub fn deinit(self: *Client) void {
switch (self.mode) { switch (self.mode) {
.cdp => |*cdp| cdp.deinit(), .cdp => |*cdp| cdp.deinit(),
.http => {}, .http => {},
@@ -461,7 +461,7 @@ pub const Client = struct {
fn upgradeConnection(self: *Client, request: []u8) !void { fn upgradeConnection(self: *Client, request: []u8) !void {
try self.ws.upgrade(request); try self.ws.upgrade(request);
self.mode = .{ .cdp = try CDP.init(self.app, self.http, self) }; self.mode = .{ .cdp = try CDP.init(self) };
} }
fn writeHTTPErrorResponse(self: *Client, comptime status: u16, comptime body: []const u8) void { fn writeHTTPErrorResponse(self: *Client, comptime status: u16, comptime body: []const u8) void {

View File

@@ -22,72 +22,71 @@ const lp = @import("lightpanda");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const json = std.json; const json = std.json;
const log = @import("../log.zig"); const Incrementing = @import("id.zig").Incrementing;
const js = @import("../browser/js/js.zig");
const log = @import("../log.zig");
const App = @import("../App.zig"); const App = @import("../App.zig");
const Notification = @import("../Notification.zig");
const Client = @import("../Server.zig").Client;
const js = @import("../browser/js/js.zig");
const Browser = @import("../browser/Browser.zig"); const Browser = @import("../browser/Browser.zig");
const Session = @import("../browser/Session.zig"); const Session = @import("../browser/Session.zig");
const HttpClient = @import("../browser/HttpClient.zig");
const Page = @import("../browser/Page.zig"); const Page = @import("../browser/Page.zig");
const Incrementing = @import("id.zig").Incrementing;
const Notification = @import("../Notification.zig");
const InterceptState = @import("domains/fetch.zig").InterceptState;
const Mime = @import("../browser/Mime.zig"); const Mime = @import("../browser/Mime.zig");
const HttpClient = @import("../browser/HttpClient.zig");
const InterceptState = @import("domains/fetch.zig").InterceptState;
pub const URL_BASE = "chrome://newtab/"; pub const URL_BASE = "chrome://newtab/";
const IS_DEBUG = @import("builtin").mode == .Debug; const IS_DEBUG = @import("builtin").mode == .Debug;
pub const CDP = CDPT(struct {
const Client = *@import("../Server.zig").Client;
});
const SessionIdGen = Incrementing(u32, "SID");
const TargetIdGen = Incrementing(u32, "TID"); const TargetIdGen = Incrementing(u32, "TID");
const SessionIdGen = Incrementing(u32, "SID");
const BrowserContextIdGen = Incrementing(u32, "BID"); const BrowserContextIdGen = Incrementing(u32, "BID");
// Generic so that we can inject mocks into it. // Generic so that we can inject mocks into it.
pub fn CDPT(comptime TypeProvider: type) type { const CDP = @This();
return struct {
// Used for sending message to the client and closing on error
client: TypeProvider.Client,
allocator: Allocator, // Used for sending message to the client and closing on error
client: *Client,
// The active browser allocator: Allocator,
browser: Browser,
// when true, any target creation must be attached. // The active browser
target_auto_attach: bool = false, browser: Browser,
target_id_gen: TargetIdGen = .{}, // when true, any target creation must be attached.
session_id_gen: SessionIdGen = .{}, target_auto_attach: bool = false,
browser_context_id_gen: BrowserContextIdGen = .{},
browser_context: ?BrowserContext(Self), target_id_gen: TargetIdGen = .{},
session_id_gen: SessionIdGen = .{},
browser_context_id_gen: BrowserContextIdGen = .{},
// Re-used arena for processing a message. We're assuming that we're getting browser_context: ?BrowserContext(CDP),
// 1 message at a time.
message_arena: std.heap.ArenaAllocator,
// Used for processing notifications within a browser context. // Re-used arena for processing a message. We're assuming that we're getting
notification_arena: std.heap.ArenaAllocator, // 1 message at a time.
message_arena: std.heap.ArenaAllocator,
// Valid for 1 page navigation (what CDP calls a "renderer") // Used for processing notifications within a browser context.
page_arena: std.heap.ArenaAllocator, notification_arena: std.heap.ArenaAllocator,
// Valid for the entire lifetime of the BrowserContext. Should minimize // Valid for 1 page navigation (what CDP calls a "renderer")
// (or altogether elimiate) our use of this. page_arena: std.heap.ArenaAllocator,
browser_context_arena: std.heap.ArenaAllocator,
const Self = @This(); // Valid for the entire lifetime of the BrowserContext. Should minimize
// (or altogether elimiate) our use of this.
browser_context_arena: std.heap.ArenaAllocator,
pub fn init(app: *App, http_client: *HttpClient, client: TypeProvider.Client) !Self { pub fn init(client: *Client) !CDP {
const app = client.app;
const allocator = app.allocator; const allocator = app.allocator;
const browser = try Browser.init(app, .{ const browser = try Browser.init(app, .{
.env = .{ .with_inspector = true }, .env = .{ .with_inspector = true },
.http_client = http_client, .http_client = client.http,
}); });
errdefer browser.deinit(); errdefer browser.deinit();
@@ -101,9 +100,9 @@ pub fn CDPT(comptime TypeProvider: type) type {
.notification_arena = std.heap.ArenaAllocator.init(allocator), .notification_arena = std.heap.ArenaAllocator.init(allocator),
.browser_context_arena = std.heap.ArenaAllocator.init(allocator), .browser_context_arena = std.heap.ArenaAllocator.init(allocator),
}; };
} }
pub fn deinit(self: *Self) void { pub fn deinit(self: *CDP) void {
if (self.browser_context) |*bc| { if (self.browser_context) |*bc| {
bc.deinit(); bc.deinit();
} }
@@ -112,39 +111,39 @@ pub fn CDPT(comptime TypeProvider: type) type {
self.message_arena.deinit(); self.message_arena.deinit();
self.notification_arena.deinit(); self.notification_arena.deinit();
self.browser_context_arena.deinit(); self.browser_context_arena.deinit();
} }
pub fn handleMessage(self: *Self, msg: []const u8) bool { pub fn handleMessage(self: *CDP, msg: []const u8) bool {
// if there's an error, it's already been logged // if there's an error, it's already been logged
self.processMessage(msg) catch return false; self.processMessage(msg) catch return false;
return true; return true;
} }
pub fn processMessage(self: *Self, msg: []const u8) !void { pub fn processMessage(self: *CDP, msg: []const u8) !void {
const arena = &self.message_arena; const arena = &self.message_arena;
defer _ = arena.reset(.{ .retain_with_limit = 1024 * 16 }); defer _ = arena.reset(.{ .retain_with_limit = 1024 * 16 });
return self.dispatch(arena.allocator(), self, msg); return self.dispatch(arena.allocator(), self, msg);
} }
// @newhttp // @newhttp
// A bit hacky right now. The main server loop doesn't unblock for // A bit hacky right now. The main server loop doesn't unblock for
// scheduled task. So we run this directly in order to process any // scheduled task. So we run this directly in order to process any
// timeouts (or http events) which are ready to be processed. // timeouts (or http events) which are ready to be processed.
pub fn pageWait(self: *Self, ms: u32) !Session.Runner.CDPWaitResult { pub fn pageWait(self: *CDP, ms: u32) !Session.Runner.CDPWaitResult {
const session = &(self.browser.session orelse return error.NoPage); const session = &(self.browser.session orelse return error.NoPage);
var runner = try session.runner(.{}); var runner = try session.runner(.{});
return runner.waitCDP(.{ .ms = ms }); return runner.waitCDP(.{ .ms = ms });
} }
// Called from above, in processMessage which handles client messages // Called from above, in processMessage which handles client messages
// but can also be called internally. For example, Target.sendMessageToTarget // but can also be called internally. For example, Target.sendMessageToTarget
// calls back into dispatch to capture the response. // calls back into dispatch to capture the response.
pub fn dispatch(self: *Self, arena: Allocator, sender: anytype, str: []const u8) !void { pub fn dispatch(self: *CDP, arena: Allocator, sender: anytype, str: []const u8) !void {
const input = json.parseFromSliceLeaky(InputMessage, arena, str, .{ const input = json.parseFromSliceLeaky(InputMessage, arena, str, .{
.ignore_unknown_fields = true, .ignore_unknown_fields = true,
}) catch return error.InvalidJSON; }) catch return error.InvalidJSON;
var command = Command(Self, @TypeOf(sender)){ var command = Command(CDP, @TypeOf(sender)){
.input = .{ .input = .{
.json = str, .json = str,
.id = input.id, .id = input.id,
@@ -177,19 +176,19 @@ pub fn CDPT(comptime TypeProvider: type) type {
command.sendError(-31998, @errorName(err), .{}) catch return err; command.sendError(-31998, @errorName(err), .{}) catch return err;
}; };
} }
} }
// A CDP session isn't 100% fully driven by the driver. There's are // A CDP session isn't 100% fully driven by the driver. There's are
// independent actions that the browser is expected to take. For example // independent actions that the browser is expected to take. For example
// Puppeteer expects the browser to startup a tab and thus have existing // Puppeteer expects the browser to startup a tab and thus have existing
// targets. // targets.
// To this end, we create a [very] dummy BrowserContext, Target and // To this end, we create a [very] dummy BrowserContext, Target and
// Session. There isn't actually a BrowserContext, just a special id. // Session. There isn't actually a BrowserContext, just a special id.
// When messages are received with the "STARTUP" sessionId, we do // When messages are received with the "STARTUP" sessionId, we do
// "special" handling - the bare minimum we need to do until the driver // "special" handling - the bare minimum we need to do until the driver
// switches to a real BrowserContext. // switches to a real BrowserContext.
// (I can imagine this logic will become driver-specific) // (I can imagine this logic will become driver-specific)
fn dispatchStartupCommand(command: anytype, method: []const u8) !void { fn dispatchStartupCommand(command: anytype, method: []const u8) !void {
// Stagehand parses the response and error if we don't return a // Stagehand parses the response and error if we don't return a
// correct one for Page.getFrameTree on startup call. // correct one for Page.getFrameTree on startup call.
if (std.mem.eql(u8, method, "Page.getFrameTree")) { if (std.mem.eql(u8, method, "Page.getFrameTree")) {
@@ -198,9 +197,9 @@ pub fn CDPT(comptime TypeProvider: type) type {
} }
return command.sendResult(null, .{}); return command.sendResult(null, .{});
} }
fn dispatchCommand(command: anytype, method: []const u8) !void { fn dispatchCommand(command: anytype, method: []const u8) !void {
const domain = blk: { const domain = blk: {
const i = std.mem.indexOfScalarPos(u8, method, 0, '.') orelse { const i = std.mem.indexOfScalarPos(u8, method, 0, '.') orelse {
return error.InvalidMethod; return error.InvalidMethod;
@@ -262,28 +261,28 @@ pub fn CDPT(comptime TypeProvider: type) type {
} }
return error.UnknownDomain; return error.UnknownDomain;
} }
fn isValidSessionId(self: *const Self, input_session_id: []const u8) bool { fn isValidSessionId(self: *const CDP, input_session_id: []const u8) bool {
const browser_context = &(self.browser_context orelse return false); const browser_context = &(self.browser_context orelse return false);
const session_id = browser_context.session_id orelse return false; const session_id = browser_context.session_id orelse return false;
return std.mem.eql(u8, session_id, input_session_id); return std.mem.eql(u8, session_id, input_session_id);
} }
pub fn createBrowserContext(self: *Self) ![]const u8 { pub fn createBrowserContext(self: *CDP) ![]const u8 {
if (self.browser_context != null) { if (self.browser_context != null) {
return error.AlreadyExists; return error.AlreadyExists;
} }
const id = self.browser_context_id_gen.next(); const id = self.browser_context_id_gen.next();
self.browser_context = @as(BrowserContext(Self), undefined); self.browser_context = @as(BrowserContext(CDP), undefined);
const browser_context = &self.browser_context.?; const browser_context = &self.browser_context.?;
try BrowserContext(Self).init(browser_context, id, self); try BrowserContext(CDP).init(browser_context, id, self);
return id; return id;
} }
pub fn disposeBrowserContext(self: *Self, browser_context_id: []const u8) bool { pub fn disposeBrowserContext(self: *CDP, browser_context_id: []const u8) bool {
const bc = &(self.browser_context orelse return false); const bc = &(self.browser_context orelse return false);
if (std.mem.eql(u8, bc.id, browser_context_id) == false) { if (std.mem.eql(u8, bc.id, browser_context_id) == false) {
return false; return false;
@@ -292,25 +291,23 @@ pub fn CDPT(comptime TypeProvider: type) type {
self.browser.closeSession(); self.browser.closeSession();
self.browser_context = null; self.browser_context = null;
return true; return true;
} }
const SendEventOpts = struct { const SendEventOpts = struct {
session_id: ?[]const u8 = null, session_id: ?[]const u8 = null,
}; };
pub fn sendEvent(self: *Self, method: []const u8, p: anytype, opts: SendEventOpts) !void { pub fn sendEvent(self: *CDP, method: []const u8, p: anytype, opts: SendEventOpts) !void {
return self.sendJSON(.{ return self.sendJSON(.{
.method = method, .method = method,
.params = if (comptime @typeInfo(@TypeOf(p)) == .null) struct {}{} else p, .params = if (comptime @typeInfo(@TypeOf(p)) == .null) struct {}{} else p,
.sessionId = opts.session_id, .sessionId = opts.session_id,
}); });
} }
pub fn sendJSON(self: *Self, message: anytype) !void { pub fn sendJSON(self: *CDP, message: anytype) !void {
return self.client.sendJSON(message, .{ return self.client.sendJSON(message, .{
.emit_null_optional_fields = false, .emit_null_optional_fields = false,
}); });
}
};
} }
pub fn BrowserContext(comptime CDP_T: type) type { pub fn BrowserContext(comptime CDP_T: type) type {
@@ -958,7 +955,7 @@ fn asUint(comptime T: type, comptime string: []const u8) T {
const testing = @import("testing.zig"); const testing = @import("testing.zig");
test "cdp: invalid json" { test "cdp: invalid json" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
try testing.expectError(error.InvalidJSON, ctx.processMessage("invalid")); try testing.expectError(error.InvalidJSON, ctx.processMessage("invalid"));
@@ -983,7 +980,7 @@ test "cdp: invalid json" {
} }
test "cdp: invalid sessionId" { test "cdp: invalid sessionId" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
{ {
@@ -1008,7 +1005,7 @@ test "cdp: invalid sessionId" {
} }
test "cdp: STARTUP sessionId" { test "cdp: STARTUP sessionId" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
{ {
@@ -1021,13 +1018,13 @@ test "cdp: STARTUP sessionId" {
// we have a brower context but no session_id // we have a brower context but no session_id
_ = try ctx.loadBrowserContext(.{}); _ = try ctx.loadBrowserContext(.{});
try ctx.processMessage(.{ .id = 3, .method = "Hi", .sessionId = "STARTUP" }); try ctx.processMessage(.{ .id = 3, .method = "Hi", .sessionId = "STARTUP" });
try ctx.expectSentResult(null, .{ .id = 3, .index = 0, .session_id = "STARTUP" }); try ctx.expectSentResult(null, .{ .id = 3, .index = 1, .session_id = "STARTUP" });
} }
{ {
// we have a brower context with a different session_id // we have a brower context with a different session_id
_ = try ctx.loadBrowserContext(.{ .session_id = "SESS-2" }); _ = try ctx.loadBrowserContext(.{ .session_id = "SESS-2" });
try ctx.processMessage(.{ .id = 4, .method = "Hi", .sessionId = "STARTUP" }); try ctx.processMessage(.{ .id = 4, .method = "Hi", .sessionId = "STARTUP" });
try ctx.expectSentResult(null, .{ .id = 4, .index = 0, .session_id = "STARTUP" }); try ctx.expectSentResult(null, .{ .id = 4, .index = 2, .session_id = "STARTUP" });
} }
} }

View File

@@ -112,7 +112,7 @@ fn resetPermissions(cmd: anytype) !void {
const testing = @import("../testing.zig"); const testing = @import("../testing.zig");
test "cdp.browser: getVersion" { test "cdp.browser: getVersion" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
try ctx.processMessage(.{ try ctx.processMessage(.{
@@ -131,7 +131,7 @@ test "cdp.browser: getVersion" {
} }
test "cdp.browser: getWindowForTarget" { test "cdp.browser: getWindowForTarget" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
try ctx.processMessage(.{ try ctx.processMessage(.{

View File

@@ -547,7 +547,7 @@ fn requestNode(cmd: anytype) !void {
const testing = @import("../testing.zig"); const testing = @import("../testing.zig");
test "cdp.dom: getSearchResults unknown search id" { test "cdp.dom: getSearchResults unknown search id" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
try ctx.processMessage(.{ try ctx.processMessage(.{
@@ -559,7 +559,7 @@ test "cdp.dom: getSearchResults unknown search id" {
} }
test "cdp.dom: search flow" { test "cdp.dom: search flow" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
_ = try ctx.loadBrowserContext(.{ .id = "BID-A", .url = "cdp/dom1.html" }); _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .url = "cdp/dom1.html" });
@@ -614,7 +614,7 @@ test "cdp.dom: search flow" {
} }
test "cdp.dom: querySelector unknown search id" { test "cdp.dom: querySelector unknown search id" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
_ = try ctx.loadBrowserContext(.{ .id = "BID-A", .url = "cdp/dom1.html" }); _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .url = "cdp/dom1.html" });
@@ -635,7 +635,7 @@ test "cdp.dom: querySelector unknown search id" {
} }
test "cdp.dom: querySelector Node not found" { test "cdp.dom: querySelector Node not found" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
_ = try ctx.loadBrowserContext(.{ .id = "BID-A", .url = "cdp/dom1.html" }); _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .url = "cdp/dom1.html" });
@@ -663,7 +663,7 @@ test "cdp.dom: querySelector Node not found" {
} }
test "cdp.dom: querySelector Nodes found" { test "cdp.dom: querySelector Nodes found" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
_ = try ctx.loadBrowserContext(.{ .id = "BID-A", .url = "cdp/dom2.html" }); _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .url = "cdp/dom2.html" });
@@ -693,7 +693,7 @@ test "cdp.dom: querySelector Nodes found" {
} }
test "cdp.dom: getBoxModel" { test "cdp.dom: getBoxModel" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
_ = try ctx.loadBrowserContext(.{ .id = "BID-A", .url = "cdp/dom2.html" }); _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .url = "cdp/dom2.html" });

View File

@@ -260,7 +260,7 @@ fn waitForSelector(cmd: anytype) !void {
const testing = @import("../testing.zig"); const testing = @import("../testing.zig");
test "cdp.lp: getMarkdown" { test "cdp.lp: getMarkdown" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
const bc = try ctx.loadBrowserContext(.{}); const bc = try ctx.loadBrowserContext(.{});
@@ -271,12 +271,12 @@ test "cdp.lp: getMarkdown" {
.method = "LP.getMarkdown", .method = "LP.getMarkdown",
}); });
const result = ctx.client.?.sent.items[0].object.get("result").?.object; const result = (try ctx.getSentMessage(0)).?.object.get("result").?.object;
try testing.expect(result.get("markdown") != null); try testing.expect(result.get("markdown") != null);
} }
test "cdp.lp: getInteractiveElements" { test "cdp.lp: getInteractiveElements" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
const bc = try ctx.loadBrowserContext(.{}); const bc = try ctx.loadBrowserContext(.{});
@@ -287,13 +287,13 @@ test "cdp.lp: getInteractiveElements" {
.method = "LP.getInteractiveElements", .method = "LP.getInteractiveElements",
}); });
const result = ctx.client.?.sent.items[0].object.get("result").?.object; const result = (try ctx.getSentMessage(0)).?.object.get("result").?.object;
try testing.expect(result.get("elements") != null); try testing.expect(result.get("elements") != null);
try testing.expect(result.get("nodeIds") != null); try testing.expect(result.get("nodeIds") != null);
} }
test "cdp.lp: getStructuredData" { test "cdp.lp: getStructuredData" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
const bc = try ctx.loadBrowserContext(.{}); const bc = try ctx.loadBrowserContext(.{});
@@ -304,12 +304,12 @@ test "cdp.lp: getStructuredData" {
.method = "LP.getStructuredData", .method = "LP.getStructuredData",
}); });
const result = ctx.client.?.sent.items[0].object.get("result").?.object; const result = (try ctx.getSentMessage(0)).?.object.get("result").?.object;
try testing.expect(result.get("structuredData") != null); try testing.expect(result.get("structuredData") != null);
} }
test "cdp.lp: action tools" { test "cdp.lp: action tools" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
const bc = try ctx.loadBrowserContext(.{}); const bc = try ctx.loadBrowserContext(.{});
@@ -370,7 +370,7 @@ test "cdp.lp: action tools" {
} }
test "cdp.lp: waitForSelector" { test "cdp.lp: waitForSelector" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
const bc = try ctx.loadBrowserContext(.{}); const bc = try ctx.loadBrowserContext(.{});
@@ -386,9 +386,8 @@ test "cdp.lp: waitForSelector" {
.method = "LP.waitForSelector", .method = "LP.waitForSelector",
.params = .{ .selector = "#existing", .timeout = 2000 }, .params = .{ .selector = "#existing", .timeout = 2000 },
}); });
var result = ctx.client.?.sent.items[0].object.get("result").?.object; var result = (try ctx.getSentMessage(0)).?.object.get("result").?.object;
try testing.expect(result.get("backendNodeId") != null); try testing.expect(result.get("backendNodeId") != null);
ctx.client.?.sent.clearRetainingCapacity();
// 2. Delayed element // 2. Delayed element
try ctx.processMessage(.{ try ctx.processMessage(.{
@@ -396,9 +395,8 @@ test "cdp.lp: waitForSelector" {
.method = "LP.waitForSelector", .method = "LP.waitForSelector",
.params = .{ .selector = "#delayed", .timeout = 5000 }, .params = .{ .selector = "#delayed", .timeout = 5000 },
}); });
result = ctx.client.?.sent.items[0].object.get("result").?.object; result = (try ctx.getSentMessage(1)).?.object.get("result").?.object;
try testing.expect(result.get("backendNodeId") != null); try testing.expect(result.get("backendNodeId") != null);
ctx.client.?.sent.clearRetainingCapacity();
// 3. Timeout error // 3. Timeout error
try ctx.processMessage(.{ try ctx.processMessage(.{
@@ -406,6 +404,6 @@ test "cdp.lp: waitForSelector" {
.method = "LP.waitForSelector", .method = "LP.waitForSelector",
.params = .{ .selector = "#nonexistent", .timeout = 100 }, .params = .{ .selector = "#nonexistent", .timeout = 100 },
}); });
const err_obj = ctx.client.?.sent.items[0].object.get("error").?.object; const err_obj = (try ctx.getSentMessage(2)).?.object.get("error").?.object;
try testing.expect(err_obj.get("code") != null); try testing.expect(err_obj.get("code") != null);
} }

View File

@@ -439,7 +439,7 @@ fn idFromRequestId(request_id: []const u8) !u64 {
const testing = @import("../testing.zig"); const testing = @import("../testing.zig");
test "cdp.network setExtraHTTPHeaders" { test "cdp.network setExtraHTTPHeaders" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
_ = try ctx.loadBrowserContext(.{ .id = "NID-A", .session_id = "NESI-A" }); _ = try ctx.loadBrowserContext(.{ .id = "NID-A", .session_id = "NESI-A" });
@@ -465,7 +465,7 @@ test "cdp.Network: cookies" {
const ResCookie = CdpStorage.ResCookie; const ResCookie = CdpStorage.ResCookie;
const CdpCookie = CdpStorage.CdpCookie; const CdpCookie = CdpStorage.CdpCookie;
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
_ = try ctx.loadBrowserContext(.{ .id = "BID-S" }); _ = try ctx.loadBrowserContext(.{ .id = "BID-S" });

View File

@@ -642,7 +642,7 @@ fn getLayoutMetrics(cmd: anytype) !void {
const testing = @import("../testing.zig"); const testing = @import("../testing.zig");
test "cdp.page: getFrameTree" { test "cdp.page: getFrameTree" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
{ {
@@ -712,7 +712,7 @@ test "cdp.page: captureScreenshot" {
const filter: LogFilter = .init(&.{.not_implemented}); const filter: LogFilter = .init(&.{.not_implemented});
defer filter.deinit(); defer filter.deinit();
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
{ {
try ctx.processMessage(.{ .id = 10, .method = "Page.captureScreenshot", .params = .{ .format = "jpg" } }); try ctx.processMessage(.{ .id = 10, .method = "Page.captureScreenshot", .params = .{ .format = "jpg" } });
@@ -728,7 +728,7 @@ test "cdp.page: captureScreenshot" {
} }
test "cdp.page: getLayoutMetrics" { test "cdp.page: getLayoutMetrics" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
_ = try ctx.loadBrowserContext(.{ .id = "BID-9", .url = "hi.html", .target_id = "FID-000000000X".* }); _ = try ctx.loadBrowserContext(.{ .id = "BID-9", .url = "hi.html", .target_id = "FID-000000000X".* });

View File

@@ -44,7 +44,7 @@ fn setIgnoreCertificateErrors(cmd: anytype) !void {
const testing = @import("../testing.zig"); const testing = @import("../testing.zig");
test "cdp.Security: setIgnoreCertificateErrors" { test "cdp.Security: setIgnoreCertificateErrors" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
_ = try ctx.loadBrowserContext(.{ .id = "BID-9" }); _ = try ctx.loadBrowserContext(.{ .id = "BID-9" });

View File

@@ -243,7 +243,7 @@ pub fn writeCookie(cookie: *const Cookie, w: anytype) !void {
const testing = @import("../testing.zig"); const testing = @import("../testing.zig");
test "cdp.Storage: cookies" { test "cdp.Storage: cookies" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
_ = try ctx.loadBrowserContext(.{ .id = "BID-S" }); _ = try ctx.loadBrowserContext(.{ .id = "BID-S" });

View File

@@ -512,7 +512,7 @@ const TargetInfo = struct {
const testing = @import("../testing.zig"); const testing = @import("../testing.zig");
test "cdp.target: getBrowserContexts" { test "cdp.target: getBrowserContexts" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
// { // {
@@ -536,7 +536,7 @@ test "cdp.target: getBrowserContexts" {
} }
test "cdp.target: createBrowserContext" { test "cdp.target: createBrowserContext" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
{ {
@@ -554,7 +554,7 @@ test "cdp.target: createBrowserContext" {
} }
test "cdp.target: disposeBrowserContext" { test "cdp.target: disposeBrowserContext" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
{ {
@@ -585,7 +585,7 @@ test "cdp.target: disposeBrowserContext" {
test "cdp.target: createTarget" { test "cdp.target: createTarget" {
{ {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about:blank" } }); try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about:blank" } });
@@ -595,7 +595,7 @@ test "cdp.target: createTarget" {
} }
{ {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
// active auto attach to get the Target.attachedToTarget event. // active auto attach to get the Target.attachedToTarget event.
try ctx.processMessage(.{ .id = 9, .method = "Target.setAutoAttach", .params = .{ .autoAttach = true, .waitForDebuggerOnStart = false } }); try ctx.processMessage(.{ .id = 9, .method = "Target.setAutoAttach", .params = .{ .autoAttach = true, .waitForDebuggerOnStart = false } });
@@ -607,7 +607,7 @@ test "cdp.target: createTarget" {
try ctx.expectSentEvent("Target.attachedToTarget", .{ .sessionId = bc.session_id.?, .targetInfo = .{ .url = "about:blank", .title = "", .attached = true, .type = "page", .canAccessOpener = false, .browserContextId = bc.id, .targetId = bc.target_id.? } }, .{}); try ctx.expectSentEvent("Target.attachedToTarget", .{ .sessionId = bc.session_id.?, .targetInfo = .{ .url = "about:blank", .title = "", .attached = true, .type = "page", .canAccessOpener = false, .browserContextId = bc.id, .targetId = bc.target_id.? } }, .{});
} }
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" }); const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
{ {
@@ -624,7 +624,7 @@ test "cdp.target: createTarget" {
} }
test "cdp.target: closeTarget" { test "cdp.target: closeTarget" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
{ {
@@ -655,7 +655,7 @@ test "cdp.target: closeTarget" {
} }
test "cdp.target: attachToTarget" { test "cdp.target: attachToTarget" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
{ {
@@ -686,7 +686,7 @@ test "cdp.target: attachToTarget" {
} }
test "cdp.target: getTargetInfo" { test "cdp.target: getTargetInfo" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
{ {
@@ -737,7 +737,7 @@ test "cdp.target: getTargetInfo" {
} }
test "cdp.target: issue#474: attach to just created target" { test "cdp.target: issue#474: attach to just created target" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" }); const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
{ {
@@ -752,7 +752,7 @@ test "cdp.target: issue#474: attach to just created target" {
} }
test "cdp.target: detachFromTarget" { test "cdp.target: detachFromTarget" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" }); const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
{ {
@@ -775,19 +775,19 @@ test "cdp.target: detachFromTarget" {
} }
test "cdp.target: detachFromTarget without session" { test "cdp.target: detachFromTarget without session" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
_ = try ctx.loadBrowserContext(.{ .id = "BID-9" }); _ = try ctx.loadBrowserContext(.{ .id = "BID-9" });
{ {
// detach when no session is attached should not send event // detach when no session is attached should not send event
try ctx.processMessage(.{ .id = 10, .method = "Target.detachFromTarget" }); try ctx.processMessage(.{ .id = 10, .method = "Target.detachFromTarget" });
try ctx.expectSentResult(null, .{ .id = 10 }); try ctx.expectSentResult(null, .{ .id = 10 });
try ctx.expectSentCount(0); try ctx.expectSentCount(1);
} }
} }
test "cdp.target: setAutoAttach false sends detachedFromTarget" { test "cdp.target: setAutoAttach false sends detachedFromTarget" {
var ctx = testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" }); const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
{ {

View File

@@ -18,12 +18,14 @@
const std = @import("std"); const std = @import("std");
const json = std.json; const json = std.json;
const posix = std.posix;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
const Testing = @This(); const Testing = @This();
const main = @import("cdp.zig"); const CDP = @import("CDP.zig");
const Server = @import("../Server.zig");
const base = @import("../testing.zig"); const base = @import("../testing.zig");
pub const allocator = base.allocator; pub const allocator = base.allocator;
@@ -35,61 +37,27 @@ pub const expectEqualSlices = base.expectEqualSlices;
pub const pageTest = base.pageTest; pub const pageTest = base.pageTest;
pub const newString = base.newString; pub const newString = base.newString;
const Client = struct {
allocator: Allocator,
send_arena: ArenaAllocator,
sent: std.ArrayList(json.Value) = .{},
serialized: std.ArrayList([]const u8) = .{},
fn init(alloc: Allocator) Client {
return .{
.allocator = alloc,
.send_arena = ArenaAllocator.init(alloc),
};
}
pub fn sendAllocator(self: *Client) Allocator {
return self.send_arena.allocator();
}
pub fn sendJSON(self: *Client, message: anytype, opts: json.Stringify.Options) !void {
var opts_copy = opts;
opts_copy.whitespace = .indent_2;
const serialized = try json.Stringify.valueAlloc(self.allocator, message, opts_copy);
try self.serialized.append(self.allocator, serialized);
const value = try json.parseFromSliceLeaky(json.Value, self.allocator, serialized, .{});
try self.sent.append(self.allocator, value);
}
pub fn sendJSONRaw(self: *Client, buf: std.ArrayList(u8)) !void {
const value = try json.parseFromSliceLeaky(json.Value, self.allocator, buf.items, .{});
try self.sent.append(self.allocator, value);
}
};
const TestCDP = main.CDPT(struct {
pub const Client = *Testing.Client;
});
const TestContext = struct { const TestContext = struct {
client: ?Client = null, read_at: usize = 0,
cdp_: ?TestCDP = null, read_buf: [1024 * 32]u8 = undefined,
arena: ArenaAllocator, cdp_: ?CDP = null,
client: Server.Client,
socket: posix.socket_t,
received: std.ArrayList(json.Value) = .empty,
received_raw: std.ArrayList([]const u8) = .empty,
pub fn deinit(self: *TestContext) void { pub fn deinit(self: *TestContext) void {
if (self.cdp_) |*c| { if (self.cdp_) |*c| {
c.deinit(); c.deinit();
} }
self.arena.deinit(); self.client.deinit();
posix.close(self.socket);
base.reset();
} }
pub fn cdp(self: *TestContext) *TestCDP { pub fn cdp(self: *TestContext) *CDP {
if (self.cdp_ == null) { if (self.cdp_ == null) {
self.client = Client.init(self.arena.allocator()); self.cdp_ = CDP.init(&self.client) catch |err| @panic(@errorName(err));
// Don't use the arena here. We want to detect leaks in CDP.
// The arena is only for test-specific stuff
self.cdp_ = TestCDP.init(base.test_app, base.test_http, &self.client.?) catch unreachable;
} }
return &self.cdp_.?; return &self.cdp_.?;
} }
@@ -100,7 +68,7 @@ const TestContext = struct {
session_id: ?[]const u8 = null, session_id: ?[]const u8 = null,
url: ?[:0]const u8 = null, url: ?[:0]const u8 = null,
}; };
pub fn loadBrowserContext(self: *TestContext, opts: BrowserContextOpts) !*main.BrowserContext(TestCDP) { pub fn loadBrowserContext(self: *TestContext, opts: BrowserContextOpts) !*CDP.BrowserContext(CDP) {
var c = self.cdp(); var c = self.cdp();
if (c.browser_context) |bc| { if (c.browser_context) |bc| {
_ = c.disposeBrowserContext(bc.id); _ = c.disposeBrowserContext(bc.id);
@@ -130,7 +98,7 @@ const TestContext = struct {
} }
const page = try bc.session.createPage(); const page = try bc.session.createPage();
const full_url = try std.fmt.allocPrintSentinel( const full_url = try std.fmt.allocPrintSentinel(
self.arena.allocator(), base.arena_allocator,
"http://127.0.0.1:9582/src/browser/tests/{s}", "http://127.0.0.1:9582/src/browser/tests/{s}",
.{url}, .{url},
0, 0,
@@ -143,19 +111,20 @@ const TestContext = struct {
} }
pub fn processMessage(self: *TestContext, msg: anytype) !void { pub fn processMessage(self: *TestContext, msg: anytype) !void {
var json_message: []const u8 = undefined; const json_message: []const u8 = blk: {
if (@typeInfo(@TypeOf(msg)) != .pointer) { if (@typeInfo(@TypeOf(msg)) != .pointer) {
json_message = try std.json.Stringify.valueAlloc(self.arena.allocator(), msg, .{}); break :blk try std.json.Stringify.valueAlloc(base.arena_allocator, msg, .{});
} else { }
// assume this is a string we want to send as-is, if it isn't, we'll // assume this is a string we want to send as-is, if it isn't, we'll
// get a compile error, so no big deal. // get a compile error, so no big deal.
json_message = msg; break :blk msg;
} };
return self.cdp().processMessage(json_message); return self.cdp().processMessage(json_message);
} }
pub fn expectSentCount(self: *TestContext, expected: usize) !void { pub fn expectSentCount(self: *TestContext, expected: usize) !void {
try expectEqual(expected, self.client.?.sent.items.len); try self.read();
try expectEqual(expected, self.received.items.len);
} }
const ExpectResultOpts = struct { const ExpectResultOpts = struct {
@@ -203,37 +172,135 @@ const TestContext = struct {
index: ?usize = null, index: ?usize = null,
}; };
pub fn expectSent(self: *TestContext, expected: anytype, opts: SentOpts) !void { pub fn expectSent(self: *TestContext, expected: anytype, opts: SentOpts) !void {
const serialized = try json.Stringify.valueAlloc(self.arena.allocator(), expected, .{ const serialized = try json.Stringify.valueAlloc(base.arena_allocator, expected, .{
.whitespace = .indent_2, .whitespace = .indent_2,
.emit_null_optional_fields = false, .emit_null_optional_fields = false,
}); });
for (0..5) |_| {
for (self.client.?.sent.items, 0..) |sent, i| { for (self.received.items, 0..) |received, i| {
if (try compareExpectedToSent(serialized, sent) == false) { if (try compareExpectedToSent(serialized, received) == false) {
continue; continue;
} }
if (opts.index) |expected_index| { if (opts.index) |expected_index| {
if (expected_index != i) { if (expected_index != i) {
std.debug.print("Expected message at index: {d}, was at index: {d}\n", .{ expected_index, i });
self.dumpReceived();
return error.ErrorAtWrongIndex; return error.ErrorAtWrongIndex;
} }
} }
_ = self.client.?.sent.orderedRemove(i); return;
_ = self.client.?.serialized.orderedRemove(i); }
std.Thread.sleep(5 * std.time.ns_per_ms);
try self.read();
}
self.dumpReceived();
return error.ErrorNotFound;
}
fn dumpReceived(self: *const TestContext) void {
std.debug.print("CDP Message Received ({d})\n", .{self.received_raw.items.len});
for (self.received_raw.items, 0..) |received, i| {
std.debug.print("===Message: {d}===\n{s}\n\n", .{ i, received });
}
}
pub fn getSentMessage(self: *TestContext, index: usize) !?json.Value {
for (0..5) |_| {
if (index < self.received.items.len) {
return self.received.items[index];
}
std.Thread.sleep(5 * std.time.ns_per_ms);
try self.read();
}
return null;
}
fn read(self: *TestContext) !void {
while (true) {
const n = posix.read(self.socket, self.read_buf[self.read_at..]) catch |err| switch (err) {
error.WouldBlock => return,
else => return err,
};
if (n == 0) {
return; return;
} }
std.debug.print("Error not found. Expecting:\n{s}\n\nGot:\n", .{serialized}); self.read_at += n;
for (self.client.?.serialized.items, 0..) |sent, i| {
std.debug.print("#{d}\n{s}\n\n", .{ i, sent }); // Try to parse complete WebSocket frames
var pos: usize = 0;
while (pos < self.read_at) {
// Need at least 2 bytes for header
if (self.read_at - pos < 2) break;
const opcode = self.read_buf[pos] & 0x0F;
const payload_len_byte = self.read_buf[pos + 1] & 0x7F;
var header_size: usize = 2;
var payload_len: usize = payload_len_byte;
if (payload_len_byte == 126) {
if (self.read_at - pos < 4) break;
payload_len = std.mem.readInt(u16, self.read_buf[pos + 2 ..][0..2], .big);
header_size = 4;
}
// Skip 8-byte length case (127) - not needed
const frame_size = header_size + payload_len;
if (self.read_at - pos < frame_size) break;
// We have a complete frame - process text (1) or binary (2), skip others
if (opcode == 1 or opcode == 2) {
const payload = self.read_buf[pos + header_size ..][0..payload_len];
const parsed = try std.json.parseFromSliceLeaky(json.Value, base.arena_allocator, payload, .{});
try self.received.append(base.arena_allocator, parsed);
try self.received_raw.append(base.arena_allocator, try base.arena_allocator.dupe(u8, payload));
}
pos += frame_size;
}
// Move remaining partial data to beginning of buffer
if (pos > 0 and pos < self.read_at) {
std.mem.copyForwards(u8, &self.read_buf, self.read_buf[pos..self.read_at]);
self.read_at -= pos;
} else if (pos == self.read_at) {
self.read_at = 0;
}
} }
return error.ErrorNotFound;
} }
}; };
pub fn context() TestContext { pub fn context() !TestContext {
var pair: [2]posix.socket_t = undefined;
const rc = std.c.socketpair(posix.AF.LOCAL, posix.SOCK.STREAM, 0, &pair);
if (rc != 0) {
return error.SocketPairFailed;
}
errdefer {
posix.close(pair[0]);
posix.close(pair[1]);
}
const timeout = std.mem.toBytes(posix.timeval{ .sec = 0, .usec = 5_000 });
try posix.setsockopt(pair[0], posix.SOL.SOCKET, posix.SO.RCVTIMEO, &timeout);
try posix.setsockopt(pair[0], posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout);
try posix.setsockopt(pair[1], posix.SOL.SOCKET, posix.SO.RCVTIMEO, &timeout);
try posix.setsockopt(pair[1], posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout);
try posix.setsockopt(pair[0], posix.SOL.SOCKET, posix.SO.RCVBUF, &std.mem.toBytes(@as(c_int, 32_768)));
try posix.setsockopt(pair[0], posix.SOL.SOCKET, posix.SO.SNDBUF, &std.mem.toBytes(@as(c_int, 32_768)));
try posix.setsockopt(pair[1], posix.SOL.SOCKET, posix.SO.RCVBUF, &std.mem.toBytes(@as(c_int, 32_768)));
try posix.setsockopt(pair[1], posix.SOL.SOCKET, posix.SO.SNDBUF, &std.mem.toBytes(@as(c_int, 32_768)));
const client = try Server.Client.init(pair[1], base.arena_allocator, base.test_app, "json-version", 2000);
return .{ return .{
.arena = ArenaAllocator.init(std.testing.allocator), .client = client,
.socket = pair[0],
}; };
} }

View File

@@ -192,9 +192,9 @@ fn dumpWPT(page: *Page, writer: *std.Io.Writer) !void {
pub inline fn assert(ok: bool, comptime ctx: []const u8, args: anytype) void { pub inline fn assert(ok: bool, comptime ctx: []const u8, args: anytype) void {
if (!ok) { if (!ok) {
if (comptime IS_DEBUG) { // if (comptime IS_DEBUG) {
unreachable; // unreachable;
} // }
assertionFailure(ctx, args); assertionFailure(ctx, args);
} }
} }

View File

@@ -324,7 +324,9 @@ pub const WsConnection = struct {
pub fn init(socket: posix.socket_t, allocator: Allocator, json_version_response: []const u8, timeout_ms: u32) !WsConnection { pub fn init(socket: posix.socket_t, allocator: Allocator, json_version_response: []const u8, timeout_ms: u32) !WsConnection {
const socket_flags = try posix.fcntl(socket, posix.F.GETFL, 0); const socket_flags = try posix.fcntl(socket, posix.F.GETFL, 0);
const nonblocking = @as(u32, @bitCast(posix.O{ .NONBLOCK = true })); const nonblocking = @as(u32, @bitCast(posix.O{ .NONBLOCK = true }));
if (builtin.is_test == false) {
assert(socket_flags & nonblocking == nonblocking, "WsConnection.init blocking", .{}); assert(socket_flags & nonblocking == nonblocking, "WsConnection.init blocking", .{});
}
var reader = try Reader(true).init(allocator); var reader = try Reader(true).init(allocator);
errdefer reader.deinit(); errdefer reader.deinit();

View File

@@ -445,10 +445,6 @@ pub fn pageTest(comptime test_file: []const u8) !*Page {
return page; return page;
} }
test {
std.testing.refAllDecls(@This());
}
const log = @import("log.zig"); const log = @import("log.zig");
const TestHTTPServer = @import("TestHTTPServer.zig"); const TestHTTPServer = @import("TestHTTPServer.zig");