use Cwd;
use File::Temp 0.12 qw(tempdir tempfile);
use Getopt::Long 2.25;
use Pod::Usage;
use URI 1.17;
my $svn = '@SVN_BINDIR@/svn';
my $testroot = '@SVN_TEST_REPOSITORY@';
my $verbose = 0;
my @svn_options = ();
my %externals_hash;
my $temp_dir;
my @errors = ();
my @warnings = ();
my $hideerrors = 0;
GetOptions( "verbose!" => sub { $verbose = 1; push( @svn_options, "--verbose" ) },
"quiet|q" => sub { $verbose = 0; push( @svn_options, "--quiet" ) },
"username=s" => sub { push( @svn_options, "--username", $_[1] ) },
"password=s" => sub { push( @svn_options, "--password", $_[1] ) },
"no_auth_cache" => sub { push( @svn_options, "--no-auth-cache" ) },
"force-log" => sub { push( @svn_options, "--force-log" ) },
"encoding=s" => sub { push( @svn_options, "--encoding", $_[1] ) },
"config-dir=s" => sub { push( @svn_options, "--config-dir", $_[1] ) },
"test-repository|t=s" => \$testroot,
"help|?" => sub{ Usage() },
) or Usage();
sub catch_signal {
my $signal = shift;
warn "$0: caught signal $signal. Quitting now.\n";
exit 1;
}
$SIG{HUP} = \&catch_signal;
$SIG{INT} = \&catch_signal;
$SIG{TERM} = \&catch_signal;
$SIG{PIPE} = \&catch_signal;
my $startDir = cwd;
if ( $0 =~ m"(.*[\\/])[^\\/]+$" )
{
my $programDir = $1;
chdir( $programDir );
}
testUpdateExternals();
if ( 0 != scalar( @errors ) )
{
print "\n*****************************************************************\n";
print "Errors:\n";
print @errors;
}
else
{
print "*** Script passed tests ***\n";
}
chdir( $startDir );
exit( scalar( @errors ) );
sub testUpdateExternals
{
my $failed = 0;
my $retval;
my $testsubdir = "svncopy-update";
my $testURL = "$testroot/$testsubdir";
my @testdirs = (
"source/dirA/dir1",
"source/dirA/dir2",
"source/dirB/dir3",
"wibble/dirA/dir2",
);
my $dirWithExternals = $testdirs[0];
my $pinnedDir = $testdirs[1];
my $dest = "$testURL/dest";
my $old_verbose = $verbose;
my %revisions = {};
my $testRev;
my $test_externals =
"DIR2 $testURL/source/dirA/dir2\n". "DIR2Pin -r __PINREV__ $testURL/source/dirA/dir2\n".
"DIR3 $testURL/source/dirB/dir3\n". "DIR4 $testURL/wibble/dirA/dir2";
my @tests = (
{ sources => [ "$testURL/source/dirA/dir1", ],
pin => 0,
update => 1,
ext_dir => "dir1",
expected_externals => [
"DIR2 $testURL/source/dirA/dir2",
"DIR2Pin -r __PINREV__ $testURL/source/dirA/dir2",
"DIR3 $testURL/source/dirB/dir3",
"DIR4 $testURL/wibble/dirA/dir2"
],
expected_tree => [
"dir1/",
],
},
{ sources => [ "$testURL/source/dirA", ],
pin => 0,
update => 1,
ext_dir => "dirA/dir1",
expected_externals => [
"DIR2 $testURL/dest/dirA/dir2",
"DIR2Pin -r __PINREV__ $testURL/source/dirA/dir2",
"DIR3 $testURL/source/dirB/dir3",
"DIR4 $testURL/wibble/dirA/dir2"
],
expected_tree => [
"dirA/",
"dirA/dir1/",
"dirA/dir2/",
],
},
{ sources => [ "$testURL/source/dirA", ],
pin => 0,
update => 0,
ext_dir => "dirA/dir1",
expected_externals => [
"DIR2 $testURL/source/dirA/dir2",
"DIR2Pin -r __PINREV__ $testURL/source/dirA/dir2",
"DIR3 $testURL/source/dirB/dir3",
"DIR4 $testURL/wibble/dirA/dir2"
],
expected_tree => [
"dirA/",
"dirA/dir1/",
"dirA/dir2/",
],
},
{ sources => [ "$testURL/source/dirA/dir1",
"$testURL/source/dirB/dir3" ],
pin => 0,
update => 1,
ext_dir => "dir1",
expected_externals => [
"DIR2 $testURL/source/dirA/dir2",
"DIR2Pin -r __PINREV__ $testURL/source/dirA/dir2",
"DIR3 $testURL/dest/dir3",
"DIR4 $testURL/wibble/dirA/dir2"
],
expected_tree => [
"dir1/",
"dir3/",
],
},
{ sources => [ "$testURL/source/dirA/dir1", ],
pin => 1,
update => 0,
ext_dir => "dir1",
expected_externals => [
"DIR2 -r __REV__ $testURL/source/dirA/dir2",
"DIR2Pin -r __PINREV__ $testURL/source/dirA/dir2",
"DIR3 -r __REV__ $testURL/source/dirB/dir3",
"DIR4 -r __REV__ $testURL/wibble/dirA/dir2",
],
expected_tree => [
"dir1/",
],
},
{ sources => [ "$testURL/source/dirA", ],
pin => 1,
update => 0,
ext_dir => "dirA/dir1",
expected_externals => [
"DIR2 -r __REV__ $testURL/source/dirA/dir2",
"DIR2Pin -r __PINREV__ $testURL/source/dirA/dir2",
"DIR3 -r __REV__ $testURL/source/dirB/dir3",
"DIR4 -r __REV__ $testURL/wibble/dirA/dir2",
],
expected_tree => [
"dirA/",
"dirA/dir1/",
"dirA/dir2/",
],
},
{ sources => [ "$testURL/source/dirA/dir1",
"$testURL/source/dirB/dir3" ],
pin => 1,
update => 0,
ext_dir => "dir1",
expected_externals => [
"DIR2 -r __REV__ $testURL/source/dirA/dir2",
"DIR2Pin -r __PINREV__ $testURL/source/dirA/dir2",
"DIR3 -r __REV__ $testURL/source/dirB/dir3",
"DIR4 -r __REV__ $testURL/wibble/dirA/dir2",
],
expected_tree => [
"dir1/",
"dir3/",
],
},
);
my $auto_temp_dir = Temp::Delete->new();
my $test_temp_dir = $auto_temp_dir->temp_dir();
$temp_dir = $test_temp_dir;
print "\n################################################################\n";
print( "Testing svncopy.pl\n" );
info( "Using temporary directory $test_temp_dir\n" );
print( "Preparing source directory structure...\n" );
info( " - Deleting '$testURL'\n" );
SVNCall( 'delete', '-m', '"Preparing to test svncopy --update-externals"', $testURL );
foreach my $dir ( @testdirs )
{
if ( !CreateTestDirectory( "$testURL/$dir" ) )
{
$failed = 1;
return;
}
}
if ( 0 != SVNCall( "co", $testURL, "$test_temp_dir/$testsubdir" ) )
{
error( "Preparatory checkout failed" );
$failed = 1;
return;
}
my $pinnedRev = CurrentRevision( "$testURL/$pinnedDir" );
$test_externals =~ s|__PINREV__|$pinnedRev|gi;
my ($handle, $tmpfile) = tempfile( DIR => $test_temp_dir );
print $handle $test_externals;
close($handle);
if ( 0 != SVNCall( "propset", "svn:externals",
"--file", $tmpfile,
"$test_temp_dir/$testsubdir/$dirWithExternals" ) )
{
error( "svn propset of svn:externals failed" );
$failed = 1;
return;
}
if ( 0 != SVNCall( "commit", "-m",
"\"Testing svncopy --update_externals - adding svn:externals\"",
"$test_temp_dir/$testsubdir/$dirWithExternals" ) )
{
error( "svn commit failed" );
$failed = 1;
return;
}
if ( 0 != SVNCall( "propset",
"svncopyTest",
"wibble",
"$test_temp_dir/$testsubdir/$pinnedDir" ) )
{
error( "svn propset of svncopyTest failed" );
$failed = 1;
return;
}
if ( 0 != SVNCall( "commit", "-m",
"\"Testing svncopy --update_externals - adding svncopyTest property\"",
"$test_temp_dir/$testsubdir/$pinnedDir" ) )
{
error( "svn commit failed" );
$failed = 1;
return;
}
foreach my $dir ( @testdirs )
{
$revisions{ "$testURL/$dir" } = CurrentRevision( "$testURL/$dir" );
}
print( "...Source directory structure complete\n" );
my $message = "\"Testing svncopy.pl\"";
TEST: foreach my $testtype ( "HEAD", "-r" )
{
my @copy_options = @svn_options;
my $testno = 1;
if ( "-r" eq $testtype )
{
$testRev = $revisions{ "$testURL/$pinnedDir" };
print "Updating repository to run --revision tests against revision ".
"$testRev...\n";
push( @copy_options, "--revision", "$testRev" );
foreach my $dir ( @testdirs )
{
if ( !UpdateTestDirectory( "$test_temp_dir/$testsubdir/$dir" ) )
{
$failed = 1;
return;
}
}
if ( 0 != SVNCall( "commit", "-m",
"\"Testing svncopy --update_externals".
" - updating directories for '--revision' test\"",
"$test_temp_dir/$testsubdir" ) )
{
error( "svn commit of updated directories failed" );
$failed = 1;
return;
}
print "...update done. Now re-running tests against new repository\n";
}
foreach my $test ( @tests )
{
my @cmd_options = @copy_options;
print "\n################################################################\n";
print "### test number $testno\n";
$verbose = 0;
SVNCall( 'delete', '-m', '"Preparing to test svncopy --update-externals"', $dest );
$verbose = $old_verbose;
my @sources = @{$test->{sources}};
my @expected_externals = @{$test->{expected_externals}};
my @expected_tree = @{$test->{expected_tree}};
push( @cmd_options, "--message", "$message" );
push( @cmd_options, "--tag" ) if ( $test->{pin} );
push( @cmd_options, "--branch" ) if ( $test->{update} );
my @cmdline = ( "perl", "svncopy.pl", @cmd_options, @sources, $dest );
info( "\n=> Calling ", join( " ", @cmdline ), "\n\n" );
if ( system( @cmdline ) )
{
error( "Copy failed" );
$failed = 1;
}
if ( !CheckTree( $dest, @expected_tree ) )
{
$failed = 1;
}
my $ext_dir = "$dest/$test->{ext_dir}";
if ( !CheckExternals( $ext_dir, \%revisions, $pinnedRev, @expected_externals ) )
{
$failed = 1;
}
if ( $failed )
{
print "\n*** '$testtype' test $testno failed ***\n";
print "****************************************************************\n";
last TEST;
}
print "\n### '$testtype' test $testno passed\n";
print "################################################################\n";
$testno++;
}
}
if ( $failed ) { error( "*** svncopy tests failed\n" ); }
else { print "... svncopy tests passed\n"; }
}
sub CreateTestDirectory
{
my $svnpath = $_[0];
my $test_uri = URI->new( "$svnpath" );
info( "Creating '$test_uri'\n" );
if ( !CreateSVNDirectories( $test_uri, "Testing svncopy --update_externals" ) )
{
error( "CreateSVNDirectories on '$test_uri' failed" );
return 0;
}
return 1;
}
sub UpdateTestDirectory
{
my $dir = $_[0];
my $testfile = "$dir/test.txt";
if ( !open FILE, ">$testfile" )
{
error( "Couldn't create test file '$testfile'" );
return 0;
}
print FILE "Test file in '$dir'\n";
close FILE;
if ( 0 != SVNCall( "add", $testfile ) )
{
error( "svn add '$testfile' failed" );
return 0;
}
return 1;
}
sub CheckTree
{
my ( $svnpath, @expected ) = @_;
my ( $retval, @output ) = SVNCall( "list", "--recursive", $svnpath );
if ( 0 != $retval )
{
error( "svn list on '$svnpath' failed" );
return 0;
}
@output = grep( { chomp($_); $_ !~ m"^\s*$"} @output );
my $compare_ctx = { list1 => [@expected], list2 => [@output] };
if ( 0 != CompareLists( $compare_ctx ) )
{
my $addedtext;
my $removedtext;
if ( @{$compare_ctx->{added}} )
{
$addedtext = "\n +".join( "\n +", @{$compare_ctx->{added}} );
}
if ( @{$compare_ctx->{removed}} )
{
$removedtext = "\n -".join( "\n -", @{$compare_ctx->{removed}} );
}
error( "'$svnpath' doesn't match expected$addedtext$removedtext\n" );
return 0;
}
return 1;
}
sub CheckExternals
{
my ( $svnpath, $revisions, $pinnedRev, @expected ) = @_;
my @new_externals;
( $retval, @new_externals ) = SVNCall( "propget", "svn:externals", $svnpath );
if ( 0 != $retval )
{
error( "svn propget on '$svnpath' failed" );
return 0;
}
@expected = grep
{
$_ =~ s|__PINREV__|$pinnedRev|g;
if ( $_ =~ m"(.*)\s+-r __REV__\s+(.*)" )
{
my $path = $1;
my $svnpath = $2;
my $rev = $revisions->{$svnpath};
$_ =~ s|__REV__|$rev|g;
}
1;
} @expected;
@new_externals = grep( { chomp($_); $_ !~ m"^\s*$"} @new_externals );
my $compare_ctx = { list1 => [@expected], list2 => [@new_externals] };
if ( 0 != CompareLists( $compare_ctx ) )
{
error( "Externals on '$svnpath' don't match expected\n".
" - expected:\n ".
join( "\n ", @expected ) .
"\n - actual:\n ".
join( "\n ", @new_externals )
);
return 0;
}
return 1;
}
sub CurrentRevision
{
my $source = shift;
my $old_verbose = $verbose;
$verbose = 0;
my ( $retval, @output ) = SVNCall( "log -q", $source );
$verbose = $old_verbose;
if ( 0 != $retval )
{
error( "CurrentRevision: log -q on '$source' failed" );
return -1;
}
if ( $output[1] =~ m"^r(\d+) \|" )
{
return $1;
}
error( "CurrentRevision: log output not formatted as expected\n" );
return -1;
}
sub SVNCall
{
my ( $command, @options ) = @_;
my @commandline = ( $svn, $command, @options );
info( " > ", join( " ", @commandline ), "\n" );
my @output = qx( @commandline 2>&1 );
my $result = $?;
my $exit = $result >> 8;
my $signal = $result & 127;
my $cd = $result & 128 ? "with core dump" : "";
if ($signal or $cd)
{
error( "$0: 'svn $command' failed $cd: exit=$exit signal=$signal\n" );
}
if ( $exit > 0 )
{
info( join( "\n", @output ) );
}
if ( wantarray )
{
return ( $exit, @output );
}
return $exit;
}
sub CreateSVNDirectories
{
my ( $URI, $message ) = @_;
my $r = $URI->clone;
my @path_segments = grep { length($_) } $r->path_segments;
my @r_path_segments;
unshift(@path_segments, '');
$r->path('');
my $found_root = 0;
my $found_tail = 0;
my ($handle, $messagefile) = tempfile( DIR => $temp_dir );
print $handle $message;
close($handle);
my @msgcmd = ( "--file", $messagefile );
my $old_verbose = $verbose;
$verbose = 0;
while (@path_segments)
{
my $segment = shift @path_segments;
push( @r_path_segments, $segment );
$r->path_segments( @r_path_segments );
if ( !$found_root )
{
if ( SVNCall( 'log', '-r', 'HEAD', $r ) == 0 )
{
$found_root = 1;
}
}
elsif ( !$found_tail )
{
if ( SVNCall( 'log', '-r', 'HEAD', $r ) != 0 )
{
$found_tail = 1;
}
}
if ( $found_tail )
{
$verbose = $old_verbose;
if ( 0 != SVNCall( 'mkdir', @msgcmd, $r ) )
{
error( "Couldn't create directory '$r'" );
return 0;
}
}
}
$verbose = $old_verbose;
return 1;
}
sub CompareLists
{
my $context = $_[0];
my %count = ();
@{$context->{added}} = ();
@{$context->{removed}} = ();
@{$context->{common}} = ();
foreach $element( @{$context->{list1}} )
{
$count{$element}++;
}
foreach $element( @{$context->{list2}} )
{
$count{$element}--;
}
foreach $element ( keys %count )
{
if ( 1 == $count{$element} ) { push( @{$context->{removed}}, $element ); }
elsif ( 0 == $count{$element} ) { push( @{$context->{common}}, $element ); }
else { push( @{$context->{added}}, $element ); }
}
$context->{diffs} = scalar( @{$context->{added}} ) +
scalar( @{$context->{removed}} );
return $context->{diffs};
}
sub info
{
if ( $verbose )
{
print @_;
}
}
sub error
{
my $error;
if ( $hideerrors )
{
return;
}
foreach $error ( @_ )
{
my $text = "svncopy.pl: $error\n";
push( @errors, $text );
if ( $verbose )
{
print $text;
}
}
}
sub Usage
{
my $msg;
$msg = "\n*** $_[0] ***\n" if $_[0];
pod2usage( { -message => $msg,
-verbose => 0 } );
}
package Temp::Delete;
use File::Temp 0.12 qw(tempdir);
sub new
{
my $this = shift;
my $class = ref($this) || $this;
my $self = {};
bless $self, $class;
my $temp_dir = tempdir("testsvncopy_XXXXXXXXXX", TMPDIR => 1);
$self->{tempdir} = $temp_dir;
return $self;
}
sub temp_dir
{
my $self = shift;
return $self->{tempdir};
}
sub DESTROY
{
my $self = shift;
my $temp_dir = $self->{tempdir};
if ( scalar( @errors ) )
{
print "Leaving $temp_dir for inspection\n";
}
else
{
info( "Cleaning up $temp_dir\n" );
File::Path::rmtree([$temp_dir], 0, 0);
}
chdir( $startDir );
}
__END__
=head1 NAME
B<testsvncopy> - tests for B<svncopy> script
=head1 SYNOPSIS
B<testsvncopy.pl> [option ...]
B<testsvncopy.pl> tests the operation of the B<svncopy.pl> script.
Options:
-t [--test-repository] : URL to repository for root of tests
-q [--quiet] : print as little as possible
--username arg : specify a username ARG
--password arg : specify a password ARG
--no-auth-cache : do not cache authentication tokens
--force-log : force validity of log message source
--encoding arg : treat value as being in charset encoding ARG
--config-dir arg : read user configuration files from directory ARG
--[no]verbose : set the script to give lots of output
=head1 OPTIONS
=over 8
=item B<-t [--test-repository]>
Specify a URL to a scratch area of repository which the tests can use.
This can be any valid repository URL.
=item B<-q [--quiet]>
Print as little as possible
=item B<--username arg>
Specify a username ARG
=item B<--password arg>
Specify a password ARG
=item B<--no-auth-cache>
Do not cache authentication tokens
=item B<--force-log>
Force validity of log message source
=item B<--encoding arg>
Treat value as being in charset encoding ARG
=item B<--config-dir arg>
Read user configuration files from directory ARG
=item B<--[no]verbose>
Set the script to give lots of output when it runs
=item B<--help>
Print a brief help message and exits
=back
=head1 DESCRIPTION
B<svncopy.pl> is a utility script which performs an B<svn copy> command.
It allows extra processing to get around some limitations of the B<svn copy>
command (in particular related to branching and tagging).
B<testsvncopy.pl> tests the operation of this script.
=cut