| author | |
| committer | |
| log | e0ffac4e3c2271a617616760f3084f5f01fb0785 |
| tree | 5769c9280878ebb3cc5f76345cf00050012e8198 |
| parent | ffc050e0557c5d951cd293ca365ed0cd3cdf83db |
* new .zig-cache subdirectory: 'v'
- stores coverage information with filename of hash of PCs that want
coverage. This hash is a hex encoding of the 64-bit coverage ID.
* build runner
* fixed bug in file system inputs when a compile step has an
overridden zig_lib_dir field set.
* set some std lib options optimized for the build runner
- no side channel mitigations
- no Transport Layer Security
- no crypto fork safety
* add a --port CLI arg for choosing the port the fuzzing web interface
listens on. it defaults to choosing a random open port.
* introduce a web server, and serve a basic single page application
- shares wasm code with autodocs
- assets are created live on request, for convenient development
experience. main.wasm is properly cached if nothing changes.
- sources.tar comes from file system inputs (introduced with the
`--watch` feature)
* receives coverage ID from test runner and sends it on a thread-safe
queue to the WebServer.
* test runner
- takes a zig cache directory argument now, for where to put coverage
information.
- sends coverage ID to parent process
* fuzzer
- puts its logs (in debug mode) in .zig-cache/tmp/libfuzzer.log
- computes coverage_id and makes it available with
`fuzzer_coverage_id` exported function.
- the memory-mapped coverage file is now namespaced by the coverage id
in hex encoding, in `.zig-cache/v`
* tokenizer
- add a fuzz test to check that several properties are upheld13 files changed, 872 insertions(+), 62 deletions(-)
lib/compiler/build_runner.zig+28-1| ... | ... | @@ -17,6 +17,12 @@ const runner = @This(); |
| 17 | 17 | pub const root = @import("@build"); |
| 18 | 18 | pub const dependencies = @import("@dependencies"); |
| 19 | 19 | |
| 20 | pub const std_options: std.Options = .{ | |
| 21 | .side_channels_mitigations = .none, | |
| 22 | .http_disable_tls = true, | |
| 23 | .crypto_fork_safety = false, | |
| 24 | }; | |
| 25 | ||
| 20 | 26 | pub fn main() !void { |
| 21 | 27 | // Here we use an ArenaAllocator backed by a page allocator because a build is a short-lived, |
| 22 | 28 | // one shot program. We don't need to waste time freeing memory and finding places to squish |
| ... | ... | @@ -106,6 +112,7 @@ pub fn main() !void { |
| 106 | 112 | var watch = false; |
| 107 | 113 | var fuzz = false; |
| 108 | 114 | var debounce_interval_ms: u16 = 50; |
| 115 | var listen_port: u16 = 0; | |
| 109 | 116 | |
| 110 | 117 | while (nextArg(args, &arg_idx)) |arg| { |
| 111 | 118 | if (mem.startsWith(u8, arg, "-Z")) { |
| ... | ... | @@ -203,6 +210,14 @@ pub fn main() !void { |
| 203 | 210 | next_arg, @errorName(err), |
| 204 | 211 | }); |
| 205 | 212 | }; |
| 213 | } else if (mem.eql(u8, arg, "--port")) { | |
| 214 | const next_arg = nextArg(args, &arg_idx) orelse | |
| 215 | fatalWithHint("expected u16 after '{s}'", .{arg}); | |
| 216 | listen_port = std.fmt.parseUnsigned(u16, next_arg, 10) catch |err| { | |
| 217 | fatal("unable to parse port '{s}' as unsigned 16-bit integer: {s}\n", .{ | |
| 218 | next_arg, @errorName(err), | |
| 219 | }); | |
| 220 | }; | |
| 206 | 221 | } else if (mem.eql(u8, arg, "--debug-log")) { |
| 207 | 222 | const next_arg = nextArgOrFatal(args, &arg_idx); |
| 208 | 223 | try debug_log_scopes.append(next_arg); |
| ... | ... | @@ -403,7 +418,19 @@ pub fn main() !void { |
| 403 | 418 | else => return err, |
| 404 | 419 | }; |
| 405 | 420 | if (fuzz) { |
| 406 | Fuzz.start(&run.thread_pool, run.step_stack.keys(), run.ttyconf, main_progress_node); | |
| 421 | const listen_address = std.net.Address.parseIp("127.0.0.1", listen_port) catch unreachable; | |
| 422 | try Fuzz.start( | |
| 423 | gpa, | |
| 424 | arena, | |
| 425 | global_cache_directory, | |
| 426 | zig_lib_directory, | |
| 427 | zig_exe, | |
| 428 | &run.thread_pool, | |
| 429 | run.step_stack.keys(), | |
| 430 | run.ttyconf, | |
| 431 | listen_address, | |
| 432 | main_progress_node, | |
| 433 | ); | |
| 407 | 434 | } |
| 408 | 435 | |
| 409 | 436 | if (!watch) return cleanExit(); |
lib/compiler/test_runner.zig+20-2| ... | ... | @@ -28,6 +28,7 @@ pub fn main() void { |
| 28 | 28 | @panic("unable to parse command line args"); |
| 29 | 29 | |
| 30 | 30 | var listen = false; |
| 31 | var opt_cache_dir: ?[]const u8 = null; | |
| 31 | 32 | |
| 32 | 33 | for (args[1..]) |arg| { |
| 33 | 34 | if (std.mem.eql(u8, arg, "--listen=-")) { |
| ... | ... | @@ -35,13 +36,18 @@ pub fn main() void { |
| 35 | 36 | } else if (std.mem.startsWith(u8, arg, "--seed=")) { |
| 36 | 37 | testing.random_seed = std.fmt.parseUnsigned(u32, arg["--seed=".len..], 0) catch |
| 37 | 38 | @panic("unable to parse --seed command line argument"); |
| 39 | } else if (std.mem.startsWith(u8, arg, "--cache-dir")) { | |
| 40 | opt_cache_dir = arg["--cache-dir=".len..]; | |
| 38 | 41 | } else { |
| 39 | 42 | @panic("unrecognized command line argument"); |
| 40 | 43 | } |
| 41 | 44 | } |
| 42 | 45 | |
| 43 | 46 | fba.reset(); |
| 44 | if (builtin.fuzz) fuzzer_init(); | |
| 47 | if (builtin.fuzz) { | |
| 48 | const cache_dir = opt_cache_dir orelse @panic("missing --cache-dir=[path] argument"); | |
| 49 | fuzzer_init(FuzzerSlice.fromSlice(cache_dir)); | |
| 50 | } | |
| 45 | 51 | |
| 46 | 52 | if (listen) { |
| 47 | 53 | return mainServer() catch @panic("internal test runner failure"); |
| ... | ... | @@ -60,6 +66,11 @@ fn mainServer() !void { |
| 60 | 66 | }); |
| 61 | 67 | defer server.deinit(); |
| 62 | 68 | |
| 69 | if (builtin.fuzz) { | |
| 70 | const coverage_id = fuzzer_coverage_id(); | |
| 71 | try server.serveU64Message(.coverage_id, coverage_id); | |
| 72 | } | |
| 73 | ||
| 63 | 74 | while (true) { |
| 64 | 75 | const hdr = try server.receiveMessage(); |
| 65 | 76 | switch (hdr.tag) { |
| ... | ... | @@ -316,15 +327,22 @@ const FuzzerSlice = extern struct { |
| 316 | 327 | ptr: [*]const u8, |
| 317 | 328 | len: usize, |
| 318 | 329 | |
| 330 | /// Inline to avoid fuzzer instrumentation. | |
| 319 | 331 | inline fn toSlice(s: FuzzerSlice) []const u8 { |
| 320 | 332 | return s.ptr[0..s.len]; |
| 321 | 333 | } |
| 334 | ||
| 335 | /// Inline to avoid fuzzer instrumentation. | |
| 336 | inline fn fromSlice(s: []const u8) FuzzerSlice { | |
| 337 | return .{ .ptr = s.ptr, .len = s.len }; | |
| 338 | } | |
| 322 | 339 | }; |
| 323 | 340 | |
| 324 | 341 | var is_fuzz_test: bool = undefined; |
| 325 | 342 | |
| 326 | 343 | extern fn fuzzer_next() FuzzerSlice; |
| 327 | extern fn fuzzer_init() void; | |
| 344 | extern fn fuzzer_init(cache_dir: FuzzerSlice) void; | |
| 345 | extern fn fuzzer_coverage_id() u64; | |
| 328 | 346 | |
| 329 | 347 | pub fn fuzzInput(options: testing.FuzzInputOptions) []const u8 { |
| 330 | 348 | @disableInstrumentation(); |
lib/docs/wasm/main.zig+2-2| ... | ... | @@ -53,7 +53,7 @@ export fn unpack(tar_ptr: [*]u8, tar_len: usize) void { |
| 53 | 53 | const tar_bytes = tar_ptr[0..tar_len]; |
| 54 | 54 | //log.debug("received {d} bytes of tar file", .{tar_bytes.len}); |
| 55 | 55 | |
| 56 | unpack_inner(tar_bytes) catch |err| { | |
| 56 | unpackInner(tar_bytes) catch |err| { | |
| 57 | 57 | fatal("unable to unpack tar: {s}", .{@errorName(err)}); |
| 58 | 58 | }; |
| 59 | 59 | } |
| ... | ... | @@ -750,7 +750,7 @@ export fn decl_type_html(decl_index: Decl.Index) String { |
| 750 | 750 | |
| 751 | 751 | const Oom = error{OutOfMemory}; |
| 752 | 752 | |
| 753 | fn unpack_inner(tar_bytes: []u8) !void { | |
| 753 | fn unpackInner(tar_bytes: []u8) !void { | |
| 754 | 754 | var fbs = std.io.fixedBufferStream(tar_bytes); |
| 755 | 755 | var file_name_buffer: [1024]u8 = undefined; |
| 756 | 756 | var link_name_buffer: [1024]u8 = undefined; |
lib/fuzzer.zig+58-15| ... | ... | @@ -17,7 +17,8 @@ fn logOverride( |
| 17 | 17 | args: anytype, |
| 18 | 18 | ) void { |
| 19 | 19 | const f = if (log_file) |f| f else f: { |
| 20 | const f = fuzzer.dir.createFile("libfuzzer.log", .{}) catch @panic("failed to open fuzzer log file"); | |
| 20 | const f = fuzzer.cache_dir.createFile("tmp/libfuzzer.log", .{}) catch | |
| 21 | @panic("failed to open fuzzer log file"); | |
| 21 | 22 | log_file = f; |
| 22 | 23 | break :f f; |
| 23 | 24 | }; |
| ... | ... | @@ -114,7 +115,10 @@ const Fuzzer = struct { |
| 114 | 115 | /// Stored in a memory-mapped file so that it can be shared with other |
| 115 | 116 | /// processes and viewed while the fuzzer is running. |
| 116 | 117 | seen_pcs: MemoryMappedList, |
| 117 | dir: std.fs.Dir, | |
| 118 | cache_dir: std.fs.Dir, | |
| 119 | /// Identifies the file name that will be used to store coverage | |
| 120 | /// information, available to other processes. | |
| 121 | coverage_id: u64, | |
| 118 | 122 | |
| 119 | 123 | const SeenPcsHeader = extern struct { |
| 120 | 124 | n_runs: usize, |
| ... | ... | @@ -189,18 +193,31 @@ const Fuzzer = struct { |
| 189 | 193 | id: Run.Id, |
| 190 | 194 | }; |
| 191 | 195 | |
| 192 | fn init(f: *Fuzzer, dir: std.fs.Dir) !void { | |
| 193 | f.dir = dir; | |
| 196 | fn init(f: *Fuzzer, cache_dir: std.fs.Dir) !void { | |
| 197 | const flagged_pcs = f.flagged_pcs; | |
| 198 | ||
| 199 | f.cache_dir = cache_dir; | |
| 200 | ||
| 201 | // Choose a file name for the coverage based on a hash of the PCs that will be stored within. | |
| 202 | const pc_digest = d: { | |
| 203 | var hasher = std.hash.Wyhash.init(0); | |
| 204 | for (flagged_pcs) |flagged_pc| { | |
| 205 | hasher.update(std.mem.asBytes(&flagged_pc.addr)); | |
| 206 | } | |
| 207 | break :d f.coverage.run_id_hasher.final(); | |
| 208 | }; | |
| 209 | f.coverage_id = pc_digest; | |
| 210 | const hex_digest = std.fmt.hex(pc_digest); | |
| 211 | const coverage_file_path = "v/" ++ hex_digest; | |
| 194 | 212 | |
| 195 | 213 | // Layout of this file: |
| 196 | 214 | // - Header |
| 197 | 215 | // - list of PC addresses (usize elements) |
| 198 | 216 | // - list of hit flag, 1 bit per address (stored in u8 elements) |
| 199 | const coverage_file = dir.createFile("coverage", .{ | |
| 217 | const coverage_file = createFileBail(cache_dir, coverage_file_path, .{ | |
| 200 | 218 | .read = true, |
| 201 | 219 | .truncate = false, |
| 202 | }) catch |err| fatal("unable to create coverage file: {s}", .{@errorName(err)}); | |
| 203 | const flagged_pcs = f.flagged_pcs; | |
| 220 | }); | |
| 204 | 221 | const n_bitset_elems = (flagged_pcs.len + 7) / 8; |
| 205 | 222 | const bytes_len = @sizeOf(SeenPcsHeader) + flagged_pcs.len * @sizeOf(usize) + n_bitset_elems; |
| 206 | 223 | const existing_len = coverage_file.getEndPos() catch |err| { |
| ... | ... | @@ -217,7 +234,8 @@ const Fuzzer = struct { |
| 217 | 234 | fatal("unable to init coverage memory map: {s}", .{@errorName(err)}); |
| 218 | 235 | }; |
| 219 | 236 | if (existing_len != 0) { |
| 220 | const existing_pcs = std.mem.bytesAsSlice(usize, f.seen_pcs.items[@sizeOf(SeenPcsHeader)..][0 .. flagged_pcs.len * @sizeOf(usize)]); | |
| 237 | const existing_pcs_bytes = f.seen_pcs.items[@sizeOf(SeenPcsHeader)..][0 .. flagged_pcs.len * @sizeOf(usize)]; | |
| 238 | const existing_pcs = std.mem.bytesAsSlice(usize, existing_pcs_bytes); | |
| 221 | 239 | for (existing_pcs, flagged_pcs, 0..) |old, new, i| { |
| 222 | 240 | if (old != new.addr) { |
| 223 | 241 | fatal("incompatible existing coverage file (differing PC at index {d}: {x} != {x})", .{ |
| ... | ... | @@ -380,6 +398,21 @@ const Fuzzer = struct { |
| 380 | 398 | } |
| 381 | 399 | }; |
| 382 | 400 | |
| 401 | fn createFileBail(dir: std.fs.Dir, sub_path: []const u8, flags: std.fs.File.CreateFlags) std.fs.File { | |
| 402 | return dir.createFile(sub_path, flags) catch |err| switch (err) { | |
| 403 | error.FileNotFound => { | |
| 404 | const dir_name = std.fs.path.dirname(sub_path).?; | |
| 405 | dir.makePath(dir_name) catch |e| { | |
| 406 | fatal("unable to make path '{s}': {s}", .{ dir_name, @errorName(e) }); | |
| 407 | }; | |
| 408 | return dir.createFile(sub_path, flags) catch |e| { | |
| 409 | fatal("unable to create file '{s}': {s}", .{ sub_path, @errorName(e) }); | |
| 410 | }; | |
| 411 | }, | |
| 412 | else => fatal("unable to create file '{s}': {s}", .{ sub_path, @errorName(err) }), | |
| 413 | }; | |
| 414 | } | |
| 415 | ||
| 383 | 416 | fn oom(err: anytype) noreturn { |
| 384 | 417 | switch (err) { |
| 385 | 418 | error.OutOfMemory => @panic("out of memory"), |
| ... | ... | @@ -397,25 +430,35 @@ var fuzzer: Fuzzer = .{ |
| 397 | 430 | .n_runs = 0, |
| 398 | 431 | .recent_cases = .{}, |
| 399 | 432 | .coverage = undefined, |
| 400 | .dir = undefined, | |
| 433 | .cache_dir = undefined, | |
| 401 | 434 | .seen_pcs = undefined, |
| 435 | .coverage_id = undefined, | |
| 402 | 436 | }; |
| 403 | 437 | |
| 438 | /// Invalid until `fuzzer_init` is called. | |
| 439 | export fn fuzzer_coverage_id() u64 { | |
| 440 | return fuzzer.coverage_id; | |
| 441 | } | |
| 442 | ||
| 404 | 443 | export fn fuzzer_next() Fuzzer.Slice { |
| 405 | 444 | return Fuzzer.Slice.fromZig(fuzzer.next() catch |err| switch (err) { |
| 406 | 445 | error.OutOfMemory => @panic("out of memory"), |
| 407 | 446 | }); |
| 408 | 447 | } |
| 409 | 448 | |
| 410 | export fn fuzzer_init() void { | |
| 449 | export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void { | |
| 411 | 450 | if (module_count_8bc == 0) fatal("__sanitizer_cov_8bit_counters_init was never called", .{}); |
| 412 | 451 | if (module_count_pcs == 0) fatal("__sanitizer_cov_pcs_init was never called", .{}); |
| 413 | 452 | |
| 414 | // TODO: move this to .zig-cache/f | |
| 415 | const fuzz_dir = std.fs.cwd().makeOpenPath("f", .{ .iterate = true }) catch |err| { | |
| 416 | fatal("unable to open fuzz directory 'f': {s}", .{@errorName(err)}); | |
| 417 | }; | |
| 418 | fuzzer.init(fuzz_dir) catch |err| fatal("unable to init fuzzer: {s}", .{@errorName(err)}); | |
| 453 | const cache_dir_path = cache_dir_struct.toZig(); | |
| 454 | const cache_dir = if (cache_dir_path.len == 0) | |
| 455 | std.fs.cwd() | |
| 456 | else | |
| 457 | std.fs.cwd().makeOpenPath(cache_dir_path, .{ .iterate = true }) catch |err| { | |
| 458 | fatal("unable to open fuzz directory '{s}': {s}", .{ cache_dir_path, @errorName(err) }); | |
| 459 | }; | |
| 460 | ||
| 461 | fuzzer.init(cache_dir) catch |err| fatal("unable to init fuzzer: {s}", .{@errorName(err)}); | |
| 419 | 462 | } |
| 420 | 463 | |
| 421 | 464 | /// Like `std.ArrayListUnmanaged(u8)` but backed by memory mapping. |
lib/fuzzer/index.html created+76| ... | ... | @@ -0,0 +1,76 @@ |
| 1 | <!doctype html> | |
| 2 | <html> | |
| 3 | <head> | |
| 4 | <meta charset="utf-8"> | |
| 5 | <title>Zig Documentation</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 | .tok-kw { | |
| 12 | color: #333; | |
| 13 | font-weight: bold; | |
| 14 | } | |
| 15 | .tok-str { | |
| 16 | color: #d14; | |
| 17 | } | |
| 18 | .tok-builtin { | |
| 19 | color: #0086b3; | |
| 20 | } | |
| 21 | .tok-comment { | |
| 22 | color: #777; | |
| 23 | font-style: italic; | |
| 24 | } | |
| 25 | .tok-fn { | |
| 26 | color: #900; | |
| 27 | font-weight: bold; | |
| 28 | } | |
| 29 | .tok-null { | |
| 30 | color: #008080; | |
| 31 | } | |
| 32 | .tok-number { | |
| 33 | color: #008080; | |
| 34 | } | |
| 35 | .tok-type { | |
| 36 | color: #458; | |
| 37 | font-weight: bold; | |
| 38 | } | |
| 39 | ||
| 40 | @media (prefers-color-scheme: dark) { | |
| 41 | body { | |
| 42 | background-color: #111; | |
| 43 | color: #bbb; | |
| 44 | } | |
| 45 | .tok-kw { | |
| 46 | color: #eee; | |
| 47 | } | |
| 48 | .tok-str { | |
| 49 | color: #2e5; | |
| 50 | } | |
| 51 | .tok-builtin { | |
| 52 | color: #ff894c; | |
| 53 | } | |
| 54 | .tok-comment { | |
| 55 | color: #aa7; | |
| 56 | } | |
| 57 | .tok-fn { | |
| 58 | color: #B1A0F8; | |
| 59 | } | |
| 60 | .tok-null { | |
| 61 | color: #ff8080; | |
| 62 | } | |
| 63 | .tok-number { | |
| 64 | color: #ff8080; | |
| 65 | } | |
| 66 | .tok-type { | |
| 67 | color: #68f; | |
| 68 | } | |
| 69 | } | |
| 70 | </style> | |
| 71 | </head> | |
| 72 | <body> | |
| 73 | <script src="main.js"></script> | |
| 74 | </body> | |
| 75 | </html> | |
| 76 |
lib/fuzzer/main.js created+40| ... | ... | @@ -0,0 +1,40 @@ |
| 1 | (function() { | |
| 2 | let wasm_promise = fetch("main.wasm"); | |
| 3 | let sources_promise = fetch("sources.tar").then(function(response) { | |
| 4 | if (!response.ok) throw new Error("unable to download sources"); | |
| 5 | return response.arrayBuffer(); | |
| 6 | }); | |
| 7 | var wasm_exports = null; | |
| 8 | ||
| 9 | const text_decoder = new TextDecoder(); | |
| 10 | const text_encoder = new TextEncoder(); | |
| 11 | ||
| 12 | WebAssembly.instantiateStreaming(wasm_promise, { | |
| 13 | js: { | |
| 14 | log: function(ptr, len) { | |
| 15 | const msg = decodeString(ptr, len); | |
| 16 | console.log(msg); | |
| 17 | }, | |
| 18 | panic: function (ptr, len) { | |
| 19 | const msg = decodeString(ptr, len); | |
| 20 | throw new Error("panic: " + msg); | |
| 21 | }, | |
| 22 | }, | |
| 23 | }).then(function(obj) { | |
| 24 | wasm_exports = obj.instance.exports; | |
| 25 | window.wasm = obj; // for debugging | |
| 26 | ||
| 27 | sources_promise.then(function(buffer) { | |
| 28 | const js_array = new Uint8Array(buffer); | |
| 29 | const ptr = wasm_exports.alloc(js_array.length); | |
| 30 | const wasm_array = new Uint8Array(wasm_exports.memory.buffer, ptr, js_array.length); | |
| 31 | wasm_array.set(js_array); | |
| 32 | wasm_exports.unpack(ptr, js_array.length); | |
| 33 | }); | |
| 34 | }); | |
| 35 | ||
| 36 | function decodeString(ptr, len) { | |
| 37 | if (len === 0) return ""; | |
| 38 | return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len)); | |
| 39 | } | |
| 40 | })(); |
lib/fuzzer/wasm/main.zig created+99| ... | ... | @@ -0,0 +1,99 @@ |
| 1 | const std = @import("std"); | |
| 2 | const assert = std.debug.assert; | |
| 3 | ||
| 4 | const Walk = @import("Walk"); | |
| 5 | ||
| 6 | const gpa = std.heap.wasm_allocator; | |
| 7 | const log = std.log; | |
| 8 | ||
| 9 | const js = struct { | |
| 10 | extern "js" fn log(ptr: [*]const u8, len: usize) void; | |
| 11 | extern "js" fn panic(ptr: [*]const u8, len: usize) noreturn; | |
| 12 | }; | |
| 13 | ||
| 14 | pub const std_options: std.Options = .{ | |
| 15 | .logFn = logFn, | |
| 16 | }; | |
| 17 | ||
| 18 | pub fn panic(msg: []const u8, st: ?*std.builtin.StackTrace, addr: ?usize) noreturn { | |
| 19 | _ = st; | |
| 20 | _ = addr; | |
| 21 | log.err("panic: {s}", .{msg}); | |
| 22 | @trap(); | |
| 23 | } | |
| 24 | ||
| 25 | fn logFn( | |
| 26 | comptime message_level: log.Level, | |
| 27 | comptime scope: @TypeOf(.enum_literal), | |
| 28 | comptime format: []const u8, | |
| 29 | args: anytype, | |
| 30 | ) void { | |
| 31 | const level_txt = comptime message_level.asText(); | |
| 32 | const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; | |
| 33 | var buf: [500]u8 = undefined; | |
| 34 | const line = std.fmt.bufPrint(&buf, level_txt ++ prefix2 ++ format, args) catch l: { | |
| 35 | buf[buf.len - 3 ..][0..3].* = "...".*; | |
| 36 | break :l &buf; | |
| 37 | }; | |
| 38 | js.log(line.ptr, line.len); | |
| 39 | } | |
| 40 | ||
| 41 | export fn alloc(n: usize) [*]u8 { | |
| 42 | const slice = gpa.alloc(u8, n) catch @panic("OOM"); | |
| 43 | return slice.ptr; | |
| 44 | } | |
| 45 | ||
| 46 | export fn unpack(tar_ptr: [*]u8, tar_len: usize) void { | |
| 47 | const tar_bytes = tar_ptr[0..tar_len]; | |
| 48 | log.debug("received {d} bytes of tar file", .{tar_bytes.len}); | |
| 49 | ||
| 50 | unpackInner(tar_bytes) catch |err| { | |
| 51 | fatal("unable to unpack tar: {s}", .{@errorName(err)}); | |
| 52 | }; | |
| 53 | } | |
| 54 | ||
| 55 | fn unpackInner(tar_bytes: []u8) !void { | |
| 56 | var fbs = std.io.fixedBufferStream(tar_bytes); | |
| 57 | var file_name_buffer: [1024]u8 = undefined; | |
| 58 | var link_name_buffer: [1024]u8 = undefined; | |
| 59 | var it = std.tar.iterator(fbs.reader(), .{ | |
| 60 | .file_name_buffer = &file_name_buffer, | |
| 61 | .link_name_buffer = &link_name_buffer, | |
| 62 | }); | |
| 63 | while (try it.next()) |tar_file| { | |
| 64 | switch (tar_file.kind) { | |
| 65 | .file => { | |
| 66 | if (tar_file.size == 0 and tar_file.name.len == 0) break; | |
| 67 | if (std.mem.endsWith(u8, tar_file.name, ".zig")) { | |
| 68 | log.debug("found file: '{s}'", .{tar_file.name}); | |
| 69 | const file_name = try gpa.dupe(u8, tar_file.name); | |
| 70 | if (std.mem.indexOfScalar(u8, file_name, '/')) |pkg_name_end| { | |
| 71 | const pkg_name = file_name[0..pkg_name_end]; | |
| 72 | const gop = try Walk.modules.getOrPut(gpa, pkg_name); | |
| 73 | const file: Walk.File.Index = @enumFromInt(Walk.files.entries.len); | |
| 74 | if (!gop.found_existing or | |
| 75 | std.mem.eql(u8, file_name[pkg_name_end..], "/root.zig") or | |
| 76 | std.mem.eql(u8, file_name[pkg_name_end + 1 .. file_name.len - ".zig".len], pkg_name)) | |
| 77 | { | |
| 78 | gop.value_ptr.* = file; | |
| 79 | } | |
| 80 | const file_bytes = tar_bytes[fbs.pos..][0..@intCast(tar_file.size)]; | |
| 81 | assert(file == try Walk.add_file(file_name, file_bytes)); | |
| 82 | } | |
| 83 | } else { | |
| 84 | log.warn("skipping: '{s}' - the tar creation should have done that", .{tar_file.name}); | |
| 85 | } | |
| 86 | }, | |
| 87 | else => continue, | |
| 88 | } | |
| 89 | } | |
| 90 | } | |
| 91 | ||
| 92 | fn fatal(comptime format: []const u8, args: anytype) noreturn { | |
| 93 | var buf: [500]u8 = undefined; | |
| 94 | const line = std.fmt.bufPrint(&buf, format, args) catch l: { | |
| 95 | buf[buf.len - 3 ..][0..3].* = "...".*; | |
| 96 | break :l &buf; | |
| 97 | }; | |
| 98 | js.panic(line.ptr, line.len); | |
| 99 | } |
lib/std/Build.zig+8-4| ... | ... | @@ -2300,22 +2300,26 @@ pub const LazyPath = union(enum) { |
| 2300 | 2300 | } |
| 2301 | 2301 | |
| 2302 | 2302 | pub fn path(lazy_path: LazyPath, b: *Build, sub_path: []const u8) LazyPath { |
| 2303 | return lazy_path.join(b.allocator, sub_path) catch @panic("OOM"); | |
| 2304 | } | |
| 2305 | ||
| 2306 | pub fn join(lazy_path: LazyPath, arena: Allocator, sub_path: []const u8) Allocator.Error!LazyPath { | |
| 2303 | 2307 | return switch (lazy_path) { |
| 2304 | 2308 | .src_path => |src| .{ .src_path = .{ |
| 2305 | 2309 | .owner = src.owner, |
| 2306 | .sub_path = b.pathResolve(&.{ src.sub_path, sub_path }), | |
| 2310 | .sub_path = try fs.path.resolve(arena, &.{ src.sub_path, sub_path }), | |
| 2307 | 2311 | } }, |
| 2308 | 2312 | .generated => |gen| .{ .generated = .{ |
| 2309 | 2313 | .file = gen.file, |
| 2310 | 2314 | .up = gen.up, |
| 2311 | .sub_path = b.pathResolve(&.{ gen.sub_path, sub_path }), | |
| 2315 | .sub_path = try fs.path.resolve(arena, &.{ gen.sub_path, sub_path }), | |
| 2312 | 2316 | } }, |
| 2313 | 2317 | .cwd_relative => |cwd_relative| .{ |
| 2314 | .cwd_relative = b.pathResolve(&.{ cwd_relative, sub_path }), | |
| 2318 | .cwd_relative = try fs.path.resolve(arena, &.{ cwd_relative, sub_path }), | |
| 2315 | 2319 | }, |
| 2316 | 2320 | .dependency => |dep| .{ .dependency = .{ |
| 2317 | 2321 | .dependency = dep.dependency, |
| 2318 | .sub_path = b.pathResolve(&.{ dep.sub_path, sub_path }), | |
| 2322 | .sub_path = try fs.path.resolve(arena, &.{ dep.sub_path, sub_path }), | |
| 2319 | 2323 | } }, |
| 2320 | 2324 | }; |
| 2321 | 2325 | } |
lib/std/Build/Fuzz.zig+438-17| ... | ... | @@ -1,59 +1,479 @@ |
| 1 | const builtin = @import("builtin"); | |
| 1 | 2 | const std = @import("../std.zig"); |
| 2 | const Fuzz = @This(); | |
| 3 | const Build = std.Build; | |
| 3 | 4 | const Step = std.Build.Step; |
| 4 | 5 | const assert = std.debug.assert; |
| 5 | 6 | const fatal = std.process.fatal; |
| 7 | const Allocator = std.mem.Allocator; | |
| 8 | const log = std.log; | |
| 9 | ||
| 10 | const Fuzz = @This(); | |
| 6 | 11 | const build_runner = @import("root"); |
| 7 | 12 | |
| 8 | 13 | pub fn start( |
| 14 | gpa: Allocator, | |
| 15 | arena: Allocator, | |
| 16 | global_cache_directory: Build.Cache.Directory, | |
| 17 | zig_lib_directory: Build.Cache.Directory, | |
| 18 | zig_exe_path: []const u8, | |
| 9 | 19 | thread_pool: *std.Thread.Pool, |
| 10 | 20 | all_steps: []const *Step, |
| 11 | 21 | ttyconf: std.io.tty.Config, |
| 22 | listen_address: std.net.Address, | |
| 12 | 23 | prog_node: std.Progress.Node, |
| 13 | ) void { | |
| 14 | const count = block: { | |
| 24 | ) Allocator.Error!void { | |
| 25 | const fuzz_run_steps = block: { | |
| 15 | 26 | const rebuild_node = prog_node.start("Rebuilding Unit Tests", 0); |
| 16 | 27 | defer rebuild_node.end(); |
| 17 | var count: usize = 0; | |
| 18 | 28 | var wait_group: std.Thread.WaitGroup = .{}; |
| 19 | 29 | defer wait_group.wait(); |
| 30 | var fuzz_run_steps: std.ArrayListUnmanaged(*Step.Run) = .{}; | |
| 31 | defer fuzz_run_steps.deinit(gpa); | |
| 20 | 32 | for (all_steps) |step| { |
| 21 | 33 | const run = step.cast(Step.Run) orelse continue; |
| 22 | 34 | if (run.fuzz_tests.items.len > 0 and run.producer != null) { |
| 23 | 35 | thread_pool.spawnWg(&wait_group, rebuildTestsWorkerRun, .{ run, ttyconf, rebuild_node }); |
| 24 | count += 1; | |
| 36 | try fuzz_run_steps.append(gpa, run); | |
| 25 | 37 | } |
| 26 | 38 | } |
| 27 | if (count == 0) fatal("no fuzz tests found", .{}); | |
| 28 | rebuild_node.setEstimatedTotalItems(count); | |
| 29 | break :block count; | |
| 39 | if (fuzz_run_steps.items.len == 0) fatal("no fuzz tests found", .{}); | |
| 40 | rebuild_node.setEstimatedTotalItems(fuzz_run_steps.items.len); | |
| 41 | break :block try arena.dupe(*Step.Run, fuzz_run_steps.items); | |
| 30 | 42 | }; |
| 31 | 43 | |
| 32 | 44 | // Detect failure. |
| 33 | for (all_steps) |step| { | |
| 34 | const run = step.cast(Step.Run) orelse continue; | |
| 35 | if (run.fuzz_tests.items.len > 0 and run.rebuilt_executable == null) | |
| 45 | for (fuzz_run_steps) |run| { | |
| 46 | assert(run.fuzz_tests.items.len > 0); | |
| 47 | if (run.rebuilt_executable == null) | |
| 36 | 48 | fatal("one or more unit tests failed to be rebuilt in fuzz mode", .{}); |
| 37 | 49 | } |
| 38 | 50 | |
| 51 | var web_server: WebServer = .{ | |
| 52 | .gpa = gpa, | |
| 53 | .global_cache_directory = global_cache_directory, | |
| 54 | .zig_lib_directory = zig_lib_directory, | |
| 55 | .zig_exe_path = zig_exe_path, | |
| 56 | .msg_queue = .{}, | |
| 57 | .mutex = .{}, | |
| 58 | .listen_address = listen_address, | |
| 59 | .fuzz_run_steps = fuzz_run_steps, | |
| 60 | }; | |
| 61 | ||
| 62 | const web_server_thread = std.Thread.spawn(.{}, WebServer.run, .{&web_server}) catch |err| { | |
| 63 | fatal("unable to spawn web server thread: {s}", .{@errorName(err)}); | |
| 64 | }; | |
| 65 | defer web_server_thread.join(); | |
| 66 | ||
| 39 | 67 | { |
| 40 | const fuzz_node = prog_node.start("Fuzzing", count); | |
| 68 | const fuzz_node = prog_node.start("Fuzzing", fuzz_run_steps.len); | |
| 41 | 69 | defer fuzz_node.end(); |
| 42 | 70 | var wait_group: std.Thread.WaitGroup = .{}; |
| 43 | 71 | defer wait_group.wait(); |
| 44 | 72 | |
| 45 | for (all_steps) |step| { | |
| 46 | const run = step.cast(Step.Run) orelse continue; | |
| 73 | for (fuzz_run_steps) |run| { | |
| 47 | 74 | for (run.fuzz_tests.items) |unit_test_index| { |
| 48 | 75 | assert(run.rebuilt_executable != null); |
| 49 | thread_pool.spawnWg(&wait_group, fuzzWorkerRun, .{ run, unit_test_index, ttyconf, fuzz_node }); | |
| 76 | thread_pool.spawnWg(&wait_group, fuzzWorkerRun, .{ | |
| 77 | run, &web_server, unit_test_index, ttyconf, fuzz_node, | |
| 78 | }); | |
| 50 | 79 | } |
| 51 | 80 | } |
| 52 | 81 | } |
| 53 | 82 | |
| 54 | fatal("all fuzz workers crashed", .{}); | |
| 83 | log.err("all fuzz workers crashed", .{}); | |
| 55 | 84 | } |
| 56 | 85 | |
| 86 | pub const WebServer = struct { | |
| 87 | gpa: Allocator, | |
| 88 | global_cache_directory: Build.Cache.Directory, | |
| 89 | zig_lib_directory: Build.Cache.Directory, | |
| 90 | zig_exe_path: []const u8, | |
| 91 | /// Messages from fuzz workers. Protected by mutex. | |
| 92 | msg_queue: std.ArrayListUnmanaged(Msg), | |
| 93 | mutex: std.Thread.Mutex, | |
| 94 | listen_address: std.net.Address, | |
| 95 | fuzz_run_steps: []const *Step.Run, | |
| 96 | ||
| 97 | const Msg = union(enum) { | |
| 98 | coverage_id: u64, | |
| 99 | }; | |
| 100 | ||
| 101 | fn run(ws: *WebServer) void { | |
| 102 | var http_server = ws.listen_address.listen(.{ | |
| 103 | .reuse_address = true, | |
| 104 | }) catch |err| { | |
| 105 | log.err("failed to listen to port {d}: {s}", .{ ws.listen_address.in.getPort(), @errorName(err) }); | |
| 106 | return; | |
| 107 | }; | |
| 108 | const port = http_server.listen_address.in.getPort(); | |
| 109 | log.info("web interface listening at http://127.0.0.1:{d}/", .{port}); | |
| 110 | ||
| 111 | while (true) { | |
| 112 | const connection = http_server.accept() catch |err| { | |
| 113 | log.err("failed to accept connection: {s}", .{@errorName(err)}); | |
| 114 | return; | |
| 115 | }; | |
| 116 | _ = std.Thread.spawn(.{}, accept, .{ ws, connection }) catch |err| { | |
| 117 | log.err("unable to spawn connection thread: {s}", .{@errorName(err)}); | |
| 118 | connection.stream.close(); | |
| 119 | continue; | |
| 120 | }; | |
| 121 | } | |
| 122 | } | |
| 123 | ||
| 124 | fn accept(ws: *WebServer, connection: std.net.Server.Connection) void { | |
| 125 | defer connection.stream.close(); | |
| 126 | ||
| 127 | var read_buffer: [8000]u8 = undefined; | |
| 128 | var server = std.http.Server.init(connection, &read_buffer); | |
| 129 | while (server.state == .ready) { | |
| 130 | var request = server.receiveHead() catch |err| switch (err) { | |
| 131 | error.HttpConnectionClosing => return, | |
| 132 | else => { | |
| 133 | log.err("closing http connection: {s}", .{@errorName(err)}); | |
| 134 | return; | |
| 135 | }, | |
| 136 | }; | |
| 137 | serveRequest(ws, &request) catch |err| switch (err) { | |
| 138 | error.AlreadyReported => return, | |
| 139 | else => |e| { | |
| 140 | log.err("unable to serve {s}: {s}", .{ request.head.target, @errorName(e) }); | |
| 141 | return; | |
| 142 | }, | |
| 143 | }; | |
| 144 | } | |
| 145 | } | |
| 146 | ||
| 147 | fn serveRequest(ws: *WebServer, request: *std.http.Server.Request) !void { | |
| 148 | if (std.mem.eql(u8, request.head.target, "/") or | |
| 149 | std.mem.eql(u8, request.head.target, "/debug") or | |
| 150 | std.mem.eql(u8, request.head.target, "/debug/")) | |
| 151 | { | |
| 152 | try serveFile(ws, request, "fuzzer/index.html", "text/html"); | |
| 153 | } else if (std.mem.eql(u8, request.head.target, "/main.js") or | |
| 154 | std.mem.eql(u8, request.head.target, "/debug/main.js")) | |
| 155 | { | |
| 156 | try serveFile(ws, request, "fuzzer/main.js", "application/javascript"); | |
| 157 | } else if (std.mem.eql(u8, request.head.target, "/main.wasm")) { | |
| 158 | try serveWasm(ws, request, .ReleaseFast); | |
| 159 | } else if (std.mem.eql(u8, request.head.target, "/debug/main.wasm")) { | |
| 160 | try serveWasm(ws, request, .Debug); | |
| 161 | } else if (std.mem.eql(u8, request.head.target, "/sources.tar") or | |
| 162 | std.mem.eql(u8, request.head.target, "/debug/sources.tar")) | |
| 163 | { | |
| 164 | try serveSourcesTar(ws, request); | |
| 165 | } else { | |
| 166 | try request.respond("not found", .{ | |
| 167 | .status = .not_found, | |
| 168 | .extra_headers = &.{ | |
| 169 | .{ .name = "content-type", .value = "text/plain" }, | |
| 170 | }, | |
| 171 | }); | |
| 172 | } | |
| 173 | } | |
| 174 | ||
| 175 | fn serveFile( | |
| 176 | ws: *WebServer, | |
| 177 | request: *std.http.Server.Request, | |
| 178 | name: []const u8, | |
| 179 | content_type: []const u8, | |
| 180 | ) !void { | |
| 181 | const gpa = ws.gpa; | |
| 182 | // The desired API is actually sendfile, which will require enhancing std.http.Server. | |
| 183 | // We load the file with every request so that the user can make changes to the file | |
| 184 | // and refresh the HTML page without restarting this server. | |
| 185 | const file_contents = ws.zig_lib_directory.handle.readFileAlloc(gpa, name, 10 * 1024 * 1024) catch |err| { | |
| 186 | log.err("failed to read '{}{s}': {s}", .{ ws.zig_lib_directory, name, @errorName(err) }); | |
| 187 | return error.AlreadyReported; | |
| 188 | }; | |
| 189 | defer gpa.free(file_contents); | |
| 190 | try request.respond(file_contents, .{ | |
| 191 | .extra_headers = &.{ | |
| 192 | .{ .name = "content-type", .value = content_type }, | |
| 193 | cache_control_header, | |
| 194 | }, | |
| 195 | }); | |
| 196 | } | |
| 197 | ||
| 198 | fn serveWasm( | |
| 199 | ws: *WebServer, | |
| 200 | request: *std.http.Server.Request, | |
| 201 | optimize_mode: std.builtin.OptimizeMode, | |
| 202 | ) !void { | |
| 203 | const gpa = ws.gpa; | |
| 204 | ||
| 205 | var arena_instance = std.heap.ArenaAllocator.init(gpa); | |
| 206 | defer arena_instance.deinit(); | |
| 207 | const arena = arena_instance.allocator(); | |
| 208 | ||
| 209 | // Do the compilation every request, so that the user can edit the files | |
| 210 | // and see the changes without restarting the server. | |
| 211 | const wasm_binary_path = try buildWasmBinary(ws, arena, optimize_mode); | |
| 212 | // std.http.Server does not have a sendfile API yet. | |
| 213 | const file_contents = try std.fs.cwd().readFileAlloc(gpa, wasm_binary_path, 10 * 1024 * 1024); | |
| 214 | defer gpa.free(file_contents); | |
| 215 | try request.respond(file_contents, .{ | |
| 216 | .extra_headers = &.{ | |
| 217 | .{ .name = "content-type", .value = "application/wasm" }, | |
| 218 | cache_control_header, | |
| 219 | }, | |
| 220 | }); | |
| 221 | } | |
| 222 | ||
| 223 | fn buildWasmBinary( | |
| 224 | ws: *WebServer, | |
| 225 | arena: Allocator, | |
| 226 | optimize_mode: std.builtin.OptimizeMode, | |
| 227 | ) ![]const u8 { | |
| 228 | const gpa = ws.gpa; | |
| 229 | ||
| 230 | const main_src_path: Build.Cache.Path = .{ | |
| 231 | .root_dir = ws.zig_lib_directory, | |
| 232 | .sub_path = "fuzzer/wasm/main.zig", | |
| 233 | }; | |
| 234 | const walk_src_path: Build.Cache.Path = .{ | |
| 235 | .root_dir = ws.zig_lib_directory, | |
| 236 | .sub_path = "docs/wasm/Walk.zig", | |
| 237 | }; | |
| 238 | ||
| 239 | var argv: std.ArrayListUnmanaged([]const u8) = .{}; | |
| 240 | ||
| 241 | try argv.appendSlice(arena, &.{ | |
| 242 | ws.zig_exe_path, | |
| 243 | "build-exe", | |
| 244 | "-fno-entry", | |
| 245 | "-O", | |
| 246 | @tagName(optimize_mode), | |
| 247 | "-target", | |
| 248 | "wasm32-freestanding", | |
| 249 | "-mcpu", | |
| 250 | "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext", | |
| 251 | "--cache-dir", | |
| 252 | ws.global_cache_directory.path orelse ".", | |
| 253 | "--global-cache-dir", | |
| 254 | ws.global_cache_directory.path orelse ".", | |
| 255 | "--name", | |
| 256 | "fuzzer", | |
| 257 | "-rdynamic", | |
| 258 | "--dep", | |
| 259 | "Walk", | |
| 260 | try std.fmt.allocPrint(arena, "-Mroot={}", .{main_src_path}), | |
| 261 | try std.fmt.allocPrint(arena, "-MWalk={}", .{walk_src_path}), | |
| 262 | "--listen=-", | |
| 263 | }); | |
| 264 | ||
| 265 | var child = std.process.Child.init(argv.items, gpa); | |
| 266 | child.stdin_behavior = .Pipe; | |
| 267 | child.stdout_behavior = .Pipe; | |
| 268 | child.stderr_behavior = .Pipe; | |
| 269 | try child.spawn(); | |
| 270 | ||
| 271 | var poller = std.io.poll(gpa, enum { stdout, stderr }, .{ | |
| 272 | .stdout = child.stdout.?, | |
| 273 | .stderr = child.stderr.?, | |
| 274 | }); | |
| 275 | defer poller.deinit(); | |
| 276 | ||
| 277 | try sendMessage(child.stdin.?, .update); | |
| 278 | try sendMessage(child.stdin.?, .exit); | |
| 279 | ||
| 280 | const Header = std.zig.Server.Message.Header; | |
| 281 | var result: ?[]const u8 = null; | |
| 282 | var result_error_bundle = std.zig.ErrorBundle.empty; | |
| 283 | ||
| 284 | const stdout = poller.fifo(.stdout); | |
| 285 | ||
| 286 | poll: while (true) { | |
| 287 | while (stdout.readableLength() < @sizeOf(Header)) { | |
| 288 | if (!(try poller.poll())) break :poll; | |
| 289 | } | |
| 290 | const header = stdout.reader().readStruct(Header) catch unreachable; | |
| 291 | while (stdout.readableLength() < header.bytes_len) { | |
| 292 | if (!(try poller.poll())) break :poll; | |
| 293 | } | |
| 294 | const body = stdout.readableSliceOfLen(header.bytes_len); | |
| 295 | ||
| 296 | switch (header.tag) { | |
| 297 | .zig_version => { | |
| 298 | if (!std.mem.eql(u8, builtin.zig_version_string, body)) { | |
| 299 | return error.ZigProtocolVersionMismatch; | |
| 300 | } | |
| 301 | }, | |
| 302 | .error_bundle => { | |
| 303 | const EbHdr = std.zig.Server.Message.ErrorBundle; | |
| 304 | const eb_hdr = @as(*align(1) const EbHdr, @ptrCast(body)); | |
| 305 | const extra_bytes = | |
| 306 | body[@sizeOf(EbHdr)..][0 .. @sizeOf(u32) * eb_hdr.extra_len]; | |
| 307 | const string_bytes = | |
| 308 | body[@sizeOf(EbHdr) + extra_bytes.len ..][0..eb_hdr.string_bytes_len]; | |
| 309 | // TODO: use @ptrCast when the compiler supports it | |
| 310 | const unaligned_extra = std.mem.bytesAsSlice(u32, extra_bytes); | |
| 311 | const extra_array = try arena.alloc(u32, unaligned_extra.len); | |
| 312 | @memcpy(extra_array, unaligned_extra); | |
| 313 | result_error_bundle = .{ | |
| 314 | .string_bytes = try arena.dupe(u8, string_bytes), | |
| 315 | .extra = extra_array, | |
| 316 | }; | |
| 317 | }, | |
| 318 | .emit_bin_path => { | |
| 319 | const EbpHdr = std.zig.Server.Message.EmitBinPath; | |
| 320 | const ebp_hdr = @as(*align(1) const EbpHdr, @ptrCast(body)); | |
| 321 | if (!ebp_hdr.flags.cache_hit) { | |
| 322 | log.info("source changes detected; rebuilt wasm component", .{}); | |
| 323 | } | |
| 324 | result = try arena.dupe(u8, body[@sizeOf(EbpHdr)..]); | |
| 325 | }, | |
| 326 | else => {}, // ignore other messages | |
| 327 | } | |
| 328 | ||
| 329 | stdout.discard(body.len); | |
| 330 | } | |
| 331 | ||
| 332 | const stderr = poller.fifo(.stderr); | |
| 333 | if (stderr.readableLength() > 0) { | |
| 334 | const owned_stderr = try stderr.toOwnedSlice(); | |
| 335 | defer gpa.free(owned_stderr); | |
| 336 | std.debug.print("{s}", .{owned_stderr}); | |
| 337 | } | |
| 338 | ||
| 339 | // Send EOF to stdin. | |
| 340 | child.stdin.?.close(); | |
| 341 | child.stdin = null; | |
| 342 | ||
| 343 | switch (try child.wait()) { | |
| 344 | .Exited => |code| { | |
| 345 | if (code != 0) { | |
| 346 | log.err( | |
| 347 | "the following command exited with error code {d}:\n{s}", | |
| 348 | .{ code, try Build.Step.allocPrintCmd(arena, null, argv.items) }, | |
| 349 | ); | |
| 350 | return error.WasmCompilationFailed; | |
| 351 | } | |
| 352 | }, | |
| 353 | .Signal, .Stopped, .Unknown => { | |
| 354 | log.err( | |
| 355 | "the following command terminated unexpectedly:\n{s}", | |
| 356 | .{try Build.Step.allocPrintCmd(arena, null, argv.items)}, | |
| 357 | ); | |
| 358 | return error.WasmCompilationFailed; | |
| 359 | }, | |
| 360 | } | |
| 361 | ||
| 362 | if (result_error_bundle.errorMessageCount() > 0) { | |
| 363 | const color = std.zig.Color.auto; | |
| 364 | result_error_bundle.renderToStdErr(color.renderOptions()); | |
| 365 | log.err("the following command failed with {d} compilation errors:\n{s}", .{ | |
| 366 | result_error_bundle.errorMessageCount(), | |
| 367 | try Build.Step.allocPrintCmd(arena, null, argv.items), | |
| 368 | }); | |
| 369 | return error.WasmCompilationFailed; | |
| 370 | } | |
| 371 | ||
| 372 | return result orelse { | |
| 373 | log.err("child process failed to report result\n{s}", .{ | |
| 374 | try Build.Step.allocPrintCmd(arena, null, argv.items), | |
| 375 | }); | |
| 376 | return error.WasmCompilationFailed; | |
| 377 | }; | |
| 378 | } | |
| 379 | ||
| 380 | fn sendMessage(file: std.fs.File, tag: std.zig.Client.Message.Tag) !void { | |
| 381 | const header: std.zig.Client.Message.Header = .{ | |
| 382 | .tag = tag, | |
| 383 | .bytes_len = 0, | |
| 384 | }; | |
| 385 | try file.writeAll(std.mem.asBytes(&header)); | |
| 386 | } | |
| 387 | ||
| 388 | fn serveSourcesTar(ws: *WebServer, request: *std.http.Server.Request) !void { | |
| 389 | const gpa = ws.gpa; | |
| 390 | ||
| 391 | var arena_instance = std.heap.ArenaAllocator.init(gpa); | |
| 392 | defer arena_instance.deinit(); | |
| 393 | const arena = arena_instance.allocator(); | |
| 394 | ||
| 395 | var send_buffer: [0x4000]u8 = undefined; | |
| 396 | var response = request.respondStreaming(.{ | |
| 397 | .send_buffer = &send_buffer, | |
| 398 | .respond_options = .{ | |
| 399 | .extra_headers = &.{ | |
| 400 | .{ .name = "content-type", .value = "application/x-tar" }, | |
| 401 | cache_control_header, | |
| 402 | }, | |
| 403 | }, | |
| 404 | }); | |
| 405 | const w = response.writer(); | |
| 406 | ||
| 407 | const DedupeTable = std.ArrayHashMapUnmanaged(Build.Cache.Path, void, Build.Cache.Path.TableAdapter, false); | |
| 408 | var dedupe_table: DedupeTable = .{}; | |
| 409 | defer dedupe_table.deinit(gpa); | |
| 410 | ||
| 411 | for (ws.fuzz_run_steps) |run_step| { | |
| 412 | const compile_step_inputs = run_step.producer.?.step.inputs.table; | |
| 413 | for (compile_step_inputs.keys(), compile_step_inputs.values()) |dir_path, *file_list| { | |
| 414 | try dedupe_table.ensureUnusedCapacity(gpa, file_list.items.len); | |
| 415 | for (file_list.items) |sub_path| { | |
| 416 | // Special file "." means the entire directory. | |
| 417 | if (std.mem.eql(u8, sub_path, ".")) continue; | |
| 418 | const joined_path = try dir_path.join(arena, sub_path); | |
| 419 | _ = dedupe_table.getOrPutAssumeCapacity(joined_path); | |
| 420 | } | |
| 421 | } | |
| 422 | } | |
| 423 | ||
| 424 | const deduped_paths = dedupe_table.keys(); | |
| 425 | const SortContext = struct { | |
| 426 | pub fn lessThan(this: @This(), lhs: Build.Cache.Path, rhs: Build.Cache.Path) bool { | |
| 427 | _ = this; | |
| 428 | return switch (std.mem.order(u8, lhs.root_dir.path orelse ".", rhs.root_dir.path orelse ".")) { | |
| 429 | .lt => true, | |
| 430 | .gt => false, | |
| 431 | .eq => std.mem.lessThan(u8, lhs.sub_path, rhs.sub_path), | |
| 432 | }; | |
| 433 | } | |
| 434 | }; | |
| 435 | std.mem.sortUnstable(Build.Cache.Path, deduped_paths, SortContext{}, SortContext.lessThan); | |
| 436 | ||
| 437 | for (deduped_paths) |joined_path| { | |
| 438 | var file = joined_path.root_dir.handle.openFile(joined_path.sub_path, .{}) catch |err| { | |
| 439 | log.err("failed to open {}: {s}", .{ joined_path, @errorName(err) }); | |
| 440 | continue; | |
| 441 | }; | |
| 442 | defer file.close(); | |
| 443 | ||
| 444 | const stat = file.stat() catch |err| { | |
| 445 | log.err("failed to stat {}: {s}", .{ joined_path, @errorName(err) }); | |
| 446 | continue; | |
| 447 | }; | |
| 448 | if (stat.kind != .file) | |
| 449 | continue; | |
| 450 | ||
| 451 | const padding = p: { | |
| 452 | const remainder = stat.size % 512; | |
| 453 | break :p if (remainder > 0) 512 - remainder else 0; | |
| 454 | }; | |
| 455 | ||
| 456 | var file_header = std.tar.output.Header.init(); | |
| 457 | file_header.typeflag = .regular; | |
| 458 | try file_header.setPath(joined_path.root_dir.path orelse ".", joined_path.sub_path); | |
| 459 | try file_header.setSize(stat.size); | |
| 460 | try file_header.updateChecksum(); | |
| 461 | try w.writeAll(std.mem.asBytes(&file_header)); | |
| 462 | try w.writeFile(file); | |
| 463 | try w.writeByteNTimes(0, padding); | |
| 464 | } | |
| 465 | ||
| 466 | // intentionally omitting the pointless trailer | |
| 467 | //try w.writeByteNTimes(0, 512 * 2); | |
| 468 | try response.end(); | |
| 469 | } | |
| 470 | ||
| 471 | const cache_control_header: std.http.Header = .{ | |
| 472 | .name = "cache-control", | |
| 473 | .value = "max-age=0, must-revalidate", | |
| 474 | }; | |
| 475 | }; | |
| 476 | ||
| 57 | 477 | fn rebuildTestsWorkerRun(run: *Step.Run, ttyconf: std.io.tty.Config, parent_prog_node: std.Progress.Node) void { |
| 58 | 478 | const gpa = run.step.owner.allocator; |
| 59 | 479 | const stderr = std.io.getStdErr(); |
| ... | ... | @@ -88,6 +508,7 @@ fn rebuildTestsWorkerRun(run: *Step.Run, ttyconf: std.io.tty.Config, parent_prog |
| 88 | 508 | |
| 89 | 509 | fn fuzzWorkerRun( |
| 90 | 510 | run: *Step.Run, |
| 511 | web_server: *WebServer, | |
| 91 | 512 | unit_test_index: u32, |
| 92 | 513 | ttyconf: std.io.tty.Config, |
| 93 | 514 | parent_prog_node: std.Progress.Node, |
| ... | ... | @@ -98,7 +519,7 @@ fn fuzzWorkerRun( |
| 98 | 519 | const prog_node = parent_prog_node.start(test_name, 0); |
| 99 | 520 | defer prog_node.end(); |
| 100 | 521 | |
| 101 | run.rerunInFuzzMode(unit_test_index, prog_node) catch |err| switch (err) { | |
| 522 | run.rerunInFuzzMode(web_server, unit_test_index, prog_node) catch |err| switch (err) { | |
| 102 | 523 | error.MakeFailed => { |
| 103 | 524 | const stderr = std.io.getStdErr(); |
| 104 | 525 | std.debug.lockStdErr(); |
lib/std/Build/Step.zig+2-1| ... | ... | @@ -559,7 +559,8 @@ fn zigProcessUpdate(s: *Step, zp: *ZigProcess, watch: bool) !?[]const u8 { |
| 559 | 559 | }, |
| 560 | 560 | .zig_lib => zl: { |
| 561 | 561 | if (s.cast(Step.Compile)) |compile| { |
| 562 | if (compile.zig_lib_dir) |lp| { | |
| 562 | if (compile.zig_lib_dir) |zig_lib_dir| { | |
| 563 | const lp = try zig_lib_dir.join(arena, sub_path); | |
| 563 | 564 | try addWatchInput(s, lp); |
| 564 | 565 | break :zl; |
| 565 | 566 | } |
lib/std/Build/Step/Run.zig+38-14| ... | ... | @@ -205,6 +205,7 @@ pub fn enableTestRunnerMode(run: *Run) void { |
| 205 | 205 | run.stdio = .zig_test; |
| 206 | 206 | run.addArgs(&.{ |
| 207 | 207 | std.fmt.allocPrint(arena, "--seed=0x{x}", .{b.graph.random_seed}) catch @panic("OOM"), |
| 208 | std.fmt.allocPrint(arena, "--cache-dir={s}", .{b.cache_root.path orelse ""}) catch @panic("OOM"), | |
| 208 | 209 | "--listen=-", |
| 209 | 210 | }); |
| 210 | 211 | } |
| ... | ... | @@ -845,7 +846,12 @@ fn make(step: *Step, options: Step.MakeOptions) !void { |
| 845 | 846 | ); |
| 846 | 847 | } |
| 847 | 848 | |
| 848 | pub fn rerunInFuzzMode(run: *Run, unit_test_index: u32, prog_node: std.Progress.Node) !void { | |
| 849 | pub fn rerunInFuzzMode( | |
| 850 | run: *Run, | |
| 851 | web_server: *std.Build.Fuzz.WebServer, | |
| 852 | unit_test_index: u32, | |
| 853 | prog_node: std.Progress.Node, | |
| 854 | ) !void { | |
| 849 | 855 | const step = &run.step; |
| 850 | 856 | const b = step.owner; |
| 851 | 857 | const arena = b.allocator; |
| ... | ... | @@ -877,7 +883,10 @@ pub fn rerunInFuzzMode(run: *Run, unit_test_index: u32, prog_node: std.Progress. |
| 877 | 883 | const has_side_effects = false; |
| 878 | 884 | const rand_int = std.crypto.random.int(u64); |
| 879 | 885 | const tmp_dir_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int); |
| 880 | try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, prog_node, unit_test_index); | |
| 886 | try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, prog_node, .{ | |
| 887 | .unit_test_index = unit_test_index, | |
| 888 | .web_server = web_server, | |
| 889 | }); | |
| 881 | 890 | } |
| 882 | 891 | |
| 883 | 892 | fn populateGeneratedPaths( |
| ... | ... | @@ -952,13 +961,18 @@ fn termMatches(expected: ?std.process.Child.Term, actual: std.process.Child.Term |
| 952 | 961 | }; |
| 953 | 962 | } |
| 954 | 963 | |
| 964 | const FuzzContext = struct { | |
| 965 | web_server: *std.Build.Fuzz.WebServer, | |
| 966 | unit_test_index: u32, | |
| 967 | }; | |
| 968 | ||
| 955 | 969 | fn runCommand( |
| 956 | 970 | run: *Run, |
| 957 | 971 | argv: []const []const u8, |
| 958 | 972 | has_side_effects: bool, |
| 959 | 973 | output_dir_path: []const u8, |
| 960 | 974 | prog_node: std.Progress.Node, |
| 961 | fuzz_unit_test_index: ?u32, | |
| 975 | fuzz_context: ?FuzzContext, | |
| 962 | 976 | ) !void { |
| 963 | 977 | const step = &run.step; |
| 964 | 978 | const b = step.owner; |
| ... | ... | @@ -977,7 +991,7 @@ fn runCommand( |
| 977 | 991 | var interp_argv = std.ArrayList([]const u8).init(b.allocator); |
| 978 | 992 | defer interp_argv.deinit(); |
| 979 | 993 | |
| 980 | const result = spawnChildAndCollect(run, argv, has_side_effects, prog_node, fuzz_unit_test_index) catch |err| term: { | |
| 994 | const result = spawnChildAndCollect(run, argv, has_side_effects, prog_node, fuzz_context) catch |err| term: { | |
| 981 | 995 | // InvalidExe: cpu arch mismatch |
| 982 | 996 | // FileNotFound: can happen with a wrong dynamic linker path |
| 983 | 997 | if (err == error.InvalidExe or err == error.FileNotFound) interpret: { |
| ... | ... | @@ -1113,7 +1127,7 @@ fn runCommand( |
| 1113 | 1127 | |
| 1114 | 1128 | try Step.handleVerbose2(step.owner, cwd, run.env_map, interp_argv.items); |
| 1115 | 1129 | |
| 1116 | break :term spawnChildAndCollect(run, interp_argv.items, has_side_effects, prog_node, fuzz_unit_test_index) catch |e| { | |
| 1130 | break :term spawnChildAndCollect(run, interp_argv.items, has_side_effects, prog_node, fuzz_context) catch |e| { | |
| 1117 | 1131 | if (!run.failing_to_execute_foreign_is_an_error) return error.MakeSkipped; |
| 1118 | 1132 | |
| 1119 | 1133 | return step.fail("unable to spawn interpreter {s}: {s}", .{ |
| ... | ... | @@ -1133,7 +1147,7 @@ fn runCommand( |
| 1133 | 1147 | |
| 1134 | 1148 | const final_argv = if (interp_argv.items.len == 0) argv else interp_argv.items; |
| 1135 | 1149 | |
| 1136 | if (fuzz_unit_test_index != null) { | |
| 1150 | if (fuzz_context != null) { | |
| 1137 | 1151 | try step.handleChildProcessTerm(result.term, cwd, final_argv); |
| 1138 | 1152 | return; |
| 1139 | 1153 | } |
| ... | ... | @@ -1298,12 +1312,12 @@ fn spawnChildAndCollect( |
| 1298 | 1312 | argv: []const []const u8, |
| 1299 | 1313 | has_side_effects: bool, |
| 1300 | 1314 | prog_node: std.Progress.Node, |
| 1301 | fuzz_unit_test_index: ?u32, | |
| 1315 | fuzz_context: ?FuzzContext, | |
| 1302 | 1316 | ) !ChildProcResult { |
| 1303 | 1317 | const b = run.step.owner; |
| 1304 | 1318 | const arena = b.allocator; |
| 1305 | 1319 | |
| 1306 | if (fuzz_unit_test_index != null) { | |
| 1320 | if (fuzz_context != null) { | |
| 1307 | 1321 | assert(!has_side_effects); |
| 1308 | 1322 | assert(run.stdio == .zig_test); |
| 1309 | 1323 | } |
| ... | ... | @@ -1357,7 +1371,7 @@ fn spawnChildAndCollect( |
| 1357 | 1371 | var timer = try std.time.Timer.start(); |
| 1358 | 1372 | |
| 1359 | 1373 | const result = if (run.stdio == .zig_test) |
| 1360 | evalZigTest(run, &child, prog_node, fuzz_unit_test_index) | |
| 1374 | evalZigTest(run, &child, prog_node, fuzz_context) | |
| 1361 | 1375 | else |
| 1362 | 1376 | evalGeneric(run, &child); |
| 1363 | 1377 | |
| ... | ... | @@ -1383,7 +1397,7 @@ fn evalZigTest( |
| 1383 | 1397 | run: *Run, |
| 1384 | 1398 | child: *std.process.Child, |
| 1385 | 1399 | prog_node: std.Progress.Node, |
| 1386 | fuzz_unit_test_index: ?u32, | |
| 1400 | fuzz_context: ?FuzzContext, | |
| 1387 | 1401 | ) !StdIoResult { |
| 1388 | 1402 | const gpa = run.step.owner.allocator; |
| 1389 | 1403 | const arena = run.step.owner.allocator; |
| ... | ... | @@ -1394,8 +1408,8 @@ fn evalZigTest( |
| 1394 | 1408 | }); |
| 1395 | 1409 | defer poller.deinit(); |
| 1396 | 1410 | |
| 1397 | if (fuzz_unit_test_index) |index| { | |
| 1398 | try sendRunTestMessage(child.stdin.?, .start_fuzzing, index); | |
| 1411 | if (fuzz_context) |fuzz| { | |
| 1412 | try sendRunTestMessage(child.stdin.?, .start_fuzzing, fuzz.unit_test_index); | |
| 1399 | 1413 | } else { |
| 1400 | 1414 | run.fuzz_tests.clearRetainingCapacity(); |
| 1401 | 1415 | try sendMessage(child.stdin.?, .query_test_metadata); |
| ... | ... | @@ -1437,7 +1451,7 @@ fn evalZigTest( |
| 1437 | 1451 | } |
| 1438 | 1452 | }, |
| 1439 | 1453 | .test_metadata => { |
| 1440 | assert(fuzz_unit_test_index == null); | |
| 1454 | assert(fuzz_context == null); | |
| 1441 | 1455 | const TmHdr = std.zig.Server.Message.TestMetadata; |
| 1442 | 1456 | const tm_hdr = @as(*align(1) const TmHdr, @ptrCast(body)); |
| 1443 | 1457 | test_count = tm_hdr.tests_len; |
| ... | ... | @@ -1466,7 +1480,7 @@ fn evalZigTest( |
| 1466 | 1480 | try requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node); |
| 1467 | 1481 | }, |
| 1468 | 1482 | .test_results => { |
| 1469 | assert(fuzz_unit_test_index == null); | |
| 1483 | assert(fuzz_context == null); | |
| 1470 | 1484 | const md = metadata.?; |
| 1471 | 1485 | |
| 1472 | 1486 | const TrHdr = std.zig.Server.Message.TestResults; |
| ... | ... | @@ -1500,6 +1514,16 @@ fn evalZigTest( |
| 1500 | 1514 | |
| 1501 | 1515 | try requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node); |
| 1502 | 1516 | }, |
| 1517 | .coverage_id => { | |
| 1518 | const web_server = fuzz_context.?.web_server; | |
| 1519 | const msg_ptr: *align(1) const u64 = @ptrCast(body); | |
| 1520 | const coverage_id = msg_ptr.*; | |
| 1521 | { | |
| 1522 | web_server.mutex.lock(); | |
| 1523 | defer web_server.mutex.unlock(); | |
| 1524 | try web_server.msg_queue.append(web_server.gpa, .{ .coverage_id = coverage_id }); | |
| 1525 | } | |
| 1526 | }, | |
| 1503 | 1527 | else => {}, // ignore other messages |
| 1504 | 1528 | } |
| 1505 | 1529 |
lib/std/zig/Server.zig+18-6| ... | ... | @@ -28,6 +28,10 @@ pub const Message = struct { |
| 28 | 28 | /// The remaining bytes is the file path relative to that prefix. |
| 29 | 29 | /// The prefixes are hard-coded in Compilation.create (cwd, zig lib dir, local cache dir) |
| 30 | 30 | file_system_inputs, |
| 31 | /// Body is a u64le that indicates the file path within the cache used | |
| 32 | /// to store coverage information. The integer is a hash of the PCs | |
| 33 | /// stored within that file. | |
| 34 | coverage_id, | |
| 31 | 35 | |
| 32 | 36 | _, |
| 33 | 37 | }; |
| ... | ... | @@ -180,6 +184,14 @@ pub fn serveMessage( |
| 180 | 184 | try s.out.writevAll(iovecs[0 .. bufs.len + 1]); |
| 181 | 185 | } |
| 182 | 186 | |
| 187 | pub fn serveU64Message(s: *Server, tag: OutMessage.Tag, int: u64) !void { | |
| 188 | const msg_le = bswap(int); | |
| 189 | return s.serveMessage(.{ | |
| 190 | .tag = tag, | |
| 191 | .bytes_len = @sizeOf(u64), | |
| 192 | }, &.{std.mem.asBytes(&msg_le)}); | |
| 193 | } | |
| 194 | ||
| 183 | 195 | pub fn serveEmitBinPath( |
| 184 | 196 | s: *Server, |
| 185 | 197 | fs_path: []const u8, |
| ... | ... | @@ -187,7 +199,7 @@ pub fn serveEmitBinPath( |
| 187 | 199 | ) !void { |
| 188 | 200 | try s.serveMessage(.{ |
| 189 | 201 | .tag = .emit_bin_path, |
| 190 | .bytes_len = @as(u32, @intCast(fs_path.len + @sizeOf(OutMessage.EmitBinPath))), | |
| 202 | .bytes_len = @intCast(fs_path.len + @sizeOf(OutMessage.EmitBinPath)), | |
| 191 | 203 | }, &.{ |
| 192 | 204 | std.mem.asBytes(&header), |
| 193 | 205 | fs_path, |
| ... | ... | @@ -201,7 +213,7 @@ pub fn serveTestResults( |
| 201 | 213 | const msg_le = bswap(msg); |
| 202 | 214 | try s.serveMessage(.{ |
| 203 | 215 | .tag = .test_results, |
| 204 | .bytes_len = @as(u32, @intCast(@sizeOf(OutMessage.TestResults))), | |
| 216 | .bytes_len = @intCast(@sizeOf(OutMessage.TestResults)), | |
| 205 | 217 | }, &.{ |
| 206 | 218 | std.mem.asBytes(&msg_le), |
| 207 | 219 | }); |
| ... | ... | @@ -209,14 +221,14 @@ pub fn serveTestResults( |
| 209 | 221 | |
| 210 | 222 | pub fn serveErrorBundle(s: *Server, error_bundle: std.zig.ErrorBundle) !void { |
| 211 | 223 | const eb_hdr: OutMessage.ErrorBundle = .{ |
| 212 | .extra_len = @as(u32, @intCast(error_bundle.extra.len)), | |
| 213 | .string_bytes_len = @as(u32, @intCast(error_bundle.string_bytes.len)), | |
| 224 | .extra_len = @intCast(error_bundle.extra.len), | |
| 225 | .string_bytes_len = @intCast(error_bundle.string_bytes.len), | |
| 214 | 226 | }; |
| 215 | 227 | const bytes_len = @sizeOf(OutMessage.ErrorBundle) + |
| 216 | 228 | 4 * error_bundle.extra.len + error_bundle.string_bytes.len; |
| 217 | 229 | try s.serveMessage(.{ |
| 218 | 230 | .tag = .error_bundle, |
| 219 | .bytes_len = @as(u32, @intCast(bytes_len)), | |
| 231 | .bytes_len = @intCast(bytes_len), | |
| 220 | 232 | }, &.{ |
| 221 | 233 | std.mem.asBytes(&eb_hdr), |
| 222 | 234 | // TODO: implement @ptrCast between slices changing the length |
| ... | ... | @@ -251,7 +263,7 @@ pub fn serveTestMetadata(s: *Server, test_metadata: TestMetadata) !void { |
| 251 | 263 | |
| 252 | 264 | return s.serveMessage(.{ |
| 253 | 265 | .tag = .test_metadata, |
| 254 | .bytes_len = @as(u32, @intCast(bytes_len)), | |
| 266 | .bytes_len = @intCast(bytes_len), | |
| 255 | 267 | }, &.{ |
| 256 | 268 | std.mem.asBytes(&header), |
| 257 | 269 | // TODO: implement @ptrCast between slices changing the length |
lib/std/zig/tokenizer.zig+45| ... | ... | @@ -1840,3 +1840,48 @@ fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !v |
| 1840 | 1840 | try std.testing.expectEqual(source.len, last_token.loc.start); |
| 1841 | 1841 | try std.testing.expectEqual(source.len, last_token.loc.end); |
| 1842 | 1842 | } |
| 1843 | ||
| 1844 | test "fuzzable properties upheld" { | |
| 1845 | const source = std.testing.fuzzInput(.{}); | |
| 1846 | const source0 = try std.testing.allocator.dupeZ(u8, source); | |
| 1847 | defer std.testing.allocator.free(source0); | |
| 1848 | var tokenizer = Tokenizer.init(source0); | |
| 1849 | var tokenization_failed = false; | |
| 1850 | while (true) { | |
| 1851 | const token = tokenizer.next(); | |
| 1852 | ||
| 1853 | // Property: token end location after start location (or equal) | |
| 1854 | try std.testing.expect(token.loc.end >= token.loc.start); | |
| 1855 | ||
| 1856 | switch (token.tag) { | |
| 1857 | .invalid => { | |
| 1858 | tokenization_failed = true; | |
| 1859 | ||
| 1860 | // Property: invalid token always ends at newline or eof | |
| 1861 | try std.testing.expect(source0[token.loc.end] == '\n' or source0[token.loc.end] == 0); | |
| 1862 | }, | |
| 1863 | .eof => { | |
| 1864 | // Property: EOF token is always 0-length at end of source. | |
| 1865 | try std.testing.expectEqual(source0.len, token.loc.start); | |
| 1866 | try std.testing.expectEqual(source0.len, token.loc.end); | |
| 1867 | break; | |
| 1868 | }, | |
| 1869 | else => continue, | |
| 1870 | } | |
| 1871 | } | |
| 1872 | ||
| 1873 | if (source0.len > 0) for (source0, source0[1..][0..source0.len]) |cur, next| { | |
| 1874 | // Property: No null byte allowed except at end. | |
| 1875 | if (cur == 0) { | |
| 1876 | try std.testing.expect(tokenization_failed); | |
| 1877 | } | |
| 1878 | // Property: No ASCII control characters other than \n and \t are allowed. | |
| 1879 | if (std.ascii.isControl(cur) and cur != '\n' and cur != '\t') { | |
| 1880 | try std.testing.expect(tokenization_failed); | |
| 1881 | } | |
| 1882 | // Property: All '\r' must be followed by '\n'. | |
| 1883 | if (cur == '\r' and next != '\n') { | |
| 1884 | try std.testing.expect(tokenization_failed); | |
| 1885 | } | |
| 1886 | }; | |
| 1887 | } |