| author | |
| committer | |
| log | eccd06f5d01d05286691bc77e6d1e582bb14b7b1 |
| tree | cacf35cfc07672e30623672a003efed8019db7b7 |
| parent | 4fba7336a9038b4abf647caf822f89df717d3cc0 |
| parent | e3f58bd5515ffd0039c7f5afde8b9d74dc5a24b5 |
| signature |
rework fuzzing API to accept a function pointer parameter14 files changed, 1042 insertions(+), 925 deletions(-)
lib/compiler/test_runner.zig+77-32| ... | @@ -145,31 +145,23 @@ fn mainServer() !void { | ... | @@ -145,31 +145,23 @@ fn mainServer() !void { |
| 145 | .start_fuzzing => { | 145 | .start_fuzzing => { |
| 146 | if (!builtin.fuzz) unreachable; | 146 | if (!builtin.fuzz) unreachable; |
| 147 | const index = try server.receiveBody_u32(); | 147 | const index = try server.receiveBody_u32(); |
| 148 | var first = true; | ||
| 149 | const test_fn = builtin.test_functions[index]; | 148 | const test_fn = builtin.test_functions[index]; |
| 150 | while (true) { | 149 | const entry_addr = @intFromPtr(test_fn.func); |
| 151 | testing.allocator_instance = .{}; | 150 | try server.serveU64Message(.fuzz_start_addr, entry_addr); |
| 152 | defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1); | 151 | defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1); |
| 153 | log_err_count = 0; | 152 | is_fuzz_test = false; |
| 154 | is_fuzz_test = false; | 153 | test_fn.func() catch |err| switch (err) { |
| 155 | test_fn.func() catch |err| switch (err) { | 154 | error.SkipZigTest => return, |
| 156 | error.SkipZigTest => continue, | 155 | else => { |
| 157 | else => { | 156 | if (@errorReturnTrace()) |trace| { |
| 158 | if (@errorReturnTrace()) |trace| { | 157 | std.debug.dumpStackTrace(trace.*); |
| 159 | std.debug.dumpStackTrace(trace.*); | 158 | } |
| 160 | } | 159 | std.debug.print("failed with error.{s}\n", .{@errorName(err)}); |
| 161 | std.debug.print("failed with error.{s}\n", .{@errorName(err)}); | 160 | std.process.exit(1); |
| 162 | std.process.exit(1); | 161 | }, |
| 163 | }, | 162 | }; |
| 164 | }; | 163 | if (!is_fuzz_test) @panic("missed call to std.testing.fuzz"); |
| 165 | if (!is_fuzz_test) @panic("missed call to std.testing.fuzzInput"); | 164 | if (log_err_count != 0) @panic("error logs detected"); |
| 166 | if (log_err_count != 0) @panic("error logs detected"); | ||
| 167 | if (first) { | ||
| 168 | first = false; | ||
| 169 | const entry_addr = @intFromPtr(test_fn.func); | ||
| 170 | try server.serveU64Message(.fuzz_start_addr, entry_addr); | ||
| 171 | } | ||
| 172 | } | ||
| 173 | }, | 165 | }, |
| 174 | 166 | ||
| 175 | else => { | 167 | else => { |
| ... | @@ -349,19 +341,72 @@ const FuzzerSlice = extern struct { | ... | @@ -349,19 +341,72 @@ const FuzzerSlice = extern struct { |
| 349 | 341 | ||
| 350 | var is_fuzz_test: bool = undefined; | 342 | var is_fuzz_test: bool = undefined; |
| 351 | 343 | ||
| 352 | extern fn fuzzer_next() FuzzerSlice; | 344 | extern fn fuzzer_start(testOne: *const fn ([*]const u8, usize) callconv(.C) void) void; |
| 353 | extern fn fuzzer_init(cache_dir: FuzzerSlice) void; | 345 | extern fn fuzzer_init(cache_dir: FuzzerSlice) void; |
| 354 | extern fn fuzzer_coverage_id() u64; | 346 | extern fn fuzzer_coverage_id() u64; |
| 355 | 347 | ||
| 356 | pub fn fuzzInput(options: testing.FuzzInputOptions) []const u8 { | 348 | pub fn fuzz( |
| 349 | comptime testOne: fn ([]const u8) anyerror!void, | ||
| 350 | options: testing.FuzzInputOptions, | ||
| 351 | ) anyerror!void { | ||
| 352 | // Prevent this function from confusing the fuzzer by omitting its own code | ||
| 353 | // coverage from being considered. | ||
| 357 | @disableInstrumentation(); | 354 | @disableInstrumentation(); |
| 358 | if (crippled) return ""; | 355 | |
| 356 | // Some compiler backends are not capable of handling fuzz testing yet but | ||
| 357 | // we still want CI test coverage enabled. | ||
| 358 | if (crippled) return; | ||
| 359 | |||
| 360 | // Smoke test to ensure the test did not use conditional compilation to | ||
| 361 | // contradict itself by making it not actually be a fuzz test when the test | ||
| 362 | // is built in fuzz mode. | ||
| 359 | is_fuzz_test = true; | 363 | is_fuzz_test = true; |
| 364 | |||
| 365 | // Ensure no test failure occurred before starting fuzzing. | ||
| 366 | if (log_err_count != 0) @panic("error logs detected"); | ||
| 367 | |||
| 368 | // libfuzzer is in a separate compilation unit so that its own code can be | ||
| 369 | // excluded from code coverage instrumentation. It needs a function pointer | ||
| 370 | // it can call for checking exactly one input. Inside this function we do | ||
| 371 | // our standard unit test checks such as memory leaks, and interaction with | ||
| 372 | // error logs. | ||
| 373 | const global = struct { | ||
| 374 | fn fuzzer_one(input_ptr: [*]const u8, input_len: usize) callconv(.C) void { | ||
| 375 | @disableInstrumentation(); | ||
| 376 | testing.allocator_instance = .{}; | ||
| 377 | defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1); | ||
| 378 | log_err_count = 0; | ||
| 379 | testOne(input_ptr[0..input_len]) catch |err| switch (err) { | ||
| 380 | error.SkipZigTest => return, | ||
| 381 | else => { | ||
| 382 | std.debug.lockStdErr(); | ||
| 383 | if (@errorReturnTrace()) |trace| std.debug.dumpStackTrace(trace.*); | ||
| 384 | std.debug.print("failed with error.{s}\n", .{@errorName(err)}); | ||
| 385 | std.process.exit(1); | ||
| 386 | }, | ||
| 387 | }; | ||
| 388 | if (log_err_count != 0) { | ||
| 389 | std.debug.lockStdErr(); | ||
| 390 | std.debug.print("error logs detected\n", .{}); | ||
| 391 | std.process.exit(1); | ||
| 392 | } | ||
| 393 | } | ||
| 394 | }; | ||
| 360 | if (builtin.fuzz) { | 395 | if (builtin.fuzz) { |
| 361 | return fuzzer_next().toSlice(); | 396 | const prev_allocator_state = testing.allocator_instance; |
| 397 | testing.allocator_instance = .{}; | ||
| 398 | fuzzer_start(&global.fuzzer_one); | ||
| 399 | testing.allocator_instance = prev_allocator_state; | ||
| 400 | return; | ||
| 362 | } | 401 | } |
| 363 | if (options.corpus.len == 0) return ""; | 402 | |
| 364 | var prng = std.Random.DefaultPrng.init(testing.random_seed); | 403 | // When the unit test executable is not built in fuzz mode, only run the |
| 365 | const random = prng.random(); | 404 | // provided corpus. |
| 366 | return options.corpus[random.uintLessThan(usize, options.corpus.len)]; | 405 | for (options.corpus) |input| { |
| 406 | try testOne(input); | ||
| 407 | } | ||
| 408 | |||
| 409 | // In case there is no provided corpus, also use an empty | ||
| 410 | // string as a smoke test. | ||
| 411 | try testOne(""); | ||
| 367 | } | 412 | } |
lib/fuzzer.zig+48-41| ... | @@ -28,7 +28,8 @@ fn logOverride( | ... | @@ -28,7 +28,8 @@ fn logOverride( |
| 28 | f.writer().print(prefix1 ++ prefix2 ++ format ++ "\n", args) catch @panic("failed to write to fuzzer log"); | 28 | f.writer().print(prefix1 ++ prefix2 ++ format ++ "\n", args) catch @panic("failed to write to fuzzer log"); |
| 29 | } | 29 | } |
| 30 | 30 | ||
| 31 | export threadlocal var __sancov_lowest_stack: usize = std.math.maxInt(usize); | 31 | /// Helps determine run uniqueness in the face of recursion. |
| 32 | export threadlocal var __sancov_lowest_stack: usize = 0; | ||
| 32 | 33 | ||
| 33 | export fn __sanitizer_cov_trace_const_cmp1(arg1: u8, arg2: u8) void { | 34 | export fn __sanitizer_cov_trace_const_cmp1(arg1: u8, arg2: u8) void { |
| 34 | handleCmp(@returnAddress(), arg1, arg2); | 35 | handleCmp(@returnAddress(), arg1, arg2); |
| ... | @@ -220,7 +221,6 @@ const Fuzzer = struct { | ... | @@ -220,7 +221,6 @@ const Fuzzer = struct { |
| 220 | .n_runs = 0, | 221 | .n_runs = 0, |
| 221 | .unique_runs = 0, | 222 | .unique_runs = 0, |
| 222 | .pcs_len = pcs.len, | 223 | .pcs_len = pcs.len, |
| 223 | .lowest_stack = std.math.maxInt(usize), | ||
| 224 | }; | 224 | }; |
| 225 | f.seen_pcs.appendSliceAssumeCapacity(std.mem.asBytes(&header)); | 225 | f.seen_pcs.appendSliceAssumeCapacity(std.mem.asBytes(&header)); |
| 226 | f.seen_pcs.appendNTimesAssumeCapacity(0, n_bitset_elems * @sizeOf(usize)); | 226 | f.seen_pcs.appendNTimesAssumeCapacity(0, n_bitset_elems * @sizeOf(usize)); |
| ... | @@ -235,22 +235,41 @@ const Fuzzer = struct { | ... | @@ -235,22 +235,41 @@ const Fuzzer = struct { |
| 235 | }; | 235 | }; |
| 236 | } | 236 | } |
| 237 | 237 | ||
| 238 | fn next(f: *Fuzzer) ![]const u8 { | 238 | fn start(f: *Fuzzer) !void { |
| 239 | const gpa = f.gpa; | 239 | const gpa = f.gpa; |
| 240 | const rng = fuzzer.rng.random(); | 240 | const rng = fuzzer.rng.random(); |
| 241 | 241 | ||
| 242 | if (f.recent_cases.entries.len == 0) { | 242 | // Prepare initial input. |
| 243 | // Prepare initial input. | 243 | assert(f.recent_cases.entries.len == 0); |
| 244 | try f.recent_cases.ensureUnusedCapacity(gpa, 100); | 244 | assert(f.n_runs == 0); |
| 245 | const len = rng.uintLessThanBiased(usize, 80); | 245 | try f.recent_cases.ensureUnusedCapacity(gpa, 100); |
| 246 | try f.input.resize(gpa, len); | 246 | const len = rng.uintLessThanBiased(usize, 80); |
| 247 | rng.bytes(f.input.items); | 247 | try f.input.resize(gpa, len); |
| 248 | f.recent_cases.putAssumeCapacity(.{ | 248 | rng.bytes(f.input.items); |
| 249 | .id = 0, | 249 | f.recent_cases.putAssumeCapacity(.{ |
| 250 | .input = try gpa.dupe(u8, f.input.items), | 250 | .id = 0, |
| 251 | .score = 0, | 251 | .input = try gpa.dupe(u8, f.input.items), |
| 252 | }, {}); | 252 | .score = 0, |
| 253 | } else { | 253 | }, {}); |
| 254 | |||
| 255 | const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]); | ||
| 256 | |||
| 257 | while (true) { | ||
| 258 | const chosen_index = rng.uintLessThanBiased(usize, f.recent_cases.entries.len); | ||
| 259 | const run = &f.recent_cases.keys()[chosen_index]; | ||
| 260 | f.input.clearRetainingCapacity(); | ||
| 261 | f.input.appendSliceAssumeCapacity(run.input); | ||
| 262 | try f.mutate(); | ||
| 263 | |||
| 264 | @memset(f.pc_counters, 0); | ||
| 265 | __sancov_lowest_stack = std.math.maxInt(usize); | ||
| 266 | f.coverage.reset(); | ||
| 267 | |||
| 268 | fuzzer_one(f.input.items.ptr, f.input.items.len); | ||
| 269 | |||
| 270 | f.n_runs += 1; | ||
| 271 | _ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic); | ||
| 272 | |||
| 254 | if (f.n_runs % 10000 == 0) f.dumpStats(); | 273 | if (f.n_runs % 10000 == 0) f.dumpStats(); |
| 255 | 274 | ||
| 256 | const analysis = f.analyzeLastRun(); | 275 | const analysis = f.analyzeLastRun(); |
| ... | @@ -301,7 +320,6 @@ const Fuzzer = struct { | ... | @@ -301,7 +320,6 @@ const Fuzzer = struct { |
| 301 | } | 320 | } |
| 302 | } | 321 | } |
| 303 | 322 | ||
| 304 | const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]); | ||
| 305 | _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic); | 323 | _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic); |
| 306 | } | 324 | } |
| 307 | 325 | ||
| ... | @@ -317,26 +335,12 @@ const Fuzzer = struct { | ... | @@ -317,26 +335,12 @@ const Fuzzer = struct { |
| 317 | // This has to be done before deinitializing the deleted items. | 335 | // This has to be done before deinitializing the deleted items. |
| 318 | const doomed_runs = f.recent_cases.keys()[cap..]; | 336 | const doomed_runs = f.recent_cases.keys()[cap..]; |
| 319 | f.recent_cases.shrinkRetainingCapacity(cap); | 337 | f.recent_cases.shrinkRetainingCapacity(cap); |
| 320 | for (doomed_runs) |*run| { | 338 | for (doomed_runs) |*doomed_run| { |
| 321 | std.log.info("culling score={d} id={d}", .{ run.score, run.id }); | 339 | std.log.info("culling score={d} id={d}", .{ doomed_run.score, doomed_run.id }); |
| 322 | run.deinit(gpa); | 340 | doomed_run.deinit(gpa); |
| 323 | } | 341 | } |
| 324 | } | 342 | } |
| 325 | } | 343 | } |
| 326 | |||
| 327 | const chosen_index = rng.uintLessThanBiased(usize, f.recent_cases.entries.len); | ||
| 328 | const run = &f.recent_cases.keys()[chosen_index]; | ||
| 329 | f.input.clearRetainingCapacity(); | ||
| 330 | f.input.appendSliceAssumeCapacity(run.input); | ||
| 331 | try f.mutate(); | ||
| 332 | |||
| 333 | f.n_runs += 1; | ||
| 334 | const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]); | ||
| 335 | _ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic); | ||
| 336 | _ = @atomicRmw(usize, &header.lowest_stack, .Min, __sancov_lowest_stack, .monotonic); | ||
| 337 | @memset(f.pc_counters, 0); | ||
| 338 | f.coverage.reset(); | ||
| 339 | return f.input.items; | ||
| 340 | } | 344 | } |
| 341 | 345 | ||
| 342 | fn visitPc(f: *Fuzzer, pc: usize) void { | 346 | fn visitPc(f: *Fuzzer, pc: usize) void { |
| ... | @@ -419,10 +423,13 @@ export fn fuzzer_coverage_id() u64 { | ... | @@ -419,10 +423,13 @@ export fn fuzzer_coverage_id() u64 { |
| 419 | return fuzzer.coverage_id; | 423 | return fuzzer.coverage_id; |
| 420 | } | 424 | } |
| 421 | 425 | ||
| 422 | export fn fuzzer_next() Fuzzer.Slice { | 426 | var fuzzer_one: *const fn (input_ptr: [*]const u8, input_len: usize) callconv(.C) void = undefined; |
| 423 | return Fuzzer.Slice.fromZig(fuzzer.next() catch |err| switch (err) { | 427 | |
| 424 | error.OutOfMemory => @panic("out of memory"), | 428 | export fn fuzzer_start(testOne: @TypeOf(fuzzer_one)) void { |
| 425 | }); | 429 | fuzzer_one = testOne; |
| 430 | fuzzer.start() catch |err| switch (err) { | ||
| 431 | error.OutOfMemory => fatal("out of memory", .{}), | ||
| 432 | }; | ||
| 426 | } | 433 | } |
| 427 | 434 | ||
| 428 | export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void { | 435 | export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void { |
| ... | @@ -432,24 +439,24 @@ export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void { | ... | @@ -432,24 +439,24 @@ export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void { |
| 432 | const pc_counters_start = @extern([*]u8, .{ | 439 | const pc_counters_start = @extern([*]u8, .{ |
| 433 | .name = "__start___sancov_cntrs", | 440 | .name = "__start___sancov_cntrs", |
| 434 | .linkage = .weak, | 441 | .linkage = .weak, |
| 435 | }) orelse fatal("missing __start___sancov_cntrs symbol"); | 442 | }) orelse fatal("missing __start___sancov_cntrs symbol", .{}); |
| 436 | 443 | ||
| 437 | const pc_counters_end = @extern([*]u8, .{ | 444 | const pc_counters_end = @extern([*]u8, .{ |
| 438 | .name = "__stop___sancov_cntrs", | 445 | .name = "__stop___sancov_cntrs", |
| 439 | .linkage = .weak, | 446 | .linkage = .weak, |
| 440 | }) orelse fatal("missing __stop___sancov_cntrs symbol"); | 447 | }) orelse fatal("missing __stop___sancov_cntrs symbol", .{}); |
| 441 | 448 | ||
| 442 | const pc_counters = pc_counters_start[0 .. pc_counters_end - pc_counters_start]; | 449 | const pc_counters = pc_counters_start[0 .. pc_counters_end - pc_counters_start]; |
| 443 | 450 | ||
| 444 | const pcs_start = @extern([*]usize, .{ | 451 | const pcs_start = @extern([*]usize, .{ |
| 445 | .name = "__start___sancov_pcs1", | 452 | .name = "__start___sancov_pcs1", |
| 446 | .linkage = .weak, | 453 | .linkage = .weak, |
| 447 | }) orelse fatal("missing __start___sancov_pcs1 symbol"); | 454 | }) orelse fatal("missing __start___sancov_pcs1 symbol", .{}); |
| 448 | 455 | ||
| 449 | const pcs_end = @extern([*]usize, .{ | 456 | const pcs_end = @extern([*]usize, .{ |
| 450 | .name = "__stop___sancov_pcs1", | 457 | .name = "__stop___sancov_pcs1", |
| 451 | .linkage = .weak, | 458 | .linkage = .weak, |
| 452 | }) orelse fatal("missing __stop___sancov_pcs1 symbol"); | 459 | }) orelse fatal("missing __stop___sancov_pcs1 symbol", .{}); |
| 453 | 460 | ||
| 454 | const pcs = pcs_start[0 .. pcs_end - pcs_start]; | 461 | const pcs = pcs_start[0 .. pcs_end - pcs_start]; |
| 455 | 462 |
lib/fuzzer/index.html deleted-161| ... | @@ -1,161 +0,0 @@ | ||
| 1 | <!doctype html> | ||
| 2 | <html> | ||
| 3 | <head> | ||
| 4 | <meta charset="utf-8"> | ||
| 5 | <title>Zig Build System Interface</title> | ||
| 6 | <style type="text/css"> | ||
| 7 | body { | ||
| 8 | font-family: system-ui, -apple-system, Roboto, "Segoe UI", sans-serif; | ||
| 9 | color: #000000; | ||
| 10 | } | ||
| 11 | .hidden { | ||
| 12 | display: none; | ||
| 13 | } | ||
| 14 | table { | ||
| 15 | width: 100%; | ||
| 16 | } | ||
| 17 | a { | ||
| 18 | color: #2A6286; | ||
| 19 | } | ||
| 20 | pre{ | ||
| 21 | font-family:"Source Code Pro",monospace; | ||
| 22 | font-size:1em; | ||
| 23 | background-color:#F5F5F5; | ||
| 24 | padding: 1em; | ||
| 25 | margin: 0; | ||
| 26 | overflow-x: auto; | ||
| 27 | } | ||
| 28 | :not(pre) > code { | ||
| 29 | white-space: break-spaces; | ||
| 30 | } | ||
| 31 | code { | ||
| 32 | font-family:"Source Code Pro",monospace; | ||
| 33 | font-size: 0.9em; | ||
| 34 | } | ||
| 35 | code a { | ||
| 36 | color: #000000; | ||
| 37 | } | ||
| 38 | kbd { | ||
| 39 | color: #000; | ||
| 40 | background-color: #fafbfc; | ||
| 41 | border-color: #d1d5da; | ||
| 42 | border-bottom-color: #c6cbd1; | ||
| 43 | box-shadow-color: #c6cbd1; | ||
| 44 | display: inline-block; | ||
| 45 | padding: 0.3em 0.2em; | ||
| 46 | font: 1.2em monospace; | ||
| 47 | line-height: 0.8em; | ||
| 48 | vertical-align: middle; | ||
| 49 | border: solid 1px; | ||
| 50 | border-radius: 3px; | ||
| 51 | box-shadow: inset 0 -1px 0; | ||
| 52 | cursor: default; | ||
| 53 | } | ||
| 54 | |||
| 55 | .l { | ||
| 56 | display: inline-block; | ||
| 57 | background: red; | ||
| 58 | width: 1em; | ||
| 59 | height: 1em; | ||
| 60 | border-radius: 1em; | ||
| 61 | } | ||
| 62 | .c { | ||
| 63 | background-color: green; | ||
| 64 | } | ||
| 65 | |||
| 66 | .tok-kw { | ||
| 67 | color: #333; | ||
| 68 | font-weight: bold; | ||
| 69 | } | ||
| 70 | .tok-str { | ||
| 71 | color: #d14; | ||
| 72 | } | ||
| 73 | .tok-builtin { | ||
| 74 | color: #0086b3; | ||
| 75 | } | ||
| 76 | .tok-comment { | ||
| 77 | color: #777; | ||
| 78 | font-style: italic; | ||
| 79 | } | ||
| 80 | .tok-fn { | ||
| 81 | color: #900; | ||
| 82 | font-weight: bold; | ||
| 83 | } | ||
| 84 | .tok-null { | ||
| 85 | color: #008080; | ||
| 86 | } | ||
| 87 | .tok-number { | ||
| 88 | color: #008080; | ||
| 89 | } | ||
| 90 | .tok-type { | ||
| 91 | color: #458; | ||
| 92 | font-weight: bold; | ||
| 93 | } | ||
| 94 | |||
| 95 | @media (prefers-color-scheme: dark) { | ||
| 96 | body { | ||
| 97 | background-color: #111; | ||
| 98 | color: #bbb; | ||
| 99 | } | ||
| 100 | pre { | ||
| 101 | background-color: #222; | ||
| 102 | color: #ccc; | ||
| 103 | } | ||
| 104 | a { | ||
| 105 | color: #88f; | ||
| 106 | } | ||
| 107 | code a { | ||
| 108 | color: #ccc; | ||
| 109 | } | ||
| 110 | .l { | ||
| 111 | background-color: red; | ||
| 112 | } | ||
| 113 | .c { | ||
| 114 | background-color: green; | ||
| 115 | } | ||
| 116 | .tok-kw { | ||
| 117 | color: #eee; | ||
| 118 | } | ||
| 119 | .tok-str { | ||
| 120 | color: #2e5; | ||
| 121 | } | ||
| 122 | .tok-builtin { | ||
| 123 | color: #ff894c; | ||
| 124 | } | ||
| 125 | .tok-comment { | ||
| 126 | color: #aa7; | ||
| 127 | } | ||
| 128 | .tok-fn { | ||
| 129 | color: #B1A0F8; | ||
| 130 | } | ||
| 131 | .tok-null { | ||
| 132 | color: #ff8080; | ||
| 133 | } | ||
| 134 | .tok-number { | ||
| 135 | color: #ff8080; | ||
| 136 | } | ||
| 137 | .tok-type { | ||
| 138 | color: #68f; | ||
| 139 | } | ||
| 140 | } | ||
| 141 | </style> | ||
| 142 | </head> | ||
| 143 | <body> | ||
| 144 | <p id="status">Loading JavaScript...</p> | ||
| 145 | <div id="sectStats" class="hidden"> | ||
| 146 | <ul> | ||
| 147 | <li>Total Runs: <span id="statTotalRuns"></span></li> | ||
| 148 | <li>Unique Runs: <span id="statUniqueRuns"></span></li> | ||
| 149 | <li>Coverage: <span id="statCoverage"></span></li> | ||
| 150 | <li>Lowest Stack: <span id="statLowestStack"></span></li> | ||
| 151 | <li>Entry Points: <ul id="entryPointsList"></ul></li> | ||
| 152 | </ul> | ||
| 153 | </div> | ||
| 154 | <div id="sectSource" class="hidden"> | ||
| 155 | <h2>Source Code</h2> | ||
| 156 | <pre><code id="sourceText"></code></pre> | ||
| 157 | </div> | ||
| 158 | <script src="main.js"></script> | ||
| 159 | </body> | ||
| 160 | </html> | ||
| 161 | |||
lib/fuzzer/main.js deleted-249| ... | @@ -1,249 +0,0 @@ | ||
| 1 | (function() { | ||
| 2 | const domStatus = document.getElementById("status"); | ||
| 3 | const domSectSource = document.getElementById("sectSource"); | ||
| 4 | const domSectStats = document.getElementById("sectStats"); | ||
| 5 | const domSourceText = document.getElementById("sourceText"); | ||
| 6 | const domStatTotalRuns = document.getElementById("statTotalRuns"); | ||
| 7 | const domStatUniqueRuns = document.getElementById("statUniqueRuns"); | ||
| 8 | const domStatCoverage = document.getElementById("statCoverage"); | ||
| 9 | const domStatLowestStack = document.getElementById("statLowestStack"); | ||
| 10 | const domEntryPointsList = document.getElementById("entryPointsList"); | ||
| 11 | |||
| 12 | let wasm_promise = fetch("main.wasm"); | ||
| 13 | let sources_promise = fetch("sources.tar").then(function(response) { | ||
| 14 | if (!response.ok) throw new Error("unable to download sources"); | ||
| 15 | return response.arrayBuffer(); | ||
| 16 | }); | ||
| 17 | var wasm_exports = null; | ||
| 18 | var curNavSearch = null; | ||
| 19 | var curNavLocation = null; | ||
| 20 | |||
| 21 | const text_decoder = new TextDecoder(); | ||
| 22 | const text_encoder = new TextEncoder(); | ||
| 23 | |||
| 24 | domStatus.textContent = "Loading WebAssembly..."; | ||
| 25 | WebAssembly.instantiateStreaming(wasm_promise, { | ||
| 26 | js: { | ||
| 27 | log: function(ptr, len) { | ||
| 28 | const msg = decodeString(ptr, len); | ||
| 29 | console.log(msg); | ||
| 30 | }, | ||
| 31 | panic: function (ptr, len) { | ||
| 32 | const msg = decodeString(ptr, len); | ||
| 33 | throw new Error("panic: " + msg); | ||
| 34 | }, | ||
| 35 | emitSourceIndexChange: onSourceIndexChange, | ||
| 36 | emitCoverageUpdate: onCoverageUpdate, | ||
| 37 | emitEntryPointsUpdate: renderStats, | ||
| 38 | }, | ||
| 39 | }).then(function(obj) { | ||
| 40 | wasm_exports = obj.instance.exports; | ||
| 41 | window.wasm = obj; // for debugging | ||
| 42 | domStatus.textContent = "Loading sources tarball..."; | ||
| 43 | |||
| 44 | sources_promise.then(function(buffer) { | ||
| 45 | domStatus.textContent = "Parsing sources..."; | ||
| 46 | const js_array = new Uint8Array(buffer); | ||
| 47 | const ptr = wasm_exports.alloc(js_array.length); | ||
| 48 | const wasm_array = new Uint8Array(wasm_exports.memory.buffer, ptr, js_array.length); | ||
| 49 | wasm_array.set(js_array); | ||
| 50 | wasm_exports.unpack(ptr, js_array.length); | ||
| 51 | |||
| 52 | window.addEventListener('popstate', onPopState, false); | ||
| 53 | onHashChange(null); | ||
| 54 | |||
| 55 | domStatus.textContent = "Waiting for server to send source location metadata..."; | ||
| 56 | connectWebSocket(); | ||
| 57 | }); | ||
| 58 | }); | ||
| 59 | |||
| 60 | function onPopState(ev) { | ||
| 61 | onHashChange(ev.state); | ||
| 62 | } | ||
| 63 | |||
| 64 | function onHashChange(state) { | ||
| 65 | history.replaceState({}, ""); | ||
| 66 | navigate(location.hash); | ||
| 67 | if (state == null) window.scrollTo({top: 0}); | ||
| 68 | } | ||
| 69 | |||
| 70 | function navigate(location_hash) { | ||
| 71 | domSectSource.classList.add("hidden"); | ||
| 72 | |||
| 73 | curNavLocation = null; | ||
| 74 | curNavSearch = null; | ||
| 75 | |||
| 76 | if (location_hash.length > 1 && location_hash[0] === '#') { | ||
| 77 | const query = location_hash.substring(1); | ||
| 78 | const qpos = query.indexOf("?"); | ||
| 79 | let nonSearchPart; | ||
| 80 | if (qpos === -1) { | ||
| 81 | nonSearchPart = query; | ||
| 82 | } else { | ||
| 83 | nonSearchPart = query.substring(0, qpos); | ||
| 84 | curNavSearch = decodeURIComponent(query.substring(qpos + 1)); | ||
| 85 | } | ||
| 86 | |||
| 87 | if (nonSearchPart[0] == "l") { | ||
| 88 | curNavLocation = +nonSearchPart.substring(1); | ||
| 89 | renderSource(curNavLocation); | ||
| 90 | } | ||
| 91 | } | ||
| 92 | |||
| 93 | render(); | ||
| 94 | } | ||
| 95 | |||
| 96 | function connectWebSocket() { | ||
| 97 | const host = document.location.host; | ||
| 98 | const pathname = document.location.pathname; | ||
| 99 | const isHttps = document.location.protocol === 'https:'; | ||
| 100 | const match = host.match(/^(.+):(\d+)$/); | ||
| 101 | const defaultPort = isHttps ? 443 : 80; | ||
| 102 | const port = match ? parseInt(match[2], 10) : defaultPort; | ||
| 103 | const hostName = match ? match[1] : host; | ||
| 104 | const wsProto = isHttps ? "wss:" : "ws:"; | ||
| 105 | const wsUrl = wsProto + '//' + hostName + ':' + port + pathname; | ||
| 106 | ws = new WebSocket(wsUrl); | ||
| 107 | ws.binaryType = "arraybuffer"; | ||
| 108 | ws.addEventListener('message', onWebSocketMessage, false); | ||
| 109 | ws.addEventListener('error', timeoutThenCreateNew, false); | ||
| 110 | ws.addEventListener('close', timeoutThenCreateNew, false); | ||
| 111 | ws.addEventListener('open', onWebSocketOpen, false); | ||
| 112 | } | ||
| 113 | |||
| 114 | function onWebSocketOpen() { | ||
| 115 | //console.log("web socket opened"); | ||
| 116 | } | ||
| 117 | |||
| 118 | function onWebSocketMessage(ev) { | ||
| 119 | wasmOnMessage(ev.data); | ||
| 120 | } | ||
| 121 | |||
| 122 | function timeoutThenCreateNew() { | ||
| 123 | ws.removeEventListener('message', onWebSocketMessage, false); | ||
| 124 | ws.removeEventListener('error', timeoutThenCreateNew, false); | ||
| 125 | ws.removeEventListener('close', timeoutThenCreateNew, false); | ||
| 126 | ws.removeEventListener('open', onWebSocketOpen, false); | ||
| 127 | ws = null; | ||
| 128 | setTimeout(connectWebSocket, 1000); | ||
| 129 | } | ||
| 130 | |||
| 131 | function wasmOnMessage(data) { | ||
| 132 | const jsArray = new Uint8Array(data); | ||
| 133 | const ptr = wasm_exports.message_begin(jsArray.length); | ||
| 134 | const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, jsArray.length); | ||
| 135 | wasmArray.set(jsArray); | ||
| 136 | wasm_exports.message_end(); | ||
| 137 | } | ||
| 138 | |||
| 139 | function onSourceIndexChange() { | ||
| 140 | render(); | ||
| 141 | if (curNavLocation != null) renderSource(curNavLocation); | ||
| 142 | } | ||
| 143 | |||
| 144 | function onCoverageUpdate() { | ||
| 145 | renderStats(); | ||
| 146 | renderCoverage(); | ||
| 147 | } | ||
| 148 | |||
| 149 | function render() { | ||
| 150 | domStatus.classList.add("hidden"); | ||
| 151 | } | ||
| 152 | |||
| 153 | function renderStats() { | ||
| 154 | const totalRuns = wasm_exports.totalRuns(); | ||
| 155 | const uniqueRuns = wasm_exports.uniqueRuns(); | ||
| 156 | const totalSourceLocations = wasm_exports.totalSourceLocations(); | ||
| 157 | const coveredSourceLocations = wasm_exports.coveredSourceLocations(); | ||
| 158 | domStatTotalRuns.innerText = totalRuns; | ||
| 159 | domStatUniqueRuns.innerText = uniqueRuns + " (" + percent(uniqueRuns, totalRuns) + "%)"; | ||
| 160 | domStatCoverage.innerText = coveredSourceLocations + " / " + totalSourceLocations + " (" + percent(coveredSourceLocations, totalSourceLocations) + "%)"; | ||
| 161 | domStatLowestStack.innerText = unwrapString(wasm_exports.lowestStack()); | ||
| 162 | |||
| 163 | const entryPoints = unwrapInt32Array(wasm_exports.entryPoints()); | ||
| 164 | resizeDomList(domEntryPointsList, entryPoints.length, "<li></li>"); | ||
| 165 | for (let i = 0; i < entryPoints.length; i += 1) { | ||
| 166 | const liDom = domEntryPointsList.children[i]; | ||
| 167 | liDom.innerHTML = unwrapString(wasm_exports.sourceLocationLinkHtml(entryPoints[i])); | ||
| 168 | } | ||
| 169 | |||
| 170 | |||
| 171 | domSectStats.classList.remove("hidden"); | ||
| 172 | } | ||
| 173 | |||
| 174 | function renderCoverage() { | ||
| 175 | if (curNavLocation == null) return; | ||
| 176 | const sourceLocationIndex = curNavLocation; | ||
| 177 | |||
| 178 | for (let i = 0; i < domSourceText.children.length; i += 1) { | ||
| 179 | const childDom = domSourceText.children[i]; | ||
| 180 | if (childDom.id != null && childDom.id[0] == "l") { | ||
| 181 | childDom.classList.add("l"); | ||
| 182 | childDom.classList.remove("c"); | ||
| 183 | } | ||
| 184 | } | ||
| 185 | const coveredList = unwrapInt32Array(wasm_exports.sourceLocationFileCoveredList(sourceLocationIndex)); | ||
| 186 | for (let i = 0; i < coveredList.length; i += 1) { | ||
| 187 | document.getElementById("l" + coveredList[i]).classList.add("c"); | ||
| 188 | } | ||
| 189 | } | ||
| 190 | |||
| 191 | function resizeDomList(listDom, desiredLen, templateHtml) { | ||
| 192 | for (let i = listDom.childElementCount; i < desiredLen; i += 1) { | ||
| 193 | listDom.insertAdjacentHTML('beforeend', templateHtml); | ||
| 194 | } | ||
| 195 | while (desiredLen < listDom.childElementCount) { | ||
| 196 | listDom.removeChild(listDom.lastChild); | ||
| 197 | } | ||
| 198 | } | ||
| 199 | |||
| 200 | function percent(a, b) { | ||
| 201 | return ((Number(a) / Number(b)) * 100).toFixed(1); | ||
| 202 | } | ||
| 203 | |||
| 204 | function renderSource(sourceLocationIndex) { | ||
| 205 | const pathName = unwrapString(wasm_exports.sourceLocationPath(sourceLocationIndex)); | ||
| 206 | if (pathName.length === 0) return; | ||
| 207 | |||
| 208 | const h2 = domSectSource.children[0]; | ||
| 209 | h2.innerText = pathName; | ||
| 210 | domSourceText.innerHTML = unwrapString(wasm_exports.sourceLocationFileHtml(sourceLocationIndex)); | ||
| 211 | |||
| 212 | domSectSource.classList.remove("hidden"); | ||
| 213 | |||
| 214 | // Empirically, Firefox needs this requestAnimationFrame in order for the scrollIntoView to work. | ||
| 215 | requestAnimationFrame(function() { | ||
| 216 | const slDom = document.getElementById("l" + sourceLocationIndex); | ||
| 217 | if (slDom != null) slDom.scrollIntoView({ | ||
| 218 | behavior: "smooth", | ||
| 219 | block: "center", | ||
| 220 | }); | ||
| 221 | }); | ||
| 222 | } | ||
| 223 | |||
| 224 | function decodeString(ptr, len) { | ||
| 225 | if (len === 0) return ""; | ||
| 226 | return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len)); | ||
| 227 | } | ||
| 228 | |||
| 229 | function unwrapInt32Array(bigint) { | ||
| 230 | const ptr = Number(bigint & 0xffffffffn); | ||
| 231 | const len = Number(bigint >> 32n); | ||
| 232 | if (len === 0) return new Uint32Array(); | ||
| 233 | return new Uint32Array(wasm_exports.memory.buffer, ptr, len); | ||
| 234 | } | ||
| 235 | |||
| 236 | function setInputString(s) { | ||
| 237 | const jsArray = text_encoder.encode(s); | ||
| 238 | const len = jsArray.length; | ||
| 239 | const ptr = wasm_exports.set_input_string(len); | ||
| 240 | const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, len); | ||
| 241 | wasmArray.set(jsArray); | ||
| 242 | } | ||
| 243 | |||
| 244 | function unwrapString(bigint) { | ||
| 245 | const ptr = Number(bigint & 0xffffffffn); | ||
| 246 | const len = Number(bigint >> 32n); | ||
| 247 | return decodeString(ptr, len); | ||
| 248 | } | ||
| 249 | })(); | ||
lib/fuzzer/wasm/main.zig deleted-428| ... | @@ -1,428 +0,0 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const assert = std.debug.assert; | ||
| 3 | const abi = std.Build.Fuzz.abi; | ||
| 4 | const gpa = std.heap.wasm_allocator; | ||
| 5 | const log = std.log; | ||
| 6 | const Coverage = std.debug.Coverage; | ||
| 7 | const Allocator = std.mem.Allocator; | ||
| 8 | |||
| 9 | const Walk = @import("Walk"); | ||
| 10 | const Decl = Walk.Decl; | ||
| 11 | const html_render = @import("html_render"); | ||
| 12 | |||
| 13 | const js = struct { | ||
| 14 | extern "js" fn log(ptr: [*]const u8, len: usize) void; | ||
| 15 | extern "js" fn panic(ptr: [*]const u8, len: usize) noreturn; | ||
| 16 | extern "js" fn emitSourceIndexChange() void; | ||
| 17 | extern "js" fn emitCoverageUpdate() void; | ||
| 18 | extern "js" fn emitEntryPointsUpdate() void; | ||
| 19 | }; | ||
| 20 | |||
| 21 | pub const std_options: std.Options = .{ | ||
| 22 | .logFn = logFn, | ||
| 23 | }; | ||
| 24 | |||
| 25 | pub fn panic(msg: []const u8, st: ?*std.builtin.StackTrace, addr: ?usize) noreturn { | ||
| 26 | _ = st; | ||
| 27 | _ = addr; | ||
| 28 | log.err("panic: {s}", .{msg}); | ||
| 29 | @trap(); | ||
| 30 | } | ||
| 31 | |||
| 32 | fn logFn( | ||
| 33 | comptime message_level: log.Level, | ||
| 34 | comptime scope: @TypeOf(.enum_literal), | ||
| 35 | comptime format: []const u8, | ||
| 36 | args: anytype, | ||
| 37 | ) void { | ||
| 38 | const level_txt = comptime message_level.asText(); | ||
| 39 | const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; | ||
| 40 | var buf: [500]u8 = undefined; | ||
| 41 | const line = std.fmt.bufPrint(&buf, level_txt ++ prefix2 ++ format, args) catch l: { | ||
| 42 | buf[buf.len - 3 ..][0..3].* = "...".*; | ||
| 43 | break :l &buf; | ||
| 44 | }; | ||
| 45 | js.log(line.ptr, line.len); | ||
| 46 | } | ||
| 47 | |||
| 48 | export fn alloc(n: usize) [*]u8 { | ||
| 49 | const slice = gpa.alloc(u8, n) catch @panic("OOM"); | ||
| 50 | return slice.ptr; | ||
| 51 | } | ||
| 52 | |||
| 53 | var message_buffer: std.ArrayListAlignedUnmanaged(u8, @alignOf(u64)) = .{}; | ||
| 54 | |||
| 55 | /// Resizes the message buffer to be the correct length; returns the pointer to | ||
| 56 | /// the query string. | ||
| 57 | export fn message_begin(len: usize) [*]u8 { | ||
| 58 | message_buffer.resize(gpa, len) catch @panic("OOM"); | ||
| 59 | return message_buffer.items.ptr; | ||
| 60 | } | ||
| 61 | |||
| 62 | export fn message_end() void { | ||
| 63 | const msg_bytes = message_buffer.items; | ||
| 64 | |||
| 65 | const tag: abi.ToClientTag = @enumFromInt(msg_bytes[0]); | ||
| 66 | switch (tag) { | ||
| 67 | .source_index => return sourceIndexMessage(msg_bytes) catch @panic("OOM"), | ||
| 68 | .coverage_update => return coverageUpdateMessage(msg_bytes) catch @panic("OOM"), | ||
| 69 | .entry_points => return entryPointsMessage(msg_bytes) catch @panic("OOM"), | ||
| 70 | _ => unreachable, | ||
| 71 | } | ||
| 72 | } | ||
| 73 | |||
| 74 | export fn unpack(tar_ptr: [*]u8, tar_len: usize) void { | ||
| 75 | const tar_bytes = tar_ptr[0..tar_len]; | ||
| 76 | log.debug("received {d} bytes of tar file", .{tar_bytes.len}); | ||
| 77 | |||
| 78 | unpackInner(tar_bytes) catch |err| { | ||
| 79 | fatal("unable to unpack tar: {s}", .{@errorName(err)}); | ||
| 80 | }; | ||
| 81 | } | ||
| 82 | |||
| 83 | /// Set by `set_input_string`. | ||
| 84 | var input_string: std.ArrayListUnmanaged(u8) = .{}; | ||
| 85 | var string_result: std.ArrayListUnmanaged(u8) = .{}; | ||
| 86 | |||
| 87 | export fn set_input_string(len: usize) [*]u8 { | ||
| 88 | input_string.resize(gpa, len) catch @panic("OOM"); | ||
| 89 | return input_string.items.ptr; | ||
| 90 | } | ||
| 91 | |||
| 92 | /// Looks up the root struct decl corresponding to a file by path. | ||
| 93 | /// Uses `input_string`. | ||
| 94 | export fn find_file_root() Decl.Index { | ||
| 95 | const file: Walk.File.Index = @enumFromInt(Walk.files.getIndex(input_string.items) orelse return .none); | ||
| 96 | return file.findRootDecl(); | ||
| 97 | } | ||
| 98 | |||
| 99 | export fn decl_source_html(decl_index: Decl.Index) String { | ||
| 100 | const decl = decl_index.get(); | ||
| 101 | |||
| 102 | string_result.clearRetainingCapacity(); | ||
| 103 | html_render.fileSourceHtml(decl.file, &string_result, decl.ast_node, .{}) catch |err| { | ||
| 104 | fatal("unable to render source: {s}", .{@errorName(err)}); | ||
| 105 | }; | ||
| 106 | return String.init(string_result.items); | ||
| 107 | } | ||
| 108 | |||
| 109 | export fn lowestStack() String { | ||
| 110 | const header: *abi.CoverageUpdateHeader = @ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]); | ||
| 111 | string_result.clearRetainingCapacity(); | ||
| 112 | string_result.writer(gpa).print("0x{d}", .{header.lowest_stack}) catch @panic("OOM"); | ||
| 113 | return String.init(string_result.items); | ||
| 114 | } | ||
| 115 | |||
| 116 | export fn totalSourceLocations() usize { | ||
| 117 | return coverage_source_locations.items.len; | ||
| 118 | } | ||
| 119 | |||
| 120 | export fn coveredSourceLocations() usize { | ||
| 121 | const covered_bits = recent_coverage_update.items[@sizeOf(abi.CoverageUpdateHeader)..]; | ||
| 122 | var count: usize = 0; | ||
| 123 | for (covered_bits) |byte| count += @popCount(byte); | ||
| 124 | return count; | ||
| 125 | } | ||
| 126 | |||
| 127 | export fn totalRuns() u64 { | ||
| 128 | const header: *abi.CoverageUpdateHeader = @alignCast(@ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)])); | ||
| 129 | return header.n_runs; | ||
| 130 | } | ||
| 131 | |||
| 132 | export fn uniqueRuns() u64 { | ||
| 133 | const header: *abi.CoverageUpdateHeader = @alignCast(@ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)])); | ||
| 134 | return header.unique_runs; | ||
| 135 | } | ||
| 136 | |||
| 137 | const String = Slice(u8); | ||
| 138 | |||
| 139 | fn Slice(T: type) type { | ||
| 140 | return packed struct(u64) { | ||
| 141 | ptr: u32, | ||
| 142 | len: u32, | ||
| 143 | |||
| 144 | fn init(s: []const T) @This() { | ||
| 145 | return .{ | ||
| 146 | .ptr = @intFromPtr(s.ptr), | ||
| 147 | .len = s.len, | ||
| 148 | }; | ||
| 149 | } | ||
| 150 | }; | ||
| 151 | } | ||
| 152 | |||
| 153 | fn unpackInner(tar_bytes: []u8) !void { | ||
| 154 | var fbs = std.io.fixedBufferStream(tar_bytes); | ||
| 155 | var file_name_buffer: [1024]u8 = undefined; | ||
| 156 | var link_name_buffer: [1024]u8 = undefined; | ||
| 157 | var it = std.tar.iterator(fbs.reader(), .{ | ||
| 158 | .file_name_buffer = &file_name_buffer, | ||
| 159 | .link_name_buffer = &link_name_buffer, | ||
| 160 | }); | ||
| 161 | while (try it.next()) |tar_file| { | ||
| 162 | switch (tar_file.kind) { | ||
| 163 | .file => { | ||
| 164 | if (tar_file.size == 0 and tar_file.name.len == 0) break; | ||
| 165 | if (std.mem.endsWith(u8, tar_file.name, ".zig")) { | ||
| 166 | log.debug("found file: '{s}'", .{tar_file.name}); | ||
| 167 | const file_name = try gpa.dupe(u8, tar_file.name); | ||
| 168 | if (std.mem.indexOfScalar(u8, file_name, '/')) |pkg_name_end| { | ||
| 169 | const pkg_name = file_name[0..pkg_name_end]; | ||
| 170 | const gop = try Walk.modules.getOrPut(gpa, pkg_name); | ||
| 171 | const file: Walk.File.Index = @enumFromInt(Walk.files.entries.len); | ||
| 172 | if (!gop.found_existing or | ||
| 173 | std.mem.eql(u8, file_name[pkg_name_end..], "/root.zig") or | ||
| 174 | std.mem.eql(u8, file_name[pkg_name_end + 1 .. file_name.len - ".zig".len], pkg_name)) | ||
| 175 | { | ||
| 176 | gop.value_ptr.* = file; | ||
| 177 | } | ||
| 178 | const file_bytes = tar_bytes[fbs.pos..][0..@intCast(tar_file.size)]; | ||
| 179 | assert(file == try Walk.add_file(file_name, file_bytes)); | ||
| 180 | } | ||
| 181 | } else { | ||
| 182 | log.warn("skipping: '{s}' - the tar creation should have done that", .{tar_file.name}); | ||
| 183 | } | ||
| 184 | }, | ||
| 185 | else => continue, | ||
| 186 | } | ||
| 187 | } | ||
| 188 | } | ||
| 189 | |||
| 190 | fn fatal(comptime format: []const u8, args: anytype) noreturn { | ||
| 191 | var buf: [500]u8 = undefined; | ||
| 192 | const line = std.fmt.bufPrint(&buf, format, args) catch l: { | ||
| 193 | buf[buf.len - 3 ..][0..3].* = "...".*; | ||
| 194 | break :l &buf; | ||
| 195 | }; | ||
| 196 | js.panic(line.ptr, line.len); | ||
| 197 | } | ||
| 198 | |||
| 199 | fn sourceIndexMessage(msg_bytes: []u8) error{OutOfMemory}!void { | ||
| 200 | const Header = abi.SourceIndexHeader; | ||
| 201 | const header: Header = @bitCast(msg_bytes[0..@sizeOf(Header)].*); | ||
| 202 | |||
| 203 | const directories_start = @sizeOf(Header); | ||
| 204 | const directories_end = directories_start + header.directories_len * @sizeOf(Coverage.String); | ||
| 205 | const files_start = directories_end; | ||
| 206 | const files_end = files_start + header.files_len * @sizeOf(Coverage.File); | ||
| 207 | const source_locations_start = files_end; | ||
| 208 | const source_locations_end = source_locations_start + header.source_locations_len * @sizeOf(Coverage.SourceLocation); | ||
| 209 | const string_bytes = msg_bytes[source_locations_end..][0..header.string_bytes_len]; | ||
| 210 | |||
| 211 | const directories: []const Coverage.String = @alignCast(std.mem.bytesAsSlice(Coverage.String, msg_bytes[directories_start..directories_end])); | ||
| 212 | const files: []const Coverage.File = @alignCast(std.mem.bytesAsSlice(Coverage.File, msg_bytes[files_start..files_end])); | ||
| 213 | const source_locations: []const Coverage.SourceLocation = @alignCast(std.mem.bytesAsSlice(Coverage.SourceLocation, msg_bytes[source_locations_start..source_locations_end])); | ||
| 214 | |||
| 215 | try updateCoverage(directories, files, source_locations, string_bytes); | ||
| 216 | js.emitSourceIndexChange(); | ||
| 217 | } | ||
| 218 | |||
| 219 | fn coverageUpdateMessage(msg_bytes: []u8) error{OutOfMemory}!void { | ||
| 220 | recent_coverage_update.clearRetainingCapacity(); | ||
| 221 | recent_coverage_update.appendSlice(gpa, msg_bytes) catch @panic("OOM"); | ||
| 222 | js.emitCoverageUpdate(); | ||
| 223 | } | ||
| 224 | |||
| 225 | var entry_points: std.ArrayListUnmanaged(u32) = .{}; | ||
| 226 | |||
| 227 | fn entryPointsMessage(msg_bytes: []u8) error{OutOfMemory}!void { | ||
| 228 | const header: abi.EntryPointHeader = @bitCast(msg_bytes[0..@sizeOf(abi.EntryPointHeader)].*); | ||
| 229 | entry_points.resize(gpa, header.flags.locs_len) catch @panic("OOM"); | ||
| 230 | @memcpy(entry_points.items, std.mem.bytesAsSlice(u32, msg_bytes[@sizeOf(abi.EntryPointHeader)..])); | ||
| 231 | js.emitEntryPointsUpdate(); | ||
| 232 | } | ||
| 233 | |||
| 234 | export fn entryPoints() Slice(u32) { | ||
| 235 | return Slice(u32).init(entry_points.items); | ||
| 236 | } | ||
| 237 | |||
| 238 | /// Index into `coverage_source_locations`. | ||
| 239 | const SourceLocationIndex = enum(u32) { | ||
| 240 | _, | ||
| 241 | |||
| 242 | fn haveCoverage(sli: SourceLocationIndex) bool { | ||
| 243 | return @intFromEnum(sli) < coverage_source_locations.items.len; | ||
| 244 | } | ||
| 245 | |||
| 246 | fn ptr(sli: SourceLocationIndex) *Coverage.SourceLocation { | ||
| 247 | return &coverage_source_locations.items[@intFromEnum(sli)]; | ||
| 248 | } | ||
| 249 | |||
| 250 | fn sourceLocationLinkHtml( | ||
| 251 | sli: SourceLocationIndex, | ||
| 252 | out: *std.ArrayListUnmanaged(u8), | ||
| 253 | ) Allocator.Error!void { | ||
| 254 | const sl = sli.ptr(); | ||
| 255 | try out.writer(gpa).print("<a href=\"#l{d}\">", .{@intFromEnum(sli)}); | ||
| 256 | try sli.appendPath(out); | ||
| 257 | try out.writer(gpa).print(":{d}:{d}</a>", .{ sl.line, sl.column }); | ||
| 258 | } | ||
| 259 | |||
| 260 | fn appendPath(sli: SourceLocationIndex, out: *std.ArrayListUnmanaged(u8)) Allocator.Error!void { | ||
| 261 | const sl = sli.ptr(); | ||
| 262 | const file = coverage.fileAt(sl.file); | ||
| 263 | const file_name = coverage.stringAt(file.basename); | ||
| 264 | const dir_name = coverage.stringAt(coverage.directories.keys()[file.directory_index]); | ||
| 265 | try html_render.appendEscaped(out, dir_name); | ||
| 266 | try out.appendSlice(gpa, "/"); | ||
| 267 | try html_render.appendEscaped(out, file_name); | ||
| 268 | } | ||
| 269 | |||
| 270 | fn toWalkFile(sli: SourceLocationIndex) ?Walk.File.Index { | ||
| 271 | var buf: std.ArrayListUnmanaged(u8) = .{}; | ||
| 272 | defer buf.deinit(gpa); | ||
| 273 | sli.appendPath(&buf) catch @panic("OOM"); | ||
| 274 | return @enumFromInt(Walk.files.getIndex(buf.items) orelse return null); | ||
| 275 | } | ||
| 276 | |||
| 277 | fn fileHtml( | ||
| 278 | sli: SourceLocationIndex, | ||
| 279 | out: *std.ArrayListUnmanaged(u8), | ||
| 280 | ) error{ OutOfMemory, SourceUnavailable }!void { | ||
| 281 | const walk_file_index = sli.toWalkFile() orelse return error.SourceUnavailable; | ||
| 282 | const root_node = walk_file_index.findRootDecl().get().ast_node; | ||
| 283 | var annotations: std.ArrayListUnmanaged(html_render.Annotation) = .{}; | ||
| 284 | defer annotations.deinit(gpa); | ||
| 285 | try computeSourceAnnotations(sli.ptr().file, walk_file_index, &annotations, coverage_source_locations.items); | ||
| 286 | html_render.fileSourceHtml(walk_file_index, out, root_node, .{ | ||
| 287 | .source_location_annotations = annotations.items, | ||
| 288 | }) catch |err| { | ||
| 289 | fatal("unable to render source: {s}", .{@errorName(err)}); | ||
| 290 | }; | ||
| 291 | } | ||
| 292 | }; | ||
| 293 | |||
| 294 | fn computeSourceAnnotations( | ||
| 295 | cov_file_index: Coverage.File.Index, | ||
| 296 | walk_file_index: Walk.File.Index, | ||
| 297 | annotations: *std.ArrayListUnmanaged(html_render.Annotation), | ||
| 298 | source_locations: []const Coverage.SourceLocation, | ||
| 299 | ) !void { | ||
| 300 | // Collect all the source locations from only this file into this array | ||
| 301 | // first, then sort by line, col, so that we can collect annotations with | ||
| 302 | // O(N) time complexity. | ||
| 303 | var locs: std.ArrayListUnmanaged(SourceLocationIndex) = .{}; | ||
| 304 | defer locs.deinit(gpa); | ||
| 305 | |||
| 306 | for (source_locations, 0..) |sl, sli_usize| { | ||
| 307 | if (sl.file != cov_file_index) continue; | ||
| 308 | const sli: SourceLocationIndex = @enumFromInt(sli_usize); | ||
| 309 | try locs.append(gpa, sli); | ||
| 310 | } | ||
| 311 | |||
| 312 | std.mem.sortUnstable(SourceLocationIndex, locs.items, {}, struct { | ||
| 313 | pub fn lessThan(context: void, lhs: SourceLocationIndex, rhs: SourceLocationIndex) bool { | ||
| 314 | _ = context; | ||
| 315 | const lhs_ptr = lhs.ptr(); | ||
| 316 | const rhs_ptr = rhs.ptr(); | ||
| 317 | if (lhs_ptr.line < rhs_ptr.line) return true; | ||
| 318 | if (lhs_ptr.line > rhs_ptr.line) return false; | ||
| 319 | return lhs_ptr.column < rhs_ptr.column; | ||
| 320 | } | ||
| 321 | }.lessThan); | ||
| 322 | |||
| 323 | const source = walk_file_index.get_ast().source; | ||
| 324 | var line: usize = 1; | ||
| 325 | var column: usize = 1; | ||
| 326 | var next_loc_index: usize = 0; | ||
| 327 | for (source, 0..) |byte, offset| { | ||
| 328 | if (byte == '\n') { | ||
| 329 | line += 1; | ||
| 330 | column = 1; | ||
| 331 | } else { | ||
| 332 | column += 1; | ||
| 333 | } | ||
| 334 | while (true) { | ||
| 335 | if (next_loc_index >= locs.items.len) return; | ||
| 336 | const next_sli = locs.items[next_loc_index]; | ||
| 337 | const next_sl = next_sli.ptr(); | ||
| 338 | if (next_sl.line > line or (next_sl.line == line and next_sl.column >= column)) break; | ||
| 339 | try annotations.append(gpa, .{ | ||
| 340 | .file_byte_offset = offset, | ||
| 341 | .dom_id = @intFromEnum(next_sli), | ||
| 342 | }); | ||
| 343 | next_loc_index += 1; | ||
| 344 | } | ||
| 345 | } | ||
| 346 | } | ||
| 347 | |||
| 348 | var coverage = Coverage.init; | ||
| 349 | /// Index of type `SourceLocationIndex`. | ||
| 350 | var coverage_source_locations: std.ArrayListUnmanaged(Coverage.SourceLocation) = .{}; | ||
| 351 | /// Contains the most recent coverage update message, unmodified. | ||
| 352 | var recent_coverage_update: std.ArrayListAlignedUnmanaged(u8, @alignOf(u64)) = .{}; | ||
| 353 | |||
| 354 | fn updateCoverage( | ||
| 355 | directories: []const Coverage.String, | ||
| 356 | files: []const Coverage.File, | ||
| 357 | source_locations: []const Coverage.SourceLocation, | ||
| 358 | string_bytes: []const u8, | ||
| 359 | ) !void { | ||
| 360 | coverage.directories.clearRetainingCapacity(); | ||
| 361 | coverage.files.clearRetainingCapacity(); | ||
| 362 | coverage.string_bytes.clearRetainingCapacity(); | ||
| 363 | coverage_source_locations.clearRetainingCapacity(); | ||
| 364 | |||
| 365 | try coverage_source_locations.appendSlice(gpa, source_locations); | ||
| 366 | try coverage.string_bytes.appendSlice(gpa, string_bytes); | ||
| 367 | |||
| 368 | try coverage.files.entries.resize(gpa, files.len); | ||
| 369 | @memcpy(coverage.files.entries.items(.key), files); | ||
| 370 | try coverage.files.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items }); | ||
| 371 | |||
| 372 | try coverage.directories.entries.resize(gpa, directories.len); | ||
| 373 | @memcpy(coverage.directories.entries.items(.key), directories); | ||
| 374 | try coverage.directories.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items }); | ||
| 375 | } | ||
| 376 | |||
| 377 | export fn sourceLocationLinkHtml(index: SourceLocationIndex) String { | ||
| 378 | string_result.clearRetainingCapacity(); | ||
| 379 | index.sourceLocationLinkHtml(&string_result) catch @panic("OOM"); | ||
| 380 | return String.init(string_result.items); | ||
| 381 | } | ||
| 382 | |||
| 383 | /// Returns empty string if coverage metadata is not available for this source location. | ||
| 384 | export fn sourceLocationPath(sli: SourceLocationIndex) String { | ||
| 385 | string_result.clearRetainingCapacity(); | ||
| 386 | if (sli.haveCoverage()) sli.appendPath(&string_result) catch @panic("OOM"); | ||
| 387 | return String.init(string_result.items); | ||
| 388 | } | ||
| 389 | |||
| 390 | export fn sourceLocationFileHtml(sli: SourceLocationIndex) String { | ||
| 391 | string_result.clearRetainingCapacity(); | ||
| 392 | sli.fileHtml(&string_result) catch |err| switch (err) { | ||
| 393 | error.OutOfMemory => @panic("OOM"), | ||
| 394 | error.SourceUnavailable => {}, | ||
| 395 | }; | ||
| 396 | return String.init(string_result.items); | ||
| 397 | } | ||
| 398 | |||
| 399 | export fn sourceLocationFileCoveredList(sli_file: SourceLocationIndex) Slice(SourceLocationIndex) { | ||
| 400 | const global = struct { | ||
| 401 | var result: std.ArrayListUnmanaged(SourceLocationIndex) = .{}; | ||
| 402 | fn add(i: u32, want_file: Coverage.File.Index) void { | ||
| 403 | const src_loc_index: SourceLocationIndex = @enumFromInt(i); | ||
| 404 | if (src_loc_index.ptr().file == want_file) result.appendAssumeCapacity(src_loc_index); | ||
| 405 | } | ||
| 406 | }; | ||
| 407 | const want_file = sli_file.ptr().file; | ||
| 408 | global.result.clearRetainingCapacity(); | ||
| 409 | |||
| 410 | // This code assumes 64-bit elements, which is incorrect if the executable | ||
| 411 | // being fuzzed is not a 64-bit CPU. It also assumes little-endian which | ||
| 412 | // can also be incorrect. | ||
| 413 | comptime assert(abi.CoverageUpdateHeader.trailing[0] == .pc_bits_usize); | ||
| 414 | const n_bitset_elems = (coverage_source_locations.items.len + @bitSizeOf(u64) - 1) / @bitSizeOf(u64); | ||
| 415 | const covered_bits = std.mem.bytesAsSlice( | ||
| 416 | u64, | ||
| 417 | recent_coverage_update.items[@sizeOf(abi.CoverageUpdateHeader)..][0 .. n_bitset_elems * @sizeOf(u64)], | ||
| 418 | ); | ||
| 419 | var sli: u32 = 0; | ||
| 420 | for (covered_bits) |elem| { | ||
| 421 | global.result.ensureUnusedCapacity(gpa, 64) catch @panic("OOM"); | ||
| 422 | for (0..@bitSizeOf(u64)) |i| { | ||
| 423 | if ((elem & (@as(u64, 1) << @intCast(i))) != 0) global.add(sli, want_file); | ||
| 424 | sli += 1; | ||
| 425 | } | ||
| 426 | } | ||
| 427 | return Slice(SourceLocationIndex).init(global.result.items); | ||
| 428 | } | ||
lib/fuzzer/web/index.html created+161| ... | @@ -0,0 +1,161 @@ | ||
| 1 | <!doctype html> | ||
| 2 | <html> | ||
| 3 | <head> | ||
| 4 | <meta charset="utf-8"> | ||
| 5 | <title>Zig Build System Interface</title> | ||
| 6 | <style type="text/css"> | ||
| 7 | body { | ||
| 8 | font-family: system-ui, -apple-system, Roboto, "Segoe UI", sans-serif; | ||
| 9 | color: #000000; | ||
| 10 | } | ||
| 11 | .hidden { | ||
| 12 | display: none; | ||
| 13 | } | ||
| 14 | table { | ||
| 15 | width: 100%; | ||
| 16 | } | ||
| 17 | a { | ||
| 18 | color: #2A6286; | ||
| 19 | } | ||
| 20 | pre{ | ||
| 21 | font-family:"Source Code Pro",monospace; | ||
| 22 | font-size:1em; | ||
| 23 | background-color:#F5F5F5; | ||
| 24 | padding: 1em; | ||
| 25 | margin: 0; | ||
| 26 | overflow-x: auto; | ||
| 27 | } | ||
| 28 | :not(pre) > code { | ||
| 29 | white-space: break-spaces; | ||
| 30 | } | ||
| 31 | code { | ||
| 32 | font-family:"Source Code Pro",monospace; | ||
| 33 | font-size: 0.9em; | ||
| 34 | } | ||
| 35 | code a { | ||
| 36 | color: #000000; | ||
| 37 | } | ||
| 38 | kbd { | ||
| 39 | color: #000; | ||
| 40 | background-color: #fafbfc; | ||
| 41 | border-color: #d1d5da; | ||
| 42 | border-bottom-color: #c6cbd1; | ||
| 43 | box-shadow-color: #c6cbd1; | ||
| 44 | display: inline-block; | ||
| 45 | padding: 0.3em 0.2em; | ||
| 46 | font: 1.2em monospace; | ||
| 47 | line-height: 0.8em; | ||
| 48 | vertical-align: middle; | ||
| 49 | border: solid 1px; | ||
| 50 | border-radius: 3px; | ||
| 51 | box-shadow: inset 0 -1px 0; | ||
| 52 | cursor: default; | ||
| 53 | } | ||
| 54 | |||
| 55 | .l { | ||
| 56 | display: inline-block; | ||
| 57 | background: red; | ||
| 58 | width: 1em; | ||
| 59 | height: 1em; | ||
| 60 | border-radius: 1em; | ||
| 61 | } | ||
| 62 | .c { | ||
| 63 | background-color: green; | ||
| 64 | } | ||
| 65 | |||
| 66 | .tok-kw { | ||
| 67 | color: #333; | ||
| 68 | font-weight: bold; | ||
| 69 | } | ||
| 70 | .tok-str { | ||
| 71 | color: #d14; | ||
| 72 | } | ||
| 73 | .tok-builtin { | ||
| 74 | color: #0086b3; | ||
| 75 | } | ||
| 76 | .tok-comment { | ||
| 77 | color: #777; | ||
| 78 | font-style: italic; | ||
| 79 | } | ||
| 80 | .tok-fn { | ||
| 81 | color: #900; | ||
| 82 | font-weight: bold; | ||
| 83 | } | ||
| 84 | .tok-null { | ||
| 85 | color: #008080; | ||
| 86 | } | ||
| 87 | .tok-number { | ||
| 88 | color: #008080; | ||
| 89 | } | ||
| 90 | .tok-type { | ||
| 91 | color: #458; | ||
| 92 | font-weight: bold; | ||
| 93 | } | ||
| 94 | |||
| 95 | @media (prefers-color-scheme: dark) { | ||
| 96 | body { | ||
| 97 | background-color: #111; | ||
| 98 | color: #bbb; | ||
| 99 | } | ||
| 100 | pre { | ||
| 101 | background-color: #222; | ||
| 102 | color: #ccc; | ||
| 103 | } | ||
| 104 | a { | ||
| 105 | color: #88f; | ||
| 106 | } | ||
| 107 | code a { | ||
| 108 | color: #ccc; | ||
| 109 | } | ||
| 110 | .l { | ||
| 111 | background-color: red; | ||
| 112 | } | ||
| 113 | .c { | ||
| 114 | background-color: green; | ||
| 115 | } | ||
| 116 | .tok-kw { | ||
| 117 | color: #eee; | ||
| 118 | } | ||
| 119 | .tok-str { | ||
| 120 | color: #2e5; | ||
| 121 | } | ||
| 122 | .tok-builtin { | ||
| 123 | color: #ff894c; | ||
| 124 | } | ||
| 125 | .tok-comment { | ||
| 126 | color: #aa7; | ||
| 127 | } | ||
| 128 | .tok-fn { | ||
| 129 | color: #B1A0F8; | ||
| 130 | } | ||
| 131 | .tok-null { | ||
| 132 | color: #ff8080; | ||
| 133 | } | ||
| 134 | .tok-number { | ||
| 135 | color: #ff8080; | ||
| 136 | } | ||
| 137 | .tok-type { | ||
| 138 | color: #68f; | ||
| 139 | } | ||
| 140 | } | ||
| 141 | </style> | ||
| 142 | </head> | ||
| 143 | <body> | ||
| 144 | <p id="status">Loading JavaScript...</p> | ||
| 145 | <div id="sectStats" class="hidden"> | ||
| 146 | <ul> | ||
| 147 | <li>Total Runs: <span id="statTotalRuns"></span></li> | ||
| 148 | <li>Unique Runs: <span id="statUniqueRuns"></span></li> | ||
| 149 | <li>Speed (Runs/Second): <span id="statSpeed"></span></li> | ||
| 150 | <li>Coverage: <span id="statCoverage"></span></li> | ||
| 151 | <li>Entry Points: <ul id="entryPointsList"></ul></li> | ||
| 152 | </ul> | ||
| 153 | </div> | ||
| 154 | <div id="sectSource" class="hidden"> | ||
| 155 | <h2>Source Code</h2> | ||
| 156 | <pre><code id="sourceText"></code></pre> | ||
| 157 | </div> | ||
| 158 | <script src="main.js"></script> | ||
| 159 | </body> | ||
| 160 | </html> | ||
| 161 | |||
lib/fuzzer/web/main.js created+252| ... | @@ -0,0 +1,252 @@ | ||
| 1 | (function() { | ||
| 2 | const domStatus = document.getElementById("status"); | ||
| 3 | const domSectSource = document.getElementById("sectSource"); | ||
| 4 | const domSectStats = document.getElementById("sectStats"); | ||
| 5 | const domSourceText = document.getElementById("sourceText"); | ||
| 6 | const domStatTotalRuns = document.getElementById("statTotalRuns"); | ||
| 7 | const domStatUniqueRuns = document.getElementById("statUniqueRuns"); | ||
| 8 | const domStatSpeed = document.getElementById("statSpeed"); | ||
| 9 | const domStatCoverage = document.getElementById("statCoverage"); | ||
| 10 | const domEntryPointsList = document.getElementById("entryPointsList"); | ||
| 11 | |||
| 12 | let wasm_promise = fetch("main.wasm"); | ||
| 13 | let sources_promise = fetch("sources.tar").then(function(response) { | ||
| 14 | if (!response.ok) throw new Error("unable to download sources"); | ||
| 15 | return response.arrayBuffer(); | ||
| 16 | }); | ||
| 17 | var wasm_exports = null; | ||
| 18 | var curNavSearch = null; | ||
| 19 | var curNavLocation = null; | ||
| 20 | |||
| 21 | const text_decoder = new TextDecoder(); | ||
| 22 | const text_encoder = new TextEncoder(); | ||
| 23 | |||
| 24 | domStatus.textContent = "Loading WebAssembly..."; | ||
| 25 | WebAssembly.instantiateStreaming(wasm_promise, { | ||
| 26 | js: { | ||
| 27 | log: function(ptr, len) { | ||
| 28 | const msg = decodeString(ptr, len); | ||
| 29 | console.log(msg); | ||
| 30 | }, | ||
| 31 | panic: function (ptr, len) { | ||
| 32 | const msg = decodeString(ptr, len); | ||
| 33 | throw new Error("panic: " + msg); | ||
| 34 | }, | ||
| 35 | timestamp: function () { | ||
| 36 | return BigInt(new Date()); | ||
| 37 | }, | ||
| 38 | emitSourceIndexChange: onSourceIndexChange, | ||
| 39 | emitCoverageUpdate: onCoverageUpdate, | ||
| 40 | emitEntryPointsUpdate: renderStats, | ||
| 41 | }, | ||
| 42 | }).then(function(obj) { | ||
| 43 | wasm_exports = obj.instance.exports; | ||
| 44 | window.wasm = obj; // for debugging | ||
| 45 | domStatus.textContent = "Loading sources tarball..."; | ||
| 46 | |||
| 47 | sources_promise.then(function(buffer) { | ||
| 48 | domStatus.textContent = "Parsing sources..."; | ||
| 49 | const js_array = new Uint8Array(buffer); | ||
| 50 | const ptr = wasm_exports.alloc(js_array.length); | ||
| 51 | const wasm_array = new Uint8Array(wasm_exports.memory.buffer, ptr, js_array.length); | ||
| 52 | wasm_array.set(js_array); | ||
| 53 | wasm_exports.unpack(ptr, js_array.length); | ||
| 54 | |||
| 55 | window.addEventListener('popstate', onPopState, false); | ||
| 56 | onHashChange(null); | ||
| 57 | |||
| 58 | domStatus.textContent = "Waiting for server to send source location metadata..."; | ||
| 59 | connectWebSocket(); | ||
| 60 | }); | ||
| 61 | }); | ||
| 62 | |||
| 63 | function onPopState(ev) { | ||
| 64 | onHashChange(ev.state); | ||
| 65 | } | ||
| 66 | |||
| 67 | function onHashChange(state) { | ||
| 68 | history.replaceState({}, ""); | ||
| 69 | navigate(location.hash); | ||
| 70 | if (state == null) window.scrollTo({top: 0}); | ||
| 71 | } | ||
| 72 | |||
| 73 | function navigate(location_hash) { | ||
| 74 | domSectSource.classList.add("hidden"); | ||
| 75 | |||
| 76 | curNavLocation = null; | ||
| 77 | curNavSearch = null; | ||
| 78 | |||
| 79 | if (location_hash.length > 1 && location_hash[0] === '#') { | ||
| 80 | const query = location_hash.substring(1); | ||
| 81 | const qpos = query.indexOf("?"); | ||
| 82 | let nonSearchPart; | ||
| 83 | if (qpos === -1) { | ||
| 84 | nonSearchPart = query; | ||
| 85 | } else { | ||
| 86 | nonSearchPart = query.substring(0, qpos); | ||
| 87 | curNavSearch = decodeURIComponent(query.substring(qpos + 1)); | ||
| 88 | } | ||
| 89 | |||
| 90 | if (nonSearchPart[0] == "l") { | ||
| 91 | curNavLocation = +nonSearchPart.substring(1); | ||
| 92 | renderSource(curNavLocation); | ||
| 93 | } | ||
| 94 | } | ||
| 95 | |||
| 96 | render(); | ||
| 97 | } | ||
| 98 | |||
| 99 | function connectWebSocket() { | ||
| 100 | const host = document.location.host; | ||
| 101 | const pathname = document.location.pathname; | ||
| 102 | const isHttps = document.location.protocol === 'https:'; | ||
| 103 | const match = host.match(/^(.+):(\d+)$/); | ||
| 104 | const defaultPort = isHttps ? 443 : 80; | ||
| 105 | const port = match ? parseInt(match[2], 10) : defaultPort; | ||
| 106 | const hostName = match ? match[1] : host; | ||
| 107 | const wsProto = isHttps ? "wss:" : "ws:"; | ||
| 108 | const wsUrl = wsProto + '//' + hostName + ':' + port + pathname; | ||
| 109 | ws = new WebSocket(wsUrl); | ||
| 110 | ws.binaryType = "arraybuffer"; | ||
| 111 | ws.addEventListener('message', onWebSocketMessage, false); | ||
| 112 | ws.addEventListener('error', timeoutThenCreateNew, false); | ||
| 113 | ws.addEventListener('close', timeoutThenCreateNew, false); | ||
| 114 | ws.addEventListener('open', onWebSocketOpen, false); | ||
| 115 | } | ||
| 116 | |||
| 117 | function onWebSocketOpen() { | ||
| 118 | //console.log("web socket opened"); | ||
| 119 | } | ||
| 120 | |||
| 121 | function onWebSocketMessage(ev) { | ||
| 122 | wasmOnMessage(ev.data); | ||
| 123 | } | ||
| 124 | |||
| 125 | function timeoutThenCreateNew() { | ||
| 126 | ws.removeEventListener('message', onWebSocketMessage, false); | ||
| 127 | ws.removeEventListener('error', timeoutThenCreateNew, false); | ||
| 128 | ws.removeEventListener('close', timeoutThenCreateNew, false); | ||
| 129 | ws.removeEventListener('open', onWebSocketOpen, false); | ||
| 130 | ws = null; | ||
| 131 | setTimeout(connectWebSocket, 1000); | ||
| 132 | } | ||
| 133 | |||
| 134 | function wasmOnMessage(data) { | ||
| 135 | const jsArray = new Uint8Array(data); | ||
| 136 | const ptr = wasm_exports.message_begin(jsArray.length); | ||
| 137 | const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, jsArray.length); | ||
| 138 | wasmArray.set(jsArray); | ||
| 139 | wasm_exports.message_end(); | ||
| 140 | } | ||
| 141 | |||
| 142 | function onSourceIndexChange() { | ||
| 143 | render(); | ||
| 144 | if (curNavLocation != null) renderSource(curNavLocation); | ||
| 145 | } | ||
| 146 | |||
| 147 | function onCoverageUpdate() { | ||
| 148 | renderStats(); | ||
| 149 | renderCoverage(); | ||
| 150 | } | ||
| 151 | |||
| 152 | function render() { | ||
| 153 | domStatus.classList.add("hidden"); | ||
| 154 | } | ||
| 155 | |||
| 156 | function renderStats() { | ||
| 157 | const totalRuns = wasm_exports.totalRuns(); | ||
| 158 | const uniqueRuns = wasm_exports.uniqueRuns(); | ||
| 159 | const totalSourceLocations = wasm_exports.totalSourceLocations(); | ||
| 160 | const coveredSourceLocations = wasm_exports.coveredSourceLocations(); | ||
| 161 | domStatTotalRuns.innerText = totalRuns; | ||
| 162 | domStatUniqueRuns.innerText = uniqueRuns + " (" + percent(uniqueRuns, totalRuns) + "%)"; | ||
| 163 | domStatCoverage.innerText = coveredSourceLocations + " / " + totalSourceLocations + " (" + percent(coveredSourceLocations, totalSourceLocations) + "%)"; | ||
| 164 | domStatSpeed.innerText = wasm_exports.totalRunsPerSecond().toFixed(0); | ||
| 165 | |||
| 166 | const entryPoints = unwrapInt32Array(wasm_exports.entryPoints()); | ||
| 167 | resizeDomList(domEntryPointsList, entryPoints.length, "<li></li>"); | ||
| 168 | for (let i = 0; i < entryPoints.length; i += 1) { | ||
| 169 | const liDom = domEntryPointsList.children[i]; | ||
| 170 | liDom.innerHTML = unwrapString(wasm_exports.sourceLocationLinkHtml(entryPoints[i])); | ||
| 171 | } | ||
| 172 | |||
| 173 | |||
| 174 | domSectStats.classList.remove("hidden"); | ||
| 175 | } | ||
| 176 | |||
| 177 | function renderCoverage() { | ||
| 178 | if (curNavLocation == null) return; | ||
| 179 | const sourceLocationIndex = curNavLocation; | ||
| 180 | |||
| 181 | for (let i = 0; i < domSourceText.children.length; i += 1) { | ||
| 182 | const childDom = domSourceText.children[i]; | ||
| 183 | if (childDom.id != null && childDom.id[0] == "l") { | ||
| 184 | childDom.classList.add("l"); | ||
| 185 | childDom.classList.remove("c"); | ||
| 186 | } | ||
| 187 | } | ||
| 188 | const coveredList = unwrapInt32Array(wasm_exports.sourceLocationFileCoveredList(sourceLocationIndex)); | ||
| 189 | for (let i = 0; i < coveredList.length; i += 1) { | ||
| 190 | document.getElementById("l" + coveredList[i]).classList.add("c"); | ||
| 191 | } | ||
| 192 | } | ||
| 193 | |||
| 194 | function resizeDomList(listDom, desiredLen, templateHtml) { | ||
| 195 | for (let i = listDom.childElementCount; i < desiredLen; i += 1) { | ||
| 196 | listDom.insertAdjacentHTML('beforeend', templateHtml); | ||
| 197 | } | ||
| 198 | while (desiredLen < listDom.childElementCount) { | ||
| 199 | listDom.removeChild(listDom.lastChild); | ||
| 200 | } | ||
| 201 | } | ||
| 202 | |||
| 203 | function percent(a, b) { | ||
| 204 | return ((Number(a) / Number(b)) * 100).toFixed(1); | ||
| 205 | } | ||
| 206 | |||
| 207 | function renderSource(sourceLocationIndex) { | ||
| 208 | const pathName = unwrapString(wasm_exports.sourceLocationPath(sourceLocationIndex)); | ||
| 209 | if (pathName.length === 0) return; | ||
| 210 | |||
| 211 | const h2 = domSectSource.children[0]; | ||
| 212 | h2.innerText = pathName; | ||
| 213 | domSourceText.innerHTML = unwrapString(wasm_exports.sourceLocationFileHtml(sourceLocationIndex)); | ||
| 214 | |||
| 215 | domSectSource.classList.remove("hidden"); | ||
| 216 | |||
| 217 | // Empirically, Firefox needs this requestAnimationFrame in order for the scrollIntoView to work. | ||
| 218 | requestAnimationFrame(function() { | ||
| 219 | const slDom = document.getElementById("l" + sourceLocationIndex); | ||
| 220 | if (slDom != null) slDom.scrollIntoView({ | ||
| 221 | behavior: "smooth", | ||
| 222 | block: "center", | ||
| 223 | }); | ||
| 224 | }); | ||
| 225 | } | ||
| 226 | |||
| 227 | function decodeString(ptr, len) { | ||
| 228 | if (len === 0) return ""; | ||
| 229 | return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len)); | ||
| 230 | } | ||
| 231 | |||
| 232 | function unwrapInt32Array(bigint) { | ||
| 233 | const ptr = Number(bigint & 0xffffffffn); | ||
| 234 | const len = Number(bigint >> 32n); | ||
| 235 | if (len === 0) return new Uint32Array(); | ||
| 236 | return new Uint32Array(wasm_exports.memory.buffer, ptr, len); | ||
| 237 | } | ||
| 238 | |||
| 239 | function setInputString(s) { | ||
| 240 | const jsArray = text_encoder.encode(s); | ||
| 241 | const len = jsArray.length; | ||
| 242 | const ptr = wasm_exports.set_input_string(len); | ||
| 243 | const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, len); | ||
| 244 | wasmArray.set(jsArray); | ||
| 245 | } | ||
| 246 | |||
| 247 | function unwrapString(bigint) { | ||
| 248 | const ptr = Number(bigint & 0xffffffffn); | ||
| 249 | const len = Number(bigint >> 32n); | ||
| 250 | return decodeString(ptr, len); | ||
| 251 | } | ||
| 252 | })(); | ||
lib/fuzzer/web/main.zig created+455| ... | @@ -0,0 +1,455 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const assert = std.debug.assert; | ||
| 3 | const abi = std.Build.Fuzz.abi; | ||
| 4 | const gpa = std.heap.wasm_allocator; | ||
| 5 | const log = std.log; | ||
| 6 | const Coverage = std.debug.Coverage; | ||
| 7 | const Allocator = std.mem.Allocator; | ||
| 8 | |||
| 9 | const Walk = @import("Walk"); | ||
| 10 | const Decl = Walk.Decl; | ||
| 11 | const html_render = @import("html_render"); | ||
| 12 | |||
| 13 | /// Nanoseconds. | ||
| 14 | var server_base_timestamp: i64 = 0; | ||
| 15 | /// Milliseconds. | ||
| 16 | var client_base_timestamp: i64 = 0; | ||
| 17 | /// Relative to `server_base_timestamp`. | ||
| 18 | var start_fuzzing_timestamp: i64 = undefined; | ||
| 19 | |||
| 20 | const js = struct { | ||
| 21 | extern "js" fn log(ptr: [*]const u8, len: usize) void; | ||
| 22 | extern "js" fn panic(ptr: [*]const u8, len: usize) noreturn; | ||
| 23 | extern "js" fn timestamp() i64; | ||
| 24 | extern "js" fn emitSourceIndexChange() void; | ||
| 25 | extern "js" fn emitCoverageUpdate() void; | ||
| 26 | extern "js" fn emitEntryPointsUpdate() void; | ||
| 27 | }; | ||
| 28 | |||
| 29 | pub const std_options: std.Options = .{ | ||
| 30 | .logFn = logFn, | ||
| 31 | }; | ||
| 32 | |||
| 33 | pub fn panic(msg: []const u8, st: ?*std.builtin.StackTrace, addr: ?usize) noreturn { | ||
| 34 | _ = st; | ||
| 35 | _ = addr; | ||
| 36 | log.err("panic: {s}", .{msg}); | ||
| 37 | @trap(); | ||
| 38 | } | ||
| 39 | |||
| 40 | fn logFn( | ||
| 41 | comptime message_level: log.Level, | ||
| 42 | comptime scope: @TypeOf(.enum_literal), | ||
| 43 | comptime format: []const u8, | ||
| 44 | args: anytype, | ||
| 45 | ) void { | ||
| 46 | const level_txt = comptime message_level.asText(); | ||
| 47 | const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; | ||
| 48 | var buf: [500]u8 = undefined; | ||
| 49 | const line = std.fmt.bufPrint(&buf, level_txt ++ prefix2 ++ format, args) catch l: { | ||
| 50 | buf[buf.len - 3 ..][0..3].* = "...".*; | ||
| 51 | break :l &buf; | ||
| 52 | }; | ||
| 53 | js.log(line.ptr, line.len); | ||
| 54 | } | ||
| 55 | |||
| 56 | export fn alloc(n: usize) [*]u8 { | ||
| 57 | const slice = gpa.alloc(u8, n) catch @panic("OOM"); | ||
| 58 | return slice.ptr; | ||
| 59 | } | ||
| 60 | |||
| 61 | var message_buffer: std.ArrayListAlignedUnmanaged(u8, @alignOf(u64)) = .{}; | ||
| 62 | |||
| 63 | /// Resizes the message buffer to be the correct length; returns the pointer to | ||
| 64 | /// the query string. | ||
| 65 | export fn message_begin(len: usize) [*]u8 { | ||
| 66 | message_buffer.resize(gpa, len) catch @panic("OOM"); | ||
| 67 | return message_buffer.items.ptr; | ||
| 68 | } | ||
| 69 | |||
| 70 | export fn message_end() void { | ||
| 71 | const msg_bytes = message_buffer.items; | ||
| 72 | |||
| 73 | const tag: abi.ToClientTag = @enumFromInt(msg_bytes[0]); | ||
| 74 | switch (tag) { | ||
| 75 | .current_time => return currentTimeMessage(msg_bytes), | ||
| 76 | .source_index => return sourceIndexMessage(msg_bytes) catch @panic("OOM"), | ||
| 77 | .coverage_update => return coverageUpdateMessage(msg_bytes) catch @panic("OOM"), | ||
| 78 | .entry_points => return entryPointsMessage(msg_bytes) catch @panic("OOM"), | ||
| 79 | _ => unreachable, | ||
| 80 | } | ||
| 81 | } | ||
| 82 | |||
| 83 | export fn unpack(tar_ptr: [*]u8, tar_len: usize) void { | ||
| 84 | const tar_bytes = tar_ptr[0..tar_len]; | ||
| 85 | log.debug("received {d} bytes of tar file", .{tar_bytes.len}); | ||
| 86 | |||
| 87 | unpackInner(tar_bytes) catch |err| { | ||
| 88 | fatal("unable to unpack tar: {s}", .{@errorName(err)}); | ||
| 89 | }; | ||
| 90 | } | ||
| 91 | |||
| 92 | /// Set by `set_input_string`. | ||
| 93 | var input_string: std.ArrayListUnmanaged(u8) = .{}; | ||
| 94 | var string_result: std.ArrayListUnmanaged(u8) = .{}; | ||
| 95 | |||
| 96 | export fn set_input_string(len: usize) [*]u8 { | ||
| 97 | input_string.resize(gpa, len) catch @panic("OOM"); | ||
| 98 | return input_string.items.ptr; | ||
| 99 | } | ||
| 100 | |||
| 101 | /// Looks up the root struct decl corresponding to a file by path. | ||
| 102 | /// Uses `input_string`. | ||
| 103 | export fn find_file_root() Decl.Index { | ||
| 104 | const file: Walk.File.Index = @enumFromInt(Walk.files.getIndex(input_string.items) orelse return .none); | ||
| 105 | return file.findRootDecl(); | ||
| 106 | } | ||
| 107 | |||
| 108 | export fn decl_source_html(decl_index: Decl.Index) String { | ||
| 109 | const decl = decl_index.get(); | ||
| 110 | |||
| 111 | string_result.clearRetainingCapacity(); | ||
| 112 | html_render.fileSourceHtml(decl.file, &string_result, decl.ast_node, .{}) catch |err| { | ||
| 113 | fatal("unable to render source: {s}", .{@errorName(err)}); | ||
| 114 | }; | ||
| 115 | return String.init(string_result.items); | ||
| 116 | } | ||
| 117 | |||
| 118 | export fn totalSourceLocations() usize { | ||
| 119 | return coverage_source_locations.items.len; | ||
| 120 | } | ||
| 121 | |||
| 122 | export fn coveredSourceLocations() usize { | ||
| 123 | const covered_bits = recent_coverage_update.items[@sizeOf(abi.CoverageUpdateHeader)..]; | ||
| 124 | var count: usize = 0; | ||
| 125 | for (covered_bits) |byte| count += @popCount(byte); | ||
| 126 | return count; | ||
| 127 | } | ||
| 128 | |||
| 129 | fn getCoverageUpdateHeader() *abi.CoverageUpdateHeader { | ||
| 130 | return @alignCast(@ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)])); | ||
| 131 | } | ||
| 132 | |||
| 133 | export fn totalRuns() u64 { | ||
| 134 | const header = getCoverageUpdateHeader(); | ||
| 135 | return header.n_runs; | ||
| 136 | } | ||
| 137 | |||
| 138 | export fn uniqueRuns() u64 { | ||
| 139 | const header = getCoverageUpdateHeader(); | ||
| 140 | return header.unique_runs; | ||
| 141 | } | ||
| 142 | |||
| 143 | export fn totalRunsPerSecond() f64 { | ||
| 144 | @setFloatMode(.optimized); | ||
| 145 | const header = getCoverageUpdateHeader(); | ||
| 146 | const ns_elapsed: f64 = @floatFromInt(nsSince(start_fuzzing_timestamp)); | ||
| 147 | const n_runs: f64 = @floatFromInt(header.n_runs); | ||
| 148 | return n_runs / (ns_elapsed / std.time.ns_per_s); | ||
| 149 | } | ||
| 150 | |||
| 151 | const String = Slice(u8); | ||
| 152 | |||
| 153 | fn Slice(T: type) type { | ||
| 154 | return packed struct(u64) { | ||
| 155 | ptr: u32, | ||
| 156 | len: u32, | ||
| 157 | |||
| 158 | fn init(s: []const T) @This() { | ||
| 159 | return .{ | ||
| 160 | .ptr = @intFromPtr(s.ptr), | ||
| 161 | .len = s.len, | ||
| 162 | }; | ||
| 163 | } | ||
| 164 | }; | ||
| 165 | } | ||
| 166 | |||
| 167 | fn unpackInner(tar_bytes: []u8) !void { | ||
| 168 | var fbs = std.io.fixedBufferStream(tar_bytes); | ||
| 169 | var file_name_buffer: [1024]u8 = undefined; | ||
| 170 | var link_name_buffer: [1024]u8 = undefined; | ||
| 171 | var it = std.tar.iterator(fbs.reader(), .{ | ||
| 172 | .file_name_buffer = &file_name_buffer, | ||
| 173 | .link_name_buffer = &link_name_buffer, | ||
| 174 | }); | ||
| 175 | while (try it.next()) |tar_file| { | ||
| 176 | switch (tar_file.kind) { | ||
| 177 | .file => { | ||
| 178 | if (tar_file.size == 0 and tar_file.name.len == 0) break; | ||
| 179 | if (std.mem.endsWith(u8, tar_file.name, ".zig")) { | ||
| 180 | log.debug("found file: '{s}'", .{tar_file.name}); | ||
| 181 | const file_name = try gpa.dupe(u8, tar_file.name); | ||
| 182 | if (std.mem.indexOfScalar(u8, file_name, '/')) |pkg_name_end| { | ||
| 183 | const pkg_name = file_name[0..pkg_name_end]; | ||
| 184 | const gop = try Walk.modules.getOrPut(gpa, pkg_name); | ||
| 185 | const file: Walk.File.Index = @enumFromInt(Walk.files.entries.len); | ||
| 186 | if (!gop.found_existing or | ||
| 187 | std.mem.eql(u8, file_name[pkg_name_end..], "/root.zig") or | ||
| 188 | std.mem.eql(u8, file_name[pkg_name_end + 1 .. file_name.len - ".zig".len], pkg_name)) | ||
| 189 | { | ||
| 190 | gop.value_ptr.* = file; | ||
| 191 | } | ||
| 192 | const file_bytes = tar_bytes[fbs.pos..][0..@intCast(tar_file.size)]; | ||
| 193 | assert(file == try Walk.add_file(file_name, file_bytes)); | ||
| 194 | } | ||
| 195 | } else { | ||
| 196 | log.warn("skipping: '{s}' - the tar creation should have done that", .{tar_file.name}); | ||
| 197 | } | ||
| 198 | }, | ||
| 199 | else => continue, | ||
| 200 | } | ||
| 201 | } | ||
| 202 | } | ||
| 203 | |||
| 204 | fn fatal(comptime format: []const u8, args: anytype) noreturn { | ||
| 205 | var buf: [500]u8 = undefined; | ||
| 206 | const line = std.fmt.bufPrint(&buf, format, args) catch l: { | ||
| 207 | buf[buf.len - 3 ..][0..3].* = "...".*; | ||
| 208 | break :l &buf; | ||
| 209 | }; | ||
| 210 | js.panic(line.ptr, line.len); | ||
| 211 | } | ||
| 212 | |||
| 213 | fn currentTimeMessage(msg_bytes: []u8) void { | ||
| 214 | client_base_timestamp = js.timestamp(); | ||
| 215 | server_base_timestamp = @bitCast(msg_bytes[1..][0..8].*); | ||
| 216 | } | ||
| 217 | |||
| 218 | /// Nanoseconds passed since a server timestamp. | ||
| 219 | fn nsSince(server_timestamp: i64) i64 { | ||
| 220 | const ms_passed = js.timestamp() - client_base_timestamp; | ||
| 221 | const ns_passed = server_base_timestamp - server_timestamp; | ||
| 222 | return ns_passed + ms_passed * std.time.ns_per_ms; | ||
| 223 | } | ||
| 224 | |||
| 225 | fn sourceIndexMessage(msg_bytes: []u8) error{OutOfMemory}!void { | ||
| 226 | const Header = abi.SourceIndexHeader; | ||
| 227 | const header: Header = @bitCast(msg_bytes[0..@sizeOf(Header)].*); | ||
| 228 | |||
| 229 | const directories_start = @sizeOf(Header); | ||
| 230 | const directories_end = directories_start + header.directories_len * @sizeOf(Coverage.String); | ||
| 231 | const files_start = directories_end; | ||
| 232 | const files_end = files_start + header.files_len * @sizeOf(Coverage.File); | ||
| 233 | const source_locations_start = files_end; | ||
| 234 | const source_locations_end = source_locations_start + header.source_locations_len * @sizeOf(Coverage.SourceLocation); | ||
| 235 | const string_bytes = msg_bytes[source_locations_end..][0..header.string_bytes_len]; | ||
| 236 | |||
| 237 | const directories: []const Coverage.String = @alignCast(std.mem.bytesAsSlice(Coverage.String, msg_bytes[directories_start..directories_end])); | ||
| 238 | const files: []const Coverage.File = @alignCast(std.mem.bytesAsSlice(Coverage.File, msg_bytes[files_start..files_end])); | ||
| 239 | const source_locations: []const Coverage.SourceLocation = @alignCast(std.mem.bytesAsSlice(Coverage.SourceLocation, msg_bytes[source_locations_start..source_locations_end])); | ||
| 240 | |||
| 241 | start_fuzzing_timestamp = header.start_timestamp; | ||
| 242 | try updateCoverage(directories, files, source_locations, string_bytes); | ||
| 243 | js.emitSourceIndexChange(); | ||
| 244 | } | ||
| 245 | |||
| 246 | fn coverageUpdateMessage(msg_bytes: []u8) error{OutOfMemory}!void { | ||
| 247 | recent_coverage_update.clearRetainingCapacity(); | ||
| 248 | recent_coverage_update.appendSlice(gpa, msg_bytes) catch @panic("OOM"); | ||
| 249 | js.emitCoverageUpdate(); | ||
| 250 | } | ||
| 251 | |||
| 252 | var entry_points: std.ArrayListUnmanaged(u32) = .{}; | ||
| 253 | |||
| 254 | fn entryPointsMessage(msg_bytes: []u8) error{OutOfMemory}!void { | ||
| 255 | const header: abi.EntryPointHeader = @bitCast(msg_bytes[0..@sizeOf(abi.EntryPointHeader)].*); | ||
| 256 | entry_points.resize(gpa, header.flags.locs_len) catch @panic("OOM"); | ||
| 257 | @memcpy(entry_points.items, std.mem.bytesAsSlice(u32, msg_bytes[@sizeOf(abi.EntryPointHeader)..])); | ||
| 258 | js.emitEntryPointsUpdate(); | ||
| 259 | } | ||
| 260 | |||
| 261 | export fn entryPoints() Slice(u32) { | ||
| 262 | return Slice(u32).init(entry_points.items); | ||
| 263 | } | ||
| 264 | |||
| 265 | /// Index into `coverage_source_locations`. | ||
| 266 | const SourceLocationIndex = enum(u32) { | ||
| 267 | _, | ||
| 268 | |||
| 269 | fn haveCoverage(sli: SourceLocationIndex) bool { | ||
| 270 | return @intFromEnum(sli) < coverage_source_locations.items.len; | ||
| 271 | } | ||
| 272 | |||
| 273 | fn ptr(sli: SourceLocationIndex) *Coverage.SourceLocation { | ||
| 274 | return &coverage_source_locations.items[@intFromEnum(sli)]; | ||
| 275 | } | ||
| 276 | |||
| 277 | fn sourceLocationLinkHtml( | ||
| 278 | sli: SourceLocationIndex, | ||
| 279 | out: *std.ArrayListUnmanaged(u8), | ||
| 280 | ) Allocator.Error!void { | ||
| 281 | const sl = sli.ptr(); | ||
| 282 | try out.writer(gpa).print("<a href=\"#l{d}\">", .{@intFromEnum(sli)}); | ||
| 283 | try sli.appendPath(out); | ||
| 284 | try out.writer(gpa).print(":{d}:{d}</a>", .{ sl.line, sl.column }); | ||
| 285 | } | ||
| 286 | |||
| 287 | fn appendPath(sli: SourceLocationIndex, out: *std.ArrayListUnmanaged(u8)) Allocator.Error!void { | ||
| 288 | const sl = sli.ptr(); | ||
| 289 | const file = coverage.fileAt(sl.file); | ||
| 290 | const file_name = coverage.stringAt(file.basename); | ||
| 291 | const dir_name = coverage.stringAt(coverage.directories.keys()[file.directory_index]); | ||
| 292 | try html_render.appendEscaped(out, dir_name); | ||
| 293 | try out.appendSlice(gpa, "/"); | ||
| 294 | try html_render.appendEscaped(out, file_name); | ||
| 295 | } | ||
| 296 | |||
| 297 | fn toWalkFile(sli: SourceLocationIndex) ?Walk.File.Index { | ||
| 298 | var buf: std.ArrayListUnmanaged(u8) = .{}; | ||
| 299 | defer buf.deinit(gpa); | ||
| 300 | sli.appendPath(&buf) catch @panic("OOM"); | ||
| 301 | return @enumFromInt(Walk.files.getIndex(buf.items) orelse return null); | ||
| 302 | } | ||
| 303 | |||
| 304 | fn fileHtml( | ||
| 305 | sli: SourceLocationIndex, | ||
| 306 | out: *std.ArrayListUnmanaged(u8), | ||
| 307 | ) error{ OutOfMemory, SourceUnavailable }!void { | ||
| 308 | const walk_file_index = sli.toWalkFile() orelse return error.SourceUnavailable; | ||
| 309 | const root_node = walk_file_index.findRootDecl().get().ast_node; | ||
| 310 | var annotations: std.ArrayListUnmanaged(html_render.Annotation) = .{}; | ||
| 311 | defer annotations.deinit(gpa); | ||
| 312 | try computeSourceAnnotations(sli.ptr().file, walk_file_index, &annotations, coverage_source_locations.items); | ||
| 313 | html_render.fileSourceHtml(walk_file_index, out, root_node, .{ | ||
| 314 | .source_location_annotations = annotations.items, | ||
| 315 | }) catch |err| { | ||
| 316 | fatal("unable to render source: {s}", .{@errorName(err)}); | ||
| 317 | }; | ||
| 318 | } | ||
| 319 | }; | ||
| 320 | |||
| 321 | fn computeSourceAnnotations( | ||
| 322 | cov_file_index: Coverage.File.Index, | ||
| 323 | walk_file_index: Walk.File.Index, | ||
| 324 | annotations: *std.ArrayListUnmanaged(html_render.Annotation), | ||
| 325 | source_locations: []const Coverage.SourceLocation, | ||
| 326 | ) !void { | ||
| 327 | // Collect all the source locations from only this file into this array | ||
| 328 | // first, then sort by line, col, so that we can collect annotations with | ||
| 329 | // O(N) time complexity. | ||
| 330 | var locs: std.ArrayListUnmanaged(SourceLocationIndex) = .{}; | ||
| 331 | defer locs.deinit(gpa); | ||
| 332 | |||
| 333 | for (source_locations, 0..) |sl, sli_usize| { | ||
| 334 | if (sl.file != cov_file_index) continue; | ||
| 335 | const sli: SourceLocationIndex = @enumFromInt(sli_usize); | ||
| 336 | try locs.append(gpa, sli); | ||
| 337 | } | ||
| 338 | |||
| 339 | std.mem.sortUnstable(SourceLocationIndex, locs.items, {}, struct { | ||
| 340 | pub fn lessThan(context: void, lhs: SourceLocationIndex, rhs: SourceLocationIndex) bool { | ||
| 341 | _ = context; | ||
| 342 | const lhs_ptr = lhs.ptr(); | ||
| 343 | const rhs_ptr = rhs.ptr(); | ||
| 344 | if (lhs_ptr.line < rhs_ptr.line) return true; | ||
| 345 | if (lhs_ptr.line > rhs_ptr.line) return false; | ||
| 346 | return lhs_ptr.column < rhs_ptr.column; | ||
| 347 | } | ||
| 348 | }.lessThan); | ||
| 349 | |||
| 350 | const source = walk_file_index.get_ast().source; | ||
| 351 | var line: usize = 1; | ||
| 352 | var column: usize = 1; | ||
| 353 | var next_loc_index: usize = 0; | ||
| 354 | for (source, 0..) |byte, offset| { | ||
| 355 | if (byte == '\n') { | ||
| 356 | line += 1; | ||
| 357 | column = 1; | ||
| 358 | } else { | ||
| 359 | column += 1; | ||
| 360 | } | ||
| 361 | while (true) { | ||
| 362 | if (next_loc_index >= locs.items.len) return; | ||
| 363 | const next_sli = locs.items[next_loc_index]; | ||
| 364 | const next_sl = next_sli.ptr(); | ||
| 365 | if (next_sl.line > line or (next_sl.line == line and next_sl.column >= column)) break; | ||
| 366 | try annotations.append(gpa, .{ | ||
| 367 | .file_byte_offset = offset, | ||
| 368 | .dom_id = @intFromEnum(next_sli), | ||
| 369 | }); | ||
| 370 | next_loc_index += 1; | ||
| 371 | } | ||
| 372 | } | ||
| 373 | } | ||
| 374 | |||
| 375 | var coverage = Coverage.init; | ||
| 376 | /// Index of type `SourceLocationIndex`. | ||
| 377 | var coverage_source_locations: std.ArrayListUnmanaged(Coverage.SourceLocation) = .{}; | ||
| 378 | /// Contains the most recent coverage update message, unmodified. | ||
| 379 | var recent_coverage_update: std.ArrayListAlignedUnmanaged(u8, @alignOf(u64)) = .{}; | ||
| 380 | |||
| 381 | fn updateCoverage( | ||
| 382 | directories: []const Coverage.String, | ||
| 383 | files: []const Coverage.File, | ||
| 384 | source_locations: []const Coverage.SourceLocation, | ||
| 385 | string_bytes: []const u8, | ||
| 386 | ) !void { | ||
| 387 | coverage.directories.clearRetainingCapacity(); | ||
| 388 | coverage.files.clearRetainingCapacity(); | ||
| 389 | coverage.string_bytes.clearRetainingCapacity(); | ||
| 390 | coverage_source_locations.clearRetainingCapacity(); | ||
| 391 | |||
| 392 | try coverage_source_locations.appendSlice(gpa, source_locations); | ||
| 393 | try coverage.string_bytes.appendSlice(gpa, string_bytes); | ||
| 394 | |||
| 395 | try coverage.files.entries.resize(gpa, files.len); | ||
| 396 | @memcpy(coverage.files.entries.items(.key), files); | ||
| 397 | try coverage.files.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items }); | ||
| 398 | |||
| 399 | try coverage.directories.entries.resize(gpa, directories.len); | ||
| 400 | @memcpy(coverage.directories.entries.items(.key), directories); | ||
| 401 | try coverage.directories.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items }); | ||
| 402 | } | ||
| 403 | |||
| 404 | export fn sourceLocationLinkHtml(index: SourceLocationIndex) String { | ||
| 405 | string_result.clearRetainingCapacity(); | ||
| 406 | index.sourceLocationLinkHtml(&string_result) catch @panic("OOM"); | ||
| 407 | return String.init(string_result.items); | ||
| 408 | } | ||
| 409 | |||
| 410 | /// Returns empty string if coverage metadata is not available for this source location. | ||
| 411 | export fn sourceLocationPath(sli: SourceLocationIndex) String { | ||
| 412 | string_result.clearRetainingCapacity(); | ||
| 413 | if (sli.haveCoverage()) sli.appendPath(&string_result) catch @panic("OOM"); | ||
| 414 | return String.init(string_result.items); | ||
| 415 | } | ||
| 416 | |||
| 417 | export fn sourceLocationFileHtml(sli: SourceLocationIndex) String { | ||
| 418 | string_result.clearRetainingCapacity(); | ||
| 419 | sli.fileHtml(&string_result) catch |err| switch (err) { | ||
| 420 | error.OutOfMemory => @panic("OOM"), | ||
| 421 | error.SourceUnavailable => {}, | ||
| 422 | }; | ||
| 423 | return String.init(string_result.items); | ||
| 424 | } | ||
| 425 | |||
| 426 | export fn sourceLocationFileCoveredList(sli_file: SourceLocationIndex) Slice(SourceLocationIndex) { | ||
| 427 | const global = struct { | ||
| 428 | var result: std.ArrayListUnmanaged(SourceLocationIndex) = .{}; | ||
| 429 | fn add(i: u32, want_file: Coverage.File.Index) void { | ||
| 430 | const src_loc_index: SourceLocationIndex = @enumFromInt(i); | ||
| 431 | if (src_loc_index.ptr().file == want_file) result.appendAssumeCapacity(src_loc_index); | ||
| 432 | } | ||
| 433 | }; | ||
| 434 | const want_file = sli_file.ptr().file; | ||
| 435 | global.result.clearRetainingCapacity(); | ||
| 436 | |||
| 437 | // This code assumes 64-bit elements, which is incorrect if the executable | ||
| 438 | // being fuzzed is not a 64-bit CPU. It also assumes little-endian which | ||
| 439 | // can also be incorrect. | ||
| 440 | comptime assert(abi.CoverageUpdateHeader.trailing[0] == .pc_bits_usize); | ||
| 441 | const n_bitset_elems = (coverage_source_locations.items.len + @bitSizeOf(u64) - 1) / @bitSizeOf(u64); | ||
| 442 | const covered_bits = std.mem.bytesAsSlice( | ||
| 443 | u64, | ||
| 444 | recent_coverage_update.items[@sizeOf(abi.CoverageUpdateHeader)..][0 .. n_bitset_elems * @sizeOf(u64)], | ||
| 445 | ); | ||
| 446 | var sli: u32 = 0; | ||
| 447 | for (covered_bits) |elem| { | ||
| 448 | global.result.ensureUnusedCapacity(gpa, 64) catch @panic("OOM"); | ||
| 449 | for (0..@bitSizeOf(u64)) |i| { | ||
| 450 | if ((elem & (@as(u64, 1) << @intCast(i))) != 0) global.add(sli, want_file); | ||
| 451 | sli += 1; | ||
| 452 | } | ||
| 453 | } | ||
| 454 | return Slice(SourceLocationIndex).init(global.result.items); | ||
| 455 | } | ||
lib/init/src/main.zig+7-3| ... | @@ -27,7 +27,11 @@ test "simple test" { | ... | @@ -27,7 +27,11 @@ test "simple test" { |
| 27 | } | 27 | } |
| 28 | 28 | ||
| 29 | test "fuzz example" { | 29 | test "fuzz example" { |
| 30 | // Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case! | 30 | const global = struct { |
| 31 | const input_bytes = std.testing.fuzzInput(.{}); | 31 | fn testOne(input: []const u8) anyerror!void { |
| 32 | try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input_bytes)); | 32 | // Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case! |
| 33 | try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input)); | ||
| 34 | } | ||
| 35 | }; | ||
| 36 | try std.testing.fuzz(global.testOne, .{}); | ||
| 33 | } | 37 | } |
lib/std/Build/Fuzz.zig+2| ... | @@ -66,6 +66,8 @@ pub fn start( | ... | @@ -66,6 +66,8 @@ pub fn start( |
| 66 | .coverage_files = .{}, | 66 | .coverage_files = .{}, |
| 67 | .coverage_mutex = .{}, | 67 | .coverage_mutex = .{}, |
| 68 | .coverage_condition = .{}, | 68 | .coverage_condition = .{}, |
| 69 | |||
| 70 | .base_timestamp = std.time.nanoTimestamp(), | ||
| 69 | }; | 71 | }; |
| 70 | 72 | ||
| 71 | // For accepting HTTP connections. | 73 | // For accepting HTTP connections. |
lib/std/Build/Fuzz/WebServer.zig+20-5| ... | @@ -33,6 +33,9 @@ coverage_mutex: std.Thread.Mutex, | ... | @@ -33,6 +33,9 @@ coverage_mutex: std.Thread.Mutex, |
| 33 | /// Signaled when `coverage_files` changes. | 33 | /// Signaled when `coverage_files` changes. |
| 34 | coverage_condition: std.Thread.Condition, | 34 | coverage_condition: std.Thread.Condition, |
| 35 | 35 | ||
| 36 | /// Time at initialization of WebServer. | ||
| 37 | base_timestamp: i128, | ||
| 38 | |||
| 36 | const fuzzer_bin_name = "fuzzer"; | 39 | const fuzzer_bin_name = "fuzzer"; |
| 37 | const fuzzer_arch_os_abi = "wasm32-freestanding"; | 40 | const fuzzer_arch_os_abi = "wasm32-freestanding"; |
| 38 | const fuzzer_cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext"; | 41 | const fuzzer_cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext"; |
| ... | @@ -43,6 +46,7 @@ const CoverageMap = struct { | ... | @@ -43,6 +46,7 @@ const CoverageMap = struct { |
| 43 | source_locations: []Coverage.SourceLocation, | 46 | source_locations: []Coverage.SourceLocation, |
| 44 | /// Elements are indexes into `source_locations` pointing to the unit tests that are being fuzz tested. | 47 | /// Elements are indexes into `source_locations` pointing to the unit tests that are being fuzz tested. |
| 45 | entry_points: std.ArrayListUnmanaged(u32), | 48 | entry_points: std.ArrayListUnmanaged(u32), |
| 49 | start_timestamp: i64, | ||
| 46 | 50 | ||
| 47 | fn deinit(cm: *CoverageMap, gpa: Allocator) void { | 51 | fn deinit(cm: *CoverageMap, gpa: Allocator) void { |
| 48 | std.posix.munmap(cm.mapped_memory); | 52 | std.posix.munmap(cm.mapped_memory); |
| ... | @@ -87,6 +91,10 @@ pub fn run(ws: *WebServer) void { | ... | @@ -87,6 +91,10 @@ pub fn run(ws: *WebServer) void { |
| 87 | } | 91 | } |
| 88 | } | 92 | } |
| 89 | 93 | ||
| 94 | fn now(s: *const WebServer) i64 { | ||
| 95 | return @intCast(std.time.nanoTimestamp() - s.base_timestamp); | ||
| 96 | } | ||
| 97 | |||
| 90 | fn accept(ws: *WebServer, connection: std.net.Server.Connection) void { | 98 | fn accept(ws: *WebServer, connection: std.net.Server.Connection) void { |
| 91 | defer connection.stream.close(); | 99 | defer connection.stream.close(); |
| 92 | 100 | ||
| ... | @@ -128,11 +136,11 @@ fn serveRequest(ws: *WebServer, request: *std.http.Server.Request) !void { | ... | @@ -128,11 +136,11 @@ fn serveRequest(ws: *WebServer, request: *std.http.Server.Request) !void { |
| 128 | std.mem.eql(u8, request.head.target, "/debug") or | 136 | std.mem.eql(u8, request.head.target, "/debug") or |
| 129 | std.mem.eql(u8, request.head.target, "/debug/")) | 137 | std.mem.eql(u8, request.head.target, "/debug/")) |
| 130 | { | 138 | { |
| 131 | try serveFile(ws, request, "fuzzer/index.html", "text/html"); | 139 | try serveFile(ws, request, "fuzzer/web/index.html", "text/html"); |
| 132 | } else if (std.mem.eql(u8, request.head.target, "/main.js") or | 140 | } else if (std.mem.eql(u8, request.head.target, "/main.js") or |
| 133 | std.mem.eql(u8, request.head.target, "/debug/main.js")) | 141 | std.mem.eql(u8, request.head.target, "/debug/main.js")) |
| 134 | { | 142 | { |
| 135 | try serveFile(ws, request, "fuzzer/main.js", "application/javascript"); | 143 | try serveFile(ws, request, "fuzzer/web/main.js", "application/javascript"); |
| 136 | } else if (std.mem.eql(u8, request.head.target, "/main.wasm")) { | 144 | } else if (std.mem.eql(u8, request.head.target, "/main.wasm")) { |
| 137 | try serveWasm(ws, request, .ReleaseFast); | 145 | try serveWasm(ws, request, .ReleaseFast); |
| 138 | } else if (std.mem.eql(u8, request.head.target, "/debug/main.wasm")) { | 146 | } else if (std.mem.eql(u8, request.head.target, "/debug/main.wasm")) { |
| ... | @@ -217,7 +225,7 @@ fn buildWasmBinary( | ... | @@ -217,7 +225,7 @@ fn buildWasmBinary( |
| 217 | 225 | ||
| 218 | const main_src_path: Build.Cache.Path = .{ | 226 | const main_src_path: Build.Cache.Path = .{ |
| 219 | .root_dir = ws.zig_lib_directory, | 227 | .root_dir = ws.zig_lib_directory, |
| 220 | .sub_path = "fuzzer/wasm/main.zig", | 228 | .sub_path = "fuzzer/web/main.zig", |
| 221 | }; | 229 | }; |
| 222 | const walk_src_path: Build.Cache.Path = .{ | 230 | const walk_src_path: Build.Cache.Path = .{ |
| 223 | .root_dir = ws.zig_lib_directory, | 231 | .root_dir = ws.zig_lib_directory, |
| ... | @@ -381,6 +389,13 @@ fn serveWebSocket(ws: *WebServer, web_socket: *std.http.WebSocket) !void { | ... | @@ -381,6 +389,13 @@ fn serveWebSocket(ws: *WebServer, web_socket: *std.http.WebSocket) !void { |
| 381 | ws.coverage_mutex.lock(); | 389 | ws.coverage_mutex.lock(); |
| 382 | defer ws.coverage_mutex.unlock(); | 390 | defer ws.coverage_mutex.unlock(); |
| 383 | 391 | ||
| 392 | // On first connection, the client needs to know what time the server | ||
| 393 | // thinks it is to rebase timestamps. | ||
| 394 | { | ||
| 395 | const timestamp_message: abi.CurrentTime = .{ .base = ws.now() }; | ||
| 396 | try web_socket.writeMessage(std.mem.asBytes(&timestamp_message), .binary); | ||
| 397 | } | ||
| 398 | |||
| 384 | // On first connection, the client needs all the coverage information | 399 | // On first connection, the client needs all the coverage information |
| 385 | // so that subsequent updates can contain only the updated bits. | 400 | // so that subsequent updates can contain only the updated bits. |
| 386 | var prev_unique_runs: usize = 0; | 401 | var prev_unique_runs: usize = 0; |
| ... | @@ -406,7 +421,6 @@ fn sendCoverageContext( | ... | @@ -406,7 +421,6 @@ fn sendCoverageContext( |
| 406 | const seen_pcs = cov_header.seenBits(); | 421 | const seen_pcs = cov_header.seenBits(); |
| 407 | const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic); | 422 | const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic); |
| 408 | const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic); | 423 | const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic); |
| 409 | const lowest_stack = @atomicLoad(usize, &cov_header.lowest_stack, .monotonic); | ||
| 410 | if (prev_unique_runs.* != unique_runs) { | 424 | if (prev_unique_runs.* != unique_runs) { |
| 411 | // There has been an update. | 425 | // There has been an update. |
| 412 | if (prev_unique_runs.* == 0) { | 426 | if (prev_unique_runs.* == 0) { |
| ... | @@ -417,6 +431,7 @@ fn sendCoverageContext( | ... | @@ -417,6 +431,7 @@ fn sendCoverageContext( |
| 417 | .files_len = @intCast(coverage_map.coverage.files.entries.len), | 431 | .files_len = @intCast(coverage_map.coverage.files.entries.len), |
| 418 | .source_locations_len = @intCast(coverage_map.source_locations.len), | 432 | .source_locations_len = @intCast(coverage_map.source_locations.len), |
| 419 | .string_bytes_len = @intCast(coverage_map.coverage.string_bytes.items.len), | 433 | .string_bytes_len = @intCast(coverage_map.coverage.string_bytes.items.len), |
| 434 | .start_timestamp = coverage_map.start_timestamp, | ||
| 420 | }; | 435 | }; |
| 421 | const iovecs: [5]std.posix.iovec_const = .{ | 436 | const iovecs: [5]std.posix.iovec_const = .{ |
| 422 | makeIov(std.mem.asBytes(&header)), | 437 | makeIov(std.mem.asBytes(&header)), |
| ... | @@ -431,7 +446,6 @@ fn sendCoverageContext( | ... | @@ -431,7 +446,6 @@ fn sendCoverageContext( |
| 431 | const header: abi.CoverageUpdateHeader = .{ | 446 | const header: abi.CoverageUpdateHeader = .{ |
| 432 | .n_runs = n_runs, | 447 | .n_runs = n_runs, |
| 433 | .unique_runs = unique_runs, | 448 | .unique_runs = unique_runs, |
| 434 | .lowest_stack = lowest_stack, | ||
| 435 | }; | 449 | }; |
| 436 | const iovecs: [2]std.posix.iovec_const = .{ | 450 | const iovecs: [2]std.posix.iovec_const = .{ |
| 437 | makeIov(std.mem.asBytes(&header)), | 451 | makeIov(std.mem.asBytes(&header)), |
| ... | @@ -584,6 +598,7 @@ fn prepareTables( | ... | @@ -584,6 +598,7 @@ fn prepareTables( |
| 584 | .mapped_memory = undefined, // populated below | 598 | .mapped_memory = undefined, // populated below |
| 585 | .source_locations = undefined, // populated below | 599 | .source_locations = undefined, // populated below |
| 586 | .entry_points = .{}, | 600 | .entry_points = .{}, |
| 601 | .start_timestamp = ws.now(), | ||
| 587 | }; | 602 | }; |
| 588 | errdefer gop.value_ptr.coverage.deinit(gpa); | 603 | errdefer gop.value_ptr.coverage.deinit(gpa); |
| 589 | 604 |
lib/std/Build/Fuzz/abi.zig+9-2| ... | @@ -13,7 +13,6 @@ pub const SeenPcsHeader = extern struct { | ... | @@ -13,7 +13,6 @@ pub const SeenPcsHeader = extern struct { |
| 13 | n_runs: usize, | 13 | n_runs: usize, |
| 14 | unique_runs: usize, | 14 | unique_runs: usize, |
| 15 | pcs_len: usize, | 15 | pcs_len: usize, |
| 16 | lowest_stack: usize, | ||
| 17 | 16 | ||
| 18 | /// Used for comptime assertions. Provides a mechanism for strategically | 17 | /// Used for comptime assertions. Provides a mechanism for strategically |
| 19 | /// causing compile errors. | 18 | /// causing compile errors. |
| ... | @@ -44,12 +43,19 @@ pub const SeenPcsHeader = extern struct { | ... | @@ -44,12 +43,19 @@ pub const SeenPcsHeader = extern struct { |
| 44 | }; | 43 | }; |
| 45 | 44 | ||
| 46 | pub const ToClientTag = enum(u8) { | 45 | pub const ToClientTag = enum(u8) { |
| 46 | current_time, | ||
| 47 | source_index, | 47 | source_index, |
| 48 | coverage_update, | 48 | coverage_update, |
| 49 | entry_points, | 49 | entry_points, |
| 50 | _, | 50 | _, |
| 51 | }; | 51 | }; |
| 52 | 52 | ||
| 53 | pub const CurrentTime = extern struct { | ||
| 54 | tag: ToClientTag = .current_time, | ||
| 55 | /// Number of nanoseconds that all other timestamps are in reference to. | ||
| 56 | base: i64 align(1), | ||
| 57 | }; | ||
| 58 | |||
| 53 | /// Sent to the fuzzer web client on first connection to the websocket URL. | 59 | /// Sent to the fuzzer web client on first connection to the websocket URL. |
| 54 | /// | 60 | /// |
| 55 | /// Trailing: | 61 | /// Trailing: |
| ... | @@ -63,6 +69,8 @@ pub const SourceIndexHeader = extern struct { | ... | @@ -63,6 +69,8 @@ pub const SourceIndexHeader = extern struct { |
| 63 | files_len: u32, | 69 | files_len: u32, |
| 64 | source_locations_len: u32, | 70 | source_locations_len: u32, |
| 65 | string_bytes_len: u32, | 71 | string_bytes_len: u32, |
| 72 | /// When, according to the server, fuzzing started. | ||
| 73 | start_timestamp: i64 align(4), | ||
| 66 | 74 | ||
| 67 | pub const Flags = packed struct(u32) { | 75 | pub const Flags = packed struct(u32) { |
| 68 | tag: ToClientTag = .source_index, | 76 | tag: ToClientTag = .source_index, |
| ... | @@ -79,7 +87,6 @@ pub const CoverageUpdateHeader = extern struct { | ... | @@ -79,7 +87,6 @@ pub const CoverageUpdateHeader = extern struct { |
| 79 | flags: Flags = .{}, | 87 | flags: Flags = .{}, |
| 80 | n_runs: u64, | 88 | n_runs: u64, |
| 81 | unique_runs: u64, | 89 | unique_runs: u64, |
| 82 | lowest_stack: u64, | ||
| 83 | 90 | ||
| 84 | pub const Flags = packed struct(u64) { | 91 | pub const Flags = packed struct(u64) { |
| 85 | tag: ToClientTag = .coverage_update, | 92 | tag: ToClientTag = .coverage_update, |
lib/std/testing.zig+6-2| ... | @@ -1141,6 +1141,10 @@ pub const FuzzInputOptions = struct { | ... | @@ -1141,6 +1141,10 @@ pub const FuzzInputOptions = struct { |
| 1141 | corpus: []const []const u8 = &.{}, | 1141 | corpus: []const []const u8 = &.{}, |
| 1142 | }; | 1142 | }; |
| 1143 | 1143 | ||
| 1144 | pub inline fn fuzzInput(options: FuzzInputOptions) []const u8 { | 1144 | /// Inline to avoid coverage instrumentation. |
| 1145 | return @import("root").fuzzInput(options); | 1145 | pub inline fn fuzz( |
| 1146 | comptime testOne: fn (input: []const u8) anyerror!void, | ||
| 1147 | options: FuzzInputOptions, | ||
| 1148 | ) anyerror!void { | ||
| 1149 | return @import("root").fuzz(testOne, options); | ||
| 1146 | } | 1150 | } |
lib/std/zig/tokenizer.zig+5-2| ... | @@ -1708,6 +1708,10 @@ test "invalid tabs and carriage returns" { | ... | @@ -1708,6 +1708,10 @@ test "invalid tabs and carriage returns" { |
| 1708 | try testTokenize("\rpub\rswitch\r", &.{ .keyword_pub, .keyword_switch }); | 1708 | try testTokenize("\rpub\rswitch\r", &.{ .keyword_pub, .keyword_switch }); |
| 1709 | } | 1709 | } |
| 1710 | 1710 | ||
| 1711 | test "fuzzable properties upheld" { | ||
| 1712 | return std.testing.fuzz(testPropertiesUpheld, .{}); | ||
| 1713 | } | ||
| 1714 | |||
| 1711 | fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !void { | 1715 | fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !void { |
| 1712 | var tokenizer = Tokenizer.init(source); | 1716 | var tokenizer = Tokenizer.init(source); |
| 1713 | for (expected_token_tags) |expected_token_tag| { | 1717 | for (expected_token_tags) |expected_token_tag| { |
| ... | @@ -1723,8 +1727,7 @@ fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !v | ... | @@ -1723,8 +1727,7 @@ fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !v |
| 1723 | try std.testing.expectEqual(source.len, last_token.loc.end); | 1727 | try std.testing.expectEqual(source.len, last_token.loc.end); |
| 1724 | } | 1728 | } |
| 1725 | 1729 | ||
| 1726 | test "fuzzable properties upheld" { | 1730 | fn testPropertiesUpheld(source: []const u8) anyerror!void { |
| 1727 | const source = std.testing.fuzzInput(.{}); | ||
| 1728 | const source0 = try std.testing.allocator.dupeZ(u8, source); | 1731 | const source0 = try std.testing.allocator.dupeZ(u8, source); |
| 1729 | defer std.testing.allocator.free(source0); | 1732 | defer std.testing.allocator.free(source0); |
| 1730 | var tokenizer = Tokenizer.init(source0); | 1733 | var tokenizer = Tokenizer.init(source0); |