文章摘要
文章探讨了如何改进基于打印的调试方法,特别是在Zig语言中利用errdefer在测试失败时打印相关信息,从而避免输出冗余信息。此外,文章还提到在需要更复杂调试时,使用调试器是更好的选择。
文章总结
标题:Zig 错误处理模式 ~ glfmn.io
引言 尽管我尝试充分利用调试器,但我仍然习惯于基于打印的调试方法,尤其是在单元测试中。我希望探索一些技巧来改进基于打印的调试,并更多地结合调试器的使用。
改进基于打印的调试 使用打印调试的一个主要问题是输出信息过于冗杂。例如,如果我在循环中运行某些代码,而只有一次迭代有值得关注的内容,我仍然需要过滤和整理每次迭代的输出。或者,如果我正在处理某个数据结构,其可打印的表示形式比原始数据更易于解析,但如果不清楚错误来源,我可能不得不在代码中到处插入打印函数,以捕捉必要的上下文。
然而,我意识到由于 Zig 测试使用 error 而不是 panic,因此可以使用 errdefer 仅在测试实际失败时打印信息。例如:
zig
test {
errdefer std.debug.print("{f}", .{ast});
// ...
}
这种方法可以避免代码混乱,并在错误发生时提供精确的上下文。
在调试器中运行测试
如果需要处理更复杂的情况,使用调试器是更好的选择。然而,直接从终端运行 seergdb 或 gdb -tui 会变得困难,因为测试二进制文件不在 zig-out 目录中,而是在 zig-cache 目录中。
我从 ziggit 学到了一个技巧,即 build.zig 可以运行命令,并将构建步骤的产物路径作为参数传递给命令:
```zig
const debugger = b.addSystemCommand(&.{ "seergdb", "--run", "--" });
debugger.addArtifactArg(exeunittests);
const debugstep = b.step("debug", "Run unit tests under debugger"); debugstep.dependOn(&debugger.step); ``` 这使得运行正确的二进制文件变得非常容易。然而,这还不够:调试器仅在遇到断点或 panic 时才会启动,而测试运行器会优雅地处理错误。
我们可以通过添加 @breakpoint 调用来解决这个问题,但这可能会导致我们回到原点,即必须推测性地添加断点并捕捉失败的确切时刻,或者手动逐步执行直到失败。
结合技巧
一个好的解决方案是结合使用 errdefer 和 @breakpoint:
zig
test {
errdefer @breakpoint();
}
这将在遇到错误时精确地中断程序。此时,调试器中可能仍有足够的上下文可供查看,并且我们还可以利用 std.testing.expect{.*} 函数提供的调试打印信息。
然而,这种方法也有缺点。例如,考虑以下程序: ```zig test "errdefer @breakpoint()" { errdefer @breakpoint(); return error.FixMe; }
test "normal test" {
return error.FixMe;
}
``
如果我们运行zig build test`,测试总结只会报告整个测试步骤失败,而不是单独报告每个测试的错误。
条件编译
Zig 构建系统允许我们使用构建选项来传递编译时值。我们可以利用这些选项来传递一个 bool 值,以决定何时在测试中调用 @breakpoint。
首先,从一个简单的构建脚本开始,该脚本仅运行测试: ```zig const std = @import("std");
pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{});
const lib = b.addModule("zig-test-patterns", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
const mod_tests = b.addTest(.{
.root_module = lib,
});
const run_mod_tests = b.addRunArtifact(mod_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
}
我们可以添加编译时选项:
zig
const options = b.addOptions();
options.addOption(bool, "debugger", false);
然后,将生成的选项模块添加到我们的库中:
zig
lib.addImport("config", options.createModule());
在 `root.zig` 文件中,我们可以引用这些选项:
zig
const std = @import("std");
const config = @import("config");
test "errdefer @breakpoint()" { errdefer if (config.debugger) @breakpoint(); return error.FixMe; }
test "no breakpoint" {
return error.FixMe;
}
现在,我们可以运行测试并获得没有断点的行为:
zig build test
然而,我们遇到了一个问题:这个值对我们的程序是可观察的,但我们必须重新编译 `build.zig` 才能更改该值。我们需要将该选项添加到构建系统本身:
zig
var options = b.addOptions();
const usedebugger = b.option(
bool,
"debugger",
"Enables code intended to only run under a debugger",
) orelse false;
options.addOption(bool, "debugger", usedebugger);
现在我们可以运行:
zig build -Ddebugger test
```
并观察行为的变化。
作为最后一步,我们可以在 use_debugger 标志设置时,将命令连接到调试器中运行:
```zig
const teststep = b.step("test", "Run tests");
if (usedebugger) {
const debugger = b.addSystemCommand(&.{ "seergdb", "--run", "--" });
debugger.addArtifactArg(mod_tests);
test_step.dependOn(&debugger.step);
} else {
teststep.dependOn(&runmod_tests.step);
}
``
现在,如果我们使用-Ddebugger` 标志运行,测试运行器将自动在调试器中运行,并在遇到错误时中断!
Zig 版本
本指南基于 Zig 版本:
0.15.0-dev.1184+c41ac8f19
特别感谢 Alanza 阅读了本文的早期草稿。
评论总结
网站设计与排版
- 多位评论者赞赏网站的设计和排版,认为其美观且易于阅读。
- 引用:
- "The website design is so pleasing, props!" (ijustlovemath)
- "I love the formatting and coloring on this blog page, it's delightful to read. Like an old school DOS game." (rkagerer)
Zig语言设计的简洁性与一致性
- 评论者认为Zig语言的设计简洁且一致,特别是
errdefer模式和核心构建块的设计。 - 引用:
- "This goes to show how Zig's language design makes everything look nicer and simpler - the
errdeferpatterns in tests are super nice!" (etyp) - "It's amazing how coherent Zig's fundamental building blocks are as a programming language, everything fits together like a puzzle." (vrnvu)
- "This goes to show how Zig's language design makes everything look nicer and simpler - the
- 评论者认为Zig语言的设计简洁且一致,特别是
调试与构建工具的改进
- 评论者提到Zig的调试和构建工具(如
build.zig中的调试器集成)简化了开发流程。 - 引用:
- "I especially like the debugger integration in build.zig. I used to grep the cache directory to find the exe. The integration avoids all the extra steps." (ww520)
- 评论者提到Zig的调试和构建工具(如
错误处理的局限性
- 有评论者指出Zig的错误处理在某些场景下(如JSON解析)可能不够详细,缺乏有效载荷信息。
- 引用:
- "For example, when parsing a JSON string, the error
UnexpectedTokenis not very helpful." (davidkunz)
- "For example, when parsing a JSON string, the error
代码风格的不一致性
- 有评论者对Zig代码中混合使用驼峰式和蛇形命名表示不适,认为这可能导致混淆。
- 引用:
- "Cool stuff, but the mixed casings I see here (and have in other Zig code) puts me on edge." (yahoozoo)
总结:评论者普遍对Zig语言的设计和网站排版表示赞赏,特别是其简洁性和一致性。同时,调试工具的改进也受到好评。然而,错误处理的局限性和代码风格的不一致性引发了一些讨论。