#! /usr/bin/perl
#
#  testsvncopy.pl  --  test script for svncopy.pl.
#
#  This program is free software; you can redistribute  it and/or modify it
#  under  the terms of  the GNU General  Public License as published by the
#  Free Software Foundation;  either version 2 of the  License, or (at your
#  option) any later version.
#
#  THIS  SOFTWARE  IS PROVIDED   ``AS  IS'' AND   ANY  EXPRESS  OR  IMPLIED
#  WARRANTIES,   INCLUDING, BUT NOT  LIMITED  TO, THE IMPLIED WARRANTIES OF
#  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN
#  NO  EVENT  SHALL   THE AUTHOR  BE    LIABLE FOR ANY   DIRECT,  INDIRECT,
#  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
#  NOT LIMITED   TO, PROCUREMENT OF  SUBSTITUTE GOODS  OR SERVICES; LOSS OF
#  USE, DATA,  OR PROFITS; OR  BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
#  ANY THEORY OF LIABILITY, WHETHER IN  CONTRACT, STRICT LIABILITY, OR TORT
#  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
#  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#  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.,
#  59 Temple Place - Suite 330, Boston MA 02111-1307 USA.
#
#  This product makes use of software developed by
#  CollabNet (http://www.Collab.Net/), see http://subversion.tigris.org/.
#
#  This software consists of voluntary contributions made by many
#  individuals.  For exact contribution history, see the revision
#  history and logs, available at http://subversion.tigris.org/.
#------------------------------------------------------------------------------

#------------------------------------------------------------------------------
#
#  This script tests the operation of svncopy.pl.
#
#  For more information see the pod documentation at the foot of the file,
#  or run testsvncopy.pl -?.
#
#------------------------------------------------------------------------------

#
# Include files
#
use Cwd;
use File::Temp   0.12   qw(tempdir tempfile);
use Getopt::Long 2.25;
use Pod::Usage;
use URI          1.17;

#
# Global definitions
#

# Specify the location of the svn command.
my $svn = '@SVN_BINDIR@/svn';

# The scratch repository location for the tests
my $testroot = '@SVN_TEST_REPOSITORY@';

# Input parameters
my $verbose = 0;
my @svn_options = ();

# Internal information
my %externals_hash;
my $temp_dir;

# Error handling
my @errors = ();
my @warnings = ();

# Testing-specific variables
my $hideerrors = 0;


#------------------------------------------------------------------------------
# Main execution block
#

#
# Process arguments
#
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();

# Put in a signal handler to clean up any temporary directories.
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;

# Make sure we're in the correct directory, saving current before we move
my $startDir = cwd;
if ( $0 =~ m"(.*[\\/])[^\\/]+$" )
{
    my $programDir = $1;
    chdir( $programDir );
}

# Run the tests
testUpdateExternals();

# Check whether they passed
if ( 0 != scalar( @errors ) )
{
  print "\n*****************************************************************\n";
  print "Errors:\n";
  print @errors;
}
else
{
  print "*** Script passed tests ***\n";
}

# Return to the original directory
chdir( $startDir );

exit( scalar( @errors ) );


