The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# BEGIN BPS TAGGED BLOCK {{{
# COPYRIGHT:
# 
# This software is Copyright (c) 2003-2006 Best Practical Solutions, LLC
#                                          <clkao@bestpractical.com>
# 
# (Except where explicitly superseded by other copyright notices)
# 
# 
# LICENSE:
# 
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of either:
# 
#   a) Version 2 of the GNU General Public License.  You should have
#      received a copy of the GNU General Public License along with this
#      program.  If not, write to the Free Software Foundation, Inc., 51
#      Franklin Street, Fifth Floor, Boston, MA 02110-1301 or visit
#      their web page on the internet at
#      http://www.gnu.org/copyleft/gpl.html.
# 
#   b) Version 1 of Perl's "Artistic License".  You should have received
#      a copy of the Artistic License with this package, in the file
#      named "ARTISTIC".  The license is also available at
#      http://opensource.org/licenses/artistic-license.php.
# 
# This work is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
# 
# CONTRIBUTION SUBMISSION POLICY:
# 
# (The following paragraph is not intended to limit the rights granted
# to you to modify and distribute this software under the terms of the
# GNU General Public License and is only of importance to you if you
# choose to contribute your changes and enhancements to the community
# by submitting them to Best Practical Solutions, LLC.)
# 
# By intentionally submitting any modifications, corrections or
# derivatives to this work, or any other work intended for use with SVK,
# to Best Practical Solutions, LLC, you confirm that you are the
# copyright holder for those contributions and you grant Best Practical
# Solutions, LLC a nonexclusive, worldwide, irrevocable, royalty-free,
# perpetual, license to use, copy, create derivative works based on
# those contributions, and sublicense and distribute those contributions
# and any derivatives thereof.
# 
# END BPS TAGGED BLOCK }}}
package SVK::Test;
use strict;

# When running tests, don't let the user's .subversion/config
# affect results.
BEGIN { $ENV{SVKNOSVNCONFIG} = 1; }

use SVK::Version;  our $VERSION = $SVK::VERSION;
use base 'Exporter';

use SVK::Logger;

our @EXPORT = qw(plan_svm new_repos build_test build_floating_test
		 get_copath append_file overwrite_file
		 overwrite_file_raw is_file_content
		 is_file_content_raw _do_run is_output
		 is_sorted_output is_deeply_like is_output_like
		 is_output_unlike is_ancestor status_native status
		 get_editor create_basic_tree waste_rev
		 tree_from_fsroot tree_from_xdroot __ _x not_x _l
		 not_l uri set_editor replace_file glob_mime_samples
		 create_mime_samples chmod_probably_useless

		 catdir HAS_SVN_MIRROR IS_WIN32 install_perl_hook

		 rmtree mkpath @TOCLEAN $output $answer $show_prompt);

use Test::More;
push @EXPORT, @Test::More::EXPORT;
sub import {
    my $class = shift;

    my $caller = caller;
    my $tb = Test::More->builder;
    $tb->exported_to($caller);

    $class->export_to_level(1, @_);
}

my $pid = $$;

our @TOCLEAN;
END {
    return unless $$ == $pid;
    rm_test($_) for @TOCLEAN;
}

use SVK;
use File::Path;
use File::Temp;
use SVK::Util qw( dirname catdir tmpdir can_run abs_path $SEP $EOL IS_WIN32 HAS_SVN_MIRROR );
require Storable;
use SVK::Path::Checkout;

# Fake standard input
our $answer = [];
our $output;

our $show_prompt = 0;

BEGIN {
    no warnings 'redefine';
    # override get_prompt in XD so devel::cover is happy for
    # already-exported symbols being overridden
    *SVK::Util::get_prompt = *SVK::XD::get_prompt = sub {
	local $| = 1;
	print "$_[0]\n" if $show_prompt;
	$logger->debug("$_[0]");
	return $answer unless ref($answer); # compat
	die "expecting input" unless @$answer;
	my $ans = shift @$answer;
	$logger->debug("-> ".($answer->[0]||''));
	return $ans unless ref($ans);
	
	if (ref($ans->[0]) eq 'Regexp') {
	    Carp::cluck "prompt mismatch ($_[0]) vs ($ans->[0])" unless $_[0] =~ m/$ans->[0]/s;
	}
	else {
	    Carp::cluck "prompt mismatch ($_[0]) vs ($ans->[0])" if $_[0] ne $ans->[0];
	}
	return $ans->[1];
    } unless $ENV{DEBUG_INTERACTIVE};

#    chdir catdir(abs_path(dirname(__FILE__)), '..' );
}

