lib/zen/src/theme/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const subject = @import("root.zig");
3 const frontmatter = @import("../frontmatter/root.zig");
4 const io = std.Options.debug_io;
5 const Theme = subject.Theme;
6 const Page = subject.Page;
7 const default_render_limits = subject.default_render_limits;
8 const RenderPlan = subject.RenderPlan;
9 const RenderStorage = subject.RenderStorage;
10 const renderPage = subject.renderPage;
11 const load = subject.load;
12 const ownsPath = subject.ownsPath;
13
14 const TestRender = struct {
15 storage: RenderStorage,
16 html: []u8,
17
18 fn init(active: Theme, page: Page) !TestRender {
19 const plan = try RenderPlan.inspect(active, page, default_render_limits);
20 var storage = try RenderStorage.init(std.testing.allocator, plan.exactLimits());
21 errdefer storage.deinit(std.testing.allocator);
22 storage.activate();
23 const rendered = try renderPage(&storage, active, page);
24 return .{ .storage = storage, .html = rendered };
25 }
26
27 fn deinit(self: *TestRender) void {
28 self.storage.reset();
29 self.storage.deinit(std.testing.allocator);
30 self.* = undefined;
31 }
32 };
33
34 test "theme renders escaped fields and raw page content" {
35 comptime {
36 @stardustClaim(
37 @import("alloc_phase").capacity.witness(@import("./root.zig").RenderStorage, "zen_theme_render_consumer"),
38 null,
39 null,
40 null,
41 null,
42 null,
43 null,
44 );
45 }
46
47 const layout = "<title>{{title}}</title><main data-path=\"{{path}}\">{{content}}</main><p>{{meta.section}}</p>";
48 const pairs = [_]frontmatter.Pair{
49 .{ .key = "section", .value = "Docs & API" },
50 };
51 const metadata = frontmatter.Metadata{ .pairs = &pairs };
52 var page = try TestRender.init(.{ .layout = layout }, .{
53 .title = "Zen <Site>",
54 .content = "<h1>Ready</h1>",
55 .path = "/docs/index.html",
56 .metadata = metadata,
57 });
58 defer page.deinit();
59 try std.testing.expectEqualStrings("<title>Zen <Site></title><main data-path=\"/docs/index.html\"><h1>Ready</h1></main><p>Docs & API</p>", page.html);
60 }
61
62 test "theme rejects malformed and unknown placeholders" {
63 const page = Page{ .title = "Broken", .content = "<p>body</p>" };
64 try std.testing.expectError(
65 error.UnclosedPlaceholder,
66 RenderPlan.inspect(.{ .layout = "<main>{{content</main>" }, page, default_render_limits),
67 );
68 try std.testing.expectError(
69 error.UnknownPlaceholder,
70 RenderPlan.inspect(.{ .layout = "<main>{{page.title}}</main>" }, page, default_render_limits),
71 );
72 try std.testing.expectError(
73 error.UnknownInclude,
74 RenderPlan.inspect(.{ .layout = "<main>{{include.header}}</main>" }, page, default_render_limits),
75 );
76 }
77
78 test "theme loads layout style and includes from source theme directory" {
79 var tmp = std.testing.tmpDir(.{});
80 defer tmp.cleanup();
81 try tmp.dir.createDirPath(io, "site/.zen");
82 try tmp.dir.createDirPath(io, "site/.zen/includes/nav");
83 try tmp.dir.writeFile(io, .{ .sub_path = "site/.zen/layout.html", .data = "<html><body>{{content}} {{site.title}}</body></html>" });
84 try tmp.dir.writeFile(io, .{ .sub_path = "site/.zen/style.css", .data = "body{color:green}\n" });
85 try tmp.dir.writeFile(io, .{ .sub_path = "site/.zen/includes/header.html", .data = "<header>Header</header>" });
86 try tmp.dir.writeFile(io, .{ .sub_path = "site/.zen/includes/nav/item.html", .data = "<a href=\"/docs/\">Docs</a>" });
87 try tmp.dir.writeFile(io, .{ .sub_path = "site/.zen/includes/readme.txt", .data = "private" });
88 const source = try tmp.dir.realPathFileAlloc(io, "site", std.testing.allocator);
89 defer std.testing.allocator.free(source);
90
91 var loaded = try load(std.testing.allocator, source, .{}, .{
92 .site_title = "Docs",
93 .style_path = "assets/site.css",
94 .style = "",
95 });
96 defer loaded.deinit(std.testing.allocator);
97
98 try std.testing.expectEqualStrings("<html><body>{{content}} {{site.title}}</body></html>", loaded.value.layout);
99 try std.testing.expectEqualStrings("body{color:green}\n", loaded.value.style);
100 try std.testing.expectEqualStrings("Docs", loaded.value.site_title);
101 try std.testing.expectEqualStrings("assets/site.css", loaded.value.style_path);
102 try std.testing.expectEqual(@as(usize, 2), loaded.value.includes.len);
103 var page = try TestRender.init(.{
104 .layout = "{{include.header}} {{include.nav.item}} {{content}}",
105 .includes = loaded.value.includes,
106 }, .{
107 .title = "With Includes",
108 .content = "<p>Body</p>",
109 });
110 defer page.deinit();
111 try std.testing.expectEqualStrings("<header>Header</header> <a href=\"/docs/\">Docs</a> <p>Body</p>", page.html);
112 try std.testing.expect(ownsPath(".zen/layout.html", .{}));
113 try std.testing.expect(!ownsPath("docs/.zen/layout.html", .{}));
114 }
115
116 test "theme rejects duplicate include names" {
117 var tmp = std.testing.tmpDir(.{});
118 defer tmp.cleanup();
119 try tmp.dir.createDirPath(io, "site/.zen/includes/nav");
120 try tmp.dir.writeFile(io, .{ .sub_path = "site/.zen/includes/nav.item.html", .data = "<a>one</a>" });
121 try tmp.dir.writeFile(io, .{ .sub_path = "site/.zen/includes/nav/item.html", .data = "<a>two</a>" });
122 const source = try tmp.dir.realPathFileAlloc(io, "site", std.testing.allocator);
123 defer std.testing.allocator.free(source);
124
125 try std.testing.expectError(error.DuplicateInclude, load(std.testing.allocator, source, .{}, .{}));
126 }
127
128 const witness_theme = Theme{
129 .site_title = "Zen Bench",
130 .layout = "<title>{{title}}</title><main data-path=\"{{path}}\">{{content}}</main>",
131 };
132
133 const witness_page = Page{
134 .title = "Exact <Theme>",
135 .content = "<h1>Ready</h1>",
136 .path = "/docs/",
137 };
138
139 test "theme render storage rejects every limit before backing mutation" {
140 comptime {
141 @stardustClaim(
142 @import("alloc_phase").capacity.witness(@import("./root.zig").RenderStorage, "zen_theme_render_boundaries"),
143 null,
144 null,
145 null,
146 null,
147 null,
148 null,
149 );
150 }
151
152 const plan = try RenderPlan.inspect(witness_theme, witness_page, default_render_limits);
153 var storage = try RenderStorage.init(std.testing.allocator, plan.exactLimits());
154 defer storage.deinit(std.testing.allocator);
155 storage.activate();
156 @memset(storage.bytes, 42);
157
158 var larger = witness_page;
159 larger.content = witness_page.content ++ "!";
160 try std.testing.expectError(
161 error.ThemeOutputByteCapacityExceeded,
162 renderPage(&storage, witness_theme, larger),
163 );
164 for (storage.bytes) |byte| try std.testing.expectEqual(@as(u8, 42), byte);
165 try std.testing.expectError(
166 error.UnknownPlaceholder,
167 renderPage(&storage, .{ .layout = "{{unknown}}" }, witness_page),
168 );
169 for (storage.bytes) |byte| try std.testing.expectEqual(@as(u8, 42), byte);
170 try std.testing.expectEqual(@as(u64, 2), storage.status().rejected_page_count);
171 try std.testing.expect(!storage.status().in_use);
172 }
173
174 test "theme render storage reuses one region after success and rejection" {
175 comptime {
176 @stardustClaim(
177 @import("alloc_phase").capacity.witness(@import("./root.zig").RenderStorage, "zen_theme_render_reuse"),
178 null,
179 null,
180 null,
181 null,
182 null,
183 null,
184 );
185 }
186
187 const plan = try RenderPlan.inspect(witness_theme, witness_page, default_render_limits);
188 var storage = try RenderStorage.init(std.testing.allocator, plan.exactLimits());
189 defer storage.deinit(std.testing.allocator);
190 storage.activate();
191 const base = @intFromPtr(storage.bytes.ptr);
192
193 const first = try renderPage(&storage, witness_theme, witness_page);
194 try std.testing.expect(std.mem.indexOf(u8, first, "Exact <Theme>") != null);
195 try std.testing.expectError(
196 error.ThemeStorageInUse,
197 renderPage(&storage, witness_theme, witness_page),
198 );
199 storage.reset();
200 var larger = witness_page;
201 larger.content = witness_page.content ++ "!";
202 try std.testing.expectError(
203 error.ThemeOutputByteCapacityExceeded,
204 renderPage(&storage, witness_theme, larger),
205 );
206 const second = try renderPage(&storage, witness_theme, witness_page);
207 try std.testing.expectEqual(first.len, second.len);
208 storage.reset();
209 try std.testing.expectEqual(base, @intFromPtr(storage.bytes.ptr));
210 try std.testing.expectEqual(plan.output_bytes, storage.status().high_water_output_bytes);
211 try std.testing.expectEqual(@as(u64, 2), storage.status().rejected_page_count);
212 }
213
214 test "activated theme rendering performs no backing allocation" {
215 comptime {
216 @stardustClaim(
217 @import("alloc_phase").capacity.witness(@import("./root.zig").RenderStorage, "zen_theme_render_sealed"),
218 null,
219 null,
220 null,
221 null,
222 null,
223 null,
224 );
225 }
226
227 const plan = try RenderPlan.inspect(witness_theme, witness_page, default_render_limits);
228 var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
229 var storage = try RenderStorage.init(counting.allocator(), plan.exactLimits());
230 defer storage.deinit(counting.allocator());
231 storage.activate();
232 const allocations = counting.alloc_index;
233 const resizes = counting.resize_index;
234 const bytes = counting.allocated_bytes;
235 counting.fail_index = allocations;
236 counting.resize_fail_index = resizes;
237
238 const rendered = try renderPage(&storage, witness_theme, witness_page);
239 defer storage.reset();
240 try std.testing.expectEqual(plan.output_bytes, rendered.len);
241 try std.testing.expectEqual(@as(usize, 1), allocations);
242 try std.testing.expectEqual(@as(usize, 0), resizes);
243 try std.testing.expectEqual(plan.output_bytes, bytes);
244 try std.testing.expectEqual(allocations, counting.alloc_index);
245 try std.testing.expectEqual(resizes, counting.resize_index);
246 try std.testing.expectEqual(bytes, counting.allocated_bytes);
247 }
248
249 test "theme render storage supports empty exact output" {
250 const active = Theme{ .layout = "" };
251 const page = Page{ .title = "Unused", .content = "Unused" };
252 const plan = try RenderPlan.inspect(active, page, default_render_limits);
253 var storage = try RenderStorage.init(std.testing.allocator, plan.exactLimits());
254 defer storage.deinit(std.testing.allocator);
255 storage.activate();
256 const rendered = try renderPage(&storage, active, page);
257 defer storage.reset();
258 try std.testing.expectEqual(@as(usize, 0), rendered.len);
259 try std.testing.expectEqual(@as(usize, 0), storage.status().storage_bytes);
260 }