The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl

#===============================================================================
#     REVISION:  $Id: perlcritic-checker.t 69 2011-02-28 10:17:38Z xdr.box $
#  DESCRIPTION:  Tests for perlcritic-checker.pl
#===============================================================================

use strict;
use warnings;

our $VERSION = qw($Revision: 69 $) [1];

use Readonly;
use Config;
use English qw( -no_match_vars );
use File::Spec::Functions qw( catfile path );
use File::Temp qw(tempdir);
use Carp;

#use Smart::Comments;

use FindBin qw($Bin);
FindBin::again();

use Test::More;
use Test::Command;

Readonly my $EXTRA_TESTS         => 0;
Readonly my $TESTS_PER_TEST_CASE => 3;

# Make sure the svn messages come in English
$ENV{'LC_MESSAGES'} = 'C';

#$ENV{'DONT_CLEANUP'} = 1;
#$ENV{'RUN_ONLY_LAST_TEST'} = 1;

Readonly my @REQUIRED_TOOLS => qw( svn svnadmin svnlook );

Readonly my $SCRIPT => catfile( $Bin, q{..}, 'bin', 'perlcritic-checker.pl' );
### SCRIPT: $SCRIPT
Readonly my $DATA_DIR => catfile( $Bin, 'test-cases' );
### DATA_DIR: $DATA_DIR
Readonly my $TMP_DIR => tempdir(
    'perlcritic-checker.XXXX',
    TMPDIR  => 1,
    CLEANUP => $ENV{'DONT_CLEANUP'} ? 0 : 1,
);
### TMP_DIR: $TMP_DIR

Readonly my $CONFIG_NAME   => 'perlcritic-checker.conf';
Readonly my $PROFILES_DIR  => 'perlcritic.d';
Readonly my $PRECOMMIT_DIR => 'precommit_files';

#Readonly my $VERBOSE => q{-v};
Readonly my $VERBOSE => q{};

sub get_svn_version {
    my $version = `svn --version | grep "\\bversion\\b"`;
    chomp $version;

    return $version;
}

sub check_required_tools {
TOOL:
    foreach my $tool (@REQUIRED_TOOLS) {
        foreach my $path ( path() ) {
            next TOOL if -x catfile( $path, $tool );
        }

        diag("Cannot find or use '$tool' binary. PATH='$ENV{'PATH'}'");
        return 0;
    }

    return 1;
}

sub write_file {
    my $file_path    = shift;
    my $file_content = shift;

    open my $fh, '>', $file_path
        or croak "Failed to open output file '$file_path': $OS_ERROR";

    print {$fh} $file_content
        or croak "Failed to write file '$file_path': $OS_ERROR";

    close $fh
        or warn "Failed to close output file '$file_path': $OS_ERROR\n";

    return;
}

sub slurp_file {
    my $file_path = shift;

    open my $fh, '<', $file_path
        or confess "Failed to open file '$file_path': $OS_ERROR";
    my $content = do { local $RS = undef; <$fh> };
    close $fh or confess "Failed to close '$file_path': $OS_ERROR";

    return $content;
}

sub slurp_expected_data {
    my $test_id   = shift;
    my $file_name = shift;

    my $full_path = get_expected_path( $test_id, $file_name );
    my $data = slurp_file($full_path);
    chomp $data;

    return $data;
}

sub escape_pattern {
    my $pattern = shift;

    # Taken from http://www.perlmonks.org/?node_id=525815
    my $in_quote = 0;

    ## no critic (RequireExtendedFormatting, RequireLineBoundaryMatching)
    $pattern =~ s{([^\\]|\\.)}{
        if ($in_quote) {
            if ( $1 eq '\\E' ) {
                $in_quote = 0;
                q{};
            }
            else {
                quotemeta($1);
            }
        }
        else {
            if ( $1 eq '\\Q' ) {
                $in_quote = 1;
                q{};
            }
            else {
                $1;
            }
        }
    }eg;

    return $pattern;
}

sub build_regexp {
    my $pattern = shift;

    # HACK: this way we can use \Q and \E inside *_like *_not_like files
    my $escaped_pattern = escape_pattern($pattern);

    return qr/$escaped_pattern/xms;
}

sub get_repo_path {
    my $test_id = shift;

    return catfile( $TMP_DIR, $test_id, 'repo' );
}

sub get_wc_path {
    my $test_id = shift;

    return catfile( $TMP_DIR, $test_id, 'wc' );
}