sub plan_svm {
    unless (HAS_SVN_MIRROR) {
	plan skip_all => "SVN::Mirror not installed";
	exit;
    };
    plan @_;
}

use Carp;
use SVK;
use SVK::XD;

END {
    return unless $$ == $pid;
    $SIG{__WARN__} = sub { 1 };
    cleanup_test($_) for @TOCLEAN;
}

for (qw/SVKRESOLVE SVKMERGE SVKDIFF SVKPGP SVKLOGOUTPUT LC_CTYPE LC_ALL LANG LC_MESSAGES/) {
    $ENV{$_} = '' if $ENV{$_};
}
$ENV{LANGUAGE} = $ENV{LANGUAGES} = 'i-default';

$ENV{SVKRESOLVE} = 's'; # default for test
$ENV{HOME} ||= (
    $ENV{HOMEDRIVE} ? catdir(@ENV{qw( HOMEDRIVE HOMEPATH )}) : ''
) || (getpwuid($<))[7];
$ENV{USER} ||= (
    (defined &Win32::LoginName) ? Win32::LoginName() : ''
) || $ENV{USERNAME} || (getpwuid($<))[0];

# Make "prove -l" happy; abs_path() returns "undef" if the path 
# does not exist. This makes perl very unhappy.
@INC = grep defined, map abs_path($_), @INC;

if ($ENV{DEBUG}) {
    {
        package Tie::StdScalar::Tee;
        require Tie::Scalar;
        our @ISA = 'Tie::StdScalar';
        sub STORE { print STDOUT $_[1] ; ${$_[0]} = $_[1]; }
    }
    tie $output => 'Tie::StdScalar::Tee';
}

my $pool = SVN::Pool->new_default;

sub new_repos {
    my $repospath = catdir(tmpdir(), "svk-$$");
    my $reposbase = $repospath;
    my $repos;
    my $i = 0;
    while (-e $repospath) {
	$repospath = $reposbase . '-'. (++$i);
    }
    my $pool = SVN::Pool->new_default;
    $repos = SVN::Repos::create("$repospath", undef, undef, undef,
				{'fs-type' => $ENV{SVNFSTYPE} || 'fsfs'})
	or die "failed to create repository at $repospath";
    return $repospath;
}

sub build_test {
    my (@depot) = @_;

    my $depotmap = {map {$_ => (new_repos())[0]} '',@depot};
    my $xd = SVK::XD->new (depotmap => $depotmap,
			   svkpath => $depotmap->{''});
    my $svk = SVK->new (xd => $xd, $ENV{DEBUG_INTERACTIVE} ? () : (output => \$output));
    push @TOCLEAN, [$xd, $svk];
    return ($xd, $svk);
}

sub build_floating_test {
    my ($directory) = @_;

    my $svkpath = File::Spec->catfile($directory, '.svk');
    my $xd = SVK::XD->new (statefile => File::Spec->catfile($svkpath, 'config'),
			   giantlock => File::Spec->catfile($svkpath, 'lock'),
			   svkpath => $svkpath,
			   floating => $directory);
    $xd->load;
    my $svk = SVK->new (xd => $xd, $ENV{DEBUG_INTERACTIVE} ? () : (output => \$output));
    push @TOCLEAN, [$xd, $svk];
    return ($xd, $svk);
}

sub get_copath {
    my ($name) = @_;
    my $copath = SVK::Path::Checkout->copath ('t', "checkout/$name");
    mkpath [$copath] unless -d $copath;
    rmtree [$copath] if -e $copath;
    return ($copath, File::Spec->rel2abs($copath));
}

sub rm_test {
    my ($xd, $svk) = @{+shift};
    for my $depot (sort keys %{$xd->{depotmap}}) {
	my $path = $xd->{depotmap}{$depot};
	die if $path eq '/';
	rmtree [$path];
    }
}

