mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 13:54:21 +00:00
fuzzer web ui: introduce entry points
so you can have somewhere to start browsing
This commit is contained in:
parent
6e6164f8a6
commit
e64a00950e
8 changed files with 163 additions and 11 deletions
|
|
@ -1,8 +1,10 @@
|
|||
//! Default test runner for unit tests.
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const std = @import("std");
|
||||
const io = std.io;
|
||||
const testing = std.testing;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
pub const std_options = .{
|
||||
.logFn = log,
|
||||
|
|
@ -141,7 +143,9 @@ fn mainServer() !void {
|
|||
});
|
||||
},
|
||||
.start_fuzzing => {
|
||||
if (!builtin.fuzz) unreachable;
|
||||
const index = try server.receiveBody_u32();
|
||||
var first = true;
|
||||
const test_fn = builtin.test_functions[index];
|
||||
while (true) {
|
||||
testing.allocator_instance = .{};
|
||||
|
|
@ -160,6 +164,10 @@ fn mainServer() !void {
|
|||
};
|
||||
if (!is_fuzz_test) @panic("missed call to std.testing.fuzzInput");
|
||||
if (log_err_count != 0) @panic("error logs detected");
|
||||
if (first) {
|
||||
first = false;
|
||||
try server.serveU64Message(.fuzz_start_addr, entry_addr);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -339,6 +347,7 @@ const FuzzerSlice = extern struct {
|
|||
};
|
||||
|
||||
var is_fuzz_test: bool = undefined;
|
||||
var entry_addr: usize = 0;
|
||||
|
||||
extern fn fuzzer_next() FuzzerSlice;
|
||||
extern fn fuzzer_init(cache_dir: FuzzerSlice) void;
|
||||
|
|
@ -348,7 +357,10 @@ pub fn fuzzInput(options: testing.FuzzInputOptions) []const u8 {
|
|||
@disableInstrumentation();
|
||||
if (crippled) return "";
|
||||
is_fuzz_test = true;
|
||||
if (builtin.fuzz) return fuzzer_next().toSlice();
|
||||
if (builtin.fuzz) {
|
||||
if (entry_addr == 0) entry_addr = @returnAddress();
|
||||
return fuzzer_next().toSlice();
|
||||
}
|
||||
if (options.corpus.len == 0) return "";
|
||||
var prng = std.Random.DefaultPrng.init(testing.random_seed);
|
||||
const random = prng.random();
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@
|
|||
<li>Unique Runs: <span id="statUniqueRuns"></span></li>
|
||||
<li>Coverage: <span id="statCoverage"></span></li>
|
||||
<li>Lowest Stack: <span id="statLowestStack"></span></li>
|
||||
<li>Entry Points: <ul id="entryPointsList"></ul></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="sectSource" class="hidden">
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
const domStatUniqueRuns = document.getElementById("statUniqueRuns");
|
||||
const domStatCoverage = document.getElementById("statCoverage");
|
||||
const domStatLowestStack = document.getElementById("statLowestStack");
|
||||
const domEntryPointsList = document.getElementById("entryPointsList");
|
||||
|
||||
let wasm_promise = fetch("main.wasm");
|
||||
let sources_promise = fetch("sources.tar").then(function(response) {
|
||||
|
|
@ -30,7 +31,8 @@
|
|||
throw new Error("panic: " + msg);
|
||||
},
|
||||
emitSourceIndexChange: onSourceIndexChange,
|
||||
emitCoverageUpdate: onCoverageUpdate,
|
||||
emitCoverageUpdate: renderStats,
|
||||
emitEntryPointsUpdate: renderStats,
|
||||
},
|
||||
}).then(function(obj) {
|
||||
wasm_exports = obj.instance.exports;
|
||||
|
|
@ -98,10 +100,6 @@
|
|||
render();
|
||||
}
|
||||
|
||||
function onCoverageUpdate() {
|
||||
renderStats();
|
||||
}
|
||||
|
||||
function render() {
|
||||
domStatus.classList.add("hidden");
|
||||
domSectSource.classList.add("hidden");
|
||||
|
|
@ -120,9 +118,26 @@
|
|||
domStatCoverage.innerText = coveredSourceLocations + " / " + totalSourceLocations + " (" + percent(coveredSourceLocations, totalSourceLocations) + "%)";
|
||||
domStatLowestStack.innerText = unwrapString(wasm_exports.lowestStack());
|
||||
|
||||
const entryPoints = unwrapInt32Array(wasm_exports.entryPoints());
|
||||
resizeDomList(domEntryPointsList, entryPoints.length, "<li></li>");
|
||||
for (let i = 0; i < entryPoints.length; i += 1) {
|
||||
const liDom = domEntryPointsList.children[i];
|
||||
liDom.innerText = unwrapString(wasm_exports.sourceLocationLinkHtml(entryPoints[i]));
|
||||
}
|
||||
|
||||
|
||||
domSectStats.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function resizeDomList(listDom, desiredLen, templateHtml) {
|
||||
for (let i = listDom.childElementCount; i < desiredLen; i += 1) {
|
||||
listDom.insertAdjacentHTML('beforeend', templateHtml);
|
||||
}
|
||||
while (desiredLen < listDom.childElementCount) {
|
||||
listDom.removeChild(listDom.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
function percent(a, b) {
|
||||
return ((Number(a) / Number(b)) * 100).toFixed(1);
|
||||
}
|
||||
|
|
@ -150,6 +165,13 @@
|
|||
return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len));
|
||||
}
|
||||
|
||||
function unwrapInt32Array(bigint) {
|
||||
const ptr = Number(bigint & 0xffffffffn);
|
||||
const len = Number(bigint >> 32n);
|
||||
if (len === 0) return new Uint32Array();
|
||||
return new Uint32Array(wasm_exports.memory.buffer, ptr, len);
|
||||
}
|
||||
|
||||
function setInputString(s) {
|
||||
const jsArray = text_encoder.encode(s);
|
||||
const len = jsArray.length;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ const js = struct {
|
|||
extern "js" fn panic(ptr: [*]const u8, len: usize) noreturn;
|
||||
extern "js" fn emitSourceIndexChange() void;
|
||||
extern "js" fn emitCoverageUpdate() void;
|
||||
extern "js" fn emitEntryPointsUpdate() void;
|
||||
};
|
||||
|
||||
pub const std_options: std.Options = .{
|
||||
|
|
@ -64,6 +65,7 @@ export fn message_end() void {
|
|||
switch (tag) {
|
||||
.source_index => return sourceIndexMessage(msg_bytes) catch @panic("OOM"),
|
||||
.coverage_update => return coverageUpdateMessage(msg_bytes) catch @panic("OOM"),
|
||||
.entry_points => return entryPointsMessage(msg_bytes) catch @panic("OOM"),
|
||||
_ => unreachable,
|
||||
}
|
||||
}
|
||||
|
|
@ -219,6 +221,19 @@ fn coverageUpdateMessage(msg_bytes: []u8) error{OutOfMemory}!void {
|
|||
js.emitCoverageUpdate();
|
||||
}
|
||||
|
||||
var entry_points: std.ArrayListUnmanaged(u32) = .{};
|
||||
|
||||
fn entryPointsMessage(msg_bytes: []u8) error{OutOfMemory}!void {
|
||||
const header: abi.EntryPointHeader = @bitCast(msg_bytes[0..@sizeOf(abi.EntryPointHeader)].*);
|
||||
entry_points.resize(gpa, header.flags.locs_len) catch @panic("OOM");
|
||||
@memcpy(entry_points.items, std.mem.bytesAsSlice(u32, msg_bytes[@sizeOf(abi.EntryPointHeader)..]));
|
||||
js.emitEntryPointsUpdate();
|
||||
}
|
||||
|
||||
export fn entryPoints() Slice(u32) {
|
||||
return Slice(u32).init(entry_points.items);
|
||||
}
|
||||
|
||||
var coverage = Coverage.init;
|
||||
var coverage_source_locations: std.ArrayListUnmanaged(Coverage.SourceLocation) = .{};
|
||||
/// Contains the most recent coverage update message, unmodified.
|
||||
|
|
@ -246,3 +261,14 @@ fn updateCoverage(
|
|||
@memcpy(coverage.directories.entries.items(.key), directories);
|
||||
try coverage.directories.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items });
|
||||
}
|
||||
|
||||
export fn sourceLocationLinkHtml(index: u32) String {
|
||||
const sl = coverage_source_locations.items[index];
|
||||
const file_name = coverage.stringAt(coverage.fileAt(sl.file).basename);
|
||||
|
||||
string_result.clearRetainingCapacity();
|
||||
string_result.writer(gpa).print("{s}:{d}:{d}", .{
|
||||
file_name, sl.line, sl.column,
|
||||
}) catch @panic("OOM");
|
||||
return String.init(string_result.items);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ const CoverageMap = struct {
|
|||
mapped_memory: []align(std.mem.page_size) const u8,
|
||||
coverage: Coverage,
|
||||
source_locations: []Coverage.SourceLocation,
|
||||
/// Elements are indexes into `source_locations` pointing to the unit tests that are being fuzz tested.
|
||||
entry_points: std.ArrayListUnmanaged(u32),
|
||||
|
||||
fn deinit(cm: *CoverageMap, gpa: Allocator) void {
|
||||
std.posix.munmap(cm.mapped_memory);
|
||||
|
|
@ -47,6 +49,10 @@ const Msg = union(enum) {
|
|||
id: u64,
|
||||
run: *Step.Run,
|
||||
},
|
||||
entry_point: struct {
|
||||
coverage_id: u64,
|
||||
addr: u64,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn run(ws: *WebServer) void {
|
||||
|
|
@ -356,14 +362,20 @@ fn serveWebSocket(ws: *WebServer, web_socket: *std.http.WebSocket) !void {
|
|||
// On first connection, the client needs all the coverage information
|
||||
// so that subsequent updates can contain only the updated bits.
|
||||
var prev_unique_runs: usize = 0;
|
||||
try sendCoverageContext(ws, web_socket, &prev_unique_runs);
|
||||
var prev_entry_points: usize = 0;
|
||||
try sendCoverageContext(ws, web_socket, &prev_unique_runs, &prev_entry_points);
|
||||
while (true) {
|
||||
ws.coverage_condition.timedWait(&ws.coverage_mutex, std.time.ns_per_ms * 500) catch {};
|
||||
try sendCoverageContext(ws, web_socket, &prev_unique_runs);
|
||||
try sendCoverageContext(ws, web_socket, &prev_unique_runs, &prev_entry_points);
|
||||
}
|
||||
}
|
||||
|
||||
fn sendCoverageContext(ws: *WebServer, web_socket: *std.http.WebSocket, prev_unique_runs: *usize) !void {
|
||||
fn sendCoverageContext(
|
||||
ws: *WebServer,
|
||||
web_socket: *std.http.WebSocket,
|
||||
prev_unique_runs: *usize,
|
||||
prev_entry_points: *usize,
|
||||
) !void {
|
||||
const coverage_maps = ws.coverage_files.values();
|
||||
if (coverage_maps.len == 0) return;
|
||||
// TODO: make each events URL correspond to one coverage map
|
||||
|
|
@ -407,6 +419,21 @@ fn sendCoverageContext(ws: *WebServer, web_socket: *std.http.WebSocket, prev_uni
|
|||
|
||||
prev_unique_runs.* = unique_runs;
|
||||
}
|
||||
|
||||
if (prev_entry_points.* != coverage_map.entry_points.items.len) {
|
||||
const header: abi.EntryPointHeader = .{
|
||||
.flags = .{
|
||||
.locs_len = @intCast(coverage_map.entry_points.items.len),
|
||||
},
|
||||
};
|
||||
const iovecs: [2]std.posix.iovec_const = .{
|
||||
makeIov(std.mem.asBytes(&header)),
|
||||
makeIov(std.mem.sliceAsBytes(coverage_map.entry_points.items)),
|
||||
};
|
||||
try web_socket.writeMessagev(&iovecs, .binary);
|
||||
|
||||
prev_entry_points.* = coverage_map.entry_points.items.len;
|
||||
}
|
||||
}
|
||||
|
||||
fn serveSourcesTar(ws: *WebServer, request: *std.http.Server.Request) !void {
|
||||
|
|
@ -508,6 +535,10 @@ pub fn coverageRun(ws: *WebServer) void {
|
|||
error.AlreadyReported => continue,
|
||||
else => |e| log.err("failed to prepare code coverage tables: {s}", .{@errorName(e)}),
|
||||
},
|
||||
.entry_point => |entry_point| addEntryPoint(ws, entry_point.coverage_id, entry_point.addr) catch |err| switch (err) {
|
||||
error.AlreadyReported => continue,
|
||||
else => |e| log.err("failed to prepare code coverage tables: {s}", .{@errorName(e)}),
|
||||
},
|
||||
};
|
||||
ws.msg_queue.clearRetainingCapacity();
|
||||
}
|
||||
|
|
@ -538,6 +569,7 @@ fn prepareTables(
|
|||
.coverage = std.debug.Coverage.init,
|
||||
.mapped_memory = undefined, // populated below
|
||||
.source_locations = undefined, // populated below
|
||||
.entry_points = .{},
|
||||
};
|
||||
errdefer gop.value_ptr.coverage.deinit(gpa);
|
||||
|
||||
|
|
@ -597,6 +629,32 @@ fn prepareTables(
|
|||
ws.coverage_condition.broadcast();
|
||||
}
|
||||
|
||||
fn addEntryPoint(ws: *WebServer, coverage_id: u64, addr: u64) error{ AlreadyReported, OutOfMemory }!void {
|
||||
ws.coverage_mutex.lock();
|
||||
defer ws.coverage_mutex.unlock();
|
||||
|
||||
const coverage_map = ws.coverage_files.getPtr(coverage_id).?;
|
||||
const ptr = coverage_map.mapped_memory;
|
||||
const pcs_bytes = ptr[@sizeOf(abi.SeenPcsHeader)..][0 .. coverage_map.source_locations.len * @sizeOf(usize)];
|
||||
const pcs: []const usize = @alignCast(std.mem.bytesAsSlice(usize, pcs_bytes));
|
||||
const index = std.sort.upperBound(usize, addr, pcs, {}, std.sort.asc(usize));
|
||||
if (index >= pcs.len) {
|
||||
log.err("unable to find unit test entry address 0x{x} in source locations (range: 0x{x} to 0x{x})", .{
|
||||
addr, pcs[0], pcs[pcs.len - 1],
|
||||
});
|
||||
return error.AlreadyReported;
|
||||
}
|
||||
if (false) {
|
||||
const sl = coverage_map.source_locations[index];
|
||||
const file_name = coverage_map.coverage.stringAt(coverage_map.coverage.fileAt(sl.file).basename);
|
||||
log.debug("server found entry point {s}:{d}:{d}", .{
|
||||
file_name, sl.line, sl.column,
|
||||
});
|
||||
}
|
||||
const gpa = ws.gpa;
|
||||
try coverage_map.entry_points.append(gpa, @intCast(index));
|
||||
}
|
||||
|
||||
fn makeIov(s: []const u8) std.posix.iovec_const {
|
||||
return .{
|
||||
.base = s.ptr,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ pub const SeenPcsHeader = extern struct {
|
|||
pub const ToClientTag = enum(u8) {
|
||||
source_index,
|
||||
coverage_update,
|
||||
entry_points,
|
||||
_,
|
||||
};
|
||||
|
||||
|
|
@ -53,3 +54,16 @@ pub const CoverageUpdateHeader = extern struct {
|
|||
unique_runs: u64 align(1),
|
||||
lowest_stack: u64 align(1),
|
||||
};
|
||||
|
||||
/// Sent to the fuzzer web client when the set of entry points is updated.
|
||||
///
|
||||
/// Trailing:
|
||||
/// * one u32 index of source_locations per locs_len
|
||||
pub const EntryPointHeader = extern struct {
|
||||
flags: Flags,
|
||||
|
||||
pub const Flags = packed struct(u32) {
|
||||
tag: ToClientTag = .entry_points,
|
||||
locs_len: u24,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1427,6 +1427,7 @@ fn evalZigTest(
|
|||
var log_err_count: u32 = 0;
|
||||
|
||||
var metadata: ?TestMetadata = null;
|
||||
var coverage_id: ?u64 = null;
|
||||
|
||||
var sub_prog_node: ?std.Progress.Node = null;
|
||||
defer if (sub_prog_node) |n| n.end();
|
||||
|
|
@ -1517,17 +1518,31 @@ fn evalZigTest(
|
|||
.coverage_id => {
|
||||
const web_server = fuzz_context.?.web_server;
|
||||
const msg_ptr: *align(1) const u64 = @ptrCast(body);
|
||||
const coverage_id = msg_ptr.*;
|
||||
coverage_id = msg_ptr.*;
|
||||
{
|
||||
web_server.mutex.lock();
|
||||
defer web_server.mutex.unlock();
|
||||
try web_server.msg_queue.append(web_server.gpa, .{ .coverage = .{
|
||||
.id = coverage_id,
|
||||
.id = coverage_id.?,
|
||||
.run = run,
|
||||
} });
|
||||
web_server.condition.signal();
|
||||
}
|
||||
},
|
||||
.fuzz_start_addr => {
|
||||
const web_server = fuzz_context.?.web_server;
|
||||
const msg_ptr: *align(1) const u64 = @ptrCast(body);
|
||||
const addr = msg_ptr.*;
|
||||
{
|
||||
web_server.mutex.lock();
|
||||
defer web_server.mutex.unlock();
|
||||
try web_server.msg_queue.append(web_server.gpa, .{ .entry_point = .{
|
||||
.addr = addr,
|
||||
.coverage_id = coverage_id.?,
|
||||
} });
|
||||
web_server.condition.signal();
|
||||
}
|
||||
},
|
||||
else => {}, // ignore other messages
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ pub const Message = struct {
|
|||
/// to store coverage information. The integer is a hash of the PCs
|
||||
/// stored within that file.
|
||||
coverage_id,
|
||||
/// Body is a u64le that indicates the function pointer virtual memory
|
||||
/// address of the fuzz unit test. This is used to provide a starting
|
||||
/// point to view coverage.
|
||||
fuzz_start_addr,
|
||||
|
||||
_,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue