From 0dd0495ab82682a91f44bda37fe78280817dcf03 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 25 Mar 2026 17:43:30 +0800 Subject: [PATCH 1/3] 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. --- src/Server.zig | 8 +- src/cdp/{cdp.zig => CDP.zig} | 529 +++++++++++++++++------------------ src/cdp/domains/browser.zig | 4 +- src/cdp/domains/dom.zig | 12 +- src/cdp/domains/lp.zig | 24 +- src/cdp/domains/network.zig | 4 +- src/cdp/domains/page.zig | 6 +- src/cdp/domains/security.zig | 2 +- src/cdp/domains/storage.zig | 2 +- src/cdp/domains/target.zig | 28 +- src/cdp/testing.zig | 219 ++++++++++----- src/lightpanda.zig | 6 +- src/network/websocket.zig | 4 +- src/testing.zig | 4 - 14 files changed, 456 insertions(+), 396 deletions(-) rename src/cdp/{cdp.zig => CDP.zig} (72%) diff --git a/src/Server.zig b/src/Server.zig index 777bb97b..a12dc563 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -27,7 +27,7 @@ const ArenaAllocator = std.heap.ArenaAllocator; const log = @import("log.zig"); const App = @import("App.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 HttpClient = @import("browser/HttpClient.zig"); @@ -212,7 +212,7 @@ pub const Client = struct { http: *HttpClient, ws: Net.WsConnection, - fn init( + pub fn init( socket: posix.socket_t, allocator: Allocator, app: *App, @@ -250,7 +250,7 @@ pub const Client = struct { self.ws.shutdown(); } - fn deinit(self: *Client) void { + pub fn deinit(self: *Client) void { switch (self.mode) { .cdp => |*cdp| cdp.deinit(), .http => {}, @@ -461,7 +461,7 @@ pub const Client = struct { fn upgradeConnection(self: *Client, request: []u8) !void { 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 { diff --git a/src/cdp/cdp.zig b/src/cdp/CDP.zig similarity index 72% rename from src/cdp/cdp.zig rename to src/cdp/CDP.zig index 98a7969e..e9c0e3eb 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/CDP.zig @@ -22,297 +22,294 @@ const lp = @import("lightpanda"); const Allocator = std.mem.Allocator; const json = std.json; -const log = @import("../log.zig"); -const js = @import("../browser/js/js.zig"); +const Incrementing = @import("id.zig").Incrementing; +const log = @import("../log.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 Session = @import("../browser/Session.zig"); -const HttpClient = @import("../browser/HttpClient.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 HttpClient = @import("../browser/HttpClient.zig"); + +const InterceptState = @import("domains/fetch.zig").InterceptState; pub const URL_BASE = "chrome://newtab/"; 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 SessionIdGen = Incrementing(u32, "SID"); const BrowserContextIdGen = Incrementing(u32, "BID"); // Generic so that we can inject mocks into it. -pub fn CDPT(comptime TypeProvider: type) type { - return struct { - // Used for sending message to the client and closing on error - client: TypeProvider.Client, +const CDP = @This(); - allocator: Allocator, +// Used for sending message to the client and closing on error +client: *Client, - // The active browser - browser: Browser, +allocator: Allocator, - // when true, any target creation must be attached. - target_auto_attach: bool = false, +// The active browser +browser: Browser, - target_id_gen: TargetIdGen = .{}, - session_id_gen: SessionIdGen = .{}, - browser_context_id_gen: BrowserContextIdGen = .{}, +// when true, any target creation must be attached. +target_auto_attach: bool = false, - 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 - // 1 message at a time. - message_arena: std.heap.ArenaAllocator, +browser_context: ?BrowserContext(CDP), - // Used for processing notifications within a browser context. - notification_arena: std.heap.ArenaAllocator, +// Re-used arena for processing a message. We're assuming that we're getting +// 1 message at a time. +message_arena: std.heap.ArenaAllocator, - // Valid for 1 page navigation (what CDP calls a "renderer") - page_arena: std.heap.ArenaAllocator, +// Used for processing notifications within a browser context. +notification_arena: std.heap.ArenaAllocator, - // Valid for the entire lifetime of the BrowserContext. Should minimize - // (or altogether elimiate) our use of this. - browser_context_arena: std.heap.ArenaAllocator, +// Valid for 1 page navigation (what CDP calls a "renderer") +page_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 { - const allocator = app.allocator; - const browser = try Browser.init(app, .{ - .env = .{ .with_inspector = true }, - .http_client = http_client, - }); - errdefer browser.deinit(); +pub fn init(client: *Client) !CDP { + const app = client.app; + const allocator = app.allocator; + const browser = try Browser.init(app, .{ + .env = .{ .with_inspector = true }, + .http_client = client.http, + }); + errdefer browser.deinit(); - return .{ - .client = client, - .browser = browser, - .allocator = allocator, - .browser_context = null, - .page_arena = std.heap.ArenaAllocator.init(allocator), - .message_arena = std.heap.ArenaAllocator.init(allocator), - .notification_arena = std.heap.ArenaAllocator.init(allocator), - .browser_context_arena = std.heap.ArenaAllocator.init(allocator), - }; - } - - pub fn deinit(self: *Self) void { - if (self.browser_context) |*bc| { - bc.deinit(); - } - self.browser.deinit(); - self.page_arena.deinit(); - self.message_arena.deinit(); - self.notification_arena.deinit(); - self.browser_context_arena.deinit(); - } - - pub fn handleMessage(self: *Self, msg: []const u8) bool { - // if there's an error, it's already been logged - self.processMessage(msg) catch return false; - return true; - } - - pub fn processMessage(self: *Self, msg: []const u8) !void { - const arena = &self.message_arena; - defer _ = arena.reset(.{ .retain_with_limit = 1024 * 16 }); - return self.dispatch(arena.allocator(), self, msg); - } - - // @newhttp - // 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 - // timeouts (or http events) which are ready to be processed. - pub fn pageWait(self: *Self, ms: u32) !Session.Runner.CDPWaitResult { - const session = &(self.browser.session orelse return error.NoPage); - var runner = try session.runner(.{}); - return runner.waitCDP(.{ .ms = ms }); - } - - // Called from above, in processMessage which handles client messages - // but can also be called internally. For example, Target.sendMessageToTarget - // calls back into dispatch to capture the response. - pub fn dispatch(self: *Self, arena: Allocator, sender: anytype, str: []const u8) !void { - const input = json.parseFromSliceLeaky(InputMessage, arena, str, .{ - .ignore_unknown_fields = true, - }) catch return error.InvalidJSON; - - var command = Command(Self, @TypeOf(sender)){ - .input = .{ - .json = str, - .id = input.id, - .action = "", - .params = input.params, - .session_id = input.sessionId, - }, - .cdp = self, - .arena = arena, - .sender = sender, - .browser_context = if (self.browser_context) |*bc| bc else null, - }; - - // See dispatchStartupCommand for more info on this. - var is_startup = false; - if (input.sessionId) |input_session_id| { - if (std.mem.eql(u8, input_session_id, "STARTUP")) { - is_startup = true; - } else if (self.isValidSessionId(input_session_id) == false) { - return command.sendError(-32001, "Unknown sessionId", .{}); - } - } - - if (is_startup) { - dispatchStartupCommand(&command, input.method) catch |err| { - command.sendError(-31999, @errorName(err), .{}) catch return err; - }; - } else { - dispatchCommand(&command, input.method) catch |err| { - command.sendError(-31998, @errorName(err), .{}) catch return err; - }; - } - } - - // 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 - // Puppeteer expects the browser to startup a tab and thus have existing - // targets. - // To this end, we create a [very] dummy BrowserContext, Target and - // Session. There isn't actually a BrowserContext, just a special id. - // When messages are received with the "STARTUP" sessionId, we do - // "special" handling - the bare minimum we need to do until the driver - // switches to a real BrowserContext. - // (I can imagine this logic will become driver-specific) - fn dispatchStartupCommand(command: anytype, method: []const u8) !void { - // Stagehand parses the response and error if we don't return a - // correct one for Page.getFrameTree on startup call. - if (std.mem.eql(u8, method, "Page.getFrameTree")) { - // The Page.getFrameTree handles startup response gracefully. - return dispatchCommand(command, method); - } - - return command.sendResult(null, .{}); - } - - fn dispatchCommand(command: anytype, method: []const u8) !void { - const domain = blk: { - const i = std.mem.indexOfScalarPos(u8, method, 0, '.') orelse { - return error.InvalidMethod; - }; - command.input.action = method[i + 1 ..]; - break :blk method[0..i]; - }; - - switch (domain.len) { - 2 => switch (@as(u16, @bitCast(domain[0..2].*))) { - asUint(u16, "LP") => return @import("domains/lp.zig").processMessage(command), - else => {}, - }, - 3 => switch (@as(u24, @bitCast(domain[0..3].*))) { - asUint(u24, "DOM") => return @import("domains/dom.zig").processMessage(command), - asUint(u24, "Log") => return @import("domains/log.zig").processMessage(command), - asUint(u24, "CSS") => return @import("domains/css.zig").processMessage(command), - else => {}, - }, - 4 => switch (@as(u32, @bitCast(domain[0..4].*))) { - asUint(u32, "Page") => return @import("domains/page.zig").processMessage(command), - else => {}, - }, - 5 => switch (@as(u40, @bitCast(domain[0..5].*))) { - asUint(u40, "Fetch") => return @import("domains/fetch.zig").processMessage(command), - asUint(u40, "Input") => return @import("domains/input.zig").processMessage(command), - else => {}, - }, - 6 => switch (@as(u48, @bitCast(domain[0..6].*))) { - asUint(u48, "Target") => return @import("domains/target.zig").processMessage(command), - else => {}, - }, - 7 => switch (@as(u56, @bitCast(domain[0..7].*))) { - asUint(u56, "Browser") => return @import("domains/browser.zig").processMessage(command), - asUint(u56, "Runtime") => return @import("domains/runtime.zig").processMessage(command), - asUint(u56, "Network") => return @import("domains/network.zig").processMessage(command), - asUint(u56, "Storage") => return @import("domains/storage.zig").processMessage(command), - else => {}, - }, - 8 => switch (@as(u64, @bitCast(domain[0..8].*))) { - asUint(u64, "Security") => return @import("domains/security.zig").processMessage(command), - else => {}, - }, - 9 => switch (@as(u72, @bitCast(domain[0..9].*))) { - asUint(u72, "Emulation") => return @import("domains/emulation.zig").processMessage(command), - asUint(u72, "Inspector") => return @import("domains/inspector.zig").processMessage(command), - else => {}, - }, - 11 => switch (@as(u88, @bitCast(domain[0..11].*))) { - asUint(u88, "Performance") => return @import("domains/performance.zig").processMessage(command), - else => {}, - }, - 13 => switch (@as(u104, @bitCast(domain[0..13].*))) { - asUint(u104, "Accessibility") => return @import("domains/accessibility.zig").processMessage(command), - else => {}, - }, - - else => {}, - } - - return error.UnknownDomain; - } - - fn isValidSessionId(self: *const Self, input_session_id: []const u8) bool { - const browser_context = &(self.browser_context orelse return false); - const session_id = browser_context.session_id orelse return false; - return std.mem.eql(u8, session_id, input_session_id); - } - - pub fn createBrowserContext(self: *Self) ![]const u8 { - if (self.browser_context != null) { - return error.AlreadyExists; - } - const id = self.browser_context_id_gen.next(); - - self.browser_context = @as(BrowserContext(Self), undefined); - const browser_context = &self.browser_context.?; - - try BrowserContext(Self).init(browser_context, id, self); - return id; - } - - pub fn disposeBrowserContext(self: *Self, browser_context_id: []const u8) bool { - const bc = &(self.browser_context orelse return false); - if (std.mem.eql(u8, bc.id, browser_context_id) == false) { - return false; - } - bc.deinit(); - self.browser.closeSession(); - self.browser_context = null; - return true; - } - - const SendEventOpts = struct { - session_id: ?[]const u8 = null, - }; - pub fn sendEvent(self: *Self, method: []const u8, p: anytype, opts: SendEventOpts) !void { - return self.sendJSON(.{ - .method = method, - .params = if (comptime @typeInfo(@TypeOf(p)) == .null) struct {}{} else p, - .sessionId = opts.session_id, - }); - } - - pub fn sendJSON(self: *Self, message: anytype) !void { - return self.client.sendJSON(message, .{ - .emit_null_optional_fields = false, - }); - } + return .{ + .client = client, + .browser = browser, + .allocator = allocator, + .browser_context = null, + .page_arena = std.heap.ArenaAllocator.init(allocator), + .message_arena = std.heap.ArenaAllocator.init(allocator), + .notification_arena = std.heap.ArenaAllocator.init(allocator), + .browser_context_arena = std.heap.ArenaAllocator.init(allocator), }; } +pub fn deinit(self: *CDP) void { + if (self.browser_context) |*bc| { + bc.deinit(); + } + self.browser.deinit(); + self.page_arena.deinit(); + self.message_arena.deinit(); + self.notification_arena.deinit(); + self.browser_context_arena.deinit(); +} + +pub fn handleMessage(self: *CDP, msg: []const u8) bool { + // if there's an error, it's already been logged + self.processMessage(msg) catch return false; + return true; +} + +pub fn processMessage(self: *CDP, msg: []const u8) !void { + const arena = &self.message_arena; + defer _ = arena.reset(.{ .retain_with_limit = 1024 * 16 }); + return self.dispatch(arena.allocator(), self, msg); +} + +// @newhttp +// 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 +// timeouts (or http events) which are ready to be processed. +pub fn pageWait(self: *CDP, ms: u32) !Session.Runner.CDPWaitResult { + const session = &(self.browser.session orelse return error.NoPage); + var runner = try session.runner(.{}); + return runner.waitCDP(.{ .ms = ms }); +} + +// Called from above, in processMessage which handles client messages +// but can also be called internally. For example, Target.sendMessageToTarget +// calls back into dispatch to capture the response. +pub fn dispatch(self: *CDP, arena: Allocator, sender: anytype, str: []const u8) !void { + const input = json.parseFromSliceLeaky(InputMessage, arena, str, .{ + .ignore_unknown_fields = true, + }) catch return error.InvalidJSON; + + var command = Command(CDP, @TypeOf(sender)){ + .input = .{ + .json = str, + .id = input.id, + .action = "", + .params = input.params, + .session_id = input.sessionId, + }, + .cdp = self, + .arena = arena, + .sender = sender, + .browser_context = if (self.browser_context) |*bc| bc else null, + }; + + // See dispatchStartupCommand for more info on this. + var is_startup = false; + if (input.sessionId) |input_session_id| { + if (std.mem.eql(u8, input_session_id, "STARTUP")) { + is_startup = true; + } else if (self.isValidSessionId(input_session_id) == false) { + return command.sendError(-32001, "Unknown sessionId", .{}); + } + } + + if (is_startup) { + dispatchStartupCommand(&command, input.method) catch |err| { + command.sendError(-31999, @errorName(err), .{}) catch return err; + }; + } else { + dispatchCommand(&command, input.method) catch |err| { + command.sendError(-31998, @errorName(err), .{}) catch return err; + }; + } +} + +// 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 +// Puppeteer expects the browser to startup a tab and thus have existing +// targets. +// To this end, we create a [very] dummy BrowserContext, Target and +// Session. There isn't actually a BrowserContext, just a special id. +// When messages are received with the "STARTUP" sessionId, we do +// "special" handling - the bare minimum we need to do until the driver +// switches to a real BrowserContext. +// (I can imagine this logic will become driver-specific) +fn dispatchStartupCommand(command: anytype, method: []const u8) !void { + // Stagehand parses the response and error if we don't return a + // correct one for Page.getFrameTree on startup call. + if (std.mem.eql(u8, method, "Page.getFrameTree")) { + // The Page.getFrameTree handles startup response gracefully. + return dispatchCommand(command, method); + } + + return command.sendResult(null, .{}); +} + +fn dispatchCommand(command: anytype, method: []const u8) !void { + const domain = blk: { + const i = std.mem.indexOfScalarPos(u8, method, 0, '.') orelse { + return error.InvalidMethod; + }; + command.input.action = method[i + 1 ..]; + break :blk method[0..i]; + }; + + switch (domain.len) { + 2 => switch (@as(u16, @bitCast(domain[0..2].*))) { + asUint(u16, "LP") => return @import("domains/lp.zig").processMessage(command), + else => {}, + }, + 3 => switch (@as(u24, @bitCast(domain[0..3].*))) { + asUint(u24, "DOM") => return @import("domains/dom.zig").processMessage(command), + asUint(u24, "Log") => return @import("domains/log.zig").processMessage(command), + asUint(u24, "CSS") => return @import("domains/css.zig").processMessage(command), + else => {}, + }, + 4 => switch (@as(u32, @bitCast(domain[0..4].*))) { + asUint(u32, "Page") => return @import("domains/page.zig").processMessage(command), + else => {}, + }, + 5 => switch (@as(u40, @bitCast(domain[0..5].*))) { + asUint(u40, "Fetch") => return @import("domains/fetch.zig").processMessage(command), + asUint(u40, "Input") => return @import("domains/input.zig").processMessage(command), + else => {}, + }, + 6 => switch (@as(u48, @bitCast(domain[0..6].*))) { + asUint(u48, "Target") => return @import("domains/target.zig").processMessage(command), + else => {}, + }, + 7 => switch (@as(u56, @bitCast(domain[0..7].*))) { + asUint(u56, "Browser") => return @import("domains/browser.zig").processMessage(command), + asUint(u56, "Runtime") => return @import("domains/runtime.zig").processMessage(command), + asUint(u56, "Network") => return @import("domains/network.zig").processMessage(command), + asUint(u56, "Storage") => return @import("domains/storage.zig").processMessage(command), + else => {}, + }, + 8 => switch (@as(u64, @bitCast(domain[0..8].*))) { + asUint(u64, "Security") => return @import("domains/security.zig").processMessage(command), + else => {}, + }, + 9 => switch (@as(u72, @bitCast(domain[0..9].*))) { + asUint(u72, "Emulation") => return @import("domains/emulation.zig").processMessage(command), + asUint(u72, "Inspector") => return @import("domains/inspector.zig").processMessage(command), + else => {}, + }, + 11 => switch (@as(u88, @bitCast(domain[0..11].*))) { + asUint(u88, "Performance") => return @import("domains/performance.zig").processMessage(command), + else => {}, + }, + 13 => switch (@as(u104, @bitCast(domain[0..13].*))) { + asUint(u104, "Accessibility") => return @import("domains/accessibility.zig").processMessage(command), + else => {}, + }, + + else => {}, + } + + return error.UnknownDomain; +} + +fn isValidSessionId(self: *const CDP, input_session_id: []const u8) bool { + const browser_context = &(self.browser_context orelse return false); + const session_id = browser_context.session_id orelse return false; + return std.mem.eql(u8, session_id, input_session_id); +} + +pub fn createBrowserContext(self: *CDP) ![]const u8 { + if (self.browser_context != null) { + return error.AlreadyExists; + } + const id = self.browser_context_id_gen.next(); + + self.browser_context = @as(BrowserContext(CDP), undefined); + const browser_context = &self.browser_context.?; + + try BrowserContext(CDP).init(browser_context, id, self); + return id; +} + +pub fn disposeBrowserContext(self: *CDP, browser_context_id: []const u8) bool { + const bc = &(self.browser_context orelse return false); + if (std.mem.eql(u8, bc.id, browser_context_id) == false) { + return false; + } + bc.deinit(); + self.browser.closeSession(); + self.browser_context = null; + return true; +} + +const SendEventOpts = struct { + session_id: ?[]const u8 = null, +}; +pub fn sendEvent(self: *CDP, method: []const u8, p: anytype, opts: SendEventOpts) !void { + return self.sendJSON(.{ + .method = method, + .params = if (comptime @typeInfo(@TypeOf(p)) == .null) struct {}{} else p, + .sessionId = opts.session_id, + }); +} + +pub fn sendJSON(self: *CDP, message: anytype) !void { + return self.client.sendJSON(message, .{ + .emit_null_optional_fields = false, + }); +} + pub fn BrowserContext(comptime CDP_T: type) type { const Node = @import("Node.zig"); const AXNode = @import("AXNode.zig"); @@ -958,7 +955,7 @@ fn asUint(comptime T: type, comptime string: []const u8) T { const testing = @import("testing.zig"); test "cdp: invalid json" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); try testing.expectError(error.InvalidJSON, ctx.processMessage("invalid")); @@ -983,7 +980,7 @@ test "cdp: invalid json" { } test "cdp: invalid sessionId" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); { @@ -1008,7 +1005,7 @@ test "cdp: invalid sessionId" { } test "cdp: STARTUP sessionId" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); { @@ -1021,13 +1018,13 @@ test "cdp: STARTUP sessionId" { // we have a brower context but no session_id _ = try ctx.loadBrowserContext(.{}); 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 _ = try ctx.loadBrowserContext(.{ .session_id = "SESS-2" }); 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" }); } } diff --git a/src/cdp/domains/browser.zig b/src/cdp/domains/browser.zig index f86feefe..63c087a5 100644 --- a/src/cdp/domains/browser.zig +++ b/src/cdp/domains/browser.zig @@ -112,7 +112,7 @@ fn resetPermissions(cmd: anytype) !void { const testing = @import("../testing.zig"); test "cdp.browser: getVersion" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); try ctx.processMessage(.{ @@ -131,7 +131,7 @@ test "cdp.browser: getVersion" { } test "cdp.browser: getWindowForTarget" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); try ctx.processMessage(.{ diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 386577db..0872a94e 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -547,7 +547,7 @@ fn requestNode(cmd: anytype) !void { const testing = @import("../testing.zig"); test "cdp.dom: getSearchResults unknown search id" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); try ctx.processMessage(.{ @@ -559,7 +559,7 @@ test "cdp.dom: getSearchResults unknown search id" { } test "cdp.dom: search flow" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); _ = 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" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); _ = 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" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); _ = 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" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .url = "cdp/dom2.html" }); @@ -693,7 +693,7 @@ test "cdp.dom: querySelector Nodes found" { } test "cdp.dom: getBoxModel" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .url = "cdp/dom2.html" }); diff --git a/src/cdp/domains/lp.zig b/src/cdp/domains/lp.zig index 9cbf32a5..eaab3e30 100644 --- a/src/cdp/domains/lp.zig +++ b/src/cdp/domains/lp.zig @@ -260,7 +260,7 @@ fn waitForSelector(cmd: anytype) !void { const testing = @import("../testing.zig"); test "cdp.lp: getMarkdown" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); const bc = try ctx.loadBrowserContext(.{}); @@ -271,12 +271,12 @@ test "cdp.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); } test "cdp.lp: getInteractiveElements" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); const bc = try ctx.loadBrowserContext(.{}); @@ -287,13 +287,13 @@ test "cdp.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("nodeIds") != null); } test "cdp.lp: getStructuredData" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); const bc = try ctx.loadBrowserContext(.{}); @@ -304,12 +304,12 @@ test "cdp.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); } test "cdp.lp: action tools" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); const bc = try ctx.loadBrowserContext(.{}); @@ -370,7 +370,7 @@ test "cdp.lp: action tools" { } test "cdp.lp: waitForSelector" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); const bc = try ctx.loadBrowserContext(.{}); @@ -386,9 +386,8 @@ test "cdp.lp: waitForSelector" { .method = "LP.waitForSelector", .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); - ctx.client.?.sent.clearRetainingCapacity(); // 2. Delayed element try ctx.processMessage(.{ @@ -396,9 +395,8 @@ test "cdp.lp: waitForSelector" { .method = "LP.waitForSelector", .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); - ctx.client.?.sent.clearRetainingCapacity(); // 3. Timeout error try ctx.processMessage(.{ @@ -406,6 +404,6 @@ test "cdp.lp: waitForSelector" { .method = "LP.waitForSelector", .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); } diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index 11e0142c..3745e398 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -439,7 +439,7 @@ fn idFromRequestId(request_id: []const u8) !u64 { const testing = @import("../testing.zig"); test "cdp.network setExtraHTTPHeaders" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); _ = try ctx.loadBrowserContext(.{ .id = "NID-A", .session_id = "NESI-A" }); @@ -465,7 +465,7 @@ test "cdp.Network: cookies" { const ResCookie = CdpStorage.ResCookie; const CdpCookie = CdpStorage.CdpCookie; - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); _ = try ctx.loadBrowserContext(.{ .id = "BID-S" }); diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 96932810..ab62feb6 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -642,7 +642,7 @@ fn getLayoutMetrics(cmd: anytype) !void { const testing = @import("../testing.zig"); test "cdp.page: getFrameTree" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); { @@ -712,7 +712,7 @@ test "cdp.page: captureScreenshot" { const filter: LogFilter = .init(&.{.not_implemented}); defer filter.deinit(); - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); { try ctx.processMessage(.{ .id = 10, .method = "Page.captureScreenshot", .params = .{ .format = "jpg" } }); @@ -728,7 +728,7 @@ test "cdp.page: captureScreenshot" { } test "cdp.page: getLayoutMetrics" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); _ = try ctx.loadBrowserContext(.{ .id = "BID-9", .url = "hi.html", .target_id = "FID-000000000X".* }); diff --git a/src/cdp/domains/security.zig b/src/cdp/domains/security.zig index 9bbf5b39..4e6ff663 100644 --- a/src/cdp/domains/security.zig +++ b/src/cdp/domains/security.zig @@ -44,7 +44,7 @@ fn setIgnoreCertificateErrors(cmd: anytype) !void { const testing = @import("../testing.zig"); test "cdp.Security: setIgnoreCertificateErrors" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); _ = try ctx.loadBrowserContext(.{ .id = "BID-9" }); diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig index e0f67456..bd2e24c0 100644 --- a/src/cdp/domains/storage.zig +++ b/src/cdp/domains/storage.zig @@ -243,7 +243,7 @@ pub fn writeCookie(cookie: *const Cookie, w: anytype) !void { const testing = @import("../testing.zig"); test "cdp.Storage: cookies" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); _ = try ctx.loadBrowserContext(.{ .id = "BID-S" }); diff --git a/src/cdp/domains/target.zig b/src/cdp/domains/target.zig index 51af7c53..4755eaba 100644 --- a/src/cdp/domains/target.zig +++ b/src/cdp/domains/target.zig @@ -512,7 +512,7 @@ const TargetInfo = struct { const testing = @import("../testing.zig"); test "cdp.target: getBrowserContexts" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); // { @@ -536,7 +536,7 @@ test "cdp.target: getBrowserContexts" { } test "cdp.target: createBrowserContext" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); { @@ -554,7 +554,7 @@ test "cdp.target: createBrowserContext" { } test "cdp.target: disposeBrowserContext" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); { @@ -585,7 +585,7 @@ test "cdp.target: disposeBrowserContext" { test "cdp.target: createTarget" { { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); 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(); // active auto attach to get the Target.attachedToTarget event. 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.? } }, .{}); } - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" }); { @@ -624,7 +624,7 @@ test "cdp.target: createTarget" { } test "cdp.target: closeTarget" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); { @@ -655,7 +655,7 @@ test "cdp.target: closeTarget" { } test "cdp.target: attachToTarget" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); { @@ -686,7 +686,7 @@ test "cdp.target: attachToTarget" { } test "cdp.target: getTargetInfo" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); { @@ -737,7 +737,7 @@ test "cdp.target: getTargetInfo" { } test "cdp.target: issue#474: attach to just created target" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); 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" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" }); { @@ -775,19 +775,19 @@ test "cdp.target: detachFromTarget" { } test "cdp.target: detachFromTarget without session" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); _ = try ctx.loadBrowserContext(.{ .id = "BID-9" }); { // detach when no session is attached should not send event try ctx.processMessage(.{ .id = 10, .method = "Target.detachFromTarget" }); try ctx.expectSentResult(null, .{ .id = 10 }); - try ctx.expectSentCount(0); + try ctx.expectSentCount(1); } } test "cdp.target: setAutoAttach false sends detachedFromTarget" { - var ctx = testing.context(); + var ctx = try testing.context(); defer ctx.deinit(); const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" }); { diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index 744104d1..87639a6c 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -18,12 +18,14 @@ const std = @import("std"); const json = std.json; +const posix = std.posix; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const Testing = @This(); -const main = @import("cdp.zig"); +const CDP = @import("CDP.zig"); +const Server = @import("../Server.zig"); const base = @import("../testing.zig"); pub const allocator = base.allocator; @@ -35,61 +37,27 @@ pub const expectEqualSlices = base.expectEqualSlices; pub const pageTest = base.pageTest; 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 { - client: ?Client = null, - cdp_: ?TestCDP = null, - arena: ArenaAllocator, + read_at: usize = 0, + read_buf: [1024 * 32]u8 = undefined, + 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 { if (self.cdp_) |*c| { 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) { - self.client = Client.init(self.arena.allocator()); - // 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; + self.cdp_ = CDP.init(&self.client) catch |err| @panic(@errorName(err)); } return &self.cdp_.?; } @@ -100,7 +68,7 @@ const TestContext = struct { session_id: ?[]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(); if (c.browser_context) |bc| { _ = c.disposeBrowserContext(bc.id); @@ -130,7 +98,7 @@ const TestContext = struct { } const page = try bc.session.createPage(); const full_url = try std.fmt.allocPrintSentinel( - self.arena.allocator(), + base.arena_allocator, "http://127.0.0.1:9582/src/browser/tests/{s}", .{url}, 0, @@ -143,19 +111,20 @@ const TestContext = struct { } pub fn processMessage(self: *TestContext, msg: anytype) !void { - var json_message: []const u8 = undefined; - if (@typeInfo(@TypeOf(msg)) != .pointer) { - json_message = try std.json.Stringify.valueAlloc(self.arena.allocator(), msg, .{}); - } else { + const json_message: []const u8 = blk: { + if (@typeInfo(@TypeOf(msg)) != .pointer) { + break :blk try std.json.Stringify.valueAlloc(base.arena_allocator, msg, .{}); + } // 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. - json_message = msg; - } + break :blk msg; + }; return self.cdp().processMessage(json_message); } 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 { @@ -203,37 +172,135 @@ const TestContext = struct { index: ?usize = null, }; 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, .emit_null_optional_fields = false, }); - - for (self.client.?.sent.items, 0..) |sent, i| { - if (try compareExpectedToSent(serialized, sent) == false) { - continue; - } - - if (opts.index) |expected_index| { - if (expected_index != i) { - return error.ErrorAtWrongIndex; + for (0..5) |_| { + for (self.received.items, 0..) |received, i| { + if (try compareExpectedToSent(serialized, received) == false) { + continue; } - } - _ = self.client.?.sent.orderedRemove(i); - _ = self.client.?.serialized.orderedRemove(i); - return; - } - std.debug.print("Error not found. Expecting:\n{s}\n\nGot:\n", .{serialized}); - for (self.client.?.serialized.items, 0..) |sent, i| { - std.debug.print("#{d}\n{s}\n\n", .{ i, sent }); + if (opts.index) |expected_index| { + 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; + } + 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; + } + + self.read_at += n; + + // 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; + } + } + } }; -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 .{ - .arena = ArenaAllocator.init(std.testing.allocator), + .client = client, + .socket = pair[0], }; } diff --git a/src/lightpanda.zig b/src/lightpanda.zig index 67e8f1b9..d4da56bf 100644 --- a/src/lightpanda.zig +++ b/src/lightpanda.zig @@ -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 { if (!ok) { - if (comptime IS_DEBUG) { - unreachable; - } + // if (comptime IS_DEBUG) { + // unreachable; + // } assertionFailure(ctx, args); } } diff --git a/src/network/websocket.zig b/src/network/websocket.zig index c6c74ed0..115b702a 100644 --- a/src/network/websocket.zig +++ b/src/network/websocket.zig @@ -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 { const socket_flags = try posix.fcntl(socket, posix.F.GETFL, 0); const nonblocking = @as(u32, @bitCast(posix.O{ .NONBLOCK = true })); - assert(socket_flags & nonblocking == nonblocking, "WsConnection.init blocking", .{}); + if (builtin.is_test == false) { + assert(socket_flags & nonblocking == nonblocking, "WsConnection.init blocking", .{}); + } var reader = try Reader(true).init(allocator); errdefer reader.deinit(); diff --git a/src/testing.zig b/src/testing.zig index 050c9849..030c0648 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -445,10 +445,6 @@ pub fn pageTest(comptime test_file: []const u8) !*Page { return page; } -test { - std.testing.refAllDecls(@This()); -} - const log = @import("log.zig"); const TestHTTPServer = @import("TestHTTPServer.zig"); From ca41bb5fa2c6f705dcc30287dc4b5f312c8d82fa Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 25 Mar 2026 17:54:24 +0800 Subject: [PATCH 2/3] fix import casing --- src/cdp/domains/page.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index ab62feb6..aee7fa45 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -82,7 +82,7 @@ fn getFrameTree(cmd: anytype) !void { .frame = .{ .id = "TID-STARTUP", .loaderId = "LID-STARTUP", - .securityOrigin = @import("../cdp.zig").URL_BASE, + .securityOrigin = @import("../CDP.zig").URL_BASE, .url = "about:blank", .secureContextType = "Secure", }, From 0fc959dcc5b91810035e3df033e471c2e331530e Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 26 Mar 2026 07:42:45 +0800 Subject: [PATCH 3/3] re-anble unreachable --- src/lightpanda.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lightpanda.zig b/src/lightpanda.zig index d4da56bf..67e8f1b9 100644 --- a/src/lightpanda.zig +++ b/src/lightpanda.zig @@ -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 { if (!ok) { - // if (comptime IS_DEBUG) { - // unreachable; - // } + if (comptime IS_DEBUG) { + unreachable; + } assertionFailure(ctx, args); } }