1#!/usr/bin/perl 2 3# Obeyed env variables: 4# OXI_TEST_ONLY (Str: comma separated list of tests dirs/files) 5# OXI_TEST_ALL (Bool: 1 = run all tests) 6# OXI_TEST_COVERAGE (Bool: 1 = only run coverage tests) 7# OXI_TEST_GITREPO (Str: Git repository) 8# OXI_TEST_GITBRANCH (Str: Git branch, default branch if not specified) 9 10use strict; 11use warnings; 12 13# Core modules 14use Cwd qw( realpath ); 15use File::Copy; 16use File::Path qw( make_path ); 17use File::Temp qw( tempdir ); 18use FindBin qw( $Bin ); 19use Getopt::Long; 20use IPC::Open3 qw( open3 ); 21use List::Util qw( sum ); 22use Pod::Usage; 23use POSIX ":sys_wait_h"; 24use Symbol qw( gensym ); 25 26# 27# Configuration 28# 29my $clone_dir = "/opt/openxpki"; 30my $config_dir = $ENV{'OXI_TEST_SAMPLECONFIG_DIR'} || die "OXI_TEST_SAMPLECONFIG_DIR is not set"; 31 32# Exit handler - run bash on errors to allow inspection of log files 33# 34sub _exit { 35 my ($start_bash, $code, $msg) = @_; 36 if ($start_bash) { 37 print STDERR "\n==========[ ERROR ]==========\n"; 38 print STDERR "$msg\n" if $msg; 39 print STDERR "You may now inspect the log files below /var/log/openxpki/\n"; 40 print STDERR "To finally stop the Docker container type 'exit'.\n\n"; 41 system "/bin/bash", "-l"; 42 } 43 else { 44 print STDERR "\n$msg\n\n" if $msg; 45 } 46 exit $code; 47} 48 49sub _failure { 50 my ($die_on_error, $code, $msg) = @_; 51 return $code unless $die_on_error; 52 my $start_bash = not $ENV{OXI_TEST_NONINTERACTIVE}; 53 _exit $start_bash, $code, $msg; 54} 55 56sub _stop { 57 my ($code, $msg) = @_; 58 _exit 0, $code, $msg; 59} 60 61# $mode: 62# code - hide output and return exit code 63# capture - hide output and return it as string instead, exit on errors 64# show - show output and return nothing, exit on errors 65sub execute { 66 my ($mode, $args, $tolerate_errors) = @_; 67 $args = [ split /\s+/, $args ] unless ref $args eq "ARRAY"; 68 69 my $output = ($mode eq "show") ? ">&STDOUT" : gensym; # gensym = filehandle 70 # execute command and wait for it to finish 71 my $pid = open3(0, $output, 0, @$args); 72 waitpid($pid, 0); 73 74 my $die_on_error = not ($mode eq "code" or $tolerate_errors); 75 my $output_str = ref $output eq "GLOB" ? do { local $/; <$output> } : ""; 76 return _failure($die_on_error, -1) if $? == -1; # execute failed: error message was already shown by system() 77 return _failure(!$tolerate_errors, $? & 127, sprintf( "'%s' died with signal %d: %s", $args->[0], ($? & 127), $output_str )) if ($? & 127); 78 return _failure($die_on_error, $? >> 8, sprintf( "'%s' exited with code %d: %s", $args->[0], $? >> 8, $output_str )) if ($? >> 8); 79 80 return if $mode eq "show"; 81 return $output_str if $mode eq "capture"; 82 return 0; 83} 84 85sub git_checkout { 86 my ($env_repo, $branch, $commit, $target) = @_; 87 88 my $repo = $ENV{$env_repo}; 89 90 _stop 100, "Please specify either a remote or local Git repo:\ndocker run -e $env_repo=https://...\ndocker run -e $env_repo=/repo -v /my/host/path:/repo ..." 91 if $repo !~ m{ \A ( / | (https?|ssh):// ) }msx; 92 93 my $is_local = not $repo =~ / \A (https?|ssh): /msx; 94 95 # local repo from host (if Docker volume is mounted) 96 if ($is_local) { 97 # stop unless $repo is a mountpoint (= device number differs from parent dir) 98 _stop 101, "Path specified in $env_repo is not a mountpoint" 99 unless (-d $repo and (stat $repo)[0] != (stat "/")[0]); 100 $repo = "file://$repo"; 101 } 102 103 my $code = execute code => [ "git", "ls-remote", "-h", $repo ]; 104 _stop 103, "Repo $repo either does not exist or is not readable" if $code; 105 106 # 107 # Clone repository 108 # 109 print "- Cloning repo into $target ... "; 110 my @branch_spec = $branch ? "--branch=$branch" : (); 111 my @restrict_depth = $commit ? () : ("--depth=1"); 112 execute capture => [ "git", "clone", @restrict_depth, @branch_spec, $repo, $target ]; 113 if ($commit) { 114 print "Checking out given commit... "; 115 chdir $target; 116 execute capture => [ "git", "checkout", $commit ]; 117 } 118 print "\n"; 119 120 # 121 # Informations 122 # 123 printf " Repo: %s%s\n", $ENV{$env_repo}, $is_local ? " (local)" : ""; 124 printf " Branch: %s\n", $branch // "(default)"; 125 printf " Commit: %s\n", $commit // "HEAD"; 126 127 # last commit's message 128 chdir $target; 129 my $logmsg = execute capture => [ "git", "log", "--format=%B", "-n" => 1, $commit // "HEAD", ]; 130 $logmsg =~ s/\R$//gm; # remove trailing newline 131 ($logmsg) = split /\R/, $logmsg; # only print first line 132 printf " » %s «\n", $logmsg; 133 134 return $is_local; 135} 136 137sub git_is_based_on { 138 my ($code_dir, $branch) = @_; 139 140 my $temp_coderepo = tempdir( CLEANUP => 1 ); 141 # get commit id of $branch in official repo 142 `git clone --quiet --depth=1 --branch=$branch https://github.com/openxpki/openxpki.git $temp_coderepo`; 143 chdir $temp_coderepo; 144 my $commit_id_develop=`git rev-parse HEAD`; 145 146 chdir $code_dir; 147 my $exit_code = execute code => [ 'git', 'merge-base', '--is-ancestor', $commit_id_develop, 'HEAD' ]; 148 149 # exit codes: 1 = develop is no ancestor of HEAD, 128 = commit ID not found 150 return ($exit_code == 0); 151} 152 153my $mode = "all"; # default mode 154$mode = "all" if $ENV{OXI_TEST_ALL}; 155$mode = "coverage" if $ENV{OXI_TEST_COVERAGE}; 156my @test_only = split ",", $ENV{OXI_TEST_ONLY}; 157$mode = "selected" if scalar @test_only; 158 159my @tests_unit; 160my @tests_qa; 161if ($mode eq "all") { 162 @tests_unit = "t/"; 163 @tests_qa = qw( qatest/backend/api2 qatest/backend/webui qatest/client ); 164} 165elsif ($mode eq "selected") { 166 @tests_unit = grep { /^t\// } map { my $t = $_; $t =~ s/ ^ core\/server\/ //x; $t } @test_only; 167 @tests_qa = grep { /^qatest\// } @test_only; 168} 169 170# 171# Test arguments and repository 172# 173print "\n####[ Run tests in Docker container ]####\n"; 174 175# 176# Code repository 177# 178print "\nCode source:\n"; 179my $local_repo = git_checkout('OXI_TEST_GITREPO', $ENV{OXI_TEST_GITBRANCH}, $ENV{OXI_TEST_GITCOMMIT}, $clone_dir); 180_stop 104, "Code coverage tests only work with local repo" if ($mode eq "coverage" and not $local_repo); 181 182# 183# Config repository 184# 185print "\nConfiguration source:\n"; 186my $config_gitbranch = $ENV{OXI_TEST_CONFIG_GITBRANCH}; 187# auto-set config branch to develop if code is based on develop 188if (not $config_gitbranch) { 189 print "- no Git branch specified\n"; 190 print " - checking if code is based on Github branch 'develop': "; 191 192 if (git_is_based_on($clone_dir, 'community')) { 193 print "yes\n"; 194 $config_gitbranch = 'community'; 195 } 196 else { 197 print "no\n"; 198 print " - checking if code is based on Github branch 'master': "; 199 if (git_is_based_on($clone_dir, 'develop')) { 200 print "yes\n"; 201 $config_gitbranch = 'develop'; 202 } 203 else { 204 print "no\n"; 205 print " --> assuming private repo based on 'community'\n"; 206 $config_gitbranch = 'community'; 207 } 208 } 209} 210git_checkout('OXI_TEST_CONFIG_GITREPO', $config_gitbranch, $ENV{OXI_TEST_CONFIG_GITCOMMIT}, $config_dir); 211 212# 213# List selected tests 214# 215print "\n"; 216my $msg = $mode eq "all" ? " all tests" : ($mode eq "coverage" ? " code coverage" : " selected tests:"); 217print `figlet '$msg'`; 218printf " - $_\n" for @test_only; 219 220# 221# Grab and install Perl module dependencies from Makefile.PL using PPI 222# 223print "\n====[ Scanning Makefile.PL for new Perl dependencies ]====\n"; 224my $cpanfile = execute capture => "/tools-copy/scripts/makefile2cpanfile.pl $clone_dir/core/server/Makefile.PL"; 225open my $fh, ">", "$clone_dir/cpanfile"; 226print $fh $cpanfile; 227close $fh; 228 229execute show => "cpanm --quiet --notest --installdeps $clone_dir"; 230 231# 232# Database setup 233# 234print "\n====[ MySQL ]====\n"; 235my $dummy = gensym; 236my $pid = open3(0, $dummy, 0, qw(sh -c /usr/sbin/mysqld) ); 237execute show => "/tools-copy/testenv/mysql-wait-for-db.sh"; 238execute show => "/tools-copy/testenv/mysql-create-user.sh"; 239# if there are only qatests, we create the database later on 240if ($mode eq "coverage" or scalar @tests_unit) { 241 execute show => "/tools-copy/testenv/mysql-create-db.sh"; 242 execute show => "/tools-copy/testenv/mysql-create-schema.sh"; 243} 244 245# 246# OpenXPKI compilation 247# 248print "\n====[ Compile OpenXPKI ]====\n"; 249## Config::Versioned reads USER env variable 250#export USER=dummy 251 252chdir "$clone_dir/core/server"; 253`perl Makefile.PL`; 254`make`; 255 256# 257# Test coverage 258# 259if ($mode eq "coverage") { 260 print "\n====[ Testing the code coverage (this will take a while) ]====\n"; 261 execute show => "cover -test"; 262 263 my $cover_src = "$clone_dir/core/server/cover_db"; 264 use DateTime; 265 my $dirname = "code-coverage-".(DateTime->now->strftime('%Y%m%d-%H%M%S')); 266 my $cover_target = sprintf "/%s/%s", $ENV{OXI_TEST_GITREPO}, $dirname; 267 268 if (-d $cover_src) { 269 system "mv", $cover_src, $cover_target; 270 if (-d $cover_target) { 271 `chmod -R g+w,o+w "$cover_target"`; 272 print "\nCode coverage results available in project root dir:\n$dirname\n"; 273 } 274 else { 275 print "\nError: code coverage results could not be moved to host dir $cover_target:\n$!\n" 276 } 277 } 278 else { 279 print "\nError: code coverage results where not found\n($cover_src does not exist)\n" 280 } 281 exit; 282} 283 284# 285# Unit tests 286# 287if (scalar @tests_unit) { 288 print "\n====[ Testing: unit tests ]====\n"; 289 execute show => "prove -I ./t/lib -b -r -q $_" for @tests_unit; 290} 291 292exit unless scalar @tests_qa; 293 294# 295# OpenXPKI installation 296# 297print "\n====[ Install OpenXPKI ]====\n"; 298print "Copying files\n"; 299`make install`; 300 301# directory list borrowed from /package/debian/core/libopenxpki-perl.dirs 302make_path "/var/openxpki/session", "/var/log/openxpki"; 303 304# copy config 305`mkdir -p /etc/openxpki && cp -R $config_dir/* /etc/openxpki`; 306 307# customize config 308use File::Slurp qw( edit_file ); 309edit_file { s/ ^ ( (user|group): \s+ ) \w+ /$1root/gmsx } "/etc/openxpki/config.d/system/server.yaml"; 310execute show => "/tools-copy/testenv/mysql-oxi-config.sh"; 311 312# 313# Database (re-)creation 314# 315execute show => "/tools-copy/testenv/mysql-create-db.sh"; 316execute show => "/tools-copy/testenv/mysql-create-schema.sh"; 317 318# 319# Start OpenXPKI and insert test certificates 320# 321`mkdir -p /etc/openxpki/local/keys/`; 322execute show => "/usr/local/bin/openxpkictl start"; 323execute show => "/tools-copy/testenv/insert-certificates.sh"; 324 325# 326# QA tests 327# 328print "\n====[ Testing: QA tests ]====\n"; 329chdir "$clone_dir/qatest"; 330my @t = map { my $t = $_; $t =~ s/ ^ qatest\/ //x; $t } @tests_qa; 331execute show => "prove -I ../core/server/t/lib -I ./lib -l -r -q $_" for @t; 332