Added dbg
[qt:falts-qtqa.git] / scripts / qt / qtmod_test.pl
1        #!/usr/bin/env perl
2 #############################################################################
3 ##
4 ## Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
5 ## Contact: http://www.qt-project.org/legal
6 ##
7 ## This file is part of the Quality Assurance module of the Qt Toolkit.
8 ##
9 ## $QT_BEGIN_LICENSE:LGPL$
10 ## Commercial License Usage
11 ## Licensees holding valid commercial Qt licenses may use this file in
12 ## accordance with the commercial license agreement provided with the
13 ## Software or, alternatively, in accordance with the terms contained in
14 ## a written agreement between you and Digia.  For licensing terms and
15 ## conditions see http://qt.digia.com/licensing.  For further information
16 ## use the contact form at http://qt.digia.com/contact-us.
17 ##
18 ## GNU Lesser General Public License Usage
19 ## Alternatively, this file may be used under the terms of the GNU Lesser
20 ## General Public License version 2.1 as published by the Free Software
21 ## Foundation and appearing in the file LICENSE.LGPL included in the
22 ## packaging of this file.  Please review the following information to
23 ## ensure the GNU Lesser General Public License version 2.1 requirements
24 ## will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
25 ##
26 ## In addition, as a special exception, Digia gives you certain additional
27 ## rights.  These rights are described in the Digia Qt LGPL Exception
28 ## version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
29 ##
30 ## GNU General Public License Usage
31 ## Alternatively, this file may be used under the terms of the GNU
32 ## General Public License version 3.0 as published by the Free Software
33 ## Foundation and appearing in the file LICENSE.GPL included in the
34 ## packaging of this file.  Please review the following information to
35 ## ensure the GNU General Public License version 3.0 requirements will be
36 ## met: http://www.gnu.org/copyleft/gpl.html.
37 ##
38 ##
39 ## $QT_END_LICENSE$
40 ##
41 #############################################################################
42
43 use strict;
44 use warnings;
45
46 use FindBin;
47 use lib "$FindBin::Bin/../lib/perl5";
48
49 package QtQA::ModuleTest;
50 use base qw(QtQA::TestScript);
51
52 use Carp;
53 use Cwd qw( abs_path );
54 use Data::Dumper;
55 use English qw( -no_match_vars );
56 use Env::Path;
57 use File::chdir;
58 use File::Basename;
59 use File::Path;
60 use File::Spec::Functions qw( :ALL );
61 use List::MoreUtils qw( any apply );
62 use autodie;
63 use Readonly;
64 use Text::Trim;
65 use Cwd;
66
67 #Code coverage tools
68 Readonly my $TESTCOCOON  => 'testcocoon';
69
70 Readonly my %COVERAGE_TOOLS => (
71     $TESTCOCOON  =>  1,
72 );
73
74 # Build parts which are useful for testing a module, but not useful for other
75 # modules built on top of the current module.
76 # For example, qtdeclarative does not use the examples or tests from qtbase,
77 # but it may use the libs and tools.
78 Readonly my @OPTIONAL_BUILD_PARTS => qw(examples tests);
79
80 # All properties used by this script.
81 my @PROPERTIES = (
82     q{base.dir}                => q{top-level source directory of module to test},
83
84     q{shadowbuild.dir}         => q{top-level build directory; defaults to $(base.dir). }
85                                 . q{Setting this to any value other than $(base.dir) implies }
86                                 . q{a shadow build, in which case the directory will be }
87                                 . q{recursively deleted if it already exists, and created if }
88                                 . q{it does not yet exist.},
89
90     q{location}                => q{location hint for git mirrors (`oslo' or `brisbane'); }
91                                 . q{only useful inside of Nokia LAN},
92
93     q{qt.branch}               => q{git branch of Qt superproject (e.g. `stable'); only used }
94                                 . q{if qt.gitmodule != "qt5"},
95
96     q{qt.deps_branch}          => q{default git branch for the tested repo's dependencies},
97
98     q{qt.configure.args}       => q{space-separated arguments passed to Qt's configure},
99
100     q{qt.configure.extra_args} => q{more space-separated arguments passed to Qt's configure; }
101                                 . q{these are appended to qt.configure.args when configure is }
102                                 . q{invoked},
103
104     q{qt.init-repository.args} => q{space-separated arguments passed to Qt5's init-repository }
105                                 . q{script},
106
107     q{qt.coverage.tests_output}
108                                => q{full path to the file gathering results from coverage tool},
109
110     q{qt.coverage.tool}        => q{coverage tool name; giving a valid coverage tool name here will }
111                                 . q{enable code coverage using the tool given here. e.g. testcocoon },
112
113     q{qt.dir}                  => q{top-level source directory of Qt superproject; }
114                                 . q{the script will clone qt.repository into this location if it }
115                                 . q{does not exist. Testing without the superproject is not }
116                                 . q{supported},
117
118     q{qt.install.dir}          => q{directory where Qt is expected to be installed (e.g. as set by }
119                                 . q{-prefix option to configure). Mandatory if qt.make_install is 1. }
120                                 . q{This directory will be recursively deleted if it already exists. }
121                                 . q{After installation, basic verification of the install will be }
122                                 . q{performed},
123
124     q{qt.make_install}         => q{if 1, perform a `make install' step after building Qt. }
125                                 . q{Generally this should only be done if (1) the `-prefix' }
126                                 . q{configure option has been used appropriately, and (2) }
127                                 . q{the `-developer-build' configure argument was not used},
128
129     q{qt.make_html_docs}       => q{if 1, perform a `make html_docs' step after building Qt. },
130
131     q{qt.make_html_docs.insignificant}
132                                => q{if 1, ignore all failures from 'make html_docs'},
133
134     q{qt.gitmodule}            => q{(mandatory) git module name of the module under test }
135                                 . q{(e.g. `qtbase').  Use special value `qt5' for testing of }
136                                 . q{all modules together in the qt5 superproject},
137
138     q{qt.revdep.gitmodule}     => q{git module name of the reverse dependency module under test }
139                                 . q{(e.g. `qtdeclarative').  Normally left empty.  Setting this }
140                                 . q{will switch the script into a mode where it tests the revdep }
141                                 . q{git module on top of one of its dependencies (e.g. testing }
142                                 . q{qtdeclarative on top of qtbase)},
143
144     q{qt.revdep.revdep_ref}    => q{git ref for the name of the reverse dependency module under test }
145                                 . q{(e.g. `refs/heads/stable'); mandatory iff qt.revdep.gitmodule is }
146                                 . q{set},
147
148     q{qt.revdep.dep_ref}       => q{git ref for the name of the dependency module upon which the }
149                                 . q{revdep shall be tested (e.g. `refs/heads/stable'); mandatory iff }
150                                 . q{qt.revdep.gitmodule is set},
151
152     q{qt.repository}           => q{giturl of Qt superproject; only used if }
153                                 . q{qt.gitmodule != "qt5"},
154
155     q{qt.minimal_deps}         => q{if 1, when building a module other than qt5 or qtbase, only }
156                                 . q{build the minimum necessary parts of each dependency.  In }
157                                 . q{particular, do not build the autotests or examples for the }
158                                 . q{modules we depend on.  This option passes -nomake tests }
159                                 . q{-nomake examples to configure, and QT_BUILD_PARTS+=tests }
160                                 . q{QT_BUILD_PARTS+=examples while qmaking the module under }
161                                 . q{test, and therefore requires these features to be correctly }
162                                 . q{implemented},
163
164     q{qt.extra.deps}           => q{additional dependecies to be cloned, when building a }
165                                 . q{module. Threre is cases where we can't add all depending }
166                                 . q{modules into sync.profile, in those cases we can bring those }
167                                 . q{in by using this file. Syntax is the same as with sync.profile },
168
169     q{qt.tests.enabled}        => q{if 1, run the autotests (for this module only, or all }
170                                 . q{modules if qt.gitmodule == "qt5")},
171
172     q{qt.tests.insignificant}  => q{if 1, ignore all failures from autotests},
173
174     q{qt.tests.args}           => q{additional arguments to pass to the tests},
175
176     q{qt.tests.timeout}        => q{default maximum runtime permitted for each autotest, in seconds; }
177                                 . q{any test which does not completed within this time will be }
178                                 . q{killed and considered a failure. When using testscheduler, may }
179                                 . q{be overridden by setting testcase.timeout in a test .pro file},
180
181     q{qt.tests.capture_logs}   => q{if set to a directory name, capture all test logs into this }
182                                 . q{directory.  For example, setting qt.tests.capture_logs=}
183                                 . q{$HOME/test-logs will create one file in $HOME/test-logs for }
184                                 . q{each autotest which is run.  If neither this nor }
185                                 . q{qt.tests.tee_logs are used, tests print to STDOUT/STDERR }
186                                 . q{as normal},
187
188     q{qt.tests.tee_logs}       => q{like qt.tests.capture_logs, but also print the test logs to }
189                                 . q{STDOUT/STDERR as normal while the tests are running},
190
191     q{qt.artifacts.dir}        => q{generic artifact dir for Jenkins artifacts},
192
193
194     q{qt.tests.backtraces}     => q{if 1, attempt to capture backtraces from crashing tests, }
195                                 . q{using the platform's best available mechanism; currently }
196                                 . q{uses gdb on Linux, CrashReporter on Mac, and does not work }
197                                 . q{on Windows},
198
199     q{qt.tests.testscheduler}  => q{if 1, run the autotests via the testscheduler script, rather }
200                                 . q{than directly by `make check'; this is intended to eventually }
201                                 . q{become the default},
202
203     q{qt.tests.testscheduler.args}
204                                => q{arguments to pass to testscheduler, if any; for example, -j4 }
205                                 . q{to run autotests in parallel},
206
207     q{qt.tests.flaky.enabled}   => q{enable flaky test plugin },
208
209     q{qt.tests.flaky_mode}     => q{how to handle flaky autotests ("best", "worst" or "ignore")},
210
211     q{qt.tests.dir}            => q{directory where to run the testplanner and execute tests},
212
213     q{qt.slow.enabled}         => q{enabling external embedded testrunner},
214
215     q{qt.slow.testrunner}      => q{external perl script to run embedded tests},
216
217     q{qt.slow.testrunner.args}      => q{args for perl script to run embedded tests},
218
219
220     q{qt.qtqa-tests.enabled}   => q{if 1, run the shared autotests in qtqa (over this module }
221                                 . q{only, or all modules if qt.gitmodule == "qt5").  The qtqa }
222                                 . q{tests are run after the other autotests.},
223
224     q{qt.qtqa-tests.insignificant}
225                                => q{if 1, ignore all failures from shared autotests in qtqa},
226
227     q{make.bin}                => q{`make' command (e.g. `make', `nmake', `jom' ...)},
228
229     q{make.args}               => q{extra arguments passed to `make' command (e.g. `-j25')},
230
231     q{make-check.bin}          => q{`make' command used for running `make check' (e.g. `make', }
232                                 . q{`nmake', `jom'); defaults to the value of make.bin},
233
234     q{make-check.args}         => q{extra arguments passed to `make check' command when running }
235                                 . q{tests (e.g. `-j2'); defaults to the value of make.args with }
236                                 . q{any -jN replaced with -j1, and with -k appended},
237
238     q{qt.sync.profile.dir}     => q{dir prefix, if sync.profile is not stored directly under }
239                                 . q{$qt.gitmodule/sync.profile. This will append it to }
240                                 . q{$qt.gitmodule/$qt.sync.profile.dir/sync.profile },
241
242
243 );
244
245 # gitmodules for which `make check' is not yet safe.
246 # These should be removed one-by-one as modules are verified to work correctly.
247 # See task QTQAINFRA-142
248 my %MAKE_CHECK_BLACKLIST = map { $_ => 1 } qw(
249     qtrepotools
250     qtwebkit
251 );
252
253 # Like abs_path, but dies instead of returning an empty value if dirname($path)
254 # doesn't exist.
255 sub safe_abs_path
256 {
257     my ($self, $path) = @_;
258
259     # On Windows, abs_path dies if $path doesn't exist.
260     # On Unix, it is OK if $path doesn't exist, as long as dirname($path) exists.
261     # Adjust the lookup to get the same behavior on both platforms.
262     my $end;
263     if (! -e $path) {
264         ($end, $path) = fileparse( $path );
265     }
266
267     my $abs_path = eval { abs_path( $path ) };
268     if ($@ || !$abs_path) {
269         $self->fatal_error(
270             "path '$path' unexpectedly does not exist (while in directory: $CWD)"
271         );
272     }
273
274     if ($end) {
275         $abs_path = catfile( $abs_path, $end );
276     }
277
278     return $abs_path;
279 }
280
281 # Returns 1 if ($path_a, $path_b) refer to the same path.
282 # Dies if either of dirname($path_a) or dirname($path_b) don't exist.
283 sub path_eq
284 {
285     my ($self, $path_a, $path_b) = @_;
286     return 1 if ($path_a eq $path_b);
287
288     ($path_a, $path_b) = ($self->safe_abs_path( $path_a ), $self->safe_abs_path( $path_b ));
289     return 1 if ($path_a eq $path_b);
290
291     return 0;
292 }
293
294 # Returns 1 if $sub is underneath or equal to $base.
295 # Dies if either of dirname($base) or dirname($sub) don't exist.
296 sub path_under
297 {
298     my ($self, $base, $sub) = @_;
299     return 1 if ($base eq $sub);
300
301     ($base, $sub) = map { canonpath( $self->safe_abs_path( $_ ) ) } ($base, $sub);
302     return 1 if ($base eq $sub);
303
304     return 1 if ($sub =~ m{\A\Q$base\E(?:/|\\)});
305     return 0;
306 }
307
308 sub make_autotest_stub_log
309 {
310     my ($self) = @_;
311     my $qt_tests_tee_logs        = $self->{ 'qt.tests.tee_logs' };
312     my $qt_artifacts_dir         = $self->{ 'qt.artifacts.dir' };
313
314     print "Creating stub xml\n";
315     my $path = canonpath($qt_artifacts_dir."/".$qt_tests_tee_logs);
316     rmtree( $path );
317     mkpath( $path );
318     print "Created log dir $path";
319     my $stub_log = canonpath( $path."/Empty.xml" );
320     open ( STUBLOG, ">$stub_log" ) or die "Unable to open stub autotest log file: $!\n";
321     print STUBLOG "<?xml version=\"1.0\" encoding=\"UTF-8\"?><TestCase>NoTests</TestCase>";
322     close ( STUBLOG );
323     my $doing = $self->doing( "   TESTS  creted stub $stub_log" );
324
325 }
326
327 sub run
328 {
329     my ($self) = @_;
330
331     $self->read_and_store_configuration;
332
333     my $qt_gitmodule = $self->{ 'qt.gitmodule' };
334     my $doing = $self->doing( "testing $qt_gitmodule" );
335
336     $self->run_clean_directories;
337     $self->run_git_checkout;
338
339     my $doing_revdep = $self->maybe_enter_revdep_context;
340
341     $self->run_configure;
342     $self->run_qtqa_autotests( 'prebuild' );
343     $self->run_compile;
344     $self->run_install;
345     $self->run_install_check;
346     $self->run_make_html_docs;
347     #$self->make_autotest_stub_log;
348     if ($self->{ 'qt.slow.enabled' }){
349         $self->run_embedded_autotest;
350     } else {
351         $self->run_autotests;
352     }
353     $self->run_coverage;
354     $self->run_qtqa_autotests( 'postbuild' );
355
356     return;
357 }
358
359 sub new
360 {
361     my ($class, @args) = @_;
362
363     my $self = $class->SUPER::new;
364
365     $self->set_permitted_properties( @PROPERTIES );
366     $self->get_options_from_array( \@args );
367
368     bless $self, $class;
369     return $self;
370 }
371
372 sub default_qt_repository
373 {
374     my ($self) = @_;
375     my $have_qtgitreadonly;
376     if (0 == system(qw(git config --get-regexp ^url\..*\.insteadof$ ^qtgitreadonly:$))) {
377         $have_qtgitreadonly = 1;
378     }
379
380     return ( $self->{'location'} ) ? 'git://scm.dev.nokia.troll.no/qt/qt5.git'
381          : ( $have_qtgitreadonly ) ? 'qtgitreadonly:qt/qt5.git'
382          :                           'git://qt.gitorious.org/qt/qt5.git';
383 }
384
385 sub default_qt_tests_enabled
386 {
387     my ($self) = @_;
388
389     my $qt_gitmodule = $self->{ 'qt.gitmodule' };
390
391     # By default, avoid the modules known to be bad.
392     # See task QTQAINFRA-142
393     if ($MAKE_CHECK_BLACKLIST{$qt_gitmodule}) {
394         warn "Autotests are not yet runnable for $qt_gitmodule";
395         return 0;
396     }
397
398     return 1;
399 }
400
401 sub default_qt_tests_backtraces
402 {
403     my ($self) = @_;
404     return ($OSNAME =~ m{linux|darwin}i);
405 }
406
407 sub default_qt_revdep_ref
408 {
409     my ($self) = @_;
410
411     # If qt.revdep.gitmodule is set, this is mandatory.
412     # Otherwise, it is unused.
413     return $self->{ 'qt.revdep.gitmodule' } ? undef : q{};
414 }
415
416 sub default_qt_dir
417 {
418     my ($self) = @_;
419
420     # We don't need to clone qt5 superproject for qt/qt5.git or qt/qt.git
421     if (($self->{'qt.gitmodule'} eq 'qt5') or ($self->{'qt.gitmodule'} eq 'qt')) {
422         return $self->{ 'base.dir' };
423     }
424
425     return catfile( $self->{'base.dir'}, 'qt' );
426 }
427
428 sub default_qt_configure_args
429 {
430     my ($self) = @_;
431
432     return q{-opensource -confirm-license -prefix }.catfile( $self->{ 'qt.dir' }, 'qtbase' );
433 }
434
435 sub default_qt_tests_args
436 {
437     my ($self) = @_;
438
439     my @out = ('-silent');
440
441     # If we're capturing logs, arrange to capture native XML by default
442     # for maximum fidelity, and also print to stdout for live feedback.
443     if ($self->{ 'qt.tests.capture_logs' } || $self->{ 'qt.tests.tee_logs' }) {
444         # Will create files like:
445         #
446         #   path/to/capturedir/tst_qstring-testresults-00.xml
447         #   path/to/capturedir/tst_qwidget-testresults-00.xml
448         #
449         # ...etc.
450         push @out, '-o', 'testresults.xml,xml', '-o', '-,txt';
451     }
452
453     return join(' ', @out);
454 }
455
456 sub default_make_check_bin
457 {
458     my ($self) = @_;
459     return $self->{ 'make.bin' };
460 }
461
462 sub default_make_check_args
463 {
464     my ($self) = @_;
465
466     my @make_args = split(/ /, $self->{ 'make.args' });
467
468     # Arguments for `make check' are like the arguments for `make',
469     # except:
470     #
471     #  - we want to keep running as many tests as possible, even after failures
472     #
473     if (! any { m{\A [/\-]k \z }xmsi } @make_args) {
474         push @make_args, '-k';
475     }
476
477     #  - we want to run the tests one at a time (-j1), as they are not all
478     #    parallel-safe; but note that nmake always behavies like -j1 and dies
479     #    if explicitly passed -j1.
480     #
481     @make_args = apply { s{\A -j\d+ \z}{-j1}xms } @make_args;
482
483     return join(' ', @make_args);
484 }
485
486 sub default_qt_minimal_deps
487 {
488     my ($self) = @_;
489
490     my $gitmodule = $self->{ 'qt.revdep.gitmodule' } || $self->{ 'qt.gitmodule' };
491
492     # minimal dependencies makes sense for everything but these three
493     return ($gitmodule ne 'qt5' && $gitmodule ne 'qtbase' && $gitmodule ne 'qt');
494 }
495
496 sub default_qt_install_dir
497 {
498     my ($self) = @_;
499
500     # qt.make_install is mandatory to be set if 'make install' is called,
501     # otherwise no value is needed.
502     if ($self->{ 'qt.make_install' }) {
503         return;
504     }
505     else {
506         return q{};
507     }
508 }
509
510
511
512 sub read_and_store_configuration
513 {
514     my $self = shift;
515
516     my $doing = $self->doing( 'determining test script configuration' );
517
518     $self->read_and_store_properties(
519         'base.dir'                => \&QtQA::TestScript::default_common_property ,
520         'shadowbuild.dir'         => \&QtQA::TestScript::default_common_property ,
521         'location'                => \&QtQA::TestScript::default_common_property ,
522         'make.args'               => \&QtQA::TestScript::default_common_property ,
523         'make.bin'                => \&QtQA::TestScript::default_common_property ,
524
525         'make-check.args'         => \&default_make_check_args                   ,
526         'make-check.bin'          => \&default_make_check_bin                    ,
527         'qt.gitmodule'            => undef                                       ,
528         'qt.revdep.gitmodule'     => q{}                                         ,
529         'qt.revdep.revdep_ref'    => \&default_qt_revdep_ref                     ,
530         'qt.revdep.dep_ref'       => \&default_qt_revdep_ref                     ,
531         'qt.dir'                  => \&default_qt_dir                            ,
532         'qt.repository'           => \&default_qt_repository                     ,
533         'qt.branch'               => q{stable}                                   ,
534         'qt.deps_branch'          => q{}                                         ,
535         'qt.extra.deps'           => q{}                                         ,
536         'qt.init-repository.args' => q{}                                         ,
537         'qt.configure.args'       => \&default_qt_configure_args                 ,
538         'qt.configure.extra_args' => q{}                                         ,
539         'qt.coverage.tests_output'=> q{}                                         ,
540         'qt.coverage.tool'        => q{}                                         ,
541         'qt.make_install'         => 0                                           ,
542         'qt.make_html_docs'       => 0                                           ,
543         'qt.make_html_docs.insignificant' => 0                                   ,
544         'qt.minimal_deps'         => \&default_qt_minimal_deps                   ,
545         'qt.install.dir'          => \&default_qt_install_dir                    ,
546         'qt.tests.enabled'        => \&default_qt_tests_enabled                  ,
547         'qt.tests.testscheduler'  => 0                                           ,
548         'qt.tests.testscheduler.args' => q{}                                     ,
549         'qt.tests.insignificant'  => 0                                           ,
550         'qt.tests.timeout'        => 450                                         ,
551         'qt.tests.capture_logs'   => q{}                                         ,
552         'qt.artifacts.dir'        => q{}                                         ,
553         'qt.tests.tee_logs'       => q{}                                         ,
554         'qt.tests.dir'            => q{}                                         ,
555         'qt.tests.args'           => \&default_qt_tests_args                     ,
556         'qt.tests.backtraces'     => \&default_qt_tests_backtraces               ,
557         'qt.tests.flaky_mode'     => q{}                                         ,
558         'qt.slow.enabled'         => q{}                                                                         ,
559         'qt.slow.testrunner'      => q{}                                                                         ,
560         'qt.slow.testrunner.args' => q{}                                         ,
561         'qt.tests.flaky.enabled'  => 1                                           ,
562
563         'qt.qtqa-tests.enabled'         => 0                                     ,
564         'qt.qtqa-tests.insignificant'   => 0                                     ,
565         'qt.sync.profile.dir'     => q{}                                         ,
566
567     );
568
569     # for convenience only - this should not be overridden
570     $self->{'qt.gitmodule.dir'} = ($self->{'qt.gitmodule'} eq 'qt5')
571         ? $self->{'qt.dir'}
572         : catfile( $self->{'qt.dir'}, $self->{'qt.gitmodule'} )
573     ;
574
575     # Path of the top-level qt5 build:
576     $self->{'qt.build.dir'} = ($self->{'base.dir'} eq $self->{'shadowbuild.dir'})
577         ? $self->{'qt.dir'}          # no shadow build - same path as qt sources
578         : $self->{'shadowbuild.dir'} # shadow build - whatever is requested by the property
579     ;
580
581     # Path of this gitmodule's build;
582     $self->{'qt.gitmodule.build.dir'} = (($self->{'qt.gitmodule'} eq 'qt5') or ($self->{'qt.gitmodule'} eq 'qt'))
583         ? $self->{'qt.build.dir'}
584         : catfile( $self->{'qt.build.dir'}, $self->{'qt.gitmodule'} )
585     ;
586
587     if ($self->{'qt.tests.capture_logs'} && $self->{'qt.tests.tee_logs'}) {
588         delete $self->{'qt.tests.capture_logs'};
589         warn 'qt.tests.capture_logs and qt.tests.tee_logs were both specified; '
590             .'tee_logs takes precedence';
591     }
592
593     if ($self->{'qt.coverage.tool'} && !$COVERAGE_TOOLS{ $self->{'qt.coverage.tool'} }) {
594         die "'$self->{'qt.coverage.tool'}' is not a valid Qt coverage tool; try one of ".join(q{,}, keys %COVERAGE_TOOLS);
595     }
596
597     if ($self->{'qt.minimal_deps'}
598         && !$self->{'qt.revdep.gitmodule'}
599         && ($self->{'qt.gitmodule'} eq 'qt5' || $self->{'qt.gitmodule'} eq 'qtbase'))
600     {
601         warn "qt.minimal_deps is set to 1.  This has no effect on $self->{ 'qt.gitmodule' }.\n";
602         $self->{'qt.minimal_deps'} = 0;
603     }
604
605     # Make sure revdep settings are sensible:
606     #  - revdep test doesn't make sense for modules with no dependencies.
607     #  - revdep test on top of qt4 (not modular) doesn't make sense
608     #  - revdep test on top of qt5 could make sense, but is not yet supported
609     my %no_dependencies = map { $_ => 1 } qw(qt qt5 qtbase);
610     my %not_supported_revdep_base = map { $_ => 1 } qw(qt qt5);
611     if ($no_dependencies{ $self->{'qt.revdep.gitmodule'} }) {
612         $self->fatal_error(
613             "'$self->{'qt.revdep.gitmodule'}' does not make sense for a revdep test; "
614            ."it has no dependencies"
615         );
616     } elsif ($self->{'qt.revdep.gitmodule'} && $not_supported_revdep_base{ $self->{'qt.gitmodule'} }) {
617         $self->fatal_error(
618             "doing a revdep test on top of $self->{'qt.gitmodule'} is currently not supported"
619         );
620     }
621
622     return;
623 }
624
625 sub read_dependencies
626 {
627     my ($self, $dependency_file) = @_;
628
629     my $doing = $self->doing( "reading dependencies from ".abs2rel( $dependency_file ) );
630
631     our (%dependencies);
632
633     my %default_dependencies = ( 'qtbase' => 'refs/heads/stable' );
634     my $default_reason;
635     my %extra_dependencies = $self->{'qt.extra.deps'};
636     if (! -e $dependency_file ) {
637         $default_reason = "$dependency_file doesn't exist";
638     }
639     else {
640         unless ( do $dependency_file ) {
641             my ($action, $error);
642             if ($@) {
643                 ($action, $error) = ('parse', $@);
644             } elsif ($!) {
645                 ($action, $error) = ('execute', $!);
646             }
647             if ($error) {
648                 $self->fail(
649                     "I couldn't $action $dependency_file, which I need to determine dependencies.\n"
650                    ."The error was $error\n"
651                 );
652             }
653         }
654         if (! %dependencies) {
655             $default_reason = "Although $dependency_file exists, it did not specify any \%dependencies";
656         }
657     }
658     
659     #%dependencies = (%dependencies, %extra_dependencies);
660     print "DBG \n";
661     print Data::Dumper(\%dependencies);
662     print Data::Dumper(\%extra_dependencies);
663     pritn "DBG \n";
664     if ($default_reason) {
665         %dependencies = %default_dependencies;
666         warn __PACKAGE__ . ": $default_reason, so I have assumed this module depends on:\n  "
667             .Data::Dumper->new([\%dependencies], ['dependencies'])->Indent(0)->Dump()
668             ."\n";
669     }
670
671     return %dependencies;
672 }
673
674 sub run_clean_directories
675 {
676     my ($self) = @_;
677
678     my $doing = $self->doing( 'cleaning existing target directories' );
679
680     my $qt_dir = $self->{ 'qt.dir' };
681     my $qt_build_dir = $self->{ 'qt.build.dir' };
682     my $qt_install_dir = $self->{ 'qt.install.dir' };
683
684     my @to_delete;
685     my @to_create;
686
687     if ($qt_dir ne $qt_build_dir) {
688         # shadow build?  make sure we start clean.
689         if (-e $qt_build_dir) {
690             push @to_delete, $qt_build_dir;
691         }
692         push @to_create, $qt_build_dir;
693     }
694
695     if ($qt_install_dir
696             && -d dirname( $qt_install_dir )
697             && !$self->path_under( $qt_build_dir, $qt_install_dir )
698             && -e $qt_install_dir
699     ) {
700         push @to_delete, $qt_install_dir;
701         # note we do not create the install dir, `make install' is expected to do that
702     }
703
704     # Job can be configured to outside base.dir.
705     # If we are in same dir, it was already deleted
706     if ((-e $qt_dir) && ( cwd() ne $qt_dir)) {
707         push @to_delete, $qt_dir;
708     }
709     # it will get created once cloned and checked out
710
711     if (@to_delete) {
712         local $LIST_SEPARATOR = qq{\n*** WARNING:    };
713         warn(
714              ("*" x 80)."\n"
715             ."*** WARNING: About to remove:$LIST_SEPARATOR@to_delete\n"
716             ."*** WARNING: You have only a few seconds to abort (CTRL+C) !\n"
717             .("*" x 80)."\n"
718         );
719         sleep 15;
720         warn "Removing...\n";
721         rmtree( \@to_delete );
722         warn "Removed.\n";
723     }
724
725     mkpath( \@to_create );
726
727     return;
728 }
729
730 # If revdep mode is enabled, set qt.gitmodule=qt.revdep.gitmodule, so the rest of the
731 # script will test the revdep.  This should be called after the initial git setup.
732 #
733 # Warning: this is non-reversible!
734 #
735 sub maybe_enter_revdep_context
736 {
737     my ($self) = @_;
738
739     my $qt_revdep_gitmodule = $self->{ 'qt.revdep.gitmodule' };
740     my $qt_revdep_revdep_ref = $self->{ 'qt.revdep.revdep_ref' };
741     my $qt_gitmodule = $self->{ 'qt.gitmodule' };
742
743     return unless $qt_revdep_gitmodule;
744
745     my $what = "testing reverse dependency $qt_revdep_gitmodule ($qt_revdep_revdep_ref) "
746               ."on top of $qt_gitmodule";
747
748     print __PACKAGE__ . ": $what\n";
749
750     $self->{ 'qt.gitmodule' } = $qt_revdep_gitmodule;
751     $self->{ 'qt.gitmodule.dir' } = catfile( $self->{'qt.dir'}, $self->{'qt.gitmodule'} );
752     $self->{ 'qt.gitmodule.build.dir' } = catfile( $self->{'qt.build.dir'}, $self->{'qt.gitmodule'} );
753
754     return $self->doing( $what );
755 }
756
757 # Run init-repository for the given @modules.
758 # This may be safely run more than once to incrementally clone additional modules.
759 # @modules may be omitted to imply _all_ modules.
760 sub do_init_repository
761 {
762     my ($self, @modules) = @_;
763
764     my $doing = $self->doing( 'running init-repository for '.join(',', @modules) );
765
766     my $qt_dir = $self->{ 'qt.dir' };
767     my $qt_init_repository_args = $self->{ 'qt.init-repository.args' };
768     my $location = $self->{ 'location' };
769
770     local $CWD = $qt_dir;
771
772     my @init_repository_arguments = split( q{ }, $qt_init_repository_args );
773
774     if (defined( $location ) && ($location eq 'brisbane')) {
775         push @init_repository_arguments, '-brisbane-nokia-developer';
776     }
777     elsif (defined( $location ) && ($location eq 'oslo')) {
778         push @init_repository_arguments, '-nokia-developer';
779     }
780
781     if (@modules) {
782         push @init_repository_arguments, '--module-subset='.join(q{,}, @modules);
783     }
784
785     # We use `-force' so that init-repository can be restarted if it hits an error
786     # halfway through.  Without this, it would refuse.
787     push @init_repository_arguments, '-force';
788
789     $self->exe( { reliable => 'git' },  # recover from transient git errors during init-repository
790         'perl', './init-repository', @init_repository_arguments
791     );
792
793     return;
794 }
795
796 sub set_module_refs
797 {
798     my ($self, %module_to_ref) = @_;
799
800     my $qt_dir = $self->{ 'qt.dir' };
801     my $qt_ref = $self->{ 'qt.deps_branch' };
802     $qt_ref = $self->{ 'qt.branch' } if ($qt_ref eq '');
803     $qt_ref = "refs/heads/".$qt_ref;
804
805
806     # Checkout dependencies as specified in the sync.profile, which specifies the sha1s/refs within them
807     # Also, this code assumes that init-repository always uses `origin' as the remote.
808     while ( my ($module, $ref) = each %module_to_ref ) {
809         local $CWD = catfile( $qt_dir, $module );
810         $ref = $qt_ref if ($ref eq '');
811
812         # FIXME how do we guarantee we have this SHA1?
813         # If it's not reachable from a branch obtained from a default `clone', it could be missing.
814         if ( $ref !~ /^[0-9a-f]{40}$/) { # Not a SHA1, fetch origin to ensure using correct SHA-1
815             $self->exe( 'git', 'fetch', '--verbose', '--update-head-ok', 'origin', "+$ref:$ref" );
816         }
817         $self->exe( 'git', 'reset', '--hard', $ref );
818
819         # init-repository is expected to initialize any nested gitmodules where
820         # necessary; however, since we are changing the tracked SHA1 here, we
821         # need to redo a `submodule update' in case any gitmodule content is
822         # affected.  Note that the `submodule update' is a no-op in the usual case
823         # of no nested gitmodules.
824         $self->exe( 'git', 'submodule', 'update', '--recursive', '--init' );
825     }
826
827     return;
828 }
829
830 # Maybe skip (warn and exit with 0 exit code) the revdep test, if:
831 #  - the revdep module actually does not depend on _this_ module.
832 #  - the revdep sync.profile refers to a ref other than qt.revdep.dep_ref
833 #
834 sub maybe_skip_revdep_test
835 {
836     my ($self, %module_to_ref) = @_;
837
838     my $qt_gitmodule = $self->{ 'qt.gitmodule' };
839     my $qt_revdep_gitmodule = $self->{ 'qt.revdep.gitmodule' };
840     my $qt_revdep_dep_ref = $self->{ 'qt.revdep.dep_ref' };
841
842     if (! exists $module_to_ref{ $qt_gitmodule }) {
843         warn "revdep module [$qt_revdep_gitmodule] does not depend on this module "
844             ."[$qt_gitmodule].\nrevdep test skipped.\n";
845         exit 0;
846     }
847
848     my $wanted_dep_ref = $module_to_ref{ $qt_gitmodule };
849     if ($wanted_dep_ref ne $qt_revdep_dep_ref && $wanted_dep_ref ne '') {
850         warn "revdep module's sync.profile refers to a ref other than this one:\n"
851             ."  [$qt_revdep_gitmodule]: $qt_gitmodule => $wanted_dep_ref\n"
852             ."  [$qt_gitmodule]: qt.revdep.dep_ref => $qt_revdep_dep_ref\n"
853             ."revdep test skipped.\n";
854             exit 0;
855     }
856
857     return;
858 }
859
860 sub run_git_checkout
861 {
862     my ($self) = @_;
863
864     my $doing = $self->doing( 'setting up git repositories' );
865
866     my $base_dir      = $self->{ 'base.dir'      };
867     my $qt_init_repository_args
868                       = $self->{ 'qt.init-repository.args' };
869     my $qt_branch     = $self->{ 'qt.branch'     };
870     my $qt_repository = $self->{ 'qt.repository' };
871     my $qt_dir        = $self->{ 'qt.dir'        };
872     my $qt_gitmodule  = $self->{ 'qt.gitmodule'  };
873     my $qt_revdep_gitmodule = $self->{ 'qt.revdep.gitmodule' };
874     my $qt_revdep_revdep_ref = $self->{ 'qt.revdep.revdep_ref' };
875     my $location      = $self->{ 'location'      };
876     my $qt_sync_profile_dir = $self->{ 'qt.sync.profile.dir' };
877
878
879     # We don't need to clone submodules for qt/qt.git
880     return if ($qt_gitmodule eq 'qt');
881
882     chdir( $base_dir );
883
884     # Store the SHA1 to be tested into refs/testing before doing anything which might
885     # move us to some other revision.
886     $self->exe( 'git', 'update-ref', 'refs/testing', 'HEAD' );
887
888     # Clone the Qt superproject
889     if ($qt_gitmodule ne 'qt5') {
890         $self->exe( 'git', 'clone', '--branch', $qt_branch, $qt_repository, $qt_dir );
891     }
892
893     if ($qt_gitmodule eq 'qt5') {
894         # We have to set the remote url to be used with older git clients
895         $self->exe( 'git', 'config', 'remote.origin.url', $qt_repository );
896     }
897
898     local $CWD = $qt_dir;
899
900     # map from gitmodule name to desired ref for testing
901     my %module_to_ref;
902
903     # list of modules we need to clone via init-repository
904     my @needed_modules;
905
906     if ($qt_revdep_gitmodule) {
907         # In revdep mode, the revdep determines the needed modules...
908         $self->do_init_repository( $qt_revdep_gitmodule );
909         $self->set_module_refs( $qt_revdep_gitmodule => $qt_revdep_revdep_ref );
910         %module_to_ref = $self->read_dependencies( catfile($qt_revdep_gitmodule, 'sync.profile') );
911         $self->maybe_skip_revdep_test( %module_to_ref );
912         # ...but we don't respect the revdep's sync.profile entry for _this_ module, since we're testing
913         # an incoming change
914         delete $module_to_ref{ $qt_gitmodule };
915     } elsif ($qt_gitmodule ne 'qt5' && $qt_gitmodule ne 'qtbase') {
916         %module_to_ref = $self->read_dependencies( catfile($base_dir, $qt_sync_profile_dir, 'sync.profile') );
917     }
918
919     # clone any remaining modules we haven't got yet.
920     push @needed_modules, keys( %module_to_ref );
921
922     # only do init-repository if there's at least one needed module, or if we're
923     # testing qt5 (in which case we expect to test _all_ modules).
924     # Note that @needed_modules should _not_ contain $qt_gitmodule, that is handled
925     # in the next step.
926     if (@needed_modules || $qt_gitmodule eq 'qt5') {
927         $self->do_init_repository( @needed_modules );
928     }
929
930     # Now we need to set the submodule content equal to our tested module's base.dir
931     if ($qt_gitmodule ne 'qt5') {
932         # Store a flag telling us whether or not this is a module hosted in qt5.git;
933         # the build/test procedure can differ slightly depending on this value.
934         $self->{ module_in_qt5 } = (-d $qt_gitmodule);
935
936         if ($self->path_eq( $base_dir, $qt_gitmodule )) {
937             # If the gitmodule directory in qt5.git is equal to base.dir, then we already
938             # have the repo; we just need to set the revision back to what it was before
939             # init-repository.
940             $module_to_ref{ $qt_gitmodule } = 'refs/testing';
941         } else {
942             # Otherwise, we don't have the repo (we didn't pass it to init-repository).
943             # base.dir's HEAD should still point at what we want to test, so just clone
944             # it, and no further work required.
945
946             # The directory should be empty; verify it first.
947             # Git will give a fatal error if it isn't empty; by verifying it first,
948             # we can give a more detailed and explicit error message.
949             my @files;
950             if (-d $qt_gitmodule) {
951                 local $CWD = $qt_gitmodule;
952                 push @files, glob( '*' );
953                 push @files, glob( '.*' );
954                 @files = grep { $_ ne '.' && $_ ne '..' } @files;
955             }
956             if (@files) {
957                 $self->fatal_error(
958                     "Dirty build directory; '$qt_gitmodule' exists and is not empty.\n"
959                    ."Saw files: @files"
960                 );
961             }
962
963             $self->exe( 'git', 'clone', '--shared', $base_dir, $qt_gitmodule );
964             # run git submodule update for module under test, if there is one for module
965             chdir( $qt_gitmodule );
966             my $res = $self->exe_qx( 'git', 'submodule', 'status');
967             if ($res ne "") {
968                 $self->exe( 'git', 'submodule', 'update', '--recursive', '--init' );
969             }
970             # return just in case
971             chdir( $qt_dir );
972         }
973     }
974
975     # Set various modules to the SHA1s we want.
976     $self->set_module_refs( %module_to_ref );
977
978     return;
979 }
980
981 sub run_configure
982 {
983     my ($self) = @_;
984
985     my $doing = $self->doing( 'configuring Qt' );
986
987     # properties
988     my $qt_dir                  = $self->{ 'qt.dir'                  };
989     my $qt_build_dir            = $self->{ 'qt.build.dir'            };
990     my $qt_configure_args       = $self->{ 'qt.configure.args'       };
991     my $qt_configure_extra_args = $self->{ 'qt.configure.extra_args' };
992     my $qt_coverage_tool        = $self->{ 'qt.coverage.tool'        };
993     my $qt_minimal_deps         = $self->{ 'qt.minimal_deps'         };
994
995     if ($qt_coverage_tool) {
996         $qt_configure_extra_args .= " -$qt_coverage_tool";
997     }
998
999     if ($qt_minimal_deps) {
1000         # In minimal deps mode, we turn off the optional build parts globally, then later
1001         # turn them on explicitly for this particular module under test.
1002         $qt_configure_extra_args .= join( ' -nomake ', q{}, @OPTIONAL_BUILD_PARTS );
1003     }
1004
1005     chdir( $qt_build_dir );
1006
1007     my $configure = catfile( $qt_dir, 'configure' );
1008     if ($OSNAME =~ /win32/i) {
1009         if ($self->{ 'qt.gitmodule' } eq 'qt') {
1010             # Qt4 does not have a .bat but .exe configure script
1011             $configure .= '.exe';
1012         }
1013         else {
1014             $configure .= '.bat';
1015         }
1016     }
1017
1018     $self->exe( $configure, split(/\s+/, "$qt_configure_args $qt_configure_extra_args") );
1019
1020     return;
1021 }
1022
1023
1024 sub run_compile
1025 {
1026     my ($self) = @_;
1027
1028     my $doing = $self->doing( 'compiling Qt' );
1029
1030     # properties
1031     my $qt_dir                  = $self->{ 'qt.dir'                  };
1032     my $qt_build_dir            = $self->{ 'qt.build.dir'            };
1033     my $qt_install_dir          = $self->{ 'qt.install.dir'          };
1034     my $qt_gitmodule            = $self->{ 'qt.gitmodule'            };
1035     my $qt_gitmodule_dir        = $self->{ 'qt.gitmodule.dir'        };
1036     my $qt_gitmodule_build_dir  = $self->{ 'qt.gitmodule.build.dir'  };
1037     my $make_bin                = $self->{ 'make.bin'                };
1038     my $make_args               = $self->{ 'make.args'               };
1039     my $qt_make_install         = $self->{ 'qt.make_install'         };
1040     my $qt_minimal_deps         = $self->{ 'qt.minimal_deps'         };
1041
1042     my $qmake_bin = catfile( $qt_build_dir, 'qtbase', 'bin', 'qmake' );
1043     my $qmake_install_bin = catfile( $qt_install_dir, 'bin', 'qmake' );
1044
1045     # true iff the module is hosted in qt5.git (affects build procedure)
1046     my $module_in_qt5 = $self->{ module_in_qt5 };
1047
1048     chdir( $qt_build_dir );
1049
1050     my @make_args = split(/ /, $make_args);
1051     my @commands;
1052
1053     my @qmake_args;
1054     if ($qt_minimal_deps) {
1055         # Qt 5 only:
1056         # minimal deps mode?  Then we turned off some build parts in configure, and must
1057         # now explicitly enable them for this module only.
1058         push @qmake_args, uc($qt_gitmodule)."_BUILD_PARTS = libs tools ".join(" ", @OPTIONAL_BUILD_PARTS);
1059     }
1060
1061     if (($self->{'qt.gitmodule'} eq 'qt5') or ($self->{'qt.gitmodule'} eq 'qt')) {
1062         # Building qt5 or qt4; just do a `make' of all default targets in the top-level makefile.
1063         push @commands, sub { $self->exe( $make_bin, @make_args ) };
1064     }
1065     elsif ($module_in_qt5) {
1066         # Building a module hosted in qt5; `configure' is expected to have generated a
1067         # makefile with a `module-FOO' target for this module, with correct dependency
1068         # information. Issuing a `make module-FOO' should automatically build the module
1069         # and all deps, as parallel as possible.
1070         my $make_target = "module-$qt_gitmodule";
1071
1072         push @commands, sub {
1073             my $GLOBAL_QMAKEFLAGS = $ENV{'QMAKEFLAGS'};
1074             local $ENV{'QMAKEFLAGS'} = join(" ", map { '"'.$_.'"' } $GLOBAL_QMAKEFLAGS, @qmake_args);
1075             $self->exe( $make_bin, @make_args, $make_target );
1076         };
1077     }
1078     else {
1079         # Building a module, hosted outside of qt5.
1080         # We need to do three steps; first, build all the dependencies, then qmake this
1081         # module, then make this module.
1082         # The Makefile generated in qt5 doesn't know anything about this module.
1083
1084         # First, we build all deps:
1085         my %dependencies = $self->read_dependencies( "$qt_gitmodule_dir/sync.profile" );
1086         my @module_targets = map { "module-$_" } keys %dependencies;
1087         push @commands, sub { $self->exe( $make_bin, @make_args, @module_targets ) };
1088
1089         if (! -e $qt_gitmodule_build_dir) {
1090             mkpath( $qt_gitmodule_build_dir );
1091             # Note, we don't have to worry about emptying the build dir,
1092             # because it's always under the top-level build dir, and we already
1093             # cleaned that if it existed.
1094         }
1095
1096         push @commands, sub { chdir( $qt_gitmodule_build_dir ) };
1097
1098         push @commands, sub { $self->exe(
1099             $qmake_bin,
1100             $qt_gitmodule_dir,
1101             @qmake_args
1102         ) };
1103
1104         push @commands, sub { $self->exe( $make_bin, @make_args ) };
1105     }
1106
1107     foreach my $command (@commands) {
1108         $command->();
1109     }
1110
1111     return;
1112 }
1113
1114 sub run_install
1115 {
1116     my ($self) = @_;
1117
1118     my $doing = $self->doing( 'installing Qt' );
1119
1120     my $make_bin        = $self->{ 'make.bin' };
1121     my $qt_dir          = $self->{ 'qt.dir' };
1122     my $qt_build_dir    = $self->{ 'qt.build.dir' };
1123     my $qt_gitmodule    = $self->{ 'qt.gitmodule' };
1124     my $qt_gitmodule_dir= $self->{ 'qt.gitmodule.dir' };
1125     my $qt_make_install = $self->{ 'qt.make_install' };
1126
1127     return if (!$qt_make_install);
1128
1129     chdir( $qt_build_dir );
1130
1131     if (($self->{'qt.gitmodule'} eq 'qt5') or ($self->{'qt.gitmodule'} eq 'qt')) {
1132         # Testing all of qt5 or qt4? Just do a top-level `make install'
1133         $self->exe( $make_bin, 'install' );
1134     }
1135     elsif ($self->{ module_in_qt5 }) {
1136         # Testing some module hosted in qt5.git? Top-level `make module-FOO-install_subtargets'
1137         # to install this module and all its dependencies.
1138         $self->exe( $make_bin, "module-$qt_gitmodule-install_subtargets" );
1139     }
1140     else {
1141         # Testing some module hosted outside of qt5.git?
1142         # Then we need to explicitly install all deps first ...
1143         my %dependencies = $self->read_dependencies( "$qt_gitmodule_dir/sync.profile" );
1144         my @module_targets = map { "module-$_-install_subtargets" } keys %dependencies;
1145         $self->exe( $make_bin, @module_targets );
1146
1147         # ... and then install the module itself
1148         chdir( $qt_gitmodule );
1149         $self->exe( $make_bin, 'install' );
1150     }
1151
1152     # Note that we are installed, since this changes some behavior elsewhere
1153     $self->{ installed } = 1;
1154
1155     return;
1156 }
1157
1158 sub run_install_check
1159 {
1160     my ($self) = @_;
1161
1162     my $doing = $self->doing( 'checking the installation' );
1163
1164     my $qt_install_dir  = $self->{ 'qt.install.dir' };
1165     my $qt_make_install = $self->{ 'qt.make_install' };
1166
1167     return if (!$qt_make_install);
1168
1169     # check whether dirs 'bin' and 'include' actually exists under qt.install.dir
1170     my @required_files = map { "$qt_install_dir/$_" } qw(bin include);
1171     my @missing_files = grep { ! -e $_ } @required_files;
1172     if (@missing_files) {
1173         $self->fail(
1174             'The make install command exited successfully, but the following expected file(s) '
1175            .'are missing from the install tree:'.join("\n ", q{}, @missing_files)."\n"
1176         );
1177     }
1178
1179     return;
1180 }
1181
1182 sub run_make_html_docs
1183 {
1184     my ($self) = @_;
1185
1186     my $doing = $self->doing( 'making Qt docs' );
1187
1188     my $make_bin        = $self->{ 'make.bin' };
1189     my $qt_build_dir    = $self->{ 'qt.build.dir' };
1190     my $qt_make_html_docs = $self->{ 'qt.make_html_docs' };
1191     my $doc_insignificant = $self->{ 'qt.make_html_docs.insignificant' };
1192
1193     return if (!$qt_make_html_docs);
1194
1195     my $run = sub {
1196       chdir( $qt_build_dir );
1197       $self->exe( $make_bin, 'html_docs' );
1198     };
1199
1200     if ($doc_insignificant) {
1201         eval { $run->() };
1202         if ($EVAL_ERROR) {
1203             warn "$EVAL_ERROR\n"
1204                 .qq{This is a warning, not an error, because the 'make html_docs' is permitted to fail};
1205         } else {
1206             print "Note: 'make html_docs' succeeded. "
1207                  ."This may indicate it is safe to enforce the 'make html_docs'.\n";
1208         }
1209     }
1210     else {
1211         $run->();
1212     }
1213
1214     return;
1215
1216 }
1217
1218
1219 # Returns a testrunner command
1220 sub get_testrunner_command
1221 {
1222     my ($self) = @_;
1223
1224     my $testrunner = catfile( $FindBin::Bin, '..', 'generic', 'testrunner.pl' );
1225     $testrunner    = canonpath abs_path( $testrunner );
1226
1227     # sanity check
1228     $self->fatal_error( "internal error: $testrunner does not exist" ) if (! -e $testrunner);
1229
1230     my @testrunner_with_args = (
1231         $EXECUTABLE_NAME,               # perl
1232         $testrunner,                    # testrunner.pl
1233         $self->get_testrunner_args( ),
1234     );
1235
1236     return join(' ', @testrunner_with_args);
1237 }
1238
1239 # Returns appropriate testrunner arguments (not including trailing --)
1240 sub get_testrunner_args
1241 {
1242     my ($self) = @_;
1243
1244     my $qt_tests_timeout         = $self->{ 'qt.tests.timeout' };
1245     my $qt_tests_capture_logs    = $self->{ 'qt.tests.capture_logs' };
1246     my $qt_coverage_tool         = $self->{ 'qt.coverage.tool' };
1247     my $qt_coverage_tests_output = $self->{ 'qt.coverage.tests_output' };
1248     my $qt_gitmodule             = $self->{ 'qt.gitmodule' };
1249     my $qt_gitmodule_dir         = $self->{ 'qt.gitmodule.dir' };
1250     my $qt_tests_tee_logs        = $self->{ 'qt.tests.tee_logs' };
1251     my $qt_artifacts_dir         = $self->{ 'qt.artifacts.dir' };
1252     my $qt_tests_backtraces      = $self->{ 'qt.tests.backtraces' };
1253     my $qt_tests_flaky_mode      = $self->{ 'qt.tests.flaky_mode' };
1254     my $qt_tests_flaky_enabled   = $self->{ 'qt.tests.flaky.enabled' };
1255     my $qt_tests_testscheduler   = $self->{ 'qt.tests.testscheduler' };
1256
1257     my @testrunner_args = (
1258         '--timeout',
1259         $qt_tests_timeout,  # kill any test which takes longer than this ...
1260     );
1261
1262     # capture or tee logs to a given directory
1263     if ($qt_tests_capture_logs) {
1264         push @testrunner_args, '--capture-logs', canonpath $qt_tests_capture_logs;
1265     }
1266     elsif ($qt_tests_tee_logs) {
1267         #push @testrunner_args, '--tee-logs', canonpath $qt_tests_tee_logs;
1268         push @testrunner_args, '--tee-logs', canonpath($qt_artifacts_dir."/".$qt_tests_tee_logs);
1269     }
1270
1271     if ($qt_tests_backtraces) {
1272         if ($OSNAME =~ m{linux}i) {
1273             push @testrunner_args, '--plugin', 'core';
1274         }
1275         elsif ($OSNAME =~ m{darwin}i) {
1276             push @testrunner_args, '--plugin', 'crashreporter';
1277         }
1278     }
1279
1280     # enable flaky test plugin
1281     if ($qt_tests_flaky_enabled) {
1282         # give more info about unstable / flaky tests
1283         push @testrunner_args, '--plugin', 'flaky';
1284         if ($qt_tests_flaky_mode) {
1285             push @testrunner_args, '--flaky-mode', $qt_tests_flaky_mode;
1286         }
1287     }
1288
1289     if ($qt_coverage_tool) {
1290         push @testrunner_args, '--plugin', $qt_coverage_tool;
1291         push @testrunner_args, "--${qt_coverage_tool}-qt-gitmodule-dir", canonpath $qt_gitmodule_dir;
1292         push @testrunner_args, "--${qt_coverage_tool}-qt-gitmodule", $qt_gitmodule;
1293     }
1294
1295     if ($qt_coverage_tests_output) {
1296         push @testrunner_args, "--${qt_coverage_tool}-tests-output", $qt_coverage_tests_output;
1297     }
1298
1299     # If using testscheduler, there is no predictable beginning/end line for each test
1300     # (e.g. from `make check') unless we request --verbose mode, so do that
1301     if ($qt_tests_testscheduler) {
1302         push @testrunner_args, '--verbose';
1303     }
1304
1305     # We cannot handle passing arguments with spaces into `make TESTRUNNER...',
1306     # so detect and abort right now if that's the case.
1307     #
1308     # Handling this properly by quoting the arguments is really quite difficult
1309     # (it depends on exactly which shell is going to be invoked by make, which may
1310     # be affected by the value of the PATH environment variable when make is run, etc...),
1311     # so we will not do it unless it becomes necessary.
1312     #
1313     if (any { /\s/ } @testrunner_args) {
1314         $self->fatal_error(
1315             "Some arguments to testrunner contain spaces, which is currently not supported.\n"
1316            ."Try removing spaces from build / log paths, if there are any.\n"
1317            .'testrunner arguments: '.Dumper(\@testrunner_args)."\n"
1318         );
1319     }
1320
1321     return @testrunner_args;
1322 }
1323
1324 sub run_autotests
1325 {
1326     my ($self) = @_;
1327
1328     return if (!$self->{ 'qt.tests.enabled' });
1329
1330     my $doing = $self->doing( 'running the autotests' );
1331
1332     my $qt_gitmodule_build_dir = $self->{ 'qt.gitmodule.build.dir' };
1333     my $qt_tests_dir = $self->{ 'qt.tests.dir' };
1334
1335     # Add this module's `bin' directory to PATH.
1336     # FIXME: verify if this is really needed (should each module's tools build directly
1337     # into the prefix `bin' ?)
1338     local %ENV = %ENV;
1339     Env::Path->PATH->Prepend( canonpath catfile( $qt_gitmodule_build_dir, 'bin' ) );
1340
1341     # In qt4, we need to set QTDIR to run some autotests like 'tst_bic',
1342     # 'tst_symbols', etc
1343     if ($self->{ 'qt.gitmodule' } eq 'qt') {
1344         $ENV{ QTDIR } = $qt_gitmodule_build_dir; ## no critic
1345     }
1346
1347     # In qt5, all tests are expected to be correctly set up in top-level .pro files, so they
1348     # do not need an explicit added compile step.
1349     # In qt4, this is not the case, so they need to be compiled separately.
1350     # By using qt.tests.dir one can change the test dir to be tests/auto in qt5 also.
1351     return $self->_run_autotests_impl(
1352         tests_dir            =>  ($self->{ 'qt.gitmodule' } ne 'qt')
1353                                  ? catfile( $qt_gitmodule_build_dir, $qt_tests_dir)  # qt5
1354                                  : catfile( $qt_gitmodule_build_dir, 'tests/auto' ), # qt4
1355         insignificant_option =>  'qt.tests.insignificant',
1356         do_compile           =>  ($self->{ 'qt.gitmodule' } ne 'qt')
1357                                  ? 0                                                 # qt5
1358                                  : 1,                                                # qt4
1359     );
1360 }
1361
1362 # Compile and run some qtqa shared autotests.
1363 #
1364 # The $type parameter decides which tests are run;
1365 # essentially, `qmake && make && make check' are run under
1366 # the qtqa/tests/$type directory.
1367 #
1368 # This function may be called multiple times for different types of tests.
1369 #
1370 sub run_qtqa_autotests
1371 {
1372     my ($self, $type) = @_;
1373
1374     return if (!$self->{ 'qt.qtqa-tests.enabled' });
1375
1376     my $qt_gitmodule           = $self->{ 'qt.gitmodule' };
1377     my $qt_gitmodule_dir       = $self->{ 'qt.gitmodule.dir' };
1378     my $qt_gitmodule_build_dir = $self->{ 'qt.gitmodule.build.dir' };
1379
1380     my $doing = $self->doing( "running the qtqa tests on $qt_gitmodule" );
1381
1382     # path to the qtqa shared autotests.
1383     my $qtqa_tests_dir = catfile( $FindBin::Bin, qw(.. .. tests), $type );
1384
1385     # director(ies) of modules we want to test
1386     my @module_dirs;
1387
1388     # module itself is always tested
1389     push @module_dirs, $qt_gitmodule_dir;
1390
1391     # if there are submodules, all of those are also tested
1392     {
1393         local $CWD = $qt_gitmodule_dir;
1394
1395         my ($testable_modules) = trim $self->exe_qx(
1396             'git',
1397             'submodule',
1398             '--quiet',
1399             'foreach',
1400             'echo $path',
1401         );
1402         my @testable_modules = split(/\n/, $testable_modules);
1403
1404         push @module_dirs, map { catfile( $qt_gitmodule_dir, $_ ) } @testable_modules;
1405     }
1406
1407     # message is superfluous if only one tested module
1408     if (@module_dirs > 1) {
1409         print __PACKAGE__ . ": qtqa $type autotests will be run over modules: @module_dirs\n";
1410     }
1411
1412     my $compiled_qtqa_tests = 0;    # whether or not the tests have been compiled
1413
1414     foreach my $module_dir (@module_dirs) {
1415         print __PACKAGE__ . ": now running qtqa $type autotests over $module_dir\n";
1416
1417         # qtqa autotests use this environment variable to locate the sources of the
1418         # module under test.
1419         local $ENV{ QT_MODULE_TO_TEST } = $module_dir;
1420
1421         $self->_run_autotests_impl(
1422             tests_dir            =>  $qtqa_tests_dir,
1423             insignificant_option =>  'qt.qtqa-tests.insignificant',
1424
1425             # Only need to `qmake', `make' the tests the first time.
1426             do_compile           =>  !$compiled_qtqa_tests,
1427
1428             # Testscheduler summary is not useful for qtqa tests
1429             testscheduler_args   =>  [ '--no-summary' ],
1430         );
1431
1432         $compiled_qtqa_tests = 1;
1433     }
1434
1435     return;
1436 }
1437
1438 sub _run_autotests_impl
1439 {
1440     my ($self, %args) = @_;
1441
1442     # global settings
1443     my $qt_build_dir   = $self->{ 'qt.build.dir' };
1444     my $qt_install_dir = $self->{ 'qt.install.dir' };
1445     my $qt_make_install = $self->{ 'qt.make_install' };
1446     my $make_bin       = $self->{ 'make.bin' };
1447     my $make_args      = $self->{ 'make.args' };
1448     my $make_check_bin = $self->{ 'make-check.bin' };
1449     my $make_check_args = $self->{ 'make-check.args' };
1450     my $qt_tests_args  = $self->{ 'qt.tests.args' };
1451     my $qt_tests_testscheduler = $self->{ 'qt.tests.testscheduler' };
1452     my $qt_tests_testscheduler_args = $self->{ 'qt.tests.testscheduler.args' };
1453
1454     # settings for this autotest run
1455     my $tests_dir            = $args{ tests_dir };
1456     my $insignificant_option = $args{ insignificant_option };
1457     my $do_compile           = $args{ do_compile };
1458     my $insignificant        = $self->{ $insignificant_option };
1459
1460     # Add tools from all the modules to PATH.
1461     # If shadow-build with install enabled, then we need to add install path
1462     # rather than build path into the PATH.
1463     local $ENV{ PATH } = $ENV{ PATH };
1464     local $ENV{ QMAKEPATH } = $ENV{ QMAKEPATH };
1465     if ($self->{ installed }) {
1466         # shadow build and installing? need to add install dir into PATH
1467         Env::Path->PATH->Prepend( canonpath catfile( $qt_install_dir, 'bin' ) );
1468     }
1469     elsif ($self->{ 'qt.gitmodule' } eq 'qt') {
1470         # qt4 case. this is needed to use the right qmake to compile the tests
1471         Env::Path->PATH->Prepend( canonpath catfile( $qt_build_dir, 'bin' ) );
1472     }
1473     else {
1474         Env::Path->PATH->Prepend( canonpath catfile( $qt_build_dir, 'qtbase', 'bin' ) );
1475
1476         # If we are expected to install, but we're not installed yet, then
1477         # make sure qmake can find its mkspecs.
1478         if ($qt_make_install) {
1479             Env::Path->QMAKEPATH->Prepend( canonpath catfile( $qt_build_dir, 'qtbase' ) );
1480         }
1481     }
1482
1483     my $run = sub {
1484         chdir( $tests_dir );
1485
1486         my @make_args = split(/ /, $make_args);
1487
1488         if ($do_compile) {
1489             $self->exe( 'qmake' );
1490             $self->exe( $make_bin, '-k', @make_args );
1491         }
1492
1493         if ($qt_tests_testscheduler) {
1494             my @testrunner_args = $self->get_testrunner_args( );
1495
1496             $self->exe(
1497                 $EXECUTABLE_NAME,
1498                 catfile( "$FindBin::Bin/../generic/testplanner.pl" ),
1499                 '--input',
1500                 '.',
1501                 '--output',
1502                 'testplan.txt',
1503                 '--make',
1504                 $make_check_bin,
1505                 '--',
1506                 split(/ /, $qt_tests_args),
1507             );
1508
1509             $self->exe(
1510                 $EXECUTABLE_NAME,
1511                 catfile( "$FindBin::Bin/../generic/testscheduler.pl" ),
1512                 '--plan',
1513                 'testplan.txt',
1514                 @{ $args{ testscheduler_args } || []},
1515                 split( m{ }, $qt_tests_testscheduler_args ),
1516                 @testrunner_args,
1517             );
1518
1519         } else {
1520             my $testrunner_command = $self->get_testrunner_command( );
1521
1522             my @make_check_args = split(/ /, $make_check_args);
1523
1524             $self->exe( $make_check_bin,
1525                 @make_check_args,                   # include args requested by user
1526                 "TESTRUNNER=$testrunner_command --",# use our testrunner script
1527                 "TESTARGS=$qt_tests_args",          # and our test args (may be empty)
1528                 'check',                            # run the autotests :)
1529             );
1530         }
1531     };
1532
1533     if ($insignificant) {
1534         eval { $run->() };
1535         if ($EVAL_ERROR) {
1536             warn "$EVAL_ERROR\n"
1537                 .qq{This is a warning, not an error, because the `$insignificant_option' option }
1538                 . q{was used.  This means the tests are currently permitted to fail};
1539         } else {
1540             print "Note: $insignificant_option is set, but the tests succeeded. "
1541                  ."This may indicate it is safe to remove $insignificant_option.\n";
1542         }
1543     }
1544     else {
1545         $run->();
1546     }
1547
1548     return;
1549 }
1550
1551 # There is a slow cycle CI, which runs test in other device than
1552 # desktop, like Android tablet. Some of those have their own tricks
1553 # to run the test.
1554 sub run_embedded_autotest
1555 {
1556     my ($self) = @_;
1557
1558     my $doing = $self->doing( 'running tests in embedded' );
1559
1560     my $qt_gitmodule             = $self->{ 'qt.tests.enabled'       };
1561     my $qt_gitmodule_dir         = $self->{ 'qt.gitmodule.dir'   };
1562     my $qt_testscript                = $self->{ 'qt.slow.testrunner' };
1563     my $qt_testargs              = $self->{ 'qt.slow.testrunner.args' };
1564     my $qt_build_dir             = $self->{ 'qt.build.dir'       };
1565     my $insignificant            = $self->{ 'qt.tests.insignificant' };
1566     
1567     my $testrunner = canonpath($qt_build_dir."/".$qt_testscript);
1568     my @test_args = split(/ /, $qt_testargs);
1569     my $qt_tests_tee_logs        = $self->{ 'qt.tests.tee_logs' };
1570     my $qt_artifacts_dir         = $self->{ 'qt.artifacts.dir' };
1571
1572     # sanity check
1573     $self->fatal_error( "internal error: $testrunner does not exist" ) if (! -e $testrunner);
1574     print("running: " .$testrunner . " , with args" . $qt_testargs."\n");
1575
1576     my $run = sub {
1577             $self->exe(
1578             $EXECUTABLE_NAME,       # perl
1579             $testrunner,        #/full/path/to/<runner>.pl
1580             @test_args,
1581             );
1582     };
1583
1584     if ($insignificant) {
1585         eval { $run->() };
1586         if ($EVAL_ERROR) {
1587             warn "$EVAL_ERROR\n"
1588                 .qq{This is a warning, not an error, because the `qt.tests.insignificant' option }
1589                 . q{was used.  This means the tests are currently permitted to fail};
1590         } else {
1591             print "Note: qt.tests.insignificant is set, but the tests succeeded. "
1592                  ."This may indicate it is safe to remove qt.tests.insignificant.\n";
1593         }
1594     }
1595     else {
1596         $run->();
1597     }
1598     
1599     return;
1600 }
1601
1602
1603 sub run_coverage
1604 {
1605     my ($self) = @_;
1606
1607     return if ((!$self->{ 'qt.tests.enabled' }) or (!$self->{ 'qt.coverage.tool' }));
1608
1609     my $doing = $self->doing( 'gathering coverage data' );
1610
1611     my $qt_coverage_tool         = $self->{ 'qt.coverage.tool' };
1612     my $qt_coverage_tests_output = $self->{ 'qt.coverage.tests_output' };
1613     my $qt_gitmodule             = $self->{ 'qt.gitmodule' };
1614     my $qt_gitmodule_dir         = $self->{ 'qt.gitmodule.dir' };
1615
1616     my $coveragerunner = catfile( $FindBin::Bin, '..', 'generic', "coveragerunner_$qt_coverage_tool.pl" );
1617     $coveragerunner    = canonpath abs_path( $coveragerunner );
1618
1619     # sanity check
1620     $self->fatal_error( "internal error: $coveragerunner does not exist" ) if (! -e $coveragerunner);
1621
1622     my @coverage_runner_args = (
1623         '--qt-gitmodule-dir',
1624         $qt_gitmodule_dir,
1625         '--qt-gitmodule',
1626         $qt_gitmodule,
1627         '--qtcoverage-tests-output',
1628         $qt_coverage_tests_output
1629     );
1630
1631     $self->exe(
1632         $EXECUTABLE_NAME,       # perl
1633         $coveragerunner,        # coveragerunner_<foo>.pl
1634         @coverage_runner_args
1635     );
1636
1637     return;
1638 }
1639
1640 sub main
1641 {
1642     my $test = QtQA::ModuleTest->new(@ARGV);
1643     $test->run;
1644
1645     return;
1646 }
1647
1648 QtQA::ModuleTest->main unless caller;
1649 1;
1650
1651 __END__
1652
1653 =head1 NAME
1654
1655 qtmod_test.pl - test a specific Qt module
1656
1657 =head1 SYNOPSIS
1658
1659   ./qtmod_test.pl [options]
1660
1661 Test the Qt module checked out into base.dir.
1662
1663 =cut