Subsecciones


Pruebas para el Analizador Léxico

Queremos separar/aislar las diferentes fases del compilador en diferentes módulos.

Módulo PL::Error

Para ello comenzamos creando un módulo conteniendo las rutinas de tratamiento de errores:

lhp@nereida:~/Lperl/src/topdown/PL0506/03lexico/PL-Tutu/lib/PL$ pwd
/home/lhp/Lperl/src/topdown/PL0506/03lexico/PL-Tutu/lib/PL
lhp@nereida:~/Lperl/src/topdown/PL0506/03lexico/PL-Tutu/lib/PL$ cat -n Error.pm
   1  package Error;
   2  use strict;
   3  use warnings;
   4  use Carp;
   5
   6  require Exporter;
   7
   8  our @ISA = qw(Exporter);
   9  our @EXPORT = qw( error fatal);
  10  our $VERSION = '0.01';
  11
  12  sub error {
  13    my $msg = join " ", @_;
  14    if (!$PL::Tutu::errorflag) {
  15      carp("Error: $msg\n");
  16      $PL::Tutu::errorflag = 1;
  17    }
  18  }
  19
  20  sub fatal {
  21    my $msg = join " ", @_;
  22    croak("Error: $msg\n");
  23  }
Observa como accedemos a la variable errorflag del paquete PL::Tutu. Para usar este módulo desde PL::Tutu, tenemos que declarar su uso:
lhp@nereida:~/Lperl/src/topdown/PL0506/03lexico/PL-Tutu/lib/PL$ cat -n Tutu.pm | head -8
     1  package PL::Tutu;
     2
     3  use 5.008004;
     4  use strict;
     5  use warnings;
     6  use IO::File;
     7  use Carp;
     8  use PL::Error;
En la línea 8 hacemos use PL::Error y no use Error ya que el módulo lo hemos puesto en el directorio PL. No olvides hacer make manifest para actualizar el fichero MANIFEST.

Módulo PL::Lexical::Analysis

Supongamos que además de modularizar el grupo de rutinas de tratamiento de errores queremos hacer lo mismo con la parte del análisis léxico. Parece lógico que el fichero lo pongamos en un subdirectorio de PL/ por lo que cambiamos el nombre del módulo a PL::Lexical::Analysis quedando la jerarquía de ficheros asi:

lhp@nereida:~/Lperl/src/topdown/PL0506/03lexico/PL-Tutu/lib/PL$ tree
.
|-- Error.pm
|-- Lexical
|   `-- Analysis.pm
`-- Tutu.pm

Por supuesto debemos modificar las correspondientes líneas en Tutu.pm:

 1 package PL::Tutu;
 2 
 3 use 5.008004;
 4 use strict;
 5 use warnings;
 6 use IO::File;
 7 use Carp;
 8 use PL::Error;
 9 use PL::Lexical::Analysis;
10 ...
11 
12 sub compile {
13   my ($input) = @_;
14   local %symbol_table = ();
15   local $data = ""; # Contiene todas las cadenas en el programa fuente
16   local $target = ""; # target code
17   my @tokens = ();
18   local $errorflag = 0;
19   local ($lookahead, $value) = ();
20   local $tree = undef; # abstract syntax tree
21   local $global_address = 0;
22 
23   
24   ########lexical analysis
25   @tokens = &PL::Lexical::Analysis::scanner($input);
26   print "@tokens\n";
27 
28   ...
29 
30   return \$target;
31 }
Observe que ahora PL::Lexical::Analysis::scanner devuelve ahora la lista con los terminales y que @tokens se ha ocultado en compile como una variable léxica (línea 17). En la línea 26 mostramos el contenido de la lista de terminales.

Sigue el listado del módulo conteniendo el analizador léxico. Obsérve las líneas 6, 16 y 44.

lhp@nereida:~/Lperl/src/topdown/PL0506/03lexico/PL-Tutu/lib/PL/Lexical$ cat -n Analysis.pm
     1  # Lexical analyzer
     2  package PL::Lexical::Analysis;
     3  use strict;
     4  use warnings;
     5  use Carp;
     6  use PL::Error;
     7
     8  require Exporter;
     9
    10  our @ISA = qw(Exporter);
    11  our @EXPORT = qw( scanner );
    12  our $VERSION = '0.01';
    13
    14  sub scanner {
    15    local $_ = shift;
    16    my @tokens;
    17
    18    { # Con el redo del final hacemos un bucle "infinito"
    19      if (m{\G\s*(\d+)}gc) {
    20        push @tokens, 'NUM', $1;
    21      }
    ..      ...
    37      elsif (m{\G\s*([+*()=;,])}gc) {
    38        push @tokens, 'PUN', $1;
    39      }
    40      elsif (m{\G\s*(\S)}gc) {
    41        Error::fatal "Caracter invalido: $1\n";
    42      }
    43      else {
    44        return @tokens;
    45      }
    46      redo;
    47    }
    48  }

El Programa Cliente

Puesto que en el paquete PL::Lexical::Analysis exportamos scanner no es necesario llamar la rutina por el nombre completo desde compile. Podemos simplificar la línea en la que se llama a scanner que queda así:

########lexical analysis
@tokens = &scanner($input);
print "@tokens\n";
De la misma forma, dado que PL::Tutu exporta la función compile_from_file, no es necesario llamarla por su nombre completo desde el guión tutu. Reescribimos la línea de llamada:
lhp@nereida:~/Lperl/src/topdown/PL0506/03lexico/PL-Tutu/scripts$ cat tutu
#!/usr/bin/perl -w
use lib ('../lib');
use PL::Tutu;

&compile_from_file(@ARGV);

Actualización del MANIFEST

Como siempre que se añaden o suprimen archivos es necesario actualizar MANIFEST:

lhp@nereida:~/Lperl/src/topdown/PL0506/03lexico/PL-Tutu$ make manifest
/usr/bin/perl "-MExtUtils::Manifest=mkmanifest" -e mkmanifest
Added to MANIFEST: lib/PL/Lexical/Analysis.pm
lhp@nereida:~/Lperl/src/topdown/PL0506/03lexico/PL-Tutu$ cat -n MANIFEST
     1  Changes
     2  lib/PL/Error.pm
     3  lib/PL/Lexical/Analysis.pm
     4  lib/PL/Tutu.pm
     5  Makefile.PL
     6  MANIFEST
     7  MANIFEST.SKIP
     8  README
     9  scripts/test01.tutu
    10  scripts/tutu
    11  scripts/tutu.pl
    12  t/PL-Tutu.t


Comprobando el Analizador Léxico

Queremos comprobar si nuestro código funciona. ¿Cómo hacerlo?. Lo adecuado es llevar una aproximación sistemática que permita validar el código.

Principios Básicos del Desarrollo de Pruebas

En general, la filosofía aconsejable para realizar un banco de pruebas de nuestro módulo es la que se articula en la metodología denominada Extreme Programming, descrita en múltiples textos, en concreto en el libro de Scott []:

La Jerarquía de Una Aplicación

Pueden haber algunas diferencias entre el esquema que se describe aqui y su versión de Perl. Lea detenidamente el capítulo Test Now, test Forever del libro de Scott [] y el libro [11] de Ian Langworth y chromatic.

Si usas una versión de Perl posterior la 5.8.0, entonces tu versión del programa h2xs creará un subdirectorio /t en el que guardar los ficheros de prueba Estos ficheros deberán ser programas Perl de prueba con el tipo .t. La utilidad h2xs incluso deja un programa de prueba PL-Tutu.t en ese directorio. La jerarquía de ficheros con la que trabajamos actualmente es:

lhp@nereida:~/Lperl/src/topdown/PL0506/03lexico/PL-Tutu$ make veryclean
rm -f blib/script/tutu blib/script/tutu.pl
rm -rf ./blib Makefile.aperl ...
mv Makefile Makefile.old > /dev/null 2>&1
rm -rf blib/lib/auto/PL/Tutu blib/arch/auto/PL/Tutu
rm -rf PL-Tutu-0.01
rm -f  blib/lib/PL/.Tutu.pm.swp ...
rm -f *~ *.orig */*~ */*.orig
lhp@nereida:~/Lperl/src/topdown/PL0506/03lexico/PL-Tutu$ tree
.
|-- .svn                # use siempre un sistema de control de versiones
|-- Changes             # la historia de cambios
|-- MANIFEST            # lista de ficheros que componen la distribución
|-- MANIFEST.SKIP       # regexps para determinar que ficheros no pertenecen
|-- META.yml            # YML no es XML
|-- Makefile.PL         # generador del Makefle independiente de la plataforma
|-- PL-Tutu-0.01.tar.gz
|-- README              # instrucciones de instalacion
|-- lib
|   `-- PL
|       |-- Error.pm            # rutinas de manejo de errores
|       |-- Lexical
|       |   `-- Analysis.pm     # modulo con el analizador lexico
|       `-- Tutu.pm             # modulo principal
|-- scripts
|   |-- test01.sal   # salida del programa de prueba
|   |-- test01.tutu  # programa de prueba
|   |-- tutu         # compilador
|   `-- tutu.pl      # compilador
`-- t
    `-- 01Lexical.t  # prueba consolidada

Un Ejemplo de Programa de Prueba

Estos son los contenidos de nuestro primer test:

lhp@nereida:~/Lperl/src/topdown/PL0506/03lexico/PL-Tutu$ cd t
lhp@nereida:~/Lperl/src/topdown/PL0506/03lexico/PL-Tutu/t$ ls -l
total 4
-rw-r--r--  1 lhp lhp 767 2005-10-10 11:27 01Lexical.t
lhp@nereida:~/Lperl/src/topdown/PL0506/03lexico/PL-Tutu/t$ cat -n 01Lexical.t
 1  # Before `make install' is performed this script should be runnable with
 2  # `make test'. After `make install' it should work as `perl PL-Tutu.t'
 3
 4  #########################
 5
 6  # change 'tests => 1' to 'tests => last_test_to_print';
 7
 8  use Test::More tests => 5;
 9  use Test::Exception;
