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