blob: a1b0b7dd99a73aede0721123967fbc3760c0a811 [file] [log] [blame]
#!/usr/bin/perl -w
# A script that allows some simple testing of Subversion, in
# particular concurrent read, write and read-write access by the 'svn'
# client. It can also create working copy trees containing a large
# number of files and directories. All repository access is via the
# 'svnadmin', 'svnlook' and 'svn' commands.
#
# This script constructs a repository, and populates it with
# files. Then it loops making changes to a subset of the files and
# committing the tree. Thus when two, or more, instances are run in
# parallel there is concurrent read and write access. Sometimes a
# commit will fail due to a commit conflict. This is expected, and is
# automatically resolved by updating the working copy.
#
# Each file starts off containing:
# A0
# 0
# A1
# 1
# A2
# .
# .
# A9
# 9
#
# The script runs with an ID in the range 0-9, and when it modifies a
# file it modifes the line that starts with its ID. Thus scripts with
# different IDs will make changes that can be merged automatically.
#
# The main loop is then:
#
# step 1: modify a random selection of files
#
# step 2: optional sleep or wait for RETURN keypress
#
# step 3: update the working copy automatically merging out-of-date files
#
# step 4: try to commit, if not successful go to step 3 otherwise go to step 1
#
# To allow break-out of potentially infinite loops, the script will
# terminate if it detects the presence of a "stop file", the path to
# which is specified with the -S option (default ./stop). This allows
# the script to be stopped without any danger of interrupting an 'svn'
# command, which experiment shows may require Berkeley db_recover to
# be used on the repository.
#
# Running the Script
# ==================
#
# Use three xterms all with shells on the same directory. In the
# first xterm run (note, this will remove anything called repostress
# in the current directory)
#
# % stress.pl -c -s1
#
# When the message "Committed revision 1." scrolls pass use the second
# xterm to run
#
# % stress.pl -s1
#
# Both xterms will modify, update and commit separate working copies to
# the same repository.
#
# Use the third xterm to touch a file 'stop' to cause the scripts to
# exit cleanly, i.e. without interrupting an svn command.
#
# To run a third, fourth, etc. instance of the script use -i
#
# % stress.pl -s1 -i2
# % stress.pl -s1 -i3
#
# Running several instances at once will cause a *lot* of disk
# activity. I have run ten instances simultaneously on a Linux tmpfs
# (RAM based) filesystem -- watching ten xterms scroll irregularly
# can be quite hypnotic!
use Getopt::Std;
use File::Find;
use File::Path;
use Cwd;
# Repository check/create
sub init_repo
{
my ( $repo, $create ) = @_;
if ( $create )
{
rmtree([$repo]) if -e $repo;
my $svnadmin_cmd = "svnadmin create $repo";
system( $svnadmin_cmd) and die "$svnadmin_cmd: failed: $?\n";
}
else
{
my $svnlook_cmd = "svnlook youngest $repo";
my $revision = readpipe $svnlook_cmd;
die "$svnlook_cmd: failed\n" if not $revision =~ m{^[0-9]};
}
$repo = getcwd . "/$repo" if not $repo =~ m[^/];
return $repo;
}
# Check-out a working copy
sub check_out
{
my ( $url ) = @_;
my $wc_dir = "wcstress.$$";
mkdir "$wc_dir", 0755 or die "mkdir wcstress.$$: $!\n";
my $svn_cmd = "svn co $url $wc_dir";
system( $svn_cmd ) and die "$svn_cmd: failed: $?\n";
return $wc_dir;
}
# Print status and update. The update is to do any required merges.
sub status_update
{
my ( $wc_dir, $wait_for_key ) = @_;
my $svn_cmd = "svn st -u $wc_dir";
print "Status:\n";
system( $svn_cmd ) and die "$svn_cmd: failed: $?\n";
print "Press return to update/commit\n" if $wait_for_key;
read STDIN, $wait_for_key, 1 if $wait_for_key;
print "Updating:\n";
$svn_cmd = "svn up $wc_dir";
system( $svn_cmd ) and die "$svn_cmd: failed: $?\n";
}
# Print status, update and commit. The update is to do any required
# merges. Returns 0 if the commit succeeds and 1 if it fails due to a
# conflict.
sub status_update_commit
{
my ( $wc_dir, $wait_for_key ) = @_;
status_update $wc_dir, $wait_for_key;
print "Committing:\n";
# Use current time as log message
my $now_time = localtime;
my $svn_cmd = "svn ci $wc_dir -m '$now_time'";
# Need to handle the commit carefully. It could fail for all sorts
# of reasons, but errors that indicate a conflict are "acceptable"
# while other errors are not. Thus there is a need to check the
# return value and parse the error text.
pipe COMMIT_ERR_READ, COMMIT_ERR_WRITE or die "pipe: $!\n";
my $pid = fork();
die "fork failed: $!\n" if not defined $pid;
if ( not $pid )
{
# This is the child process
open( STDERR, ">&COMMIT_ERR_WRITE" ) or die "redirect failed: $!\n";
exec $svn_cmd or die "exec $svn_cmd failed: $!\n";
}
# This is the main parent process, look for acceptable errors
close COMMIT_ERR_WRITE or die "close COMMIT_ERR_WRITE: $!\n";
my $acceptable_error = 0;
while ( <COMMIT_ERR_READ> )
{
print STDERR;
$acceptable_error = 1 if ( /^svn:[ ]
(
Transaction[ ]is[ ]out[ ]of[ ]date
|
Merge[ ]conflict[ ]during[ ]commit
|
Baseline[ ]incorrect
)
$/x );
}
close COMMIT_ERR_READ or die "close COMMIT_ERR_READ: $!\n";
# Get commit subprocess exit status
die "waitpid: $!\n" if $pid != waitpid $pid, 0;
die "unexpected commit fail: exit status: $?\n"
if ( $? != 0 and $? != 256 ) or ( $? == 256 and $acceptable_error != 1 );
return $? == 256 ? 1 : 0;
}
# Get a list of all versioned files in the working copy
{
my @get_list_of_files_helper_array;
sub GetListOfFilesHelper
{
$File::Find::prune = 1 if $File::Find::name =~ m[/.svn];
return if $File::Find::prune or -d;
push @get_list_of_files_helper_array, $File::Find::name;
}
sub GetListOfFiles
{
my ( $wc_dir ) = @_;
@get_list_of_files_helper_array = ();
find( \&GetListOfFilesHelper, $wc_dir);
return @get_list_of_files_helper_array;
}
}
# Populate a working copy
sub populate
{
my ( $dir, $dir_width, $file_width, $depth ) = @_;
return if not $depth--;
for $nfile ( 1..$file_width )
{
my $filename = "$dir/foo$nfile";
open( FOO, ">$filename" ) or die "open $filename: $!\n";
for $line ( 0..9 )
{
print FOO "A$line\n$line\n" or die "write to $filename: $!\n";
}
close FOO or die "close $filename: $!\n";
my $svn_cmd = "svn add $filename";
system( $svn_cmd ) and die "$svn_cmd: failed: $?\n";
}
if ( $depth )
{
for $ndir ( 1..$dir_width )
{
my $dirname = "$dir/bar$ndir";
my $svn_cmd = "svn mkdir $dirname";
system( $svn_cmd ) and die "$svn_cmd: failed: $?\n";
populate( "$dirname", $dir_width, $file_width, $depth );
}
}
}
# Modify a versioned file in the working copy
sub ModFile
{
my ( $filename, $mod_number, $id ) = @_;
# Read file into memory replacing the line that starts with our ID
open( FOO, "<$filename" ) or die "open $filename: $!\n";
@lines = map { s[(^$id.*)][$1,$mod_number]; $_ } <FOO>;
close FOO or die "close $filename: $!\n";
# Write the memory back to the file
open( FOO, ">$filename" ) or die "open $filename: $!\n";
print FOO or die "print $filename: $!\n" foreach @lines;
close FOO or die "close $filename: $!\n";
}
sub ParseCommandLine
{
my %cmd_opts;
my $usage = "
usage: stress.pl [-c] [-i num] [-n num] [-s secs] [-x num]
[-D num] [-F num] [-N num] [-R path] [-S path] [-U url]
where
-c cause repository creation
-i the ID (valid IDs are 0 to 9, default is 0 if -c given, 1 otherwise)
-n the number of sets of changes to commit
-s the sleep delay (-1 wait for key, 0 none)
-x the number of files to modify in each commit
-D the number of sub-directories per directory in the tree
-F the number of files per directory in the tree
-N the depth of the tree
-R the path to the repository
-S the path to the file whose presence stops this script
-U the URL to the repository (file:///<-R path> by default)
";
# defaults
$cmd_opts{'D'} = 2; # number of subdirs per dir
$cmd_opts{'F'} = 2; # number of files per dir
$cmd_opts{'N'} = 2; # depth
$cmd_opts{'R'} = "repostress"; # repository name
$cmd_opts{'S'} = "stop"; # path of file to stop the script
$cmd_opts{'U'} = "none"; # URL
$cmd_opts{'c'} = 0; # create repository
$cmd_opts{'i'} = 0; # ID
$cmd_opts{'s'} = -1; # sleep interval
$cmd_opts{'n'} = 200; # sets of changes
$cmd_opts{'x'} = 4; # files to modify
getopts( 'ci:n:s:x:D:F:N:R:U:', \%cmd_opts ) or die $usage;
# default ID if not set
$cmd_opts{'i'} = 1 - $cmd_opts{'c'} if not $cmd_opts{'i'};
die $usage if $cmd_opts{'i'} !~ /^[0-9]$/;
return %cmd_opts;
}
############################################################################
# Main
srand 123456789;
my %cmd_opts = ParseCommandLine();
my $repo = init_repo $cmd_opts{'R'}, $cmd_opts{'c'};
# Make URL from path if URL not explicitly specified
$cmd_opts{'U'} = "file://$repo" if $cmd_opts{'U'} eq "none";
my $wc_dir = check_out $cmd_opts{'U'};
if ( $cmd_opts{'c'} )
{
my $svn_cmd = "svn mkdir $wc_dir/trunk";
system( $svn_cmd ) and die "$svn_cmd: failed: $?\n";
populate "$wc_dir/trunk", $cmd_opts{'D'}, $cmd_opts{'F'}, $cmd_opts{'N'};
status_update_commit $wc_dir, 0 and die "populate checkin failed\n";
}
my @wc_files = GetListOfFiles $wc_dir;
die "not enough files in repository\n" if $#wc_files + 1 < $cmd_opts{'x'};
my $wait_for_key = $cmd_opts{'s'} < 0;
my $stop_file = $cmd_opts{'S'};
for $mod_number ( 1..$cmd_opts{'n'} )
{
my @chosen;
for ( 1..$cmd_opts{'x'} )
{
# Extract random file from list and modify it
my $mod_file = splice @wc_files, int rand $#wc_files, 1;
ModFile $mod_file, $mod_number, $cmd_opts{'i'};
push @chosen, $mod_file;
}
# Reinstate list of files, the order doesn't matter
push @wc_files, @chosen;
if ( $cmd_opts{'x'} > 0 ) {
# Loop committing until successful or the stop file is created
1 while not -e $stop_file and status_update_commit $wc_dir, $wait_for_key;
} else {
status_update $wc_dir, $wait_for_key;
}
# Break out of loop, or sleep, if required
print( "stop file '$stop_file' detected\n" ), last if -e $stop_file;
sleep $cmd_opts{'s'} if $cmd_opts{'s'} > 0;
}