如何在Perl中实现assert?

6

在尝试将C语言的assert()宏实现到Perl中时,会遇到一些基本问题。首先看一下以下代码:

sub assert($$) {
   my ($assertion, $failure_msg) = @_;
   die $failure_msg unless $assertion;
}

# ...
assert($boolean, $message);

虽然这样可行,但不像C语言的实现,我会写assert($foo <= $bar),但是使用这个实现我需要写assert($foo <= $bar, '$foo <= $bar'),即以字符串形式重复条件。

现在我想知道如何高效地实现它。简单的方法似乎是将字符串传递给assert(),并使用eval来评估字符串,但是当评估eval时无法访问变量。即使它能够工作,每次都要解析和评估条件,这样也会非常低效。

在传递表达式时,我不知道该如何生成字符串,特别是因为它已经被计算了。

另一种使用assert(sub { $condition })的变体,其中可能更容易从代码引用中制作字符串,但被认为太丑陋了。

使用assert(sub { (eval $_[0], $_[0]) }->("condition"));结构时,

sub assert($)
{
    die "Assertion failed: $_[1]\n" unless $_[0];
}

可以实现,但是调用起来很丑陋。

我正在寻找的 解决方案 是只需检查一次条件,同时能够复制原始(未计算)条件高效地计算条件

那么有哪些更优雅的解决方案呢?显然,如果Perl有一个宏或类似语法机制,允许在编译或评估之前转换输入,那么解决方案将更加容易实现。


可以使用 Filter::Simple 完成吗? - U. Windl
4个回答

9

Use B::Deparse?

#!/usr/bin/perl
use strict;
use warnings;

use B::Deparse;
my $deparser = B::Deparse->new();

sub assert(&) {
    my($condfunc) = @_;
    my @caller    = caller();
    unless ($condfunc->()) {
        my $src = $deparser->coderef2text($condfunc);
        $src =~ s/^\s*use\s.*$//mg;
        $src =~ s/^\s+(.+?)/$1/mg;
        $src =~ s/(.+?)\s+$/$1/mg;
        $src =~ s/[\r\n]+/ /mg;
        $src =~ s/^\{\s*(.+?)\s*\}$/$1/g;
        $src =~ s/;$//mg;
        die "Assertion failed: $src at $caller[1] line $caller[2].\n";
    }
}

my $var;
assert { 1 };
#assert { 0 };
assert { defined($var) };

exit 0;

测试输出:

$ perl dummy.pl
Assertion failed: defined $var at dummy.pl line 26.

更新了答案,使用了语法糖,即 assert { CONDITION }; - Stefan Becker
如果输出只是 defined $var(实际失败的条件)而不是执行断言的整个块,那么这将是可以接受的。 - U. Windl
我能够稍微修改一下输出,但是条件仍然必须作为代码引用传递。最重要的是,该条件已经被优化,因此可能很难识别:{ 1 < 2 && 7 == 8 } 变成了 Assertion failed: !1 at /tmp/c.pl line 26. - U. Windl

8

CPAN上有很多关于断言的模块。这些都是开源的,所以很容易查看它们的实现方式。

Carp::Assert 是一个低魔法实现。它在其文档中链接了一些更复杂的断言模块,其中之一是我的模块PerlX::Assert


1
实际上,在来这里询问之前,我已经看过一些“解决方案”,但它们都不是我正在寻找的。也许这就是为什么有那么多不同的解决方案的原因。尽管如此,我一直在思考这个问题,并且想知道最优雅的解决方案可能是什么(“学习效应”)。 - U. Windl

5
使用caller并提取执行断言的源代码行?
sub assert {
    my ($condition, $msg) = @_;
    return if $condition;
    if (!$msg) {
        my ($pkg, $file, $line) = caller(0);
        open my $fh, "<", $file;
        my @lines = <$fh>;
        close $fh;
        $msg = "$file:$line: " . $lines[$line - 1];
    }
    die "Assertion failed: $msg";
}

assert(2 + 2 == 5);

输出:

Assertion failed:  assert.pl:14: assert(2 + 2 == 5);

如果你使用 Carp::croak 而不是 die,Perl 还会报告堆栈跟踪信息,并标识出失败的断言是在哪里调用的。


