master
1const builtin = @import("builtin");
2
3const std = @import("std");
4const Io = std.Io;
5const Writer = std.Io.Writer;
6const fatal = std.process.fatal;
7const mem = std.mem;
8const fs = std.fs;
9const process = std.process;
10const Allocator = std.mem.Allocator;
11const testing = std.testing;
12const getExternalExecutor = std.zig.system.getExternalExecutor;
13
14const max_doc_file_size = 10 * 1024 * 1024;
15
16const usage =
17 \\Usage: doctest [options] -i input -o output
18 \\
19 \\ Compiles and possibly runs a code example, capturing output and rendering
20 \\ it to HTML documentation.
21 \\
22 \\Options:
23 \\ -h, --help Print this help and exit
24 \\ -i input Source code file path
25 \\ -o output Where to write output HTML docs to
26 \\ --zig zig Path to the zig compiler
27 \\ --zig-lib-dir dir Override the zig compiler library path
28 \\ --cache-root dir Path to local .zig-cache/
29 \\
30;
31
32pub fn main() !void {
33 var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator);
34 defer arena_instance.deinit();
35
36 const arena = arena_instance.allocator();
37
38 var args_it = try process.argsWithAllocator(arena);
39 if (!args_it.skip()) fatal("missing argv[0]", .{});
40
41 const gpa = arena;
42
43 var threaded: std.Io.Threaded = .init(gpa);
44 defer threaded.deinit();
45 const io = threaded.io();
46
47 var opt_input: ?[]const u8 = null;
48 var opt_output: ?[]const u8 = null;
49 var opt_zig: ?[]const u8 = null;
50 var opt_zig_lib_dir: ?[]const u8 = null;
51 var opt_cache_root: ?[]const u8 = null;
52
53 while (args_it.next()) |arg| {
54 if (mem.startsWith(u8, arg, "-")) {
55 if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
56 try std.fs.File.stdout().writeAll(usage);
57 process.exit(0);
58 } else if (mem.eql(u8, arg, "-i")) {
59 opt_input = args_it.next() orelse fatal("expected parameter after -i", .{});
60 } else if (mem.eql(u8, arg, "-o")) {
61 opt_output = args_it.next() orelse fatal("expected parameter after -o", .{});
62 } else if (mem.eql(u8, arg, "--zig")) {
63 opt_zig = args_it.next() orelse fatal("expected parameter after --zig", .{});
64 } else if (mem.eql(u8, arg, "--zig-lib-dir")) {
65 opt_zig_lib_dir = args_it.next() orelse fatal("expected parameter after --zig-lib-dir", .{});
66 } else if (mem.eql(u8, arg, "--cache-root")) {
67 opt_cache_root = args_it.next() orelse fatal("expected parameter after --cache-root", .{});
68 } else {
69 fatal("unrecognized option: '{s}'", .{arg});
70 }
71 } else {
72 fatal("unexpected positional argument: '{s}'", .{arg});
73 }
74 }
75
76 const input_path = opt_input orelse fatal("missing input file (-i)", .{});
77 const output_path = opt_output orelse fatal("missing output file (-o)", .{});
78 const zig_path = opt_zig orelse fatal("missing zig compiler path (--zig)", .{});
79 const cache_root = opt_cache_root orelse fatal("missing cache root path (--cache-root)", .{});
80
81 const source_bytes = try fs.cwd().readFileAlloc(input_path, arena, .limited(std.math.maxInt(u32)));
82 const code = try parseManifest(arena, source_bytes);
83 const source = stripManifest(source_bytes);
84
85 const tmp_dir_path = try std.fmt.allocPrint(arena, "{s}/tmp/{x}", .{
86 cache_root, std.crypto.random.int(u64),
87 });
88 fs.cwd().makePath(tmp_dir_path) catch |err|
89 fatal("unable to create tmp dir '{s}': {s}", .{ tmp_dir_path, @errorName(err) });
90 defer fs.cwd().deleteTree(tmp_dir_path) catch |err| std.log.err("unable to delete '{s}': {s}", .{
91 tmp_dir_path, @errorName(err),
92 });
93
94 var out_file = try fs.cwd().createFile(output_path, .{});
95 defer out_file.close();
96 var out_file_buffer: [4096]u8 = undefined;
97 var out_file_writer = out_file.writer(&out_file_buffer);
98
99 const out = &out_file_writer.interface;
100
101 try printSourceBlock(arena, out, source, fs.path.basename(input_path));
102 try printOutput(
103 arena,
104 io,
105 out,
106 code,
107 tmp_dir_path,
108 try std.fs.path.relative(arena, tmp_dir_path, zig_path),
109 try std.fs.path.relative(arena, tmp_dir_path, input_path),
110 if (opt_zig_lib_dir) |zig_lib_dir|
111 try std.fs.path.relative(arena, tmp_dir_path, zig_lib_dir)
112 else
113 null,
114 );
115
116 try out_file_writer.end();
117}
118
119fn printOutput(
120 arena: Allocator,
121 io: Io,
122 out: *Writer,
123 code: Code,
124 /// Relative to this process' cwd.
125 tmp_dir_path: []const u8,
126 /// Relative to `tmp_dir_path`.
127 zig_exe: []const u8,
128 /// Relative to `tmp_dir_path`.
129 input_path: []const u8,
130 /// Relative to `tmp_dir_path`.
131 opt_zig_lib_dir: ?[]const u8,
132) !void {
133 var env_map = try process.getEnvMap(arena);
134 try env_map.put("CLICOLOR_FORCE", "1");
135
136 const host = try std.zig.system.resolveTargetQuery(io, .{});
137 const obj_ext = builtin.object_format.fileExt(builtin.cpu.arch);
138 const print = std.debug.print;
139
140 var shell_buffer: Writer.Allocating = .init(arena);
141 defer shell_buffer.deinit();
142 const shell_out = &shell_buffer.writer;
143
144 const code_name = std.fs.path.stem(input_path);
145
146 switch (code.id) {
147 .exe => |expected_outcome| code_block: {
148 var build_args = std.array_list.Managed([]const u8).init(arena);
149 defer build_args.deinit();
150 try build_args.appendSlice(&[_][]const u8{
151 zig_exe, "build-exe",
152 "--name", code_name,
153 "--color", "on",
154 input_path,
155 });
156 if (opt_zig_lib_dir) |zig_lib_dir| {
157 try build_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
158 }
159
160 try shell_out.print("$ zig build-exe {s}.zig ", .{code_name});
161
162 switch (code.mode) {
163 .Debug => {},
164 else => {
165 try build_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) });
166 try shell_out.print("-O {s} ", .{@tagName(code.mode)});
167 },
168 }
169 for (code.link_objects) |link_object| {
170 const name_with_ext = try std.fmt.allocPrint(arena, "{s}{s}", .{ link_object, obj_ext });
171 try build_args.append(name_with_ext);
172 try shell_out.print("{s} ", .{name_with_ext});
173 }
174 if (code.link_libc) {
175 try build_args.append("-lc");
176 try shell_out.print("-lc ", .{});
177 }
178
179 if (code.target_str) |triple| {
180 try build_args.appendSlice(&[_][]const u8{ "-target", triple });
181 try shell_out.print("-target {s} ", .{triple});
182 }
183 if (code.use_llvm) |use_llvm| {
184 if (use_llvm) {
185 try build_args.append("-fllvm");
186 try shell_out.print("-fllvm", .{});
187 } else {
188 try build_args.append("-fno-llvm");
189 try shell_out.print("-fno-llvm", .{});
190 }
191 }
192 if (code.verbose_cimport) {
193 try build_args.append("--verbose-cimport");
194 try shell_out.print("--verbose-cimport ", .{});
195 }
196 for (code.additional_options) |option| {
197 try build_args.append(option);
198 try shell_out.print("{s} ", .{option});
199 }
200
201 try shell_out.print("\n", .{});
202
203 if (expected_outcome == .build_fail) {
204 const result = try process.Child.run(.{
205 .allocator = arena,
206 .argv = build_args.items,
207 .cwd = tmp_dir_path,
208 .env_map = &env_map,
209 .max_output_bytes = max_doc_file_size,
210 });
211 switch (result.term) {
212 .Exited => |exit_code| {
213 if (exit_code == 0) {
214 print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
215 dumpArgs(build_args.items);
216 fatal("example incorrectly compiled", .{});
217 }
218 },
219 else => {
220 print("{s}\nThe following command crashed:\n", .{result.stderr});
221 dumpArgs(build_args.items);
222 fatal("example compile crashed", .{});
223 },
224 }
225 const escaped_stderr = try escapeHtml(arena, result.stderr);
226 const colored_stderr = try termColor(arena, escaped_stderr);
227 try shell_out.writeAll(colored_stderr);
228 break :code_block;
229 }
230 const exec_result = run(arena, &env_map, tmp_dir_path, build_args.items) catch
231 fatal("example failed to compile", .{});
232
233 if (code.verbose_cimport) {
234 const escaped_build_stderr = try escapeHtml(arena, exec_result.stderr);
235 try shell_out.writeAll(escaped_build_stderr);
236 }
237
238 if (code.target_str) |triple| {
239 if (mem.startsWith(u8, triple, "wasm32") or
240 mem.startsWith(u8, triple, "riscv64-linux") or
241 (mem.startsWith(u8, triple, "x86_64-linux") and
242 builtin.os.tag != .linux or builtin.cpu.arch != .x86_64))
243 {
244 // skip execution
245 break :code_block;
246 }
247 }
248 const target_query = try std.Target.Query.parse(.{
249 .arch_os_abi = code.target_str orelse "native",
250 });
251 const target = try std.zig.system.resolveTargetQuery(io, target_query);
252
253 const path_to_exe = try std.fmt.allocPrint(arena, "./{s}{s}", .{
254 code_name, target.exeFileExt(),
255 });
256 const run_args = &[_][]const u8{path_to_exe};
257
258 var exited_with_signal = false;
259
260 const result = if (expected_outcome == .fail) blk: {
261 const result = try process.Child.run(.{
262 .allocator = arena,
263 .argv = run_args,
264 .env_map = &env_map,
265 .cwd = tmp_dir_path,
266 .max_output_bytes = max_doc_file_size,
267 });
268 switch (result.term) {
269 .Exited => |exit_code| {
270 if (exit_code == 0) {
271 print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
272 dumpArgs(run_args);
273 fatal("example incorrectly compiled", .{});
274 }
275 },
276 .Signal => exited_with_signal = true,
277 else => {},
278 }
279 break :blk result;
280 } else blk: {
281 break :blk run(arena, &env_map, tmp_dir_path, run_args) catch
282 fatal("example crashed", .{});
283 };
284
285 const escaped_stderr = try escapeHtml(arena, result.stderr);
286 const escaped_stdout = try escapeHtml(arena, result.stdout);
287
288 const colored_stderr = try termColor(arena, escaped_stderr);
289 const colored_stdout = try termColor(arena, escaped_stdout);
290
291 try shell_out.print("$ ./{s}\n{s}{s}", .{ code_name, colored_stdout, colored_stderr });
292 if (exited_with_signal) {
293 try shell_out.print("(process terminated by signal)", .{});
294 }
295 try shell_out.writeAll("\n");
296 },
297 .@"test" => {
298 var test_args = std.array_list.Managed([]const u8).init(arena);
299 defer test_args.deinit();
300
301 try test_args.appendSlice(&[_][]const u8{
302 zig_exe, "test", input_path,
303 });
304 if (opt_zig_lib_dir) |zig_lib_dir| {
305 try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
306 }
307 try shell_out.print("$ zig test {s}.zig ", .{code_name});
308
309 switch (code.mode) {
310 .Debug => {},
311 else => {
312 try test_args.appendSlice(&[_][]const u8{
313 "-O", @tagName(code.mode),
314 });
315 try shell_out.print("-O {s} ", .{@tagName(code.mode)});
316 },
317 }
318 if (code.link_libc) {
319 try test_args.append("-lc");
320 try shell_out.print("-lc ", .{});
321 }
322 if (code.target_str) |triple| {
323 try test_args.appendSlice(&[_][]const u8{ "-target", triple });
324 try shell_out.print("-target {s} ", .{triple});
325
326 const target_query = try std.Target.Query.parse(.{
327 .arch_os_abi = triple,
328 });
329 const target = try std.zig.system.resolveTargetQuery(io, target_query);
330 switch (getExternalExecutor(&host, &target, .{
331 .link_libc = code.link_libc,
332 })) {
333 .native => {},
334 else => {
335 try test_args.appendSlice(&[_][]const u8{"--test-no-exec"});
336 try shell_out.writeAll("--test-no-exec");
337 },
338 }
339 }
340 if (code.use_llvm) |use_llvm| {
341 if (use_llvm) {
342 try test_args.append("-fllvm");
343 try shell_out.print("-fllvm", .{});
344 } else {
345 try test_args.append("-fno-llvm");
346 try shell_out.print("-fno-llvm", .{});
347 }
348 }
349
350 const result = run(arena, &env_map, tmp_dir_path, test_args.items) catch
351 fatal("test failed", .{});
352 const escaped_stderr = try escapeHtml(arena, result.stderr);
353 const escaped_stdout = try escapeHtml(arena, result.stdout);
354 try shell_out.print("\n{s}{s}\n", .{ escaped_stderr, escaped_stdout });
355 },
356 .test_error => |error_match| {
357 var test_args = std.array_list.Managed([]const u8).init(arena);
358 defer test_args.deinit();
359
360 try test_args.appendSlice(&[_][]const u8{
361 zig_exe, "test",
362 "--color", "on",
363 input_path,
364 });
365 if (opt_zig_lib_dir) |zig_lib_dir| {
366 try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
367 }
368 try shell_out.print("$ zig test {s}.zig ", .{code_name});
369
370 switch (code.mode) {
371 .Debug => {},
372 else => {
373 try test_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) });
374 try shell_out.print("-O {s} ", .{@tagName(code.mode)});
375 },
376 }
377 if (code.link_libc) {
378 try test_args.append("-lc");
379 try shell_out.print("-lc ", .{});
380 }
381 const result = try process.Child.run(.{
382 .allocator = arena,
383 .argv = test_args.items,
384 .env_map = &env_map,
385 .cwd = tmp_dir_path,
386 .max_output_bytes = max_doc_file_size,
387 });
388 switch (result.term) {
389 .Exited => |exit_code| {
390 if (exit_code == 0) {
391 print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
392 dumpArgs(test_args.items);
393 fatal("example incorrectly compiled", .{});
394 }
395 },
396 else => {
397 print("{s}\nThe following command crashed:\n", .{result.stderr});
398 dumpArgs(test_args.items);
399 fatal("example compile crashed", .{});
400 },
401 }
402 if (mem.indexOf(u8, result.stderr, error_match) == null) {
403 print("{s}\nExpected to find '{s}' in stderr\n", .{ result.stderr, error_match });
404 fatal("example did not have expected compile error", .{});
405 }
406 const escaped_stderr = try escapeHtml(arena, result.stderr);
407 const colored_stderr = try termColor(arena, escaped_stderr);
408 try shell_out.print("\n{s}\n", .{colored_stderr});
409 },
410 .test_safety => |error_match| {
411 var test_args = std.array_list.Managed([]const u8).init(arena);
412 defer test_args.deinit();
413
414 try test_args.appendSlice(&[_][]const u8{
415 zig_exe, "test",
416 input_path,
417 });
418 if (opt_zig_lib_dir) |zig_lib_dir| {
419 try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
420 }
421 var mode_arg: []const u8 = "";
422 switch (code.mode) {
423 .Debug => {},
424 .ReleaseSafe => {
425 try test_args.append("-OReleaseSafe");
426 mode_arg = "-OReleaseSafe";
427 },
428 .ReleaseFast => {
429 try test_args.append("-OReleaseFast");
430 mode_arg = "-OReleaseFast";
431 },
432 .ReleaseSmall => {
433 try test_args.append("-OReleaseSmall");
434 mode_arg = "-OReleaseSmall";
435 },
436 }
437
438 const result = try process.Child.run(.{
439 .allocator = arena,
440 .argv = test_args.items,
441 .env_map = &env_map,
442 .cwd = tmp_dir_path,
443 .max_output_bytes = max_doc_file_size,
444 });
445 switch (result.term) {
446 .Exited => |exit_code| {
447 if (exit_code == 0) {
448 print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
449 dumpArgs(test_args.items);
450 fatal("example test incorrectly succeeded", .{});
451 }
452 },
453 else => {
454 print("{s}\nThe following command crashed:\n", .{result.stderr});
455 dumpArgs(test_args.items);
456 fatal("example compile crashed", .{});
457 },
458 }
459 if (mem.indexOf(u8, result.stderr, error_match) == null) {
460 print("{s}\nExpected to find '{s}' in stderr\n", .{ result.stderr, error_match });
461 fatal("example did not have expected runtime safety error message", .{});
462 }
463 const escaped_stderr = try escapeHtml(arena, result.stderr);
464 const colored_stderr = try termColor(arena, escaped_stderr);
465 try shell_out.print("$ zig test {s}.zig {s}\n{s}\n", .{
466 code_name,
467 mode_arg,
468 colored_stderr,
469 });
470 },
471 .obj => |maybe_error_match| {
472 const name_plus_obj_ext = try std.fmt.allocPrint(arena, "{s}{s}", .{ code_name, obj_ext });
473 var build_args = std.array_list.Managed([]const u8).init(arena);
474 defer build_args.deinit();
475
476 try build_args.appendSlice(&[_][]const u8{
477 zig_exe, "build-obj",
478 "--color", "on",
479 "--name", code_name,
480 input_path, try std.fmt.allocPrint(arena, "-femit-bin={s}", .{name_plus_obj_ext}),
481 });
482 if (opt_zig_lib_dir) |zig_lib_dir| {
483 try build_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
484 }
485
486 try shell_out.print("$ zig build-obj {s}.zig ", .{code_name});
487
488 switch (code.mode) {
489 .Debug => {},
490 else => {
491 try build_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) });
492 try shell_out.print("-O {s} ", .{@tagName(code.mode)});
493 },
494 }
495
496 if (code.target_str) |triple| {
497 try build_args.appendSlice(&[_][]const u8{ "-target", triple });
498 try shell_out.print("-target {s} ", .{triple});
499 }
500 if (code.use_llvm) |use_llvm| {
501 if (use_llvm) {
502 try build_args.append("-fllvm");
503 try shell_out.print("-fllvm", .{});
504 } else {
505 try build_args.append("-fno-llvm");
506 try shell_out.print("-fno-llvm", .{});
507 }
508 }
509 for (code.additional_options) |option| {
510 try build_args.append(option);
511 try shell_out.print("{s} ", .{option});
512 }
513
514 if (maybe_error_match) |error_match| {
515 const result = try process.Child.run(.{
516 .allocator = arena,
517 .argv = build_args.items,
518 .env_map = &env_map,
519 .cwd = tmp_dir_path,
520 .max_output_bytes = max_doc_file_size,
521 });
522 switch (result.term) {
523 .Exited => |exit_code| {
524 if (exit_code == 0) {
525 print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
526 dumpArgs(build_args.items);
527 fatal("example build incorrectly succeeded", .{});
528 }
529 },
530 else => {
531 print("{s}\nThe following command crashed:\n", .{result.stderr});
532 dumpArgs(build_args.items);
533 fatal("example compile crashed", .{});
534 },
535 }
536 if (mem.indexOf(u8, result.stderr, error_match) == null) {
537 print("{s}\nExpected to find '{s}' in stderr\n", .{ result.stderr, error_match });
538 fatal("example did not have expected compile error message", .{});
539 }
540 const escaped_stderr = try escapeHtml(arena, result.stderr);
541 const colored_stderr = try termColor(arena, escaped_stderr);
542 try shell_out.print("\n{s} ", .{colored_stderr});
543 } else {
544 _ = run(arena, &env_map, tmp_dir_path, build_args.items) catch fatal("example failed to compile", .{});
545 }
546 try shell_out.writeAll("\n");
547 },
548 .lib => {
549 const bin_basename = try std.zig.binNameAlloc(arena, .{
550 .root_name = code_name,
551 .target = &builtin.target,
552 .output_mode = .Lib,
553 });
554
555 var test_args = std.array_list.Managed([]const u8).init(arena);
556 defer test_args.deinit();
557
558 try test_args.appendSlice(&[_][]const u8{
559 zig_exe, "build-lib",
560 input_path, try std.fmt.allocPrint(arena, "-femit-bin={s}", .{bin_basename}),
561 });
562 if (opt_zig_lib_dir) |zig_lib_dir| {
563 try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
564 }
565 try shell_out.print("$ zig build-lib {s}.zig ", .{code_name});
566
567 switch (code.mode) {
568 .Debug => {},
569 else => {
570 try test_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) });
571 try shell_out.print("-O {s} ", .{@tagName(code.mode)});
572 },
573 }
574 if (code.target_str) |triple| {
575 try test_args.appendSlice(&[_][]const u8{ "-target", triple });
576 try shell_out.print("-target {s} ", .{triple});
577 }
578 if (code.use_llvm) |use_llvm| {
579 if (use_llvm) {
580 try test_args.append("-fllvm");
581 try shell_out.print("-fllvm", .{});
582 } else {
583 try test_args.append("-fno-llvm");
584 try shell_out.print("-fno-llvm", .{});
585 }
586 }
587 if (code.link_mode) |link_mode| {
588 switch (link_mode) {
589 .static => {
590 try test_args.append("-static");
591 try shell_out.print("-static ", .{});
592 },
593 .dynamic => {
594 try test_args.append("-dynamic");
595 try shell_out.print("-dynamic ", .{});
596 },
597 }
598 }
599 for (code.additional_options) |option| {
600 try test_args.append(option);
601 try shell_out.print("{s} ", .{option});
602 }
603 const result = run(arena, &env_map, tmp_dir_path, test_args.items) catch fatal("test failed", .{});
604 const escaped_stderr = try escapeHtml(arena, result.stderr);
605 const escaped_stdout = try escapeHtml(arena, result.stdout);
606 try shell_out.print("\n{s}{s}\n", .{ escaped_stderr, escaped_stdout });
607 },
608 }
609
610 if (!code.just_check_syntax) {
611 try printShell(out, shell_buffer.written(), false);
612 }
613}
614
615fn dumpArgs(args: []const []const u8) void {
616 for (args) |arg|
617 std.debug.print("{s} ", .{arg})
618 else
619 std.debug.print("\n", .{});
620}
621
622fn printSourceBlock(arena: Allocator, out: *Writer, source_bytes: []const u8, name: []const u8) !void {
623 try out.print("<figure><figcaption class=\"{s}-cap\"><cite class=\"file\">{s}</cite></figcaption><pre>", .{
624 "zig", name,
625 });
626 try tokenizeAndPrint(arena, out, source_bytes);
627 try out.writeAll("</pre></figure>");
628}
629
630fn tokenizeAndPrint(arena: Allocator, out: *Writer, raw_src: []const u8) !void {
631 const src_non_terminated = mem.trim(u8, raw_src, " \r\n");
632 const src = try arena.dupeZ(u8, src_non_terminated);
633
634 try out.writeAll("<code>");
635 var tokenizer = std.zig.Tokenizer.init(src);
636 var index: usize = 0;
637 var next_tok_is_fn = false;
638 while (true) {
639 const prev_tok_was_fn = next_tok_is_fn;
640 next_tok_is_fn = false;
641
642 const token = tokenizer.next();
643 if (mem.indexOf(u8, src[index..token.loc.start], "//")) |comment_start_off| {
644 // render one comment
645 const comment_start = index + comment_start_off;
646 const comment_end_off = mem.indexOf(u8, src[comment_start..token.loc.start], "\n");
647 const comment_end = if (comment_end_off) |o| comment_start + o else token.loc.start;
648
649 try writeEscapedLines(out, src[index..comment_start]);
650 try out.writeAll("<span class=\"tok-comment\">");
651 try writeEscaped(out, src[comment_start..comment_end]);
652 try out.writeAll("</span>");
653 index = comment_end;
654 tokenizer.index = index;
655 continue;
656 }
657
658 try writeEscapedLines(out, src[index..token.loc.start]);
659 switch (token.tag) {
660 .eof => break,
661
662 .keyword_addrspace,
663 .keyword_align,
664 .keyword_and,
665 .keyword_asm,
666 .keyword_break,
667 .keyword_catch,
668 .keyword_comptime,
669 .keyword_const,
670 .keyword_continue,
671 .keyword_defer,
672 .keyword_else,
673 .keyword_enum,
674 .keyword_errdefer,
675 .keyword_error,
676 .keyword_export,
677 .keyword_extern,
678 .keyword_for,
679 .keyword_if,
680 .keyword_inline,
681 .keyword_noalias,
682 .keyword_noinline,
683 .keyword_nosuspend,
684 .keyword_opaque,
685 .keyword_or,
686 .keyword_orelse,
687 .keyword_packed,
688 .keyword_anyframe,
689 .keyword_pub,
690 .keyword_resume,
691 .keyword_return,
692 .keyword_linksection,
693 .keyword_callconv,
694 .keyword_struct,
695 .keyword_suspend,
696 .keyword_switch,
697 .keyword_test,
698 .keyword_threadlocal,
699 .keyword_try,
700 .keyword_union,
701 .keyword_unreachable,
702 .keyword_var,
703 .keyword_volatile,
704 .keyword_allowzero,
705 .keyword_while,
706 .keyword_anytype,
707 => {
708 try out.writeAll("<span class=\"tok-kw\">");
709 try writeEscaped(out, src[token.loc.start..token.loc.end]);
710 try out.writeAll("</span>");
711 },
712
713 .keyword_fn => {
714 try out.writeAll("<span class=\"tok-kw\">");
715 try writeEscaped(out, src[token.loc.start..token.loc.end]);
716 try out.writeAll("</span>");
717 next_tok_is_fn = true;
718 },
719
720 .string_literal,
721 .multiline_string_literal_line,
722 .char_literal,
723 => {
724 try out.writeAll("<span class=\"tok-str\">");
725 try writeEscaped(out, src[token.loc.start..token.loc.end]);
726 try out.writeAll("</span>");
727 },
728
729 .builtin => {
730 try out.writeAll("<span class=\"tok-builtin\">");
731 try writeEscaped(out, src[token.loc.start..token.loc.end]);
732 try out.writeAll("</span>");
733 },
734
735 .doc_comment,
736 .container_doc_comment,
737 => {
738 try out.writeAll("<span class=\"tok-comment\">");
739 try writeEscaped(out, src[token.loc.start..token.loc.end]);
740 try out.writeAll("</span>");
741 },
742
743 .identifier => {
744 const tok_bytes = src[token.loc.start..token.loc.end];
745 if (mem.eql(u8, tok_bytes, "undefined") or
746 mem.eql(u8, tok_bytes, "null") or
747 mem.eql(u8, tok_bytes, "true") or
748 mem.eql(u8, tok_bytes, "false"))
749 {
750 try out.writeAll("<span class=\"tok-null\">");
751 try writeEscaped(out, tok_bytes);
752 try out.writeAll("</span>");
753 } else if (prev_tok_was_fn) {
754 try out.writeAll("<span class=\"tok-fn\">");
755 try writeEscaped(out, tok_bytes);
756 try out.writeAll("</span>");
757 } else {
758 const is_int = blk: {
759 if (src[token.loc.start] != 'i' and src[token.loc.start] != 'u')
760 break :blk false;
761 var i = token.loc.start + 1;
762 if (i == token.loc.end)
763 break :blk false;
764 while (i != token.loc.end) : (i += 1) {
765 if (src[i] < '0' or src[i] > '9')
766 break :blk false;
767 }
768 break :blk true;
769 };
770 const isType = std.zig.isPrimitive;
771 if (is_int or isType(tok_bytes)) {
772 try out.writeAll("<span class=\"tok-type\">");
773 try writeEscaped(out, tok_bytes);
774 try out.writeAll("</span>");
775 } else {
776 try writeEscaped(out, tok_bytes);
777 }
778 }
779 },
780
781 .number_literal => {
782 try out.writeAll("<span class=\"tok-number\">");
783 try writeEscaped(out, src[token.loc.start..token.loc.end]);
784 try out.writeAll("</span>");
785 },
786
787 .bang,
788 .pipe,
789 .pipe_pipe,
790 .pipe_equal,
791 .equal,
792 .equal_equal,
793 .equal_angle_bracket_right,
794 .bang_equal,
795 .l_paren,
796 .r_paren,
797 .semicolon,
798 .percent,
799 .percent_equal,
800 .l_brace,
801 .r_brace,
802 .l_bracket,
803 .r_bracket,
804 .period,
805 .period_asterisk,
806 .ellipsis2,
807 .ellipsis3,
808 .caret,
809 .caret_equal,
810 .plus,
811 .plus_plus,
812 .plus_equal,
813 .plus_percent,
814 .plus_percent_equal,
815 .plus_pipe,
816 .plus_pipe_equal,
817 .minus,
818 .minus_equal,
819 .minus_percent,
820 .minus_percent_equal,
821 .minus_pipe,
822 .minus_pipe_equal,
823 .asterisk,
824 .asterisk_equal,
825 .asterisk_asterisk,
826 .asterisk_percent,
827 .asterisk_percent_equal,
828 .asterisk_pipe,
829 .asterisk_pipe_equal,
830 .arrow,
831 .colon,
832 .slash,
833 .slash_equal,
834 .comma,
835 .ampersand,
836 .ampersand_equal,
837 .question_mark,
838 .angle_bracket_left,
839 .angle_bracket_left_equal,
840 .angle_bracket_angle_bracket_left,
841 .angle_bracket_angle_bracket_left_equal,
842 .angle_bracket_angle_bracket_left_pipe,
843 .angle_bracket_angle_bracket_left_pipe_equal,
844 .angle_bracket_right,
845 .angle_bracket_right_equal,
846 .angle_bracket_angle_bracket_right,
847 .angle_bracket_angle_bracket_right_equal,
848 .tilde,
849 => try writeEscaped(out, src[token.loc.start..token.loc.end]),
850
851 .invalid, .invalid_periodasterisks => fatal("syntax error", .{}),
852 }
853 index = token.loc.end;
854 }
855 try out.writeAll("</code>");
856}
857
858fn writeEscapedLines(out: *Writer, text: []const u8) !void {
859 return writeEscaped(out, text);
860}
861
862const Code = struct {
863 id: Id,
864 mode: std.builtin.OptimizeMode,
865 link_objects: []const []const u8,
866 target_str: ?[]const u8,
867 link_libc: bool,
868 link_mode: ?std.builtin.LinkMode,
869 disable_cache: bool,
870 verbose_cimport: bool,
871 just_check_syntax: bool,
872 additional_options: []const []const u8,
873 use_llvm: ?bool,
874
875 const Id = union(enum) {
876 @"test",
877 test_error: []const u8,
878 test_safety: []const u8,
879 exe: ExpectedOutcome,
880 obj: ?[]const u8,
881 lib,
882 };
883
884 const ExpectedOutcome = enum {
885 succeed,
886 fail,
887 build_fail,
888 };
889};
890
891fn stripManifest(source_bytes: []const u8) []const u8 {
892 const manifest_start = mem.lastIndexOf(u8, source_bytes, "\n\n// ") orelse
893 fatal("missing manifest comment", .{});
894 return source_bytes[0 .. manifest_start + 1];
895}
896
897fn parseManifest(arena: Allocator, source_bytes: []const u8) !Code {
898 const manifest_start = mem.lastIndexOf(u8, source_bytes, "\n\n// ") orelse
899 fatal("missing manifest comment", .{});
900 var it = mem.tokenizeScalar(u8, source_bytes[manifest_start..], '\n');
901 const first_line = skipPrefix(it.next().?);
902
903 var just_check_syntax = false;
904 const id: Code.Id = if (mem.eql(u8, first_line, "syntax")) blk: {
905 just_check_syntax = true;
906 break :blk .{ .obj = null };
907 } else if (mem.eql(u8, first_line, "test"))
908 .@"test"
909 else if (mem.eql(u8, first_line, "lib"))
910 .lib
911 else if (mem.eql(u8, first_line, "obj"))
912 .{ .obj = null }
913 else if (mem.startsWith(u8, first_line, "test_error="))
914 .{ .test_error = first_line["test_error=".len..] }
915 else if (mem.startsWith(u8, first_line, "test_safety="))
916 .{ .test_safety = first_line["test_safety=".len..] }
917 else if (mem.startsWith(u8, first_line, "exe="))
918 .{ .exe = std.meta.stringToEnum(Code.ExpectedOutcome, first_line["exe=".len..]) orelse
919 fatal("bad exe expected outcome in line '{s}'", .{first_line}) }
920 else if (mem.startsWith(u8, first_line, "obj="))
921 .{ .obj = first_line["obj=".len..] }
922 else
923 fatal("unrecognized manifest id: '{s}'", .{first_line});
924
925 var mode: std.builtin.OptimizeMode = .Debug;
926 var link_mode: ?std.builtin.LinkMode = null;
927 var link_objects: std.ArrayList([]const u8) = .empty;
928 var additional_options: std.ArrayList([]const u8) = .empty;
929 var target_str: ?[]const u8 = null;
930 var link_libc = false;
931 var disable_cache = false;
932 var verbose_cimport = false;
933 var use_llvm: ?bool = null;
934
935 while (it.next()) |prefixed_line| {
936 const line = skipPrefix(prefixed_line);
937 if (mem.startsWith(u8, line, "optimize=")) {
938 mode = std.meta.stringToEnum(std.builtin.OptimizeMode, line["optimize=".len..]) orelse
939 fatal("bad optimization mode line: '{s}'", .{line});
940 } else if (mem.startsWith(u8, line, "link_mode=")) {
941 link_mode = std.meta.stringToEnum(std.builtin.LinkMode, line["link_mode=".len..]) orelse
942 fatal("bad link mode line: '{s}'", .{line});
943 } else if (mem.startsWith(u8, line, "link_object=")) {
944 try link_objects.append(arena, line["link_object=".len..]);
945 } else if (mem.startsWith(u8, line, "additional_option=")) {
946 try additional_options.append(arena, line["additional_option=".len..]);
947 } else if (mem.startsWith(u8, line, "target=")) {
948 target_str = line["target=".len..];
949 } else if (mem.eql(u8, line, "llvm=true")) {
950 use_llvm = true;
951 } else if (mem.eql(u8, line, "llvm=false")) {
952 use_llvm = false;
953 } else if (mem.eql(u8, line, "link_libc")) {
954 link_libc = true;
955 } else if (mem.eql(u8, line, "disable_cache")) {
956 disable_cache = true;
957 } else if (mem.eql(u8, line, "verbose_cimport")) {
958 verbose_cimport = true;
959 } else {
960 fatal("unrecognized manifest line: {s}", .{line});
961 }
962 }
963
964 return .{
965 .id = id,
966 .mode = mode,
967 .additional_options = try additional_options.toOwnedSlice(arena),
968 .link_objects = try link_objects.toOwnedSlice(arena),
969 .target_str = target_str,
970 .link_libc = link_libc,
971 .link_mode = link_mode,
972 .disable_cache = disable_cache,
973 .verbose_cimport = verbose_cimport,
974 .just_check_syntax = just_check_syntax,
975 .use_llvm = use_llvm,
976 };
977}
978
979fn skipPrefix(line: []const u8) []const u8 {
980 if (!mem.startsWith(u8, line, "// ")) {
981 fatal("line does not start with '// ': '{s}", .{line});
982 }
983 return line[3..];
984}
985
986fn escapeHtml(gpa: Allocator, input: []const u8) ![]u8 {
987 var allocating: Writer.Allocating = .init(gpa);
988 defer allocating.deinit();
989 try writeEscaped(&allocating.writer, input);
990 return allocating.toOwnedSlice();
991}
992
993fn writeEscaped(w: *Writer, input: []const u8) !void {
994 for (input) |c| try switch (c) {
995 '&' => w.writeAll("&"),
996 '<' => w.writeAll("<"),
997 '>' => w.writeAll(">"),
998 '"' => w.writeAll("""),
999 else => w.writeByte(c),
1000 };
1001}
1002
1003fn termColor(allocator: Allocator, input: []const u8) ![]u8 {
1004 // The SRG sequences generates by the Zig compiler are in the format:
1005 // ESC [ <foreground-color> ; <n> m
1006 // or
1007 // ESC [ <n> m
1008 //
1009 // where
1010 // foreground-color is 31 (red), 32 (green), 36 (cyan)
1011 // n is 0 (reset), 1 (bold), 2 (dim)
1012 //
1013 // Note that 37 (white) is currently not used by the compiler.
1014 //
1015 // See std.debug.TTY.Color.
1016 const supported_sgr_colors = [_]u8{ 31, 32, 36 };
1017 const supported_sgr_numbers = [_]u8{ 0, 1, 2 };
1018
1019 var buf = std.array_list.Managed(u8).init(allocator);
1020 defer buf.deinit();
1021
1022 var sgr_param_start_index: usize = undefined;
1023 var sgr_num: u8 = undefined;
1024 var sgr_color: u8 = undefined;
1025 var i: usize = 0;
1026 var state: enum {
1027 start,
1028 escape,
1029 lbracket,
1030 number,
1031 after_number,
1032 arg,
1033 arg_number,
1034 expect_end,
1035 } = .start;
1036 var last_new_line: usize = 0;
1037 var open_span_count: usize = 0;
1038 while (i < input.len) : (i += 1) {
1039 const c = input[i];
1040 switch (state) {
1041 .start => switch (c) {
1042 '\x1b' => state = .escape,
1043 '\n' => {
1044 try buf.append(c);
1045 last_new_line = buf.items.len;
1046 },
1047 else => try buf.append(c),
1048 },
1049 .escape => switch (c) {
1050 '[' => state = .lbracket,
1051 else => return error.UnsupportedEscape,
1052 },
1053 .lbracket => switch (c) {
1054 '0'...'9' => {
1055 sgr_param_start_index = i;
1056 state = .number;
1057 },
1058 else => return error.UnsupportedEscape,
1059 },
1060 .number => switch (c) {
1061 '0'...'9' => {},
1062 else => {
1063 sgr_num = try std.fmt.parseInt(u8, input[sgr_param_start_index..i], 10);
1064 sgr_color = 0;
1065 state = .after_number;
1066 i -= 1;
1067 },
1068 },
1069 .after_number => switch (c) {
1070 ';' => state = .arg,
1071 'D' => state = .start,
1072 'K' => {
1073 buf.items.len = last_new_line;
1074 state = .start;
1075 },
1076 else => {
1077 state = .expect_end;
1078 i -= 1;
1079 },
1080 },
1081 .arg => switch (c) {
1082 '0'...'9' => {
1083 sgr_param_start_index = i;
1084 state = .arg_number;
1085 },
1086 else => return error.UnsupportedEscape,
1087 },
1088 .arg_number => switch (c) {
1089 '0'...'9' => {},
1090 else => {
1091 // Keep the sequence consistent, foreground color first.
1092 // 32;1m is equivalent to 1;32m, but the latter will
1093 // generate an incorrect HTML class without notice.
1094 sgr_color = sgr_num;
1095 if (!in(&supported_sgr_colors, sgr_color)) return error.UnsupportedForegroundColor;
1096
1097 sgr_num = try std.fmt.parseInt(u8, input[sgr_param_start_index..i], 10);
1098 if (!in(&supported_sgr_numbers, sgr_num)) return error.UnsupportedNumber;
1099
1100 state = .expect_end;
1101 i -= 1;
1102 },
1103 },
1104 .expect_end => switch (c) {
1105 'm' => {
1106 state = .start;
1107 while (open_span_count != 0) : (open_span_count -= 1) {
1108 try buf.appendSlice("</span>");
1109 }
1110 if (sgr_num == 0) {
1111 if (sgr_color != 0) return error.UnsupportedColor;
1112 continue;
1113 }
1114 if (sgr_color != 0) {
1115 try buf.print("<span class=\"sgr-{d}_{d}m\">", .{ sgr_color, sgr_num });
1116 } else {
1117 try buf.print("<span class=\"sgr-{d}m\">", .{sgr_num});
1118 }
1119 open_span_count += 1;
1120 },
1121 else => return error.UnsupportedEscape,
1122 },
1123 }
1124 }
1125 return try buf.toOwnedSlice();
1126}
1127
1128// Returns true if number is in slice.
1129fn in(slice: []const u8, number: u8) bool {
1130 return mem.indexOfScalar(u8, slice, number) != null;
1131}
1132
1133fn run(
1134 allocator: Allocator,
1135 env_map: *process.EnvMap,
1136 cwd: []const u8,
1137 args: []const []const u8,
1138) !process.Child.RunResult {
1139 const result = try process.Child.run(.{
1140 .allocator = allocator,
1141 .argv = args,
1142 .env_map = env_map,
1143 .cwd = cwd,
1144 .max_output_bytes = max_doc_file_size,
1145 });
1146 switch (result.term) {
1147 .Exited => |exit_code| {
1148 if (exit_code != 0) {
1149 std.debug.print("{s}\nThe following command exited with code {}:\n", .{ result.stderr, exit_code });
1150 dumpArgs(args);
1151 return error.ChildExitError;
1152 }
1153 },
1154 else => {
1155 std.debug.print("{s}\nThe following command crashed:\n", .{result.stderr});
1156 dumpArgs(args);
1157 return error.ChildCrashed;
1158 },
1159 }
1160 return result;
1161}
1162
1163fn printShell(out: *Writer, shell_content: []const u8, escape: bool) !void {
1164 const trimmed_shell_content = mem.trim(u8, shell_content, " \r\n");
1165 try out.writeAll("<figure><figcaption class=\"shell-cap\">Shell</figcaption><pre><samp>");
1166 var cmd_cont: bool = false;
1167 var iter = std.mem.splitScalar(u8, trimmed_shell_content, '\n');
1168 while (iter.next()) |orig_line| {
1169 const line = mem.trimEnd(u8, orig_line, " \r");
1170 if (!cmd_cont and line.len > 1 and mem.eql(u8, line[0..2], "$ ") and line[line.len - 1] != '\\') {
1171 try out.writeAll("$ <kbd>");
1172 const s = std.mem.trimStart(u8, line[1..], " ");
1173 if (escape) {
1174 try writeEscaped(out, s);
1175 } else {
1176 try out.writeAll(s);
1177 }
1178 try out.writeAll("</kbd>" ++ "\n");
1179 } else if (!cmd_cont and line.len > 1 and mem.eql(u8, line[0..2], "$ ") and line[line.len - 1] == '\\') {
1180 try out.writeAll("$ <kbd>");
1181 const s = std.mem.trimStart(u8, line[1..], " ");
1182 if (escape) {
1183 try writeEscaped(out, s);
1184 } else {
1185 try out.writeAll(s);
1186 }
1187 try out.writeAll("\n");
1188 cmd_cont = true;
1189 } else if (line.len > 0 and line[line.len - 1] != '\\' and cmd_cont) {
1190 if (escape) {
1191 try writeEscaped(out, line);
1192 } else {
1193 try out.writeAll(line);
1194 }
1195 try out.writeAll("</kbd>" ++ "\n");
1196 cmd_cont = false;
1197 } else {
1198 if (escape) {
1199 try writeEscaped(out, line);
1200 } else {
1201 try out.writeAll(line);
1202 }
1203 try out.writeAll("\n");
1204 }
1205 }
1206
1207 try out.writeAll("</samp></pre></figure>");
1208}
1209
1210test "term supported colors" {
1211 const test_allocator = testing.allocator;
1212
1213 {
1214 const input = "A\x1b[31;1mred\x1b[0mB";
1215 const expect = "A<span class=\"sgr-31_1m\">red</span>B";
1216
1217 const result = try termColor(test_allocator, input);
1218 defer test_allocator.free(result);
1219 try testing.expectEqualSlices(u8, expect, result);
1220 }
1221
1222 {
1223 const input = "A\x1b[32;1mgreen\x1b[0mB";
1224 const expect = "A<span class=\"sgr-32_1m\">green</span>B";
1225
1226 const result = try termColor(test_allocator, input);
1227 defer test_allocator.free(result);
1228 try testing.expectEqualSlices(u8, expect, result);
1229 }
1230
1231 {
1232 const input = "A\x1b[36;1mcyan\x1b[0mB";
1233 const expect = "A<span class=\"sgr-36_1m\">cyan</span>B";
1234
1235 const result = try termColor(test_allocator, input);
1236 defer test_allocator.free(result);
1237 try testing.expectEqualSlices(u8, expect, result);
1238 }
1239
1240 {
1241 const input = "A\x1b[1mbold\x1b[0mB";
1242 const expect = "A<span class=\"sgr-1m\">bold</span>B";
1243
1244 const result = try termColor(test_allocator, input);
1245 defer test_allocator.free(result);
1246 try testing.expectEqualSlices(u8, expect, result);
1247 }
1248
1249 {
1250 const input = "A\x1b[2mdim\x1b[0mB";
1251 const expect = "A<span class=\"sgr-2m\">dim</span>B";
1252
1253 const result = try termColor(test_allocator, input);
1254 defer test_allocator.free(result);
1255 try testing.expectEqualSlices(u8, expect, result);
1256 }
1257}
1258
1259test "term output from zig" {
1260 // Use data generated by https://github.com/perillo/zig-tty-test-data,
1261 // with zig version 0.11.0-dev.1898+36d47dd19.
1262 const test_allocator = testing.allocator;
1263
1264 {
1265 // 1.1-with-build-progress.out
1266 const input = "Semantic Analysis [1324] \x1b[25D\x1b[0KLLVM Emit Object... \x1b[20D\x1b[0KLLVM Emit Object... \x1b[20D\x1b[0KLLD Link... \x1b[12D\x1b[0K";
1267 const expect = "";
1268
1269 const result = try termColor(test_allocator, input);
1270 defer test_allocator.free(result);
1271 try testing.expectEqualSlices(u8, expect, result);
1272 }
1273
1274 {
1275 // 2.1-with-reference-traces.out
1276 const input = "\x1b[1msrc/2.1-with-reference-traces.zig:3:7: \x1b[31;1merror: \x1b[0m\x1b[1mcannot assign to constant\n\x1b[0m x += 1;\n \x1b[32;1m~~^~~~\n\x1b[0m\x1b[0m\x1b[2mreferenced by:\n main: src/2.1-with-reference-traces.zig:7:5\n callMain: /usr/local/lib/zig/lib/std/start.zig:607:17\n remaining reference traces hidden; use '-freference-trace' to see all reference traces\n\n\x1b[0m";
1277 const expect =
1278 \\<span class="sgr-1m">src/2.1-with-reference-traces.zig:3:7: </span><span class="sgr-31_1m">error: </span><span class="sgr-1m">cannot assign to constant
1279 \\</span> x += 1;
1280 \\ <span class="sgr-32_1m">~~^~~~
1281 \\</span><span class="sgr-2m">referenced by:
1282 \\ main: src/2.1-with-reference-traces.zig:7:5
1283 \\ callMain: /usr/local/lib/zig/lib/std/start.zig:607:17
1284 \\ remaining reference traces hidden; use '-freference-trace' to see all reference traces
1285 \\
1286 \\</span>
1287 ;
1288
1289 const result = try termColor(test_allocator, input);
1290 defer test_allocator.free(result);
1291 try testing.expectEqualSlices(u8, expect, result);
1292 }
1293
1294 {
1295 // 2.2-without-reference-traces.out
1296 const input = "\x1b[1m/usr/local/lib/zig/lib/std/io/fixed_buffer_stream.zig:128:29: \x1b[31;1merror: \x1b[0m\x1b[1minvalid type given to fixedBufferStream\n\x1b[0m else => @compileError(\"invalid type given to fixedBufferStream\"),\n \x1b[32;1m^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\x1b[0m\x1b[1m/usr/local/lib/zig/lib/std/io/fixed_buffer_stream.zig:116:66: \x1b[36;1mnote: \x1b[0m\x1b[1mcalled from here\n\x1b[0mpub fn fixedBufferStream(buffer: anytype) FixedBufferStream(Slice(@TypeOf(buffer))) {\n; \x1b[32;1m~~~~~^~~~~~~~~~~~~~~~~\n\x1b[0m";
1297 const expect =
1298 \\<span class="sgr-1m">/usr/local/lib/zig/lib/std/io/fixed_buffer_stream.zig:128:29: </span><span class="sgr-31_1m">error: </span><span class="sgr-1m">invalid type given to fixedBufferStream
1299 \\</span> else => @compileError("invalid type given to fixedBufferStream"),
1300 \\ <span class="sgr-32_1m">^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1301 \\</span><span class="sgr-1m">/usr/local/lib/zig/lib/std/io/fixed_buffer_stream.zig:116:66: </span><span class="sgr-36_1m">note: </span><span class="sgr-1m">called from here
1302 \\</span>pub fn fixedBufferStream(buffer: anytype) FixedBufferStream(Slice(@TypeOf(buffer))) {
1303 \\; <span class="sgr-32_1m">~~~~~^~~~~~~~~~~~~~~~~
1304 \\</span>
1305 ;
1306
1307 const result = try termColor(test_allocator, input);
1308 defer test_allocator.free(result);
1309 try testing.expectEqualSlices(u8, expect, result);
1310 }
1311
1312 {
1313 // 2.3-with-notes.out
1314 const input = "\x1b[1msrc/2.3-with-notes.zig:6:9: \x1b[31;1merror: \x1b[0m\x1b[1mexpected type '*2.3-with-notes.Derp', found '*2.3-with-notes.Wat'\n\x1b[0m bar(w);\n \x1b[32;1m^\n\x1b[0m\x1b[1msrc/2.3-with-notes.zig:6:9: \x1b[36;1mnote: \x1b[0m\x1b[1mpointer type child '2.3-with-notes.Wat' cannot cast into pointer type child '2.3-with-notes.Derp'\n\x1b[0m\x1b[1msrc/2.3-with-notes.zig:2:13: \x1b[36;1mnote: \x1b[0m\x1b[1mopaque declared here\n\x1b[0mconst Wat = opaque {};\n \x1b[32;1m^~~~~~~~~\n\x1b[0m\x1b[1msrc/2.3-with-notes.zig:1:14: \x1b[36;1mnote: \x1b[0m\x1b[1mopaque declared here\n\x1b[0mconst Derp = opaque {};\n \x1b[32;1m^~~~~~~~~\n\x1b[0m\x1b[1msrc/2.3-with-notes.zig:4:18: \x1b[36;1mnote: \x1b[0m\x1b[1mparameter type declared here\n\x1b[0mextern fn bar(d: *Derp) void;\n \x1b[32;1m^~~~~\n\x1b[0m\x1b[0m\x1b[2mreferenced by:\n main: src/2.3-with-notes.zig:10:5\n callMain: /usr/local/lib/zig/lib/std/start.zig:607:17\n remaining reference traces hidden; use '-freference-trace' to see all reference traces\n\n\x1b[0m";
1315 const expect =
1316 \\<span class="sgr-1m">src/2.3-with-notes.zig:6:9: </span><span class="sgr-31_1m">error: </span><span class="sgr-1m">expected type '*2.3-with-notes.Derp', found '*2.3-with-notes.Wat'
1317 \\</span> bar(w);
1318 \\ <span class="sgr-32_1m">^
1319 \\</span><span class="sgr-1m">src/2.3-with-notes.zig:6:9: </span><span class="sgr-36_1m">note: </span><span class="sgr-1m">pointer type child '2.3-with-notes.Wat' cannot cast into pointer type child '2.3-with-notes.Derp'
1320 \\</span><span class="sgr-1m">src/2.3-with-notes.zig:2:13: </span><span class="sgr-36_1m">note: </span><span class="sgr-1m">opaque declared here
1321 \\</span>const Wat = opaque {};
1322 \\ <span class="sgr-32_1m">^~~~~~~~~
1323 \\</span><span class="sgr-1m">src/2.3-with-notes.zig:1:14: </span><span class="sgr-36_1m">note: </span><span class="sgr-1m">opaque declared here
1324 \\</span>const Derp = opaque {};
1325 \\ <span class="sgr-32_1m">^~~~~~~~~
1326 \\</span><span class="sgr-1m">src/2.3-with-notes.zig:4:18: </span><span class="sgr-36_1m">note: </span><span class="sgr-1m">parameter type declared here
1327 \\</span>extern fn bar(d: *Derp) void;
1328 \\ <span class="sgr-32_1m">^~~~~
1329 \\</span><span class="sgr-2m">referenced by:
1330 \\ main: src/2.3-with-notes.zig:10:5
1331 \\ callMain: /usr/local/lib/zig/lib/std/start.zig:607:17
1332 \\ remaining reference traces hidden; use '-freference-trace' to see all reference traces
1333 \\
1334 \\</span>
1335 ;
1336
1337 const result = try termColor(test_allocator, input);
1338 defer test_allocator.free(result);
1339 try testing.expectEqualSlices(u8, expect, result);
1340 }
1341
1342 {
1343 // 3.1-with-error-return-traces.out
1344
1345 const input = "error: Error\n\x1b[1m/home/zig/src/3.1-with-error-return-traces.zig:5:5\x1b[0m: \x1b[2m0x20b008 in callee (3.1-with-error-return-traces)\x1b[0m\n return error.Error;\n \x1b[32;1m^\x1b[0m\n\x1b[1m/home/zig/src/3.1-with-error-return-traces.zig:9:5\x1b[0m: \x1b[2m0x20b113 in caller (3.1-with-error-return-traces)\x1b[0m\n try callee();\n \x1b[32;1m^\x1b[0m\n\x1b[1m/home/zig/src/3.1-with-error-return-traces.zig:13:5\x1b[0m: \x1b[2m0x20b153 in main (3.1-with-error-return-traces)\x1b[0m\n try caller();\n \x1b[32;1m^\x1b[0m\n";
1346 const expect =
1347 \\error: Error
1348 \\<span class="sgr-1m">/home/zig/src/3.1-with-error-return-traces.zig:5:5</span>: <span class="sgr-2m">0x20b008 in callee (3.1-with-error-return-traces)</span>
1349 \\ return error.Error;
1350 \\ <span class="sgr-32_1m">^</span>
1351 \\<span class="sgr-1m">/home/zig/src/3.1-with-error-return-traces.zig:9:5</span>: <span class="sgr-2m">0x20b113 in caller (3.1-with-error-return-traces)</span>
1352 \\ try callee();
1353 \\ <span class="sgr-32_1m">^</span>
1354 \\<span class="sgr-1m">/home/zig/src/3.1-with-error-return-traces.zig:13:5</span>: <span class="sgr-2m">0x20b153 in main (3.1-with-error-return-traces)</span>
1355 \\ try caller();
1356 \\ <span class="sgr-32_1m">^</span>
1357 \\
1358 ;
1359
1360 const result = try termColor(test_allocator, input);
1361 defer test_allocator.free(result);
1362 try testing.expectEqualSlices(u8, expect, result);
1363 }
1364
1365 {
1366 // 3.2-with-stack-trace.out
1367 const input = "\x1b[1m/usr/local/lib/zig/lib/std/debug.zig:561:19\x1b[0m: \x1b[2m0x22a107 in writeCurrentStackTrace__anon_5898 (3.2-with-stack-trace)\x1b[0m\n while (it.next()) |return_address| {\n \x1b[32;1m^\x1b[0m\n\x1b[1m/usr/local/lib/zig/lib/std/debug.zig:157:80\x1b[0m: \x1b[2m0x20bb23 in dumpCurrentStackTrace (3.2-with-stack-trace)\x1b[0m\n writeCurrentStackTrace(stderr, debug_info, detectTTYConfig(io.getStdErr()), start_addr) catch |err| {\n \x1b[32;1m^\x1b[0m\n\x1b[1m/home/zig/src/3.2-with-stack-trace.zig:5:36\x1b[0m: \x1b[2m0x20d3b2 in foo (3.2-with-stack-trace)\x1b[0m\n std.debug.dumpCurrentStackTrace(null);\n \x1b[32;1m^\x1b[0m\n\x1b[1m/home/zig/src/3.2-with-stack-trace.zig:9:8\x1b[0m: \x1b[2m0x20b458 in main (3.2-with-stack-trace)\x1b[0m\n foo();\n \x1b[32;1m^\x1b[0m\n\x1b[1m/usr/local/lib/zig/lib/std/start.zig:607:22\x1b[0m: \x1b[2m0x20a965 in posixCallMainAndExit (3.2-with-stack-trace)\x1b[0m\n root.main();\n \x1b[32;1m^\x1b[0m\n\x1b[1m/usr/local/lib/zig/lib/std/start.zig:376:5\x1b[0m: \x1b[2m0x20a411 in _start (3.2-with-stack-trace)\x1b[0m\n @call(.never_inline, posixCallMainAndExit, .{});\n \x1b[32;1m^\x1b[0m\n";
1368 const expect =
1369 \\<span class="sgr-1m">/usr/local/lib/zig/lib/std/debug.zig:561:19</span>: <span class="sgr-2m">0x22a107 in writeCurrentStackTrace__anon_5898 (3.2-with-stack-trace)</span>
1370 \\ while (it.next()) |return_address| {
1371 \\ <span class="sgr-32_1m">^</span>
1372 \\<span class="sgr-1m">/usr/local/lib/zig/lib/std/debug.zig:157:80</span>: <span class="sgr-2m">0x20bb23 in dumpCurrentStackTrace (3.2-with-stack-trace)</span>
1373 \\ writeCurrentStackTrace(stderr, debug_info, detectTTYConfig(io.getStdErr()), start_addr) catch |err| {
1374 \\ <span class="sgr-32_1m">^</span>
1375 \\<span class="sgr-1m">/home/zig/src/3.2-with-stack-trace.zig:5:36</span>: <span class="sgr-2m">0x20d3b2 in foo (3.2-with-stack-trace)</span>
1376 \\ std.debug.dumpCurrentStackTrace(null);
1377 \\ <span class="sgr-32_1m">^</span>
1378 \\<span class="sgr-1m">/home/zig/src/3.2-with-stack-trace.zig:9:8</span>: <span class="sgr-2m">0x20b458 in main (3.2-with-stack-trace)</span>
1379 \\ foo();
1380 \\ <span class="sgr-32_1m">^</span>
1381 \\<span class="sgr-1m">/usr/local/lib/zig/lib/std/start.zig:607:22</span>: <span class="sgr-2m">0x20a965 in posixCallMainAndExit (3.2-with-stack-trace)</span>
1382 \\ root.main();
1383 \\ <span class="sgr-32_1m">^</span>
1384 \\<span class="sgr-1m">/usr/local/lib/zig/lib/std/start.zig:376:5</span>: <span class="sgr-2m">0x20a411 in _start (3.2-with-stack-trace)</span>
1385 \\ @call(.never_inline, posixCallMainAndExit, .{});
1386 \\ <span class="sgr-32_1m">^</span>
1387 \\
1388 ;
1389
1390 const result = try termColor(test_allocator, input);
1391 defer test_allocator.free(result);
1392 try testing.expectEqualSlices(u8, expect, result);
1393 }
1394}
1395
1396test "printShell" {
1397 const test_allocator = std.testing.allocator;
1398
1399 {
1400 const shell_out =
1401 \\$ zig build test.zig
1402 ;
1403 const expected =
1404 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
1405 \\</samp></pre></figure>
1406 ;
1407
1408 var buffer: Writer.Allocating = .init(test_allocator);
1409 defer buffer.deinit();
1410
1411 try printShell(&buffer.writer, shell_out, false);
1412 try testing.expectEqualSlices(u8, expected, buffer.written());
1413 }
1414 {
1415 const shell_out =
1416 \\$ zig build test.zig
1417 \\build output
1418 ;
1419 const expected =
1420 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
1421 \\build output
1422 \\</samp></pre></figure>
1423 ;
1424
1425 var buffer: Writer.Allocating = .init(test_allocator);
1426 defer buffer.deinit();
1427
1428 try printShell(&buffer.writer, shell_out, false);
1429 try testing.expectEqualSlices(u8, expected, buffer.written());
1430 }
1431 {
1432 const shell_out = "$ zig build test.zig\r\nbuild output\r\n";
1433 const expected =
1434 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
1435 \\build output
1436 \\</samp></pre></figure>
1437 ;
1438
1439 var buffer: Writer.Allocating = .init(test_allocator);
1440 defer buffer.deinit();
1441
1442 try printShell(&buffer.writer, shell_out, false);
1443 try testing.expectEqualSlices(u8, expected, buffer.written());
1444 }
1445 {
1446 const shell_out =
1447 \\$ zig build test.zig
1448 \\build output
1449 \\$ ./test
1450 ;
1451 const expected =
1452 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
1453 \\build output
1454 \\$ <kbd>./test</kbd>
1455 \\</samp></pre></figure>
1456 ;
1457
1458 var buffer: Writer.Allocating = .init(test_allocator);
1459 defer buffer.deinit();
1460
1461 try printShell(&buffer.writer, shell_out, false);
1462 try testing.expectEqualSlices(u8, expected, buffer.written());
1463 }
1464 {
1465 const shell_out =
1466 \\$ zig build test.zig
1467 \\
1468 \\$ ./test
1469 \\output
1470 ;
1471 const expected =
1472 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
1473 \\
1474 \\$ <kbd>./test</kbd>
1475 \\output
1476 \\</samp></pre></figure>
1477 ;
1478
1479 var buffer: Writer.Allocating = .init(test_allocator);
1480 defer buffer.deinit();
1481
1482 try printShell(&buffer.writer, shell_out, false);
1483 try testing.expectEqualSlices(u8, expected, buffer.written());
1484 }
1485 {
1486 const shell_out =
1487 \\$ zig build test.zig
1488 \\$ ./test
1489 \\output
1490 ;
1491 const expected =
1492 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
1493 \\$ <kbd>./test</kbd>
1494 \\output
1495 \\</samp></pre></figure>
1496 ;
1497
1498 var buffer: Writer.Allocating = .init(test_allocator);
1499 defer buffer.deinit();
1500
1501 try printShell(&buffer.writer, shell_out, false);
1502 try testing.expectEqualSlices(u8, expected, buffer.written());
1503 }
1504 {
1505 const shell_out =
1506 \\$ zig build test.zig \
1507 \\ --build-option
1508 \\build output
1509 \\$ ./test
1510 \\output
1511 ;
1512 const expected =
1513 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig \
1514 \\ --build-option</kbd>
1515 \\build output
1516 \\$ <kbd>./test</kbd>
1517 \\output
1518 \\</samp></pre></figure>
1519 ;
1520
1521 var buffer: Writer.Allocating = .init(test_allocator);
1522 defer buffer.deinit();
1523
1524 try printShell(&buffer.writer, shell_out, false);
1525 try testing.expectEqualSlices(u8, expected, buffer.written());
1526 }
1527 {
1528 // intentional space after "--build-option1 \"
1529 const shell_out =
1530 \\$ zig build test.zig \
1531 \\ --build-option1 \
1532 \\ --build-option2
1533 \\$ ./test
1534 ;
1535 const expected =
1536 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig \
1537 \\ --build-option1 \
1538 \\ --build-option2</kbd>
1539 \\$ <kbd>./test</kbd>
1540 \\</samp></pre></figure>
1541 ;
1542
1543 var buffer: Writer.Allocating = .init(test_allocator);
1544 defer buffer.deinit();
1545
1546 try printShell(&buffer.writer, shell_out, false);
1547 try testing.expectEqualSlices(u8, expected, buffer.written());
1548 }
1549 {
1550 const shell_out =
1551 \\$ zig build test.zig \
1552 \\$ ./test
1553 ;
1554 const expected =
1555 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig \
1556 \\$ ./test</kbd>
1557 \\</samp></pre></figure>
1558 ;
1559
1560 var buffer: Writer.Allocating = .init(test_allocator);
1561 defer buffer.deinit();
1562
1563 try printShell(&buffer.writer, shell_out, false);
1564 try testing.expectEqualSlices(u8, expected, buffer.written());
1565 }
1566 {
1567 const shell_out =
1568 \\$ zig build test.zig
1569 \\$ ./test
1570 \\$1
1571 ;
1572 const expected =
1573 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
1574 \\$ <kbd>./test</kbd>
1575 \\$1
1576 \\</samp></pre></figure>
1577 ;
1578
1579 var buffer: Writer.Allocating = .init(test_allocator);
1580 defer buffer.deinit();
1581
1582 try printShell(&buffer.writer, shell_out, false);
1583 try testing.expectEqualSlices(u8, expected, buffer.written());
1584 }
1585 {
1586 const shell_out =
1587 \\$zig build test.zig
1588 ;
1589 const expected =
1590 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$zig build test.zig
1591 \\</samp></pre></figure>
1592 ;
1593
1594 var buffer: Writer.Allocating = .init(test_allocator);
1595 defer buffer.deinit();
1596
1597 try printShell(&buffer.writer, shell_out, false);
1598 try testing.expectEqualSlices(u8, expected, buffer.written());
1599 }
1600}