File Coverage

blib/lib/Gerrit/Client/Test.pm
Criterion Covered Total %
statement 95 346 27.4
branch 10 96 10.4
condition 3 48 6.2
subroutine 24 60 40.0
pod 19 19 100.0
total 151 569 26.5


line stmt bran cond sub pod time code
1             #############################################################################
2             ##
3             ## Copyright (C) 2012-2014 Rohan McGovern
4             ##
5             ## This library is free software; you can redistribute it and/or
6             ## modify it under the terms of the GNU Lesser General Public
7             ## License as published by the Free Software Foundation; either
8             ## version 2.1 of the License, or (at your option) any later version.
9             ##
10             ## This library is distributed in the hope that it will be useful,
11             ## but WITHOUT ANY WARRANTY; without even the implied warranty of
12             ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13             ## Lesser General Public License for more details.
14             ##
15             ## You should have received a copy of the GNU Lesser General Public
16             ## License along with this library; if not, write to the Free Software
17             ## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18             ##
19             ##
20             #############################################################################
21              
22             =head1 NAME
23              
24             Gerrit::Client::Test - test helpers for Gerrit::Client
25              
26             =head1 DESCRIPTION
27              
28             This package provides various utilities written for testing of the
29             Gerrit::Client package. It is not intended for general use and the interface is
30             subject to change.
31              
32             Gerrit::Client::Test may be used to install and manage a local Gerrit instance
33             for the purpose of running system tests.
34              
35             =cut
36              
37             package Gerrit::Client::Test;
38              
39 7     7   85898 use strict;
  7         15  
  7         258  
40 7     7   39 use warnings;
  7         13  
  7         219  
41              
42 7     7   6474 use AnyEvent::Socket;
  7         232886  
  7         1058  
43 7     7   8788 use Archive::Zip qw(:ERROR_CODES);
  7         507512  
  7         914  
44 7     7   4854 use Capture::Tiny qw(capture_merged capture);
  7         43373  
  7         539  
45 7     7   55 use Carp;
  7         12  
  7         392  
46 7     7   4813 use English;
  7         15955  
  7         55  
47 7     7   4263 use File::Basename;
  7         14  
  7         483  
48 7     7   40 use File::Path;
  7         17  
  7         348  
49 7     7   90 use File::Temp;
  7         11  
  7         670  
50 7     7   1762 use File::chdir;
  7         6966  
  7         681  
51 7     7   46 use IO::File;
  7         29  
  7         1575  
52 7     7   9063 use LWP::UserAgent;
  7         329008  
  7         286  
53 7     7   6858 use List::MoreUtils qw(any);
  7         8545  
  7         641  
54 7     7   4745 use Params::Validate qw(:all);
  7         55543  
  7         1711  
55 7     7   8060 use Test::More;
  7         129105  
  7         79  
56 7     7   9476 use autodie;
  7         152078  
  7         57  
