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 }