# New Ticket Created by  Mark Glines 
# Please include the string:  [perl #51718]
# in the subject line of all future correspondence about this issue. 
# <URL: http://rt.perl.org/rt3/Ticket/Display.html?id=51718 >


There's a lot of common code in the tests in these directories, for
looping across a bunch of files, doing tests, and collating results.
Also, there's no consistency in the output of these tests at all.
(Some of them have a count of total errors afterwards, most don't.
Some of them list line numbers in parens, most don't.)

I've got a start at consolidating this into a helper module; I've
attached my current diff.  I'm posting it to RT in case someone wants
to take it and run with it, or in case someone can think of a better
way.

Mark
Index: lib/Parrot/Test/Util/Runloop.pm
===================================================================
--- lib/Parrot/Test/Util/Runloop.pm	(revision 0)
+++ lib/Parrot/Test/Util/Runloop.pm	(revision 0)
@@ -0,0 +1,127 @@
+# Copyright (C) 2008, The Perl Foundation.
+# $Id$
+
+=head1 NAME
+
+Parrot::Test::Util::Runloop - consolidated test for lots of files
+
+=head1 SYNOPSIS
+
+    use Parrot::Test::Util::Runloop;
+
+    Parrot::Test::Util::Runloop->testloop(
+        name     => 'No trailing spaces or tabs',
+        files    => [ $DIST->get_c_language_files() ],
+        skips    => { 'lib/Parrot/Test/Util/Runloop.pm' => 'devel' },
+        per_line => sub { !/.?[ \t]+$/ };
+
+
+=head1 DESCRIPTION
+
+This module provides a basic runloop for test scripts which perform the same
+test, over and over, on lots of files.  It is intended to consolidate some code
+to handle loops, skips etc, replicated many times in the t/distro/ and
+t/codingstd/ test directories.
+
+You can specify a callback routine to get called back once per line (with the
+per_line attribute), or once per file (with the per_file attribute).  The
+per_line callback gets passed the line as a text string.  The per_file callback
+gets passed the whole file as a text string.  If the callback function returns
+positive, the test passed, otherwise the test failed.  Failures are tallied,
+and later reported to the test harness once, as a single test.  On failure,
+some informational diagnostics are also generated, showing the user which
+file(s) and which line(s) (if applicable) had the failure.
+
+
+=head1 AUTHOR
+
+Written by Mark Glines, based on an idea (and lots of enthusiasm) from
+Jerry Gay and Will Coleda.
+
+
+=cut
+
+package Parrot::Test::Util::Runloop;
+
+use strict;
+use warnings;
+
+use Carp;
+use Test::More;
+use IO::File;
+
+sub testloop {
+    my ($self, %args) = @_;
+    # sanity
+    my $usage = "Usage: Parrot::Test::Util::Runloop->testloop(\n"
+               ."    name => 'foo',\n"
+               ."    files => [ ... ],\n"
+               ."    per_line => sub { ... });\n";
+    croak $usage unless exists $args{name};
+    croak $usage unless exists $args{files};
+    croak "'files' is not an array reference!" unless ref($args{files}) eq 'ARRAY';
+    croak "no per_file or per_line test callback was provided!"
+        unless exists($args{per_file}) || exists($args{per_line});
+
+    my @failures;
+    my $failed_files = 0;
+
+    foreach my $path (sort @{$args{files}}) {
+        $path = $path->path if ref $path;
+        next if exists($args{skips}) && exists($args{skips}{$path});
+
+        my $file = IO::File->new("<$path")
+            or die "Cannot open '$path' for reading: $!\n";
+
+        my @lines = $file->getlines();
+        my $error_line = "$path";
+        my $have_errors = 0;
+
+        if(exists($args{per_file})) {
+            my $cb = $args{per_file};
+            my $buf = join('', @lines);
+            # do the per-file test
+            unless($cb->($buf)) {
+                push(@failures, $error_line);
+            }
+        }
+
+        if(exists($args{per_line})) {
+            my $cb = $args{per_line};
+
+            # do the test, once for each line
+            foreach my $n ([EMAIL PROTECTED]) {
+                my $line = $lines[$n];
+                unless($cb->($line)) {
+                    $error_line .= "," if $have_errors;
+                    $error_line .= " " . ($n+1);
+                    $have_errors = 1;
+                }
+            }
+
+            push(@failures, $error_line) if $have_errors;
+            $failed_files++ if $have_errors;
+        }
+    }
+    local $Test::Builder::Level = $Test::Builder::Level + 1;
+    ok(!scalar @failures, $args{name});
+    if(scalar @failures) {
+        diag($args{diag_prefix} . " in the following files:")
+            if exists $args{diag_prefix};
+        foreach my $failure (@failures) {
+            diag($failure);
+        }
+        my $failures = scalar @failures;
+        my $total_files = scalar @{$args{files}};
+        diag("That's $failures failed files out of $total_files files total.");
+    }
+}
+
+1;
+
+# Local Variables:
+#   mode: cperl
+#   cperl-indent-level: 4
+#   fill-column: 100
+# End:
+# vim: expandtab shiftwidth=4:
Index: t/codingstd/c_cuddled_else.t
===================================================================
--- t/codingstd/c_cuddled_else.t	(revision 26353)
+++ t/codingstd/c_cuddled_else.t	(working copy)
@@ -7,6 +7,7 @@
 use lib qw( . lib ../lib ../../lib );
 use Test::More tests => 1;
 use Parrot::Distribution;
+use Parrot::Test::Util::Runloop;
 
 =head1 NAME
 
@@ -32,32 +33,15 @@
 
 my $DIST = Parrot::Distribution->new;
 my @files = @ARGV ? @ARGV : $DIST->get_c_language_files();
-my @else;
 
-foreach my $file (@files) {
 
-    # if we have command line arguments, the file is the full path
-    # otherwise, use the relevant Parrot:: path method
-    my $path = @ARGV ? $file : $file->path;
+Parrot::Test::Util::Runloop->testloop(
+    name        => 'no cuddled elses',
+    files       => [EMAIL PROTECTED],
+    per_line    => sub { shift !~ /}\s*else\s*{/ },
+    diag_prefix => 'Cuddled else found'
+);
 
-    open my $fh, '<', $path
-        or die "Cannot open '$path' for reading: $!\n";
-
-    my $tabcount;
-    my $message = qq<  $path:>;
-    while (<$fh>) {
-        next unless /}\s*else\s*{/;
-        $message .= " $.";
-        $tabcount++;
-    }
-    push @else => "$message\n"
-        if $tabcount;
-    close $fh;
-}
-
-ok( !scalar(@else), "cuddled else" )
-    or diag( "cuddled else found in " . scalar @else . " files:[EMAIL PROTECTED]" );
-
 # Local Variables:
 #   mode: cperl
 #   cperl-indent-level: 4
Index: t/codingstd/check_isxxx.t
===================================================================
--- t/codingstd/check_isxxx.t	(revision 26353)
+++ t/codingstd/check_isxxx.t	(working copy)
@@ -8,6 +8,8 @@
 use lib qw( . lib ../lib ../../lib );
 use Test::More tests => 1;
 use Parrot::Distribution;
+use Parrot::Test::Util::Runloop;
+use Carp;
 
 =head1 NAME
 
@@ -54,35 +56,23 @@
 
 my $isxxx_functions = join '|', @isxxx_functions_list;
 
-foreach my $file (@files) {
+sub check_isxxx {
+    my $line = shift;
 
-    # if we have command line arguments, the file is the full path
-    # otherwise, use the relevant Parrot:: path method
-    my $path         = @ARGV ? $file : $file->path;
-    my $buf          = $DIST->slurp($path);
-    my @buffer_lines = split( /\n/, $buf );
-    my $i            = 1;
-
-    # get the lines just matching isxxx
-    my @isxxx_lines  = grep { $_->[0] =~ /[^_]($isxxx_functions)\(/ }
-        map { [ $_, $i++ ] } @buffer_lines;
-
-    next unless @isxxx_lines;
-
-    # find the instances without the explicit cast
-    my @no_cast =
-        grep { $_->[0] !~ /[^_]($isxxx_functions)\(\(unsigned char\)/ }
-        @isxxx_lines;
-
-    push @no_explicit_cast, $path . ' (' . join( ", ",
-        map { $_->[1] } @no_cast ) . ")\n"
-        if @no_cast;
+    # does the line contain an isxxx call?
+    return 1 unless $line =~ /[^_]($isxxx_functions)\(/;
+    # is the line missing a cast?
+    return 1 unless $line !~ /[^_]($isxxx_functions)\(\(unsigned char\)/;
+    # yes!  fail.
+    return 0;
 }
 
-ok( [EMAIL PROTECTED], 'isxxx() functions cast correctly' )
-    or diag( "isxxx() function not cast to unsigned char "
-        . @no_explicit_cast
-        . " files:[EMAIL PROTECTED]" );
+Parrot::Test::Util::Runloop->testloop(
+    name     => 'isxxx() functions cast correctly',
+    files    => [EMAIL PROTECTED],
+    per_line => \&check_isxxx,
+    diag_prefix => 'isxxx() function not cast to unsigned char'
+);
 
 # Local Variables:
 #   mode: cperl
Index: t/codingstd/trailing_space.t
===================================================================
--- t/codingstd/trailing_space.t	(revision 26353)
+++ t/codingstd/trailing_space.t	(working copy)
@@ -8,6 +8,7 @@
 use lib qw( . lib ../lib ../../lib );
 use Parrot::Distribution;
 use Test::More tests => 1;
+use Parrot::Test::Util::Runloop;
 
 =head1 NAME
 
@@ -39,40 +40,15 @@
     $DIST->get_c_language_files(),
     $DIST->get_perl_language_files(),
 );
-my @failed_files;
 
-foreach my $file (@files) {
+Parrot::Test::Util::Runloop->testloop(
+    name     => 'no trailing whitespace',
+    files    => [EMAIL PROTECTED],
+    skips    => $skip_files,
+    per_line => sub { shift !~ m{.?[ \t]+$}m },
+    diag_prefix => 'Trailing space or tab char found'
+);
 
-    # if we have command line arguments, the file is the full path
-    # otherwise, use the relevant Parrot:: path method
-    my $path = @ARGV ? $file : $file->path;
-
-    next if exists $skip_files->{$path};
-
-    open my $fh, '<', $path
-        or die "Cannot open '$path' for reading: $!\n";
-
-    my $spacecount = 0;
-
-    my $message = qq<  $path:>;
-    while (<$fh>) {
-        next unless m{.?[ \t]+$}m;
-        $message .= " $.";
-        $spacecount++;
-    }
-    push @failed_files => "$message\n"
-        if $spacecount;
-    
-}
-
-# check the file
-ok( !scalar(@failed_files), 'No trailing spaces or tabs' )
-    or diag(
-    join
-        $/ => "Trailing space or tab char found in " . scalar @failed_files . " files:",
-    @failed_files
-    );
-
 # Local Variables:
 #   mode: cperl
 #   cperl-indent-level: 4
Index: t/codingstd/c_cppcomments.t
===================================================================
--- t/codingstd/c_cppcomments.t	(revision 26353)
+++ t/codingstd/c_cppcomments.t	(working copy)
@@ -8,6 +8,7 @@
 use lib qw( . lib ../lib ../../lib );
 use Test::More tests => 1;
 use Parrot::Distribution;
+use Parrot::Test::Util::Runloop;
 
 =head1 NAME
 
@@ -34,29 +35,24 @@
 my $DIST = Parrot::Distribution->new();
 my @files = @ARGV ? @ARGV : $DIST->get_c_language_files();
 
-check_cppcomments(@files);
 
+Parrot::Test::Util::Runloop->testloop(
+    name        => 'no cuddled elses',
+    files       => [EMAIL PROTECTED],
+    per_file    => \&check_cppcomments,
+    diag_prefix => 'Cuddled else found'
+);
+
 sub check_cppcomments {
-    my @files = @_;
+    my $buf = shift;
+    $buf =~ s{ (?:
+                   (?: ' (?: \\\\ | \\' | [^'] )* ' )  # remove ' string
+                 | (?: " (?: \\\\ | \\" | [^"] )* " )  # remove " string
+                 | /\* .*? \*/                         # remove C comment
+               )
+            }{}gsx;
 
-    my @comments;
-    foreach my $file (@files) {
-        my $path = @ARGV ? $file : $file->path();
-        my $buf = $DIST->slurp($path);
-        $buf =~ s{ (?:
-                       (?: ' (?: \\\\ | \\' | [^'] )* ' )  # remove ' string
-                     | (?: " (?: \\\\ | \\" | [^"] )* " )  # remove " string
-                     | /\* .*? \*/                         # remove C comment
-                   )
-                }{}gsx;
-
-        if ( $buf =~ m{ ( .*? // .* ) }x ) {
-            push( @comments, "$path: $1\n" );
-        }
-    }
-
-    ok( !scalar(@comments), 'C++ comments' )
-        or diag( "C++ comments found in " . scalar @comments . " files:[EMAIL PROTECTED]" );
+    return $buf !~ m{ ( .*? // .* ) }x;
 }
 
 # Local Variables:
Index: t/codingstd/c_code_coda.t
===================================================================
--- t/codingstd/c_code_coda.t	(revision 26353)
+++ t/codingstd/c_code_coda.t	(working copy)
@@ -8,6 +8,7 @@
 use lib qw( . lib ../lib ../../lib );
 use Test::More tests => 2;
 use Parrot::Distribution;
+use Parrot::Test::Util::Runloop;
 
 =head1 NAME
 
@@ -45,36 +46,32 @@
 
 my $DIST = Parrot::Distribution->new;
 my @files = @ARGV ? @ARGV : $DIST->get_c_language_files();
-my @no_coda;
-my @extra_coda;
 
-foreach my $file (@files) {
+Parrot::Test::Util::Runloop->testloop(
+    name        => 'every file has a coda',
+    files       => [EMAIL PROTECTED],
+    per_file    => sub { shift =~ m{\Q$coda\E\n*\z} },
+    diag_prefix => 'No coda found'
+);
 
-    # if we have command line arguments, the file is the full path
-    # otherwise, use the relevant Parrot:: path method
-    my $path = @ARGV ? $file : $file->path;
+Parrot::Test::Util::Runloop->testloop(
+    name        => 'only one coda per file',
+    files       => [EMAIL PROTECTED],
+    per_file    => \&check_duplicates,
+    diag_prefix => 'Duplicate coda found'
+);
 
-    my $buf = $DIST->slurp($path);
+sub check_duplicates {
+    my $buf = shift;
 
-    # append to the no_coda array if the code doesn't match
-    push @no_coda => "$path\n"
-        unless $buf =~ m{\Q$coda\E\n*\z};
-
     # append to the extra_coda array if coda-like text appears more than once
     my $vim_many = 0;
     $vim_many++ while $buf =~ m{^ [* \t]* vim: }gmx;
     my $emacs_many = 0;
     $emacs_many++ while $buf =~ m{^ [* \t]* Local \s variables: }gmx;
-    push @extra_coda => "$path\n"
-        if $vim_many > 1 || $emacs_many > 1;
+    return ($vim_many <= 1 && $emacs_many <= 1);
 }
 
-ok( !scalar(@no_coda), 'C code coda present' )
-    or diag( "C code coda missing in " . scalar @no_coda . " files:[EMAIL PROTECTED]" );
-
-ok( !scalar(@extra_coda), 'C code coda appears only once' )
-    or diag( "C code coda repeating in " . scalar @extra_coda . " files:[EMAIL PROTECTED]" );
-
 # Local Variables:
 #   mode: cperl
 #   cperl-indent-level: 4

Reply via email to