#------------------------------------------------------------------------------
# Function:    testUpdateExternals
#
# Tests the script, pushing any errors onto @errors.
#
# Parameters:
#       none
#
# Returns:     none
#------------------------------------------------------------------------------
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".     # 1 space
                  "DIR2Pin -r __PINREV__ $testURL/source/dirA/dir2\n".
                  "DIR3     $testURL/source/dirB/dir3\n". # 5 spaces
                  "DIR4		$testURL/wibble/dirA/dir2";   # 2 tabs

  my @tests = (
        # Updating with nothing to update
        { 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/",
                    ],
        },
        # Updating a tree - enclosed should change, unless pinned
        { 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/",
                    ],
        },
        # Updating with no update - no change
        { 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/",
                    ],
        },
        # Updating with two sources
        { 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/",
                    ],
        },
        # Pinning
        { 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/",
                    ],
        },
        # Pinning a tree
        { 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/",
                    ],
        },
        # Pinning with two sources
        { 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" );

  #
  # Set up the source directory to copy
  #

  # Kill the directory if it's there
  info( " - Deleting '$testURL'\n" );
  SVNCall( 'delete', '-m', '"Preparing to test svncopy --update-externals"', $testURL );

  # Now create the source directories
  foreach my $dir ( @testdirs )
    {
      if ( !CreateTestDirectory( "$testURL/$dir" ) )
        {
          $failed = 1;
          return;
        }
    }

  # Check out the test root
  if ( 0 != SVNCall( "co", $testURL, "$test_temp_dir/$testsubdir" ) )
    {
      error( "Preparatory checkout failed" );
      $failed = 1;
      return;
    }

  # Set an svn:externals on it
  # - work our what externals we need to set (get the revision for the
  #   pinned directory)
  my $pinnedRev = CurrentRevision( "$testURL/$pinnedDir" );
  $test_externals =~ s|__PINREV__|$pinnedRev|gi;

  # Now write the externals to a temporary file and set them on the dir.
  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;
    }

  # And commit them
  if ( 0 != SVNCall( "commit", "-m",
                     "\"Testing svncopy --update_externals - adding svn:externals\"",
                     "$test_temp_dir/$testsubdir/$dirWithExternals" ) )
    {
      error( "svn commit failed" );
      $failed = 1;
      return;
    }

  #
  # Also set a property on the pinned directory to make sure the pinned
  # revision isn't the last one.
  #
  if ( 0 != SVNCall( "propset",
                     "svncopyTest",
                     "wibble",
                     "$test_temp_dir/$testsubdir/$pinnedDir" ) )
    {
      error( "svn propset of svncopyTest failed" );
      $failed = 1;
      return;
    }
  # And commit
  if ( 0 != SVNCall( "commit", "-m",
                     "\"Testing svncopy --update_externals - adding svncopyTest property\"",
                     "$test_temp_dir/$testsubdir/$pinnedDir" ) )
    {
      error( "svn commit failed" );
      $failed = 1;
      return;
    }

  # Having done all the set-up, get our revision numbers.
  foreach my $dir ( @testdirs )
    {
      $revisions{ "$testURL/$dir" } = CurrentRevision( "$testURL/$dir" );
    }

  print( "...Source directory structure complete\n" );

  # Script parameters
  my $message = "\"Testing svncopy.pl\"";

  TEST: foreach my $testtype ( "HEAD", "-r" )
    {
      my @copy_options = @svn_options;
      my $testno = 1;

      # Do extra setup for -r
      if ( "-r" eq $testtype )
        {
          $testRev = $revisions{ "$testURL/$pinnedDir" };
          print "Updating repository to run --revision tests against revision ".
                "$testRev...\n";

          #
          # Copy the same revision we did before
          # The last thing we changed was the pinned directory, so
          # take its revision as the one we want to copy.
          #
          push( @copy_options, "--revision", "$testRev" );

          #
          # Now add a file to each directory.
          #
          foreach my $dir ( @testdirs )
            {
              if ( !UpdateTestDirectory( "$test_temp_dir/$testsubdir/$dir" ) )
                {
                  $failed = 1;
                  return;
                }
            }

          # And commit
          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";

          # Kill the destination directory if it's there
          $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}};

          # Update global parameters
          push( @cmd_options, "--message", "$message" );
          push( @cmd_options, "--tag" ) if ( $test->{pin} );
          push( @cmd_options, "--branch" ) if ( $test->{update} );

          # Now do the copy
          my @cmdline = ( "perl", "svncopy.pl", @cmd_options, @sources, $dest );
          info( "\n=> Calling ", join( " ", @cmdline ), "\n\n" );
          if ( system( @cmdline ) )
            {
              error( "Copy failed" );
              $failed = 1;
            }

          # Check that the generated tree is as expected.
          if ( !CheckTree( $dest, @expected_tree ) )
            {
              # CheckTree outputs an error message if it fails
              $failed = 1;
            }

          # And check the externals
          my $ext_dir = "$dest/$test->{ext_dir}";
          if ( !CheckExternals( $ext_dir, \%revisions, $pinnedRev, @expected_externals ) )
            {
              # CheckExternals outputs an error message if it fails
              $failed = 1;
            }

          # Bomb out if we had an error
          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"; }
}


