正如死亡和税收一样,错误是不可避免的。然而,以下内容应该可以帮助您避免这些错误。某些示例需要 Perl 5.6.0 或至少是 5.005 的版本。如果要尝试 Emacs 示例,可能还需要安装 Emacs 编辑器。
错误所带来的麻烦
软件开发人员通常都低估了软件测试的重要性。这一现象的根本原因很简单:处理错误很困难!因为错误往往暴露了代码的根本缺陷,所以有时候开发人员甚至会为了几个错误而从头开始重新编写项目的主要部分。
我认为,调试如此重要,以至于至少要为其分配整个项目 30% 的时间。额外的调试时间将导致更好的产品。另一方面,如果为了更快地推出软件而缩短调试时间,那么在软件生成后,您将花上双份的时间来修复那些稍后暴露出的问题。
有三种基本类型的错误:编码错误、文档错误和需求错误。需求错误通常由于需求不严密或缺少需求而导致。文档错误存在于手册或联机帮助中。编码错误是由程序员在实现需求时的错误而引起的。不幸的是,需求错误和文档错误不在本文范围之内,因此,我们只好只讨论如何“检测”、“解决”和 “修复”编码错误了。
调试术语
断点:程序中的一个位置,程序将在这里停止执行,并且控制将返回到调试器。
调试器(调试员):1) 控制调试进程并带有为支持该进程而特别编写的设施的程序;2) 执行调试的人员。
执行堆栈:到目前为止在程序执行期间输入的函数列表。例如,如果主程序调用函数 A,然后函数 A 调用函数 B,则执行堆栈是:主程序 -> A -> B。
单步进入:通过 进入
当前选定的代码行继续执行步骤。如果当前代码行包含函数,则“单步进入”将进入该函数。
单步跳过:跳过当前选定的代码行继续执行。将不考虑代码行的内容而无条件地执行,并且一旦完成,控制即返回到调试器。
观察:变量值发生更改时自动触发的操作。
有关调试方面的其它信息源,请 参阅下面的 参考资料一节。
调试的基本概念
我们已经将编码错误定义成程序员在实现需求时产生的错误。编码错误会导致不正确的程序行为(偏离需求的行为)。因此,程序员在编写或调试程序之前首先应该知道的是程序需求。
调试与狩猎没什么不同。第一步是检测错误(通过观察错误的行为并确认其模式)。在这个阶段,错误只是一些症状。
第二步是解决错误。因为必须要在源代码中消除错误,所以,应该有一个精通程序的人来检查错误,并知道这些错误的根本原因。如果代码理解起来更容易,并且现在的代码没有比当初错误版本中的代码更多,则您可能做对了。
第三步,也是最后一步,是修复错误(请注意“修复”与“解决”是有区别的)。调试程序将源代码更改放入“现场”的生产过程,然后检查它是否正确。如果代码不正确,则表明您没有解决错误,甚至更糟糕的是,可能还引入了新的错误。既然解决错误的目的不应该是引入新错误,请确保在解决错误之后修复每个错误。
要确保迅速找到错误并很好地理解它们,您应该对调试过程中程序使用模块和类在每个主要分支处的操作非常清楚。当然,这要求您对编写代码所用的语言(在我们的示例中是 Perl)有深入的了解。因为存在所有这些需求,所以很难找到好的软件测试人员。
Perl 调试器
Perl 程序员的第一个资源是 Perl 所带的调试器。如您所见,着手使用该调试器是非常容易的。
用调试器运行一个脚本
perl -d program.pl
Perl 调试器自带帮助('h' 或 'h h' 分别用于详细和简短的帮助屏幕)。perldoc perldebug 页面(在命令提示窗口输入 "perldoc perldebug")有更完整的 Perl 调试器描述。
现在,让我们从一个有错误的程序着手,看一下 Perl 调试器是如何工作的。首先,它将尝试打印一个文件的前 20 行。
buggy.pl
#!/usr/bin/perl -w
use strict;
foreach (0..20)
{
my $line = <>;
print "$_ : $line";
}
当它独自运行时,buggy.pl 将失败,并给出消息:"Use of uninitialized value in concatenation (.) at ./buggy.pl line 8, <> line 9."。更神秘的是,它还自己在一行上打印 "9:" 并等待用户输入。
那意味着什么?如果调用了 Perl 调试器,您可能已经找到问题所在了。
首先,让我们证实这个错误是可以重复的。我们将在第 8 行设置一个操作来打印发生错误的 $line,然后再运行程序。
buggy.pl 调试器命令 > perl -d ./buggy.pl buggy.pl
Default die handler restored.
Loading DB routines from perl5db.pl version 1.07
Editor support available.
Enter h or `h h' for help, or `man perldebug' for more help.
main::(./buggy.pl:5): foreach (0..20)
main::(./buggy.pl:6): {
DB<1> use Data::Dumper
DB<2> a 8 print 'The line variable is now ', Dumper $line
装入了 Data::Dumper 模块,以便自动操作可以使用一种美观的输出格式。自动操作被设置成每次到达第 8 行时都执行打印语句。现在,让我们演示一下。
buggy.pl 调试器命令,第 2 部分 DB<3> c
The line variable is now $VAR1 = '#!/usr/bin/perl -w
';
0 : #!/usr/bin/perl -w
The line variable is now $VAR1 = '
';
1 :
The line variable is now $VAR1 = 'use strict;
';
2 : use strict;
The line variable is now $VAR1 = '
';
3 :
The line variable is now $VAR1 = 'foreach (0..20)
';
4 : foreach (0..20)
The line variable is now $VAR1 = '{
';
5 : {
The line variable is now $VAR1 = ' my $line = <>;
';
6 : my $line = <>;
The line variable is now $VAR1 = ' print "$_ : $line";
';
7 : print "$_ : $line";
The line variable is now $VAR1 = '}
';
8 : }
The line variable is now $VAR1 = undef;
Use of uninitialized value in concatenation (.) at ./buggy.pl line 8, <> line 9.
9 :
现在很清楚,没有定义行变量时就会出问题。而且,程序等待更多的输入。再按 11 次回车键产生了以下输出:
buggy.pl 调试器命令,第 3 部分The line variable is now $VAR1 = '
';
10 :
The line variable is now $VAR1 = '
';
11 :
The line variable is now $VAR1 = '
';
12 :
The line variable is now $VAR1 = '
';
13 :
The line variable is now $VAR1 = '
';
14 :
The line variable is now $VAR1 = '
';
15 :
The line variable is now $VAR1 = '
';
16 :
The line variable is now $VAR1 = '
';
17 :
The line variable is now $VAR1 = '
';
18 :
The line variable is now $VAR1 = '
';
19 :
The line variable is now $VAR1 = '
';
20 :
Debugged program terminated. Use q to quit or R to restart,
use O inhibit_exit to avoid stopping after program termination,
h q, h R or h O to get additional info.
DB<3>
到现在为止已经很清楚了,由于即使在不存在行的情况下,程序仍无条件地等待 20 行的输入,所以程序会出错。修复就是要在从 <> filehandle 读取 $line 之后测试它:
buggy.pl fixed
#!/usr/bin/perl -w
use strict;
foreach (0..20)
{
my $line = <>;
last unless defined $line; # exit loop if $line is not defined
print "$_ : $line";
}
如您所见,修复过的程序在所有情况下都可以正确工作!
关于 Perl 调试器的结论
Emacs 编辑器支持 Perl 调试器并使其更易于使用。您可以在 Emacs 中使用 Info(输入 M-x info)来阅读有关 GUD Emacs 的更详细信息。GUD 是与 Perl 调试器一起工作的全局调试方式(当在 Emacs 中编辑 Perl 程序时输入 M-x perldb)。
只需少量工作就可以让 vi 系列的编辑器也能支持 Perl 调试器。有关详细信息,请参阅 perldoc perldebug 页面。有关其它编辑器的信息,请参考每个编辑器的文档。
Perl 内置的调试器是一个强大的工具,可以执行比我们刚刚看到的简单用法复杂得多的任务。但它的确要求使用者具备大量 Perl 专门知识。正因为如此,我们现在要看一些简单些的工具,这些工具将更适合初级和中级 Perl 程序员。
Devel::ptkdb
要使用 Devel::ptkdb 调试器,首先得从 CPAN(请 参阅下面的 参考资料)下载它并将它安装在您的系统上。(某些用户可能还需要安装 Tk 模块,该模块也可以从 CPAN 获得。)就我个人看来,Devel::ptkdb 在 UNIX 系统(如 Linux)上最好用。(虽然在理论上 Devel::ptkdb 并不限于与 UNIX 兼容的系统,但是,我从未听说过有人成功地在 Windows 上使用 Devel::ptkdb。正如一句老话所讲:除了滑雪穿过旋转门之外,任何事都是可能的。)
如果无法让系统管理员为您安装(例如,因为您自己 就是
系统管理员),可以尝试在命令提示行执行以下操作(可能需要以 root 身份执行这些操作):
从 CPAN 安装 Devel::ptkdbperl -MCPAN -e'install Tk'
perl -MCPAN -e'install Devel::ptkdb'
如果是第一次运行 CPAN 安装例程,那么,在回答一些初始问题之后,将自动下载并安装适当的模块。
可以用 ptkdb 调试器运行程序,如下所示(使用我们以前的 buggy.pl 示例):
使用 Devel::ptkdbperl -d:ptkdb buggy.pl buggy.pl
要阅读 Devel::ptkdb 模块的文档,请使用命令 "perldoc Devel::ptkdb"。我们在本文中使用版本 1.1071。(虽然更新的版本可能随时问世,但它们与我们正在使用的版本应该没有很大的不同。)
将出现一个窗口,在该窗口的左侧是程序源代码,右侧是观察过的表达式列表(初始为空)。在 "Enter Expr:" 框中输入字 "$line"。然后单击 "Step Over" 按钮观察程序的执行情况。
"Run" 按钮将运行程序,直到运行完毕或到遇到断点为止。单击源代码清单窗口中的行号可以设置或删除断点。如果选择右侧的 "BrkPts" 选项卡,则可以编辑断点列表,并使它们受变量或函数的制约。(用这种方法设置条件断点非常简单。)
Ptkdb 还有 File、Control、Data、Stack 和 Bookmarks 菜单。这些菜单全部在 perldoc 文档中解释。因为 Ptkdb 使用起来如此方便,所以,它绝对是初级和中级 Perl 程序员的必备工具。甚至对于 Perl 高手,它也很有用(只要他们不告诉任何人他们正在使用那些新款图形界面)。
编写自己的 Perl shell
有时使用调试器显得大材小用。例如,如果要与大型程序的其余部分 隔离
来单独测试某些简单的代码,那么对于这种任务,调试器就显得过于复杂。这时,Perl shell 就派上用场了。
当然还有其它一些有效的方法来实现 Perl shell,但是,我们将要看到的是一种常规的解决方案,该方案非常适用于大多数日常工作。一旦理解了这个工具,就应该可以随意地按照您的需要和喜好来修改它。
以下代码需要 Term::ReadLine 模块。使用几乎与 Devel::ptkdb 相同的方式从 CPAN 下载并安装它。
Perl shell
#!/usr/bin/perl -w
use Term::ReadLine;
use Data::Dumper;
my $historyfile = $ENV{HOME} . '/.phistory';
my $term = new Term::ReadLine 'Perl Shell';
sub save_list
{
my $f = shift;
my $l = shift;
open F, $f;
print F "$_\n" foreach @$l
}
if (open H, $historyfile)
{
@h = ;
chomp @h;
close H;
$h{$_} = 1 foreach @h;
$term->addhistory($_) foreach keys %h;
}
while ( defined ($_ = $term->readline("My Perl Shell> ")) )
{
my $res = eval($_);
warn $@ if $@;
unless ($@)
{
open H, ">>$historyfile";
print H "$_\n";
close H;
print "\n", Data::Dumper->Dump([$res], ['Result']);
}
$term->addhistory($_) if /\S/;
}
Perl shell 可以极好地完成几项工作,也可以较好地完成某些工作。
首先,它在您的主目录中名为 ".phistory" 的文件内保留一个唯一的、已经输入的命令历史记录。如果对同一条命令输入两次,将只保留一条命令(请参阅用于打开 $historyfile 并从中读取历史行的函数)。
每输入一条新命令,就将命令列表保存到 .phistory 文件中。因此,如果输入一条导致 shell 崩溃的命令,上一次会话的历史记录将不会丢失。
Term::ReadLine 模块使输入命令来执行更加容易。因为将命令限制成每次仅一行,所以可以将前面不错的 buggy.pl 编写为:
Perl shell 中的 buggy.plDa Perl Shell> use strict
$Result = undef;
Perl Shell> print "$_: " . <> foreach (0..20)
0: ...
1: ...
问题当然是 <> input 操作符最终会吃掉 shell 自己的输入。因此,不要在 Perl shell 中使用 <> 或 STDIN,因为它们会使事情更困难。您可以试一下这个:
Perl shell 中修复了错误的 buggy.pl Perl Shell> open F, "buggy.pl"
$Result = 1;
Perl Shell> foreach (0..20) { last if eof(F); print "$_: " . ; }
0: #!/usr/bin/perl -w
1:
2: use strict;
3:
4: foreach (0..20)
5: {
6: my $line = <>;
7: last unless defined $line; # exit loop if $line is not defined
8: print "$_ : $line";
9: }
$Result = undef;
如您所见,shell 可以使您轻松地将语句精简成一行。它还是非常普遍的一种隔离错误的解决方案,并提供了极好的学习环境。自己实践一下,看看是否可以自己编写一个 Perl shell 来进行调试,并看看您学到了多少。
构建工具库
我们已经讨论了内置 Perl 调试器、Devel::ptkdb 和相关工具的最基本内容。还有很多调试 Perl 的方法。重要的是要理解调试过程:如何发现、解决和修复错误。当然,最重要的一点是确保深入理解程序的需求。
Perl 内置的调试器非常强大,但是它不适合于初级或中级 Perl 程序员。(Emacs 是个例外,只要理解了 Emacs 下的调试,那么即使对于初学者,它也可以称得上是一种有用的工具。)
目前为止,Devel::ptkdb 模块和调试器(因为它们的能力和易用性)是初级和中级程序员最好的选择。另一方面,Perl shell 是用于解决少量代码中孤立问题的个性化调试解决方案。
无论是带有 GUD 的 Emacs 编辑器,还是 Perl shell,或者是代码中的打印语句,每一个软件测试人员都构建自己的调试工具集。希望我们本文所讨论的工具可以将您的调试过程变得更轻松。