1#!/usr/bin/env perl 2use strict; 3use warnings; 4 5=pod 6 7=head1 NAME 8 9docker-test.pl - run OpenXPKI tests in a Docker container 10 11=cut 12 13# Core modules 14use Cwd qw( realpath getcwd ); 15use File::Copy; 16use File::Temp qw( tempdir ); 17use FindBin qw( $Bin ); 18use Getopt::Long; 19use IPC::Open3 qw( open3 ); 20use List::Util qw( sum ); 21use Pod::Usage; 22use POSIX ":sys_wait_h"; 23use Symbol qw( gensym ); 24 25# CPAN modules 26use File::Which qw(); 27 28 29# $interactive 30# 1 = STDIN can be used for input 31# 0 = output will be hidden if execution takes less than 5 seconds 32sub execute { 33 my ($args, $interactive) = @_; 34 35 my $pid; 36 my $output; 37 if ($interactive) { 38 $pid = open3("<&STDIN", ">&STDOUT", 0, @$args); 39 } 40 else { 41 $output = gensym; # filehandle 42 $pid = open3(0, $output, 0, @$args); 43 } 44 my $tick = 0; 45 my $kid; 46 while (not $kid) { 47 # show output if command takes more than 5 sec 48 if (not $interactive and $tick > 10) { 49 while( my $lines = <$output> ) { print "$lines" } 50 } 51 $kid = waitpid($pid, WNOHANG); # wait for command to exit 52 last if $kid; 53 sleep 1; $tick++; 54 }; 55 56 exit 1 if $? == -1; # execute failed: error message was already shown by system() 57 58 my $lines = ""; $lines = do { local $/; $lines = <$output> || "" } unless $interactive; 59 die sprintf "ERROR - '%s' died with signal %d\n%s\n", $args->[0], ($? & 127), $lines if ($? & 127); 60 die sprintf "ERROR - '%s' exited with code %d\n%s\n", $args->[0], $? >> 8, $lines if ($? >> 8); 61} 62 63sub normalize_if_github_repo { 64 my ($repo) = @_; 65 return $repo =~ / \A [[:word:]-]+ \/ [[:word:]-]+ \Z /msx 66 ? "https://github.com/$repo.git" 67 : $repo; 68} 69 70sub get_branch_commit { 71 my ($dir) = @_; 72 my ($branch, $commit); 73 74 chdir($dir); 75 76 $branch = `git rev-parse --abbrev-ref HEAD`; 77 chomp $branch; 78 # if we are currently in a detached head (i.e. no branch name) 79 if ($branch eq 'HEAD') { 80 $commit = `git rev-parse --short HEAD`; # get commit ID 81 chomp $commit; 82 $branch = undef; 83 } 84 return ($branch, $commit); 85} 86 87 88my $project_root = realpath("$Bin/../"); 89my $engine = File::Which::which('docker') || File::Which::which('podman') || die "Could not find 'docker' or 'podman'\n"; 90 91# 92# Parse command line arguments 93# 94my ($help, $repo, $branch, $commit, $conf_repo, $conf_branch, $conf_commit, $test_all, @_only, $test_coverage, $batch); 95my $parseok = GetOptions( 96 'all' => \$test_all, 97 'only=s' => \@_only, 98 'cover|coverage' => \$test_coverage, 99 'c|commit=s' => \$commit, 100 'b|branch=s' => \$branch, 101 'r|repo=s' => \$repo, 102 'confrepo=s' => \$conf_repo, 103 'confbranch=s' => \$conf_branch, 104 'confcommit=s' => \$conf_commit, 105 'batch' => \$batch, 106 'help' => \$help, 107); 108 109my $test_only = join ',', @_only; 110 111pod2usage(-exitval => 1, -verbose => 0) 112 unless ($parseok and ($test_all or $test_only or $test_coverage or $help)); 113 114pod2usage(-exitval => 0, -verbose => 2) if $help; 115 116my $mode_switches = sum ( map { $_ ? 1 : 0 } ($test_all, $test_only, $test_coverage) ); 117die "ERROR: Please specify only one of: --all | --only | --cover\n" if $mode_switches > 1; 118 119# 120# Construct Docker arguments 121# 122my %docker_env = ( 123 OXI_TEST_ONLY => undef, 124 OXI_TEST_ALL => undef, 125 OXI_TEST_COVERAGE => undef, 126 OXI_TEST_GITREPO => undef, 127 OXI_TEST_GITBRANCH => undef, 128 OXI_TEST_GITCOMMIT => undef, 129 OXI_TEST_CONFIG_GITREPO => undef, 130 OXI_TEST_CONFIG_GITBRANCH => undef, 131 OXI_TEST_CONFIG_GITCOMMIT => undef, 132 OXI_TEST_NONINTERACTIVE => undef, 133); 134my @docker_args = (); 135 136# Restricted set of tests specified? 137$docker_env{OXI_TEST_ONLY} = $test_only if $test_only; 138$docker_env{OXI_TEST_ALL} = $test_all if $test_all; 139$docker_env{OXI_TEST_COVERAGE} = $test_coverage if $test_coverage; 140$docker_env{OXI_TEST_NONINTERACTIVE} = 1 if $batch; 141 142# 143# Code repository 144# 145my $is_local_repo = 0; 146if ($repo) { 147 $docker_env{OXI_TEST_GITREPO} = normalize_if_github_repo($repo); 148} 149# default to current branch in case of local repo 150else { 151 push @docker_args, "-v", "$project_root:/repo"; 152 $docker_env{OXI_TEST_GITREPO} = "/repo"; 153 ($branch, $commit) = get_branch_commit($project_root); 154 $is_local_repo = 1; 155} 156 157$docker_env{OXI_TEST_GITBRANCH} = $branch if $branch; 158$docker_env{OXI_TEST_GITCOMMIT} = $commit if $commit; 159 160# 161# Configuration repository 162# 163if ($conf_repo) { 164 $docker_env{OXI_TEST_CONFIG_GITREPO} = normalize_if_github_repo($conf_repo); 165} 166# default 167else { 168 my $conf_dir = "$project_root/config"; 169 170 # local code repo is used and local config repo exists 171 if ($is_local_repo and -f "$conf_dir/.git" and -f "$conf_dir/config.d/system/server.yaml") { 172 # use the local config's current commit 173 $docker_env{OXI_TEST_CONFIG_GITREPO} = "/repo/config"; 174 ($conf_branch, $conf_commit) = get_branch_commit($conf_dir); 175 } 176 else { 177 # no local config repo: use default Github repo 178 $docker_env{OXI_TEST_CONFIG_GITREPO} = normalize_if_github_repo('openxpki/openxpki-config'); 179 } 180} 181 182$docker_env{OXI_TEST_CONFIG_GITBRANCH} = $conf_branch if $conf_branch; 183$docker_env{OXI_TEST_CONFIG_GITCOMMIT} = $conf_commit if $conf_commit; 184 185push @docker_args, 186 map { 187 $docker_env{$_} 188 ? ("-e", sprintf "%s=%s", $_, $docker_env{$_}) 189 : () 190 } 191 sort keys %docker_env; 192 193# 194# Build container 195# 196print "\n====[ Build Docker image ]====\n"; 197my @cmd; 198if ($batch) { 199 print "Skipping (batch mode)\n"; 200} 201else { 202 `which sha256sum` or die "Sorry, I need the 'sha256sum' command line tool\n"; 203 204 print "(might take more than 10 minutes when first run on a new host)\n"; 205 # Make scripts accessible for "docker build" (Dockerfile). 206 # Only update TAR file if neccessary to prevent Docker from always rebuilding the image 207 my $tarfile = "$Bin/docker-test/scripts.tar"; 208 my $olddir = getcwd; chdir $Bin; 209 # keep existing TAR file (and its timestamp) unless there were changes 210 if (-f $tarfile) { 211 # create a temp TAR file and compare checksums 212 `tar --create -f "$tarfile.new" scripts testenv`; 213 (my $checksum_old = `sha256sum "$tarfile"`) =~ s/^(\S+)\s+.*/$1/s; 214 (my $checksum_new = `sha256sum "$tarfile.new"`) =~ s/^(\S+)\s+.*/$1/s; 215 if ($checksum_new ne $checksum_old) { 216 move("$tarfile.new", $tarfile); 217 } 218 else { 219 unlink "$tarfile.new"; 220 } 221 } 222 else { 223 `tar --create -f "$tarfile" scripts testenv`; 224 } 225 chdir $olddir; 226 227 @cmd = ( $engine, qw( build -t oxi-test ), "$Bin/docker-test"); 228 execute \@cmd; 229} 230 231# 232# Run container 233# 234@cmd = ( $engine, qw( run -it --rm ), @docker_args, "oxi-test" ); 235printf "\nExecuting: %s\n", join(" ", @cmd); 236execute \@cmd, 1; 237 238__END__ 239 240=head1 SYNOPSIS 241 242docker-test.pl --all [Options] 243 244docker-test.pl --only TESTSPEC [Options] 245 246docker-test.pl --cover [Options] 247 248docker-test.pl --help 249 250Modes: 251 252 --all 253 Run all unit and QA tests 254 255 --only TESTSPEC 256 Run only the given tests (option can be specified multiple times) 257 258 --cover 259 Run code coverage tests instead of normal tests and copy the directory 260 containing the test results into the root of the local repository 261 (code-coverage-YYMMDD-HHMMSS) 262 263Options: 264 265 -c COMMIT 266 --commit COMMIT 267 Use the given COMMIT instead of "HEAD" (can be anything git understands) 268 269 -b BRANCH 270 --branch BRANCH 271 Use the given BRANCH instead of the current (local repo) or the default 272 (remote repo) 273 274 -r REPO 275 --repo REPO 276 Use the given (remote) REPOsitory instead of the local one where we are 277 currently in 278 279 --batch 280 Run the tests non-interactively in "batch"/"script" mode: 281 1. Do not try to rebuild the Docker image 282 2. In case of errors, do not open a shell in the container, just exit. 283 284 --help 285 Show full documentation 286 287=head1 DESCRIPTION 288 289Run unit and QA tests for OpenXPKI in a Docker container. 290 291You need a working Docker installation to run this script. 292On first execution a Docker image called "oxi-test" is built (might take 293more than 10 minutes). 294Then (on every call) a Docker container is created from the image in which 295the repo is cloned and the tests are executed (takes a few minutes). 296 297This means that nothing will be modified on your host system (in your local 298repository) except for --coverage, in that case the results are copied over. 299 300=head1 SYNTAX 301 302=head2 TESTSPEC 303 304Tests can be specified either by directory: 305 306 core/server/t/31_database 307 # same as: t/31_database 308 qatest/backend/api2/ 309 310or file name: 311 312 core/server/t/31_database/01-base.t 313 # same as: t/31_database/01-base.t 314 qatest/backend/api2/10_list_profiles.t 315 316=head1 EXAMPLES 317 318docker-test.pl --all 319 320 Test the latest commit (not working dir!) of the current Git branch in your 321 local repo. 322 323docker-test.pl --only t/31_database --only t/45_session 324 325 Only run database and session related unit tests. 326 327docker-test.pl --coverage 328 329 Test the code coverage using "cover -test". 330 331docker-test.pl --all -b myfix 332 333 Test latest commit of branch "myfix" in your local repo. 334 335docker-test.pl --all -r openxpki/openxpki 336 337 Test latest commit of default branch in Github repository "openxpki/openxpki". 338 339docker-test.pl --all -r https://github.com/openxpki/openxpki.git 340 341 Same as above. 342 343=cut 344