#------------------------------------------------------------------------------
# Function:    CreateTestDirectory
#
# Creates a directory in svn.
#
# Parameters:
#       svnpath    directory to create
#
# Returns:     non-zero on success
#------------------------------------------------------------------------------
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;
}


#------------------------------------------------------------------------------
# Function:    UpdateTestDirectory
#
# Modifies the directory in the working copy so that we can check the version
# copied is correct.
#
# Parameters:
#       dir        directory to modify (on file system)
#
# Returns:     non-zero on success
#------------------------------------------------------------------------------
sub UpdateTestDirectory
{
  my $dir = $_[0];
  my $testfile = "$dir/test.txt";

  # Create a file in the directory
  if ( !open FILE, ">$testfile" )
    {
      error( "Couldn't create test file '$testfile'" );
      return 0;
    }
  print FILE "Test file in '$dir'\n";
  close FILE;

  # Now add it to Subversion
  if ( 0 != SVNCall( "add", $testfile ) )
    {
      error( "svn add '$testfile' failed" );
      return 0;
    }

  # We're done
  return 1;
}


#------------------------------------------------------------------------------
# Function:    CheckTree
#
# Checks that directory structure in the subversion location matches
# the given tree.
#
# Parameters:
#       svnpath    Subversion location to check.
#       expected   Expected response - list of files and dirs as returned
#                  by svn list.
#
# Returns:     non-zero on success
#------------------------------------------------------------------------------
sub CheckTree
{
  my ( $svnpath, @expected ) = @_;

  my ( $retval, @output ) = SVNCall( "list", "--recursive", $svnpath );
  if ( 0 != $retval )
    {
      error( "svn list on '$svnpath' failed" );
      return 0;
    }

  # Remove any blank lines and carriage returns
  @output = grep( { chomp($_); $_ !~ m"^\s*$"} @output );

  # Now compare with expected
  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;
}


#------------------------------------------------------------------------------
# Function:    CheckExternals
#
# Checks that the subversion location matches the given tree.
#
# Parameters:
#       svnpath    Subversion location to check.
#       revisions  Hash containing the revisions for externals.
#       pinnedRev  Revision of pinned directory.
#       expected   Expected response - list of externals as returned
#                  by svn propget svn:externals.
#
# Returns:     non-zero on success
#------------------------------------------------------------------------------
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;
    }

  # Update @expected with revisions
  @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;

  # Remove any blank lines and carriage returns from the output
  @new_externals = grep( { chomp($_); $_ !~ m"^\s*$"} @new_externals );

  # Now compare with expected
  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;
}


#------------------------------------------------------------------------------
# Function:    CurrentRevision
#
# Returns the repository revision of the last change to the given object.
#
# Parameters:
#       source      The URL to check
#
# Returns:     The relevant revision number
#------------------------------------------------------------------------------
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;
    }

  #
  # The second line should give us the info we need: e.g.
  #
  # >svn log -q http://subversion/svn/scratch/ianb/svncopy-update/source/dirA
  # ------------------------------------------------------------------------
  # r1429 | ib | 2004-06-14 17:39:36 +0100 (Mon, 14 Jun 2004)
  # ------------------------------------------------------------------------
  # r1423 | ib | 2004-06-14 17:39:26 +0100 (Mon, 14 Jun 2004)
  # ------------------------------------------------------------------------
  # r1422 | ib | 2004-06-14 17:39:23 +0100 (Mon, 14 Jun 2004)
  # ------------------------------------------------------------------------
  # r1421 | ib | 2004-06-14 17:39:22 +0100 (Mon, 14 Jun 2004)
  # ------------------------------------------------------------------------
  #
  # The second line starts with the latest revision number.
  #
  if ( $output[1] =~ m"^r(\d+) \|" )
    {
      return $1;
    }

  error( "CurrentRevision: log output not formatted as expected\n" );

  return -1;
}


