Let me try to explain to you, what to my taste is characteristic for all intelligent thinking. It is, that one is willing to study in depth an aspect of one's subject matter in isolation for the sake of its own consistency, all the time knowing that one is occupying oneself only with one of the aspects. ...Nothing is gained... by tackling these various aspects simultaneously. --Edsger W. Dijkstra
I have a play template, a general script for fooling around with stuff I may or may not understand very well, such as an idiom for testing prime numbers or one for scraping email addys out of arbitrary text (in case I ever want to sell breast enlargement pills). It's not a rigid framework so much as a general way of doing stuff, especially unknown stuff; and in such a way that I can show it to others and say, Hey, why doesn't this work?
The example I give here is quite long so I show it at the bottom of this node. You may want to open the code in another window to follow along with my discussion of it.
There are several things I want this template to do. First, I want to have a template. I don't want to be typing stuff over and over. When I start a new fooling-around script, I want to start with something that works; I don't want to get stuck on the boring stuff.
I want the instanced script to be readable to others. That's a broad audience. Experienced Monks will find a lot of stuff offensively verbose; newer inductees will find other stuff unreadable because it's too terse. I need all the help I can get so I try to cover the range here.
I want a flexible template. Nothing here is inviolable; I might add, delete, or change anything. When I do that, I don't want the whole house of cards to come crashing down.
The hashbang line invokes my development interpreter.
Even though this is just fooling around, the code may get around. So, I copyright it, license it, and be sure that anyone with questions can email me.
I might just run something without strict or without warnings. It's not likely. If I do, I'll disable, not delete, the line -- even if I publish the script. I want to advertise clearly any suspension of strictures or warnings.
There are certain modules I use frequently. I could delete or comment out almost all of the ones I show here, in this example. For that matter, I could easily add to the list. In another file, I have a sort of canonical list of modules I like, with argument lists, ready to be pasted in.
Since every module use()-ed anywhere is loaded at "compile time", I don't see a good reason not to collect all use lines before I start writing code. Why make anyone look for the big surprise later?
I have a scrap file of bits of code such as the section separator shown.
A Monk objected to @test_data: "Test data is useless... Test data is junk. Real data is what I use..." You're strongly suggested to use real data from your real project when experimenting. It's okay to cut it down. At first, I might test only 3 records; when I have those working, paste in 12 more. By the time I have my experiment running correctly over 20-30 records, it's probably time to turn off the experiment, stuff the routine into my real project, and see how it performs. But still, the data in the experiment is called "test data". If this offends you, call this variable something else; but please avoid @data.
The test data is presented in the form of a table as a single multilevel variable. The base variable is an array because later, we loop through it and run play_sub() on each line. Each line is an individual test. In the template I show, each individual given is a simple scalar. So is each expected result. Your data might be more complex, of course.
Here's a small portion of a much longer and more complex table:
# declare test inputs and expected outputs # # KEY MEANING DEFAULT # # -basename 1st part of subtest name undef # -name 2nd part of subtest name undef # # -skip don't test if 1 undef # # -inparms parms passed to sub under test empty list () # -argv @ARGV set before call untouched # -env %ENV set before call untouched # # -return return from sub under test 1 # # -stdout STDOUT don't test # -string exact string eq # -regex regex against # -matches number of regex matches 1 # -lines number of lines captured # # -stderr STDERR don't test # # -evalerr eval error $@ 0 (no error) # my @test_data = ( # test options parsing by Getopt::Long # if *only* a bad @ARGV then no %options should be parsed out { -name => 'foobar-opt', -argv => [ 'foobar' ], # bad argument -inparms => [ $opt_flag ], # abort with \%opt +ions -return => {}, # empty hashref in + return -stdout => 0, # expect exactly n +othing # -dump => 1, }, # error conditions { -name => 'foobar', -argv => [ 'foobar' ], # bad argument -inparms => [ ], # normal execution -return => 0, # perl failure -stdout => 0, # expect exactly n +othing -stderr => { -regex => [ qw{ bad command line usage hump help} ], -matches => 6, -lines => 3, }, # -dump => 1, }, { -name => '-hh(help)-disp', -inparms => [ $disp_flag ], # abort with $disp +atch -return => [ q{h}, q{help} ], # $dispatch, $do_p +arm -stdout => 0, # expect exactly n +othing -stderr => 0, # expect exactly n +othing }, );
Key is to prepare ahead of time all the data I am going to run through play_sub(), whatever that may become, and my expectations for what it might return, before writing play_sub() -- before writing a line of it. Yes, this is test-driven development (TDD). I may feel I want to take a more relaxed approach; fine. Write the routine first, then write the data. But absolutely write in some data and my expectations for each chunk of it. People who read only this far may well be able to tell me what I need to know.
Note the giant block comment atop the @test_data table. If the data is complex enough to warrant this kind of hash-keyed approach, I want to be sure to document what each key means. The right place for this documentation is right before the data itself.
Now comes play_sub() itself. I'm not going to comment on it because it's obviously a nonsensical placeholder. Writing good subroutine declarations is a topic for another node. In the template, this just means Your code goes here ------->.
I put in another full-width separator because I'm shifting gears from declaring data and code to executing them.
In the execution loop, I pull out a given from @test_data and run it through play_sub(). To keep my concerns separate, I simply store the results. I'm mindful of the fact that play_sub() may well fatal out on some givens and not on others, so I trap that in an eval block. Again, this can be elaborated. Here's an execution block I'm using elsewhere (inside of the loop):
# execute code within Test::Hump-ish box $self->{-capture}{-stdout}->start(); # STDOUT captured $self->{-capture}{-stderr}->start(); # STDERR captured { try { $self->{-got}{-return} = &$self->{coderef}( $self->{-inparms} + ); } catch { $self->{-got}{-evalerr} = $_; }; } $self->{-capture}{-stdout}->stop(); # not captured $self->{-capture}{-stderr}->stop(); # not captured
In this example, everything, including a ref to the code being tested, is stored in $self. STDOUT, STDERR, the intended return value of the routine, and any error emitted (not, strictly, $@) are all captured from the execution.
I don't suggest that all this is needed in a simple what-if script. You might not even bother to wrap the call in eval{ }. Key is just to loop through your givens and store your results.
Comes another separator and time to examine results. Dumping stuff out is tedious; that's why we have tools for it. I've shown five approaches.
I've defended Smart::Comments. You can hardly beat it for terseness, an important consideration if you're rattling off a play-around script with a very short life expectancy. It uses Data::Dumper internally so its output format is similar.
Data::Dumper is perhaps the conventional dump method. I found I could get a lot out of it by study-up on its docs and tweaking its various configuration variables. It's polite to do this sort of thing inside a bare block so your weirdo setup doesn't leak; you can omit this in your play script, if you dare.
Either of the above approaches looks kinda stupid on the particular set of example @test_data I've chosen. For other setups, they're great.
I've only recently started to use Perl6:Form, the replacement for the nasty format/formline/write process. It's pretty slick, although it requires a slightly different way of looking at your stuff. It was difficult to write the first one; it was much easier to copy-paste-edit the second. I get much more control over output this way. One shortcoming is that it doesn't always handle undef values well; you need to groom your data a little (not shown) before the dump.
It's often enough just to dump to screen and see what you got. I might want to automate this, though. After all, if results differ from expectations, I want to see much more than if they agree; a dump, well, dumps everything. Here's two approaches:
Test::More is so good and so very widely-used that I figure, me too. I wrote many hand-rolled comparisons until I realized I didn't have to do them. As usual, I get more control but have to pay a price in labor. Either way, the actual comparison needs to be chosen carefully. is() uses eq() internally. Test::More (and friends) have many other methods of comparison. Use them.
When I get to the point where I'm done, or temporarily done, or stuck, or I want to cry for help, I go to Terminal, copy the whole output of my script, and paste it off the end of my script between =pod and =cut tags. (I indent one level, which tells POD formatters to treat it as preformatted instead of word-wrapping it.) I consider this an important step. Not everyone wants to download and execute my crummy script just to see what it does; some won't. This also avoids the very nasty bug: "It works fine for me." Also, it advertises to any reader that I have, indeed, executed the script before complaining that it "doesn't work".
Comment your code! You'll notice plenty of comments in this script, even though it's a template for throwaways. I was taught the Old Rule: One line of comment for every one line of code. Modern languages have clearer syntax, making many comments unnecessary. But not all! You expect others to look at your script? Tell us what you're trying to do. Tell us in the script, right next to the code you're trying.
Use whitespace! Please don't say, "Oh noes, I don't wanna kill another electron just to insert some non-essential whitespace." This is demo code, hence unpredictable. You see many blank lines in the code. This is a good thing. You see many lines of say q{}; to insert blank lines in output. This is also a good thing.
Label your output. The main reason I like Smart::Comments is that it takes some of the tedium out of this task. But I also introduce blocks of output with little lines like say 'Data::Dumper dump:';, just as I introduce the block of code with # Dump using Data::Dumper:. You might say this is redundant; it is. Sometimes you wind up in a situation where it looks much, much less redundant.
Be neat. It can be argued that the Superior Programmer can read past any sloppyness and anyway, such gingerbread is for novices who carry umbrellas to the bathroom. Go ahead and argue. But be neat.
The most important points are:
This should not be considered a ready-to-run script. You need to modify this to do what you want.
#!/run/bin/perl # play-template.pl # = Copyright 2010 Xiong Changnian <xiong@xuefang.com> = # = Free Software = Artistic License 2.0 = NO WARRANTY = # Pragmata; comment out to disable use strict; use warnings; # Commonly-used modules; delete if unused use Readonly; use feature qw(switch say state); use Perl6::Junction qw( all any none one ); use List::Util qw(first max maxstr min minstr reduce shuffle sum); use File::Spec; use File::Spec::Functions qw( catdir catfile catpath splitpath curdir rootdir updir canonpath ); use Cwd; use Smart::Comments '###', '####'; # Modules specially used in this script; if any use Perl6::Form; use Data::Dumper; use Test::More; #--------------------------------------------------------------------- +-------# # Put given test data and expected results in a table; actual results +to come. my @test_data = ( # given expected [ 0 => 0 ], [ 1 => 1 ], [ 2 => 0 ], [ 3 => 1 ], [ 4 => 0 ], [ 17 => 0 ], [ 42 => 0 ], [ PI => 0 ], [ 4423 => 0 ], [ -0.5 => 0 ], ); # Implement the subroutine to test -- this could be anything sub play_sub { my $given = $_[0]; my $result ; #force a demo error if ( $given eq "PI" ) { $result = (1-1)/(1-1); }; $result = $given %2; # 0 if even, 1 if odd return $result }; #--------------------------------------------------------------------- +-------# # Loop through @test_data, executing play_sub() and capturing the resu +lt. foreach my $test (@test_data) { my $given = $test->[0]; my $expected = $test->[1]; my $actual ; my $error ; $actual = eval{ play_sub( $given ) }; $error = $@; # You might insert other work here. if ( $error && !$actual ) { $actual = 'undef'; }; # Store results $test->[2] = $actual; $test->[3] = $error; }; #--------------------------------------------------------------------- +-------# # Dump @test_data; choose one of the alternatives # and delete the rest or write your own... ### Dump using Smart Comments : ### @test_data # OR # Dump using Data::Dumper: say q{}; say 'Data::Dumper dump:'; { # See D::D POD for some choices here local $Data::Dumper::Indent = 2; local $Data::Dumper::Purity = 0; local $Data::Dumper::Terse = 1; local $Data::Dumper::Sortkeys = 0; print Dumper(@test_data); } say q{}; # OR # Dump using Perl6::Form: say 'Perl6::Form dump:'; say q*| given expected actual error + |*; say q*|-------------------------------------------------------------- +-----|*; foreach my $test (@test_data) { if ( $test->[3] ) { # error may overflow field print form q*| {<<<<<} {<<<<<} {<<<<<} {<<<<<<<<<<<<<<<<<<<<<<<<< +<<<} |*, $test->[0], $test->[1], $test->[2], $test->[3], q*| {VVVVVVVVVVVVVVVVVVVVVVVVV +VVV} |* ; } else { print form q*| {<<<<<} {<<<<<} {<<<<<} {<<<<<<<<<<<<<<<<<<<<<<<<< +<<<} |*, $test->[0], $test->[1], $test->[2], $test->[3], ; }; }; say q*|-------------------------------------------------------------- +-----|*; say q{}; # Compare expectations with actual results... # Do it by hand: say q{}; say 'Testing by hand:'; foreach my $test (@test_data) { my $given = $test->[0]; my $expected = $test->[1]; my $actual = $test->[2]; my $error = $test->[3]; if ($error) { say "Given: $given: Error: $error"; }; if ($actual ne $expected) { # $actual != $expected + if num say "Given: $given: Actual: $actual Expected: $expected"; }; }; say q{}; # OR # Test with Test::More: say q{}; say 'Testing with Test::More:'; my $test_counter ; foreach my $test (@test_data) { my $given = $test->[0]; my $expected = $test->[1]; my $actual = $test->[2]; my $error = $test->[3]; if ($error) { fail( 'Given: ' . $given ); $test_counter++; diag( $error ); }; is($actual, $expected, 'Given: ' . $given); $test_counter++; }; done_testing($test_counter); #--------------------------------------------------------------------- +-------# # Report end of script execution. say 'Done.'; __END__ =pod Output: ### Dump using Smart Comments : ### @test_data: [ ### [ ### 0, ### 0, ### 0, ### '' ### ], ### [ ### 1, ### 1, ### 1, ### '' ### ], ### [ ### 2, ### 0, ### 0, ### '' ### ], ### [ ### 3, ### 1, ### 1, ### '' ### ], ### [ ### 4, ### 0, ### 0, ### '' ### ], ### [ ### 17, ### 0, ### 1, ### '' ### ], ### [ ### 42, ### 0, ### 0, ### '' ### ], ### [ ### 'PI', ### 0, ### 'undef', ### 'Illegal division by zero at ./play-template.p +l line 60. ' ### ], ### [ ### 4423, ### 0, ### 1, ### '' ### ], ### [ ### '-0.5', ### 0, ### 0, ### '' ### ] ### ] Data::Dumper dump: [ 0, 0, 0, '' ] [ 1, 1, 1, '' ] [ 2, 0, 0, '' ] [ 3, 1, 1, '' ] [ 4, 0, 0, '' ] [ 17, 0, 1, '' ] [ 42, 0, 0, '' ] [ 'PI', 0, 'undef', 'Illegal division by zero at ./play-template.pl line 60. ' ] [ 4423, 0, 1, '' ] [ '-0.5', 0, 0, '' ] Perl6::Form dump: | given expected actual error + | |----------------------------------------------------------------- +--| | 0 0 0 + | | 1 1 1 + | | 2 0 0 + | | 3 1 1 + | | 4 0 0 + | | 17 0 1 + | | 42 0 0 + | | PI 0 undef Illegal division by zero at + | | ./play-template.pl line 60. + | | 4423 0 1 + | | -0.5 0 0 + | |----------------------------------------------------------------- +--| Testing by hand: Given: 17: Actual: 1 Expected: 0 Given: PI: Error: Illegal division by zero at ./play-template.pl l +ine 60. Given: PI: Actual: undef Expected: 0 Given: 4423: Actual: 1 Expected: 0 Testing with Test::More: ok 1 - Given: 0 ok 2 - Given: 1 ok 3 - Given: 2 ok 4 - Given: 3 ok 5 - Given: 4 not ok 6 - Given: 17 # Failed test 'Given: 17' # at ./play-template.pl line 177. # got: '1' # expected: '0' ok 7 - Given: 42 not ok 8 - Given: PI # Failed test 'Given: PI' # at ./play-template.pl line 172. # Illegal division by zero at ./play-template.pl line 60. not ok 9 - Given: PI # Failed test 'Given: PI' # at ./play-template.pl line 177. # got: 'undef' # expected: '0' not ok 10 - Given: 4423 # Failed test 'Given: 4423' # at ./play-template.pl line 177. # got: '1' # expected: '0' ok 11 - Given: -0.5 1..11 Done. # Looks like you failed 4 tests of 11. =cut
|
|---|
| Replies are listed 'Best First'. | |
|---|---|
|
Re: Play Template
by BrowserUk (Patriarch) on Aug 01, 2010 at 07:21 UTC | |
by Xiong (Hermit) on Aug 01, 2010 at 09:56 UTC | |
by BrowserUk (Patriarch) on Aug 01, 2010 at 10:14 UTC | |
by Xiong (Hermit) on Aug 01, 2010 at 11:49 UTC | |
by BrowserUk (Patriarch) on Aug 01, 2010 at 12:35 UTC | |
by BrowserUk (Patriarch) on Aug 03, 2010 at 21:33 UTC | |
by BrowserUk (Patriarch) on Aug 12, 2010 at 18:30 UTC | |
|