wiki.techinc.nl/tests/phpunit/unit/includes/exception/MWExceptionHandlerTest.php
Timo Tijhof 8c906e237f exception: Add the 'from' file/line to the logged exception trace
This should make error logs easier to work with through a couple
of ways:

* The stack trace is now complete, instead of missing the first
  crucial step, which is often the one used for filtering
  purposes and for identifying errors within a given deployed
  version of MediaWiki. (E.g. when filtering out an error that is
  expected to be fixed by the next release and/or when checking
  how prominent an error currently is).

* Logstash reports that report message + trace will not need to be
  edited by hand to include the file+line.

* The workflow for Logstash generally follows one of two patterns.

  The default is to filter by exception.file (including line number),
  which is very sure to catch all possible variants thrown from
  the same code, regardless of any variables in the message, but
  has the downside of not matching week-over-week consistency due to
  file paths (at least for WMF) containing the deployment version.

  The other option is to filter by message, which has the risk of
  possibly excluding too much if there are multiple unrelated ways
  to trigger the issue, but is a sensible second option. This is
  usually done by filtering on normalized_message for non-exception
  errors, but doesn't work well for exceptions because they contain
  the file paths and do so in-between the class and message words,
  and thus are not compatible with Logstash's default substring/term
  match. The alternative of exception.message is then considered but
  is lacking the class/type, which can be fragile.

  With this change applied, no editing is needed, and no multiple
  approaches need to be considered with the same option.
  Either filtering by exception.file as-is, or filtering by
  normalized_message as-is, regardless of whether it is an exception
  error or other message in another channel, will both work.

Bug: T271496
Change-Id: I5908ed53f9b97b3c9cde126aca89ab6fc197c845
2021-01-16 22:15:54 +00:00

177 lines
5.1 KiB
PHP

<?php
/**
* @author Antoine Musso
* @copyright Copyright © 2013, Antoine Musso
* @copyright Copyright © 2013, Wikimedia Foundation Inc.
* @file
*/
class MWExceptionHandlerTest extends \MediaWikiUnitTestCase {
private $oldSettingValue;
protected function setUp() : void {
parent::setUp();
// We need to make sure the traces have function arguments as we're testing
// their handling.
$this->oldSettingValue = ini_set( 'zend.exception_ignore_args', 0 );
}
protected function tearDown() : void {
ini_set( 'zend.exception_ignore_args', $this->oldSettingValue );
parent::tearDown();
}
/**
* Test end-to-end formatting of an exception, such as used by LogstashFormatter.
*
* @covers MWExceptionHandler
* @see MWExceptionHandler::prettyPrintTrace
*/
public function testTraceFormatting() {
try {
$dummy = new TestThrowerDummy();
$startLine = __LINE__ + 1;
$dummy->main();
} catch ( Exception $e ) {
}
$startFile = __FILE__;
$dummyFile = TestThrowerDummy::getFile();
$dummyClass = TestThrowerDummy::class;
$expected = <<<TEXT
from ${dummyFile}(17)
#0 ${dummyFile}(13): ${dummyClass}->getQuux()
#1 ${dummyFile}(9): ${dummyClass}->getBar()
#2 ${dummyFile}(5): ${dummyClass}->doFoo()
#3 ${startFile}($startLine): ${dummyClass}->main()
TEXT;
// Trim up until our call()
$trace = MWExceptionHandler::getRedactedTraceAsString( $e );
$actual = implode( "\n", array_slice( explode( "\n", trim( $trace ) ), 0, 5 ) );
$this->assertEquals( $expected, $actual );
}
/**
* @covers MWExceptionHandler::getRedactedTrace
*/
public function testGetRedactedTrace() {
$refvar = 'value';
try {
$array = [ 'a', 'b' ];
$object = (object)[];
self::helperThrowForArgs( $array, $object, $refvar );
} catch ( Exception $e ) {
}
// Make sure our stack trace contains an array and an object passed to
// some function in the stacktrace. Else, we can not assert the trace
// redaction achieved its job.
$trace = $e->getTrace();
$hasObject = false;
$hasArray = false;
foreach ( $trace as $frame ) {
if ( !isset( $frame['args'] ) ) {
continue;
}
foreach ( $frame['args'] as $arg ) {
$hasObject = $hasObject || is_object( $arg );
$hasArray = $hasArray || is_array( $arg );
}
if ( $hasObject && $hasArray ) {
break;
}
}
$this->assertTrue( $hasObject, "The stacktrace has a frame with an object parameter" );
$this->assertTrue( $hasArray, "The stacktrace has a frame with an array parameter" );
// Now we redact the trace.. and verify there are no longer any arrays or objects
$redacted = MWExceptionHandler::getRedactedTrace( $e );
foreach ( $redacted as $frame ) {
if ( !isset( $frame['args'] ) ) {
continue;
}
foreach ( $frame['args'] as $arg ) {
$this->assertIsNotArray( $arg );
$this->assertIsNotObject( $arg );
}
}
$this->assertEquals( 'value', $refvar, 'Reference variable' );
}
/**
* @dataProvider provideJsonSerializedKeys
* @covers MWExceptionHandler::jsonSerializeException
*
* @param string $expectedKeyType Type expected as returned by gettype()
* @param string $exClass An exception class (ie: Exception, MWException)
* @param string $key Name of the key to validate in the serialized JSON
*/
public function testJsonserializeexceptionKeys( $expectedKeyType, $exClass, $key ) {
// Make sure we log a backtrace:
$GLOBALS['wgLogExceptionBacktrace'] = true;
$json = json_decode(
MWExceptionHandler::jsonSerializeException( new $exClass() )
);
$this->assertObjectHasAttribute( $key, $json );
$this->assertSame( $expectedKeyType, gettype( $json->$key ), "Type of the '$key' key" );
}
/**
* Each case provides: [ type, exception class, key name ]
*/
public static function provideJsonSerializedKeys() {
foreach ( [ Exception::class, MWException::class ] as $exClass ) {
yield [ 'string', $exClass, 'id' ];
yield [ 'string', $exClass, 'file' ];
yield [ 'integer', $exClass, 'line' ];
yield [ 'string', $exClass, 'message' ];
yield [ 'NULL', $exClass, 'url' ];
// Backtrace only enabled with wgLogExceptionBacktrace = true
yield [ 'array', $exClass, 'backtrace' ];
}
}
/**
* Given wgLogExceptionBacktrace is true
* then serialized exception must have a backtrace
*
* @covers MWExceptionHandler::jsonSerializeException
*/
public function testJsonserializeexceptionBacktracingEnabled() {
$GLOBALS['wgLogExceptionBacktrace'] = true;
$json = json_decode(
MWExceptionHandler::jsonSerializeException( new Exception() )
);
$this->assertObjectHasAttribute( 'backtrace', $json );
}
/**
* Given wgLogExceptionBacktrace is false
* then serialized exception must not have a backtrace
*
* @covers MWExceptionHandler::jsonSerializeException
*/
public function testJsonserializeexceptionBacktracingDisabled() {
$GLOBALS['wgLogExceptionBacktrace'] = false;
$json = json_decode(
MWExceptionHandler::jsonSerializeException( new Exception() )
);
$this->assertObjectNotHasAttribute( 'backtrace', $json );
}
/**
* Helper function for testGetRedactedTrace
*
* @throws Exception
*/
protected static function helperThrowForArgs( array $a, object $b, &$c ) {
throw new Exception();
}
}