lib/zen/src/site/catalog/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3 const subject = @import("root.zig");
4
5 const Storage = subject.Storage;
6
7 const first = subject.EntryInput{
8 .relative_path = "writing/first.md",
9 .url = "/writing/first/",
10 .title = "First",
11 .date = "2026-08-07",
12 .type_label = "release",
13 };
14
15 const second = subject.EntryInput{
16 .relative_path = "guides/second.md",
17 .url = "/guides/second/",
18 .title = "Second",
19 .dispatch = .presentation,
20 .slop = true,
21 };
22
23 const one_text_byte = subject.EntryInput{
24 .relative_path = "",
25 .url = "",
26 .title = "x",
27 };
28
29 test "site catalog seals an empty static site" {
30 var storage = try Storage.init(std.testing.allocator, .{
31 .max_pages = 0,
32 .max_text_bytes = 0,
33 });
34 defer storage.deinit(std.testing.allocator);
35 storage.activate();
36 const catalog = storage.seal("/essay");
37 try std.testing.expectEqual(@as(usize, 0), catalog.pages.len);
38 try std.testing.expectEqualStrings("/essay", catalog.base);
39 try std.testing.expect(storage.status().sealed);
40 }
41
42 test "site catalog storage accepts exact limits and rejects max plus one" {
43 comptime {
44 @stardustClaim(
45 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "zen_site_catalog_boundaries"),
46 null,
47 null,
48 null,
49 null,
50 null,
51 null,
52 );
53 }
54
55 const first_bytes = try subject.Demand.fromInput(first).textBytes();
56 var page_storage = try Storage.init(std.testing.allocator, .{
57 .max_pages = 1,
58 .max_text_bytes = first_bytes,
59 });
60 defer page_storage.deinit(std.testing.allocator);
61 page_storage.activate();
62 _ = try page_storage.append(first);
63 try std.testing.expectError(error.SiteCatalogPageCapacityExceeded, page_storage.append(second));
64
65 var text_storage = try Storage.init(std.testing.allocator, .{
66 .max_pages = 2,
67 .max_text_bytes = first_bytes,
68 });
69 defer text_storage.deinit(std.testing.allocator);
70 text_storage.activate();
71 _ = try text_storage.append(first);
72 try std.testing.expectEqual(@as(usize, 1), try subject.Demand.fromInput(one_text_byte).textBytes());
73 try std.testing.expectError(
74 error.SiteCatalogTextCapacityExceeded,
75 text_storage.append(one_text_byte),
76 );
77 }
78
79 test "site catalog overload preserves retained entries and text" {
80 comptime {
81 @stardustClaim(
82 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "zen_site_catalog_atomic_rejection"),
83 null,
84 null,
85 null,
86 null,
87 null,
88 null,
89 );
90 }
91
92 const first_bytes = try subject.Demand.fromInput(first).textBytes();
93 var storage = try Storage.init(std.testing.allocator, .{
94 .max_pages = 2,
95 .max_text_bytes = first_bytes,
96 });
97 defer storage.deinit(std.testing.allocator);
98 storage.activate();
99 const entry = try storage.append(first);
100 const entry_address = @intFromPtr(entry);
101 const text_address = @intFromPtr(entry.relative_path.ptr);
102 try std.testing.expectError(error.SiteCatalogTextCapacityExceeded, storage.append(second));
103 try std.testing.expectEqual(@as(usize, 1), storage.status().pages);
104 try std.testing.expectEqual(first_bytes, storage.status().text_bytes);
105 try std.testing.expectEqual(entry_address, @intFromPtr(&storage.entries[0]));
106 try std.testing.expectEqual(text_address, @intFromPtr(storage.entries[0].relative_path.ptr));
107 try std.testing.expectEqualStrings("writing/first.md", storage.entries[0].relative_path);
108 try std.testing.expectEqualStrings("First", storage.entries[0].title);
109 }
110
111 test "site catalog append and seal remain allocation-free after acquisition" {
112 comptime {
113 @stardustClaim(
114 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "zen_site_catalog_sealed"),
115 null,
116 null,
117 null,
118 null,
119 null,
120 null,
121 );
122 }
123
124 const first_bytes = try subject.Demand.fromInput(first).textBytes();
125 const second_bytes = try subject.Demand.fromInput(second).textBytes();
126 var phase_allocator = try alloc_phase.SealedPhaseAllocator.init(std.testing.allocator);
127 var maybe_storage: ?Storage = null;
128 defer {
129 if (phase_allocator.phase() == .initialization) phase_allocator.abortInitialization();
130 if (phase_allocator.phase() == .steady) phase_allocator.beginTeardown();
131 if (maybe_storage) |*storage| {
132 if (storage.phase != .teardown) storage.deinit(phase_allocator.teardownAllocator());
133 }
134 phase_allocator.deinit();
135 }
136 maybe_storage = try Storage.init(phase_allocator.initializationAllocator(), .{
137 .max_pages = 2,
138 .max_text_bytes = first_bytes + second_bytes,
139 });
140 phase_allocator.seal();
141 const storage = &maybe_storage.?;
142 storage.activate();
143 _ = try storage.append(second);
144 _ = try storage.append(first);
145 const catalog = storage.seal("/tiny");
146 try std.testing.expectEqual(@as(usize, 2), catalog.pages.len);
147 try std.testing.expectEqualStrings("First", catalog.pages[0].title);
148 try std.testing.expectEqualStrings("writing", catalog.pages[0].section);
149 try std.testing.expectEqualStrings("release", catalog.pages[0].type_label);
150 try std.testing.expectEqualStrings("Second", catalog.pages[1].title);
151 try std.testing.expectEqualStrings("guides", catalog.pages[1].section);
152 try std.testing.expectEqual(subject.Dispatch.document, catalog.pages[0].dispatch);
153 try std.testing.expectEqual(subject.Dispatch.presentation, catalog.pages[1].dispatch);
154
155 const first_route = catalog.findRoute("/tiny/writing/first/") orelse
156 return error.MissingFirstRoute;
157 try std.testing.expectEqualStrings("writing/first.md", first_route.relative_path);
158 const second_source = catalog.findSource("guides/second.md") orelse
159 return error.MissingSecondSource;
160 try std.testing.expectEqualStrings("/guides/second/", second_source.url);
161 try std.testing.expectEqual(subject.Dispatch.presentation, second_source.dispatch);
162 try std.testing.expect(catalog.findRoute("/writing/first/") == null);
163 try std.testing.expect(catalog.findRoute("/tiny/writing/first") == null);
164 try std.testing.expect(catalog.findRoute("/tiny/writing/first/extra") == null);
165 try std.testing.expect(catalog.findSource("guides/second.markdown") == null);
166
167 var manifest_bytes: [512]u8 = undefined;
168 var manifest_writer = std.Io.Writer.fixed(&manifest_bytes);
169 try subject.writeManifest(catalog, &manifest_writer);
170 try std.testing.expectEqual(
171 try subject.manifestEncodedSize(catalog),
172 manifest_writer.buffered().len,
173 );
174 try std.testing.expectEqualStrings(
175 "{\"version\":1,\"routes\":[" ++
176 "{\"route\":\"/tiny/writing/first/\",\"source\":\"writing/first.md\",\"dispatch\":\"document\",\"title\":\"First\",\"draft\":false,\"slop\":false}," ++
177 "{\"route\":\"/tiny/guides/second/\",\"source\":\"guides/second.md\",\"dispatch\":\"presentation\",\"title\":\"Second\",\"draft\":false,\"slop\":true}]}\n",
178 manifest_writer.buffered(),
179 );
180 }
181
182 test "site catalog namespace" {
183 std.testing.refAllDecls(subject);
184 }