使用 IEx 调试
学习 Elixir 中的各种调试方法。
我们预期一种相对较新的语言会自带一套相对简单的工具集。毕竟,开发团队会乐于探索这种语言。但 Elixir 并非如此。从一开始,工具支持就至关重要,核心团队投入了大量时间,打造了一个世界级的开发环境。
在本章简短的内容中,我们将探讨其中的一些方面。本章并非完整列表。我们已经见识过 ExDoc 工具,它能为我们的代码生成精美的文档。稍后,在学习 OTP 应用时,我们会尝试使用 Elixir 的发布管理器,这是一个在应用持续运行的同时管理发布的工具。现在,让我们先关注测试、代码探索和服务器监控工具。
我们已经知道,IEx 是用于试玩 Elixir 代码的首选工具。它还有一个鲜为人知的第二用途:调试器。虽然不够花哨,但它允许我们进入正在运行的程序,检查当前环境。
实现
当运行中的 Elixir 代码到达断点时,我们就进入了调试模式。有两种方式设置断点:一种是在我们想要调试的代码中添加调用;另一种则是在 IEx 内部触发。我们将通过以下存在错误的代码来分别演示这两种方法:
No configuration found for ID: e58d6c1cb9306b9cc088bb920f2b16fb82f5b0f05ffdd19152da65a183198025
这段代码旨在解析 MIDI 头部帧的数据部分。该部分包含三个 16 位字段:格式、轨道数量和时间刻度。最后一个字段有两种格式,如下所示:
parse_header/1 函数将整个头部拆分为这三个字段,而 decode/1 函数则判断时间刻度的具体类型。
让我们运行这段代码,使用从 MIDI 文件中提取的一个示例头部数据:
1iex> header = << 0, 1, 0, 8, 0, 120 >> 2<<0, 1, 0, 8, 0, 120>> 3iex> Buggy.parse_header header 4format: 1 5tracks: 8 6** (FunctionClauseError) no function clause matching in Buggy.decode/1
执行 header = << 0, 1, 0, 8, 0, 120 >> 和 Buggy.parse_header header 命令以运行上述代码。
糟糕!这完全出乎意料。看起来我们传给 decode 的参数不正确。让我们使用调试器找出问题所在。
使用 IEx.pry 注入断点
我们可以通过 pry 函数在源码中插入断点。例如,要在调用 decode 之前暂停代码,可以这样写:
1defmodule Buggy do 2 def parse_header( 3 << 4 format::integer-16, 5 tracks::integer-16, 6 division::integer-16 7 >> 8 ) do 9 10 require IEx; IEx.pry 11 IO.puts "format: #{format}" 12 IO.puts "tracks: #{tracks}" 13 IO.puts "division: #{decode(division)}" 14 end
注意: 我们需要
require,因为pry是一个宏。
现在尝试运行代码:
1iex> Buggy.parse_header << 0, 1, 0, 8, 0, 120 >> 2Break reached: Buggy.parse_header/1 (lib/buggy.ex:11) 3 9: 4 10: require IEx; IEx.pry 5 11: IO.puts "format: #{format}" 6pry> binding 7[division: 120, format: 1, tracks: 8] 8iex> continue() 9format: 1 10tracks: 8 11** (FunctionClauseError) no function clause matching in Buggy.decode/1
我们成功到达了断点,IEx 进入了 pry 模式。它显示了当前所在的函数以及断点附近的源码行。
执行 Buggy.parse_header << 0, 1, 0, 8, 0, 120 >>、binding 和 continue() 命令以运行以下代码:
No configuration found for ID: 4215bf4494e40c73215a07e9fcb09b349c620a5e1e2b4355292058337268284f
此时,IEx 正在该函数的上下文中运行,因此调用 binding 可以查看局部变量。division 的值是 120,但这与 decode 函数的任何参数都不匹配。
原来,decode 函数期望的是一个二进制数据,而不是整数。我们来修复代码:
1def parse_header( 2 << 3 format::integer-16, 4 tracks::integer-16, 5 division::bits-16 6 >> 7) do 8...
pry 调用仍然存在,因此重新编译并再次尝试:
1iex> Buggy.parse_header << 0, 1, 0, 8, 0, 120 >> 2Break reached: Buggy.parse_header/1 (lib/buggy.ex:12) 3 10: )do 4 11: 5 12: require IEx; IEx.pry 6 13: IO.puts "format: #{format}" 7 14: IO.puts "tracks: #{tracks}" 8 9pry> binding 10[division: <<0, 120>>, format: 1, tracks: 8] 11pry> continue 12format: 1 13tracks: 8 14division: 0 fps, 120/frame 15:ok
执行 Buggy.parse_header << 0, 1, 0, 8, 0, 120 >>、binding 和 continue 命令以运行以下代码:
No configuration found for ID: cb4fb7723a0fe795c6699fd95d92bca85f3c3560075579576cadb9a1cd1399d1
</webpackbin
现在,division 是一个二进制数据,当我们继续执行时,代码正常运行并输出了头部字段。但问题是,它把时间刻度误认为是 SMPTE 格式,而非每四分音符的节拍格式。
使用 break 设置断点
第二种设置断点的方式无需修改代码。我们可以在 IEx 中使用 break! 命令,在任意公开函数上添加断点。我们先移除 pry 调用,重新运行代码。然后在 IEx 中对 decode 函数设置断点:
1iex> require IEx 2IEx 3iex> break! Buggy.decode/1 41 5iex> breaks 6 ID Module.function/arity Pending stops 7---- ----------------------- --------------- 8 91 Buggy.decode/1 1 10iex> Buggy.parse_header << 0, 1, 0, 8, 0, 120 >> 11format: 1 12tracks: 8 13Break reached: Buggy.decode/1 (lib/buggy.ex:21) 14 19: end 15 20: 16 21: def decode(<< 0::1, fps::7, beats::8 >>) do 17 22: "#{-fps} fps, #{beats}/frame" 18 23: end 19pry> binding 20[beats: 120, fps: 0]
我们命中了断点,确实,当传入 0000000001111000 时,我们匹配到了错误的 decode 函数版本。这是因为我们根据最高位的值进行判断,但方向弄反了。正确的 SMPTE 版本应为:
1def decode(<< 1::1, fps::7, beats::8 >>) do
而每四分音符节拍版本应为:
1def decode(<< 0::1, beats::15 >>) do
调试器还有更多功能。我们可以从 IEx.break/4 的帮助信息开始了解。