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 &amp; 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 }