Revision control
++ #!/usr/bin/env perl
++ use 5.010;
++ use strict;
++ use warnings;
++ use utf8;
++
++ =head1 NAME
++
++ 15-testrunner-capture-logs.t - tests for testrunner.pl's log capturing
++
++ =head1 SYNOPSIS
++
++ perl ./15-testrunner-capture-logs.t [--debug]
++
++ This test will run the testrunner.pl script with various types of logging
++ from subprocesses, verifying that the logs are captured correctly.
++
++ If --debug is given, temporary files are left behind for inspection after
++ the test completes.
++
++ =cut
++
++ use Capture::Tiny qw( capture );
++ use Carp;
++ use Encode qw( encode_utf8 );
++ use English qw( -no_match_vars );
++ use File::Path qw( rmtree );
++ use File::Slurp qw( read_file );
++ use File::Spec::Functions;
++ use File::Temp qw( tempdir );
++ use FindBin;
++ use Getopt::Long;
++ use IO::Handle;
++ use Readonly;
++ use Test::More;
++ use Time::HiRes qw( sleep );
++
++ use lib "$FindBin::Bin/../../lib/perl5";
++ use QtQA::Test::More qw( is_or_like );
++
++ Readonly my $WINDOWS => ($OSNAME =~ m{mswin32}i);
++
++ # Directory containing some helper scripts
++ Readonly my $HELPER_DIR => catfile( $FindBin::Bin, 'helpers' );
++
++ # Stream markers for use in TEST_OUTPUT
++ Readonly my $STREAM_OUTPUT => 1; # well-behaved output, to stdout or to -o testlog
++ Readonly my $STREAM_ERROR => 2; # standard error
++ Readonly my $STREAM_OUTPUT_STDOUT => 3; # badly behaved output, goes to stdout even in -o mode
++
++ # Various test texts we can output
++ Readonly my %TEST_OUTPUT => (
++ mixed => [
++ $STREAM_OUTPUT => "1:testlog\n2:testlog\n",
++ $STREAM_ERROR => "1:stderr\n2:stderr\n",
++ $STREAM_OUTPUT_STDOUT => "1:stdout\n2:stdout\n",
++ $STREAM_OUTPUT_STDOUT => "3:stdout\n4:stdout\n",
++ $STREAM_ERROR => "3:stderr\n4:stderr\n",
++ $STREAM_OUTPUT => "3:testlog\n4:testlog\n",
++ ],
++ mixed_nonascii => [
++ $STREAM_OUTPUT => encode_utf8( "1:testlog:早上好\n2:testlog:你好马?\n" ),
++ $STREAM_ERROR => encode_utf8( "1:stderr:早上好\n2:stderr:你好马?\n" ),
++ $STREAM_OUTPUT_STDOUT => encode_utf8( "1:stdout:早上好\n2:stdout:你好马?\n" ),
++ $STREAM_OUTPUT_STDOUT => encode_utf8( "3:stdout:早上好\n4:stdout:你好马?\n" ),
++ $STREAM_ERROR => encode_utf8( "3:stderr:早上好\n4:stderr:你好马?\n" ),
++ $STREAM_OUTPUT => encode_utf8( "3:testlog:早上好\n4:testlog:你好马?\n" ),
++ ],
++ );
++
++ # Command used to output the above and handle `-o somefile' in testlib-compatible way
++ Readonly my @TEST_COMMAND => (
++ 'perl', # force `perl', not `$EXECUTABLE_NAME', as test log naming depends on it
++ "-I$FindBin::Bin/../../lib/perl5",
++ '-e',
++ "do q{$0} || die; run_from_subprocess( )", # call run_from_subprocess in this file
++ '--', # tell perl interpreter not to handle any more args
++ );
++
++ # Testrunner script
++ Readonly my @TESTRUNNER => (
++ $EXECUTABLE_NAME,
++ "$FindBin::Bin/../testrunner.pl",
++ );
++
++ # Whether or not we appear to have superuser permissions (used to skip one test)
++ Readonly my $IS_SUPERUSER
++ => ($EFFECTIVE_USER_ID == 0) # unix: are we root?
++ || $WINDOWS # windows: no sane way to tell for now, so be safe
++ # and always act like we're an admin
++ || ($OSNAME =~ m{darwin}i) # mac: users other than root may have elevated
++ # permissions, so play it safe
++ ;
++
++ # Pattern matching --verbose 'begin' line, without trailing \n.
++ Readonly my $TESTRUNNER_VERBOSE_BEGIN
++ => qr{\QQtQA::App::TestRunner: begin \E.*?:\Q [perl]\E[^\n]*};
++
++ # Pattern matching --verbose 'end' line, without trailing \n.
++ # Ends with [^\n]*, so it can match or not match the exit status portion,
++ # as appropriate.
++ Readonly my $TESTRUNNER_VERBOSE_END
++ => qr{\QQtQA::App::TestRunner: end \E[^:]+\Q: \E[^\n]*};
++
++ # Returns expected error text when a nonexistent $cmd is run
++ sub error_nonexistent_command
++ {
++ my ($cmd) = @_;
++
++ # Note subtle difference here: on Unix, it is QtQA::App::TestRunner who
++ # determines that the command doesn't exist. On Windows, there is always
++ # an intermediate cmd.exe (due to the way system() works on Windows), and
++ # that process is the one who determines that the command doesn't exist.
++ if ($WINDOWS) {
++ return "'$cmd' is not recognized as an internal or external command,\n"
++ ."operable program or batch file.\n";
++ }
++
++ return "QtQA::App::TestRunner: $cmd: No such file or directory\n";
++ }
++
++ # Do a test run for a single dataset.
++ #
++ # Parameters:
++ # a single hashref, with keys (all optional):
++ # command => command to run (defaults to @TEST_COMMAND)
++ # command_args => arrayref of extra args to pass to command (defaults to nothing)
++ # testname => test name passed into Test::More functions
++ # testrunner_args => arrayref of extra args to pass to testrunner
++ # expected_success => 0 if testrunner is expected to fail (non-zero exit code)
++ # expected_stdout => expected standard output of subprocess (text or regex)
++ # expected_stderr => expected standard error of subprocess (text or regex)
++ # expected_logfile => expected log file which should be created by subprocess (if any)
++ # expected_logtext => expected content of the above logfile (text or regex)
++ #
++ sub run_one_test
++ {
++ my ($arg_ref) = @_;
++
++ my @command = @{$arg_ref->{ command } // \@TEST_COMMAND};
++ my @command_args = @{$arg_ref->{ command_args } // []};
++ my @testrunner_args = @{$arg_ref->{ testrunner_args } // ['--']};
++ my $expected_success = $arg_ref->{ expected_success } // 1;
++ my $expected_stdout = $arg_ref->{ expected_stdout } // "";
++ my $expected_stderr = $arg_ref->{ expected_stderr } // "";
++ my $expected_logfile = $arg_ref->{ expected_logfile };
++ my $expected_logtext = $arg_ref->{ expected_logtext } // "";
++ my $testname = $arg_ref->{ testname };
++
++ my @expected_logfiles
++ = $arg_ref->{ expected_logfiles } ? @{ $arg_ref->{ expected_logfiles } }
++ : $expected_logfile ? ( $expected_logfile )
++ : ();
++
++ my @expected_logtexts
++ = $arg_ref->{ expected_logtexts } ? @{ $arg_ref->{ expected_logtexts } }
++ : $expected_logtext ? ( $expected_logtext )
++ : ();
++
++ if (scalar(@expected_logfiles) != scalar(@expected_logtexts)) {
++ die 'test error: expected_logfiles and expected_logtexts count do not match!';
++ }
++
++ foreach my $logfile (@expected_logfiles) {
++ # Ensure the log file doesn't exist prior to the test (should "never" happen)
++ ok( ! -e $logfile, "$testname $logfile doesn't exist prior to test" );
++ }
++
++ my $status;
++ my ($stdout, $stderr) = capture {
++ $status = system( @TESTRUNNER, @testrunner_args, @command, @command_args );
++ };
++
++ if ($expected_success) {
++ is( $status, 0, "$testname exited with zero exit code" );
++ }
++ else {
++ isnt( $status, 0, "$testname exited with non-zero exit code" );
++ }
++
++ is_or_like( $stdout, $expected_stdout, "$testname stdout is as expected" );
++ is_or_like( $stderr, $expected_stderr, "$testname stderr is as expected" );
++
++ # The rest of the verification steps are only applicable if a log file is expected and created
++ return if (!@expected_logfiles);
++
++ my $i = 0;
++ foreach my $logfile (@expected_logfiles) {
++ return if (!ok( -e $logfile, "$testname created $logfile" ));
++
++ my $logtext = read_file( $logfile ); # dies on error
++ is_or_like( $logtext, $expected_logtexts[$i], "$testname $logfile logtext is as expected" );
++ ++$i;
++ }
++
++ return;
++ }
++
++ #============================== subprocess parts ==================================================
++ # These are never called in the main test process, they are called by `perl -e' subprocesses
++
++ # Open and return a writable FH to the specified $filename (which may be - for STDOUT)
++ sub open_log_fh
++ {
++ my ($filename) = @_;
++
++ if ($filename eq '-') {
++ return IO::Handle->new_from_fd(fileno(STDOUT), 'w') || confess "internal error: open STDOUT for write: $!";
++ }
++
++ open( my $out, '>', $filename ) || confess "internal error: open $filename for write: $!";
++ return $out;
++ }
++
++ # Iterate through a set of testdata and print each chunk to the correct stream.
++ #
++ # Parameters:
++ # a hashref with the following keys:
++ # content => test data array (a value from %TEST_OUTPUT)
++ # filename => the file to be used as a log file (or - for STDOUT)
++ #
++ sub print_testdata
++ {
++ my ($arg_ref) = @_;
++
++ my $content = $arg_ref->{ content };
++ my $filename = $arg_ref->{ filename };
++
++ my $log_fh = open_log_fh( $filename );
++
++ # $content is an arrayref of (stream, text) pairs, which determine what
++ # text is printed and to which stream(s). For example:
++ #
++ # [
++ # $STREAM_STDOUT => 'This line should be printed to STDOUT',
++ # $STREAM_STDERR => 'This line should be printed to STDERR',
++ # ]
++ #
++
++ my @text_chunks = @{$content};
++ while (@text_chunks) {
++ my $stream = shift @text_chunks;
++ my $text = shift @text_chunks;
++
++ confess 'internal error: odd number of elements in testdata' if (! defined $text);
++
++ # These prints are all flushed to ensure that line ordering is correct in the case
++ # where $log_fh is another handle to STDOUT or STDERR.
++ if ($stream == $STREAM_OUTPUT) {
++ $log_fh->printflush( $text );
++ }
++ elsif ($stream == $STREAM_ERROR) {
++ STDERR->printflush( $text );
++ }
++ elsif ($stream == $STREAM_OUTPUT_STDOUT) {
++ STDOUT->printflush( $text );
++ }
++ else {
++ confess "internal error: bad test stream $stream in testdata";
++ }
++
++ # This is effectively a yield to the parent process, to give it a chance to read
++ # our output one line at a time. If we don't do this, we may easily fill up both
++ # the buffers in the parent connected to our stdout/stderr, and information about
++ # the order is lost.
++ sleep 0.05;
++ }
++
++ close( $log_fh ) || confess "internal error: close $filename after write: $!";
++
++ return;
++ }
++
++ # Primary entry point for test subprocess.
++ sub run_from_subprocess
++ {
++ my $exitcode = 0;
++ my @output_filenames;
++ my $output_content;
++ my $skip_log;
++
++ GetOptions(
++ 'o=s' => \@output_filenames,
++ 'maxwarnings=s' => sub {}, # don't care, just emulating testlib
++ 'exitcode=i' => \$exitcode,
++ 'skip-log' => \$skip_log,
++ ) || die;
++
++ # If skiplog is set, we ignore the -o option, to simulate a badly behaved test
++ # which has been passed -o but ignored it (e.g. because QTest::qExec was used
++ # incorrectly).
++ if ($skip_log) {
++ @output_filenames = ('-');
++ }
++
++ if (!@output_filenames) {
++ @output_filenames = ('-');
++ }
++
++ $output_content = shift @ARGV;
++
++ confess 'internal error: not enough arguments'
++ if (!$output_content);
++
++ confess "internal error: `$output_content' is not valid testdata"
++ if (!$TEST_OUTPUT{$output_content});
++
++ foreach my $filename_and_format (@output_filenames) {
++ my $filename;
++ my $format;
++
++ if ($filename_and_format =~ m{,}) {
++ ($filename, $format) = split(/,/, $filename_and_format);
++ }
++ else {
++ $filename = $filename_and_format;
++ }
++
++ my $content = $TEST_OUTPUT{$output_content};
++
++ # When printing more than one log, prefix with the format,
++ # to verify that we're really getting the right log in the right place.
++ if ($format) {
++ $content = [
++ $STREAM_OUTPUT => "format:$format\n",
++ @{ $content },
++ ];
++ }
++
++ print_testdata({
++ content => $content,
++ filename => $filename,
++ });
++ }
++
++ exit $exitcode;
++ }
++
++ #==================================================================================================
++
++ # Calculate a set of regular expressions to match sequences of lines.
++ #
++ # Input: a list of arrayrefs, each containing a sequence of lines (strings or regular expressions,
++ # _without_ trailing \n)
++ #
++ # Returns: an arrayref containing a set of regular expressions. Some text satisfies _all_ the
++ # line sequences only if _all_ the regular expressions match.
++ #
++ # The primary use case for this function is to match the output of commands/logs where
++ # both STDOUT and STDERR should be separately checked. In this case, there are multiple output
++ # streams, and the order of output from each stream can be guaranteed but the order of output
++ # _between_ streams cannot.
++ #
++ # Example:
++ #
++ # my $rx = rx_for_lines(
++ # [
++ # "stdout line one",
++ # "stdout line two",
++ # ],
++ # [
++ # "stderr line one",
++ # "stderr line two",
++ # ],
++ # );
++ # is_or_like( $some_text, $rx );
++ #
++ # In this example, is_or_like will pass only if the following is true:
++ #
++ # - $some_text contains exactly 4 lines of output
++ # - $some_text contains both "stdout line one\n" and "stdout line two\n", in that order
++ # - $some_text contains both "stderr line one\n" and "stderr line two\n", in that order
++ #
++ # The comparison will succeed regardless of the order of stdout/stderr interleaving
++ # (as long as line-based flushing was used on stdout/stderr).
++ #
++ sub rx_for_lines
++ {
++ my (@line_refs) = @_;
++
++ my @out;
++
++ my $line_count = 0;
++
++ foreach my $lines (@line_refs) {
++ my @rx_lines = map {
++ if (ref($_) eq 'Regexp') {
++ "$_";
++ } else {
++ quotemeta($_).'\n'
++ }
++ } @{$lines};
++ $line_count += scalar( @rx_lines );
++
++ my $rx;
++
++ # First line
++ $rx .= q{
++ (?: # First line may appear in two cases:
++ \A # (1) very first line in the text
++ |
++ \n # (2) not the first line in the text
++ )
++ };
++
++ # Rest of lines
++ $rx .= join( q{
++ .*? # 0 or more of any character between lines
++ (?<=\n) # lines must follow a \n (but that must not be consumed, because
++ # it may be the \n at the end of the previously matched line)
++ }, @rx_lines );
++
++ push @out, qr{$rx}xms;
++ }
++
++ # The above regular expressions ensure we get all lines in the correct order.
++ # Finally, add an rx to make sure we got exactly the correct amount of lines.
++ push @out, qr/
++ \A # beginning ...
++ (?: [^\n]* \n ){$line_count} # exactly $line_count newline characters
++ [^\n]* # maybe some other stuff between last \n and end of text
++ \z # end
++ /xms;
++
++ return \@out;
++ }
++
++ # Primary entry point for the test.
++ sub run
++ {
++ my $debug;
++
++ GetOptions(
++ 'debug' => \$debug,
++ ) || die;
++
++ my $tempdir = tempdir( 'qtqa-test-capture-logs.XXXXXX', TMPDIR => 1, CLEANUP => !$debug );
++ if ($debug) {
++ diag( "Using $tempdir as temporary directory" );
++ }
++
++
++ ###############################################################################################
++ # Basic controls to ensure output is correct when there is no capturing
++ run_one_test({
++ testname => 'mixed no capturing failing',
++ command_args => [ '--exitcode', 2, 'mixed' ],
++ expected_stdout =>
++ "1:testlog\n2:testlog\n"
++ ."1:stdout\n2:stdout\n"
++ ."3:stdout\n4:stdout\n"
++ ."3:testlog\n4:testlog\n"
++ ,
++ expected_stderr =>
++ "1:stderr\n2:stderr\n"
++ ."3:stderr\n4:stderr\n"
++ ,
++ expected_success => 0,
++ });
++ run_one_test({
++ testname => 'mixed_nonascii no capturing',
++ command_args => [ 'mixed_nonascii' ],
++ expected_stdout => encode_utf8(
++ "1:testlog:早上好\n2:testlog:你好马?\n"
++ ."1:stdout:早上好\n2:stdout:你好马?\n"
++ ."3:stdout:早上好\n4:stdout:你好马?\n"
++ ."3:testlog:早上好\n4:testlog:你好马?\n"
++ ),
++ expected_stderr => encode_utf8(
++ "1:stderr:早上好\n2:stderr:你好马?\n"
++ ."3:stderr:早上好\n4:stderr:你好马?\n"
++ ),
++ });
++
++
++ ###############################################################################################
++ # Test simple capture error cases without -o
++ SKIP: {
++ skip( 'unsafe to run this test as superuser', 1 ) if ($IS_SUPERUSER);
++
++ # We've assumed that /some_notexist_dir:
++ # - does not exist, and
++ # - cannot be created by testrunner (because we're not superuser)
++ #
++ run_one_test({
++ testname => 'mixed, capture error',
++ testrunner_args => [ '--capture-logs', '/some_notexist_dir', '--'],
++ command_args => [ 'mixed' ],
++ expected_stderr => qr{\Amkdir /some_notexist_dir: Permission denied}ms,
++ expected_success => 0,
++ });
++ }
++
++
++ ###############################################################################################
++ # Test regular capture without -o;
++ # since -o is not used, it should capture everything
++ rmtree( $tempdir ); # testrunner should create the logdir if necessary
++ run_one_test({
++ testname => 'mixed_nonascii with capture',
++ testrunner_args => [ '--capture-logs', $tempdir, '--'],
++ command_args => [ '--exitcode', 3, 'mixed_nonascii' ],
++ expected_success => 0,
++ expected_logfile => "$tempdir/perl-00.txt",
++ expected_logtext => rx_for_lines(
++ [map { encode_utf8($_) } qw(
++ 1:testlog:早上好
++ 2:testlog:你好马?
++ 1:stdout:早上好
++ 2:stdout:你好马?
++ 3:stdout:早上好
++ 4:stdout:你好马?
++ 3:testlog:早上好
++ 4:testlog:你好马?
++ )],
++ [map { encode_utf8($_) } qw(
++ 1:stderr:早上好
++ 2:stderr:你好马?
++ 3:stderr:早上好
++ 4:stderr:你好马?
++ )],
++ ),
++ });
++ run_one_test({
++ testname => 'mixed_nonascii with tee',
++ testrunner_args => [ '--tee-logs', $tempdir, '--'],
++ command_args => [ 'mixed_nonascii' ],
++ expected_logfile => "$tempdir/perl-01.txt", # we didn't clean up, so a new filename is used
++ # tee, so stdout and stderr are both captured, and printed:
++ expected_stdout => encode_utf8(
++ "1:testlog:早上好\n2:testlog:你好马?\n"
++ ."1:stdout:早上好\n2:stdout:你好马?\n"
++ ."3:stdout:早上好\n4:stdout:你好马?\n"
++ ."3:testlog:早上好\n4:testlog:你好马?\n"
++ ),
++ expected_stderr => encode_utf8(
++ "1:stderr:早上好\n2:stderr:你好马?\n"
++ ."3:stderr:早上好\n4:stderr:你好马?\n"
++ ),
++ expected_logtext => rx_for_lines(
++ [map { encode_utf8($_) } qw(
++ 1:testlog:早上好
++ 2:testlog:你好马?
++ 1:stdout:早上好
++ 2:stdout:你好马?
++ 3:stdout:早上好
++ 4:stdout:你好马?
++ 3:testlog:早上好
++ 4:testlog:你好马?
++ )],
++ [map { encode_utf8($_) } qw(
++ 1:stderr:早上好
++ 2:stderr:你好马?
++ 3:stderr:早上好
++ 4:stderr:你好马?
++ )],
++ ),
++ });
++ run_one_test({
++ testname => 'mixed with capture',
++ testrunner_args => [ '--sync-output', '--verbose', '--capture-logs', $tempdir, '--'],
++ command_args => [ 'mixed' ],
++ expected_logfile => "$tempdir/perl-02.txt",
++ expected_logtext => rx_for_lines(
++ [
++ $TESTRUNNER_VERBOSE_BEGIN,
++ qw(
++ 1:testlog
++ 2:testlog
++ 1:stdout
++ 2:stdout
++ 3:stdout
++ 4:stdout
++ 3:testlog
++ 4:testlog
++ ),
++ qr{$TESTRUNNER_VERBOSE_END, exit code 0},
++ ],
++ [qw(
++ 1:stderr
++ 2:stderr
++ 3:stderr
++ 4:stderr
++ )],
++ ),
++ });
++
++
++
++ ###############################################################################################
++ # Test capture with the usage of -o;
++ # in this case, the test writes some of its stuff correctly to the log,
++ # but the stuff which instead goes directly to stdout/stderr will be appended to the end
++ # of the log by the testrunner.
++ rmtree( $tempdir );
++ run_one_test({
++ testname => 'mixed_nonascii with capture and verbose and -o',
++ testrunner_args => [ '--capture-logs', $tempdir, '--verbose', '--' ],
++ command_args => [ '--exitcode', 12, '-o', 'testlog.log', 'mixed_nonascii' ],
++ expected_success => 0,
++ # note the naming convention with -o is to reuse the basename and extension
++ expected_logfile => "$tempdir/perl-testlog-00.log",
++ expected_logtext => rx_for_lines(
++ [map { encode_utf8($_) } qw(
++ 1:testlog:早上好
++ 2:testlog:你好马?
++ 3:testlog:早上好
++ 4:testlog:你好马?
++ ),
++ q{},
++ qq{QtQA::App::TestRunner: test output additional content directly to stdout/stderr:},
++ ],
++ [map { encode_utf8($_) } qw(
++ 1:stderr:早上好
++ 2:stderr:你好马?
++ 3:stderr:早上好
++ 4:stderr:你好马?
++ )],
++ [map { encode_utf8($_) } qw(
++ 1:stdout:早上好
++ 2:stdout:你好马?
++ 3:stdout:早上好
++ 4:stdout:你好马?
++ )],
++ ),
++ # Known limitation: with --capture-logs and -o, these messages can't go to
++ # the captured log, because they may be printed before the test has created
++ # the log file.
++ expected_stderr => qr{
++ \A
++ $TESTRUNNER_VERBOSE_BEGIN \n
++ $TESTRUNNER_VERBOSE_END \Q, exit code 12\E\n
++ \z
++ }xms,
++ });
++ run_one_test({
++ testname => 'mixed with capture and -o',
++ testrunner_args => [ '--capture-logs', $tempdir, '--' ],
++ command_args => [ '-o', 'testlog.log', 'mixed' ],
++ expected_logfile => "$tempdir/perl-testlog-01.log",
++ expected_logtext => rx_for_lines(
++ [qw(
++ 1:testlog
++ 2:testlog
++ 3:testlog
++ 4:testlog
++ ),
++ q{},
++ qq{QtQA::App::TestRunner: test output additional content directly to stdout/stderr:},
++ ],
++ [qw(
++ 1:stderr
++ 2:stderr
++ 3:stderr
++ 4:stderr
++ )],
++ [qw(
++ 1:stdout
++ 2:stdout
++ 3:stdout
++ 4:stdout
++ )],
++ ),
++ });
++ run_one_test({
++ testname => 'mixed with capture and -o and tee',
++ testrunner_args => [ '--sync-output', '--tee-logs', $tempdir, '--' ],
++ command_args => [ '-o', 'testlog.log', 'mixed' ],
++ expected_logfile => "$tempdir/perl-testlog-02.log",
++ expected_logtext => rx_for_lines(
++ [qw(
++ 1:testlog
++ 2:testlog
++ 3:testlog
++ 4:testlog
++ ),
++ q{},
++ qq{QtQA::App::TestRunner: test output additional content directly to stdout/stderr:},
++ ],
++ [qw(
++ 1:stderr
++ 2:stderr
++ 3:stderr
++ 4:stderr
++ )],
++ [qw(
++ 1:stdout
++ 2:stdout
++ 3:stdout
++ 4:stdout
++ )],
++ ),
++ # because we used --sync-output, stdout and stderr are merged
++ expected_stdout =>
++ "1:stderr\n2:stderr\n"
++ ."1:stdout\n2:stdout\n"
++ ."3:stdout\n4:stdout\n"
++ ."3:stderr\n4:stderr\n"
++ ,
++ });
++
++
++
++ ###############################################################################################
++ # Capture with the new testlib simultaneous loggers.
++ # If we capture with:
++ #
++ # -o file1,fmt1 -o file2,fmt2 -o -,fmt3
++ #
++ # ... then the testrunner should capture file1 and file2, but should pass through the third
++ # stream (fmt3) to stdout as normal. stderr is also passed through uncaptured.
++ #
++ run_one_test({
++ testname => 'mixed_nonascii new style -o, capture, multiple logs',
++ testrunner_args => [ '--capture-logs', $tempdir, '--' ],
++ command_args => [ '-o', 'testlog.xml,xml', '-o', 'testlog.xunitxml,xunitxml', '-o', '-,txt', 'mixed_nonascii' ],
++ expected_logfiles => [ "$tempdir/perl-testlog-00.xml", "$tempdir/perl-testlog-00.xunitxml" ],
++ expected_logtexts => [
++
++ # first log: xml format
++ encode_utf8(
++ "format:xml\n"
++ ."1:testlog:早上好\n"
++ ."2:testlog:你好马?\n"
++ ."3:testlog:早上好\n"
++ ."4:testlog:你好马?\n"
++ ),
++
++ # second log: xunitxml format
++ encode_utf8(
++ "format:xunitxml\n"
++ ."1:testlog:早上好\n"
++ ."2:testlog:你好马?\n"
++ ."3:testlog:早上好\n"
++ ."4:testlog:你好马?\n"
++ ),
++ ],
++ expected_stdout => encode_utf8(
++ # we get the first portion of stdout also from the first two loggers
++ "1:stdout:早上好\n2:stdout:你好马?\n"
++ ."3:stdout:早上好\n4:stdout:你好马?\n"
++ ."1:stdout:早上好\n2:stdout:你好马?\n"
++ ."3:stdout:早上好\n4:stdout:你好马?\n"
++ ."format:txt\n"
++ ."1:testlog:早上好\n2:testlog:你好马?\n"
++ ."1:stdout:早上好\n2:stdout:你好马?\n"
++ ."3:stdout:早上好\n4:stdout:你好马?\n"
++ ."3:testlog:早上好\n4:testlog:你好马?\n"
++ ),
++ expected_stderr => encode_utf8(
++ (
++ "1:stderr:早上好\n2:stderr:你好马?\n"
++ ."3:stderr:早上好\n4:stderr:你好马?\n"
++ ) x 3 # three loggers, so printed three times
++ ),
++ });
++
++
++
++ ###############################################################################################
++ # Tee with the new testlib simultaneous loggers.
++ # If we tee with:
++ #
++ # -o file1,fmt1 -o file2,fmt2 -o -,fmt3
++ #
++ # ... the behavior is exactly the same as capture-logs.
++ # It is explicitly documented that tee-logs and capture-logs is the same thing in the
++ # multi-logger case, since testlib already is implementing the tee-like behavior.
++ #
++ run_one_test({
++ testname => 'mixed_nonascii new style -o, tee, multiple logs, one stdout',
++ testrunner_args => [ '--tee-logs', $tempdir, '--' ],
++ command_args => [ '-o', 'testlog.xml,xml', '-o', 'testlog.txt,txt', '-o', '-,txt', 'mixed_nonascii' ],
++ expected_logfiles => [ "$tempdir/perl-testlog-01.xml", "$tempdir/perl-testlog-00.txt" ],
++ expected_logtexts => [
++
++ # first log: xml format, non-log text is not captured
++ encode_utf8(
++ "format:xml\n"
++ ."1:testlog:早上好\n"
++ ."2:testlog:你好马?\n"
++ ."3:testlog:早上好\n"
++ ."4:testlog:你好马?\n"
++ ),
++
++ # second log: text format, non-log text is not captured
++ encode_utf8(
++ "format:txt\n"
++ ."1:testlog:早上好\n"
++ ."2:testlog:你好马?\n"
++ ."3:testlog:早上好\n"
++ ."4:testlog:你好马?\n"
++ ),
++ ],
++ expected_stdout => encode_utf8(
++ # we get the first portion of stdout also from the first two loggers
++ "1:stdout:早上好\n2:stdout:你好马?\n"
++ ."3:stdout:早上好\n4:stdout:你好马?\n"
++ ."1:stdout:早上好\n2:stdout:你好马?\n"
++ ."3:stdout:早上好\n4:stdout:你好马?\n"
++ ."format:txt\n"
++ ."1:testlog:早上好\n2:testlog:你好马?\n"
++ ."1:stdout:早上好\n2:stdout:你好马?\n"
++ ."3:stdout:早上好\n4:stdout:你好马?\n"
++ ."3:testlog:早上好\n4:testlog:你好马?\n"
++ ),
++ expected_stderr => encode_utf8(
++ (
++ "1:stderr:早上好\n2:stderr:你好马?\n"
++ ."3:stderr:早上好\n4:stderr:你好马?\n"
++ ) x 3 # three loggers, so printed three times
++ ),
++ });
++
++ ###############################################################################################
++ # Make sure the options parser doesn't get tricked by bizarre options like
++ # `-maxwarnings -o somefile', where -o is a value and not an option
++ run_one_test({
++ testname => 'mixed_nonascii with capture and tricky arguments',
++ testrunner_args => [ '--capture-logs', $tempdir, '--' ],
++ command_args => [ '--exitcode', 50, '-maxwarnings', '-o', 'mixed_nonascii' ],
++ expected_success => 0,
++ expected_logfile => "$tempdir/perl-00.txt",
++ # despite appearances, there was no -o option, so we expect a "raw" capture of everything
++ expected_logtext => rx_for_lines(
++ [map { encode_utf8($_) } qw(
++ 1:testlog:早上好
++ 2:testlog:你好马?
++ 1:stdout:早上好
++ 2:stdout:你好马?
++ 3:stdout:早上好
++ 4:stdout:你好马?
++ 3:testlog:早上好
++ 4:testlog:你好马?
++ )],
++ [map { encode_utf8($_) } qw(
++ 1:stderr:早上好
++ 2:stderr:你好马?
++ 3:stderr:早上好
++ 4:stderr:你好马?
++ )],
++ ),
++ });
++ run_one_test({
++ testname => 'mixed with capture and tricky arguments',
++ testrunner_args => [ '--capture-logs', $tempdir, '--' ],
++ command_args => [ '-maxwarnings', '-o', 'mixed' ],
++ expected_logfile => "$tempdir/perl-01.txt",
++ expected_logtext => rx_for_lines(
++ [qw(
++ 1:testlog
++ 2:testlog
++ 1:stdout
++ 2:stdout
++ 3:stdout
++ 4:stdout
++ 3:testlog
++ 4:testlog
++ )],
++ [qw(
++ 1:stderr
++ 2:stderr
++ 3:stderr
++ 4:stderr
++ )],
++ ),
++ });
++
++
++
++ ###############################################################################################
++ # test capture when -o is passed to test, but is ignored (badly behaved test)
++ rmtree( $tempdir );
++ run_one_test({
++ testname => 'mixed with capture and ignored -o',
++ testrunner_args => [ '--capture-logs', $tempdir, '--' ],
++ command_args => [ '-o', 'testlog.log', '--skip-log', 'mixed' ],
++ expected_success => 0, # failure should be forced even though exit code of test is 0
++ expected_logfile => "$tempdir/perl-testlog-00.log",
++ expected_logtext => rx_for_lines(
++ [
++ q{Log file created by QtQA::App::TestRunner},
++ q{},
++ q{QtQA::App::TestRunner: FAIL! Test was badly behaved, the `-o' argument was ignored.},
++ q{QtQA::App::TestRunner: stdout/stderr follows:},
++ qw(
++ 1:testlog
++ 2:testlog
++ 1:stdout
++ 2:stdout
++ 3:stdout
++ 4:stdout
++ 3:testlog
++ 4:testlog
++ )
++ ],
++ [qw(
++ 1:stderr
++ 2:stderr
++ 3:stderr
++ 4:stderr
++ )],
++ ),
++ });
++ run_one_test({
++ testname => 'mixed_nonascii with capture and ignored -o',
++ testrunner_args => [ '--capture-logs', $tempdir, '--' ],
++ command_args => [ '--exitcode', 23, '-o', 'testlog.log.txt',
++ '--skip-log', 'mixed_nonascii' ],
++ expected_success => 0,
++ expected_logfile => "$tempdir/perl-testlog.log-00.txt",
++ expected_logtext => rx_for_lines(
++ [
++ q{Log file created by QtQA::App::TestRunner},
++ q{},
++ q{QtQA::App::TestRunner: FAIL! Test was badly behaved, the `-o' argument was ignored.},
++ q{QtQA::App::TestRunner: stdout/stderr follows:},
++ map { encode_utf8($_) } qw(
++ 1:testlog:早上好
++ 2:testlog:你好马?
++ 1:stdout:早上好
++ 2:stdout:你好马?
++ 3:stdout:早上好
++ 4:stdout:你好马?
++ 3:testlog:早上好
++ 4:testlog:你好马?
++ )
++ ],
++ [map { encode_utf8($_) } qw(
++ 1:stderr:早上好
++ 2:stderr:你好马?
++ 3:stderr:早上好
++ 4:stderr:你好马?
++ )],
++ ),
++ });
++ run_one_test({
++ testname => 'mixed_nonascii with capture and ignored -o and tee',
++ testrunner_args => [ '--tee-logs', $tempdir, '--' ],
++ command_args => [ '-o', 'testlog.log.txt', '--skip-log', 'mixed_nonascii' ],
++ expected_logtext => rx_for_lines(
++ [
++ q{Log file created by QtQA::App::TestRunner},
++ q{},
++ q{QtQA::App::TestRunner: FAIL! Test was badly behaved, the `-o' argument was ignored.},
++ q{QtQA::App::TestRunner: stdout/stderr follows:},
++ map { encode_utf8($_) } qw(
++ 1:testlog:早上好
++ 2:testlog:你好马?
++ 1:stdout:早上好
++ 2:stdout:你好马?
++ 3:stdout:早上好
++ 4:stdout:你好马?
++ 3:testlog:早上好
++ 4:testlog:你好马?
++ )
++ ],
++ [map { encode_utf8($_) } qw(
++ 1:stderr:早上好
++ 2:stderr:你好马?
++ 3:stderr:早上好
++ 4:stderr:你好马?
++ )],
++ ),
++ expected_stdout => encode_utf8(
++ "1:testlog:早上好\n2:testlog:你好马?\n"
++ ."1:stdout:早上好\n2:stdout:你好马?\n"
++ ."3:stdout:早上好\n4:stdout:你好马?\n"
++ ."3:testlog:早上好\n4:testlog:你好马?\n"
++ ),
++ expected_stderr => encode_utf8(
++ "1:stderr:早上好\n2:stderr:你好马?\n"
++ ."3:stderr:早上好\n4:stderr:你好马?\n"
++ ),
++ expected_logfile => "$tempdir/perl-testlog.log-01.txt",
++ expected_success => 0,
++ });
++
++
++ ###############################################################################################
++ # test that Proc::Reliable errors are correctly captured/teed
++ rmtree( $tempdir );
++ run_one_test({
++ testname => 'capture error nonexistent process',
++ testrunner_args => [ '--capture-logs', $tempdir, '--'],
++ command => [ 'command_which_does_not_exist' ],
++ expected_logfile => "$tempdir/command_which_does_not_exist-00.txt",
++ expected_logtext => error_nonexistent_command('command_which_does_not_exist'),
++ expected_stderr => "",
++ expected_success => 0,
++ });
++ run_one_test({
++ testname => 'tee error nonexistent process',
++ testrunner_args => [ '--tee-logs', $tempdir, '--'],
++ command => [ 'command_which_does_not_exist' ],
++ expected_logfile => "$tempdir/command_which_does_not_exist-01.txt",
++ expected_logtext => error_nonexistent_command('command_which_does_not_exist'),
++ expected_stderr => error_nonexistent_command('command_which_does_not_exist'),
++ expected_success => 0,
++ });
++
++ my $crash_script = catfile( $HELPER_DIR, 'dereference_bad_pointer.pl' );
++ my $crash_rx = ($WINDOWS)
++ ? "QtQA::App::TestRunner: Process exited with exit code 0xC0000005 (STATUS_ACCESS_VIOLATION)\n"
++ : qr{\AQtQA::App::TestRunner: Process exited due to signal 11(; dumped core)?\n\z}ms;
++ run_one_test({
++ testname => 'capture error crashing',
++ testrunner_args => [ '--capture-logs', $tempdir, '--'],
++ command => [ 'perl', $crash_script ],
++ expected_logfile => "$tempdir/perl-00.txt",
++ expected_logtext => $crash_rx,
++ expected_stderr => "",
++ expected_success => 0,
++ });
++ run_one_test({
++ testname => 'tee error crashing',
++ testrunner_args => [ '--tee-logs', $tempdir, '--'],
++ command => [ 'perl', $crash_script ],
++ expected_logfile => "$tempdir/perl-01.txt",
++ expected_logtext => $crash_rx,
++ expected_stderr => $crash_rx,
++ expected_success => 0,
++ });
++
++
++ done_testing( );
++
++ return;
++ }
++
++ run if (!caller);
++ 1;