Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions fuzz_tests.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
test {
_ = @import("test/fuzz/property_test.zig");
_ = @import("test/fuzz/corpus_test.zig");
}
22 changes: 22 additions & 0 deletions src/frontmatter.zig
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,10 @@ fn linkCommentAnchor(allocator: std.mem.Allocator, content: []const u8, anchor:
continue;
}

if (in_files_section and trimmed.len == 0) {
continue;
}

if (in_files_section and trimmed.len > 0 and !std.mem.startsWith(u8, trimmed, "- ")) {
if (!found_existing and !wrote_anchor) {
try writer.writeAll(" - ");
Expand Down Expand Up @@ -1056,6 +1060,24 @@ test "linkAnchor adds to comment-based anchor" {
try std.testing.expect(std.mem.indexOf(u8, result, "src/new.ts@abc") != null);
}

test "linkAnchor is idempotent for comment-based anchors" {
const allocator = std.testing.allocator;
const content =
"# Doc\n\n" ++
"<!-- drift:\n" ++
" files:\n" ++
" - src/existing.ts\n" ++
"-->\n";

const first = try linkAnchor(allocator, content, "src/new.ts@abc");
defer allocator.free(first);

const second = try linkAnchor(allocator, first, "src/new.ts@abc");
defer allocator.free(second);

try std.testing.expectEqualStrings(first, second);
}

test "linkAnchor preserves comment-based drift when unrelated frontmatter exists" {
const allocator = std.testing.allocator;
const content =
Expand Down
123 changes: 123 additions & 0 deletions test/fuzz/corpus_test.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
const std = @import("std");
const helper = @import("helpers.zig");

const frontmatter = helper.frontmatter;
const scanner = helper.scanner;

test "corpus: inline refs ignore varied markdown code contexts" {
const allocator = std.testing.allocator;
const first = "src/deep/file.test.ts";
const second = "src/.hidden/mod.impl.rs#Thing";
const content =
"# Spec\n\n" ++
"See (@./src/deep/file.test.ts) and <@./src/.hidden/mod.impl.rs#Thing>.\n\n" ++
"``@./src/deep/file.test.ts`` should be ignored.\n\n" ++
"~~~md\n@./src/.hidden/mod.impl.rs#Thing\n~~~\n\n" ++
" ```ts\n" ++
" @./src/deep/file.test.ts\n" ++
" ```\n";

var anchors = scanner.parseInlineAnchors(allocator, content);
defer {
for (anchors.items) |anchor| allocator.free(anchor);
anchors.deinit(allocator);
}

try std.testing.expectEqual(@as(usize, 2), anchors.items.len);
try std.testing.expectEqualStrings(first, anchors.items[0]);
try std.testing.expectEqualStrings(second, anchors.items[1]);

const updated = try scanner.updateInlineAnchors(allocator, content, null, "corpus123");
defer allocator.free(updated);

try std.testing.expect(std.mem.indexOf(u8, updated, "(@./src/deep/file.test.ts@corpus123)") != null);
try std.testing.expect(std.mem.indexOf(u8, updated, "<@./src/.hidden/mod.impl.rs#Thing@corpus123>") != null);
try std.testing.expect(std.mem.indexOf(u8, updated, "``@./src/deep/file.test.ts``") != null);
try std.testing.expect(std.mem.indexOf(u8, updated, "~~~md\n@./src/.hidden/mod.impl.rs#Thing\n~~~") != null);
try std.testing.expect(std.mem.indexOf(u8, updated, " @./src/deep/file.test.ts\n ```") != null);
}

test "corpus: punctuation wrapped refs parse and rewrite as expected" {
const allocator = std.testing.allocator;
const cases = [_]struct {
raw: []const u8,
parsed: []const u8,
rewritten: []const u8,
}{
.{
.raw = "\"@./src/app/main.test.ts\"",
.parsed = "src/app/main.test.ts",
.rewritten = "\"@./src/app/main.test.ts@seedwrap\"",
},
.{
.raw = "'@./src/lib/core.rs#Thing'",
.parsed = "src/lib/core.rs#Thing",
.rewritten = "'@./src/lib/core.rs#Thing@seedwrap'",
},
.{
.raw = "(@./src/tools/gen.zig)",
.parsed = "src/tools/gen.zig",
.rewritten = "(@./src/tools/gen.zig@seedwrap)",
},
.{
.raw = "<@./src/.hidden/mod.impl.py#Thing>",
.parsed = "src/.hidden/mod.impl.py#Thing",
.rewritten = "<@./src/.hidden/mod.impl.py#Thing@seedwrap>",
},
};

for (cases) |case| {
const content = try std.fmt.allocPrint(allocator, "# Spec\n\nSee {s}.\n", .{case.raw});
defer allocator.free(content);

var anchors = scanner.parseInlineAnchors(allocator, content);
defer {
for (anchors.items) |anchor| allocator.free(anchor);
anchors.deinit(allocator);
}

try std.testing.expectEqual(@as(usize, 1), anchors.items.len);
try std.testing.expectEqualStrings(case.parsed, anchors.items[0]);

const updated = try scanner.updateInlineAnchors(allocator, content, null, "seedwrap");
defer allocator.free(updated);
try std.testing.expect(std.mem.indexOf(u8, updated, case.rewritten) != null);
}
}

test "corpus: relink preserves identities in fixed frontmatter and comment docs" {
const allocator = std.testing.allocator;
const anchors = [_][]const u8{
"src/auth/login.test.ts",
"src/payments/.hidden/stripe.impl.rs#Config",
};
const provenance = "c0ffee12";

const frontmatter_doc = try helper.renderFrontmatterDoc(allocator, &anchors);
defer allocator.free(frontmatter_doc);
const relinked_frontmatter = try frontmatter.relinkAllAnchors(allocator, frontmatter_doc, provenance);
defer allocator.free(relinked_frontmatter);

var parsed_frontmatter = frontmatter.parseDriftSpec(allocator, relinked_frontmatter) orelse return error.TestUnexpectedResult;
defer {
for (parsed_frontmatter.items) |anchor| allocator.free(anchor);
parsed_frontmatter.deinit(allocator);
}
try std.testing.expectEqual(@as(usize, 2), parsed_frontmatter.items.len);
try helper.expectAnchorPresent(parsed_frontmatter.items, "src/auth/login.test.ts@c0ffee12");
try helper.expectAnchorPresent(parsed_frontmatter.items, "src/payments/.hidden/stripe.impl.rs#Config@c0ffee12");

const comment_doc = try helper.renderCommentDoc(allocator, &anchors);
defer allocator.free(comment_doc);
const relinked_comment = try frontmatter.relinkAllAnchors(allocator, comment_doc, provenance);
defer allocator.free(relinked_comment);

var parsed_comment = frontmatter.parseDriftSpec(allocator, relinked_comment) orelse return error.TestUnexpectedResult;
defer {
for (parsed_comment.items) |anchor| allocator.free(anchor);
parsed_comment.deinit(allocator);
}
try std.testing.expectEqual(@as(usize, 2), parsed_comment.items.len);
try helper.expectAnchorPresent(parsed_comment.items, "src/auth/login.test.ts@c0ffee12");
try helper.expectAnchorPresent(parsed_comment.items, "src/payments/.hidden/stripe.impl.rs#Config@c0ffee12");
}
178 changes: 178 additions & 0 deletions test/fuzz/helpers.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
const std = @import("std");

pub const frontmatter = @import("../../src/frontmatter.zig");
pub const scanner = @import("../../src/scanner.zig");

pub const Wrapper = struct {
prefix: []const u8,
suffix: []const u8,
};

pub fn appendRandomChars(
buf: *std.ArrayList(u8),
allocator: std.mem.Allocator,
random: std.Random,
alphabet: []const u8,
len: usize,
) !void {
for (0..len) |_| {
const idx = random.uintLessThan(usize, alphabet.len);
try buf.append(allocator, alphabet[idx]);
}
}

pub fn randomAnchor(allocator: std.mem.Allocator, random: std.Random) ![]const u8 {
const path_chars = "abcdefghijklmnopqrstuvwxyz0123456789_-";
const symbol_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_";
const extensions = [_][]const u8{ ".ts", ".py", ".rs", ".zig" };
const infixes = [_][]const u8{ ".test", ".spec", ".impl", ".gen" };

var buf: std.ArrayList(u8) = .{};
defer buf.deinit(allocator);

try buf.appendSlice(allocator, "src");

const segment_count = random.intRangeAtMost(usize, 1, 3);
for (0..segment_count) |segment_idx| {
try buf.append(allocator, '/');
if (random.boolean()) {
try buf.append(allocator, '.');
}
const segment_len = random.intRangeAtMost(usize, 3, 10);
try appendRandomChars(&buf, allocator, random, path_chars, segment_len);
if (segment_idx + 1 == segment_count and random.boolean()) {
const infix = infixes[random.uintLessThan(usize, infixes.len)];
try buf.appendSlice(allocator, infix);
}
}

const ext = extensions[random.uintLessThan(usize, extensions.len)];
try buf.appendSlice(allocator, ext);

if (random.boolean()) {
try buf.append(allocator, '#');
const symbol_len = random.intRangeAtMost(usize, 3, 12);
try appendRandomChars(&buf, allocator, random, symbol_chars, symbol_len);
}

return try allocator.dupe(u8, buf.items);
}

pub fn baseDoc(allocator: std.mem.Allocator, variant: usize) ![]const u8 {
return switch (variant % 6) {
0 => try allocator.dupe(
u8,
"# Spec\n\nSome prose.\n",
),
1 => try allocator.dupe(
u8,
"---\n" ++
"drift:\n" ++
" files:\n" ++
"---\n" ++
"# Spec\n",
),
2 => try allocator.dupe(
u8,
"---\n" ++
"title: My Doc\n" ++
"tags:\n" ++
" - docs\n" ++
"---\n" ++
"# Spec\n",
),
3 => try allocator.dupe(
u8,
"# Spec\n\n" ++
"<!-- drift:\n" ++
" files:\n" ++
" - src/existing.ts\n" ++
"-->\n",
),
4 => try allocator.dupe(
u8,
"---\n" ++
"title: My Doc\n" ++
"---\n\n" ++
"<!-- drift:\n" ++
" files:\n" ++
" - src/existing.ts\n" ++
"-->\n\n" ++
"Body.\n",
),
else => try allocator.dupe(
u8,
"---\n" ++
"drift:\n" ++
" files:\n" ++
" - src/existing.ts\n" ++
"---\n\n" ++
"See @./src/existing.ts in prose.\n",
),
};
}

pub fn randomProvenance(allocator: std.mem.Allocator, random: std.Random) ![]const u8 {
const chars = "abcdef0123456789";

var buf: std.ArrayList(u8) = .{};
defer buf.deinit(allocator);

const len = random.intRangeAtMost(usize, 6, 12);
try appendRandomChars(&buf, allocator, random, chars, len);
return try allocator.dupe(u8, buf.items);
}

pub fn anchorFilePath(anchor: []const u8) []const u8 {
const identity = frontmatter.anchorFileIdentity(anchor);
const hash_pos = std.mem.indexOfScalar(u8, identity, '#');
return if (hash_pos) |pos| identity[0..pos] else identity;
}

pub fn renderFrontmatterDoc(allocator: std.mem.Allocator, anchors: []const []const u8) ![]const u8 {
var out: std.ArrayList(u8) = .{};
defer out.deinit(allocator);
const writer = out.writer(allocator);

try writer.writeAll("---\n");
try writer.writeAll("drift:\n");
try writer.writeAll(" files:\n");
for (anchors) |anchor| {
try writer.print(" - {s}\n", .{anchor});
}
try writer.writeAll("---\n# Spec\n");

return try allocator.dupe(u8, out.items);
}

pub fn renderCommentDoc(allocator: std.mem.Allocator, anchors: []const []const u8) ![]const u8 {
var out: std.ArrayList(u8) = .{};
defer out.deinit(allocator);
const writer = out.writer(allocator);

try writer.writeAll("# Spec\n\n<!-- drift:\n");
try writer.writeAll(" files:\n");
for (anchors) |anchor| {
try writer.print(" - {s}\n", .{anchor});
}
try writer.writeAll("-->\n");

return try allocator.dupe(u8, out.items);
}

pub fn expectAnchorPresent(anchors: []const []const u8, expected: []const u8) !void {
for (anchors) |anchor| {
if (std.mem.eql(u8, anchor, expected)) return;
}
std.debug.print("expected anchor missing: {s}\n", .{expected});
return error.TestUnexpectedResult;
}

pub fn expectAnchorAbsent(anchors: []const []const u8, unexpected: []const u8) !void {
for (anchors) |anchor| {
if (std.mem.eql(u8, anchor, unexpected)) {
std.debug.print("unexpected anchor present: {s}\n", .{unexpected});
return error.TestUnexpectedResult;
}
}
}
Loading
Loading