Revision control

++ # Copyright (C) 2017 The Qt Company Ltd.
++ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
++
++ package QtQA::App::TestRunner::Plugin::crashreporter;
++ use strict;
++ use warnings;
++
++ use Carp;
++ use English qw( -no_match_vars );
++ use File::Basename;
++ use File::Spec::Functions;
++ use IO::File;
++ use Readonly;
++
++ # uncomment for debugging
++ #use Smart::Comments;
++
++ # 1 if we are on mac
++ Readonly my $MAC => ($OSNAME =~ m{darwin}i);
++
++ # Path to user CrashReporter directory;
++ # may be overridden with an environment variable, for testing
++ Readonly my $CRASHREPORTER_DIR => exists( $ENV{ QTQA_CRASHREPORTER_DIR } )
++ ? $ENV{ QTQA_CRASHREPORTER_DIR }
++ : catfile( $ENV{ HOME }, qw(Library Logs CrashReporter) );
++
++ # CrashReporter may take a few seconds to write out the crash report after a test
++ # crashes. We'll wait up to this amount in seconds.
++ Readonly my $CRASHREPORTER_TIMEOUT => 4;
++
++ # CrashReporter does not generate a crash log for these signals, so don't
++ # waste time looking for them. The set of handled signals doesn't appear to be
++ # documented or configurable, so this is based on experience/testing.
++ Readonly my %CRASHREPORTER_IGNORED_SIGNALS => (map { $_ => 1 } qw(
++ 2
++ 15
++ ));
++
++ sub new
++ {
++ my ($class, %args) = @_;
++
++ if (!$MAC) {
++ croak "crashreporter plugin is specific to mac; not usable on $OSNAME";
++ }
++
++ return bless \%args, $class;
++ }
++
++ sub about_to_run
++ {
++ my ($self) = @_;
++
++ # Save names of all crash reports prior to the run, so we can check
++ # new crash reports only.
++ $self->{ old_crash_reports } = [ glob "$CRASHREPORTER_DIR/*" ];
++
++ return;
++ }
++
++ sub run_completed
++ {
++ my ($self) = @_;
++
++ my $testrunner = $self->{ testrunner };
++ my $proc = $testrunner->proc( );
++ my $status = $proc->status( );
++ my $signal = ($status & 127);
++
++ # If no signal or crashreporter ignores this signal, then nothing to do
++ return if (!$signal || $CRASHREPORTER_IGNORED_SIGNALS{ $signal });
++
++ my $crashreport = $self->_find_crash_report_robustly(
++ # Must not be one of these...
++ exclude => $self->{ old_crash_reports },
++
++ # Parent PID should be us
++ parent_pid => $PID,
++ );
++
++ if (!$crashreport) {
++ $testrunner->print_info(
++ "Sorry, a crash report could not be found in $CRASHREPORTER_DIR.\n"
++ );
++ return;
++ }
++
++ $self->_print_crashreport( $crashreport );
++
++ return;
++ }
++
++ sub _print_crashreport
++ {
++ my ($self, $filename) = @_;
++
++ my $testrunner = $self->{ testrunner };
++
++ my $fh = IO::File->new( $filename, '<' );
++ if (!$fh) {
++ $testrunner->print_info(
++ "open $filename: $!\n"
++ ."The crash report could not be displayed.\n"
++ );
++ return;
++ }
++
++ #
++ # create nice chunk of text like:
++ #
++ # ================== crash report follows: ===============
++ # (the crash report here)
++ # ========================================================
++ #
++ $testrunner->print_info(
++ ('=' x 29). ' crash report follows: ' . ('=' x 28) . "\n"
++ );
++
++ while (my $line = <$fh>) {
++ $testrunner->print_info( $line );
++ }
++
++ if (!$fh->close( )) {
++ $testrunner->print_info(
++ "close $filename: $!\n"
++ ."The crash report may be incomplete.\n"
++ );
++ }
++
++ $testrunner->print_info( ('=' x 80)."\n" );
++
++ return;
++ }
++
++ # Returns crash report filename if possible,
++ # retrying for up to $CRASHREPORTER_TIMEOUT seconds.
++ sub _find_crash_report_robustly
++ {
++ my ($self, %args) = @_;
++
++ my $time_remaining = $CRASHREPORTER_TIMEOUT;
++ my $out;
++
++ while ($time_remaining) {
++ if ($out = $self->_find_crash_report( %args )) {
++ last;
++ }
++ sleep 1;
++ --$time_remaining;
++ }
++
++ return $out;
++ }
++
++ # Returns crash report filename if possible,
++ # attempting only once to find the crash report according
++ # to the given information:
++ #
++ # exclude => arrayref of filenames to exclude from consideration
++ # parent_pid => only consider crash reports whose parent PID matches this
++ #
++ # This parses crash reports.
++ # See Technical Note TN2123 for information on crash report format.
++ #
++ # Surprisingly, there is actually no way to get the PID of the child process
++ # out of Proc::Reliable, so we can't use that for the matching.
++ #
++ sub _find_crash_report
++ {
++ my ($self, %args) = @_;
++
++ my %exclude = map { $_ => 1 } @{$args{ exclude }};
++
++ my @found;
++
++ foreach my $candidate (glob "$CRASHREPORTER_DIR/*") {
++ ### Checking candidate: $candidate
++ if ($exclude{ $candidate }) {
++ ### excluded via %exclude
++ next;
++ }
++
++ if ($self->_looks_like_crash_report( $candidate, %args )) {
++ ### Match!
++ push @found, $candidate;
++ }
++ }
++
++ ### Matches: @found
++ if (@found == 1) {
++ return $found[0];
++ }
++
++ # Too few or too many matches.
++ return;
++ }
++
++ # Returns 1 if the given $filename looks like a crash
++ # report according to the criteria in %args
++ sub _looks_like_crash_report
++ {
++ my ($self, $filename, %args) = @_;
++
++ my $parent_pid = $args{ parent_pid };
++
++ my $fh = IO::File->new( $filename, '<' );
++ if (!$fh) {
++ ### could not be opened: $!
++ return;
++ }
++
++ my $match = 0;
++
++ while (my $line = <$fh>) {
++ chomp $line;
++
++ # Example:
++ # Parent Process: launchd [241]
++ if ($line =~ m{\A Parent \s Process: \s+ .+ \[(\d+)\] \z}xms) {
++ my $ppid = $1;
++ if ($parent_pid != $ppid) {
++ ### Parent PID does not match: $ppid
++ return;
++ }
++ $match = 1;
++ last;
++ }
++ }
++
++ if (!$match) {
++ ### Crash report was missing a Parent Process line?
++ return;
++ }
++
++ return 1;
++ }
++
++ =head1 NAME
++
++ QtQA::App::TestRunner::Plugin::crashreporter - show crash reports for crashing tests (on mac)
++
++ =head1 SYNOPSIS
++
++ # without this plugin:
++ $ testrunner --capture-logs $HOME/test-logs -- tst_crashy
++ # $HOME/test-logs/tst_crashy-00.txt says "process exited with signal 11 ..."
++
++ # with this plugin:
++ $ testrunner --plugin crashreporter --capture-logs $HOME/test-logs -- tst_crashy
++ # $HOME/test-logs/tst_crashy-00.txt says "process exited with signal 11 ..."
++ # and also contains all crash information collected by the OSX CrashReporter service
++
++ =head1 DESCRIPTION
++
++ If a test crashes (exited due to a signal), this plugin will attempt to find and print
++ any crash log generated by the CrashReporter service. This is the same information
++ displayed in native Mac crash dialogs when a GUI application crashes.
++
++ The method for finding the application's crash log is simple:
++ if the crash log was created after the test was begun, and the parent process mentioned
++ in the crash log is this process (the testrunner), it is determined to be the test's
++ crash log.
++
++ =head1 CAVEATS
++
++ Finding the crash log may fail if one of the following occurs:
++
++ =over
++
++ =item *
++
++ the CrashReporter process was very slow, or itself crashed
++
++ =item *
++
++ some other subprocess of testrunner unexpectedly crashed (for example, a utility
++ run by some other testrunner plugin crashed)
++
++ =item *
++
++ the crashing test was not a direct child process of the testrunner (for example,
++ this testrunner was used in combination with another testrunner script)
++
++ =back
++
++ If any of the above situations occur, testrunner will warn about the failure.
++
++ =cut
++
++ 1;