sub cleanup_test {
    my ($xd, $svk) = @{+shift};
    for my $depotname (sort keys %{$xd->{depotmap}}) {
	my $pool = SVN::Pool->new_default;
        my $depot = eval { $xd->find_depot($depotname) } or next;
        my @txns = @{ $depot->repos->fs->list_transactions };
        if (@txns) {
            my $how_many = @txns;
            diag "uncleaned txns ($how_many) on /$depotname/";
            if ( $ENV{SVKTESTUNCLEANTXN} ) {
                for my $txn_name ( sort @txns ) {
                    my $txn = $depot->repos->fs->open_txn($txn_name);
                    my $log = $txn->prop('svn:log');
                    diag "$txn_name: $log";
                }
            }
        }
    }
    return unless $ENV{TEST_VERBOSE};
    use YAML::Syck;
    print Dump($xd);
    for my $depotname (sort keys %{$xd->{depotmap}}) {
	my $pool = SVN::Pool->new_default;
        my $depot = eval { $xd->find_depot($depotname) } or next;
	print "===> depot /$depotname/ (".$depot->repos->fs->get_uuid."):\n";
	$svk->log ('-v', "/$depotname/");
        # if DEBUG is set, the log command already printed the log to
        # stdout; if it isn't, we have to do it ourself
	print ${$svk->{output}} unless $ENV{DEBUG};
    }
}

sub append_file {
    my ($file, $content) = @_;
    open my ($fh), '>>', $file or die "can't append $file: $!";
    print $fh $content;
    close $fh;
}

sub overwrite_file {
    my ($file, $content) = @_;
    open my ($fh), '>', $file or confess "Cannot overwrite $file: $!";
    print $fh $content;
    close $fh;
}

sub overwrite_file_raw {
    my ($file, $content) = @_;
    open my ($fh), '>:raw', $file or confess "Cannot overwrite $file: $!";
    print $fh $content;
    close $fh;
}

sub is_file_content {
    my ($file, $content, $test) = @_;
    open my ($fh), '<', $file or confess "Cannot read from $file: $!";
    my $actual_content = do { local $/; <$fh> };

    @_ = ($actual_content, $content, $test);
    goto &is;
}

sub is_file_content_raw {
    my ($file, $content, $test) = @_;
    open my ($fh), '<:raw', $file or confess "Cannot read from $file: $!";
    local $/;
    @_ = (<$fh>, $content, $test);
    goto &is;
}

sub _do_run {
    my ($svk, $cmd, $arg) = @_;
    my $unlock = SVK::XD->can('unlock');
    my $giant_unlock = SVK::XD->can('giant_unlock');
    no warnings 'redefine';
    my $origxd = Storable::dclone($svk->{xd}->{checkout});
    require SVK::Command::Checkout;
    my $giant_locked = 1;
    local *SVK::XD::giant_unlock = sub {
	$giant_locked = 0;
	goto $giant_unlock;
    };
    local *SVK::XD::unlock = sub {
	my $self = shift;
	unless ($giant_locked) {
	    my $newxd = Storable::dclone($self->{checkout});
	    my @paths = $self->{checkout}->find ('', {lock => $$});
	    my %empty = (lock => undef, '.conflict' => undef,
			 '.deleted' => undef,
			  SVK::Command::Checkout::detach->_remove_entry,
			  SVK::Command->_schedule_empty);
	    for (@paths) {
		$origxd->store($_, \%empty, {override_sticky_descendents => 1});
		$newxd-> store($_, \%empty, {override_sticky_descendents => 1});
	    }
	    diag Carp::longmess.YAML::Syck::Dump({orig => $origxd, new => $newxd, paths => \@paths})
		unless eq_hash($origxd, $newxd);
	}
	$unlock->($self, @_);
    };
    $svk->$cmd (@$arg);
}

sub is_output {
    my ($svk, $cmd, $arg, $expected, $test) = @_;
    _do_run($svk, $cmd, $arg);
    my $cmp = (grep {ref ($_) eq 'Regexp'} @$expected)
	? \&is_deeply_like : \&is_deeply;
    my $o = $output;
    $o =~ s/\r?\n$//;
    @_ = ([split (/\r?\n/, $o, -1)], $expected, $test || join(' ', map { / / ? qq("$_") : $_ } $cmd, @$arg));
    goto &$cmp;
}

sub is_sorted_output {
    my ($svk, $cmd, $arg, $expected, $test) = @_;
    _do_run($svk, $cmd, $arg);
    my $cmp = (grep {ref ($_) eq 'Regexp'} @$expected)
	? \&is_deeply_like : \&is_deeply;
    @_ = ([sort split (/\r?\n/, $output)], [sort @$expected], $test || join(' ', $cmd, @$arg));
    goto &$cmp;
}

