mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-02-05 06:47:11 +00:00
V8's inspector world is made up of 4 components: Inspector, Client, Channel and Session. Currently, we treat all 4 components as a single unit which is tied to the lifetime of CDP BrowserContext - or, loosely speaking, 1 "Inspector Unit" per page / v8::Context. According to https://web.archive.org/web/20210622022956/https://hyperandroid.com/2020/02/12/v8-inspector-from-an-embedder-standpoint/ and conversation with Gemini, it's more typical to have 1 inspector per isolate. The general breakdown is the Inspector is the top-level manager, the Client is our implementation which control how the Inspector works (its function we expose that v8 calls into). These should be tied to the Isolate. Channels and Sessions are more closely tied to Context, where the Channel is v8->zig and the Session us zig->v8. This PR does a few things 1 - It creates 1 Inspector and Client per Isolate (Env.js) 2 - It creates 1 Session/Channel per BrowserContext 3 - It merges v8::Session and v8::Channel into Inspector.Session 4 - It moves the Inspector instance directly into the Env 5 - BrowserContext interacts with the Inspector.Session, not the Inspector 4 is arguably unnecessary with respect to the main goal of this commit, but the end-goal is to tighten the integration. Specifically, rather than CDP having to inform the inspector that a context was created/destroyed, the Env which manages Contexts directly (https://github.com/lightpanda-io/browser/pull/1432) and which now has direct access to the Inspector, is now equipped to keep this in sync.
273 lines
8.3 KiB
Zig
273 lines
8.3 KiB
Zig
const std = @import("std");
|
|
const lp = @import("lightpanda");
|
|
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
// used in custom panic handler
|
|
var current_test: ?[]const u8 = null;
|
|
|
|
pub fn main() !void {
|
|
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
|
defer _ = gpa.deinit();
|
|
|
|
const allocator = gpa.allocator();
|
|
|
|
var args = try std.process.argsWithAllocator(allocator);
|
|
defer args.deinit();
|
|
_ = args.next(); // executable name
|
|
|
|
var filter: ?[]const u8 = null;
|
|
if (args.next()) |n| {
|
|
filter = n;
|
|
}
|
|
|
|
var http_server = try TestHTTPServer.init();
|
|
defer http_server.deinit();
|
|
|
|
{
|
|
var wg: std.Thread.WaitGroup = .{};
|
|
wg.startMany(1);
|
|
var thrd = try std.Thread.spawn(.{}, TestHTTPServer.run, .{ &http_server, &wg });
|
|
thrd.detach();
|
|
wg.wait();
|
|
}
|
|
lp.log.opts.level = .warn;
|
|
|
|
var app = try lp.App.init(allocator, .{
|
|
.run_mode = .serve,
|
|
.tls_verify_host = false,
|
|
.user_agent = "User-Agent: Lightpanda/1.0 internal-tester",
|
|
});
|
|
defer app.deinit();
|
|
|
|
var test_arena = std.heap.ArenaAllocator.init(allocator);
|
|
defer test_arena.deinit();
|
|
|
|
var browser = try lp.Browser.init(app, .{});
|
|
defer browser.deinit();
|
|
|
|
const session = try browser.newSession();
|
|
|
|
var dir = try std.fs.cwd().openDir("src/browser/tests/legacy/", .{ .iterate = true, .no_follow = true });
|
|
defer dir.close();
|
|
var walker = try dir.walk(allocator);
|
|
defer walker.deinit();
|
|
while (try walker.next()) |entry| {
|
|
_ = test_arena.reset(.retain_capacity);
|
|
if (entry.kind != .file) {
|
|
continue;
|
|
}
|
|
|
|
if (!std.mem.endsWith(u8, entry.basename, ".html")) {
|
|
continue;
|
|
}
|
|
|
|
if (std.mem.endsWith(u8, entry.basename, ".skip.html")) {
|
|
continue;
|
|
}
|
|
|
|
if (filter) |f| {
|
|
if (std.mem.indexOf(u8, entry.path, f) == null) {
|
|
continue;
|
|
}
|
|
}
|
|
std.debug.print("\n===={s}====\n", .{entry.path});
|
|
current_test = entry.path;
|
|
run(test_arena.allocator(), entry.path, session) catch |err| {
|
|
std.debug.print("Failure: {s} - {any}\n", .{ entry.path, err });
|
|
};
|
|
}
|
|
}
|
|
|
|
pub fn run(allocator: Allocator, file: []const u8, session: *lp.Session) !void {
|
|
const url = try std.fmt.allocPrintSentinel(allocator, "http://localhost:9589/{s}", .{file}, 0);
|
|
|
|
const page = try session.createPage();
|
|
defer session.removePage();
|
|
|
|
var ls: lp.js.Local.Scope = undefined;
|
|
page.js.localScope(&ls);
|
|
defer ls.deinit();
|
|
|
|
var try_catch: lp.js.TryCatch = undefined;
|
|
try_catch.init(&ls.local);
|
|
defer try_catch.deinit();
|
|
|
|
try page.navigate(url, .{});
|
|
_ = session.wait(2000);
|
|
|
|
ls.local.eval("testing.assertOk()", "testing.assertOk()") catch |err| {
|
|
const caught = try_catch.caughtOrError(allocator, err);
|
|
std.debug.print("{s}: test failure\nError: {f}\n", .{ file, caught });
|
|
return err;
|
|
};
|
|
}
|
|
|
|
const TestHTTPServer = struct {
|
|
shutdown: bool,
|
|
dir: std.fs.Dir,
|
|
listener: ?std.net.Server,
|
|
|
|
pub fn init() !TestHTTPServer {
|
|
return .{
|
|
.dir = try std.fs.cwd().openDir("src/browser/tests/legacy/", .{}),
|
|
.shutdown = true,
|
|
.listener = null,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *TestHTTPServer) void {
|
|
self.shutdown = true;
|
|
if (self.listener) |*listener| {
|
|
listener.deinit();
|
|
}
|
|
self.dir.close();
|
|
}
|
|
|
|
pub fn run(self: *TestHTTPServer, wg: *std.Thread.WaitGroup) !void {
|
|
const address = try std.net.Address.parseIp("127.0.0.1", 9589);
|
|
|
|
self.listener = try address.listen(.{ .reuse_address = true });
|
|
var listener = &self.listener.?;
|
|
|
|
wg.finish();
|
|
|
|
while (true) {
|
|
const conn = listener.accept() catch |err| {
|
|
if (self.shutdown) {
|
|
return;
|
|
}
|
|
return err;
|
|
};
|
|
const thrd = try std.Thread.spawn(.{}, handleConnection, .{ self, conn });
|
|
thrd.detach();
|
|
}
|
|
}
|
|
|
|
fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !void {
|
|
defer conn.stream.close();
|
|
|
|
var req_buf: [2048]u8 = undefined;
|
|
var conn_reader = conn.stream.reader(&req_buf);
|
|
var conn_writer = conn.stream.writer(&req_buf);
|
|
|
|
var http_server = std.http.Server.init(conn_reader.interface(), &conn_writer.interface);
|
|
|
|
while (true) {
|
|
var req = http_server.receiveHead() catch |err| switch (err) {
|
|
error.ReadFailed => continue,
|
|
error.HttpConnectionClosing => continue,
|
|
else => {
|
|
std.debug.print("Test HTTP Server error: {}\n", .{err});
|
|
return err;
|
|
},
|
|
};
|
|
|
|
self.handler(&req) catch |err| {
|
|
std.debug.print("test http error '{s}': {}\n", .{ req.head.target, err });
|
|
try req.respond("server error", .{ .status = .internal_server_error });
|
|
return;
|
|
};
|
|
}
|
|
}
|
|
|
|
fn handler(server: *TestHTTPServer, req: *std.http.Server.Request) !void {
|
|
const path = req.head.target;
|
|
|
|
if (std.mem.eql(u8, path, "/xhr")) {
|
|
return req.respond("1234567890" ** 10, .{
|
|
.extra_headers = &.{
|
|
.{ .name = "Content-Type", .value = "text/html; charset=utf-8" },
|
|
},
|
|
});
|
|
}
|
|
|
|
if (std.mem.eql(u8, path, "/xhr/json")) {
|
|
return req.respond("{\"over\":\"9000!!!\"}", .{
|
|
.extra_headers = &.{
|
|
.{ .name = "Content-Type", .value = "application/json" },
|
|
},
|
|
});
|
|
}
|
|
|
|
// strip out leading '/' to make the path relative
|
|
const file = try server.dir.openFile(path[1..], .{});
|
|
defer file.close();
|
|
|
|
const stat = try file.stat();
|
|
var send_buffer: [4096]u8 = undefined;
|
|
|
|
var res = try req.respondStreaming(&send_buffer, .{
|
|
.content_length = stat.size,
|
|
.respond_options = .{
|
|
.extra_headers = &.{
|
|
.{ .name = "content-type", .value = getContentType(path) },
|
|
},
|
|
},
|
|
});
|
|
|
|
var read_buffer: [4096]u8 = undefined;
|
|
var reader = file.reader(&read_buffer);
|
|
_ = try res.writer.sendFileAll(&reader, .unlimited);
|
|
try res.writer.flush();
|
|
try res.end();
|
|
}
|
|
|
|
pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void {
|
|
var file = std.fs.cwd().openFile(file_path, .{}) catch |err| switch (err) {
|
|
error.FileNotFound => return req.respond("server error", .{ .status = .not_found }),
|
|
else => return err,
|
|
};
|
|
defer file.close();
|
|
|
|
const stat = try file.stat();
|
|
var send_buffer: [4096]u8 = undefined;
|
|
|
|
var res = try req.respondStreaming(&send_buffer, .{
|
|
.content_length = stat.size,
|
|
.respond_options = .{
|
|
.extra_headers = &.{
|
|
.{ .name = "content-type", .value = getContentType(file_path) },
|
|
},
|
|
},
|
|
});
|
|
|
|
var read_buffer: [4096]u8 = undefined;
|
|
var reader = file.reader(&read_buffer);
|
|
_ = try res.writer.sendFileAll(&reader, .unlimited);
|
|
try res.writer.flush();
|
|
try res.end();
|
|
}
|
|
|
|
fn getContentType(file_path: []const u8) []const u8 {
|
|
if (std.mem.endsWith(u8, file_path, ".js")) {
|
|
return "application/json";
|
|
}
|
|
|
|
if (std.mem.endsWith(u8, file_path, ".html")) {
|
|
return "text/html";
|
|
}
|
|
|
|
if (std.mem.endsWith(u8, file_path, ".htm")) {
|
|
return "text/html";
|
|
}
|
|
|
|
if (std.mem.endsWith(u8, file_path, ".xml")) {
|
|
// some wpt tests do this
|
|
return "text/xml";
|
|
}
|
|
|
|
std.debug.print("TestHTTPServer asked to serve an unknown file type: {s}\n", .{file_path});
|
|
return "text/html";
|
|
}
|
|
};
|
|
|
|
pub const panic = std.debug.FullPanic(struct {
|
|
pub fn panicFn(msg: []const u8, first_trace_addr: ?usize) noreturn {
|
|
if (current_test) |ct| {
|
|
std.debug.print("===panic running: {s}===\n", .{ct});
|
|
}
|
|
std.debug.defaultPanic(msg, first_trace_addr);
|
|
}
|
|
}.panicFn);
|