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 &lt;Site&gt;</title><main data-path=\"/docs/index.html\"><h1>Ready</h1></main><p>Docs &amp; 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 &lt;Theme&gt;") != 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 }