lib/css/src/match/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 const atom = @import("../atom.zig");
4 const engine = @import("engine.zig");
5 const selector = @import("../selector/root.zig");
6 const tree_fixture = @import("fixture/tree.zig");
7
8 const corpus = @embedFile("corpus");
9
10 const Harness = struct {
11 records: [256]atom.Record = undefined,
12 slots: [1024]u32 = @splat(0),
13 arena: [4096]u8 = undefined,
14 classes: [256]u32 = undefined,
15 compounds: [256]selector.Compound = undefined,
16 combinators: [256]selector.Combinator = undefined,
17 nths: [256]selector.nth.Nth = undefined,
18 selectors: [64]selector.Selector = undefined,
19 diagnostics: [64][]const u8 = undefined,
20 table: atom.Table = undefined,
21 tree: tree_fixture.Tree = .{},
22
23 fn start(self: *Harness) void {
24 self.slots = @splat(0);
25 self.table = .{ .records = &self.records, .slots = &self.slots, .arena = &self.arena };
26 self.tree = .{};
27 }
28
29 fn builder(self: *Harness, source: []const u8) selector.Builder {
30 return .{
31 .source = source,
32 .atoms = &self.table,
33 .classes = &self.classes,
34 .compounds = &self.compounds,
35 .combinators = &self.combinators,
36 .nths = &self.nths,
37 .selectors = &self.selectors,
38 .diagnostics = &self.diagnostics,
39 };
40 }
41
42 fn context(self: *Harness) engine.Context {
43 return .{ .classes = &self.classes, .nths = &self.nths };
44 }
45
46 fn matched(self: *Harness, source: []const u8, out: *[tree_fixture.max_nodes]u32) !u32 {
47 var build = self.builder(source);
48 const range = try selector.parseList(&build, 0, @intCast(source.len));
49 const element = self.tree.element();
50 var count: u32 = 0;
51 var node: u32 = 0;
52 while (node < self.tree.node_count) : (node += 1) {
53 var index: u32 = 0;
54 while (index < range.count) : (index += 1) {
55 const item = self.selectors[range.first + index];
56 if (!engine.matches(self.context(), item, element, node)) continue;
57 out[count] = node;
58 count += 1;
59 break;
60 }
61 }
62 return count;
63 }
64 };
65
66 fn expectMatch(harness: *Harness, source: []const u8, expected: []const u32) !void {
67 var found: [tree_fixture.max_nodes]u32 = undefined;
68 const count = try harness.matched(source, &found);
69 try std.testing.expectEqualSlices(u32, expected, found[0..count]);
70 }
71
72 test "the selector corpus matches its expected node sets" {
73 var lines = std.mem.splitScalar(u8, corpus, '\n');
74 var harness: Harness = undefined;
75 harness.start();
76 var cases: u32 = 0;
77 var checks: u32 = 0;
78 while (lines.next()) |line| {
79 const trimmed = std.mem.trimEnd(u8, line, " \r");
80 if (trimmed.len == 0) continue;
81 if (std.mem.startsWith(u8, trimmed, "case ")) {
82 harness.start();
83 cases += 1;
84 continue;
85 }
86 if (std.mem.eql(u8, trimmed, "tree")) continue;
87 if (std.mem.startsWith(u8, trimmed, "match ")) {
88 try runCase(&harness, trimmed["match ".len..]);
89 checks += 1;
90 continue;
91 }
92 var depth: u32 = 0;
93 while (depth < trimmed.len and trimmed[depth] == '>') depth += 1;
94 const spec = std.mem.trim(u8, trimmed[depth..], " ");
95 _ = try harness.tree.add(&harness.table, depth, spec);
96 harness.tree.finish();
97 }
98 try std.testing.expectEqual(@as(u32, 9), cases);
99 try std.testing.expectEqual(@as(u32, 45), checks);
100 }
101
102 fn runCase(harness: *Harness, body: []const u8) !void {
103 const bar = std.mem.indexOfScalar(u8, body, '|') orelse return error.BadCorpusLine;
104 const source = std.mem.trim(u8, body[0..bar], " ");
105 var expected: [tree_fixture.max_nodes]u32 = undefined;
106 var count: u32 = 0;
107 var parts = std.mem.tokenizeScalar(u8, body[bar + 1 ..], ' ');
108 while (parts.next()) |part| {
109 expected[count] = try std.fmt.parseInt(u32, std.mem.trim(u8, part, " \r"), 10);
110 count += 1;
111 }
112 var found: [tree_fixture.max_nodes]u32 = undefined;
113 const produced = try harness.matched(source, &found);
114 var wanted_bytes: [512]u8 = undefined;
115 var produced_bytes: [512]u8 = undefined;
116 try std.testing.expectEqualStrings(
117 try render(&wanted_bytes, source, expected[0..count]),
118 try render(&produced_bytes, source, found[0..produced]),
119 );
120 }
121
122 fn render(buffer: []u8, source: []const u8, nodes: []const u32) ![]const u8 {
123 var writer = std.Io.Writer.fixed(buffer);
124 try writer.print("{s} ->", .{source});
125 for (nodes) |node| try writer.print(" {d}", .{node});
126 return writer.buffered();
127 }
128
129 test "a descendant match backtracks past a first matching ancestor" {
130 var harness: Harness = undefined;
131 harness.start();
132 _ = try harness.tree.add(&harness.table, 0, "box.root");
133 _ = try harness.tree.add(&harness.table, 1, "box.mid");
134 _ = try harness.tree.add(&harness.table, 2, "box.mid");
135 _ = try harness.tree.add(&harness.table, 3, "box.leaf");
136 harness.tree.finish();
137 try expectMatch(&harness, ".root > .mid .leaf", &.{3});
138 try expectMatch(&harness, ".root > .mid > .leaf", &.{});
139 try expectMatch(&harness, ".mid .mid .leaf", &.{3});
140 try expectMatch(&harness, ".leaf .mid", &.{});
141 }
142
143 test "a child combinator refuses to climb further than one step" {
144 var harness: Harness = undefined;
145 harness.start();
146 _ = try harness.tree.add(&harness.table, 0, "main");
147 _ = try harness.tree.add(&harness.table, 1, "box");
148 _ = try harness.tree.add(&harness.table, 2, "text");
149 harness.tree.finish();
150 try expectMatch(&harness, "main > text", &.{});
151 try expectMatch(&harness, "main text", &.{2});
152 try expectMatch(&harness, "box > text", &.{2});
153 }
154
155 test "a compound requires every class it names" {
156 var harness: Harness = undefined;
157 harness.start();
158 _ = try harness.tree.add(&harness.table, 0, "box.a.b.c");
159 _ = try harness.tree.add(&harness.table, 1, "box.a");
160 harness.tree.finish();
161 try expectMatch(&harness, ".a.b", &.{0});
162 try expectMatch(&harness, ".a.b.c", &.{0});
163 try expectMatch(&harness, ".a.d", &.{});
164 try expectMatch(&harness, ".a", &.{ 0, 1 });
165 }
166
167 test "a pseudo class compound requires every bit it names" {
168 var harness: Harness = undefined;
169 harness.start();
170 _ = try harness.tree.add(&harness.table, 0, "box:hover:focus");
171 _ = try harness.tree.add(&harness.table, 1, "box:hover");
172 harness.tree.finish();
173 try expectMatch(&harness, ":hover", &.{ 0, 1 });
174 try expectMatch(&harness, ":hover:focus", &.{0});
175 try expectMatch(&harness, ":focus", &.{0});
176 try expectMatch(&harness, ":active", &.{});
177 }
178
179 test "the sibling word round trips its index and count" {
180 const word = engine.packSibling(3, 9);
181 try std.testing.expectEqual(@as(u32, 3), engine.siblingIndex(word));
182 try std.testing.expectEqual(@as(u32, 9), engine.siblingCount(word));
183 }