57              
58             my $HAVE_GIT;
59              
60             # like system(), but fail test and diag() the output if the command fails
61             sub _system_or_fail {
62 0     0   0 my (@cmd) = @_;
63 0         0 local $Test::Builder::Level = $Test::Builder::Level + 1;
64 0         0 my $status;
65 0     0   0 my ($out) = capture_merged { $status = system(@cmd) };
  0         0  
66 0 0       0 if ( $status != 0 ) {
67 0         0 diag($out);
68             }
69 0         0 return is( $status, 0 );
70             }
71              
72             # Returns a TCP port which, at the time of the call, can be bound on 127.0.0.1
73             sub _find_available_tcp_port {
74 0     0   0 my $port;
75 0     0   0 my $guard = tcp_server '127.0.0.1', 0, sub { }, sub {
76 0     0   0 $port = $_[2];
77 0         0 };
78 0         0 return $port;
79             }
80              
81             # Fetches the given $url to a local File::Temp, which is returned.
82             sub _fetch_remote {
83 0     0   0 my ($url) = @_;
84 0         0 local $Test::Builder::Level = $Test::Builder::Level + 1;
85 0         0 my $file = File::Temp->new(
86             TEMPLATE => 'Gerrit-Client-war.XXXXXX',
87             TMPDIR => 1,
88             CLEANUP => 1
89             );
90              
91 0         0 my $ua = LWP::UserAgent->new();
92 0         0 diag("Downloading $url ...");
93 0         0 my $response = $ua->get( $url, ':content_file' => "$file" );
94 0 0       0 if ( !ok( $response->is_success(), "can fetch $url" ) ) {
95 0         0 diag( $response->status_line() . "\n" . $response->decoded_content() );
96 0         0 return;
97             }
98              
99 0         0 return $file;
100             }
101              
102             # Get or set some gerrit config
103             sub _gerrit_config {
104 0     0   0 my ( $key, $value ) = @_;
105              
106 0         0 local $Test::Builder::Level = $Test::Builder::Level + 1;
107              
108 0         0 my @cmd = ( 'git', 'config', '-f', 'etc/gerrit.config', $key );
109 0 0       0 if ( defined($value) ) {
110 0         0 push @cmd, $value;
111 0         0 return _system_or_fail(@cmd);
112             }
113 0         0 my $status;
114             my ( $out, $err ) = capture {
115 0     0   0 $status = system(@cmd);
116 0         0 };
117 0 0       0 if ( !is( $status, 0, "retrieve gerrit config $key OK" ) ) {
118 0         0 diag($err);
119 0         0 return;
120             }
121 0         0 chomp $out;
122 0         0 return $out;
123             }
124              
125             # Returns a string suitable for usage as an Authorization header
126             # for the given $username and $password, using HTTP Basic authentication
127             sub _http_basic_auth {
128 0     0   0 my ( $username, $password ) = @_;
129              
130 7     7   56009 use MIME::Base64;
  7         6264  
  7         36259  
131 0         0 return 'Basic ' . encode_base64("$username:$password");
132             }
133              
134             # Creates a gerrit user account with the given $username and $password;
135             # this relies on gerrit being set up such that logging in automatically
136             # creates an account, which is true for HTTP authentication.
137             sub _create_gerrit_user {
138 0     0   0 my ( $self, $username, $password ) = @_;
139              
140 0         0 local $Test::Builder::Level = $Test::Builder::Level + 1;
141              
142 0   0     0 $password ||= $username;
143              
144 0 0       0 return unless $self->ensure_gerrit_running();
145              
146 0         0 diag("Creating gerrit user $username ...");
147              
148 0         0 my $auth = _http_basic_auth( $username, $password );
149 0         0 my $response = LWP::UserAgent->new()
150             ->get( $self->{http_url} . '/login/mine', Authorization => $auth );
151              
152 0 0       0 ok( $response->is_success(), "user $username created" )
153             || diag( $response->status_line() . "\n" . $response->decoded_content() );
154              
155 0         0 return;
156             }
157              
158             # Generates an ssh key pair, passphraseless, and sets it as a peer key of
159             # gerrit; this allows it to be used as a key for the 'Gerrit Code Review' user.
160             # Sets the following:
161             # $self->{ sshkey_public_key }: ssh public key, unadorned (i.e. no 'ssh-rsa'
162             # prefix or comment suffix)
163             # $self->{ sshkey_file }: ssh private key filename; public key filename
164             # is identical with .pub appended
165             # Note: it may be necessary to flush-cache after this.
166             sub _setup_peer_key {
167 0     0   0 my ($self) = @_;
168              
169 0         0 local $Test::Builder::Level = $Test::Builder::Level + 1;
170              
171             # create an ssh key set as a peer key, making it usable as the
172             # 'Gerrit Code Review' superuser.
173 0         0 my $sshkey_file = "$CWD/gerrit-client-test-id_rsa";
174 0 0       0 if ( !-f $sshkey_file ) {
175             return
176 0 0       0 unless _system_or_fail( 'ssh-keygen', '-f', $sshkey_file, '-N', q{} );
177             }
178              
179             {
180 0         0 my $fh;
  0         0  
181 0 0       0 if ( !ok( open( $fh, '<', $sshkey_file . '.pub' ) ) ) {
182 0         0 diag "open $sshkey_file.pub: $!";
183 0         0 return;
184             }
185 0         0 my $line = <$fh>;
186 0         0 close($fh);
187 0         0 my ( undef, $key, undef ) = split( / /, $line, 3 );
188 0 0       0 if ( !ok( open( $fh, '>', 'etc/peer_keys' ) ) ) {
189 0         0 diag "open peer_keys for write: $!";
190 0         0 return;
191             }
192 0         0 print $fh "$key\n";
193 0 0       0 if ( !ok( close($fh) ) ) {
194 0         0 diag "close peer_keys after write: $!";
195 0         0 return;
196             }
197 0         0 $self->{sshkey_public_key} = $key;
198             }
199              
200 0         0 $self->{sshkey_file} = $sshkey_file;
201 0         0 return;
202             }
203              
204             # Creates wrapper scripts for git and ssh to ensure git invokes ssh
205             # with the needed options for interacting with this gerrit.
206             sub _setup_git_ssh {
207 0     0   0 my ($self) = @_;
208 0         0 my (@ssh) = $self->ssh_base();
209              
210 0         0 my $ssh_cmd;
211             {
212 0         0 local $LIST_SEPARATOR = '" "';
  0         0  
213 0         0 $ssh_cmd = "\"@ssh\"";
214             }
215              
216 0         0 local $CWD = $self->{dir};
217 0         0 my $fh;
218              
219 0         0 eval {
220 0         0 open( $fh, '>', 'git_ssh_helper' );
221 0         0 print $fh <<"END_GIT_SSH";
222             #!/bin/sh
223             exec $ssh_cmd "\$\@\"
224             END_GIT_SSH
225 0         0 close($fh);
226              
227 0         0 open( $fh, '>', 'git_wrapper' );
228 0         0 print $fh <<'END_GIT';
229             #!/bin/sh
230             GIT_SSH=$(readlink -f $(dirname $0)/git_ssh_helper)
231             export GIT_SSH
232             exec git "$@"
233             END_GIT
234 0         0 close($fh);
235              
236 0         0 chmod( 0755, 'git_ssh_helper', 'git_wrapper' );
237             };
238 0 0       0 if ( my $error = $EVAL_ERROR ) {
239 0         0 fail("setting up git ssh: $error");
240 0         0 return;
241             }
242              
243 0         0 return 1;
244             }
245              
246             # Perform a $query via h2 Shell.
247             # Gerrit should not be running when the query is executed.
248             # NOTE: this method currently doesn't report errors (because the h2 shell exits
249             # with a zero exit code even if queries fail)
250             sub _do_h2_query {
251 0     0   0 my ( $self, $query ) = @_;
252              
253 0         0 my (@cmd) = (
254             'java',
255             '-cp', $self->_h2_jar(),
256             'org.h2.tools.Shell',
257             '-url', $self->_h2_jdbc_url(),
258             '-sql', $query,
259             );
260              
261 0         0 return _system_or_fail(@cmd);
262             }
263              
264             # Returns path to the h2 jar for this gerrit installation, extracting it
265             # from gerrit.war if needed.
266             sub _h2_jar {
267 0     0   0 my ( $self ) = @_;
268              
269 0         0 my $h2_jar = "$self->{dir}/h2.jar";
270 0 0       0 if (-e $h2_jar) {
271 0         0 return $h2_jar;
272             }
273              
274 0         0 my $zip = Archive::Zip->new( $self->_installed_gerrit_war() );
275 0 0       0 ok( $zip, 'open gerrit.war' ) || return;
276              
277 0         0 my (@possible) = $zip->membersMatching( '^WEB-INF/lib/h2-.*\.jar$' );
278 0 0       0 is( scalar(@possible), 1, 'find h2.jar in gerrit.war' ) || return;
279              
280 0 0       0 is( $zip->extractMember( $possible[0], $h2_jar ), AZ_OK, 'extract h2.jar' )
281             || return;
282              
283 0         0 return $h2_jar;
284             }
285              
286             sub _installed_gerrit_war
287             {
288 0     0   0 my ( $self ) = @_;
289 0         0 return "$self->{dir}/bin/gerrit.war";
290             }
291              
292             sub _h2_jdbc_url
293             {
294 0     0   0 my ( $self ) = @_;
295 0         0 return "jdbc:h2:$self->{dir}/db/ReviewDB";
296             }
297              
298             # Like _system_or_fail, but operates on a $subref instead of
299             # a command via system()
300             sub _cmd_ok {
301 0     0   0 my ( $self, $name, $subref, @cmd ) = @_;
302              
303 0         0 local $Test::Builder::Level = $Test::Builder::Level + 1;
304              
305 0         0 my $cmdstr;
306             {
307 0         0 local $LIST_SEPARATOR = '] [';
  0         0  
308 0         0 $cmdstr = "[$name] [@cmd]";
309             }
310              
311 0         0 my $status;
312             my $captured = capture_merged {
313 0     0   0 $status = $subref->( $self, @cmd );
314 0         0 };
315 0 0       0 if ( $status != 0 ) {
316 0         0 diag($captured);
317             }
318 0         0 return is( $status, 0, "cmd ok: $cmdstr" );
319             }
320              
321             sub _load_gerrit {
322 0     0   0 my ( $package, %args ) = @_;
323              
324 0         0 local $CWD = $args{dir};
325              
326 0         0 my $http_url = URI->new( _gerrit_config('httpd.listenUrl') );
327 0 0       0 if ( !$http_url ) {
328 0         0 warn "Can't load httpd.listenUrl from gerrit in $CWD";
329 0         0 return;
330             }
331              
332 0         0 my $ssh_address = _gerrit_config('sshd.listenAddress');
333 0 0       0 if ( !$ssh_address ) {
334 0         0 warn "Can't load sshd.listenAddress from gerrit in $CWD";
335 0         0 return;
336             }
337              
338 0         0 diag("Found gerrit in $CWD, listening on $http_url and $ssh_address");
339              
340 0         0 $args{http_url} = $http_url->as_string();
341 0         0 $args{http_port} = $http_url->port();
342 0         0 ( undef, $args{ssh_port} ) = split( /:/, $ssh_address, 2 );
343 0   0     0 $args{war} ||= "$CWD/bin/gerrit.war";
344 0         0 $args{user} = _gerrit_config('gerrit-client-test.user');
345              
346 0         0 my $self = bless \%args, $package;
347 0         0 $self->_setup_peer_key();
348 0         0 return $self;
349             }
350              
351             sub _mock_command_step_filename {
352 400     400   465 my ( $script, $i ) = @_;
353 400         1037 return sprintf( '%s.step-%02d', $script, $i );
354             }
355              
356             sub _mock_command_write_step_file {
357 14     14   25 my ( $filename, $data ) = @_;
358              
359             # $data is something like:
360             #
361             # { stdout => 'something', stderr => 'something', exitcode => 43 }
362             #
363             # We want to write literally a string like the above to the step file.
364             #
365 14         157 my $data_code = Data::Dumper->new( [$data] )->Terse(1)->Dump();
366              
367 14   33     1534 my $fh = IO::File->new( $filename, '>' )
368             || croak "open $filename for write: $!";
369              
370 14         2009 $fh->print("$data_code;\n");
371              
372 14 50       168 $fh->close()
373             || croak "close $filename after write: $!";
374              
375 14         1102 return;
376             }
377              
378             sub _mock_command_write_command {
379 4     4   13 my ( $command_file, @step_files ) = @_;
380              
381 4         29 my $step_files_code = Data::Dumper->new( [ \@step_files ] )->Terse(1)->Dump();
382              
383 4   33     239 my $fh = IO::File->new( $command_file, '>' )
384             || croak "open $command_file for write: $!";
385              
386 4         415 $fh->print(
387             q|#!/usr/bin/env perl
388             use strict;
389             use warnings;
390             use utf8;
391             use Data::Dumper;
392             use English qw(-no_match_vars);
393              
394             binmode( STDOUT, ':utf8' );
395             binmode( STDERR, ':utf8' );
396              
397             my $step_files = | . $step_files_code . q|;
398             foreach my $file (@{$step_files}) {
399             next if (! -e $file);
400             my $data = do $file;
401              
402             die "couldn't parse $file: $@" if $@;
403             die "couldn't do $file: $!" if (! defined $data);
404             die "$file did not give a hashref" if (ref($data) ne 'HASH');
405             die "couldn't unlink $file: $!" if (! unlink( $file ));
406              
407             local $OUTPUT_AUTOFLUSH = 1;
408             print STDOUT $data->{stdout};
409             print STDERR $data->{stderr};
410             sleep( $data->{delay} ) if $data->{delay};
411             exit $data->{exitcode};
412             }
413              
414             die "no more test steps!\n"
415             .'A mocked command created by Gerrit::Client::Test::create_mock_command was '
416             ."run more times than expected.\n"
417             .'I expected to be run at most '.scalar(@{$step_files}).' time(s), reading '
418             ."instructions from these files:\n".Dumper($step_files)
419             .'...but the files do not exist!';|
420             );
421              
422 4 50       33 $fh->close() || croak "close $command_file after write: $!";
423              
424             # On most OS, we simply need to make the script have executable permission
425 4 50       178 if ( $OSNAME !~ m{win32}i ) {
426 4 50       49 chmod( 0755, $command_file ) || croak "chmod $command_file: $!";
427             }
428              
429             # On Windows, we cannot directly execute the above script as a command.
430             # Make a .bat file which executes it.
431             else {
432 0   0     0 $fh = IO::File->new( "$command_file.bat", '>' )
433             || croak "open $command_file.bat for write: $!";
434              
435             # %~dp0 == the full path to the directory containing the .bat
436             # %* == all arguments
437              
438 0         0 $fh->print( '@perl.exe %~dp0' . basename($command_file) . " %*\n" );
439 0 0       0 $fh->close() || croak "close $command_file.bat after write: $!";
440             }
441              
442 4         1496 return;
443             }
444              
445             ################################## public ######################################
446              
447             =head1 FUNCTIONS
448              
449             =over
450              
451             =item B( OPTIONS )
452              
453             Creates a mock command whose behavior is defined by the content of OPTIONS.
454              
455             The purpose of this function is to facilitate the testing of code
456             which interacts with possibly failing external processes. This is
457             made clear with an example: to test how a script handles temporary
458             network failures from git, the following code could be used:
459              
460             create_mock_command(
461             name => 'git',
462             directory => $tempdir,
463             sequence => [
464             # first two times, simulate the server hanging up for unknown
465             # reasons after a few seconds
466             { stdout => q{}, stderr => "fatal: The remote end hung up unexpectedly\n",
467             exitcode => 2, delay => 3 },
468             { stdout => q{}, stderr => "fatal: The remote end hung up unexpectedly\n",
469             exitcode => 2, delay => 3 },
470             # on the third try, complete successfully
471             { stdout => q{}, stderr => q{}, exitcode => 0 },
472             ],
473             );
474              
475             # make the above mocked git first in PATH...
476             local $ENV{PATH} = $tempdir . ':' . $ENV{PATH};
477              
478             # and verify that some code can robustly handle the above errors
479             # (but warned about them)
480             my $result;
481             my ($stdout, $stderr) = capture {
482             $result = $git->clone('git://example.com/repo')
483             };
484             ok( $result );
485             is( $stderr,
486             "Warning: 3 attempt(s) required to successfully complete git operation\n" );
487              
488             OPTIONS is a hash or hashref with the following keys:
489              
490             =over
491              
492             =item name
493              
494             The basename of the command, e.g. `git'.
495              
496             =item directory
497              
498             The directory in which the command should be created, e.g. `/tmp/command-test'.
499              
500             This should be a temporary directory, because B will write
501             some otherwise useless data files to this directory. The caller is responsible
502             for creating and deleting this directory (and prepending it to $ENV{PATH}, if
503             that is appropriate).
504              
505             =item sequence
506              
507             The test sequence which should be simulated by the command.
508              
509             This is a reference to an array of hashrefs, each of which has these keys:
510              
511             =over
512              
513             =item stdout
514              
515             Standard output to be written by the command.
516              
517             =item stderr
518              
519             Standard error to be written by the command.
520              
521             =item exitcode
522              
523             The exit code for the command.
524              
525             =item delay
526              
527             Delay, in seconds, to wait after the command has printed its output and before
528             the command exits.
529              
530             =back
531              
532             Each time the mock command is executed, the next element in the array is used
533             to determine the command's behavior. For example, with this sequence:
534              
535             sequence => [
536             { stdout => q{}, stderr => "example.com: host not found\n",
537             exitcode => 2 },
538             { stdout => "OK\n", stderr => q{}, exitcode => 0 },
539             ]
540              
541             ... the first time the command is run, it will print "example.com:
542             host not found" to standard error, and exit with exit code 2
543             (failure). The second time the command is run, it will print "OK" to
544             standard output, and exit with exit code 0 (success). (It is an error
545             to run the command a third time - if this is done, it will die,
546             noisily).
547              
548             =back
549              
550             =cut
551             sub create_mock_command {
552 4     4 1 2077749 my %options = validate(
553             @_,
554             { name => 1,
555             directory => 1,
556             sequence => { type => ARRAYREF },
557             }
558             );
559              
560 4         44 my ( $name, $directory, $sequence_ref ) =
561             @options{qw(name directory sequence)};
562              
563 4 50       25 croak "`$directory' is not an existing directory" if ( !-d $directory );
564 4 50       172 croak 'name is empty' if ( !$name );
565              
566 4         61 my $script = File::Spec->catfile( $directory, $name );
567 4 50       174 croak "`$script' already exists" if ( -e $script );
568              
569 4         9 my @sequence = @{$sequence_ref};
  4         23  
570              
571             # We use data files like:
572             #
573             # command.step-NN
574             #
575             # ... to instruct the command on what to do.
576             #
577             # Each time the command is run, it will read and delete the
578             # lowest-numbered step file.
579             #
580             # We arbitrarily choose 2 digits, meaning a maximum of 100 steps.
581             #
582             # Note that we intentionally support having 0 steps. This means that
583             # we create a command which simply dies immediately if it is called.
584             # This may be used to test that a command is _not_ called, or to
585             # satisfy code which requires some command to be in PATH but does not
586             # actually invoke it.
587 4         10 my $MAX = 100;
588 4 50       22 croak "test sequence is too large! Maximum of $MAX steps permitted"
589             if ( @sequence > $MAX );
590              
591             # Verify that none of the step files exist
592 4         54 my @FILENAMES = map { _mock_command_step_filename( $script, $_ ) }
  400         709  
593             ( 0 .. ( $MAX - 1 ) );
594              
595             croak
596             "step file(s) still exist in $directory - did you forget to clean this up "
597             . 'since an earlier test?'
598 4 50   400   131 if ( any { -e $_ } @FILENAMES );
  400         7389  
599              
600 4         120 my $step_number = 0;
601 4         17 foreach my $step (@sequence) {
602 14         29 my $validated_step = eval {
603 14         473 validate_with(
604             params => [$step],
605             spec => {
606             stdout => { default => q{} },
607             stderr => { default => q{} },
608             exitcode => { default => 0 },
609             delay => { default => 0 },
610             },
611             );
612             };
613              
614 14 50       118 croak "at step $step_number of test sequence: $EVAL_ERROR" if ($EVAL_ERROR);
615              
616 14         32 my $filename = $FILENAMES[ $step_number++ ];
617 14         34 _mock_command_write_step_file( $filename, $validated_step );
618             }
619              
620 4         24 _mock_command_write_command( $script,
621             @FILENAMES[ 0 .. ( $step_number - 1 ) ] );
622              
623 4         52 return;
624             }
625              
626             =back
627              
628             =head1 METHODS
629              
630             In typical usage, B would first be called in order to
631             obtain a handle to a local Gerrit instance; afterwards, other methods act in
632             the context of that Gerrit. This means that git and ssh commands are adjusted so
633             that passwordless superuser access is available to the local Gerrit.
634              
635             =over
636              
637             =item Gerrit::Client::Test->B
638              
639             =item Gerrit::Client::Test->B( OPTIONS )
640              
641             Installs Gerrit, or checks an existing Gerrit installation, and returns an
642             object representing the Gerrit site.
643              
644             If no options are provided, an arbitrary version of Gerrit is downloaded and
645             installed to a temporary directory.
646              
647             OPTIONS is a hashref with the following permitted keys:
648              
649             =over
650              
651             =item dir
652              
653             Directory in which gerrit should be installed.
654              
655             Defaults to a new temporary directory, which will be removed when the returned
656             object is destroyed.
657              
658             =item war
659              
660             URL or path to a gerrit.war to use for installation.
661              
662             Defaults to http://gerrit-releases.storage.googleapis.com/gerrit-2.8.5.war .
663              
664             =item user
665              
666             Username for the initial gerrit user account (account 1000000).
667             This account has administrative privileges.
668              
669             Defaults to "perl-gerrit-client-test".
670              
671             =item ssh_port
672              
673             =item http_port
674              
675             TCP ports for the ssh and http interfaces to this Gerrit site.
676              
677             Defaults to any unused ports chosen by the operating system.
678              
679             =back
680              
681             All of the above described options may also be directly extracted from the
682             returned object, which is a blessed hashref.
683              
684             =cut
685              
686             sub ensure_gerrit_installed {
687 0     0 1 0 my ( $package, %args ) = @_;
688              
689             # We consider that gerrit is installed if gerrit.war exists in the destination
690             # directory
691 0 0 0     0 if ( $args{dir} && -f "$args{ dir }/bin/gerrit.war" ) {
692 0         0 return $package->_load_gerrit(%args);
693             }
694              
695 0   0     0 $args{ssh_port} ||= _find_available_tcp_port();
696 0   0     0 $args{http_port} ||= _find_available_tcp_port();
697 0   0     0 $args{war} ||= 'http://gerrit-releases.storage.googleapis.com/gerrit-2.8.5.war';
698 0   0     0 $args{dir} ||= File::Temp->newdir( 'Gerrit-Client-Test.XXXXXX', TMPDIR => 1 );
699 0   0     0 $args{user} ||= 'perl-gerrit-client-test';
700              
701 0         0 my $local_war;
702 0         0 my $uri = URI->new( $args{war} );
703 0 0       0 if ( !$uri->scheme() ) {
    0          
704 0         0 $local_war = $args{war};
705             }
706             elsif ( $uri->scheme() eq 'file' ) {
707 0         0 $local_war = $uri->path();
708             }
709             else {
710 0         0 $local_war = _fetch_remote( $args{war} );
711             }
712              
713 0 0       0 if ( !-d $args{dir} ) {
714 0         0 mkpath( $args{dir} );
715             }
716 0         0 local $CWD = $args{dir};
717 0         0 my @installcmd =
718             ( 'java', '-jar', $local_war, 'init', '--batch', '--no-auto-start' );
719 0 0       0 return unless _system_or_fail(@installcmd);
720              
721 0         0 $args{http_url} = "http://127.0.0.1:$args{http_port}";
722              
723 0 0       0 return unless _gerrit_config( 'auth.type', 'HTTP' );
724             return
725 0 0       0 unless _gerrit_config( 'sshd.listenAddress', "127.0.0.1:$args{ssh_port}" );
726 0 0       0 return unless _gerrit_config( 'httpd.listenUrl', $args{http_url} );
727 0 0       0 return unless _gerrit_config( 'gerrit.canonicalWebUrl', $args{http_url} );
728 0 0       0 return unless _gerrit_config( 'gerrit-client-test.user', $args{user} );
729              
730 0         0 my $self = bless \%args, $package;
731 0         0 $self->_setup_peer_key();
732 0         0 $self->_setup_git_ssh();
733 0         0 $self->_create_gerrit_user( $args{user} );
734 0         0 $self->ensure_gerrit_stopped();
735 0         0 $self->_do_h2_query( "insert into account_ssh_keys("
736             . "ssh_public_key, valid, account_id, seq"
737             . ") values("
738             . "'ssh-rsa $args{ sshkey_public_key } test','Y',1000000,0"
739             . ")" );
740 0         0 $self->_do_h2_query( "insert into account_external_ids("
741             . "account_id,email_address,external_id"
742             . ") values("
743             . "1000000, 'perl-gerrit-client-test\@127.0.0.1', "
744             . "'mailto:perl-gerrit-client-test\@127.0.0.1'"
745             . ")" );
746 0         0 $self->_do_h2_query( "UPDATE account_external_ids "
747             . "SET password='abcdefghijkl' "
748             . "WHERE account_id=1000000;"
749             );
750              
751 0         0 return $self;
752             }
753              
754             =item B
755              
756             =item B( USERNAME )
757              
758             Returns the initial part of the ssh command which should be used when
759             interacting with this Gerrit installation. The command includes options for
760             setting the port number and identity file to allow passwordless access to this
761             Gerrit site.
762              
763             If USERNAME is given, the command will also contain the USER@HOST argument;
764             otherwise, it must be specified manually. The HOST is always "127.0.0.1".
765              
766             =cut
767              
768             sub ssh_base {
769 0     0 1 0 my ( $self, $user ) = @_;
770              
771 0         0 my @out = (
772             'ssh',
773             "-oUserKnownHostsFile=$self->{ dir }/ssh_known_hosts",
774             '-oStrictHostKeyChecking=no',
775             '-oBatchMode=yes',
776             '-i',
777             $self->{sshkey_file},
778             '-p',
779             $self->{ssh_port},
780             );
781              
782 0 0       0 if ($user) {
783 0         0 push @out, "$user\@127.0.0.1";
784             }
785 0         0 return @out;
786             }
787              
788             =item B( COMMAND )
789              
790             Runs the given git COMMAND in the context of this gerrit.
791             COMMAND should be a git command with arguments, excluding the leading 'git', as
792             in the following example:
793              
794             $gerrit->git( 'fetch', 'origin', 'refs/heads/*:refs/remotes/origin/*' );
795              
796             Returns the exit status of the git command.
797              
798             =cut
799              
800             sub git {
801 0     0 1 0 my ( $self, @cmd ) = @_;
802 0         0 return system( "$self->{ dir }/git_wrapper", @cmd );
803             }
804              
805             =item B( COMMAND )
806              
807             Like B, but the command is treated as a test.
808             If the command fails, the test fails and the command's output is printed to the
809             test log.
810              
811             =cut
812              
813             sub git_ok {
814 0     0 1 0 my ( $self, @cmd ) = @_;
815 0         0 local $Test::Builder::Level = $Test::Builder::Level + 1;
816 0         0 return $self->_cmd_ok( 'git', \&git, @cmd );
817             }
818              
819             =item B( COMMAND )
820              
821             =item B( OPTIONS, COMMAND )
822              
823             Runs the given gerrit COMMAND, via ssh, in the context of this gerrit.
824             COMMAND should be a gerrit command with arguments, excluding the leading
825             'gerrit', as in the following example:
826              
827             $gerrit->gerrit( 'create-project', 'testproject' );
828              
829             OPTIONS may be passed as a hashref with the following keys:
830              
831             =over
832              
833             =item user
834              
835             Username for the gerrit connection.
836              
837             Defaults to $gerrit->{user}, which is the first created user and has
838             administrative privileges.
839              
840             =back
841              
842             Returns the exit status of the ssh command.
843              
844             =cut
845              
846             sub gerrit {
847 0     0 1 0 my ( $self, @cmd ) = @_;
848 0         0 $self->ensure_gerrit_running();
849 0         0 my $opts;
850 0 0       0 if ( ref( $cmd[0] ) ) {
851 0         0 $opts = shift @cmd;
852             }
853 0   0     0 $opts->{user} ||= $self->{user};
854              
855 0         0 my (@base) = $self->ssh_base( $opts->{user} );
856 0         0 return system( @base, 'gerrit', @cmd );
857             }
858              
859             =item B( COMMAND )
860              
861             =item B( OPTIONS, COMMAND )
862              
863             Like B, but the command is treated as a test.
864             If the command fails, the test fails and the command's output is printed to the
865             test log.
866              
867             =cut
868              
869             sub gerrit_ok {
870 0     0 1 0 my ( $self, @cmd ) = @_;
871 0         0 local $Test::Builder::Level = $Test::Builder::Level + 1;
872 0         0 return $self->_cmd_ok( 'gerrit', \&gerrit, @cmd );
873             }
874              
875             =item B
876              
877             =item B( MESSAGE )
878              
879             Create a test commit (an arbitrary, non-empty commit) in the local git
880             repository.
881              
882             If MESSAGE is given, it is used as the commit message; otherwise, a reasonable
883             default is used.
884              
885             =cut
886              
887             sub git_test_commit {
888 0     0 1 0 my ( $self, $message ) = @_;
889              
890 0         0 my $opts;
891 0 0 0     0 if ( $message && ref($message) ) {
892 0         0 $opts = $message;
893 0         0 $message = shift;
894             }
895              
896 0   0     0 $message ||= 'test commit';
897              
898 0         0 my $fh;
899 0         0 open( $fh, '>>', 'testfile' );
900 0         0 print $fh "===\n$message\n";
901 0         0 close($fh);
902              
903 0         0 my @commit_cmd = ( 'commit', '-m', $message );
904 0 0       0 if ( $opts->{amend} ) {
905 0         0 @commit_cmd = ( 'commit', '--amend', '--reuse-message', 'HEAD' );
906             }
907              
908 0         0 local $Test::Builder::Level = $Test::Builder::Level + 1;
909              
910 0   0     0 return $self->git_ok( 'add', 'testfile' )
911             && $self->git_ok(@commit_cmd);
912             }
913              
914             =item B
915              
916             =item B( USER )
917              
918             Returns the base git URL for this gerrit site.
919              
920             The URL contains scheme, user, host and port components.
921             By default, $gerrit->{user} is used as the username;
922             this may be overriden by the USER parameter.
923              
924             The URL has no path component, and hence the full git URL for a given
925             project may be constructed as in the following example:
926              
927             my $giturl = $gerrit->giturl_base() . '/some/project';
928             $gerrit->git( 'clone', $giturl );
929             ...
930              
931             =cut
932              
933             sub giturl_base {
934 0     0 1 0 my ( $self, $user ) = @_;
935 0   0     0 $user ||= $self->{user};
936 0         0 return "ssh://$user\@127.0.0.1:$self->{ ssh_port }";
937             }
938              
939             =item B
940              
941             Returns the base HTTP URL for this gerrit site.
942              
943             =cut
944              
945             sub http_url {
946 0     0 1 0 my ( $self ) = @_;
947 0         0 return $self->{http_url};
948             }
949              
950             =item B
951              
952             Returns the path to a wrapper script for the ssh command. The wrapper
953             script may be used in place of 'ssh' to ensure that the correct setup
954             is used for passwordless access to this gerrit site.
955              
956             Useful in conjunction with @Gerrit::Client::SSH to allow Gerrit::Client
957             passwordless access to this gerrit:
958              
959             local @Gerrit::Client::SSH = ( $gerrit->git_ssh_wrapper() );
960             my $stream = Gerrit::Client::stream_events(
961             url => $gerrit->giturl_base(),
962             ...
963             );
964              
965             =cut
966              
967             sub git_ssh_wrapper {
968 0     0 1 0 my ($self) = @_;
969 0         0 return "$self->{ dir }/git_ssh_helper";
970             }
971              
972             =item B
973              
974             Returns the path to a wrapper script for the git command. The wrapper
975             script may be used in place of 'git' to ensure that the correct setup
976             is used for passwordless access to this gerrit site.
977              
978             Useful in conjunction with @Gerrit::Client::GIT to allow Gerrit::Client
979             passwordless access to this gerrit:
980              
981             local @Gerrit::Client::GIT = ( $gerrit->git_wrapper() );
982             my $stream = Gerrit::Client::stream_events(
983             url => $gerrit->giturl_base(),
984             ...
985             );
986              
987             =cut
988              
989             sub git_wrapper {
990 0     0 1 0 my ($self) = @_;
991 0         0 return "$self->{ dir }/git_wrapper";
992             }
993              
994              
995             =item B
996              
997             Start the gerrit daemon or add a failure to the test log.
998              
999             =cut
1000              
1001             sub start_gerrit {
1002 0     0 1 0 my ($self) = @_;
1003              
1004 0         0 local $CWD = $self->{dir};
1005 0         0 diag("Starting gerrit in $CWD...");
1006 0         0 return _system_or_fail( 'bin/gerrit.sh', 'start' );
1007             }
1008              
1009             =item B
1010              
1011             Stop the gerrit daemon or add a failure to the test log.
1012              
1013             =cut
1014              
1015             sub stop_gerrit {
1016 0     0 1 0 my ($self) = @_;
1017              
1018 0         0 local $CWD = $self->{dir};
1019 0         0 diag("Stopping gerrit in $CWD...");
1020 0         0 return _system_or_fail( 'bin/gerrit.sh', 'stop' );
1021             }
1022              
1023             =item B
1024              
1025             Returns the PID of the main Gerrit process, if available.
1026              
1027             This may return a stale value if Gerrit was terminated unexpectedly.
1028              
1029             =cut
1030              
1031             sub gerrit_pid {
1032 0     0 1 0 my ($self) = @_;
1033              
1034 0         0 my $pidfile = "$self->{ dir }/logs/gerrit.pid";
1035 0 0       0 if ( !-f $pidfile ) {
1036 0         0 return;
1037             }
1038 0         0 open( my $fh, '<', $pidfile );
1039 0         0 my $pid = <$fh>;
1040 0         0 chomp $pid;
1041 0         0 close($fh);
1042              
1043 0         0 return 0 + $pid;
1044             }
1045              
1046             =item B
1047              
1048             Returns 1 if and only if this Gerrit instance appears to be running.
1049              
1050             =cut
1051              
1052             sub gerrit_running {
1053 0     0 1 0 my ($self) = @_;
1054              
1055 0         0 my $pid = $self->gerrit_pid();
1056 0 0       0 if ( !$pid ) {
1057 0         0 return;
1058             }
1059 0         0 return kill( 0, $pid );
1060             }
1061              
1062             =item B
1063              
1064             Start gerrit only if it is not already running, or add a failure to the test
1065             log.
1066              
1067             =cut
1068              
1069             sub ensure_gerrit_running {
1070 0     0 1 0 my ($self) = @_;
1071              
1072 0 0       0 if ( $self->gerrit_running() ) {
1073 0         0 return 1;
1074             }
1075 0         0 return $self->start_gerrit();
1076             }
1077              
1078             =item B
1079              
1080             Stop gerrit only if it is running, or add a failure to the test log.
1081              
1082             =cut
1083              
1084             sub ensure_gerrit_stopped {
1085 0     0 1 0 my ($self) = @_;
1086              
1087 0 0       0 if ( !$self->gerrit_running() ) {
1088 0         0 return 1;
1089             }
1090 0         0 return $self->stop_gerrit();
1091             }
1092              
1093             =item B
1094              
1095             Returns true if and only if a functional git command is in PATH.
1096              
1097             =cut
1098              
1099             sub have_git {
1100 2   33 2 1 15403 $HAVE_GIT //= (0 == system('git', '--version'));
1101              
1102 2         77 return $HAVE_GIT;
1103             }
1104              
1105             =back
1106              
1107             =cut
1108              
1109             1;