thekestrel has asked for the wisdom of the Perl Monks concerning the following question:
Following a post I made here on testing Edges (i.e. Not just testing what should be going into a subroutine, but what shouldn't) and I wanted to see if you can created Automated tests as I've just started writing the testing routines in t/ for my module and there is around 20 per subroutine and that is just the basics. I'm not sure if there are ways that exist that already do this, but I wanted to try out my ideas.
I know that good module design says, write the tests and documentation first and then thee may chisel out some code, but I know that this isn't always the case in practice. The amount of times I've typed make test and had it say 1 test successful i.e. There was just the default test created after they used h2xs to make the module.
So I was thinking how could one write some code to auto generate some tests and this is what I came up with. Similar to electric fence when you want to check for memory overruns in C, instead of just hooking Malloc I hooked every single subroutine. Now every subroutine gets routed through my hooked versions first. From there you can analyze the incoming data on the fly to make generalizations about the system.
So the idea is you run this setup script over your module and it hooks all of the subroutines and calls from the subroutines. Then you run all of your scripts that exercise your module and its gathers information about them. After you've finished running tests you run a second script which uses the logged data to generate .t test files that can be used in make test. The neat thing is you know the configuration of 'real' data so you can send the functions sample data an expect the same results or moreover you now know what it isn't expecting generally.
So to that end here is generalize_setup.plx which hooks all of the code. *I'm sure somewhere a whole group of readable code Gods are turning over in their graves (if they died =P), with this code, but this is the first version*
( Update: Now have two files per subroutine .data which has the runtime data and .type which stores the operator type information into a subroutine. It can also handle a module a few layers down i.e. My::Deep::Module. Also cleaned up code a lil' (ok everything is relative =P) )#!/usr/bin/perl use strict; use warnings; #Check command line arguments die "Incorrect number of arguments\n\tFormat : generalize_setup <file> +\n\n" if ( $#ARGV != 0 ); my $file = $ARGV[0]; my ($file_noext, $ext) = split '\.', $file; #load in the code to hooked print "Loading file \'$file\' for processing\n"; open "IN", "<$file" or die "Could not open file for read : $!\n"; my @old_code = <IN>; close(IN); #Back up the old code print "Backing up file \'$file\' to \'$file.bak\'\n"; open "BACKUP", ">$file.bak" or die "Could not open file for write : $! +\n"; print BACKUP @old_code; close (BACKUP); my @new_code; my $linenum = 1; my $subcount = 0; my @subroutines; my $packageline; my $package; my $dir = ""; my $new_package = ""; #do the subroutines foreach (@old_code) { if ( $linenum == 2) { my $pl = $packageline; $pl = s/package/use/; push @new_code, "$pl;\n"; } if ( /^package/ ) { $packageline = $_; $packageline =~ /\w+\s+(.+)\;/; $package = $1; my @bits = split "::", $package; for ( 0 .. scalar @bits - 2 ) { $dir .= $bits[$_] . "/"; $new_package .= $bits[$_] . "::"; } $new_package .= "_" . $bits[scalar @bits - 1]; chop $dir; push @new_code, "package $new_package;\n"; } elsif ( /^sub/ ) { my ($pre, $subroutine, $post) = split ' ', $_; push @new_code, "$pre __$subroutine $post\n"; $subcount++; push @subroutines, $subroutine; } else { push @new_code, $_; } $linenum++; } #set the data directory my $datadir = "$dir\\/data"; #do the calls foreach (@new_code) { #s/add_input\s*\(/$package\:\:add_input\(/; foreach my $s ( @subroutines ) { if ( /$s\s*\(/ ) { if ( !/>$s/ ) { s/$s\s*\(/$package\:\:$s\(/; } } #fix up &func's if ( /\&$s/ ) { s/$s/__$s/; } } } #Write the modified old code open "OUT", ">$dir/_Neural.pm" or die "Could not open file for write : + $!\n"; print OUT $_ foreach ( @new_code ); close(OUT); #Create the hooks print "Replacing \'$file\' code with hooked version\n"; open "HOOK", ">$file" or die "Could not open file for write : $!\n"; print HOOK "$packageline\n\n"; print HOOK "use $new_package;\n\n"; print HOOK "use attributes;\n\n"; foreach my $sub ( @subroutines ) { print HOOK <<__END__; #Hook from __$sub to $sub sub $sub { my \$string; open "SUB_TYPE", ">>$datadir\\/$sub.type" or die "Could not open f +ile for write : \$!\\n"; open "SUB_DATA", ">>$datadir\\/$sub.data" or die "Could not open f +ile for write : \$!\\n"; my \$args; foreach my \$arg ( \@_ ) { \$args .= \$arg . ","; } chop \$args; \$args .= "\n"; print SUB_DATA \$args; close (SUB_DATA); foreach \$attr ( \@_ ) { if ( ref(\$attr) ) { \$string .= "REF:" . ref(\$attr) . ","; } else { \$string .= attributes::reftype(\\\$attr) . ","; } } chop \$string; \$string .= "\\n"; print SUB_TYPE \$string; close (SUB_TYPE); return $new_package\:\:__$sub(\@_); } __END__ } print HOOK "1;\n"; close (HOOK); #Create a directory and blank file(s) for data system ("mkdir $datadir"); foreach my $sub ( @subroutines ) { open "SUB", ">$datadir/$sub.type" or die "Could not open file for +write : $!\n"; close (SUB); } print "Hooked $subcount subroutines\n\nNow exercise code...\n\n";
Here is an example of the code that is produced for the hooks.
package AI::Neural; use AI::_Neural; use attributes; #Hook from __new to new sub new { my $string; open "SUB_TYPE", ">>AI\/data\/new.type" or die "Could not open fil +e for write : $!\n"; open "SUB_DATA", ">>AI\/data\/new.data" or die "Could not open fil +e for write : $!\n"; my $args; foreach my $arg ( @_ ) { $args .= $arg . ","; } chop $args; $args .= "\n"; print SUB_DATA $args; close (SUB_DATA); foreach $attr ( @_ ) { if ( ref($attr) ) { $string .= "REF:" . ref($attr) . ","; } else { $string .= attributes::reftype(\$attr) . ","; } } chop $string; $string .= "\n"; print SUB_TYPE $string; close (SUB_TYPE); return AI::_Neural::__new(@_); } ..... .....
Running it over my module we get
Loading file 'Neural.pm' for processing Backing up file 'Neural.pm' to 'Neural.pm.bak' Replacing 'Neural.pm' code with hooked version Hooked 37 subroutines Now exercise code...
Now run one of my scripts that excercises the module. The script runs as per normal as expected (just a smidgen slower =P. I create a directory called data and store data on each of the subroutines. As you can see only subroutines that get exercised have data. A .type file is created for each subroutine as we can still make tests that check type just not check the data.
total 21 drwxr-xr-x+ 2 paul matthews mkgroup-l-d 0 Apr 1 20:21 . drwxr-xr-x+ 3 paul matthews mkgroup-l-d 0 Apr 1 20:28 .. -rw-r--r-- 1 paul matthews mkgroup-l-d 0 Apr 1 20:21 AddEdgeOverRa +ndomVertex.type -rw-r--r-- 1 paul matthews mkgroup-l-d 0 Apr 1 20:21 AddRandomConn +ectedNeuron.type -rw-r--r-- 1 paul matthews mkgroup-l-d 0 Apr 1 20:21 AddRandomEdge +.type -rw-r--r-- 1 paul matthews mkgroup-l-d 0 Apr 1 20:21 AddRandomNeur +on.type -rw-r--r-- 1 paul matthews mkgroup-l-d 0 Apr 1 20:21 AddVertexOver +RandomEdge.type -rw-r--r-- 1 paul matthews mkgroup-l-d 0 Apr 1 20:21 DeepCopy.type -rw-r--r-- 1 paul matthews mkgroup-l-d 144 Apr 1 20:21 GetNewVertexN +umber.data -rw-r--r-- 1 paul matthews mkgroup-l-d 11 Apr 1 20:21 GetNewVertexN +umber.type -rw-r--r-- 1 paul matthews mkgroup-l-d 0 Apr 1 20:21 RandomizeVert +exActivationFunction.type -rw-r--r-- 1 paul matthews mkgroup-l-d 0 Apr 1 20:21 SaveNetwork.t +ype -rw-r--r-- 1 paul matthews mkgroup-l-d 0 Apr 1 20:21 XMLout.type -rw-r--r-- 1 paul matthews mkgroup-l-d 6 Apr 1 20:21 __PACKAGE -rw-r--r-- 1 paul matthews mkgroup-l-d 0 Apr 1 20:21 activation_si +gmoid.type -rw-r--r-- 1 paul matthews mkgroup-l-d 0 Apr 1 20:21 activation_ta +nh.type -rw-r--r-- 1 paul matthews mkgroup-l-d 287 Apr 1 20:21 add_edge.data -rw-r--r-- 1 paul matthews mkgroup-l-d 32 Apr 1 20:21 add_edge.type -rw-r--r-- 1 paul matthews mkgroup-l-d 0 Apr 1 20:21 add_graph.typ +e -rw-r--r-- 1 paul matthews mkgroup-l-d 83 Apr 1 20:21 add_input.dat +a -rw-r--r-- 1 paul matthews mkgroup-l-d 18 Apr 1 20:21 add_input.typ +e -rw-r--r-- 1 paul matthews mkgroup-l-d 92 Apr 1 20:21 add_neuron.da +ta -rw-r--r-- 1 paul matthews mkgroup-l-d 32 Apr 1 20:21 add_neuron.ty +pe ..... ...l. .....
There are a number of different tests we can add, at the moment the file just has the variable type information, but you can also store the realtime data too. From this you can take random sample and run a select few through the subroutines created as tests so that when a user runs make test they have a more robust suite.
I'm still writing the tests, but this is the second script generalize_tests.plx thus far.
(Update:First version that produces actual tests now up.)
#!/usr/bin/perl use strict; use warnings; #Check command line arguments die "Incorrect number of arguments\n\tFormat : generalize_tests <file> +\n\n" if ( $#ARGV != 0 ); my $package = $ARGV[0]; my @bits = split "/", $package; my $datadir = ""; my $new_package = ""; for ( 0 .. scalar @bits - 2 ) { $datadir .= $bits[$_] . "/"; $new_package .= $bits[$_] . "::"; } $new_package .= $bits[scalar @bits - 1]; $new_package =~ s/\.pm//; print "NEW PACKAGE : $new_package\n"; chop $datadir; #make a directory to store the tests my $testdir = "t"; if ( ! -d "$datadir/$testdir" ) { system ("mkdir $datadir/$testdir"); +} #get the list of subroutines my @files = glob("$datadir/data/*.type"); print "Creating tests for the following Subroutines\n\n"; my $f; foreach $f ( sort @files ) { if ( -s $f ) { $f =~ /$datadir\/data\/(\w+).type/; my $subroutine = $1; print "$subroutine "; open SUB, "<$f" or die "Could not open file for read2 : $!\n"; my @lines = <SUB>; close (SUB); my %hash; #Now create a hash of hashes of what each subroutine can take +as inputs foreach my $l (@lines) { chomp $l; my @arr = split ',', $l; for ( 0 .. scalar @arr - 1) { ${$hash{$_}}{$arr[$_]} = 1; } } foreach my $h ( keys %hash ) { #print "[$h] : "; foreach my $k ( keys %{$hash{$h}} ) { # print "$k "; } #print "\n"; } #Now create tests based on the input parameters. $f =~ s/\/data\//\/t\//; $f =~ s/\.type/\.t/; open "TEST", ">$f" or die "Could not open file for write : $!\ +n"; print TEST <<__END__; use Test::More tests=> 1; use $new_package; my \$self = $new_package->new(); ok(\$self->$subroutine(), "Testing $subroutine with no args"); __END__ close (TEST); } } print "\n\n";
which gives the following output...
Creating tests for the following Subroutines GetNewVertexNumber add_edge add_input add_neuron add_output new run_ne +twork run_node set_edge_weights set_input_value
Which as you can see makes .t files for the excercised functions.
total 10 drwxr-xr-x+ 2 paul matthews mkgroup-l-d 0 Apr 1 16:32 . drwxr-xr-x+ 4 paul matthews mkgroup-l-d 0 Apr 1 16:34 .. -rw-r--r-- 1 paul matthews mkgroup-l-d 37 Apr 1 16:34 GetNewVertexNu +mber.t -rw-r--r-- 1 paul matthews mkgroup-l-d 37 Apr 1 16:34 add_edge.t -rw-r--r-- 1 paul matthews mkgroup-l-d 37 Apr 1 16:34 add_input.t -rw-r--r-- 1 paul matthews mkgroup-l-d 37 Apr 1 16:34 add_neuron.t -rw-r--r-- 1 paul matthews mkgroup-l-d 37 Apr 1 16:34 add_output.t -rw-r--r-- 1 paul matthews mkgroup-l-d 37 Apr 1 16:34 new.t -rw-r--r-- 1 paul matthews mkgroup-l-d 37 Apr 1 16:34 run_network.t -rw-r--r-- 1 paul matthews mkgroup-l-d 37 Apr 1 16:34 run_node.t -rw-r--r-- 1 paul matthews mkgroup-l-d 37 Apr 1 16:34 set_edge_weigh +ts.t -rw-r--r-- 1 paul matthews mkgroup-l-d 37 Apr 1 16:34 set_input_valu +e.t
I'm still writing the test generation code, but wanted to get feedback on the concept and the approach? Are there already 15 modules that do this 10 times better (I'm still going to do it for the mental exercise anyways). Does anyone think that this would be useful or can offer some ways to improve (mainly the concept not my badly written code.
(Update:The main point of this is to see if I can get it to stress my code and point out areas that should or should not fail. Has this worked? YES! On my first set of tests that just runs each subroutine with no Arguments several ran through without aborting as I hadn't added boundary condition code.)
This might not run every module style at this stage, but its given me a better understanding of code structure and argument types along the way.
|
---|
Replies are listed 'Best First'. | |
---|---|
Re: Automated Edge Testing
by BrowserUk (Patriarch) on Apr 01, 2005 at 23:56 UTC | |
by thekestrel (Friar) on Apr 02, 2005 at 01:10 UTC | |
Re: Automated Edge Testing
by pernod (Chaplain) on Apr 04, 2005 at 08:23 UTC |