#------------------------------------------------------------------------------
# Function:    SVNCall
#
# Makes a call to subversion.
#
# Parameters:
#       command     Subversion command
#       options     Other options to pass to Subversion
#
# Returns:     exit status, output from command
#------------------------------------------------------------------------------
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;
}


#------------------------------------------------------------------------------
# Function:    CreateSVNDirectories
#
# Creates a directory in Subversion, including all intermediate directories.
#
# Parameters:
#       URI         directory path to create.
#       message     commit message (optional).
#
# Returns:     1 on success, 0 on error
#------------------------------------------------------------------------------
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;

  # Prepare a file containing the message
  my ($handle, $messagefile) = tempfile( DIR => $temp_dir );
  print $handle $message;
  close($handle);
  my @msgcmd = ( "--file", $messagefile );

  # We're going to get errors while we do this.  Don't show the user.
  my $old_verbose = $verbose;
  $verbose = 0;
  # Find the repository root
  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 )
            {
              # We've found the root of the repository.
              $found_root = 1;
            }
        }
      elsif ( !$found_tail )
        {
          if ( SVNCall( 'log', '-r', 'HEAD', $r ) != 0 )
            {
              # We've found the first directory which doesn't exist.
              $found_tail = 1;
            }
        }

      if ( $found_tail )
        {
          # We're creating directories
          $verbose = $old_verbose;
          if ( 0 != SVNCall( 'mkdir', @msgcmd, $r ) )
            {
              error( "Couldn't create directory '$r'" );
              return 0;
            }
        }
    }
  $verbose = $old_verbose;

  return 1;
}


#------------------------------------------------------------------------------
# Function:    CompareLists
#
# Compares two lists.
#
# Parameters:
#       context    Structure containing the current state of the comparison:
#           list1      [in]  first list
#           list2      [in]  second list
#           diffs      [out] The number of differences
#           added      [out] The entries in list2 not in list1
#           removed    [out] The entries in list1 not in list2
#           common     [out] The entries in both lists
#
# Returns:     The number of differences
#------------------------------------------------------------------------------
sub CompareLists
{
  my $context = $_[0];
  my %count = ();

  # Make sure everything's clean
  @{$context->{added}} = ();
  @{$context->{removed}} = ();
  @{$context->{common}} = ();

  # Add the elements from list 1 into the hash
  foreach $element( @{$context->{list1}} )
    {
      $count{$element}++;
    }
  # Add the elements from list 2 into the hash (negative)
  foreach $element( @{$context->{list2}} )
    {
      $count{$element}--;
    }

  # Now elements in list1 only have a count of 1, in list2 only have a
  # count of -1, and in both have a count of 0
  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};
}


#------------------------------------------------------------------------------
# Function:    info
#
# Prints out an informational message in verbose mode
#
# Parameters:
#       @_     The message(s) to print
#
# Returns:     none
#------------------------------------------------------------------------------
sub info
{
  if ( $verbose )
    {
      print @_;
    }
}


#------------------------------------------------------------------------------
# Function:    error
#
# Prints out and logs an error message
#
# Parameters:
#       @_     The error messages
#
# Returns:     none
#------------------------------------------------------------------------------
sub error
{
  my $error;

  # This is used during testing
  if ( $hideerrors )
    {
      return;
    }

  # Now print out each error message and add it to the list.
  foreach $error ( @_ )
    {
      my $text = "svncopy.pl: $error\n";
      push( @errors, $text );
      if ( $verbose )
        {
          print $text;
        }
    }
}


#------------------------------------------------------------------------------
# Function:    Usage
#
# Prints out usage information.
#
# Parameters:
#       optional error message
#
# Returns:     none
#------------------------------------------------------------------------------
sub Usage
{
  my $msg;
  $msg = "\n*** $_[0] ***\n" if $_[0];

  pod2usage( { -message => $msg,
               -verbose => 0 } );
}


#------------------------------------------------------------------------------
# This package exists just to delete the temporary directory.
#------------------------------------------------------------------------------
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);
  }

  # Return to the original directory
  chdir( $startDir );
}


#------------------------------------------------------------------------------
# Documentation follows, in pod format.
#------------------------------------------------------------------------------
__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

#------------------------------- END OF FILE ----------------------------------
