From 04f719c33c244da33ffa61f4193eeee88a13dc01 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 14 Nov 2025 16:14:12 +0800 Subject: [PATCH] wpt runner --- src/browser/parser/Parser.zig | 1 - src/browser/parser/html5ever.zig | 1 - src/main_wpt.zig | 190 ++++++++++++++++++++++++++----- 3 files changed, 160 insertions(+), 32 deletions(-) diff --git a/src/browser/parser/Parser.zig b/src/browser/parser/Parser.zig index f4c6232f..f7cd5c55 100644 --- a/src/browser/parser/Parser.zig +++ b/src/browser/parser/Parser.zig @@ -16,7 +16,6 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - const std = @import("std"); const h5e = @import("html5ever.zig"); diff --git a/src/browser/parser/html5ever.zig b/src/browser/parser/html5ever.zig index ea3e7668..52985290 100644 --- a/src/browser/parser/html5ever.zig +++ b/src/browser/parser/html5ever.zig @@ -16,7 +16,6 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - const ParsedNode = @import("Parser.zig").ParsedNode; pub extern "c" fn html5ever_parse_document( diff --git a/src/main_wpt.zig b/src/main_wpt.zig index 99d7adc6..ff512e40 100644 --- a/src/main_wpt.zig +++ b/src/main_wpt.zig @@ -17,17 +17,11 @@ // along with this program. If not, see . const std = @import("std"); - -const log = @import("log.zig"); -const js = @import("browser/js/js.zig"); +const lp = @import("lightpanda"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; -const App = @import("app.zig").App; -const Browser = @import("browser/browser.zig").Browser; -const TestHTTPServer = @import("TestHTTPServer.zig"); - const WPT_DIR = "tests/wpt"; // use in custom panic handler @@ -38,9 +32,8 @@ pub fn main() !void { defer _ = gpa.deinit(); const allocator = gpa.allocator(); - log.opts.level = .err; - var http_server = TestHTTPServer.init(httpHandler); + var http_server = try TestHTTPServer.init(); defer http_server.deinit(); { @@ -64,19 +57,21 @@ pub fn main() !void { var writer = try Writer.init(allocator, cmd.format); defer writer.deinit(); - // An arena for running each tests. Is reset after every test. - var test_arena = ArenaAllocator.init(allocator); - defer test_arena.deinit(); - - var app = try App.init(allocator, .{ - .run_mode = .fetch, - .user_agent = "User-Agent: Lightpanda/1.0 Lightpanda/WPT", + 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 browser = try Browser.init(app); + var browser = try lp.Browser.init(app); defer browser.deinit(); + // An arena for running each tests. Is reset after every test. + var test_arena = ArenaAllocator.init(allocator); + defer test_arena.deinit(); + var i: usize = 0; while (try it.next()) |test_file| { defer _ = test_arena.reset(.retain_capacity); @@ -108,7 +103,7 @@ pub fn main() !void { fn run( arena: Allocator, - browser: *Browser, + browser: *lp.Browser, test_file: []const u8, err_out: *?[]const u8, ) ![]const u8 { @@ -118,13 +113,13 @@ fn run( const page = try session.createPage(); defer session.removePage(); - const url = try std.fmt.allocPrint(arena, "http://localhost:9582/{s}", .{test_file}); + const url = try std.fmt.allocPrintSentinel(arena, "http://localhost:9582/{s}", .{test_file}, 0); try page.navigate(url, .{}); _ = page.wait(2000); const js_context = page.js; - var try_catch: js.TryCatch = undefined; + var try_catch: lp.js.TryCatch = undefined; try_catch.init(js_context); defer try_catch.deinit(); @@ -442,19 +437,154 @@ const Test = struct { cases: []Case, }; -fn httpHandler(req: *std.http.Server.Request) !void { - const path = req.head.target; +const TestHTTPServer = struct { + shutdown: bool, + dir: std.fs.Dir, + listener: ?std.net.Server, - if (std.mem.eql(u8, path, "/")) { - // There's 1 test that does an XHR request to this, and it just seems - // to want a 200 success. - return req.respond("Hello!", .{}); + pub fn init() !TestHTTPServer { + return .{ + .dir = try std.fs.cwd().openDir(WPT_DIR, .{}), + .shutdown = true, + .listener = null, + }; } - var buf: [1024]u8 = undefined; - const file_path = try std.fmt.bufPrint(&buf, WPT_DIR ++ "{s}", .{path}); - return TestHTTPServer.sendFile(req, file_path); -} + 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", 9582); + + 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, "/")) { + // There's 1 test that does an XHR request to this, and it just seems + // to want a 200 success. + return req.respond("Hello!", .{}); + } + + // 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 {