使用断点调试测试
问题
在调试 Erlang 应用程序时,经常需要额外的输出以了解正在发生的事情。
执行此操作的标准方法包括
- 使用
io:format/2-3
(或在通用测试中使用ct:pal/2
)添加打印语句调试 - 使用 OTP 进程打开
sys
模块跟踪以显示更多输出 - 尝试附加一个调试器
调试器的问题在于,在涉及计时器的运行系统中正确观察正在发生的事情可能非常困难;类似地,当您无法访问交互式会话,也无法以任何方式与测试框架执行同步以在正确区域附加时,很难使用任何更高级的工具(例如在比 OTP 交互更细粒度的级别进行跟踪)。
在本文件中,我们将看到两种可以使用 3.7.0 版本中引入的新断点功能启用的模式:启用跟踪探针的断点,以及在某些代码的关键部分进行探测的断点。
👍
断点对于许多类型的测试很有用
此页面显示了与通用测试一起使用的断点,但它们可以与任何提供程序一起使用,包括 QuickCheck 或 PropEr 的插件,开箱即用。如果您是插件开发人员,您也应该能够使用这些断点。它们只会触发一次安全地从 shell 执行异步任务,否则将被忽略。这应该可以防止插件需要在 shell 本身启动之前运行,或者用户可能意外挂起构建的任何问题。
基本机制
断点功能需要以下步骤
- 测试从具有
rebar3 shell
的交互式 shell 运行,并使用异步模式。这意味着,您需要调用r3:async_do(ct)
或r3:async_do(eunit)
,而不是为通用测试调用r3:ct()
或为 EUnit 测试调用r3:eunit()
(或其更长的形式r3:do(Command)
)。 - 您必须从要执行的测试中调用
r3:break()
。这将设置一个断点,该断点将通过等待秘密消息来动态暂停代码。系统其余部分不会暂停,并且各种框架启用的测试超时将保持活动状态。 - 您将可以使用 shell 执行任何操作;会显示一条确认消息,让您知道您处于暂停状态
- 完成调查后,您可以通过调用
r3:resume()
恢复执行。
您的会话可能如下所示
$ rebar3 shell
...
1> r3:async_do(ct).
ok
...
Running Common Test suites...
%%% rebar_alias_SUITE: .
=== BREAK ===
2> % <do some checks>
2> r3:resume().
ok
3> .....
%%% rebar_as_SUITE: ...........
%%% rebar_compile_SUITE: ......
...
请注意,其他调试器已经存在。如果您想要更多功能,可以将它们与异步任务一起使用。有关良好的调试器示例,请参阅https://github.com/hachreak/cedb。
中断到跟踪
在您中断到跟踪的模式中,您可能希望使用像dbg
、redbug
或recon
这样的跟踪工具。在本节中,我们将使用后者。
首先,让我们从一个虚拟项目开始
$ rebar3 new app break_check
===> Writing break_check/src/break_check_app.erl
===> Writing break_check/src/break_check_sup.erl
===> Writing break_check/src/break_check.app.src
===> Writing break_check/rebar.config
===> Writing break_check/.gitignore
===> Writing break_check/LICENSE
===> Writing break_check/README.md
$ cd break_check
打开rebar.config
文件并确保它如下所示
{deps, [recon]}.
{profiles, [
{test, [{erl_opts, [nowarn_export_all]}]}
]}.
{shell, [
% {config, "config/sys.config"},
{apps, [break_check]}
]}.
然后,您可以添加以下测试套件并将其保存在名为test/start_SUITE.erl
的文件下
-module(start_SUITE).
-include_lib("common_test/include/ct.hrl").
-include_lib("stdlib/include/assert.hrl").
-compile(export_all).
all() -> [start].
start(_Config) ->
?assertEqual(ok, application:start(break_check)),
Props = supervisor:count_children(break_check_sup),
?assertEqual(1, proplists:get_value(workers, Props)),
ok.
如果运行测试,您将看到类似以下的错误
$ rebar3 ct
===> Verifying dependencies...
===> Compiling break_check
===> Running Common Test suites...
%%% start_SUITE:
%%% start_SUITE ==> start: FAILED
%%% start_SUITE ==>
Failure/Error: ?assertEqual(1, proplists : get_value ( workers , Props ))
expected: 1
got: 0
line: 11
因此,显然某些内容出错了,因为看到的 worker 比预期的少。
为了弄清楚此时发生了什么,查看代码就足够了,但让我们假设我们不知道为什么不存在子进程。
我们将使用断点来让我们设置跟踪。将测试更改为如下所示
-module(start_SUITE).
-include_lib("common_test/include/ct.hrl").
-include_lib("stdlib/include/assert.hrl").
-compile(export_all).
all() -> [start].
start(_Config) ->
r3:break(), % <===== this is the new line
?assertEqual(ok, application:start(break_check)),
Props = supervisor:count_children(break_check_sup),
?assertEqual(1, proplists:get_value(workers, Props)),
ok.
$ rebar3 shell --start-clean
...
1> r3:async_do(ct).
===> This feature is experimental and may be modified or removed at any time.
ok
2> Verifying dependencies...
Compiling break_check
Running Common Test suites...
%%% start_SUITE:
=== BREAK ===
此时,我们有一个 shell,并且测试已暂停。我们可以设置我们自己的跟踪探针
%% Set up a trace for all functions in the supervisor, returning their result
2> recon_trace:calls({break_check_sup, '_', fun(_) -> return_trace() end}, 10).
0
%% 0 functions matching on a wildcard means the module is likely not loaded,
%% so let's do that
3> l(break_check_sup).
{module,break_check_sup}
%% Try setting the trace call again
4> recon_trace:calls({break_check_sup, '_', fun(_) -> return_trace() end}, 10).
4 % <-- and now we have a match
从这一点开始,我们只需要恢复断点并查看跟踪发生即可
5> r3:resume().
ok
13:34:51.207925 <0.249.0> break_check_sup:start_link()
13:34:51.208075 <0.250.0> break_check_sup:init([])
13:34:51.208197 <0.250.0> break_check_sup:init/1 --> {ok,
{{one_for_all,0,1},[]}}
13:34:51.208350 <0.249.0> break_check_sup:start_link/0 --> {ok,<0.250.0>}
7>
%%% start_SUITE ==> start: FAILED
%%% start_SUITE ==>
Failure/Error: ?assertEqual(1, proplists : get_value ( workers , Props ))
expected: 1
got: 0
line: 12
我们仍然得到相同的错误,但现在我们可以清楚地看到问题在于init/1
函数没有返回任何单个子进程。因此,在主管中找不到任何 worker 也就不足为奇了。
中断到跟踪的本质在于,您希望尽早暂停测试;您知道需要什么信息,您只需要一种获取它的方法。您进行小的观察设置(您不仅可以跟踪,还可以更改日志级别),然后在离开之前获取数据。
中断到探测
此模式类似于前一个模式,但我们将改为与系统交互,对其进行探测,并可能更改一些值以查看事物如何工作。
在本节中,我们将重用与前一个相同的设置,但我们将更改断点的位置和测试
-include_lib("common_test/include/ct.hrl").
-include_lib("stdlib/include/assert.hrl").
-compile(export_all).
all() -> [start].
start(_Config) ->
?assertEqual(ok, application:start(break_check)),
Props = supervisor:count_children(break_check_sup),
case proplists:get_value(workers, Props) of
0 -> r3:break();
_ -> ignore
end,
?assertEqual(1, proplists:get_value(workers, Props)),
ok.
在此版本中,我们不仅在检查子进程后设置断点,而且仅在结果意外时才触发它。当您有一些并非总是失败,只是有时失败的测试时,这很有用。由于断点是正常的函数调用,因此您甚至可以将它们保留在原位,并带有在此时开始调试的指令。
让我们运行测试以查看它是否在我们设置的断点处暂停
$ rebar3 shell --start-clean
...
1> r3:async_do(ct).
===> This feature is experimental and may be modified or removed at any time.
ok
2> Verifying dependencies...
Compiling break_check
Running Common Test suites...
%%% start_SUITE:
=== BREAK ===
现在我们可以查看系统。应用程序已启动,主管正在运行,我们可以操作某些内容,例如启动一个假的子进程以查看这是否可以修复测试
%% Poke at the supervisor's state and see it has no children
2> sys:get_state(break_check_sup).
{state,{local,break_check_sup},
one_for_all,
{[],#{}},
undefined,0,1,[],0,break_check_sup,[]}
%% Add a child by hand
3> supervisor:start_child(
3> break_check_sup,
3> #{id => some_child,
3> start => {gen_event, start_link, []},
3> type => worker}
3> ).
{ok,<0.232.0>}
%% Check that it is actually tracked
4> sys:get_state(break_check_sup).
{state,{local,break_check_sup},
one_for_all,
{[some_child],
#{some_child =>
{child,<0.232.0>,some_child,
{gen_event,start_link,[]},
permanent,5000,worker,
[gen_event]}}},
undefined,0,1,[],0,break_check_sup,[]}
%% and keep going to see the test pass
5> r3:resume().
ok
All 1 tests passed.
最好在完成断点后将其全部移除,但有趣的是,您可以使用诸如通用测试的使用组进行重复执行或随机运行等功能,以检测奇怪的条件并在需要时自动触发断点。