sub get_expected_path {
    my $test_id   = shift;
    my $file_name = shift;

    return catfile( $DATA_DIR, $test_id, 'expected_results', $file_name );
}

sub get_log_message {
    my $test_id = shift;

    my $file_path = catfile( $DATA_DIR, $test_id, 'log_message' );
    my $log_message = slurp_file($file_path);
    chomp $log_message;

    return $log_message;
}

sub get_coverage_report_options {
    return q{} if !$ENV{'HARNESS_PERL_SWITCHES'};
    ## no critic (RequireExtendedFormatting, RequireLineBoundaryMatching)
    return q{} if $ENV{'HARNESS_PERL_SWITCHES'} !~ /Devel::Cover/;

    return $ENV{'HARNESS_PERL_SWITCHES'};
}

sub get_recursive_rm_command {
    return $OSNAME eq 'solaris' ? "rm -fR" : "rm $VERBOSE -fRd";
}

sub configure_pre_commit_hook {
    my $test_id = shift;

    my $config_path = catfile( $DATA_DIR, $test_id, $CONFIG_NAME );
    my $hook_name = catfile( get_repo_path($test_id), 'hooks', 'pre-commit' );

    my $coverage_report_opts = get_coverage_report_options();

    my $hook_content = <<"END_HOOK_CONTENT";
#!/bin/sh

REPOS="\$1"
TXN="\$2"
$Config{'perlpath'} $coverage_report_opts $SCRIPT --repository "\$REPOS" --config "$config_path" --transaction "\$TXN" || exit 1
exit 0
END_HOOK_CONTENT

    write_file( $hook_name, $hook_content );

    ## no critic (ProhibitMagicNumbers)
    chmod 0755, $hook_name
        or croak "Cannot chmod 755 file '$hook_name': $OS_ERROR";

    return;
}

sub setup_repo {
    my $test_id = shift;

    my $repo_path = get_repo_path($test_id);
    my $wc_path   = get_wc_path($test_id);

    my $test_dir = catfile( $TMP_DIR, $test_id );

    my $perlcritic_d_link_from
        = catfile( $repo_path, 'hooks', $PROFILES_DIR );
    my $perlcritic_d_link_to = catfile( $DATA_DIR, $test_id, $PROFILES_DIR );

    my $command = <<"END_COMMAND";
mkdir $VERBOSE '$test_dir' &&
svnadmin create '$repo_path' &&
svn checkout 'file://$repo_path' '$wc_path' &&
ln $VERBOSE -s '$perlcritic_d_link_to' '$perlcritic_d_link_from'
END_COMMAND
    ### create svn repo command: $command

    system $command;
    if ( $CHILD_ERROR != 0 ) {
        confess "Cannot create SVN repository. Command: '$command'";
    }

    precommit_files($test_id);
    configure_pre_commit_hook($test_id);

    return;
}

