Commit ef4c2193fc

Andrew Kelley <andrew@ziglang.org>
2024-08-05 23:10:43
fuzzer web UI: navigate by source location index
This will help scroll the point of interest into view
1 parent db69641
Changed files (2)
lib
fuzzer
lib/fuzzer/wasm/main.zig
@@ -235,7 +235,59 @@ export fn entryPoints() Slice(u32) {
     return Slice(u32).init(entry_points.items);
 }
 
+/// Index into `coverage_source_locations`.
+const SourceLocationIndex = enum(u32) {
+    _,
+
+    fn haveCoverage(sli: SourceLocationIndex) bool {
+        return @intFromEnum(sli) < coverage_source_locations.items.len;
+    }
+
+    fn ptr(sli: SourceLocationIndex) *Coverage.SourceLocation {
+        return &coverage_source_locations.items[@intFromEnum(sli)];
+    }
+
+    fn sourceLocationLinkHtml(
+        sli: SourceLocationIndex,
+        out: *std.ArrayListUnmanaged(u8),
+    ) Allocator.Error!void {
+        const sl = sli.ptr();
+        try out.writer(gpa).print("<a href=\"#l{d}\">", .{@intFromEnum(sli)});
+        try sli.appendPath(out);
+        try out.writer(gpa).print(":{d}:{d}</a>", .{ sl.line, sl.column });
+    }
+
+    fn appendPath(sli: SourceLocationIndex, out: *std.ArrayListUnmanaged(u8)) Allocator.Error!void {
+        const sl = sli.ptr();
+        const file = coverage.fileAt(sl.file);
+        const file_name = coverage.stringAt(file.basename);
+        const dir_name = coverage.stringAt(coverage.directories.keys()[file.directory_index]);
+        try html_render.appendEscaped(out, dir_name);
+        try out.appendSlice(gpa, "/");
+        try html_render.appendEscaped(out, file_name);
+    }
+
+    fn toWalkFile(sli: SourceLocationIndex) ?Walk.File.Index {
+        var buf: std.ArrayListUnmanaged(u8) = .{};
+        defer buf.deinit(gpa);
+        sli.appendPath(&buf) catch @panic("OOM");
+        return @enumFromInt(Walk.files.getIndex(buf.items) orelse return null);
+    }
+
+    fn fileHtml(
+        sli: SourceLocationIndex,
+        out: *std.ArrayListUnmanaged(u8),
+    ) error{ OutOfMemory, SourceUnavailable }!void {
+        const walk_file_index = sli.toWalkFile() orelse return error.SourceUnavailable;
+        const root_node = walk_file_index.findRootDecl().get().ast_node;
+        html_render.fileSourceHtml(walk_file_index, out, root_node, .{}) catch |err| {
+            fatal("unable to render source: {s}", .{@errorName(err)});
+        };
+    }
+};
+
 var coverage = Coverage.init;
+/// Index of type `SourceLocationIndex`.
 var coverage_source_locations: std.ArrayListUnmanaged(Coverage.SourceLocation) = .{};
 /// Contains the most recent coverage update message, unmodified.
 var recent_coverage_update: std.ArrayListUnmanaged(u8) = .{};
@@ -263,27 +315,24 @@ fn updateCoverage(
     try coverage.directories.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items });
 }
 
