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 }