sub precommit_files {
    my $test_id = shift;

    my $wc_path   = get_wc_path($test_id);
    my $from_path = catfile( $DATA_DIR, $test_id, $PRECOMMIT_DIR );
    my $to_path   = catfile( $wc_path, $PRECOMMIT_DIR );

    return if !-d $from_path;

    my $recursive_rm = get_recursive_rm_command();
    my $command      = <<"END_COMMAND";
cp $VERBOSE -R '$from_path' '$wc_path' &&
find '$to_path' -name '.svn' -type d | xargs $recursive_rm &&
mv $VERBOSE $to_path/* '$wc_path' &&
rmdir $VERBOSE '$to_path' &&
cd '$wc_path' &&
svn add * &&
svn commit -m "pre-commit files" *
END_COMMAND
    ### pre-commit files to svn repo command: $command

    system $command;
    if ( $CHILD_ERROR != 0 ) {
        confess
            "Cannot pre-commit files to SVN repository. Command '$command'";
    }

    return;
}

sub add_files {
    my $test_id = shift;

    my $wc_path       = get_wc_path($test_id);
    my $from_path     = catfile( $DATA_DIR, $test_id, 'files' );
    my $wc_files_path = catfile( $wc_path, 'files' );

    my $recursive_rm = get_recursive_rm_command();
    my $command      = <<"END_COMMAND";
cp $VERBOSE -R '$from_path' '$wc_path' &&
find '$wc_files_path' -name '.svn' -type d | xargs $recursive_rm &&
mv $VERBOSE $wc_files_path/* '$wc_path' &&
rmdir $VERBOSE '$wc_files_path' &&
cd '$wc_path' &&
svn --quiet add *
END_COMMAND
    ### add files to svn repo command: $command

    system $command;
    if ( $CHILD_ERROR != 0 ) {
        confess "Cannot add files to SVN repository. Command: '$command'";
    }

    return;
}

sub commit_files {
    my $test_id = shift;

    my $wc_path     = get_wc_path($test_id);
    my $log_message = get_log_message($test_id);

    my $command = <<"END_COMMAND";
cd '$wc_path' &&
svn commit * -m "$log_message"
END_COMMAND
    ### commit files to svn repo command: $command

    my $result = Test::Command->new( cmd => $command );

    return $result;
}

sub set_test_plan {
    my $number_of_test_cases = shift;

    plan tests => $TESTS_PER_TEST_CASE * $number_of_test_cases + $EXTRA_TESTS;

    return;
}

sub get_test_ids {

    # Find all dir names consisting of three digits
    my @test_ids = sort map { $_ =~ /(?<!\d)(\d{3})\z/xms ? $1 : () }
        grep { -d $_ } glob "$DATA_DIR/*";

    # Setting RUN_ONLY_LAST_TEST is useful when adding new tests
    return ( $ENV{'RUN_ONLY_LAST_TEST'} ? $test_ids[-1] : @test_ids );
}

sub get_expected_status {
    my $test_id = shift;

    my $status = slurp_expected_data( $test_id, 'status' );

    if ( $status ne 'ok' and $status ne 'fail' ) {
        confess "Invalid status '$status': Use either 'ok' or 'fail'";
    }

    return $status;
}

sub get_test_description {
    my $test_id = shift;

    my $file_name = catfile( $DATA_DIR, $test_id, 'description' );
    my $description = slurp_file($file_name);
    chomp $description;

    return $description;
}

sub check_output {
    my $test_id     = shift;
    my $result      = shift;
    my $output_type = shift;
    my $label       = shift;

    my $like     = $output_type . '_like';
    my $not_like = $output_type . '_not_like';

    if ( -e get_expected_path( $test_id, $like ) ) {
        my $pattern = slurp_expected_data( $test_id, $like );
        my $like_method = $output_type . '_like';

        $result->$like_method( build_regexp($pattern),
            "$label: check $output_type matches regexp" );
    }
    elsif ( -e get_expected_path( $test_id, $not_like ) ) {
        my $pattern = slurp_expected_data( $test_id, $not_like );
        my $unlike_method = $output_type . '_unlike';

        $result->$unlike_method( build_regexp($pattern),
            "$label: check $output_type doesn't match regexp" );
    }
    else {
        confess
            "Neither '$like' nor '$not_like' files found for test $test_id";
    }

    return;
}

sub check_status {
    my $test_id = shift;
    my $result  = shift;
    my $label   = shift;

    my $expected_status = get_expected_status($test_id);
    if ( $expected_status eq 'ok' ) {
        $result->exit_is_num( 0, "$label: check commit is ok" );
    }
    else {
        $result->exit_cmp_ok( q{!=}, 0, "$label: check commit is failed" );
    }

    return;
}

sub check_result {
    my $test_id = shift;
    my $result  = shift;

    my $test_description = get_test_description($test_id);
    my $label            = "[$test_id] $test_description";

    check_status( $test_id, $result, $label );
    check_output( $test_id, $result, 'stdout', $label );
    check_output( $test_id, $result, 'stderr', $label );

    return;
}

sub test_perlcritic_checker {
    my @test_ids = get_test_ids();
    ### test_ids: @test_ids

    set_test_plan( scalar @test_ids );

    foreach my $test_id (@test_ids) {
        setup_repo($test_id);
        add_files($test_id);

        my $result = commit_files($test_id);

        check_result( $test_id, $result );
    }

    return;
}

sub run_tests {
    if ( check_required_tools() ) {
        diag( 'svn version: ' . get_svn_version() );
        test_perlcritic_checker();
    }
    else {
        plan skip_all => 'Cannot find or use all required svn binaries';
    }

    return;
}

run_tests();

# Workaround for bug in File::Temp:
# - http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=479317
# - http://rt.cpan.org/Public/Bug/Display.html?id=35779
sub END {
    chdir q{/};
}