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 }