6
votes

Comment implémenter assert en Perl?

Lorsque vous essayez d'implémenter la macro assert () de C en Perl, il y a un problème fondamental. Considérez d'abord ce code:

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

Bien que cela fonctionne, ce n'est pas comme C: En C, j'écrirais assert ($ foo , mais avec cette implémentation je devrais écrire assert ($ foo , c'est-à-dire répéter la condition sous forme de chaîne. p>

Maintenant, je me demande comment implémenter cela efficacement . La variante simple semble passer la chaîne à assert () et utiliser eval pour évaluer la chaîne, mais vous ne pouvez pas accéder aux variables lors de l'évaluation de eval. Même si cela fonctionnait, ce serait assez inefficace car la condition est analysée et évaluée à chaque fois.

En passant l'expression, je n'ai aucune idée de comment en faire une chaîne, d'autant plus qu'elle est déjà évaluée .

Une autre variante utilisant assert (sub {$ condition}) où il est probablement plus facile de créer une chaîne à partir du code ref, est considérée comme trop moche.

La construction assert (sub {(eval $ _ [0], $ _ [0])} -> ("condition")); avec

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

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


1 commentaires

Cela pourrait-il être fait en utilisant Filter :: Simple ?


4 Réponses :


8
votes

Utilisez B :: Deparse ?

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

Sortie de test:

#!/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;


3 commentaires

Mise à jour de la réponse pour utiliser le sucre syntaxique, c'est-à-dire àssert {CONDITION}; .


Si la sortie était juste définie $ var (la condition qui a effectivement échoué) au lieu du bloc entier où l'assertion a été exécutée, cela serait acceptable.


J'ai pu modifier un peu la sortie, mais la condition doit toujours être passée comme référence CODE. Surtout, la condition est optimisée, elle peut donc être difficile à reconnaître: {1 <2 && 7 == 8} devient L’assertion a échoué:! 1 à la ligne /tmp/c.pl 26.



7
votes

Il existe une multitude de modules d'assertion sur CPAN. Ceux-ci sont open source, il est donc assez facile de les regarder et de voir comment ils sont faits.

Carp :: Assert est une implémentation peu magique. Il contient des liens vers quelques modules d'assertion plus compliqués dans sa documentation, dont l'un est mon module PerlX :: Assert .


1 commentaires

En fait, avant de demander ici, j'avais examiné quelques «solutions», mais aucune d'entre elles n'était vraiment ce que je cherchais. C'est peut-être la raison pour laquelle il existe tant de solutions différentes. Malgré cela, j'avais réfléchi à ce problème, et je me demandais à quoi pourrait ressembler la solution la plus élégante («l'effet d'apprentissage»).



5
votes

Utilisez caller et extrayez la ligne de code source qui a fait l'assertion?

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

Sortie:

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);

Si vous utilisez Carp :: croak au lieu de die , Perl rapportera également les informations de trace de pile et identifiera où l'assertion défaillante a été appelée.

p >


2 commentaires

Il semble que l'utilité de caller () soit sous-estimée ;-) J'aime l'approche pour obtenir la ligne d'origine, même si cela peut sembler un peu inefficace. Cependant, il y a un petit problème si la condition ne rentre pas dans une ligne; alors seule la première ligne est sortie.


L'appelant () pourrait probablement être amélioré: si j'imprime un sous-programme anonyme (code ref), j'obtiens une sortie montrant la plage de lignes, pas seulement la première ligne, comme " CODE (0x24b57e0) -> & main :: __ ANON __ [/ tmp / t.pl: 13] dans /tmp/t.pl:8-13 ". Mais lorsque l'appel à un sous-programme est réparti sur plusieurs lignes, j'obtiens juste le numéro de la première ligne.



4
votes

Une approche à tout type d '"assertions" consiste à utiliser un cadre de test. Ce n'est pas aussi net que assert de C, mais il est alors incomparablement plus flexible et gérable, alors que les tests peuvent toujours être librement intégrés dans le code, tout comme les instructions assert .

Quelques exemples très simples

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.

avec sortie

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);

Ceci est juste une dispersion d'exemples, où le le dernier répond directement à la question.

Le module Test :: More a> regroupe un certain nombre d'outils; il existe de nombreuses options pour savoir comment l'utiliser et comment manipuler la sortie. Voir Test :: Harness et Test :: Builder (utilisé ci-dessus), et un certain nombre de tutoriels et de messages SO.

Je ne le fais pas sachez comment l ' eval ci-dessus compte pour «élégant», mais cela vous fait passer d'instructions assert de style C singulières et individuellement soignées vers un système plus facile à gérer. p >

Les bonnes assertions sont conçues et planifiées comme des tests systémiques et de la documentation de code, mais de par leur nature, elles manquent de structure formelle (et peuvent donc finir par être dispersées et ad hoc). Lorsque cela est fait de cette façon, ils sont fournis avec un cadre et peuvent être gérés et réglés avec de nombreux outils, et en tant que suite.


5 commentaires

En fait, je ne suis pas d'accord: les assertions ont un but de documentation et font partie du code, tandis que les cas de test sont toujours externes au code. Surtout, vous ne pouvez pas vérifier certains détails internes avec des tests externes. (Désolé, j'ai aussi fait de la programmation Eiffel où les assertions font en fait partie du langage. J'ai aussi joué avec JUnit, mais les sources ont toujours des affirmations.)


@ U.Windl Hum? L'exemple ci-dessus est votre programme (comme indiqué par un say ... non lié), et les tests en font partie. Je ne montre pas de suite de tests (externe) séparée, mais des tests dans votre code . Cela fera tout assert (ce que j'avais utilisé et aimé depuis des années), et bien plus encore si nécessaire.


Mais pour résumer la fonctionnalité la plus recherchée: tout Test :: ne peut pas reproduire la condition de test sous forme de chaîne; à la place, vous devrez spécifier une chaîne pour chaque condition de test. Donc, fondamentalement, vous remplacez assert ($ a <$ b) par quelque chose comme assert_less ($ a, $ b) , de sorte que assert_less () sait tout ce dont il a besoin (opérandes et opérateur). Il est donc facile de sortir "$ a <$ b failed" (mais $ a et $ b sont alors déjà évalués).


@ U.Windl Re " ne peut pas reproduire la condition de test sous forme de chaîne " - Ajout d'un exemple de cela: configurez la condition, puis utilisez-la à la fois pour le test et le rapport. Il utilise eval car il n'y a aucun moyen d'obtenir exactement ce que vous voulez; ce n'est que deux lignes épurées ici. Il s'agit d'un système complexe qui peut être empilé comme vous le souhaitez. (J'ai également édité la réponse.)


@ U.Windl En fin de compte, il peut s'agir de ce que vous allez simplement aimer . Cependant, envisagez peut-être de sacrifier certains «élégants» pour certains «meilleurs». Dans un langage de script moderne de haut niveau, nous n'avons pas à nous contenter des (bonnes vieilles) assertions de style C. Vous pouvez aller avec un peu moins de cela (pas tout à fait aussi propre) mais avec beaucoup plus d'autres (tests dans le cadre d'un système gérable). Je suggère juste une option.