10
11  BEGIN { use_ok('PL::Lexical::Analysis') };
12  BEGIN { use_ok('PL::Tutu') };
13
14  #########################
15
16  # Insert your test code below, the Test::More module is use()ed here so read
17  # its man page ( perldoc Test::More ) for help writing this test script.
18
19  can_ok('PL::Lexical::Analysis', 'scanner');
20
21  # Test result of call
22  my $a = 'int a,b; string c; c = "hello"; a = 4; b = a +1; p b';
23  my @tokens = scanner($a);
24  my @expected_tokens = qw{
25  INT INT
26  ID a
27  PUN ,
28  ID b
29  PUN ;
30  STRING STRING
31  ID c
32  PUN ;
33  ID c
34  PUN =
35  STR "hello"
36  PUN ;
37  ID a
38  PUN =
39  NUM 4
40  PUN ;
41  ID b
42  PUN =
43  ID a
44  PUN +
45  NUM 1
46  PUN ;
47  P P
48  ID b
49  };
50  is(@tokens, @expected_tokens, "lexical analysis");
51
52  # test a lexically  erroneous program
53  $a = 'int a,b; string c[2]; c = "hello"; a = 4; b = a +1; p b';
54  throws_ok { scanner($a) } qr{Error: Caracter invalido:}, 'erroneous program';

El nombre del fichero de prueba debe cumplir que:

Ejecución de Las Pruebas

Ahora ejecutamos las pruebas:

lhp@nereida:~/Lperl/src/topdown/PL0506/03lexico/PL-Tutu$ make test
PERL_DL_NONLAZY=1 /usr/bin/perl "-MExtUtils::Command::MM" "-e" "test_harness(0, 'blib/lib', 'blib/arch')" t/*.t
t/01Lexical....ok 1/5Possible attempt to separate words with commas at t/01Lexical.t line 49.
t/01Lexical....ok
All tests successful.
Files=1, Tests=5,  0 wallclock secs ( 0.08 cusr +  0.00 csys =  0.08 CPU)
O bien usamos prove:
lhp@nereida:~/Lperl/src/topdown/PL0506/03lexico/PL-Tutu/t$ prove -I../lib 01Lexical.t
01Lexical....ok
All tests successful.
Files=1, Tests=2,  0 wallclock secs ( 0.03 cusr +  0.01 csys =  0.04 CPU)
También podemos añadir la opción verbose a prove:
lhp@nereida:~/Lperl/src/topdown/PL0506/03lexico/PL-Tutu/t$ prove -v 01Lexical.t
01Lexical....1..5
ok 1 - use PL::Lexical::Analysis;
ok 2 - use PL::Tutu;
ok 3 - PL::Lexical::Analysis->can('scanner')
ok 4 - lexical analysis
ok 5 - erroneous program
ok
All tests successful.
Files=1, Tests=5,  0 wallclock secs ( 0.07 cusr +  0.01 csys =  0.08 CPU)

Repáse [*] [10] para un mejor conocimiento de la metodología de pruebas en Perl.

Versiones anteriores a la 5.8

En esta sección he usado la versión 5.6.1 de Perl. Creeemos un subdirectorio tutu_src/ y en él un programa de prueba pruebalex.pl:
$ pwd
/home/lhp/projects/perl/src/tmp/PL/Tutu/tutu_src
$ cat pruebalex.pl
#!/usr/bin/perl -w -I..
#use PL::Tutu;
use Tutu;

my $a = 'int a,b; string c; c = "hello"; a = 4; b = a +1; p b';
Lexical::Analysis::scanner($a);
print "prog = $a\ntokens = @PL::Tutu::tokens\n";
Observa como la opción -I.. hace que se busque por las librerías en el directorio padre del actual. Cuando ejecutamos pruebalex.pl obtenemos la lista de terminales:
$ ./pruebalex.pl
prog = int a,b; string c; c = "hello"; a = 4; b = a +1; p b
tokens = INT INT ID a PUN , ID b PUN ; STRING STRING ID c PUN ; 
ID c PUN = STR hello PUN ; ID a PUN = NUM 4 PUN ; ID b PUN = 
ID a PUN + NUM 1 PUN ; P P ID b
La última línea ha sido partida por razones de legibilidad, pero consituye una sóla línea. Editemos el fichero test.pl en el directorio del módulo. Sus contenidos son como sigue:
$ cat -n test.pl
 1  # Before `make install' is performed this script should be runnable with
 2  # `make test'. After `make install' it should work as `perl test.pl'
 3
 4  #########################
 5
 6  # change 'tests => 1' to 'tests => last_test_to_print';
 7
 8  use Test;
 9  BEGIN { plan tests => 1 };
10  use PL::Tutu;
11  ok(1); # If we made it this far, we're ok.
12
13  #########################
14
15  # Insert your test code below, the Test module is use()ed here so read
16  # its man page ( perldoc Test ) for help writing this test script.
17
En la línea 9 se establece el número de pruebas a realizar. La primera prueba aparece en la línea 11. Puede parecer que no es una prueba, ¡pero lo es!. Si se ha alcanzado la línea 11 es que se pudo cargar el módulo PL::Tutu y eso ¡tiene algún mérito!.

Seguiremos el consejo de la línea 15 y escribiremos nuestra segunda prueba al final del fichero test.pl:

$ cat -n test.pl | tail -7
16  # its man page ( perldoc Test ) for help writing this test script.
17
18  my $a = 'int a,b; string c; c = "hello"; a = 4; b = a +1; p b';
19  local @PL::Tutu::tokens = ();
20  Lexical::Analysis::scanner($a);
21  ok("@PL::Tutu::tokens" eq
22  'INT INT ID a PUN , ID b PUN ; STRING STRING ID c PUN ; ID c 
     PUN = STR hello PUN ; ID a PUN = NUM 4 PUN ; ID b PUN = 
     ID a PUN + NUM 1 PUN ; P P ID b');
La línea 22 ha sido partida por razones de legibilidad, pero constituye una sóla línea. Ahora podemos ejecutar make test y comprobar que las dos pruebas funcionan:
$ make test
PERL_DL_NONLAZY=1 /usr/bin/perl -Iblib/arch -Iblib/lib -I/usr/lib/perl/5.6.1 \
                                -I/usr/share/perl/5.6.1 test.pl
1..2
ok 1
ok 2
¿Recordaste cambiar la línea 9 de test.pl? ¿Añadiste los nuevos ficheros a la lista en MANIFEST?


Práctica: Pruebas en el Análisis Léxico

Extienda su compilador para modularizar el analizador léxico tal y como se explicó en la sección 33.4.
  1. Lea los siguientes documentos

  2. Incluya la estrategia de pruebas de no regresión explicada en las secciones previas. Dado que ahora la estructura del terminal es una estructura de datos mas compleja (token, [value, line_number]) no podrá usar is, ya que este último sólo comprueba la igualdad entre escalares. Use is_deeply para comprobar que la estructura de datos devuelta por el analizador léxico es igual a la esperada. Sigue un ejemplo:

    nereida:~/src/perl/YappWithDefaultAction/t> cat -n  15treeregswith2arrays.t
     1  #!/usr/bin/perl -w
     2  use strict;
     3  #use Test::More qw(no_plan);
     4  use Test::More tests => 3;
     5  use_ok qw(Parse::Eyapp) or exit;
    
    ..  ..... etc., etc.
    
    84  my $expected_tree = bless( {
    85    'children' => [
    86      bless( { 'children' => [
    87          bless( { 'children' => [], 'attr' => 'a', 'token' => 'a' }, 'TERMINAL' )
    88        ]
    89      }, 'A' ),
    90      bless( { 'children' => [
    91          bless( { 'children' => [], 'attr' => 'c', 'token' => 'c' }, 'TERMINAL' )
    92        ]
    93      }, 'C' )
    94    ]
    95  }, 'ABC' );
    96  is_deeply($t, $expected_tree, "deleting node between arrays");
    

  3. Extienda los tests con una prueba en la que la entrada contenga un carácter ilegal. Obsérve que, tal y como esta escrito la rutina scanner, si la entrada tiene un carácter ilegal se ejecutarán las líneas
    26     elsif (/\G\s*(.)/gc) {
    27       Error::fatal "Caracter invalido: $1\n";
    28     }
    
    lo que causa la parada del programa de prueba, al ejecutarse fatal el cuál llama a croak.
      sub fatal {
        my $msg = join " ", @_;
        croak("Error: $msg\n");
      }
    
    El objetivo es lograr que el programa de pruebas continúe ejecutando las subsiguientes pruebas.

    Para ello puede usar Test::Exception o bien eval y la variable especial $@ para controlar que el programa .t no termine prematuramente. Repase la sección [*] [10], el capítulo [*] y mas específicamente la sección [*] del capítulo sobre construcción de módulos.

  4. Pruebe a dar como entrada un fichero vacío
  5. Pruebe a dar como entrada un fichero que no existe
  6. Pruebe a dar como entrada un fichero binario
  7. Si tiene sentido en su caso, llame a las subrutinas con mas argumentos (y también con menos) de los que esperan.
  8. Si tiene sentido en su caso, llame a las subrutinas con argumentos cuyo tipo no es el que se espera.

    No use prototipos para lograrlo. No es una buena idea. Los prototipos en Perl a menudo producen un preprocesado del parámetro. Escriba código que controle que la naturaleza del parámetro es la que se espera. Por ejemplo:

    sub tutu {
      my $refhash = shift;
      croak "Error" unless UNIVERSAL::isa($refhash, 'HASH');
      ...
    }
    
  9. Cambie los argumentos de orden (si es que se aplica a su código)
  10. Comentarios: pruebe con /* * */ a = 4; /* * / */. Tambien con comentarios anidados (debería producirse un error)
  11. Flotantes: compruebe su expresión regular con 0.0 0e0 .0 0 1.e-5 1.0e2 -2.0 . (un punto sólo)
  12. Cadenas. Pruebe con las cadenas
    "" 
    "h\"a\"h" 
    "\""
    
    Pruebe también con una cadena con varias líneas y otra que contenga un carácter de control en su interior.
  13. Convierta los fallos (bugs) que encontró durante el desarrollo en pruebas
  14. Compruebe la documentación usando el módulo Test::Pod de Andy Lester. Instálelo si es necesario.

  15. Utilice el módulo Test::Warn para comprobar que los mensajes de warning (uso de warn and carp) se muestran correctamente.

  16. Una prueba SKIP declara un bloque de pruebas que - bajo ciertas circustancias - puede saltarse. Puede ser que sepamos que ciertas pruebas sólo funcionan en ciertos sistemas operativos o que la prueba requiera que ciertos paquetes están instalados o que la máquina disponga de ciertos recursos (por ejemplo, acceso a internet). En tal caso queremos que los tests se consideren si se dan las circustancias favorables pero que en otro caso se descarten sin protestas. Consulte la documentación de los módulos Test::More y Test::Harness sobre pruebas tipo SKIP. El ejemplo que sigue declara un bloque de pruebas que pueden saltarse. La llamada a skip indica cuantos tests hay, bajo que condición saltarselos.
     1  SKIP: {
     2      eval { require HTML::Lint };
     3 
     4      skip "HTML::Lint not installed", 2 if $@;
     5 
     6      my $lint = new HTML::Lint;
     7      isa_ok( $lint, "HTML::Lint" );
     8 
     9      $lint->parse( $html );
    10      is( $lint->errors, 0, "No errors found in HTML" );
    11  }
    
    Si el usuario no dispone del módulo HTML::Lint el bloque no será ejecutado. El módulo Test::More producirá oks que serán interpretados por Test::Harness como tests skipped pero ok.

    Otra razón para usar una prueba SKIP es disponer de la posibilidad de saltarse ciertos grupos de pruebas. Por ejemplo, aquellas que llevan demasiado tiempo de ejecución y no son tan significativas que no se pueda prescindir de ellas cuando se introducen pequeños cambios en el código. El siguiente código muestra como usando una variable de entorno TEST_FAST podemos controlar que pruebas se ejecutan.

    nereida:~/src/perl/YappWithDefaultAction/t> cat 02Cparser.t | head -n 56 -
    #!/usr/bin/perl -w
    use strict;
    #use Test::More qw(no_plan);
    use Test::More tests => 6;
    
    use_ok qw(Parse::Eyapp) or exit;
    
    SKIP: {
      skip "You decided to skip C grammar test (env var TEST_FAST)", 5 if $ENV{TEST_FAST} ;
      my ($grammar, $parser);
      $grammar=join('',<DATA>);
      $parser=new Parse::Eyapp(input => $grammar, inputfile => 'DATA', firstline => 52);
    
      #is($@, undef, "Grammar module created");
    
      # Does not work. May I have done s.t. wrong?
      #is(keys(%{$parser->{GRAMMAR}{NULLABLE}}), 43, "43 nullable productions");
    
      is(keys(%{$parser->{GRAMMAR}{NTERM}}), 233, "233 syntactic variables");
    
      is(scalar(@{$parser->{GRAMMAR}{UUTERM}}), 3, "3 UUTERM");
    
      is(scalar(keys(%{$parser->{GRAMMAR}{TERM}})), 108, "108 terminals");
    
      is(scalar(@{$parser->{GRAMMAR}{RULES}}), 825, "825 rules");
    
      is(scalar(@{$parser->{STATES}}), 1611, "1611 states");
    }
    
    __DATA__
    /*
       This grammar is a stripped form of the original C++ grammar
       from the GNU CC compiler :
    
       YACC parser for C++ syntax.
       Copyright (C) 1988, 89, 93-98, 1999 Free Software Foundation, Inc.
       Hacked by Michael Tiemann (tiemann@cygnus.com)
    
       The full gcc compiler an the original grammar file are freely
       available under the GPL license at :
    
       ftp://ftp.gnu.org/gnu/gcc/
       ...................... etc. etc.
    */
    nereida:~/src/perl/YappWithDefaultAction> echo $TEST_FAST
    1
    nereida:~/src/perl/YappWithDefaultAction> make test
    PERL_DL_NONLAZY=1 /usr/bin/perl "-MExtUtils::Command::MM" "-e" "test_harness(0, 'blib/lib', 'blib/arch')" t/*.t
    t/01calc....................................ok
    t/02Cparser.................................ok
            5/6 skipped: various reasons
    t/03newgrammar..............................ok
    t/04foldandzero.............................ok
    t/05treewithvars............................ok
    t/06meta....................................ok
    t/07translationschemetype...................ok
    t/08tschemetypestar.........................ok
    t/09ts_with_defaultaction...................ok
    t/10ts_with_treereg.........................ok
    
    etc., etc...................................ok
    
    t/28unshifttwoitems.........................ok
    t/29foldinglistsofexpressions...............ok
    t/30complextreereg..........................ok
    t/32deletenodewithwarn......................ok
    t/33moveinvariantoutofloop..................ok
    t/34moveinvariantoutofloopcomplexformula....ok
    All tests successful, 5 subtests skipped.
    Files=33, Tests=113,  5 wallclock secs ( 4.52 cusr +  0.30 csys =  4.82 CPU)
    
    Introduzca una prueba SKIP similar a la anterior y otra que si el módulo Test::Pod esta instalado comprueba que la documentación esta bien escrita. Estudie la documentación del módulo Test::Pod.

  17. Introduzca pruebas TODO (que, por tanto, deben fallar) para las funciones que están por escribir (parser, Optimize, code_generator, transform). Repáse [*] [10]. Sigue un ejemplo:

    42 TODO: {
    43   local $TODO = "Randomly generated problem";
    44   can_ok('Algorithm::Knap01DP', 'GenKnap'); # sub GenKnap no ha sido escrita aún
    45 }
    

  18. Cuando compruebe el funcionamiento de su módulo nunca descarte que el error pueda estar en el código de la prueba. En palabras de Schwern

    Code has bugs. Tests are code. Ergo, tests have bugs.

    Michael Schwern

  19. Instale el módulo Devel::Cover. El módulo Devel::Cover ha sido escrito por Paul Johnson y proporciona estadísticas del cubrimiento alcanzado por una ejecución. Para usarlo siga estos pasos:
    pl@nereida:~/src/perl/YappWithDefaultAction$ cover -delete
    Deleting database /home/pl/src/perl/YappWithDefaultAction/cover_db
    pl@nereida:~/src/perl/YappWithDefaultAction$ HARNESS_PERL_SWITCHES=-MDevel::Cover make test
    PERL_DL_NONLAZY=1 /usr/bin/perl "-MExtUtils::Command::MM" "-e" "test_harness(0, 'blib/lib', 'blib/arch')" t/*.t
    t/01calc....................................ok 
    t/01calc....................................ok
    t/02Cparser.................................ok
            5/6 skipped: various reasons
    t/03newgrammar..............................ok 
    t/03newgrammar..............................ok
    t/04foldandzero.............................ok
    etc., etc. .................................ok
    t/34moveinvariantoutofloopcomplexformula....ok
    All tests successful, 5 subtests skipped.
    Files=33, Tests=113, 181 wallclock secs (177.95 cusr +  2.94 csys = 180.89 CPU)
    
    La ejecución toma ahora mucho mas tiempo: ¡181 segundos frente a los 5 que toma la ejecución sin cover !. Al ejecutar cover de nuevo obtenemos una tabla con las estadísticas de cubrimiento:

    pl@nereida:~/src/perl/YappWithDefaultAction$ cover
    Reading database from /home/pl/src/perl/YappWithDefaultAction/cover_db
    
    ---------------------------- ------ ------ ------ ------ ------ ------ ------
    File                           stmt   bran   cond    sub    pod   time  total
    ---------------------------- ------ ------ ------ ------ ------ ------ ------
    blib/lib/Parse/Eyapp.pm       100.0    n/a    n/a  100.0    n/a    0.2  100.0
    ...lib/Parse/Eyapp/Driver.pm   72.4   63.2   50.0   64.3    0.0   21.3   64.4
    ...ib/Parse/Eyapp/Grammar.pm   90.9   77.8   66.7  100.0    0.0   16.6   84.3
    blib/lib/Parse/Eyapp/Lalr.pm   91.4   72.6   78.6  100.0    0.0   48.3   85.6
    blib/lib/Parse/Eyapp/Node.pm   74.4   58.3   29.2   88.2    0.0    1.6   64.7
    ...ib/Parse/Eyapp/Options.pm   86.4   50.0    n/a  100.0    0.0    2.7   72.8
    ...lib/Parse/Eyapp/Output.pm   82.3   47.4   60.0   70.6    0.0    3.7   70.0
    .../lib/Parse/Eyapp/Parse.pm  100.0    n/a    n/a  100.0    n/a    0.2  100.0
    ...Parse/Eyapp/Treeregexp.pm  100.0    n/a    n/a  100.0    n/a    0.1  100.0
    blib/lib/Parse/Eyapp/YATW.pm   89.4   63.9   66.7   85.7    0.0    4.8   77.6
    ...app/_TreeregexpSupport.pm   73.1   33.3   50.0  100.0    0.0    0.4   60.8
    main.pm                        52.2    0.0    n/a   80.0    0.0    0.0   45.7
    Total                          83.8   64.7   60.0   84.5    0.0  100.0   75.5
    ---------------------------- ------ ------ ------ ------ ------ ------ ------
    
    Writing HTML output to /home/pl/src/perl/YappWithDefaultAction/cover_db/coverage.html ...
    pl@nereida:~/src/perl/YappWithDefaultAction$
    
    El HTML generado nos permite tener una visión mas detallada de los niveles de cubrimiento.

    Para mejorar el cubrimiento de tu código comienza por el informe de cubrimiento de subrutinas. Cualquier subrutina marcada como no probada es un candidato a contener errores o incluso a ser código muerto.

    Para poder hacer el cubrimiento del código usando Devel::Cover, si se usa una csh o tcsh se debe escribir:

    nereida:~/src/perl/YappWithDefaultAction> setenv HARNESS_PERL_SWITCHES -MDevel::Cover
    nereida:~/src/perl/YappWithDefaultAction> make test
    PERL_DL_NONLAZY=1 /usr/bin/perl "-MExtUtils::Command::MM" "-e" "test_harness(0, 'blib/lib', 'blib/arch')" t/*.t
    t/01calc....................................ok 
    t/01calc....................................ok
    t/02Cparser.................................ok
            5/6 skipped: various reasons
    t/03newgrammar..............................ok 
    t/03newgrammar..............................ok
    t/04foldandzero.............................ok
    t/05treewithvars............................ok
    t/06meta....................................ok 
    t/06meta....................................ok
    t/07translationschemetype...................ok
    ............................................ok
    t/38tspostfix_resultisarray.................ok
    t/39tspostfix...............................ok 
    All tests successful, 5 subtests skipped.
    Files=38, Tests=135, 210 wallclock secs (206.28 cusr +  3.27 csys = 209.55 CPU)
    nereida:~/src/perl/YappWithDefaultAction>
    
    Aún mas robusto - más independiente de la shell que usemos - es pasar las opciones en HARNESS_PERL_SWITCHES como parámetro a make:
    make HARNESS_PERL_SWITCHES=-MDevel::Cover test
    

    Añade el informe de cubrimiento al MANIFEST para que se incluya en la distribución que subas. Si lo consideras conveniente añade un directorio informes en los que vayan los informes asociados a esta práctica. Incluye en el README o en la documentación una breve descripción de donde están los informes.

  20. Se conoce con el nombre de perfilado o profiling de un programa al estudio de su rendimiento mediante un programa (conocido como profiler) que monitoriza la ejecución del mismo mediante una técnica que interrumpe cada cierto tiempo el programa para comprobar en que punto de la ejecución se encuentra. Las estadísticas acumuladas se vuelcan al final de la ejecución en un fichero que puede ser visualizado mediante la aplicación apropiada.

    En Perl hay dos módulos que permiten realizar profiling. El mas antiguo es Devel::DProf . La aplicación para visualizar los resultados se llama dprofpp . Sigue un ejemplo de uso:

    nereida:~/src/perl/YappWithDefaultAction/t> perl -d:DProf 02Cparser.t
    1..6
    ok 1 - use Parse::Eyapp;
    ok 2 - 233 syntactic variables
    ok 3 - 3 UUTERM
    ok 4 - 108 terminals
    ok 5 - 825 rules
    ok 6 - 1611 states
    nereida:~/src/perl/YappWithDefaultAction/t> dprofpp tmon.out
    Total Elapsed Time = 3.028396 Seconds
      User+System Time = 3.008396 Seconds
    Exclusive Times
    %Time ExclSec CumulS #Calls sec/call Csec/c  Name
     31.4   0.945  1.473   1611   0.0006 0.0009  Parse::Eyapp::Lalr::_Transitions
     17.5   0.528  0.528   1611   0.0003 0.0003  Parse::Eyapp::Lalr::_Closures
     16.1   0.486  0.892      1   0.4861 0.8918  Parse::Eyapp::Lalr::_ComputeFollows
     8.04   0.242  0.391      1   0.2419 0.3906  Parse::Yapp::Driver::_Parse
     8.04   0.242  0.242  11111   0.0000 0.0000  Parse::Eyapp::Lalr::__ANON__
     4.59   0.138  0.138   8104   0.0000 0.0000  Parse::Eyapp::Lalr::_Preds
     2.66   0.080  0.080      1   0.0800 0.0800  Parse::Eyapp::Lalr::_SetDefaults
     2.66   0.080  0.972      1   0.0800 0.9718  Parse::Eyapp::Lalr::_ComputeLA
     2.46   0.074  0.074   3741   0.0000 0.0000  Parse::Eyapp::Parse::_Lexer
     1.89   0.057  0.074   8310   0.0000 0.0000  Parse::Eyapp::Parse::__ANON__
     0.96   0.029  0.028      1   0.0288 0.0276  Parse::Eyapp::Lalr::_SolveConflict
                                                 s
     0.66   0.020  0.050      6   0.0033 0.0083  Parse::Eyapp::Output::BEGIN
     0.60   0.018  1.500      1   0.0176 1.4997  Parse::Eyapp::Lalr::_LR0
     0.53   0.016  0.259      3   0.0054 0.0863  Parse::Eyapp::Lalr::_Digraph
     0.33   0.010  0.010      1   0.0100 0.0100  Parse::Eyapp::Grammar::_SetNullable
    

    Tambien es posible usar el módulo -MDevel::Profiler :

    nereida:~/src/perl/YappWithDefaultAction/examples> perl -MDevel::Profiler eyapp 02Cparser.yp
    Unused terminals:
    
            END_OF_LINE, declared line 128
            ALL, declared line 119
            PRE_PARSED_CLASS_DECL, declared line 120
    
    27 shift/reduce conflicts and 22 reduce/reduce conflicts
    nereida:~/src/perl/YappWithDefaultAction/examples> dprofpp tmon.out
    Total Elapsed Time = 3.914144 Seconds
      User+System Time = 3.917144 Seconds
    Exclusive Times
    %Time ExclSec CumulS #Calls sec/call Csec/c  Name
     22.3   0.877  1.577   1611   0.0005 0.0010  Parse::Eyapp::Lalr::_Transitions
     17.8   0.700  0.700   1611   0.0004 0.0004  Parse::Eyapp::Lalr::_Closures
     15.6   0.614  1.185      1   0.6142 1.1854  Parse::Eyapp::Lalr::_ComputeFollow
                                                 s
     9.60   0.376  0.545      1   0.3758 0.5453  Parse::Yapp::Driver::_Parse
     7.99   0.313  0.313   8104   0.0000 0.0000  Parse::Eyapp::Lalr::_Preds
     5.85   0.229  0.229      3   0.0763 0.0763  Parse::Eyapp::Lalr::_Digraph
     4.06   0.159  0.159   3741   0.0000 0.0000  Parse::Eyapp::Parse::_Lexer
     3.32   0.130  0.130      1   0.1300 0.1300  Parse::Eyapp::Lalr::DfaTable
     2.27   0.089  0.089      1   0.0890 0.0890  Parse::Eyapp::Lalr::_SetDefaults
     2.04   0.080  1.265      1   0.0800 1.2654  Parse::Eyapp::Lalr::_ComputeLA
     1.17   0.046  0.057      1   0.0464 0.0567  Parse::Eyapp::Grammar::Rules
     1.02   0.040  1.617      1   0.0397 1.6169  Parse::Eyapp::Lalr::_LR0
     0.77   0.030  0.030   1185   0.0000 0.0000  Parse::Eyapp::Lalr::_FirstSfx
     0.71   0.028  0.039      1   0.0284 0.0387  Parse::Eyapp::Grammar::RulesTable
     0.54   0.021  0.021   1650   0.0000 0.0000  Parse::Eyapp::Grammar::classname
    

    Presente un informe del perfil de su compilador. Añade el informe del perfil al MANIFEST para que se incluya en la distribución que subas.

  21. El módulo Devel::Size proporciona la posibilidad de conocer cuanto ocupa una estructura de datos. Considere el siguiente ejemplo:
     71 .................................... codigo omitido
     72
     73 use Devel::Size qw(size total_size);
     74 use Perl6::Form;
     75
     76 sub sizes {
     77   my $d = shift;
     78   my ($ps, $ts) = (size($d), total_size($d));
     79   my $ds = $ts-$ps;
     80   return ($ps, $ds, $ts);
     81 }
     82
     83 print form(
     84 ' ==============================================================',
     85 '| VARIABLE | SOLO ESTRUCTURA |     SOLO DATOS |          TOTAL |',
     86 '|----------+-----------------+----------------+----------------|',
     87 '| $parser  | {>>>>>>} bytes  | {>>>>>>} bytes | {>>>>>>} bytes |', sizes($parser),
     88 '| $t       | {>>>>>>} bytes  | {>>>>>>} bytes | {>>>>>>} bytes |', sizes($t),
     89 ' ==============================================================',
     90 );
    
    Al ejecutarlo se obtiene esta salida:

     ....... salida previa omitida
    
     ==============================================================
    | VARIABLE | SOLO ESTRUCTURA |     SOLO DATOS |          TOTAL |
    |----------+-----------------+----------------+----------------|
    | $parser  |      748 bytes  |      991 bytes |     1739 bytes |
    | $t       |       60 bytes  |     1237 bytes |     1297 bytes |
     ==============================================================
    
    Elabore un informe con el consumo de memoria de las variables mas importantes de su programa. Añadelo el informe al MANIFEST para que se incluya en la distribución que subas. Explica en el README o en la documentación el significado de los ficheros de informe.


Repaso: Pruebas en el Análisis Léxico

  1. ¿Cuál es la diferencia entre los operadores == y eq?
  2. ¿Cuáles son los parámetros de la función ok?
  3. ¿Cuáles son los parámetros de la función is?
  4. ¿Porqué es conveniente nombrar las pruebas con un nombre que empiece por un número?
  5. ¿Como puedo ejecutar los tests en modo verbose?
  6. ¿Como puedo probar un código que produce la detención del programa?
  7. ¿Que contiene la variable $@?
  8. ¿Que hace la función like?
  9. ¿Que contiene la variable $#-? ¿Y $+? (Consulte 31.1.4)
  10. ¿Porqué la función use_ok es llamada dentro de un BEGIN?
  11. ¿Que es una prueba SKIP?
  12. ¿Que es una prueba TODO?
  13. ¿Que hace la función pod_file_ok? ¿A que módulo pertenece?
  14. ¿Que hace el operador tr?
  15. ¿Qué devuelve el operador tr?
  16. ¿Que hace la opción d de tr? (consulte 31.5)
  17. Explique la conducta de la siguiente sesión con el depurador:
      DB<1> $a = '"Focho \"mucha\" chufa"'
      DB<2> print $a
    "Focho \"mucha\" chufa"
      DB<3> print $& if $a =~ m{^"([^"]|\\")*"}
    "Focho \"
      DB<4> print $& if $a =~ m{^"(\\"||[^"])*"}
    "Focho \"mucha\" chufa"
    
  18. Supuesto que tuvieramos el operador menos en nuestro lenguaje y dada la entrada a = 2-3, ¿Que devolverá su analizador léxico? ¿Devuelve ID PUN NUM NUM o bien ID PUN NUM PUN NUM? (hemos simplificado el flujo eliminando los atributos).
  19. ¿Que hace la llamada use Test::More qw(no_plan);?
  20. ¿Que hace la función can_ok? ¿Qué argumentos tiene?
  21. Explique las causas de la siguiente conducta del depurador:
      DB<1> $a='4+5'
      DB<2> print "($&) " while ($a =~ m/(\G\d+)/gc) or ($a =~ m/(\G\+)/gc);
    (4) (+) (5)
      DB<3> $a='4+5' # inicializamos la posición de búsqueda
      DB<4> print "($&) " while ($a =~ m/(\G\d+)/g) or ($a =~ m/(\G\+)/g);
    (4)
      DB<5> $a='4+5'
      DB<6> print "($&) " while ($a =~ m/\G\d+|\G\+/g)
    (4) (+) (5)
    
  22. ¿Que diferencia hay entre is_deeply e is?

  23. ¿Que argumentos recibe la función throws_ok? ¿En que módulo se encuentra?

  24. ¿Que hace el comando HARNESS_PERL_SWITCHES=-MDevel::Cover make test?

  25. ¿Cómo se interpreta el cubrimiento de las sentencias? ¿y de las subrutinas? ¿y de las ramas? ¿y las condiciones lógicas? ¿En cual de estos factores es realista y deseable lograr un cubrimiento del %100 con nuestras pruebas?

  26. ¿Que pasa si después de haber desarrollado un número de pruebas cambio la interfaz de mi API?

  27. ¿Que hace el comando perl -d:DProf programa? ¿Para que sirve?

Casiano Rodríguez León
2013-04-23