如何在Perl中实现assert?

时间:2019-01-28 14:41:04

标签: perl eval assert

当尝试在Perl中实现C的assert()宏时,存在一些基本问题。首先考虑以下代码:

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

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

虽然可行,但它不像C:在C中我要写assert($foo <= $bar),但通过这种实现,我不得不写assert($foo <= $bar, '$foo <= $bar'),即 repeat 条件为字符串。

现在,我想知道如何有效地实现 。简单的变体似乎将字符串传递给assert()并使用eval来评估字符串,但是在评估eval时不能访问变量。即使可行,每次条件都会被分析和评估,效率会非常低。

传递表达式时,我不知道如何用它来创建字符串,尤其是已经对其求值的时候。

另一个使用assert(sub { $condition })的变体太丑陋了,在该变体中可能更容易从代码ref中创建字符串。

构造assert(sub { (eval $_[0], $_[0]) }->("condition"));

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

会做,但是很难打电话。 我正在寻找的解决方案编写条件以仅检查一次 ,同时能够复制原始内容(非评估)条件有效评估条件

那么还有什么更优雅的解决方案?显然,如果Perl具有宏或相当的语法机制,该机制允许在编译或评估之前转换输入,则解决方案会更容易。

4 个答案:

答案 0 :(得分:7)

使用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.

答案 1 :(得分:6)

CPAN上有很多断言模块。这些都是开源的,因此很容易窥视它们并查看它们是如何完成的。

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

答案 2 :(得分:4)

使用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还将报告堆栈跟踪信息并确定调用失败的断言的位置。

答案 3 :(得分:2)

任何一种“断言”的方法都是使用测试框架。它不像C的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);

有输出

A few examples of tests, scattered around code

#   Failed test 'string equality'
#   at assertion.pl line 19.
#          got: 'a'
#     expected: 'a '
#   Failed test '$x == $y'
#   at assertion.pl line 20.
#          got: 1.7
#     expected: 13

'eval' expression in a string so we can see the failing code

#   Failed test 'Quadratic'
#   at assertion.pl line 26.
# $x**2 == $y
# Looks like you failed 3 tests of 4.

这只是示例的分散,最后一个示例直接回答了问题。

Test::More模块汇集了许多工具;如何使用它以及如何操纵输出有很多选择。请参阅Test::HarnessTest::Builder(上面使用过),以及一些教程和SO帖子。

我不知道上面的eval是如何算作“优雅”的,但是它的确使您从单一且个人关心的C风格assert语句转向了更易于管理的系统。

好的断言是作为系统测试和代码文档而设计的,但从本质上讲,它们缺乏正式的结构(因此可能最终会分散且临时存在)。但是,以这种方式完成操作后,它们会附带一个框架,并且可以作为套件使用许多工具进行管理和调整。