-export fn sourceLocationLinkHtml(index: u32) String {
+export fn sourceLocationLinkHtml(index: SourceLocationIndex) String {
     string_result.clearRetainingCapacity();
-    sourceLocationLinkHtmlFallible(index, &string_result) catch @panic("OOM");
+    index.sourceLocationLinkHtml(&string_result) catch @panic("OOM");
     return String.init(string_result.items);
 }
 
-fn sourceLocationLinkHtmlFallible(index: u32, out: *std.ArrayListUnmanaged(u8)) Allocator.Error!void {
-    const sl = coverage_source_locations.items[index];
-    const file = coverage.fileAt(sl.file);
-    const file_name = coverage.stringAt(file.basename);
-    const dir_name = coverage.stringAt(coverage.directories.keys()[file.directory_index]);
-
-    out.clearRetainingCapacity();
-    try out.appendSlice(gpa, "<a href=\"#");
-    _ = html_render.missing_feature_url_escape;
-    try out.writer(gpa).print("{s}/{s}:{d}:{d}", .{
-        dir_name, file_name, sl.line, sl.column,
-    });
-    try out.appendSlice(gpa, "\">");
-    try html_render.appendEscaped(out, dir_name);
-    try out.appendSlice(gpa, "/");
-    try html_render.appendEscaped(out, file_name);
-    try out.writer(gpa).print(":{d}:{d}</a>", .{ sl.line, sl.column });
+/// Returns empty string if coverage metadata is not available for this source location.
+export fn sourceLocationPath(sli: SourceLocationIndex) String {
+    string_result.clearRetainingCapacity();
+    if (sli.haveCoverage()) sli.appendPath(&string_result) catch @panic("OOM");
+    return String.init(string_result.items);
+}
+
+export fn sourceLocationFileHtml(sli: SourceLocationIndex) String {
+    string_result.clearRetainingCapacity();
+    sli.fileHtml(&string_result) catch |err| switch (err) {
+        error.OutOfMemory => @panic("OOM"),
+        error.SourceUnavailable => {},
+    };
+    return String.init(string_result.items);
 }
lib/fuzzer/main.js
@@ -68,6 +68,8 @@
   }
 
   function navigate(location_hash) {
+    domSectSource.classList.add("hidden");
+
     curNavLocation = null;
     curNavSearch = null;
 
@@ -82,23 +84,19 @@
         curNavSearch = decodeURIComponent(query.substring(qpos + 1));
       }
 
-      if (nonSearchPart.length > 0) {
-        curNavLocation = nonSearchPart;
+      if (nonSearchPart[0] == "l") {
+        curNavLocation = +nonSearchPart.substring(1);
+        renderSource(curNavLocation);
       }
     }
 
     render();
-
-    if (curNavLocation != null) {
-      // TODO
-      // scrollToSourceLocation(findSourceLocationIndex(curNavLocation));
-    }
   }
 
   function connectWebSocket() {
-    const host = window.document.location.host;
-    const pathname = window.document.location.pathname;
-    const isHttps = window.document.location.protocol === 'https:';
+    const host = document.location.host;
+    const pathname = document.location.pathname;
+    const isHttps = document.location.protocol === 'https:';
     const match = host.match(/^(.+):(\d+)$/);
     const defaultPort = isHttps ? 443 : 80;
     const port = match ? parseInt(match[2], 10) : defaultPort;
@@ -139,17 +137,12 @@
   }
 
   function onSourceIndexChange() {
-    console.log("source location index metadata updated");
     render();
+    if (curNavLocation != null) renderSource(curNavLocation);
   }
 
   function render() {
     domStatus.classList.add("hidden");
-    domSectSource.classList.add("hidden");
-
-    if (curNavLocation != null) {
-      renderSource(curNavLocation.split(":")[0]);
-    }
   }
 
   function renderStats() {
@@ -186,22 +179,23 @@
     return ((Number(a) / Number(b)) * 100).toFixed(1);
   }
 
-  function renderSource(path) {
-    const decl_index = findFileRoot(path);
-    if (decl_index == null) throw new Error("file not found: " + path);
+  function renderSource(sourceLocationIndex) {
+    const pathName = unwrapString(wasm_exports.sourceLocationPath(sourceLocationIndex));
+    if (pathName.length === 0) return;
 
     const h2 = domSectSource.children[0];
-    h2.innerText = path;
-    domSourceText.innerHTML = declSourceHtml(decl_index);
+    h2.innerText = pathName;
+    domSourceText.innerHTML = unwrapString(wasm_exports.sourceLocationFileHtml(sourceLocationIndex));
 
     domSectSource.classList.remove("hidden");
-  }
 
-  function findFileRoot(path) {
-    setInputString(path);
-    const result = wasm_exports.find_file_root();
-    if (result === -1) return null;
-    return result;
+    const slDom = document.getElementById("l" + sourceLocationIndex);
+    if (slDom != null) {
+      slDom.scrollIntoView({
+        behavior: "smooth",
+        block: "center",
+      });
+    }
   }
 
   function decodeString(ptr, len) {
@@ -224,10 +218,6 @@
     wasmArray.set(jsArray);
   }
 
-  function declSourceHtml(decl_index) {
-    return unwrapString(wasm_exports.decl_source_html(decl_index));
-  }
-
   function unwrapString(bigint) {
     const ptr = Number(bigint & 0xffffffffn);
     const len = Number(bigint >> 32n);