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 }