lib/zen/src/diagram/ascii/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const subject = @import("root.zig");
3 const diagram = @import("../root.zig");
4 const limits = diagram.limits;
5 const spec = diagram.spec;
6 const RenderPlan = subject.RenderPlan;
7 const RenderCapacity = subject.RenderCapacity;
8 const RenderStorage = subject.RenderStorage;
9 const renderJsonl = subject.renderJsonl;
10 const render = subject.render;
11
12 test "ASCII JSONL rendering owns exact diagram Document storage" {
13 comptime {
14 @stardustClaim(
15 @import("alloc_phase").capacity.witness(@import("../document/root.zig").Storage, "zen_diagram_document_consumer"),
16 null,
17 null,
18 null,
19 null,
20 null,
21 null,
22 );
23 }
24
25 const input =
26 \\{"kind":"frame","title":"Coin","x_label":"outcome","y_label":"p","y_min":0,"y_max":1}
27 \\{"kind":"data","name":"coin","outcome":"heads","probability":0.62,"label":"heads"}
28 \\{"kind":"data","name":"coin","outcome":"tails","probability":0.38,"label":"tails"}
29 \\{"kind":"mark","type":"bar","data":"coin","x":"outcome","y":"probability","label":"label"}
30 \\
31 ;
32 var rendered = try renderJsonl(std.testing.allocator, input, .{ .width = 40, .height = 12 });
33 defer rendered.deinit(std.testing.allocator);
34 const output = rendered.output;
35 const expected =
36 \\ Coin
37 \\p
38 \\1 |
39 \\ | heads
40 \\ | ########
41 \\ | ######## tails
42 \\ | ######## ########
43 \\ | ######## ########
44 \\0 +---########-------########----
45 \\ heads tails
46 \\
47 \\ outcome
48 \\
49 ;
50 try std.testing.expectEqualStrings(expected, output);
51 }
52
53 test "ASCII renderer renders boxes and directed edges" {
54 const input =
55 \\{"kind":"frame","title":"Flow","axes":false,"x_min":0,"x_max":10,"y_min":0,"y_max":10}
56 \\{"kind":"edge","x1":2,"y1":5,"x2":6.6,"y2":5,"label":"step"}
57 \\{"kind":"box","x":2,"y":5,"width":2,"height":2,"text":"A"}
58 \\{"kind":"box","x":8,"y":5,"width":2,"height":2,"text":"B"}
59 \\
60 ;
61 var rendered = try renderJsonl(std.testing.allocator, input, .{ .width = 44, .height = 12 });
62 defer rendered.deinit(std.testing.allocator);
63 const output = rendered.output;
64 const expected =
65 \\ Flow
66 \\
67 \\
68 \\
69 \\
70 \\ +-------+ step +-------+
71 \\ | A---|--------------> | B |
72 \\ +-------+ +-------+
73 \\
74 \\
75 \\
76 \\
77 \\
78 ;
79 try std.testing.expectEqualStrings(expected, output);
80 }
81
82 test "ASCII renderer renders point rule and text marks exactly" {
83 const input =
84 \\{"kind":"frame","title":"Marks","x_min":0,"x_max":10,"y_min":0,"y_max":10}
85 \\{"kind":"point","x":2,"y":8,"label":"P"}
86 \\{"kind":"rule","x1":2,"y1":2,"x2":8,"y2":2}
87 \\{"kind":"text","x":7,"y":7,"text":"T"}
88 \\
89 ;
90 var rendered = try renderJsonl(
91 std.testing.allocator,
92 input,
93 .{ .width = 40, .height = 12 },
94 );
95 defer rendered.deinit(std.testing.allocator);
96 const expected =
97 \\ Marks
98 \\
99 \\10 | P
100 \\ | *
101 \\ | T
102 \\ |
103 \\ |
104 \\ | -------------------
105 \\0 +------------------------------
106 \\ 0 10
107 \\
108 \\
109 \\
110 ;
111 try std.testing.expectEqualStrings(expected, rendered.output);
112 }
113
114 const witness_jsonl =
115 \\{"kind":"frame","title":"Traffic","x_label":"phase","y_label":"bytes","y_min":0,"y_max":100}
116 \\{"kind":"data","name":"traffic","phase":"parse","bytes":42,"label":"parse"}
117 \\{"kind":"data","name":"traffic","phase":"render","bytes":73,"label":"render"}
118 \\{"kind":"data","name":"traffic","phase":"write","bytes":31,"label":"write"}
119 \\{"kind":"mark","type":"bar","data":"traffic","x":"phase","y":"bytes","label":"label"}
120 ;
121
122 fn parseTestDocument(jsonl: []const u8) !spec.Document {
123 const measured = limits.measure(jsonl);
124 const scratch_bytes = try limits.lineScratchBytes(measured.max_line_bytes);
125 const scratch = try std.testing.allocator.alloc(
126 u8,
127 std.math.cast(usize, scratch_bytes) orelse return error.OutOfMemory,
128 );
129 defer std.testing.allocator.free(scratch);
130 return try spec.Document.parse(std.testing.allocator, scratch, jsonl);
131 }
132
133 test "ASCII render storage rejects every limit before backing mutation" {
134 comptime {
135 @stardustClaim(
136 @import("alloc_phase").capacity.witness(@import("./root.zig").RenderStorage, "zen_ascii_render_boundaries"),
137 null,
138 null,
139 null,
140 null,
141 null,
142 null,
143 );
144 }
145
146 var document = try parseTestDocument(witness_jsonl);
147 defer document.deinit();
148 const plan = try RenderPlan.inspect(&document, .{});
149 var storage = try RenderStorage.init(std.testing.allocator, plan.exactLimits());
150 defer storage.deinit(std.testing.allocator);
151 storage.activate();
152 @memset(storage.bytes, 42);
153
154 var excessive = plan;
155 excessive.categories += 1;
156 try std.testing.expectError(error.AsciiCategoryCapacityExceeded, storage.acquire(excessive));
157 excessive = plan;
158 excessive.stack_totals += 1;
159 try std.testing.expectError(error.AsciiStackTotalCapacityExceeded, storage.acquire(excessive));
160 excessive = plan;
161 excessive.canvas_bytes += 1;
162 try std.testing.expectError(error.AsciiCanvasByteCapacityExceeded, storage.acquire(excessive));
163 excessive = plan;
164 excessive.output_bytes += 1;
165 try std.testing.expectError(error.AsciiOutputByteCapacityExceeded, storage.acquire(excessive));
166 for (storage.bytes) |byte| try std.testing.expectEqual(@as(u8, 42), byte);
167 try std.testing.expectEqual(@as(u64, 4), storage.status().rejected_render_count);
168 try std.testing.expect(!storage.status().in_use);
169 }
170
171 test "ASCII render storage reuses one region after success and rejection" {
172 comptime {
173 @stardustClaim(
174 @import("alloc_phase").capacity.witness(@import("./root.zig").RenderStorage, "zen_ascii_render_reuse"),
175 null,
176 null,
177 null,
178 null,
179 null,
180 null,
181 );
182 }
183
184 var document = try parseTestDocument(witness_jsonl);
185 defer document.deinit();
186 const plan = try RenderPlan.inspect(&document, .{});
187 var storage = try RenderStorage.init(std.testing.allocator, plan.exactLimits());
188 defer storage.deinit(std.testing.allocator);
189 storage.activate();
190 const base = @intFromPtr(storage.bytes.ptr);
191
192 const first = try render(&storage, &document, .{});
193 const first_len = first.len;
194 try std.testing.expect(std.mem.indexOf(u8, first, "Traffic") != null);
195 try std.testing.expectError(error.AsciiRenderStorageInUse, render(&storage, &document, .{}));
196 storage.reset();
197 try std.testing.expectError(
198 error.AsciiCanvasByteCapacityExceeded,
199 render(&storage, &document, .{ .width = 73, .height = 22 }),
200 );
201 const second = try render(&storage, &document, .{});
202 try std.testing.expectEqual(first_len, second.len);
203 storage.reset();
204 try std.testing.expectEqual(base, @intFromPtr(storage.bytes.ptr));
205 try std.testing.expectEqual(plan.categories, storage.status().high_water_categories);
206 try std.testing.expectEqual(plan.stack_totals, storage.status().high_water_stack_totals);
207 try std.testing.expectEqual(plan.canvas_bytes, storage.status().high_water_canvas_bytes);
208 try std.testing.expectEqual(plan.output_bytes, storage.status().high_water_output_bytes);
209 try std.testing.expectEqual(@as(u64, 2), storage.status().rejected_render_count);
210 }
211
212 test "activated ASCII rendering performs no backing allocation" {
213 comptime {
214 @stardustClaim(
215 @import("alloc_phase").capacity.witness(@import("./root.zig").RenderStorage, "zen_ascii_render_sealed"),
216 null,
217 null,
218 null,
219 null,
220 null,
221 null,
222 );
223 }
224
225 var document = try parseTestDocument(witness_jsonl);
226 defer document.deinit();
227 const plan = try RenderPlan.inspect(&document, .{});
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 output = try render(&storage, &document, .{});
239 defer storage.reset();
240 try std.testing.expect(std.mem.indexOf(u8, output, "render") != null);
241 try std.testing.expectEqual(@as(usize, 1), allocations);
242 try std.testing.expectEqual(@as(usize, 0), resizes);
243 try std.testing.expectEqual(
244 (try RenderCapacity.derive(plan.exactLimits())).storage_bytes,
245 bytes,
246 );
247 try std.testing.expectEqual(allocations, counting.alloc_index);
248 try std.testing.expectEqual(resizes, counting.resize_index);
249 try std.testing.expectEqual(bytes, counting.allocated_bytes);
250 }