Revision control

++ #!/usr/bin/env perl
++ # Copyright (C) 2017 The Qt Company Ltd.
++ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
++
++ use 5.010;
++ use strict;
++ use warnings;
++
++ package QtQA::App::TestRunner;
++
++ =head1 NAME
++
++ testrunner - helper script to safely run autotests
++
++ =head1 SYNOPSIS
++
++ # Run one autotest safely...
++ $ path/to/testrunner [options] -- some/tst_test1
++
++ # Run many autotests safely... (from within a Qt project)
++ # A maximum runtime of 2 minutes each...
++ $ make check "TESTRUNNER=path/to/testrunner --timeout 120 --"
++ # Will run:
++ # path/to/testrunner --timeout 120 -- some/tst_test1
++ # path/to/testrunner --timeout 120 -- some/tst_test2
++ # etc...
++
++ # Run many autotests and capture the logs to $HOME/test-logs, while also
++ # displaying the logs as they are written
++ $ make check "TESTRUNNER=path/to/testrunner --tee-logs $HOME/test-logs --timeout 120 --"
++
++ # Will generate:
++ #
++ # $HOME/test-logs/testcase1-00.txt
++ # $HOME/test-logs/testcase2-00.txt
++ #
++
++ This script is a wrapper for running autotests safely and ensuring that
++ uniform results are generated. It is designed to integrate with Qt's
++ `make check' feature.
++
++ Some features of this script depend on QTestLib-style autotests, while others
++ are usable for any kind of autotest.
++
++ =head1 OPTIONS
++
++ =over
++
++ =item B<--help>
++
++ Print this message.
++
++ =item B<-->
++
++ Separates the options to testrunner from the test command and arguments (mandatory).
++
++ =item B<--timeout> <value>
++
++ If the test takes longer than <value> seconds, it will be killed, and
++ the testrunner will exit with a non-zero exit code to indicate failure.
++
++ =item B<-C> <directory>
++
++ =item B<--chdir> <directory>
++
++ Change to the specified directory before running the test.
++
++ =item B<--label> <label>
++
++ Use the given label as the human-readable name of this test in certain
++ output messages. May be omitted.
++
++ =item B<-v>
++
++ =item B<--verbose>
++
++ Print a line with some information before the test begins and after the
++ test completes.
++
++ For example:
++
++ $ testrunner --verbose -- ./tst_mytest -silent
++ QtQA::App::TestRunner: begin mytest: [./tst_mytest] [-silent]
++ Testing tst_MyTest
++ QFATAL : tst_MyTest::buggyTest() Cannot quux the fnord
++ QtQA::App::TestRunner: Process exited due to signal 6; dumped core
++ QtQA::App::TestRunner: end mytest: [./tst_mytest] [-silent], signal 6
++
++ The lines are guaranteed to match C</^QtQA::App::TestRunner: begin (?<label>[^:]+):/>
++ and C</^QtQA::App::TestRunner: end (?<label>[^:]+):/>, where 'label' is the label set
++ by the --label command-line option with any ':' characters replaced with '_', but
++ the format is otherwise undefined.
++
++ This is primarily used to unambiguously identify which output belongs to
++ which test, from a log containing many consecutive test runs.
++
++ =item B<--capture-logs> <directory>
++
++ The output of the test will be stored in a file under the given <directory>.
++ The directory is created if it doesn't yet exist.
++
++ As long as only one test is run at a time, it is guaranteed that a unique name
++ is used for the log file, so that tests will not clobber each other's output.
++ The specific naming strategy is undefined.
++
++ The behavior of this option is affected by the logging options passed to the
++ test:
++
++ =over
++
++ =item *
++
++ In the normal case, the combined stdout/stderr of the test is saved
++ into the log file.
++
++ =item *
++
++ If the test is run with a testlib old-style output file specifier, e.g. C<-o somefile>,
++ then the testrunner modifies the C<-o> value passed to the test so that the
++ test writes to a file under the given <directory>. If the test also writes anything
++ to stdout/stderr, that will be appended to the log file. It is considered an error
++ if the test doesn't generate the log file.
++
++ =item *
++
++ If the test is run with testlib new-style output file specifiers,
++ e.g. C<-o somefile.xml,xml -o -,txt>, then the testrunner modifies the C<-o> values
++ similarly as described above. However, only the non-stdout streams are captured;
++ any logger explicitly set to stdout is passed through untouched.
++ Again, it is considered an error if the test doesn't generate all the expected log
++ files.
++
++ =back
++
++ Note that in the C<-o> cases, output is not guaranteed to be completely silent;
++ messages from the testrunner itself (such as the output generated by C<--verbose>)
++ may be printed to standard error, since it would not be safe to print them to the
++ log file while the test itself is also writing to that file.
++
++ For example, if the following command is run:
++
++ testrunner --capture-logs $HOME/test-logs -- ./tst_qstring -o testlog.xml,xml -o -,txt
++
++ ...then testrunner may run the test as:
++
++ ./tst_qstring -o $HOME/test-logs/qstring-testlog-00.xml,xml -o -,txt
++
++ ...and if $HOME/test-logs/qstring-testlog-00.xml does not exist when the test completes,
++ the test will be considered a failure.
++
++ =item B<--tee-logs> <directory>
++
++ Exactly like C<--capture-logs directory>, except that stdout/stderr from the autotest
++ are also written to stdout/stderr of the testrunner, rather than only being written
++ to the captured log file.
++
++ When using the testlib multiple-file logger, with stdout as one of the logger
++ destinations, --tee-logs and --capture-logs are identical. This is intentional,
++ as testlib is already implementing its own tee-like behavior.
++
++ =item B<--sync-output>
++
++ Buffer and synchronize outputs to avoid interleaved output from multiple tests
++ in parallel.
++
++ When this option is in use, each concurrent testrunner instance will co-operate
++ to ensure that each test's output appears as a contiguous block. The output from
++ each test will be buffered until the test completes, then written to standard output
++ as a single block.
++
++ This provides the benefit of a more readable test log, but has the downside
++ that a test which is currently running provides no progress information.
++
++ As a side effect of this option, standard output and standard error from the test
++ may be merged.
++
++ =item B<--plugin> <plugin>
++
++ Loads the specified testrunner plugin to customize behavior.
++
++ May be specified multiple times to load multiple plugins.
++
++ When multiple plugins are specified, they are activated in the order they are given
++ on the command line. This may have no effect, or it may alter the ordering of some
++ output, or it may have more profound effects, depending on the behavior of the
++ plugins.
++
++ Plugins customize the behavior of the testrunner in various ways. The exact
++ list of plugins is platform-specific and undefined. Some plugins which may
++ be available include:
++
++ =over
++
++ =item core
++
++ Attempt to intercept core dumps from any crashing process, and append a backtrace
++ to the test log. (Linux and Mac only, not recommended on Mac)
++
++ =item crashreporter
++
++ Attempt to find and print crash logs from the CrashReporter service from any
++ crashing process. (Mac only)
++
++ =item flaky
++
++ When a test fails, run it again, to help determine if it is unstable.
++ Accepts the following options:
++
++ =over
++
++ =item B<--flaky-mode worst>
++
++ When a test is flaky, use the worst result (default).
++
++ =item B<--flaky-mode best>
++
++ When a test is flaky, use the best result.
++
++ =item B<--flaky-mode ignore>
++
++ When a test is flaky, ignore the result.
++
++ =back
++
++ See the flaky plugin's perldoc for more discussion.
++
++ =item testcocoon
++
++ After a test is run, it will check for the source coverage database csmes and the execution report csexe.
++ If found the csexe is imported into the test csmes and a global csmes is created or updated.
++ This global coverage database with execution report gathers all tests results. The "local" test csmes
++ and csexe are deleted to save space. Note: It makes sense only if the module is built with coverage enabled.
++
++ =over
++
++ =item B<--testcocoon-tests-output> <fullpath>
++
++ Full path to csmes database (to create) where all the tests coverage results are gathered.
++ The file should not exist to avoid data corruption.
++
++ =item B<--testcocoon-qt-gitmodule-dir> <directory>
++
++ Full path of git dir module. Used to retrieve the plugins csmes.
++
++ =back
++
++ See the testcocoon plugin's perldoc for more discussion.
++
++ =back
++
++ =back
++
++ =head1 CAVEATS
++
++ Note that, if a test is killed (e.g. due to a timeout), no attempt is made
++ to terminate the entire process tree spawned by that test (if any), as this
++ appears to be impractical. In practice, that means any helper programs run
++ by a test may be left running if the test is killed.
++
++ It is possible that the usage of this script may alter the apparent ordering
++ of stdout/stderr lines from the test, though this is expected to be rare
++ enough to be negligible.
++
++ As an implementation detail, this script may retain the entire stdout/stderr
++ of the test in memory until the script exits. This will make it inappropriate
++ for certain uses; for example, if your test is expected to run for one day
++ and print 100MB of text to stdout, testrunner will use (at least) 100MB of
++ memory, which is possibly unacceptable.
++
++ Currently, it is not advisable to combine the --capture-logs / --tee-logs
++ options with tests run in XML mode, as testrunner may append messages to the
++ log which are not valid XML.
++
++ =cut
++
++ use Getopt::Long qw(
++ GetOptionsFromArray
++ :config pass_through
++ );
++
++ use Carp;
++ use Capture::Tiny qw( capture_merged );
++ use Cwd qw( realpath );
++ use English qw( -no_match_vars );
++ use Fcntl qw( :flock );
++ use File::Basename;
++ use File::HomeDir;
++ use File::Path qw( mkpath );
++ use File::Spec::Functions;
++ use File::chdir;
++ use IO::File;
++ use IO::Handle;
++ use List::Util qw( first sum );
++ use Pod::Usage qw( pod2usage );
++ use Readonly;
++ use Timer::Simple;
++ use Win32::Status;
++
++ # We may be run from `scripts' or from `bin' via symlink.
++ # Support both cases for finding our own modules.
++ use FindBin;
++ use lib (
++ first { -d $_ } ("$FindBin::Bin/../lib/perl5", "$FindBin::Bin/../scripts/lib/perl5")
++ );
++
++ BEGIN {
++ # Proc::Reliable is not reliable on Windows
++ if ($OSNAME !~ m{win32}i) {
++ require AutoLoader;
++ require Proc::Reliable;
++ Proc::Reliable->import( );
++ }
++ else {
++ require QtQA::Proc::Reliable::Win32;
++ QtQA::Proc::Reliable::Win32->import( );
++ }
++ }
++
++ #use Smart::Comments; # uncomment for debugging
++
++ # a long time, but not forever
++ Readonly my $LONG_TIME => 60*60*24*7;
++
++ # default values for some command-line options
++ Readonly my %DEFAULTS => (
++ timeout => $LONG_TIME,
++ verbose => 0,
++ );
++
++ # exit code for strange process issues, such as a failure to fork
++ # or failure to waitpid; this is expected to be extremely rare,
++ # so the exit code is unusual
++ Readonly my $EXIT_PROCESS_ERROR => 96;
++
++ # exit code if subprocess dies due to signal; not all that rare
++ # (tests crash or hang frequently), so the exit code is not too unusual
++ Readonly my $EXIT_PROCESS_SIGNALED => 3;
++
++ # exit code if we can't capture logs as requested
++ Readonly my $EXIT_LOGGING_ERROR => 58;
++
++ # initial content of created log files
++ Readonly my $INITIAL_LOG_CONTENT => 'Log file created by '.__PACKAGE__;
++
++ # default fractional value for timeout duration warning
++ Readonly my $TIMEOUT_DURATION_WARNING => .75;
++
++ sub new
++ {
++ my ($class) = @_;
++
++ my $self = bless {}, $class;
++ return $self;
++ }
++
++ sub run
++ {
++ my ($self, @args) = @_;
++
++ %{$self} = ( %DEFAULTS, %{$self} );
++
++ my $tee_logs;
++
++ GetOptionsFromArray( \@args,
++ 'help|?' => sub { pod2usage(1) },
++ 'timeout=i' => \$self->{ timeout },
++ 'capture-logs=s' => \$self->{ capture_logs },
++ 'plugin=s' => \@{$self->{ plugin_names }},
++ 'tee-logs=s' => \$tee_logs,
++ 'sync-output' => \$self->{ sync_output },
++ 'C|chdir=s' => \$CWD,
++ 'v|verbose' => \$self->{ verbose },
++ 'label=s' => \$self->{ label },
++ ) || pod2usage(2);
++
++ # tee-logs implies that we both capture the logs, and print the output like `tee'
++ if ($tee_logs) {
++ $self->{ capture_logs } = $tee_logs;
++ $self->{ tee } = 1;
++ }
++
++ # note: plugins may consume additional arguments
++ $self->plugins_init( \@args );
++
++ # Chomp the remaining --, if any
++ if (@args && $args[0] eq '--') {
++ shift @args;
++ }
++
++ if ($self->{ sync_output }) {
++ $self->do_subprocess_with_sync_output( @args );
++ }
++ else {
++ $self->do_subprocess( @args );
++ }
++
++ $self->exit_appropriately( );
++
++ return;
++ }
++
++ # Returns this test's label (human-readable name)
++ sub label
++ {
++ my ($self) = @_;
++
++ # If not explicitly set from command-line, calculate on first
++ # use as the basename of the test command.
++ if (!$self->{ label }) {
++ my $basename = basename( ($self->command())[0] );
++ $self->{ label } = $basename;
++ }
++
++ return $self->{ label };
++ }
++
++ # When there is an error relating to logging, print a message and exit.
++ #
++ # Parameters:
++ # $message - error message to print (one line)
++ #
++ sub exit_with_logging_error
++ {
++ my ($self, $message) = @_;
++
++ my $directory = $self->{ capture_logs };
++
++ $self->print_info(
++ "cannot capture logs to $directory as requested:\n"
++ ."$message\n"
++ ."please check the test environment.\n"
++ );
++
++ # We're about to do an emergency exit; in sync-output mode, flush
++ # the buffer (even if we don't have a lock on the output lockfile)
++ $self->flush_sync_output_buffer( );
++
++ exit $EXIT_LOGGING_ERROR;
++ }
++
++ # Generate a unique (not yet existing) filename for a test log.
++ # The log filename is based on the command we are about to run.
++ #
++ # capture_logs mode must be enabled.
++ #
++ # Parameters:
++ # A hash ref with the following keys, all optional:
++ # basename => string to append to the file's basename.
++ # suffix => string to append to the end of the filename.
++ #
++ # Example:
++ #
++ # # when running tst_qstring...
++ #
++ # my $filename = $self->generate_unique_logfile_name( );
++ # # filename is now: some/directory/tst_qstring-00.txt
++ #
++ # $filename = $self->generate_unique_logfile_name({basename=>'testlog', suffix=>'.log'});
++ # # filename is now: some/directory/tst_qstring-testlog-00.log
++ #
++ sub generate_unique_logfile_name
++ {
++ my ($self, $args_ref) = @_;
++
++ my $directory = $self->{ capture_logs };
++ return if (!$directory);
++
++ my $lockfile = catfile( $self->datadir(), '.qtqa-testrunner-logfile-lock' );
++ my $lockfh = IO::File->new( $lockfile, '>>' ) || $self->exit_with_logging_error( "open $lockfile: $!" );
++ flock( $lockfh, LOCK_EX ) || die "flock $lockfile: $!";
++
++ # We need to come up with a unique filename.
++ # Our chosen naming pattern is:
++ #
++ # <basename>-<number>.<type>
++ #
++ # e.g., for tst_qstring,
++ #
++ # tst_qstring-00.txt
++ # tst_qstring-01.txt
++ # ...
++ #
++ # We fail if there are already 100 files named like the above.
++ # This would be indicative of some problem with the test setup.
++ #
++ my $suffix = $args_ref->{ suffix } || '.txt';
++ my $basename = basename( ($self->command())[0] );
++
++ # basename is now e.g. tst_qstring
++
++ if (defined $args_ref->{ basename }) {
++ my $original_basename = $args_ref->{ basename };
++ if ($original_basename eq '-') {
++ $original_basename = 'stdout';
++ }
++ $basename .= "-$original_basename";
++ }
++
++ # basename is now e.g. tst_qstring-testlog
++
++ $basename = catfile( $directory, $basename );
++
++ # basename is now e.g. path/to/capturedir/tst_qstring-testlog
++
++ my $i = 0;
++ my $candidate;
++ while ($i <= 99) {
++ $candidate = sprintf( '%s-%02d%s', $basename, $i, $suffix );
++ last if (! -e $candidate);
++ ++$i;
++ }
++
++ if (-e $candidate) {
++ $self->exit_with_logging_error(
++ "$directory seems to be already full of files named like $basename-XX.$suffix"
++ );
++ }
++
++ # Ensure the file exists before we release the lock, so that other testrunner instances
++ # won't clobber it. The file is initialized with some text so we can identify that it
++ # was created by us.
++ my $fh = $self->create_and_open_logfile( $candidate, "$INITIAL_LOG_CONTENT\n" );
++ close( $fh );
++ flock( $lockfh, LOCK_UN ) || die "flock $lockfile (unlock): $!";
++
++ return $candidate;
++ }
++
++ # Returns path to a local data dir suitable for lock files and state files,
++ # creating it if necessary.
++ sub datadir
++ {
++ my ($self) = @_;
++
++ my $path = File::HomeDir->my_data();
++ $self->safe_mkpath( $path ) || $self->exit_with_logging_error( "mkpath $path: $!" );
++
++ return;
++ }
++
++ # Creates a log file at the given $filename, and returns an open file handle.
++ # The file's contents are initialized to $content (if set).
++ # The file is truncated if it already exists.
++ #
++ # The log may later be checked for the existence of $content to decide if the
++ # log file has been correctly overwritten by the test process.
++ sub create_and_open_logfile
++ {
++ my ($self, $filename, $content) = @_;
++
++ my $logdir = dirname( $filename );
++ $self->safe_mkpath( $logdir ) || $self->exit_with_logging_error( "mkpath $logdir: $!" );
++
++ open( my $fh, '>', $filename ) || $self->exit_with_logging_error( "open $filename for write: $!" );
++
++ if (defined $content) {
++ print $fh $content;
++ }
++
++ return $fh;
++ }
++
++ # Returns a hashref for the logfiles which should be used. Generates a new filename if necessary.
++ # The hash keys are filenames and values are testlib-compatible log formats (e.g. `txt' for plain
++ # text).
++ sub logfiles
++ {
++ my ($self) = @_;
++
++ if (!$self->{ logfiles }) {
++ $self->set_logfiles( $self->generate_unique_logfile_name( ) => 'txt' );
++ }
++
++ return $self->{ logfiles };
++ }
++
++ # Sets the logfiles which should be used.
++ sub set_logfiles
++ {
++ my ($self, %logfiles) = @_;
++
++ $self->{ logfiles } = \%logfiles;
++
++ return;
++ }
++
++ # Like File::Path::mkpath, but does not fail if multiple processes mkpath() concurrently.
++ # On success, or if the path already exists, returns 1.
++ # On failure, returns 0.
++ sub safe_mkpath
++ {
++ my ($self, $path) = @_;
++
++ # $path could be created by another process in parallel; this is why we need
++ # a check both before and after the mkpath().
++ if (! -d $path && !mkpath( $path )) {
++ sleep 1;
++ return (-d $path);
++ }
++
++ return 1;
++ }
++
++ # Creates new logfiles, and returns an arrayref of open filehandles to them.
++ # The logfiles will be initialized with $content, if given.
++ sub create_and_open_logfiles
++ {
++ my ($self, $content) = @_;
++
++ my $logfiles = $self->logfiles( );
++ my @out;
++
++ while (my ($logfile, $format) = each %{ $logfiles }) {
++ my $fh = $self->create_and_open_logfile( $logfile, $content );
++ push @out, $fh;
++ }
++
++ return \@out;
++ }
++
++ # Set up logging where the subprocess tried to log to a file via `-o'
++ # and we intercepted it to write to a different file.
++ #
++ # Parameters:
++ # $proc - a Proc::Reliable instance
++ # $args_ref - hashref as returned by parse_and_rewrite_testlib_args_for_logging
++ #
++ sub setup_file_to_file_logging
++ {
++ my ($self, $proc, $args_ref) = @_;
++
++ # As the logfiles, we'll use the rewritten -o options...
++ $self->set_logfiles( %{ $args_ref->{ replaced_output } } );
++
++ # ...but we do _not_ create or open it, as the subprocess is expected to do this.
++ $self->{ subprocess_creates_logfile } = 1;
++
++ # We'd better create the directory if it doesn't exist - testlib won't do this.
++ my $logdir = $self->{ capture_logs };
++ if (!$self->safe_mkpath( $logdir )) {
++ $self->exit_with_logging_error( "mkpath $logdir: $!" );
++ }
++
++ my $print_sub = sub {
++ $self->proc_reliable_print_to_logbuffer(@_);
++ if ($self->{ tee } || $self->{ subprocess_logs_to_stdout }) {
++ $self->proc_reliable_print_to_handle(@_);
++ }
++ };
++
++ $proc->stdout_cb( $print_sub );
++ $proc->stderr_cb( $print_sub );
++
++ return;
++ }
++
++ # Set up logging where the subprocess is logging only by stdout/stderr
++ # and we intercepted them to write to a file.
++ sub setup_stream_to_file_logging
++ {
++ my ($self, $proc) = @_;
++
++ $self->{ logfh } = $self->create_and_open_logfiles( );
++
++ my $print_sub = sub {
++ $self->proc_reliable_print_to_log(@_);
++ if ($self->{ tee } || $self->{ subprocess_logs_to_stdout }) {
++ $self->proc_reliable_print_to_handle(@_);
++ }
++ };
++
++ $proc->stdout_cb( $print_sub );
++ $proc->stderr_cb( $print_sub );
++
++ return;
++ }
++
++ # Attempt to parse the subprocess arguments as testlib-style arguments,
++ # and rewrite them in such a way that logs may be captured.
++ #
++ # May change the executed command.
++ #
++ # Returns a reference to a hash with at least the following keys:
++ #
++ # replaced_output => ref to an array of new filenames passed to -o, or undef if no -o
++ #
++ sub parse_and_rewrite_testlib_args_for_logging
++ {
++ my ($self) = @_;
++
++ my @args = $self->command( );
++ my $command = shift @args;
++
++ my $out = {};
++ my @rewritten_args;
++
++ my $parser = Getopt::Long::Parser->new( );
++ $parser->configure(qw(
++ no_auto_abbrev
++ no_bundling
++ no_getopt_compat
++ no_gnu_compat
++ no_ignore_case
++ pass_through
++ permute
++ prefix_pattern=-
++ ));
++
++ # Accept an argument via <> (i.e. an `unknown' argument), without modifying it in any way.
++ my $sub_accept_unknown_argument = sub {
++ push @rewritten_args, @_;
++ };
++
++ # Accept an argument which takes a value.
++ # This has to prepend `-' back to the option name, as Getopt::Long strips
++ # that before calling us (unlike in the <> case).
++ my $sub_accept_argument_with_value = sub {
++ my ($option, $value) = @_;
++ push @rewritten_args, "-$option", $value;
++ };
++
++ # Accept and rewrite the -o argument
++ my $sub_accept_rewrite_output_argument = sub {
++ my ($option, $value) = @_;
++
++ my ($filename, $format) = split(/,/, $value);
++
++ if ($filename eq '-') {
++ # remember that we explicitly asked to log to stdout, so we know
++ # not to warn about it later, and to pass it through.
++ $self->{ subprocess_logs_to_stdout } = 1;
++
++ # Do not rewrite the `-o -,fmt' args.
++ # We explicitly do not attempt to capture or tee in this case,
++ # to keep things simple.
++ return $sub_accept_argument_with_value->( $option, $value );
++ }
++
++ my ($basename, undef, $suffix) = fileparse( $filename, qr{\.[^.]*} );
++ $filename = $self->generate_unique_logfile_name({
++ basename => $basename,
++ suffix => $suffix,
++ });
++
++ push @rewritten_args, "-$option";
++ push @rewritten_args, ( $format ? "$filename,$format" : $filename );
++ $out->{ replaced_output }{ $filename } = $format || 'txt';
++ };
++
++ # Note that Getopt::Long object-oriented interface oddly has no way to read
++ # from anything other than @ARGV. So, we have a choice to either (1) kludge
++ # @ARGV, or (2) kludge via Getopt::Long::Configure, neither of which is
++ # particularly attractive. But localizing @ARGV seems the least error-prone.
++ local @ARGV = @args;
++
++ while (@ARGV) {
++ $parser->getoptions(
++
++ # -o option which we want to rewrite...
++ 'o=s' => $sub_accept_rewrite_output_argument,
++
++ # Parse all other testlib arguments which take a value.
++ # We must parse these even though we don't use them.
++ # If we didn't, we would be tricked by weird command lines like:
++ #
++ # -maxwarnings -o something
++ #
++ # ... which should be parsed as ("-maxwarnings=-o", "something" )
++ # and not ("-maxwarnings" , "-o=something")
++ #
++ map( { +"$_=s" => $sub_accept_argument_with_value } qw(
++ eventdelay
++ graphicssystem
++ iterations
++ keydelay
++ maxwarnings
++ median
++ minimumvalue
++ mousedelay
++ seed
++ )),
++
++ # keep any other arguments exactly as they are
++ '<>' => $sub_accept_unknown_argument,
++
++ ) || return;
++
++ # If we stopped at a --, then store it and continue; testlib does not stop argument
++ # processing on --, so neither do we.
++ if (@ARGV && $ARGV[0] eq '--') {
++ shift @ARGV;
++ push @rewritten_args, '--';
++ }
++ }
++
++ # Overwrite command iff we found and munged the -o option
++ if ($out->{ replaced_output }) {
++ $self->set_command( $command, @rewritten_args );
++ }
++
++ return $out;
++ }
++
++ # Set up for logging of $proc, which should not yet be started.
++ sub setup_logging
++ {
++ my ($self) = @_;
++
++ return if (!$self->{ capture_logs });
++
++ my $proc = $self->proc( );
++
++ # We need to check if the test is being run with a `-o' option to write to a particular
++ # log file. If it is, then we will rewrite that option and expect the test script to
++ # create the log file. Otherwise, we will create the log file ourself.
++ my $args_ref = $self->parse_and_rewrite_testlib_args_for_logging( );
++
++ if ($args_ref->{ replaced_output }) {
++ $self->setup_file_to_file_logging( $proc, $args_ref );
++ }
++ else {
++ $self->setup_stream_to_file_logging( $proc );
++ }
++
++ return;
++ }
++
++ # Print info about proc's termination (if any)
++ sub proc_print_exit_info
++ {
++ my ($self) = @_;
++
++ my $proc = $self->proc( );
++
++ # Print out any messages from the Proc::Reliable; this will include information
++ # such as "process timed out", etc.
++ my $msg = $proc->msg( );
++ if ($msg) {
++ # Don't mention the `Exceeded retry limit'; we never retry, so it would only be
++ # confusing. Note that this can (and often will) reduce $msg to nothing.
++ $msg =~ s{ ^ Exceeded \s retry \s limit \s* }{}xms;
++ }
++
++ # Print out a warning message if the process took longer than a certain percentage
++ # of the timeout amount.
++ my $test_duration = int( $self->{ timer }[-1]->elapsed( ) );
++ my $warning_threshold = $self->{ timeout } * $TIMEOUT_DURATION_WARNING;
++ if ($test_duration > $warning_threshold && $test_duration < $self->{ timeout }) {
++ $msg .= "warning: test duration ($test_duration seconds) is dangerously close to maximum permitted time ($self->{ timeout } seconds)\n";
++ $msg .= "warning: Either modify the test to reduce its runtime, or use a higher timeout.\n";
++ }
++
++ my $status = $proc->status( );
++ if ($status == -1 && !$msg) {
++ # we should have a msg, but avoid being entirely silent if we don't
++ $msg = "Proc::Reliable failed to run process for unknown reasons\n";
++ }
++
++ # "abnormal exit" (e.g. a crashing process) has very different behavior
++ # on Windows vs other platforms.
++ if ($OSNAME =~ m{mswin32}i) {
++ $msg .= $self->check_abnormal_exit_win32( $status );
++ } else {
++ $msg .= $self->check_abnormal_exit( $status );
++ }
++
++ $self->print_info( $msg );
++ return;
++ }
++
++ # Given an exit $status, returns some string describing the abnormal exit
++ # condition, or an empty string if the process exited normally.
++ sub check_abnormal_exit
++ {
++ my ($self, $status) = @_;
++
++ my $out = q{};
++
++ my $signal = ($status & 127);
++ if ($signal) {
++ my $coredumped = ($status & 128);
++ $out .=
++ "Process exited due to signal $signal"
++ .($coredumped ? '; dumped core' : q{})
++ ."\n"
++ ;
++ }
++
++ # Proc::Reliable gives an exit code of 255 if the binary doesn't exist.
++ # Try to give a helpful hint about this case.
++ # This is racy and not guaranteed to be correct.
++ # Note this logic does not apply to Windows, because the system shell is
++ # always used there, and it already complains when the command is not
++ # found.
++ my $exitcode = ($status >> 8);
++ if ($exitcode == 255) {
++ my $command = ($self->command( ))[0];
++ if (! -e $command) {
++ $out .= "$command: No such file or directory\n";
++ }
++ }
++
++ return $out;
++ }
++
++ # Given an exit $status, returns some string describing the abnormal exit
++ # condition, or an empty string if the process exited normally.
++ # Note that this is heuristic, because Windows does not really have any
++ # concept of exiting normally or abnormally.
++ sub check_abnormal_exit_win32
++ {
++ my ($self, $status) = @_;
++
++ my $out = q{};
++
++ my $exitcode = ($status >> 8);
++
++ # If the exitcode is greater than 16 bits, it is most likely an abnormal
++ # error condition. Print it in hex form to make it more recognizable
++ # and searchable.
++ if ($exitcode & 0xFFFF0000) {
++ $out .= sprintf( "Process exited with exit code 0x%X", $exitcode );
++
++ # Do we also have some text form of this status code?
++ if (my $str = $Win32::Status::INTEGER_TO_SYMBOL{ $exitcode }) {
++ $out .= " ($str)";
++ }
++
++ $out .= "\n";
++ }
++
++ return $out;
++ }
++
++ # Returns 1 iff $filename appears to contain a valid test log.
++ #
++ # "valid" currently means "exists, and was not created by testrunner.pl"
++ #
++ sub looks_like_valid_log
++ {
++ my ($self, $filename) = @_;
++
++ if (! -f $filename) {
++ return 0;
++ }
++
++ open( my $fh, '<', $filename ) ## no critic
++ || $self->exit_with_logging_error( "open $filename for read: $!" );
++
++ my $firstline = <$fh>;
++ if ($firstline && $firstline eq "$INITIAL_LOG_CONTENT\n") {
++ return 0;
++ }
++
++ return 1;
++ }
++
++ # Finish up the logging of $proc, which should have completed by now.
++ sub finalize_logging
++ {
++ my ($self) = @_;
++
++ if (!$self->{ subprocess_creates_logfile }) {
++ # If the logfile was created by us and not the subprocess, or if there was no log
++ # file at all, then there is nothing to finalize.
++ return;
++ }
++
++ # In this case, we expect the subprocess to have created a log file, so we have to
++ # check that (and also append to it if necessary).
++ my $logfiles = $self->logfiles( );
++ my $all_logfiles_valid = 1;
++
++ foreach my $logfile (keys %{ $logfiles }) {
++ if (!$self->looks_like_valid_log( $logfile )) {
++ $all_logfiles_valid = 0;
++ last;
++ }
++ }
++
++ # Expected to create some log files, but didn't do so?
++ if (!$all_logfiles_valid) {
++ $self->{ force_failure_exitcode } ||= $EXIT_LOGGING_ERROR;
++ $self->append_logbuffer_to_logfiles( $self->format_info(
++ "FAIL! Test was badly behaved, the `-o' argument was ignored.\n"
++ ."stdout/stderr follows:\n"
++ ));
++ }
++ # Unexpectedly wrote some stuff to stdout/stderr ?
++ elsif (!$self->{ subprocess_logs_to_stdout }) {
++ $self->append_logbuffer_to_logfiles( $self->format_info(
++ "test output additional content directly to stdout/stderr:\n"
++ ));
++ }
++
++ return;
++ }
++
++ # If we captured any stdout/stderr from the subprocess which hasn't yet been logged,
++ # append it to the logfiles. If $prefix is given, it is appended to the log files
++ # prior to other text.
++ sub append_logbuffer_to_logfiles
++ {
++ my ($self, $prefix) = @_;
++
++ # nothing to do if no output was captured
++ return if (! defined $self->{ logbuffer });
++
++ my $text = $prefix . $self->{ logbuffer };
++
++ my $logfiles = $self->logfiles( );
++
++ while (my ($logfile, $format) = each %{ $logfiles }) {
++ # Do _not_ add the messages for formats other than plaintext.
++ # That would create invalid XML, for example.
++ next unless $format eq 'txt';
++
++ # If the logfile already exists, we'll put a newline to separate our messages
++ # from the existing messages.
++ if (-e $logfile) {
++ $text = "\n".$text;
++ }
++
++ open( my $fh, '>>', $logfile )
++ || $self->exit_with_logging_error( "open $logfile for append: $!" );
++
++ $fh->print( $text );
++
++ close( $fh )
++ || $self->exit_with_logging_error( "close $logfile after append: $!" );
++ }
++
++ return;
++ }
++
++ # Callback for Proc::Reliable which prints to the given handle,
++ # or if --sync-output is enabled, prints to a buffer to be output later.
++ # The first parameter to the callback is the correct IO handle (STDOUT or STDERR)
++ sub proc_reliable_print_to_handle
++ {
++ my ($self, $handle, @to_print) = @_;
++
++ if ($self->{ sync_output }) {
++ # --sync-output mode, buffer for later.
++ # $handle is ignored, all output is merged into one buffer.
++ # This is a documented limitation of --sync-output
++ $self->{ sync_output_buffer } .= join( q{}, @to_print );
++ }
++ else {
++ # flush so we print as much as possible if we're killed without completing
++ $handle->printflush( @to_print );
++ }
++
++ return;
++ }
++
++ # Callback for Proc::Reliable which prints to a unique log file.
++ sub proc_reliable_print_to_log
++ {
++ my ($self, $handle, @to_print) = @_;
++
++ # $handle is ignored, instead we print to the log file.
++ foreach my $fh (@{ $self->{ logfh }}) {
++ $fh->printflush( @to_print );
++ }
++
++ return;
++ }
++
++ # Print a message to the right place, which means:
++ # - the logfiles (if any), or...
++ # - sync_output_buffer (if --sync-output mode), or...
++ # - STDERR (if no log and no --sync-output mode), or...
++ # - both (a logfile) AND (sync_output_buffer OR STDERR) (if tee-logs mode)
++ #
++ sub print_to_log_or_stderr
++ {
++ my ($self, @to_print) = @_;
++
++ # flush so the log contains as much as possible if we're killed without completing
++ foreach my $fh (@{ $self->{ logfh }}) {
++ $fh->printflush( @to_print );
++ }
++ if (!$self->{ logfh } || !@{ $self->{ logfh } } || $self->{ tee }) {
++ if ($self->{ sync_output }) {
++ # --sync-output mode, buffer for later.
++ $self->{ sync_output_buffer } .= join( q{}, @to_print );
++ }
++ else {
++ STDERR->printflush( @to_print );
++ }
++ }
++
++ return;
++ }
++
++ # Callback for Proc::Reliable which stores the text in memory.
++ sub proc_reliable_print_to_logbuffer
++ {
++ my ($self, $handle, @to_print) = @_;
++
++ # $handle is ignored, meaning that we are merging STDOUT and STDERR.
++ foreach my $chunk (@to_print) {
++ $self->{ logbuffer } .= $chunk;
++ }
++
++ return;
++ }
++
++ # Creates and returns a process object.
++ # On all platforms other than Windows, this is a Proc::Reliable object.
++ # On Windows, it is currently a stub missing many features.
++ sub create_proc
++ {
++ my ($self) = @_;
++
++ my $proc;
++ if ($OSNAME =~ m{win32}i) {
++ $proc = $self->create_proc_win32( );
++ # There are no Windows-only options
++ } else {
++ $proc = Proc::Reliable->new( );
++
++ # These options only work for platforms other than Windows
++ $proc->stdin_error_ok( 1 ); # OK if child does not read all stdin
++ $proc->num_tries( 1 ); # don't automatically retry on error
++ $proc->child_exit_time( $LONG_TIME ); # don't consider it an error if the test
++ # doesn't quit soon after closing stdout
++ $proc->time_per_try( $self->{timeout} ); # don't run for longer than this
++ $proc->want_single_list( 0 ); # force stdout/stderr handled separately
++ }
++
++ # These options work for all platforms
++
++ $proc->maxtime( $self->{timeout} );
++
++ # Default callbacks just print everything as we receive it.
++ # The logging setup function is permitted to change these callbacks.
++ $proc->stdout_cb( sub { $self->proc_reliable_print_to_handle(@_) } );
++ $proc->stderr_cb( sub { $self->proc_reliable_print_to_handle(@_) } );
++
++ return $proc;
++ }
++
++ sub create_proc_win32
++ {
++ my ($self) = @_;
++
++ return QtQA::Proc::Reliable::Win32->new( );
++ }
++
++ sub do_subprocess_with_sync_output
++ {
++ my ($self, @args) = @_;
++
++ my $lockfile = catfile( $self->datadir(), '.qtqa-testrunner-lock' );
++ my $fh = IO::File->new( $lockfile, '>>' ) || die "open $lockfile: $!";
++
++ # The output will be buffered while we run the subprocess ...
++ $self->{ sync_output_buffer } = q{};
++ $self->do_subprocess( @args );
++ # ...and we can't output until we can get the lock
++ flock($fh, LOCK_EX) || die "flock $lockfile: $!";
++
++ $self->flush_sync_output_buffer( );
++
++ $fh->close( ) || die "close $lockfile: $!";
++ return;
++ }
++
++ # Empties the sync_output_buffer, printing its content to stdout.
++ sub flush_sync_output_buffer
++ {
++ my ($self) = @_;
++
++ if (my $buffer = $self->{ sync_output_buffer }) {
++ local $OUTPUT_AUTOFLUSH = 1;
++ print $buffer;
++ $self->{ sync_output_buffer } = q{};
++ }
++
++ return;
++ }
++
++ # Run a subprocess for the given @command, and do all appropriate logging.
++ # A Proc::Reliable encapsulating the process is returned, after the process completes.
++ sub do_subprocess
++ {
++ my ($self, @command) = @_;
++
++ @command || die 'not enough arguments';
++
++ $self->set_command( @command );
++
++ my $proc = $self->create_proc();
++
++ $self->set_proc( $proc );
++
++ $self->setup_logging( ); # this is allowed to modify the command
++
++ $self->print_test_begin_info( );
++
++ my $keep_running = 1;
++ my $attempt = 1;
++ my $force_failure_exitcode;
++ my $kill_cmd = "killall -v \"iPhone Simulator\"";
++
++ # creates an array to store the timer details for each attempt
++ $self->{ timer } = [];
++
++ while ($keep_running) {
++
++ # Plugins may ask us to force a failure.
++ # We clear this at each run so that we only consider forced failures
++ # during the _last_ run.
++ $force_failure_exitcode = 0;
++
++ push @{$self->{ timer }}, Timer::Simple->new( );
++
++ my @command = $self->command( );
++
++ # Running test in iOS simulator, with ios-sim
++ if ( $ENV{QT_TEST_USE_IOS_SIM} ) {
++ # Apps are launched as ios-sim launch <tst_app> --args <tst_app args >
++ my $app = shift @command;
++ unshift @command, 'ios-sim', 'launch', $app,'--args';
++ # Looks like ios-sim can't re-launch the simulator properly, so we'll kill it
++ qx{${kill_cmd}};
++ }
++
++ $self->plugins_about_to_run( \@command );
++
++ {
++ # Put the attempt number into the environment for these reasons:
++ #
++ # - makes it easier to write autotests for this feature
++ #
++ # - (hopefully) captures the information in core dumps etc
++ # about how many times the test has been run
++ #
++ local $ENV{ QTQA_APP_TESTRUNNER_ATTEMPT } = $attempt;
++
++ $proc->run( \@command );
++ }
++
++ $self->proc_print_exit_info( );
++
++ my $result = $self->plugins_run_completed( );
++
++ $force_failure_exitcode = $result->{ force_failure_exitcode };
++
++ # We may retry the test any number of times, if some plugin asks us to.
++ $keep_running = $result->{ retry };
++ $self->{ timer }[-1]->stop( );
++ ++$attempt;
++ }
++
++ $self->{ force_failure_exitcode } ||= $force_failure_exitcode;
++
++ $self->print_test_end_info( );
++
++ $self->finalize_logging( );
++
++ return $proc;
++ }
++
++ sub pretty_printed_command
++ {
++ my ($self) = @_;
++
++ my @out = map {
++ my $arg = $_;
++ $arg =~ s{\n}{\\n}g; # make sure we can never break across lines
++ "[$arg]"
++ } $self->command( );
++
++ return @out;
++ }
++
++ # Prints line prior to running a test, depending on verbosity.
++ sub print_test_begin_info
++ {
++ my ($self) = @_;
++
++ return unless ($self->{ verbose });
++
++ my $label = $self->label( );
++ $label =~ s{:}{_}g;
++
++ my @command = $self->pretty_printed_command( );
++
++ $self->print_info( "begin $label @ $CWD: @command\n" );
++
++ return;
++ }
++
++ # Prints line after a test has completed, depending on verbosity.
++ sub print_test_end_info
++ {
++ my ($self) = @_;
++
++ return unless ($self->{ verbose });
++
++ my $label = $self->label( );
++ $label =~ s{:}{_}g;
++
++ my $proc = $self->proc( );
++ my $status = $proc->status( );
++
++ my $seconds = sum( map { $_->elapsed( ) } @{$self->{ timer }});
++
++ # If it was at least two seconds, only report the integer part.
++ # For very fast tests, we report the fractional part as well - mostly
++ # because "0 seconds" looks weird.
++ if ($seconds > 1) {
++ $seconds = int($seconds);
++ }
++
++ my $message = "end $label: $seconds seconds";
++
++ if ($status == -1) {
++ $message .= ", status -1 (unusual error)";
++ } elsif ((my $signal = ($status & 127)) && $OSNAME !~ m{win32}i) {
++ $message .= ", signal $signal";
++ } else {
++ my $exitcode = ($status >> 8);
++ if ($exitcode == 0 && $self->{ force_failure_exitcode }) {
++ $message .= ", exit code $self->{ force_failure_exitcode } (forced)";
++ } else {
++ $message .= ", exit code $exitcode";
++ }
++ }
++
++ $self->print_info( "$message\n" );
++
++ return;
++ }
++
++ # Returns a plain text $msg formatted into something suitable for printing.
++ # The formatted message may contain some kind of additional info to make it clear
++ # in build/test logs that the message comes from testrunner.
++ sub format_info
++ {
++ my ($self, $msg) = @_;
++
++ return "" if (!$msg);
++
++ # Prefix every line with __PACKAGE__ so it is clear where this message comes from
++ my $prefix = __PACKAGE__ . ': ';
++ $msg =~ s{ \n (?! \z ) }{\n$prefix}gxms; # replace all newlines except the trailing one
++ $msg = $prefix.$msg;
++
++ return $msg;
++ }
++
++ # Print a $msg to the log or stderr, after formatting it via format_info.
++ sub print_info
++ {
++ my ($self, $msg) = @_;
++
++ return if (!$msg);
++
++ $self->print_to_log_or_stderr( $self->format_info( $msg ) );
++
++ return;
++ }
++
++ # Exit the process with an appropriate exit code, according to the state of
++ # $proc.
++ # The testrunner usually exits with same exit code as the child, unless some
++ # unusual error has occurred.
++ sub exit_appropriately
++ {
++ my ($self) = @_;
++
++ my $proc = $self->proc( );
++
++ my $status = $proc->status( );
++ if ($status == -1) {
++ exit( $EXIT_PROCESS_ERROR );
++ }
++
++ if ($status & 127) {
++ exit( $EXIT_PROCESS_SIGNALED );
++ }
++
++ my $exitcode = ($status >> 8);
++
++ # Allow a failure exit code to be forced, for e.g. log validation problems, etc.
++ if ($exitcode == 0 && $self->{ force_failure_exitcode }) {
++ exit( $self->{ force_failure_exitcode } );
++ }
++
++ exit( $exitcode );
++ }
++
++ sub proc
++ {
++ my ($self) = @_;
++
++ return $self->{ proc };
++ }
++
++ sub set_proc
++ {
++ my ($self, $proc) = @_;
++
++ $self->{ proc } = $proc;
++
++ return;
++ }
++
++ sub command
++ {
++ my ($self) = @_;
++
++ return @{$self->{ command_and_args }};
++ }
++
++ sub set_command
++ {
++ my ($self, @command) = @_;
++
++ $self->{ command_and_args } = \@command;
++
++ return;
++ }
++
++ #============================== functions relating to plugins =====================================
++
++ # Any .pm file in the testrunner-plugins directory is a plugin.
++ # For example, a file named `core.pm' will be loaded if testrunner is run with `--plugin core'.
++ #
++ # The plugin interface consists of:
++ #
++ # new( testrunner => $self, argv => \@args )
++ # Called as each plugin is created.
++ # `testrunner' is a reference to the testrunner object.
++ # `argv' is a reference to any remaining unprocessed command line arguments.
++ # Plugins may use argv to implement additional options.
++ # Should return a plugin object.
++ #
++ # about_to_run( \@argv )
++ # Called prior to running the process.
++ # `argv' is a reference to the command and arguments about to be run; the plugin
++ # is permitted to rewrite these.
++ # Should return nothing.
++ #
++ # run_completed( )
++ # Called after running the process.
++ # Should return nothing, or a hashref with the following keys:
++ # retry => if 1, testrunner is requested to run this test again
++ # force_failure_exitcode => if non-zero, the exit code of testrunner is forced to this,
++ # even if the test succeeded
++ #
++ # Plugins are permitted to do anything. So, take care :)
++
++ sub plugins_init
++ {
++ my ($self, $argv_ref) = @_;
++
++ my @plugin_names = @{$self->{ plugin_names } // []};
++
++ my $plugin_dir = catfile(
++ dirname( realpath( $0 ) ), # directory containing this script (symlinks resolved)
++ 'testrunner-plugins',
++ );
++
++ ### Requested plugins: @plugin_names
++ ### Looking in : $plugin_dir
++
++ foreach my $plugin_name (@plugin_names) {
++ my $plugin_file = catfile( $plugin_dir, "$plugin_name.pm" );
++ if (! -f $plugin_file) {
++ croak "requested plugin $plugin_name does not exist (looked at $plugin_file)";
++ }
++
++ require $plugin_file;
++
++ my $plugin_class = __PACKAGE__ . "::Plugin::$plugin_name";
++ my $plugin_object = eval {
++ $plugin_class->new( testrunner => $self, argv => $argv_ref )
++ };
++
++ if (! $plugin_object) {
++ croak "internal error: $plugin_file loaded OK, "
++ ."but $plugin_class could not be instantiated: $@";
++ }
++
++ ### Loaded plugin: $plugin_class
++
++ push @{$self->{ plugin_objects }}, $plugin_object;
++ }
++
++ return;
++ }
++
++ sub plugins_about_to_run
++ {
++ my ($self, @args) = @_;
++
++ foreach my $plugin (@{$self->{ plugin_objects }}) {
++ ### about_to_run on plugin: $plugin
++ if ($plugin->can( 'about_to_run' )) {
++ $plugin->about_to_run( @args )
++ }
++ }
++
++ return;
++ }
++
++ sub plugins_run_completed
++ {
++ my ($self, @args) = @_;
++
++ # These are the keys understood in the return value of run_completed
++ my @possible_option_keys = qw(
++ force_failure_exitcode
++ retry
++ );
++
++ # All options are disabled by default
++ my %options = map { $_ => 0 } @possible_option_keys;
++
++ foreach my $plugin (@{$self->{ plugin_objects }}) {
++ my $plugin_options;
++
++ ### run_completed on plugin: $plugin
++ if ($plugin->can( 'run_completed' )) {
++ $plugin_options = $plugin->run_completed( @args );
++ }
++
++ if ($plugin_options && ref($plugin_options) eq 'HASH') {
++ %options = map {
++ # plugins may turn options on, never off
++ $_ => ($options{$_} || $plugin_options->{$_})
++ } @possible_option_keys;
++ }
++ }
++
++ return \%options;
++ }
++
++ #==================================================================================================
++
++ QtQA::App::TestRunner->new( )->run( @ARGV ) if (!caller);
++ 1;
++