似乎 caller() 的用处被低估了 ;-) 我喜欢获取原始行的方法,即使它看起来有点低效。 然而,如果条件不适合一行,那么只有第一行会输出,这是个小问题。 - U. Windl
可能可以改进caller()函数:如果我打印一个匿名子例程(代码引用),输出会显示行范围,而不仅仅是第一行,例如"CODE(0x24b57e0) -> &main::__ANON__[/tmp/t.pl:13] in /tmp/t.pl:8-13"。但是当对子例程的调用跨越多行时,我只能得到第一行的行号。 - U. Windl

3
一种处理任何类型“assertions”的方法是使用测试框架。它不像C语言中的assert那么干净利落,但比起 assert 更加灵活和可管理,而且测试可以自由地嵌入到代码中,就像 assert 语句一样。
以下是几个非常简单的例子:
use warnings;
use strict;
use feature 'say';

use Test::More 'no_plan';
Test::More->builder->output('/dev/null');

say "A few examples of tests, scattered around code\n";

like('may be', qr/(?:\w+\s+)?be/, 'regex');
cmp_ok('a', 'eq', 'a ', 'string equality');

my ($x, $y) = (1.7, 13);

cmp_ok($x, '==', $y, '$x == $y');

say "\n'eval' expression in a string so we can see the failing code\n";

my $expr = '$x**2 == $y';
ok(eval $expr, 'Quadratic') || diag explain $expr;  

# ok(eval $expr, $expr);

带输出的几个测试示例,散布在代码中

#   字符串相等性失败的测试
#   在assertion.pl的第19行。
#          得到:'a'
#     期望得到:'a '
#   $x == $y 失败的测试
#   在assertion.pl的第20行。
#          得到:1.7
#     期望得到:13
'eval'表达式是字符串,以便我们可以看到失败的代码
# 二次方测试失败 # 在assertion.pl的第26行。 # $x**2 == $y # 看起来你有4个测试中的3个测试失败了。

这只是一些例子,最后一个直接回答了问题。

模块Test::More汇集了许多工具;有很多选项可供使用,以及如何操作输出。请参见Test::HarnessTest::Builder(以上使用),以及许多教程和SO帖子。

我不知道上面的eval如何计入"优雅",但它确实使您从单数和单独关心的C样式assert语句向更易于管理的系统移动。

好的断言是作为系统测试和代码文档而设计和计划的,但由于它们的性质缺乏正式结构(因此仍然可能分散和临时)。当以这种方式完成时,它们附带一个框架,并且可以使用许多工具进行管理和调整,以及作为套件。


实际上我不同意:断言具有文档目的,是代码的一部分,而测试用例始终是外部的。最重要的是,你无法通过外部测试检查一些内部细节。(抱歉,我也做过一些 Eiffel 编程,在那里断言实际上是语言的一部分。我也玩过 JUnit,但源代码仍然有断言。) - U. Windl
@U.Windl 嗯?上面的例子就是你的程序(如无关的 say ... 所示),测试也是它的一部分。我没有展示一个单独的(外部的)测试套件,而是在你的代码中展示了测试。这将完成 assert 所有的功能(我已经使用并喜欢了多年),如果需要的话还可以做更多的事情。 - zdim
但是,就最受欢迎的功能而言,任何Test::都无法将测试条件重现为字符串;相反,您必须为每个测试条件指定一个字符串。因此,基本上您将使用类似于assert_less($a, $b)的东西来替换assert($a < $b),以便assert_less()知道它所需的所有内容(操作数和运算符)。因此,很容易输出"$a < $b failed"(但是$a$b已经被评估了)。 - U. Windl
@U.Windl 关于“无法将测试条件作为字符串重现”的问题--我添加了一个示例:设置条件,然后将其用于测试和报告。它使用eval,因为没有办法得到你想要的东西;这里只有两行干净的代码。这是一个可以按任意方式堆叠的复杂系统。(我还编辑了答案。) - zdim
@U.Windl 最终可能是关于你简单地_喜欢_什么。但是,也许可以考虑牺牲一些“优雅”,换取更好的效果。在现代高级脚本语言中,我们不必满足于(老式的)C风格断言。你可以选择稍微少一点那个(不太干净),但是有更多其他的东西(作为可管理系统的一部分的测试)。只是提供一个选项。 - zdim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接