lib/pretty/core/src/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const namespace = @import("root.zig");
2 const pretty = namespace;
3 const std = @import("std");
4 const types = @import("types.zig");
5 const builder_product = @import("builder.zig");
6 const render = @import("render.zig");
7
8 const json = namespace.json;
9 const compact = namespace.compact;
10 const diagnostic = namespace.diagnostic;
11 const default_width = namespace.default_width;
12 const LayoutOptions = namespace.LayoutOptions;
13 const ColorMode = namespace.ColorMode;
14 const Style = namespace.Style;
15 const Doc = namespace.Doc;
16 const Nested = namespace.Nested;
17 const Styled = namespace.Styled;
18 const WriteState = namespace.WriteState;
19 const softline = namespace.softline;
20 const softbreak = namespace.softbreak;
21 const hardline = namespace.hardline;
22 const Builder = namespace.Builder;
23 const FlatFitWriter = namespace.FlatFitWriter;
24 const renderAlloc = namespace.renderAlloc;
25 const write = namespace.write;
26 const writeWithState = namespace.writeWithState;
27 test {
28 _ = @import("compact/test.zig");
29 _ = @import("json/test.zig");
30 _ = @import("stream.zig");
31 }
32
33 test "pretty package namespace" {
34 std.testing.refAllDecls(types);
35 std.testing.refAllDecls(builder_product);
36 std.testing.refAllDecls(render);
37 std.testing.refAllDecls(json);
38 std.testing.refAllDecls(compact);
39 std.testing.refAllDecls(diagnostic);
40 }
41 test "group keeps soft breaks flat when it fits" {
42 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
43 defer arena_state.deinit();
44 const arena = arena_state.allocator();
45 const builder = Builder.init(arena);
46
47 const body = try builder.concat(&.{
48 builder.text("["),
49 try builder.nest(2, try builder.concat(&.{
50 softbreak,
51 builder.text("alpha"),
52 builder.text(","),
53 softline,
54 builder.text("beta"),
55 })),
56 softbreak,
57 builder.text("]"),
58 });
59 const doc = try builder.group(body);
60
61 const rendered = try renderAlloc(std.testing.allocator, doc, .{ .width = 20 });
62 defer std.testing.allocator.free(rendered);
63 try std.testing.expectEqualStrings("[alpha, beta]", rendered);
64 }
65
66 test "group breaks soft breaks when it does not fit" {
67 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
68 defer arena_state.deinit();
69 const arena = arena_state.allocator();
70 const builder = Builder.init(arena);
71
72 const body = try builder.concat(&.{
73 builder.text("["),
74 try builder.nest(2, try builder.concat(&.{
75 softbreak,
76 builder.text("alpha"),
77 builder.text(","),
78 softline,
79 builder.text("beta"),
80 })),
81 softbreak,
82 builder.text("]"),
83 });
84 const doc = try builder.group(body);
85
86 const rendered = try renderAlloc(std.testing.allocator, doc, .{ .width = 8 });
87 defer std.testing.allocator.free(rendered);
88 try std.testing.expectEqualStrings(
89 \\[
90 \\ alpha,
91 \\ beta
92 \\]
93 , rendered);
94 }
95
96 test "group fit includes the same-line continuation" {
97 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
98 defer arena_state.deinit();
99 const builder = Builder.init(arena_state.allocator());
100
101 const grouped = try builder.group(try builder.concat(&.{
102 builder.text("abc"),
103 softline,
104 builder.text("de"),
105 }));
106 const doc = try builder.concat(&.{ grouped, builder.text("XY") });
107 const rendered = try renderAlloc(std.testing.allocator, doc, .{ .width = 6 });
108 defer std.testing.allocator.free(rendered);
109
110 try std.testing.expectEqualStrings("abc\ndeXY", rendered);
111 }
112
113 test "unavoidable width overflow remains renderable" {
114 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
115 defer arena_state.deinit();
116 const builder = Builder.init(arena_state.allocator());
117
118 const atom = try builder.concat(&.{
119 try builder.group(try builder.concat(&.{
120 builder.text("a"),
121 softline,
122 builder.text("b"),
123 })),
124 builder.text("1234567"),
125 });
126 const atom_rendered = try renderAlloc(std.testing.allocator, atom, .{ .width = 6 });
127 defer std.testing.allocator.free(atom_rendered);
128 try std.testing.expectEqualStrings("a\nb1234567", atom_rendered);
129
130 const contextual = try builder.group(try builder.concat(&.{
131 builder.text("a"),
132 softline,
133 builder.text("b"),
134 }));
135 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
136 defer out.deinit();
137 try writeWithState(&out.writer, contextual, .{ .width = 6 }, .{ .column = 8 });
138 try std.testing.expectEqualStrings("a\nb", out.written());
139 }
140
141 test "hardline stays hard inside a flat group" {
142 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
143 defer arena_state.deinit();
144 const arena = arena_state.allocator();
145 const builder = Builder.init(arena);
146
147 const doc = try builder.group(try builder.concat(&.{
148 builder.text("first"),
149 softline,
150 builder.text("middle"),
151 hardline,
152 builder.text("second"),
153 }));
154
155 const rendered = try renderAlloc(std.testing.allocator, doc, .{ .width = 80 });
156 defer std.testing.allocator.free(rendered);
157 try std.testing.expectEqualStrings(
158 \\first
159 \\middle
160 \\second
161 , rendered);
162 }
163
164 test "sibling hardline ends group continuation fit" {
165 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
166 defer arena_state.deinit();
167 const builder = Builder.init(arena_state.allocator());
168
169 const grouped = try builder.group(try builder.concat(&.{
170 builder.text("abc"),
171 softline,
172 builder.text("de"),
173 }));
174 const doc = try builder.concat(&.{ grouped, hardline, builder.text("XY") });
175 const rendered = try renderAlloc(std.testing.allocator, doc, .{ .width = 6 });
176 defer std.testing.allocator.free(rendered);
177
178 try std.testing.expectEqualStrings("abc de\nXY", rendered);
179 }
180
181 test "continuation text fits its prefix before a line reset" {
182 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
183 defer arena_state.deinit();
184 const builder = Builder.init(arena_state.allocator());
185
186 const grouped = try builder.group(try builder.concat(&.{
187 builder.text("abc"),
188 softline,
189 }));
190 const doc = try builder.concat(&.{ grouped, builder.text("XY12\n") });
191 const rendered = try renderAlloc(std.testing.allocator, doc, .{ .width = 6 });
192 defer std.testing.allocator.free(rendered);
193
194 try std.testing.expectEqualStrings("abc\nXY12\n", rendered);
195 }
196
197 test "styled docs emit ANSI without changing fit width" {
198 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
199 defer arena_state.deinit();
200 const arena = arena_state.allocator();
201 const builder = Builder.init(arena);
202
203 const doc = try builder.group(try builder.concat(&.{
204 builder.text("a"),
205 softline,
206 try builder.style(.keyword, builder.text("bc")),
207 }));
208
209 const plain = try renderAlloc(std.testing.allocator, doc, .{ .width = 4 });
210 defer std.testing.allocator.free(plain);
211 try std.testing.expectEqualStrings("a bc", plain);
212
213 const colored = try renderAlloc(std.testing.allocator, doc, .{ .width = 4, .color = .ansi });
214 defer std.testing.allocator.free(colored);
215 try std.testing.expect(std.mem.indexOf(u8, colored, "\x1b[1;94m") != null);
216 try std.testing.expect(std.mem.indexOf(u8, colored, "a \x1b[1;94mbc\x1b[0m") != null);
217 }
218
219 test "nested styled docs restore the outer ANSI style" {
220 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
221 defer arena_state.deinit();
222 const arena = arena_state.allocator();
223 const builder = Builder.init(arena);
224
225 const doc = try builder.style(.keyword, try builder.concat(&.{
226 builder.text("a "),
227 try builder.styledText(.number, "42"),
228 builder.text(" c"),
229 }));
230
231 const plain = try renderAlloc(std.testing.allocator, doc, .{ .width = 80 });
232 defer std.testing.allocator.free(plain);
233 try std.testing.expectEqualStrings("a 42 c", plain);
234
235 const colored = try renderAlloc(std.testing.allocator, doc, .{ .width = 80, .color = .ansi });
236 defer std.testing.allocator.free(colored);
237 try std.testing.expect(std.mem.indexOf(
238 u8,
239 colored,
240 "\x1b[1;94ma \x1b[35m42\x1b[0m\x1b[1;94m c\x1b[0m",
241 ) != null);
242 }
243
244 test "builder helpers standardize styled punctuation and joins" {
245 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
246 defer arena_state.deinit();
247 const arena = arena_state.allocator();
248 const builder = Builder.init(arena);
249
250 const items = [_]Doc{
251 try builder.styledText(.name, "alpha"),
252 try builder.styledFmt(.number, "{d}", .{42}),
253 };
254 const doc = try builder.group(try builder.concat(&.{
255 try builder.spaces(2),
256 try builder.punct("["),
257 try builder.join(&items, try builder.concat(&.{
258 try builder.punct(","),
259 softline,
260 })),
261 try builder.punct("]"),
262 }));
263
264 const plain = try renderAlloc(std.testing.allocator, doc, .{ .width = 80 });
265 defer std.testing.allocator.free(plain);
266 try std.testing.expectEqualStrings(" [alpha, 42]", plain);
267
268 const colored = try renderAlloc(std.testing.allocator, doc, .{ .width = 80, .color = .ansi });
269 defer std.testing.allocator.free(colored);
270 try std.testing.expect(std.mem.indexOf(u8, colored, "\x1b[96malpha\x1b[0m") != null);
271 try std.testing.expect(std.mem.indexOf(u8, colored, "\x1b[2m[\x1b[0m") != null);
272 }
273
274 test "word docs wrap greedily at soft boundaries" {
275 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
276 defer arena_state.deinit();
277 const arena = arena_state.allocator();
278 const builder = Builder.init(arena);
279
280 const doc = try builder.words("alpha beta gamma");
281 const rendered = try renderAlloc(std.testing.allocator, doc, .{ .width = 10 });
282 defer std.testing.allocator.free(rendered);
283
284 try std.testing.expectEqualStrings(
285 \\alpha beta
286 \\gamma
287 , rendered);
288 }
289
290 test "word docs preserve explicit hard lines" {
291 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
292 defer arena_state.deinit();
293 const arena = arena_state.allocator();
294 const builder = Builder.init(arena);
295
296 const doc = try builder.words("alpha\nbeta gamma");
297 const rendered = try renderAlloc(std.testing.allocator, doc, .{ .width = 80 });
298 defer std.testing.allocator.free(rendered);
299
300 try std.testing.expectEqualStrings(
301 \\alpha
302 \\beta gamma
303 , rendered);
304 }
305
306 test "FlatFitWriter rejects position resets and overflow" {
307 var fit = FlatFitWriter.init(5);
308 try fit.writer.writeAll("abc");
309 try std.testing.expectEqual(@as(usize, 3), fit.count);
310
311 try std.testing.expectError(error.WriteFailed, fit.writer.writeAll("\n"));
312 try std.testing.expectError(error.WriteFailed, fit.writer.writeAll("def"));
313 try std.testing.expectEqual(@as(usize, 3), fit.count);
314
315 var carriage_return = FlatFitWriter.init(5);
316 try std.testing.expectError(
317 error.WriteFailed,
318 carriage_return.writer.writeAll("\r"),
319 );
320 try std.testing.expectEqual(@as(usize, 0), carriage_return.count);
321
322 var carriage_return_line_feed = FlatFitWriter.init(5);
323 try std.testing.expectError(
324 error.WriteFailed,
325 carriage_return_line_feed.writer.writeAll("\r\n"),
326 );
327 try std.testing.expectEqual(@as(usize, 0), carriage_return_line_feed.count);
328 }
329
330 test "writeWithState uses caller column and break indentation" {
331 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
332 defer arena_state.deinit();
333 const arena = arena_state.allocator();
334 const builder = Builder.init(arena);
335
336 const doc = try builder.group(try builder.concat(&.{
337 builder.text("alpha"),
338 softline,
339 builder.text("beta"),
340 }));
341
342 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
343 defer out.deinit();
344 try out.writer.writeAll("Label: ");
345 try writeWithState(&out.writer, doc, .{ .width = 12 }, .{ .column = 7, .indent = 7 });
346 const rendered = try out.toOwnedSlice();
347 defer std.testing.allocator.free(rendered);
348
349 try std.testing.expectEqualStrings(
350 \\Label: alpha
351 \\ beta
352 , rendered);
353 }
354
355 test "nested indentation rejects contextual overflow before nested output" {
356 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
357 defer arena_state.deinit();
358 const builder = Builder.init(arena_state.allocator());
359
360 const nested = try builder.nest(
361 std.math.maxInt(usize),
362 builder.text("nested"),
363 );
364 var maximum = std.Io.Writer.Allocating.init(std.testing.allocator);
365 defer maximum.deinit();
366 try writeWithState(&maximum.writer, nested, .{}, .{});
367 try std.testing.expectEqualStrings("nested", maximum.written());
368
369 const contextual = try builder.concat(&.{
370 builder.text("outer"),
371 nested,
372 });
373 var overflow = std.Io.Writer.Allocating.init(std.testing.allocator);
374 defer overflow.deinit();
375 const expected: pretty.RenderError = error.IndentationOverflow;
376 try std.testing.expectError(
377 expected,
378 writeWithState(&overflow.writer, contextual, .{}, .{ .indent = 1 }),
379 );
380 try std.testing.expectEqualStrings("outer", overflow.written());
381 }
382
383 test "embedded text line reset composes with later group" {
384 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
385 defer arena_state.deinit();
386 const builder = Builder.init(arena_state.allocator());
387
388 const suffix = try builder.group(try builder.concat(&.{
389 softline,
390 builder.text("c"),
391 }));
392 const doc = try builder.concat(&.{
393 builder.text("a\nb"),
394 suffix,
395 });
396
397 const rendered = try renderAlloc(std.testing.allocator, doc, .{ .width = 3 });
398 defer std.testing.allocator.free(rendered);
399 try std.testing.expectEqualStrings("a\nb c", rendered);
400 }
401
402 fn expectResetComposition(
403 prefix: []const u8,
404 tail: []const u8,
405 width: usize,
406 state: pretty.WriteState,
407 expected: []const u8,
408 ) !void {
409 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
410 defer arena_state.deinit();
411 const builder = Builder.init(arena_state.allocator());
412 const suffix = try builder.group(try builder.concat(&.{
413 softline,
414 builder.text(tail),
415 }));
416 const doc = try builder.concat(&.{ builder.text(prefix), suffix });
417
418 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
419 defer out.deinit();
420 try writeWithState(&out.writer, doc, .{ .width = width }, state);
421 try std.testing.expectEqualStrings(expected, out.written());
422 }
423
424 test "CR LF and CRLF reset caller position" {
425 try expectResetComposition(
426 "a\r\nb",
427 "c",
428 3,
429 .{},
430 "a\r\nb c",
431 );
432 try expectResetComposition(
433 "abcd\r",
434 "xy",
435 4,
436 .{},
437 "abcd\r xy",
438 );
439 try expectResetComposition(
440 "\n",
441 "abcdef",
442 8,
443 .{ .column = 7, .indent = 3 },
444 "\n abcdef",
445 );
446 }
447
448 test "newline-bearing flat alternatives stay broken" {
449 var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
450 defer arena_state.deinit();
451 const builder = Builder.init(arena_state.allocator());
452
453 const text_doc = try builder.group(try builder.concat(&.{
454 builder.text("ab\ncd"),
455 softline,
456 builder.text("e"),
457 }));
458 const text_rendered = try renderAlloc(
459 std.testing.allocator,
460 text_doc,
461 .{ .width = 10 },
462 );
463 defer std.testing.allocator.free(text_rendered);
464 try std.testing.expectEqualStrings("ab\ncd\ne", text_rendered);
465
466 const line_doc = try builder.group(.{ .line = "a\nb" });
467 const line_rendered = try renderAlloc(
468 std.testing.allocator,
469 line_doc,
470 .{ .width = 10 },
471 );
472 defer std.testing.allocator.free(line_rendered);
473 try std.testing.expectEqualStrings("\n", line_rendered);
474 }