sub is_deeply_like {
    my ($got, $expected, $test) = @_;
    for (0..$#{$expected}) {
	if (ref ($expected->[$_]) eq 'SCALAR' ) {
	    @_ = ($#{$got}, $#{$got}, $test);
	    goto &is;
	}
	elsif (ref ($expected->[$_]) eq 'Regexp' ) {
	    unless ($got->[$_] =~ m/$expected->[$_]/) {
		diag "Different at $_:\n$got->[$_]\n$expected->[$_]";
		@_ = (0, $test);
		goto &ok;
	    }
	}
	else {
	    if ($got->[$_] ne $expected->[$_]) {
		diag "Different at $_:\n$got->[$_]\n$expected->[$_]";
		@_ = (0, $test);
		goto &ok;
	    }
	}
    }
    @_ = ($#{$expected}, $#{$got}, $test);
    goto &is;
}

sub is_output_like {
    my ($svk, $cmd, $arg, $expected, $test) = @_;
    _do_run($svk, $cmd, $arg);
    @_ = ($output, $expected, $test || join(' ', $cmd, @$arg));
    goto &like;
}

sub is_output_unlike {
    my ($svk, $cmd, $arg, $expected, $test) = @_;
    _do_run($svk, $cmd, $arg);
    @_ = ($output, $expected, $test || join(' ', $cmd, @$arg));
    goto &unlike;
}

sub is_ancestor {
    my ($svk, $path, @expected) = @_;
    $svk->info ($path);
    my (@copied) = $output =~ m/Copied From: (.*?), Rev. (\d+)/mg;
    @_ = (\@copied, \@expected);
    goto &is_deeply;
}

sub status_native {
    my $copath = shift;
    my @ret;
    while (my ($status, $path) = splice (@_, 0, 2)) {
	push @ret, join (' ', $status, $copath ? SVK::Path::Checkout->copath($copath, $path) :
			 File::Spec->catfile (File::Spec::Unix->splitdir ($path)));
    }
    return @ret;
}

sub status {
    my @ret;
    while (my ($status, $path) = splice (@_, 0, 2)) {
	push @ret, join (' ', $status, $path);
    }
    return @ret;
}

require SVN::Simple::Edit;

sub get_editor {
    my ($repospath, $path, $repos) = @_;

    return SVN::Simple::Edit->new
	(_editor => [SVN::Repos::get_commit_editor($repos,
						   "file://$repospath",
						   $path,
						   'svk', 'test init tree',
						   sub {})],
	 base_path => $path,
	 root => $repos->fs->revision_root ($repos->fs->youngest_rev),
	 missing_handler => SVN::Simple::Edit::check_missing ());
}

sub create_basic_tree {
    my ($xd, $depotpath) = @_;
    my $pool = SVN::Pool->new_default;
    my ($depot, $path) = $xd->find_depotpath($depotpath);

    local $/ = $EOL;
    my $edit = get_editor ($depot->repospath, $path, $depot->repos);
    $edit->open_root ();

    $edit->modify_file ($edit->add_file ('/me'),
			"first line in me$/2nd line in me$/");
    $edit->modify_file ($edit->add_file ('/A/be'),
			"\$Rev\$ \$Revision\$$/\$FileRev\$$/first line in be$/2nd line in be$/");
    $edit->change_file_prop ('/A/be', 'svn:keywords', 'Rev URL Revision FileRev');
    $edit->modify_file ($edit->add_file ('/A/P/pe'),
			"first line in pe$/2nd line in pe$/");
    $edit->add_directory ('/B');
    $edit->add_directory ('/C');
    $edit->add_directory ('/A/Q');
    $edit->change_dir_prop ('/A/Q', 'foo', 'prop on A/Q');
    $edit->modify_file ($edit->add_file ('/A/Q/qu'),
			"first line in qu$/2nd line in qu$/");
    $edit->modify_file ($edit->add_file ('/A/Q/qz'),
			"first line in qz$/2nd line in qz$/");
    $edit->add_directory ('/C/R');
    $edit->close_edit ();
    my $tree = { child => { me => {},
			    A => { child => { be => {},
					      P => { child => {pe => {},
							      }},
					      Q => { child => {qu => {},
							       ez => {},
							      }},
					    }},
			    B => {},
			    C => { child => { R => { child => {}}}}
			  }};
    my $rev = $depot->repos->fs->youngest_rev;
    $edit = get_editor ($depot->repospath, $path, $depot->repos);
    $edit->open_root ();
    $edit->modify_file ('/me', "first line in me$/2nd line in me - mod$/");
    $edit->modify_file ($edit->add_file ('/B/fe'),
			"file fe added later$/");
    $edit->delete_entry ('/A/P');
    $edit->copy_directory('/B/S', "file://@{[$depot->repospath]}/${path}/A", $rev);
    $edit->modify_file ($edit->add_file ('/D/de'),
			"file de added later$/");
    $edit->close_edit ();

    $tree->{child}{B}{child}{fe} = {};
    # XXX: have to clone this...
    %{$tree->{child}{B}{child}{S}} = (child => {%{$tree->{child}{A}{child}}},
				      history => '/A:1');
    delete $tree->{child}{A}{child}{P};
    $tree->{child}{D}{child}{de} = {};

    return $tree;
}

sub waste_rev {
    my ($svk, $path) = @_;
    $svk->mkdir('-m', 'create', $path);
    $svk->rm('-m', 'create', $path);
}

sub tree_from_fsroot {
    # generate a hash describing a given fs root
}

sub tree_from_xdroot {
    # generate a hash describing the content in an xdroot
}

sub __ ($) {
    my $path = shift;
    $path =~ s{/}{$SEP}go;
    return $path;
}

sub _x { IS_WIN32 ? 1 : -x $_[0] }
sub not_x { IS_WIN32 ? 1 : not -x $_[0] }
sub _l { IS_WIN32 ? 1 : -l $_[0] }
sub not_l { IS_WIN32 ? 1 : not -l $_[0] }

sub uri {
    my $file = shift;
    $file =~ s{^|\\}{/}g if IS_WIN32;
    return "file://$file";
}

my @unlink;
sub set_editor {
    my $tmp = File::Temp->new( SUFFIX => '.pl', UNLINK => 0 );
    print $tmp $_[0];
    $tmp->close;

    my $perl = can_run($^X);
    my $tmpfile = $tmp->filename;

    if (defined &Win32::GetShortPathName) {
	$perl = Win32::GetShortPathName($perl);
	$tmpfile = Win32::GetShortPathName($tmpfile);
    }

    chmod 0755, $tmpfile;
    push @unlink, $tmpfile;

    $ENV{SVN_EDITOR} = "$perl $tmpfile";
}

sub replace_file {
    my ($file, $from, $to) = @_;
    my @content;

    open my $fh, '<', $file or croak "Cannot open $file: $!";
    while (<$fh>) {
        s/$from/$to/g;
        push @content, $_;
    }
    close $fh;

    open $fh, '>', $file or croak "Cannot open $file: $!";
    print $fh @content;
    close $fh;
}

# Samples of files with various MIME types
{
my %samples = (
    'empty.txt'     => q{},
    'false.bin'     => 'LZ  Not application/octet-stream',
    'foo.pl'        => "#!/usr/bin/perl\n",
    'foo.jpg'       => "\xff\xd8\xff\xe0\x00this is jpeg",
    'foo.bin'       => "\x1f\xf0\xff\x01\x00\xffthis is binary",
    'foo.html'      => "<html>",
    'foo.txt'       => "test....",
    'foo.c'         => "/*\tHello World\t*/",
    'not-audio.txt' => "if\n",  # reported: alley_cat 2006-06-02
);

# Return the names of mime sample files relative to a particular directory
sub glob_mime_samples {
    my ($directory) = @_;
    my @names;
    push @names, "$directory/$_" for sort keys %samples;
    return @names;
}

# Create a directory and fill it with files of different MIME types.
# The directory must be specified as the first argument.
sub create_mime_samples {
    my ($directory) = @_;

    mkdir $directory;
    overwrite_file ("mime/not-audio.txt", "if\n"); # reported: alley_cat 2006-06-02
    while ( my ($basename, $content) = each %samples ) {
        overwrite_file( "$directory/$basename", $content );
    }
}
}

sub chmod_probably_useless {
    return $^O eq 'MSWin32' || Cwd::cwd() =~ m!^/afs/!;
}

sub install_perl_hook {
    my ($repospath, $hook, $content) = @_;
    $hook = "$repospath/hooks/$hook".(IS_WIN32 ? '.bat' : '');
    open my $fh, '>', $hook or die $!;
    if (IS_WIN32) {
        print $fh "\@rem = '--*-Perl-*--\n";
        print $fh '@echo off'."\n$^X".' -x -S %0 %*'."\n";
        print $fh 'if errorlevel 1 goto script_failed_so_exit_with_non_zero_val 2>nul'."\n";
	print $fh "goto endofperl\n\@rem ';\n";
    }
    print $fh "#!$^X\n" . $content;
    print $fh "\n__END__\n:endofperl\n" if IS_WIN32;
    chmod(0755, $hook);
    return $hook;
}

END {
    return unless $$ == $pid;
    unlink $_ for @unlink;
}

1;