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