lib/zen/src/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const subject = @import("root.zig");
3 const diagram_test = @import("diagram/test.zig");
4 const document_test = @import("document/test.zig");
5 const footnote_test = @import("footnote/test.zig");
6 const frontmatter_test = @import("frontmatter/test.zig");
7 const math_test = @import("math/test.zig");
8 const quiz_test = @import("quiz/test.zig");
9 const site_test = @import("site/test.zig");
10 const site_catalog_test = @import("site/catalog/test.zig");
11 const site_page_test = @import("site/page/test.zig");
12 const site_path_test = @import("site/path/test.zig");
13 const slides_test = @import("slides/test.zig");
14 const theme_test = @import("theme/test.zig");
15 const FrontmatterPlan = subject.FrontmatterPlan;
16 const FrontmatterStorage = subject.FrontmatterStorage;
17 const parseFrontmatter = subject.parseFrontmatter;
18 const default_footnote_limits = subject.default_footnote_limits;
19 const FootnotePlan = subject.FootnotePlan;
20 const FootnoteCapacity = subject.FootnoteCapacity;
21 const FootnoteStorage = subject.FootnoteStorage;
22 const parseFootnotes = subject.parseFootnotes;
23 const Quiz = subject.Quiz;
24 const default_quiz_limits = subject.default_quiz_limits;
25 const QuizPlan = subject.QuizPlan;
26 const QuizCapacity = subject.QuizCapacity;
27 const QuizStorage = subject.QuizStorage;
28 const parseQuiz = subject.parseQuiz;
29 const HeadingCapacity = subject.HeadingCapacity;
30 const HeadingStorage = subject.HeadingStorage;
31 const planMarkdownHeadings = subject.planMarkdownHeadings;
32 const inspectMarkdown = subject.inspectMarkdown;
33 const Theme = subject.Theme;
34 const Page = subject.Page;
35 const default_theme_render_limits = subject.default_theme_render_limits;
36 const ThemeRenderPlan = subject.ThemeRenderPlan;
37 const ThemeRenderCapacity = subject.ThemeRenderCapacity;
38 const ThemeRenderStorage = subject.ThemeRenderStorage;
39 const renderPage = subject.renderPage;
40 const default_site_catalog_limits = subject.default_site_catalog_limits;
41 const SiteCatalogPlan = subject.SiteCatalogPlan;
42 const SiteCatalogCapacity = subject.SiteCatalogCapacity;
43 const SiteCatalogStorage = subject.SiteCatalogStorage;
44 const default_site_page_limits = subject.default_site_page_limits;
45 const SitePagePlan = subject.SitePagePlan;
46 const SitePageCapacity = subject.SitePageCapacity;
47 const SitePageStorage = subject.SitePageStorage;
48 const default_site_path_limits = subject.default_site_path_limits;
49 const SitePathPlan = subject.SitePathPlan;
50 const SitePathCapacity = subject.SitePathCapacity;
51 const SitePathStorage = subject.SitePathStorage;
52 const io = std.Options.debug_io;
53
54 test "zen package namespace" {
55 std.testing.refAllDecls(subject);
56 std.testing.refAllDecls(diagram_test);
57 std.testing.refAllDecls(document_test);
58 std.testing.refAllDecls(footnote_test);
59 std.testing.refAllDecls(frontmatter_test);
60 std.testing.refAllDecls(math_test);
61 std.testing.refAllDecls(quiz_test);
62 std.testing.refAllDecls(site_test);
63 std.testing.refAllDecls(site_catalog_test);
64 std.testing.refAllDecls(site_page_test);
65 std.testing.refAllDecls(site_path_test);
66 std.testing.refAllDecls(slides_test);
67 std.testing.refAllDecls(theme_test);
68 }
69
70 test "Zen root exposes bounded site catalog controls" {
71 comptime {
72 @stardustClaim(
73 @import("alloc_phase").capacity.witness(@import("./site/catalog/root.zig").Storage, "zen_site_catalog_root"),
74 null,
75 null,
76 null,
77 null,
78 null,
79 null,
80 );
81 }
82
83 const input = subject.SiteCatalogEntryInput{
84 .relative_path = "guides/start.md",
85 .url = "/guides/start/",
86 .title = "Start",
87 };
88 const plan = try SiteCatalogPlan.inspect(
89 subject.SiteCatalogDemand.fromInput(input),
90 default_site_catalog_limits,
91 );
92 var storage = try SiteCatalogStorage.init(std.testing.allocator, plan.exactLimits());
93 defer storage.deinit(std.testing.allocator);
94 storage.activate();
95 _ = try storage.append(input);
96 const catalog = storage.seal("");
97 try std.testing.expectEqualStrings("Start", catalog.pages[0].title);
98 try std.testing.expectEqual(
99 (try SiteCatalogCapacity.derive(plan.exactLimits())).storage_bytes,
100 storage.status().storage_bytes,
101 );
102 }
103
104 test "Zen root exposes bounded frontmatter controls" {
105 comptime {
106 @stardustClaim(
107 @import("alloc_phase").capacity.witness(@import("./frontmatter/root.zig").Storage, "zen_frontmatter_root"),
108 null,
109 null,
110 null,
111 null,
112 null,
113 null,
114 );
115 }
116
117 const source = "---\ntitle: Zen\n---\n# Body\n";
118 const plan = try FrontmatterPlan.inspect(source, .{
119 .max_pairs = 1,
120 .max_continuation_bytes = 0,
121 });
122 var storage = try FrontmatterStorage.init(std.testing.allocator, plan.exactLimits());
123 defer storage.deinit(std.testing.allocator);
124 storage.activate();
125 const parsed = try parseFrontmatter(&storage, source);
126 defer storage.reset();
127 try std.testing.expectEqualStrings("Zen", parsed.metadata.title().?);
128 }
129
130 test "Zen root exposes bounded quiz controls" {
131 comptime {
132 @stardustClaim(
133 @import("alloc_phase").capacity.witness(@import("./quiz/root.zig").Storage, "zen_quiz_root"),
134 null,
135 null,
136 null,
137 null,
138 null,
139 null,
140 );
141 }
142
143 const source = "? What is bounded?\n! Quiz storage\n";
144 const plan = try QuizPlan.inspect(source, default_quiz_limits);
145 var storage = try QuizStorage.init(std.testing.allocator, plan.exactLimits());
146 defer storage.deinit(std.testing.allocator);
147 storage.activate();
148 const parsed = try parseQuiz(&storage, source);
149 defer storage.reset();
150 try std.testing.expectEqualStrings("What is bounded?", parsed.questions[0].prompt);
151 try std.testing.expectEqual(
152 (try QuizCapacity.derive(plan.exactLimits())).storage_bytes,
153 storage.status().storage_bytes,
154 );
155 }
156
157 test "Zen root exposes bounded heading controls" {
158 comptime {
159 @stardustClaim(
160 @import("alloc_phase").capacity.witness(@import("./document/root.zig").Storage, "zen_heading_root"),
161 null,
162 null,
163 null,
164 null,
165 null,
166 null,
167 );
168 }
169
170 const source = "# Title\n\n## Section\n";
171 const plan = try planMarkdownHeadings(source);
172 var storage = try HeadingStorage.init(std.testing.allocator, plan.exactLimits());
173 defer storage.deinit(std.testing.allocator);
174 storage.activate();
175 const document_value = try inspectMarkdown(&storage, source);
176 defer storage.reset();
177 try std.testing.expectEqualStrings("Title", document_value.firstHeading().?.text);
178 try std.testing.expectEqual(
179 (try HeadingCapacity.derive(plan.exactLimits())).storage_bytes,
180 storage.status().storage_bytes,
181 );
182 }
183
184 test "Zen root exposes bounded footnote controls" {
185 comptime {
186 @stardustClaim(
187 @import("alloc_phase").capacity.witness(@import("./footnote/root.zig").Storage, "zen_footnote_root"),
188 null,
189 null,
190 null,
191 null,
192 null,
193 null,
194 );
195 }
196
197 const source = "[^one]: First\n continued\n";
198 const plan = try FootnotePlan.inspect(source, default_footnote_limits);
199 var storage = try FootnoteStorage.init(std.testing.allocator, plan.exactLimits());
200 defer storage.deinit(std.testing.allocator);
201 storage.activate();
202 const parsed = try parseFootnotes(&storage, source);
203 defer storage.reset();
204 try std.testing.expectEqualStrings("First continued", parsed.definitions[0].text);
205 try std.testing.expectEqual(
206 (try FootnoteCapacity.derive(plan.exactLimits())).storage_bytes,
207 storage.status().storage_bytes,
208 );
209 }
210
211 test "Zen root exposes bounded theme rendering controls" {
212 comptime {
213 @stardustClaim(
214 @import("alloc_phase").capacity.witness(@import("./theme/root.zig").RenderStorage, "zen_theme_render_root"),
215 null,
216 null,
217 null,
218 null,
219 null,
220 null,
221 );
222 }
223
224 const active = Theme{ .layout = "<main>{{title}} {{content}}</main>" };
225 const page = Page{ .title = "Zen & Tiny", .content = "<p>Ready</p>" };
226 const plan = try ThemeRenderPlan.inspect(active, page, default_theme_render_limits);
227 var storage = try ThemeRenderStorage.init(std.testing.allocator, plan.exactLimits());
228 defer storage.deinit(std.testing.allocator);
229 storage.activate();
230 const rendered = try renderPage(&storage, active, page);
231 defer storage.reset();
232 try std.testing.expectEqualStrings(
233 "<main>Zen & Tiny <p>Ready</p></main>",
234 rendered,
235 );
236 try std.testing.expectEqual(
237 ThemeRenderCapacity.derive(plan.exactLimits()).storage_bytes,
238 storage.status().storage_bytes,
239 );
240 }
241
242 test "Zen root exposes bounded site path controls" {
243 comptime {
244 @stardustClaim(
245 @import("alloc_phase").capacity.witness(@import("./site/path/root.zig").Storage, "zen_site_path_root"),
246 null,
247 null,
248 null,
249 null,
250 null,
251 null,
252 );
253 }
254
255 const plan = try SitePathPlan.inspect(
256 "guides/start.md",
257 true,
258 .url,
259 default_site_path_limits,
260 );
261 var storage = try SitePathStorage.init(std.testing.allocator, plan.exactLimits());
262 defer storage.deinit(std.testing.allocator);
263 storage.activate();
264 const paths = try storage.acquire("guides/start.md", true, .url);
265 defer storage.reset();
266 try std.testing.expectEqualStrings("guides/start/index.html", paths.target);
267 try std.testing.expectEqualStrings("/guides/start/", paths.public);
268 try std.testing.expectEqual(
269 (try SitePathCapacity.derive(plan.exactLimits())).storage_bytes,
270 storage.status().storage_bytes,
271 );
272 }
273
274 test "Zen root exposes bounded site page controls" {
275 comptime {
276 @stardustClaim(
277 @import("alloc_phase").capacity.witness(@import("./site/page/root.zig").Storage, "zen_site_page_root"),
278 null,
279 null,
280 null,
281 null,
282 null,
283 null,
284 );
285 }
286
287 const plan = try SitePagePlan.inspect(17, default_site_page_limits);
288 var storage = try SitePageStorage.init(std.testing.allocator, plan.exactLimits());
289 defer storage.deinit(std.testing.allocator);
290 storage.activate();
291 try std.testing.expectEqual(
292 (try SitePageCapacity.derive(plan.exactLimits())).storage_bytes,
293 storage.status().storage_bytes,
294 );
295 }
296
297 test "example repairs a Markdown diagnostic and releases the render" {
298 var diagnostic: subject.Diagnostic = .{};
299 const broken = "# Page\n\n## First {#section}\n\n## Second {#section}\n";
300 try std.testing.expectError(
301 error.DuplicateHeadingAnchor,
302 subject.renderMarkdown(
303 std.testing.allocator,
304 broken,
305 .{ .diagnostic = &diagnostic },
306 ),
307 );
308 try std.testing.expectEqualStrings("duplicate heading anchor", diagnostic.reason);
309
310 diagnostic.reset();
311 const repaired = "# Page\n\n## First {#first}\n\n## Second {#second}\n";
312 var rendered = try subject.renderMarkdown(
313 std.testing.allocator,
314 repaired,
315 .{ .diagnostic = &diagnostic },
316 );
317 defer rendered.deinit(std.testing.allocator);
318 try std.testing.expectEqualStrings("Page", rendered.document.firstHeading().?.text);
319 try std.testing.expect(std.mem.indexOf(u8, rendered.html, "id=\"second\"") != null);
320 }
321
322 test "example plans one heading region and observes rejection" {
323 const source = "# Page\n\n## Section\n";
324 const plan = try subject.planMarkdownHeadings(source);
325 var storage = try subject.HeadingStorage.init(
326 std.testing.allocator,
327 plan.exactLimits(),
328 );
329 defer storage.deinit(std.testing.allocator);
330 storage.activate();
331
332 {
333 const document = try subject.inspectMarkdown(&storage, source);
334 defer storage.reset();
335 try std.testing.expectEqualStrings("section", document.headings[1].id);
336 try std.testing.expect(storage.status().in_use);
337 }
338
339 try std.testing.expectError(
340 error.HeadingCapacityExceeded,
341 subject.inspectMarkdown(&storage, source ++ "\n### Extra\n"),
342 );
343 try std.testing.expect(!storage.status().in_use);
344 try std.testing.expectEqual(@as(u64, 1), storage.status().rejected_source_count);
345 }
346
347 test "example preserves a site until repaired content can publish" {
348 var tmp = std.testing.tmpDir(.{});
349 defer tmp.cleanup();
350 try tmp.dir.createDirPath(io, "site");
351 try tmp.dir.createDirPath(io, "out");
352 try tmp.dir.writeFile(io, .{ .sub_path = "out/index.html", .data = "previous" });
353 try tmp.dir.writeFile(io, .{
354 .sub_path = "site/index.md",
355 .data = "# First {#same}\n\n## Second {#same}\n",
356 });
357 const source = try tmp.dir.realPathFileAlloc(io, "site", std.testing.allocator);
358 defer std.testing.allocator.free(source);
359 const output = try tmp.dir.realPathFileAlloc(io, "out", std.testing.allocator);
360 defer std.testing.allocator.free(output);
361
362 var diagnostic: subject.Diagnostic = .{};
363 try std.testing.expectError(error.DuplicateHeadingAnchor, subject.buildSite(
364 std.testing.allocator,
365 .{
366 .source = source,
367 .output = output,
368 .base = "/docs",
369 .diagnostic = &diagnostic,
370 },
371 ));
372 try expectFile(output, "index.html", "previous");
373 try std.testing.expectEqualStrings("index.md", diagnostic.page());
374
375 try tmp.dir.writeFile(io, .{
376 .sub_path = "site/index.md",
377 .data = "# Ready\n\n[API](/api/)\n",
378 });
379 const result = try subject.buildSite(std.testing.allocator, .{
380 .source = source,
381 .output = output,
382 .base = "/docs",
383 .diagnostic = &diagnostic,
384 });
385 try std.testing.expectEqual(@as(usize, 1), result.pages);
386 try expectFileContains(output, "index.html", "href=\"/docs/api/\"");
387 }
388
389 test "example renders an ASCII figure and observes its bounds" {
390 const plot = subject.diagram.figure(.{
391 .frame = .{ .title = "Coin", .y_min = 0, .y_max = 1 },
392 .data = &.{subject.diagram.row("coin", &.{
393 subject.diagram.text("outcome", "heads"),
394 subject.diagram.number("p", 0.62),
395 })},
396 .layers = &.{subject.diagram.bar(.{
397 .data = "coin",
398 .x = "outcome",
399 .y = "p",
400 })},
401 });
402 var rendered = try plot.renderAscii(
403 std.testing.allocator,
404 .{ .width = 40, .height = 12 },
405 );
406 defer rendered.deinit(std.testing.allocator);
407 try std.testing.expect(std.mem.indexOf(u8, rendered.output, "Coin") != null);
408 try std.testing.expectEqual(@as(usize, 40 * 12), rendered.status().max_canvas_bytes);
409
410 var document = try plot.document(std.testing.allocator);
411 defer document.deinit();
412 const plan = try subject.AsciiRenderPlan.inspect(
413 &document,
414 .{ .width = 40, .height = 12 },
415 );
416 var storage = try subject.AsciiRenderStorage.init(
417 std.testing.allocator,
418 plan.exactLimits(),
419 );
420 defer storage.deinit(std.testing.allocator);
421 storage.activate();
422 _ = try subject.renderAsciiDocument(&storage, &document, .{ .width = 40, .height = 12 });
423 storage.reset();
424 try std.testing.expectError(
425 error.AsciiCanvasByteCapacityExceeded,
426 subject.renderAsciiDocument(&storage, &document, .{ .width = 41, .height = 12 }),
427 );
428 try std.testing.expectEqual(@as(u64, 1), storage.status().rejected_render_count);
429 }
430
431 fn expectFile(root: []const u8, relative: []const u8, expected: []const u8) !void {
432 const path = try std.fs.path.join(std.testing.allocator, &.{ root, relative });
433 defer std.testing.allocator.free(path);
434 const actual = try std.Io.Dir.cwd().readFileAlloc(
435 io,
436 path,
437 std.testing.allocator,
438 .limited(1024),
439 );
440 defer std.testing.allocator.free(actual);
441 try std.testing.expectEqualStrings(expected, actual);
442 }
443
444 fn expectFileContains(root: []const u8, relative: []const u8, needle: []const u8) !void {
445 const path = try std.fs.path.join(std.testing.allocator, &.{ root, relative });
446 defer std.testing.allocator.free(path);
447 const actual = try std.Io.Dir.cwd().readFileAlloc(
448 io,
449 path,
450 std.testing.allocator,
451 .limited(1024 * 1024),
452 );
453 defer std.testing.allocator.free(actual);
454 try std.testing.expect(std.mem.indexOf(u8, actual, needle) != null);
455 }