При попытке реализовать макрос assert() на Perl возникает фундаментальная проблема. Сначала рассмотрим этот код:
sub assert($$) {
my ($assertion, $failure_msg) = @_;
die $failure_msg unless $assertion;
}
# ...
assert($boolean, $message);
Хотя это работает, это не похоже на C: в 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 имел макрос или аналогичный синтаксический механизм, позволяющий преобразовывать ввод перед компиляцией или оценкой.





Использовать B:: Депарсировать?
#!/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.
Обновлен ответ, чтобы использовать синтаксический сахар, то есть àssert { CONDITION };.
Если бы на выходе был только defined $var (условие, которое фактически не удалось), а не весь блок, в котором выполняется утверждение, это было бы приемлемо.
Мне удалось немного изменить вывод, но все же условие должно быть передано как ссылка на КОД. Больше всего оптимизировано условие, поэтому его может быть трудно распознать: { 1 < 2 && 7 == 8 } становится Assertion failed: !1 at /tmp/c.pl line 26..
В CPAN есть множество модулей утверждений. Они с открытым исходным кодом, так что довольно легко заглянуть в них и посмотреть, как они сделаны.
Карп::Утвердить — реализация с низким уровнем магии. В его документации есть ссылки на несколько более сложных модулей утверждений, одним из которых является мой модуль PerlX::Утвердить.
На самом деле, прежде чем спросить здесь, я рассмотрел несколько «решений», но ни одно из них не было тем, что я ищу. Может быть, поэтому существует так много разных решений. Несмотря на это, я думал об этой проблеме, и мне было интересно, как может выглядеть самое элегантное решение («эффект обучения»).
Использовать 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() недооценена ;-) Мне нравится подход к получению исходной строки, даже если он может показаться немного неэффективным. Однако есть небольшая проблема, если условие не помещается в одну строку; то выводится только первая строка.
Возможно, caller() можно было бы улучшить: если я печатаю анонимную подпрограмму (ссылка на код), я получаю вывод, показывающий диапазон строк, а не только первую строку, например «CODE(0x24b57e0) -> &main::__ANON__[/tmp/t.pl:13] in /tmp/t.pl:8-13». Но когда вызов подпрограммы распределяется по нескольким строкам, я просто получаю номер первой строки.
Одним из подходов к любым «утверждениям» является использование среды тестирования. Он не такой четкий, как assert в C, но несравненно более гибкий и управляемый, в то время как тесты по-прежнему можно свободно встраивать в код, как операторы 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.
Это просто разрозненные примеры, где последний прямо отвечает на вопрос.
Модуль Тест::Больше объединяет ряд инструментов; есть много вариантов того, как его использовать и как манипулировать выводом. См. Тест::Упряжь и Тест::Строитель (используемые выше), а также ряд руководств и сообщений SO.
Я не знаю, как приведенный выше eval считается «элегантным», но он действительно перемещает вас от единичных и индивидуально заботящихся об операторах assert в стиле C к более легко управляемой системе.
Хорошие утверждения задуманы и запланированы как системные тесты и документация по коду, но по своей природе им не хватает формальной структуры (и поэтому они все равно могут оказаться разрозненными и случайными). Когда это сделано таким образом, они поставляются с инфраструктурой, и ими можно управлять и настраивать с помощью многих инструментов и в виде пакета.
На самом деле я не согласен: утверждения служат цели документирования и являются частью кода, в то время как тестовые примеры всегда являются внешними по отношению к коду. Прежде всего, вы не можете проверить некоторые внутренние детали с помощью внешних тестов. (Извините, я также занимался программированием Eiffel, где утверждения на самом деле являются частью языка. Я также играл с JUnit, но в исходниках все же есть утверждения.)
@ У.Виндл Хам? В приведенном выше примере является ваша программа (на что указывает несвязанный say ...), и тесты являются ее частью. Я не показываю отдельный (внешний) набор тестов, а тестирую в вашем коде. Этого вполне достаточно для всеassert (которым я пользовался и любил много лет), и даже намного больше, если это необходимо.
Но резюмируя наиболее востребованную функцию: ни один Test:: не может воспроизвести тестовое условие в виде строки; вместо этого вам нужно указать строку для каждого условия теста. Таким образом, в основном вы заменяете assert($a < $b) чем-то вроде assert_less($a, $b), чтобы assert_less() знал все, что ему нужно (операнды и операторы). Таким образом, легко вывести "$a < $b failed" (но все же $a и $b уже оцениваются).
@U.Windl Re "не может воспроизвести тестовое условие в виде строки" - добавлен пример этого: установите условие, а затем используйте его как для тестирования, так и для отчета. Он использует eval, поскольку просто так получить то, что вы хотите, невозможно; здесь всего две чистые линии. Это сложная система, которую можно сложить как угодно. (Я также отредактировал ответ.)
@U.Windl В конце концов, это может означать, что вы просто как. Тем не менее, возможно, стоит пожертвовать некоторыми из «элегантных» ради некоторых из «лучших». В современном языке сценариев высокого уровня нам не нужно довольствоваться (старыми добрыми) утверждениями в стиле C. Вы можете использовать немного меньше этого (не так чисто), но гораздо больше другого (тесты как часть управляемой системы). Просто предложил вариант.
Можно ли это сделать с помощью
Filter::Simple?