diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml index 79602dc4..6ef0ec61 100644 --- a/.github/workflows/test-package.yml +++ b/.github/workflows/test-package.yml @@ -19,7 +19,6 @@ jobs: guzzle-version: '^7.0' - php-version: 8.0 guzzle-version: '^7.0' - composer-flags: '--ignore-platform-reqs' steps: - uses: actions/checkout@v2 @@ -28,6 +27,8 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} + coverage: none + extensions: intl, mbstring - run: composer validate diff --git a/CHANGELOG.md b/CHANGELOG.md index a04b368d..3b9bb53a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ Changelog ========= +## 3.26.0 (2021-02-10) + +### Enhancements + +* Out of memory errors will now be reported by increasing the memory limit by 5 MiB. Use the new `memoryLimitIncrease` configuration option to change the amount of memory, or set it to `null` to disable the increase entirely. + [#621](https://github.com/bugsnag/bugsnag-php/pull/621) + +* Add a "discard classes" configuration option that allows events to be discarded based on the exception class name or PHP error name + [#622](https://github.com/bugsnag/bugsnag-php/pull/622) + +* Add a "redacted keys" configuration option. This is similar to `filters` but allows both strings and regexes. String matching is exact but case-insensitive. Regex matching allows for partial and wildcard matching. + [#623](https://github.com/bugsnag/bugsnag-php/pull/623) + +### Deprecations + +* The `filters` configuration option is now deprecated as `redactedKeys` can express everything that filters could and more. + ## 3.25.0 (2020-11-25) ### Enhancements diff --git a/src/Client.php b/src/Client.php index 5d7be7de..4a2cb825 100644 --- a/src/Client.php +++ b/src/Client.php @@ -12,6 +12,7 @@ use Bugsnag\Internal\GuzzleCompat; use Bugsnag\Middleware\BreadcrumbData; use Bugsnag\Middleware\CallbackBridge; +use Bugsnag\Middleware\DiscardClasses; use Bugsnag\Middleware\NotificationSkipper; use Bugsnag\Middleware\SessionData; use Bugsnag\Request\BasicResolver; @@ -137,6 +138,7 @@ public function __construct( $this->sessionTracker = new SessionTracker($config, $this->http); $this->registerMiddleware(new NotificationSkipper($config)); + $this->registerMiddleware(new DiscardClasses($config)); $this->registerMiddleware(new BreadcrumbData($this->recorder)); $this->registerMiddleware(new SessionData($this)); @@ -530,6 +532,8 @@ public function shouldNotify() * * Eg. ['password', 'credit_card']. * + * @deprecated Use redactedKeys instead + * * @param string[] $filters an array of metaData filters * * @return $this @@ -544,7 +548,9 @@ public function setFilters(array $filters) /** * Get the array of metaData filters. * - * @var string + * @deprecated Use redactedKeys instead + * + * @var string[] */ public function getFilters() { @@ -935,4 +941,78 @@ public function getSessionClient() { return $this->config->getSessionClient(); } + + /** + * Set the amount to increase the memory_limit when an OOM is triggered. + * + * This is an amount of bytes or 'null' to disable increasing the limit. + * + * @param int|null $value + */ + public function setMemoryLimitIncrease($value) + { + return $this->config->setMemoryLimitIncrease($value); + } + + /** + * Get the amount to increase the memory_limit when an OOM is triggered. + * + * This will return 'null' if this feature is disabled. + * + * @return int|null + */ + public function getMemoryLimitIncrease() + { + return $this->config->getMemoryLimitIncrease(); + } + + /** + * Set the array of classes that should not be sent to Bugsnag. + * + * @param array $discardClasses + * + * @return $this + */ + public function setDiscardClasses(array $discardClasses) + { + $this->config->setDiscardClasses($discardClasses); + + return $this; + } + + /** + * Get the array of classes that should not be sent to Bugsnag. + * + * This can contain both fully qualified class names and regular expressions. + * + * @var array + */ + public function getDiscardClasses() + { + return $this->config->getDiscardClasses(); + } + + /** + * Set the array of metadata keys that should be redacted. + * + * @param string[] $redactedKeys + * + * @return $this + */ + public function setRedactedKeys(array $redactedKeys) + { + $this->config->setRedactedKeys($redactedKeys); + + return $this; + } + + /** + * Get the array of metadata keys that should be redacted. + * + * @var string[] + */ + public function getRedactedKeys() + { + return $this->config->getRedactedKeys(); + } } diff --git a/src/Configuration.php b/src/Configuration.php index d6d900a2..c3856edc 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -43,6 +43,8 @@ class Configuration /** * The strings to filter out from metaData. * + * @deprecated Use redactedKeys instead + * * @var string[] */ protected $filters = [ @@ -82,7 +84,7 @@ class Configuration */ protected $notifier = [ 'name' => 'Bugsnag PHP (Official)', - 'version' => '3.25.0', + 'version' => '3.26.0', 'url' => 'https://bugsnag.com', ]; @@ -152,6 +154,31 @@ class Configuration */ protected $buildEndpoint = self::BUILD_ENDPOINT; + /** + * The amount to increase the memory_limit to handle an OOM. + * + * The default is 5MiB and can be disabled by setting it to 'null' + * + * @var int|null + */ + protected $memoryLimitIncrease = 5242880; + + /** + * An array of classes that should not be sent to Bugsnag. + * + * This can contain both fully qualified class names and regular expressions. + * + * @var array + */ + protected $discardClasses = []; + + /** + * An array of metadata keys that should be redacted. + * + * @var string[] + */ + protected $redactedKeys = []; + /** * Create a new config instance. * @@ -243,6 +270,8 @@ public function shouldNotify() * * Eg. ['password', 'credit_card']. * + * @deprecated Use redactedKeys instead + * * @param string[] $filters an array of metaData filters * * @return $this @@ -257,7 +286,9 @@ public function setFilters(array $filters) /** * Get the array of metaData filters. * - * @var string + * @deprecated Use redactedKeys instead + * + * @var string[] */ public function getFilters() { @@ -766,4 +797,80 @@ public function getSessionClient() return $this->sessionClient; } + + /** + * Set the amount to increase the memory_limit when an OOM is triggered. + * + * This is an amount of bytes or 'null' to disable increasing the limit. + * + * @param int|null $value + */ + public function setMemoryLimitIncrease($value) + { + $this->memoryLimitIncrease = $value; + + return $this; + } + + /** + * Get the amount to increase the memory_limit when an OOM is triggered. + * + * This will return 'null' if this feature is disabled. + * + * @return int|null + */ + public function getMemoryLimitIncrease() + { + return $this->memoryLimitIncrease; + } + + /** + * Set the array of classes that should not be sent to Bugsnag. + * + * @param array $discardClasses + * + * @return $this + */ + public function setDiscardClasses(array $discardClasses) + { + $this->discardClasses = $discardClasses; + + return $this; + } + + /** + * Get the array of classes that should not be sent to Bugsnag. + * + * This can contain both fully qualified class names and regular expressions. + * + * @var array + */ + public function getDiscardClasses() + { + return $this->discardClasses; + } + + /** + * Set the array of metadata keys that should be redacted. + * + * @param string[] $redactedKeys + * + * @return $this + */ + public function setRedactedKeys(array $redactedKeys) + { + $this->redactedKeys = $redactedKeys; + + return $this; + } + + /** + * Get the array of metadata keys that should be redacted. + * + * @var string[] + */ + public function getRedactedKeys() + { + return $this->redactedKeys; + } } diff --git a/src/Handler.php b/src/Handler.php index 8c2dee81..3dcc453f 100644 --- a/src/Handler.php +++ b/src/Handler.php @@ -28,6 +28,25 @@ class Handler */ protected $previousExceptionHandler; + /** + * A bit of reserved memory to ensure we are able to increase the memory + * limit on an OOM. + * + * We can't reserve all of the memory that we need to send OOM reports + * because this would have a big overhead on every request, instead of just + * on shutdown in requests with errors. + * + * @var string|null + */ + private $reservedMemory; + + /** + * A regex that matches PHP OOM errors. + * + * @var string + */ + private $oomRegex = '/^Allowed memory size of (\d+) bytes exhausted \(tried to allocate \d+ bytes\)/'; + /** * Whether the shutdown handler will run. * @@ -136,6 +155,9 @@ public function registerExceptionHandler($callPrevious) */ public function registerShutdownHandler() { + // Reserve some memory that we can free in the shutdown handler + $this->reservedMemory = str_repeat(' ', 1024 * 32); + register_shutdown_function([$this, 'shutdownHandler']); } @@ -267,6 +289,9 @@ public function errorHandler($errno, $errstr, $errfile = '', $errline = 0) */ public function shutdownHandler() { + // Free the reserved memory to give ourselves some room to work + $this->reservedMemory = null; + // If we're disabled, do nothing. This avoids reporting twice if the // exception handler is forcing the native PHP handler to run if (!self::$enableShutdownHandler) { @@ -275,6 +300,17 @@ public function shutdownHandler() $lastError = error_get_last(); + // If this is an OOM and memory increase is enabled, bump the memory + // limit so we can report it + if ($lastError !== null + && $this->client->getMemoryLimitIncrease() !== null + && preg_match($this->oomRegex, $lastError['message'], $matches) === 1 + ) { + $currentMemoryLimit = (int) $matches[1]; + + ini_set('memory_limit', $currentMemoryLimit + $this->client->getMemoryLimitIncrease()); + } + // Check if a fatal error caused this shutdown if (!is_null($lastError) && ErrorTypes::isFatal($lastError['type']) && !$this->client->getConfig()->shouldIgnoreErrorCode($lastError['type'])) { $report = Report::fromPHPError( diff --git a/src/Middleware/DiscardClasses.php b/src/Middleware/DiscardClasses.php new file mode 100644 index 00000000..50e0ab7e --- /dev/null +++ b/src/Middleware/DiscardClasses.php @@ -0,0 +1,50 @@ +config = $config; + } + + /** + * @param \Bugsnag\Report $report + * @param callable $next + * + * @return void + */ + public function __invoke(Report $report, callable $next) + { + $errors = $report->getErrors(); + + foreach ($this->config->getDiscardClasses() as $discardClass) { + foreach ($errors as $error) { + if ($error['errorClass'] === $discardClass + || @preg_match($discardClass, $error['errorClass']) === 1 + ) { + syslog(LOG_INFO, sprintf( + 'Discarding event because error class "%s" matched discardClasses configuration', + $error['errorClass'] + )); + + return; + } + } + } + + $next($report); + } +} diff --git a/src/Report.php b/src/Report.php index ec7d6511..8a1dfd9b 100644 --- a/src/Report.php +++ b/src/Report.php @@ -644,6 +644,39 @@ public function setSessionData(array $session) $this->session = $session; } + /** + * Get a list of all errors in a fixed format of: + * - 'errorClass' + * - 'errorMessage' + * - 'type' (always 'php'). + * + * @return array + */ + public function getErrors() + { + $errors = [$this->toError()]; + $previous = $this->previous; + + while ($previous) { + $errors[] = $previous->toError(); + $previous = $previous->previous; + } + + return $errors; + } + + /** + * @return array + */ + private function toError() + { + return [ + 'errorClass' => $this->name, + 'errorMessage' => $this->message, + 'type' => 'php', + ]; + } + /** * Get the array representation. * @@ -752,11 +785,21 @@ protected function cleanupObj($obj, $isMetaData) */ protected function shouldFilter($key, $isMetaData) { - if ($isMetaData) { - foreach ($this->config->getFilters() as $filter) { - if (stripos($key, $filter) !== false) { - return true; - } + if (!$isMetaData) { + return false; + } + + foreach ($this->config->getFilters() as $filter) { + if (stripos($key, $filter) !== false) { + return true; + } + } + + foreach ($this->config->getRedactedKeys() as $redactedKey) { + if (@preg_match($redactedKey, $key) === 1) { + return true; + } elseif (Utils::stringCaseEquals($redactedKey, $key)) { + return true; } } diff --git a/src/Utils.php b/src/Utils.php index 5529061e..fbac1088 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -2,6 +2,8 @@ namespace Bugsnag; +use Normalizer; + class Utils { /** @@ -40,4 +42,50 @@ public static function getBuilderName() return $builderName; } + + /** + * Check if two strings are equal, ignoring case. + * + * @param string $a + * @param string $b + * + * @return bool + */ + public static function stringCaseEquals($a, $b) + { + // Avoid unicode normalisation and MB comparison if possible + if (strcasecmp($a, $b) === 0) { + return true; + } + + // Normalise code points into their decomposed form. For example "ñ" + // can be a single code point (U+00F1) or "n" (U+006E) with a combining + // tilde (U+0303). The decomposed form will always represent this as + // U+006E and U+0303, which means we'll match strings more accurately + // and makes case-insensitive comparisons easier + if (function_exists('normalizer_is_normalized') + && function_exists('normalizer_normalize') + ) { + $form = Normalizer::NFD; + + if (!normalizer_is_normalized($a, $form)) { + $a = normalizer_normalize($a, $form); + } + + if (!normalizer_is_normalized($b, $form)) { + $b = normalizer_normalize($b, $form); + } + } + + if (function_exists('mb_stripos') && function_exists('mb_strlen')) { + // There's no MB equivalent to strcasecmp, so we have to use + // mb_stripos with a length check instead + return mb_strlen($a) === mb_strlen($b) && mb_stripos($a, $b) === 0; + } + + // If the MB extension isn't available we can still use strcasecmp + // This will still work for multi-byte strings in some cases because + // the strings were normalised + return strcasecmp($a, $b) === 0; + } } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 0df26ae7..441e59ad 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -10,6 +10,7 @@ use Exception; use GuzzleHttp\Client as Guzzle; use GuzzleHttp\Psr7\Uri; +use LogicException; use PHPUnit\Framework\MockObject\MockObject; /** @@ -461,6 +462,37 @@ function (Report $report) use (&$pipelineCompleted) { $this->assertTrue($pipelineCompleted); } + public function testItAddsDiscardClassesMiddlewareByDefault() + { + $syslog = $this->getFunctionMock('Bugsnag\Middleware', 'syslog'); + $syslog->expects($this->once())->with( + LOG_INFO, + 'Discarding event because error class "Exception" matched discardClasses configuration' + ); + + $client = Client::make('foo'); + $client->setDiscardClasses([Exception::class]); + + $report = Report::fromPHPThrowable( + $client->getConfig(), + new Exception('oh no') + ); + + $pipeline = $client->getPipeline(); + $pipelineCompleted = false; + + $pipeline->execute( + $report, + function () use (&$pipelineCompleted) { + $pipelineCompleted = true; + + throw new LogicException('This should never be reached!'); + } + ); + + $this->assertFalse($pipelineCompleted); + } + public function testBreadcrumbsWorks() { $this->client = new Client($this->config = new Configuration('example-api-key'), null, $this->guzzle); @@ -1127,6 +1159,58 @@ public function testMakeGuzzleCreatesTimeoutCanBeSpecified() $this->assertSame(2, $connectTimeout); } + public function testMemoryLimitIncreaseDefault() + { + $this->assertSame(1024 * 1024 * 5, $this->client->getMemoryLimitIncrease()); + } + + public function testMemoryLimitIncreaseCanBeSet() + { + $this->client->setMemoryLimitIncrease(12345); + + $this->assertSame(12345, $this->client->getMemoryLimitIncrease()); + } + + public function testMemoryLimitIncreaseCanBeSetToNull() + { + $this->client->setMemoryLimitIncrease(null); + + $this->assertNull($this->client->getMemoryLimitIncrease()); + } + + public function testDiscardClassesDefault() + { + $this->assertSame([], $this->client->getDiscardClasses()); + } + + public function testDiscardClassesCanBeSet() + { + $discardClasses = [ + \RuntimeException::class, + \LogicException::class, + \TypeError::class, + '/^(Under|Over)flowException$/', + ]; + + $this->client->setDiscardClasses($discardClasses); + + $this->assertSame($discardClasses, $this->client->getDiscardClasses()); + } + + public function testRedactedKeysDefault() + { + $this->assertSame([], $this->client->getRedactedKeys()); + } + + public function testRedactedKeysCanBeSet() + { + $redactedKeys = ['password', 'password_confirmation']; + + $this->client->setRedactedKeys($redactedKeys); + + $this->assertSame($redactedKeys, $this->client->getRedactedKeys()); + } + private function getGuzzleOption($guzzle, $name) { if (GuzzleCompat::isUsingGuzzle5()) { diff --git a/tests/ConfigurationTest.php b/tests/ConfigurationTest.php index 808dddff..7c04d260 100644 --- a/tests/ConfigurationTest.php +++ b/tests/ConfigurationTest.php @@ -306,4 +306,56 @@ public function testTheSessionEndpointCanBeSetIfNecessary() $this->assertSame($expected, $this->config->getSessionEndpoint()); } + + public function testMemoryLimitIncreaseDefault() + { + $this->assertSame(1024 * 1024 * 5, $this->config->getMemoryLimitIncrease()); + } + + public function testMemoryLimitIncreaseCanBeSet() + { + $this->config->setMemoryLimitIncrease(12345); + + $this->assertSame(12345, $this->config->getMemoryLimitIncrease()); + } + + public function testMemoryLimitIncreaseCanBeSetToNull() + { + $this->config->setMemoryLimitIncrease(null); + + $this->assertNull($this->config->getMemoryLimitIncrease()); + } + + public function testDiscardClassesDefault() + { + $this->assertSame([], $this->config->getDiscardClasses()); + } + + public function testDiscardClassesCanBeSet() + { + $discardClasses = [ + \RuntimeException::class, + \LogicException::class, + \TypeError::class, + '/^(Under|Over)flowException$/', + ]; + + $this->config->setDiscardClasses($discardClasses); + + $this->assertSame($discardClasses, $this->config->getDiscardClasses()); + } + + public function testRedactedKeysDefault() + { + $this->assertSame([], $this->config->getRedactedKeys()); + } + + public function testRedactedKeysCanBeSet() + { + $redactedKeys = ['password', 'password_confirmation']; + + $this->config->setRedactedKeys($redactedKeys); + + $this->assertSame($redactedKeys, $this->config->getRedactedKeys()); + } } diff --git a/tests/Fakes/SomeException.php b/tests/Fakes/SomeException.php new file mode 100644 index 00000000..d853d5d6 --- /dev/null +++ b/tests/Fakes/SomeException.php @@ -0,0 +1,9 @@ +getFunctionMock('Bugsnag\Middleware', 'syslog'); + $syslog->expects($this->never()); + + $config = new Configuration('API-KEY'); + $middleware = new DiscardClasses($config); + + $report = Report::fromPHPThrowable($config, new Exception()); + $this->assertReportIsNotDiscarded($middleware, $report); + + $report = Report::fromPHPThrowable($config, new LogicException()); + $this->assertReportIsNotDiscarded($middleware, $report); + } + + public function testShouldNotifyWhenExceptionIsNotInDiscardClasses() + { + $syslog = $this->getFunctionMock('Bugsnag\Middleware', 'syslog'); + $syslog->expects($this->never()); + + $config = new Configuration('API-KEY'); + $config->setDiscardClasses([LogicException::class]); + + $middleware = new DiscardClasses($config); + + $report = Report::fromPHPThrowable($config, new Exception()); + $this->assertReportIsNotDiscarded($middleware, $report); + + $report = Report::fromPHPThrowable($config, new UnderflowException()); + $this->assertReportIsNotDiscarded($middleware, $report); + } + + public function testShouldNotifyWhenExceptionDoesNotMatchRegex() + { + $syslog = $this->getFunctionMock('Bugsnag\Middleware', 'syslog'); + $syslog->expects($this->never()); + + $config = new Configuration('API-KEY'); + $config->setDiscardClasses(['/^\d+$/']); + + $middleware = new DiscardClasses($config); + + $report = Report::fromPHPThrowable($config, new Exception()); + $this->assertReportIsNotDiscarded($middleware, $report); + + $report = Report::fromPHPThrowable($config, new LogicException()); + $this->assertReportIsNotDiscarded($middleware, $report); + } + + public function testShouldDiscardExceptionsThatExactlyMatchADiscardedClass() + { + $syslog = $this->getFunctionMock('Bugsnag\Middleware', 'syslog'); + $syslog->expects($this->once())->with( + LOG_INFO, + 'Discarding event because error class "LogicException" matched discardClasses configuration' + ); + + $config = new Configuration('API-KEY'); + $config->setDiscardClasses([LogicException::class]); + + $middleware = new DiscardClasses($config); + + $report = Report::fromPHPThrowable($config, new LogicException()); + $this->assertReportIsDiscarded($middleware, $report); + + $report = Report::fromPHPThrowable($config, new Exception()); + $this->assertReportIsNotDiscarded($middleware, $report); + } + + public function testShouldDiscardPreviousExceptionsThatExactlyMatchADiscardedClass() + { + $syslog = $this->getFunctionMock('Bugsnag\Middleware', 'syslog'); + $syslog->expects($this->once())->with( + LOG_INFO, + 'Discarding event because error class "LogicException" matched discardClasses configuration' + ); + + $config = new Configuration('API-KEY'); + $config->setDiscardClasses([LogicException::class]); + + $middleware = new DiscardClasses($config); + + $report = Report::fromPHPThrowable($config, new Exception('', 0, new LogicException())); + $this->assertReportIsDiscarded($middleware, $report); + + $report = Report::fromPHPThrowable($config, new Exception()); + $this->assertReportIsNotDiscarded($middleware, $report); + + $report = Report::fromPHPThrowable($config, new Exception('', 0, new UnderflowException())); + $this->assertReportIsNotDiscarded($middleware, $report); + } + + public function testShouldDiscardExceptionsThatMatchADiscardClassRegex() + { + $syslog = $this->getFunctionMock('Bugsnag\Middleware', 'syslog'); + $syslog->expects($this->exactly(3))->withConsecutive( + [LOG_INFO, 'Discarding event because error class "UnderflowException" matched discardClasses configuration'], + [LOG_INFO, 'Discarding event because error class "OverflowException" matched discardClasses configuration'], + [LOG_INFO, 'Discarding event because error class "OverflowException" matched discardClasses configuration'] + ); + + $config = new Configuration('API-KEY'); + $config->setDiscardClasses(['/^(Under|Over)flowException$/']); + + $middleware = new DiscardClasses($config); + + $report = Report::fromPHPThrowable($config, new UnderflowException()); + $this->assertReportIsDiscarded($middleware, $report); + + $report = Report::fromPHPThrowable($config, new OverflowException()); + $this->assertReportIsDiscarded($middleware, $report); + + $report = Report::fromPHPThrowable($config, new LogicException()); + $this->assertReportIsNotDiscarded($middleware, $report); + + $report = Report::fromPHPThrowable($config, new LogicException('', 0, new OverflowException())); + $this->assertReportIsDiscarded($middleware, $report); + } + + public function testShouldDiscardErrorsThatExactlyMatchAGivenErrorName() + { + $syslog = $this->getFunctionMock('Bugsnag\Middleware', 'syslog'); + $syslog->expects($this->once())->with( + LOG_INFO, + 'Discarding event because error class "PHP Warning" matched discardClasses configuration' + ); + + $config = new Configuration('API-KEY'); + $config->setDiscardClasses([ErrorTypes::getName(E_WARNING)]); + + $middleware = new DiscardClasses($config); + + $report = Report::fromPHPError($config, E_WARNING, 'warning', '/a/b/c.php', 123); + $this->assertReportIsDiscarded($middleware, $report); + + $report = Report::fromPHPError($config, E_USER_WARNING, 'user warning', '/a/b/c.php', 123); + $this->assertReportIsNotDiscarded($middleware, $report); + } + + public function testShouldDiscardErrorsThatMatchARegex() + { + $syslog = $this->getFunctionMock('Bugsnag\Middleware', 'syslog'); + $syslog->expects($this->exactly(2))->withConsecutive( + [LOG_INFO, 'Discarding event because error class "PHP Notice" matched discardClasses configuration'], + [LOG_INFO, 'Discarding event because error class "User Notice" matched discardClasses configuration'] + ); + + $config = new Configuration('API-KEY'); + $config->setDiscardClasses(['/\bNotice\b/']); + + $middleware = new DiscardClasses($config); + + $report = Report::fromPHPError($config, E_NOTICE, 'notice', '/a/b/c.php', 123); + $this->assertReportIsDiscarded($middleware, $report); + + $report = Report::fromPHPError($config, E_USER_NOTICE, 'user notice', '/a/b/c.php', 123); + $this->assertReportIsDiscarded($middleware, $report); + + $report = Report::fromPHPError($config, E_WARNING, 'warning', '/a/b/c.php', 123); + $this->assertReportIsNotDiscarded($middleware, $report); + } + + /** + * Assert that DiscardClasses calls the next middleware for this Report. + * + * @param DiscardClasses $middleware + * @param Report $report + * + * @return void + */ + private function assertReportIsNotDiscarded(DiscardClasses $middleware, Report $report) + { + $wasCalled = $this->runMiddleware($middleware, $report); + + $this->assertTrue($wasCalled, 'Expected the DiscardClasses middleware to call $next'); + } + + /** + * Assert that DiscardClasses does not call the next middleware for this Report. + * + * @param DiscardClasses $middleware + * @param Report $report + * + * @return void + */ + private function assertReportIsDiscarded(DiscardClasses $middleware, Report $report) + { + $wasCalled = $this->runMiddleware($middleware, $report); + + $this->assertFalse($wasCalled, 'Expected the DiscardClasses middleware not to call $next'); + } + + /** + * Run the given middleware against the report and return if it called $next. + * + * @param callable $middleware + * @param Report $report + * + * @return bool + */ + private function runMiddleware(callable $middleware, Report $report) + { + $wasCalled = false; + + $middleware($report, function () use (&$wasCalled) { + $wasCalled = true; + }); + + return $wasCalled; + } +} diff --git a/tests/ReportTest.php b/tests/ReportTest.php index a3749a86..50b7806e 100644 --- a/tests/ReportTest.php +++ b/tests/ReportTest.php @@ -2,13 +2,18 @@ namespace Bugsnag\Tests; +use BadMethodCallException; +use Bugsnag\Breadcrumbs\Breadcrumb; use Bugsnag\Configuration; use Bugsnag\Report; use Bugsnag\Stacktrace; +use Bugsnag\Tests\Fakes\SomeException; use Bugsnag\Tests\Fakes\StringableObject; use Exception; use InvalidArgumentException; +use LogicException; use ParseError; +use RuntimeException; use stdClass; class ReportTest extends TestCase @@ -174,6 +179,160 @@ public function testFiltersAreCaseInsensitive() ); } + /** + * @dataProvider redactedKeysProvider + * + * @param array $metadata + * @param string[] $redactedKeys + * @param array $expected + * + * @return void + */ + public function testRedactedKeys( + array $metadata, + array $redactedKeys, + array $expected + ) { + $this->config->setRedactedKeys($redactedKeys); + $this->report->setMetaData(['Testing' => $metadata]); + + $actual = $this->report->toArray()['metaData']['Testing']; + + $this->assertEquals($expected, $actual); + } + + /** + * @dataProvider redactedKeysProvider + * + * @param array $metadata + * @param string[] $redactedKeys + * @param array $expected + * + * @return void + */ + public function testRedactedKeysWithBreadcrumbMetadata( + array $metadata, + array $redactedKeys, + array $expected + ) { + $this->config->setRedactedKeys($redactedKeys); + + $breadcrumb = new Breadcrumb('abc', Breadcrumb::LOG_TYPE, ['Testing' => $metadata]); + $this->report->addBreadcrumb($breadcrumb); + + $actual = $this->report->toArray()['breadcrumbs'][0]['metaData']['Testing']; + + $this->assertEquals($expected, $actual); + } + + public function redactedKeysProvider() + { + yield [ + ['abc' => 'xyz', 'a' => 1, 'b' => 2, 'c' => 3], + ['a', 'c'], + ['abc' => 'xyz', 'a' => '[FILTERED]', 'b' => 2, 'c' => '[FILTERED]'], + ]; + + yield [ + ['abc' => 'xyz', 'a' => 1, 'b' => 2, 'C' => 3], + ['A', 'c'], + ['abc' => 'xyz', 'a' => '[FILTERED]', 'b' => 2, 'C' => '[FILTERED]'], + ]; + + yield [ + ['â' => 1, 'b' => 2, 'ñ' => 3, 'n' => 4], + ['â', 'ñ'], + ['â' => '[FILTERED]', 'b' => 2, 'ñ' => '[FILTERED]', 'n' => 4], + ]; + + yield [ + ['â' => 1, 'b' => 2, 'Ñ' => 3], + ['Â', 'ñ'], + ['â' => '[FILTERED]', 'b' => 2, 'Ñ' => '[FILTERED]'], + ]; + + // 6e cc 83 is equivalent to "\u{006E}\u{0303}" but in a way PHP 5 can + // understand. This is the character "ñ" built out of "n" and a + // combining tilde + yield [ + ["\x6e\xcc\x83" => 1, 'b' => 2, 'c' => 3, 'n' => 4], + ["\x6e\xcc\x83", 'c'], + ["\x6e\xcc\x83" => '[FILTERED]', 'b' => 2, 'c' => '[FILTERED]', 'n' => 4], + ]; + + // 4e cc 83 is equivalent to "\u{004E}\u{0303}", which is the capital + // version of the above ("N" + a combining tilde) + yield [ + ["\x6e\xcc\x83" => 1, 'b' => 2, 'c' => 3, 'n' => 4], + ["\x4e\xcc\x83", 'c'], + ["\x6e\xcc\x83" => '[FILTERED]', 'b' => 2, 'c' => '[FILTERED]', 'n' => 4], + ]; + + // This is "ñ" both as a single character and with the combining tilde + yield [ + ["\x6e\xcc\x83" => 1, 'b' => 2, 'c' => 3, 'n' => 4], + ["\xc3\xb1", 'c'], + ["\x6e\xcc\x83" => '[FILTERED]', 'b' => 2, 'c' => '[FILTERED]', 'n' => 4], + ]; + + // This is "Ñ" as a single character and "ñ" with the combining tilde + yield [ + ["\x6e\xcc\x83" => 1, 'b' => 2, 'c' => 3, 'n' => 4], + ["\xc3\x91", 'c'], + ["\x6e\xcc\x83" => '[FILTERED]', 'b' => 2, 'c' => '[FILTERED]', 'n' => 4], + ]; + + // This is "Ñ" as a single character and "ñ" with the combining tilde + yield [ + ["\xc3\x91" => 1, 'b' => 2, 'c' => 3, 'n' => 4], + ["\x6e\xcc\x83", 'c'], + ["\xc3\x91" => '[FILTERED]', 'b' => 2, 'c' => '[FILTERED]', 'n' => 4], + ]; + + yield [ + ['abc' => 1, 'xyz' => 2], + ['/^.b.$/'], + ['abc' => '[FILTERED]', 'xyz' => 2], + ]; + + yield [ + ['abc' => 1, 'xyz' => 2, 'oOo' => 3], + ['/^[a-z]{3}$/'], + ['abc' => '[FILTERED]', 'xyz' => '[FILTERED]', 'oOo' => 3], + ]; + + yield [ + ['abc' => 1, 'xyz' => 2, 'oOo' => 3, 'oOoOo' => 4], + ['/^[A-z]{3}$/'], + ['abc' => '[FILTERED]', 'xyz' => '[FILTERED]', 'oOo' => '[FILTERED]', 'oOoOo' => 4], + ]; + + yield [ + ['abc' => 1, 'xyz' => 2, 'yyy' => 3], + ['/(c|y)$/'], + ['abc' => '[FILTERED]', 'xyz' => 2, 'yyy' => '[FILTERED]'], + ]; + + yield [ + ['abc' => 1, 'xyz' => 2, 'yyy' => 3], + ['/c$/', '/y$/'], + ['abc' => '[FILTERED]', 'xyz' => 2, 'yyy' => '[FILTERED]'], + ]; + + // This doesn't match the regex but does match as a string comparison + yield [ + ['/^abc$/' => 1, 'xyz' => 2, 'oOo' => 3], + ['/^abc$/'], + ['/^abc$/' => '[FILTERED]', 'xyz' => 2, 'oOo' => 3], + ]; + + yield [ + ['/abc/' => 1, 'xyz' => 2, 'oOo' => 3], + ['/abc/'], + ['/abc/' => '[FILTERED]', 'xyz' => 2, 'oOo' => 3], + ]; + } + public function testCanGetStacktrace() { $beginningOfTest = __LINE__; @@ -581,4 +740,52 @@ public function testDefaultSeverityTypeSet() $data = $report->toArray(); $this->assertSame($data['severityReason'], ['foo' => 'bar', 'type' => 'userSpecifiedSeverity']); } + + public function testGetErrorsWithNoPreviousErrors() + { + $exception = new Exception('abc xyz'); + + $report = Report::fromPHPThrowable($this->config, $exception); + $actual = $report->getErrors(); + + $expected = [ + ['errorClass' => 'Exception', 'errorMessage' => 'abc xyz', 'type' => 'php'], + ]; + + $this->assertSame($expected, $actual); + } + + public function testGetErrorsWithPreviousErrors() + { + $exception5 = new SomeException('exception5'); + $exception4 = new BadMethodCallException('exception4', 0, $exception5); + $exception3 = new LogicException('exception3', 0, $exception4); + $exception2 = new RuntimeException('exception2', 0, $exception3); + $exception1 = new Exception('exception1', 0, $exception2); + + $report = Report::fromPHPThrowable($this->config, $exception1); + $actual = $report->getErrors(); + + $expected = [ + ['errorClass' => 'Exception', 'errorMessage' => 'exception1', 'type' => 'php'], + ['errorClass' => 'RuntimeException', 'errorMessage' => 'exception2', 'type' => 'php'], + ['errorClass' => 'LogicException', 'errorMessage' => 'exception3', 'type' => 'php'], + ['errorClass' => 'BadMethodCallException', 'errorMessage' => 'exception4', 'type' => 'php'], + ['errorClass' => 'Bugsnag\Tests\Fakes\SomeException', 'errorMessage' => 'exception5', 'type' => 'php'], + ]; + + $this->assertSame($expected, $actual); + } + + public function testGetErrorsWithPhpError() + { + $report = Report::fromPHPError($this->config, E_WARNING, 'bad stuff!', '/usr/src/stuff.php', 1234); + $actual = $report->getErrors(); + + $expected = [ + ['errorClass' => 'PHP Warning', 'errorMessage' => 'bad stuff!', 'type' => 'php'], + ]; + + $this->assertSame($expected, $actual); + } } diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php new file mode 100644 index 00000000..162bba47 --- /dev/null +++ b/tests/UtilsTest.php @@ -0,0 +1,111 @@ +markTestSkipped("This test requires at least PHP {$requiredVersion} to run"); + } + } + + $this->assertSame( + $expected, + Utils::stringCaseEquals($a, $b), + sprintf( + 'Expected "%s" %s "%s"', + $a, + $expected ? 'to equal' : 'not to equal', + $b + ) + ); + + $this->assertSame( + $expected, + Utils::stringCaseEquals($b, $a), + sprintf( + 'Expected "%s" %s "%s"', + $b, + $expected ? 'to equal' : 'not to equal', + $a + ) + ); + } + + public function stringCaseEqualsProvider() + { + yield ['a', 'a', true]; + yield ['a', 'A', true]; + yield ['A', 'A', true]; + + yield ['a', 'b', false]; + yield ['c', 'b', false]; + + yield ['jalapeño', 'jalapeño', true]; + yield ['JALAPEÑO', 'jalapeño', true]; + yield ['JaLaPeÑo', 'jAlApEñO', true]; + yield ['jalapeño', 'jalapeno', false]; + + // 6e cc 83 is equivalent to "\u{006E}\u{0303}" but in a way PHP 5 can + // understand. This is the character "ñ" built out of "n" and a + // combining tilde + yield ["jalape\x6e\xcc\x83o", "jalape\x6e\xcc\x83o", true]; + yield ["jalape\x6e\xcc\x83o", 'jalapeño', true]; + + // 4e cc 83 is equivalent to "\u{004E}\u{0303}", which is the capital + // version of the above ("N" + a combining tilde) + yield ["jalape\x6e\xcc\x83o", "jalape\x4e\xcc\x83o", true]; + + // This is "ñ" both as a single character and with the combining tilde + yield ["jalape\x6e\xcc\x83o", "jalape\xc3\xb1o", true]; + + // This is "Ñ" as a single character and "ñ" with the combining tilde + yield ["jalape\x6e\xcc\x83o", "jalape\xc3\x91o", true]; + + yield ["jalape\x6e\xcc\x83o", 'jalapeno', false]; + + // This test fails with a simple strcasecmp, proving that the MB string + // functions are necessary in some cases + // This requires PHP 7.3, which contains many MB String improvements: + // https://www.php.net/manual/en/migration73.new-features.php#migration73.new-features.mbstring + yield ['größer', 'gröẞer', true, '7.3.0']; + yield ['größer', 'GRÖẞER', true, '7.3.0']; + + // Tests with characters from various unicode planes + + yield ['Iñtërnâtiônàližætiøn', 'Iñtërnâtiônàližætiøn', true]; + yield ['iñtërnâtiônàližætiøn', 'IÑTËRNÂTIÔNÀLIŽÆTIØN', true, '5.6.0']; + + yield ['обичам те', 'обичам те', true]; + yield ['обичам те', 'ОБИЧАМ ТЕ', true, '5.6.0']; + yield ['ОбИчАм Те', 'оБиЧаМ тЕ', true, '5.6.0']; + yield ['обичам те', 'oбичam te', false]; + + yield ['大好きだよ', '大好きだよ', true]; + yield ['أحبك', 'أحبك', true]; + + yield ['😀😀', '😀😀', true]; + + yield ['👨‍👩‍👧‍👦🇬🇧', '👨‍👩‍👧‍👦🇬🇧', true]; + yield ['🇬🇧👨‍👩‍👧‍👦', '👨‍👩‍👧‍👦🇬🇧', false]; + + $ukFlag = "\xf0\x9f\x87\xac\xf0\x9f\x87\xa7"; + yield ['👨‍👩‍👧‍👦'.$ukFlag, '👨‍👩‍👧‍👦🇬🇧', true]; + yield [$ukFlag.'👨‍👩‍👧‍👦', '👨‍👩‍👧‍👦🇬🇧', false]; + } +} diff --git a/tests/phpt/_prelude.php b/tests/phpt/_prelude.php index 24f15015..26cfd04a 100644 --- a/tests/phpt/_prelude.php +++ b/tests/phpt/_prelude.php @@ -6,14 +6,20 @@ use Bugsnag\Configuration; use Bugsnag\Tests\phpt\Utilities\FakeGuzzle; +date_default_timezone_set('UTC'); + $config = new Configuration('11111111111111111111111111111111'); $config->setNotifyEndpoint('http://localhost/notify'); $config->setSessionEndpoint('http://localhost/sessions'); $guzzle = new FakeGuzzle(); -return new Client( +$client = new Client( $config, null, $guzzle ); + +$client->registerDefaultCallbacks(); + +return $client; diff --git a/tests/phpt/handler_can_be_manually_called_multiple_times.phpt b/tests/phpt/handler_can_be_manually_called_multiple_times.phpt index 408518de..d956b0df 100644 --- a/tests/phpt/handler_can_be_manually_called_multiple_times.phpt +++ b/tests/phpt/handler_can_be_manually_called_multiple_times.phpt @@ -17,7 +17,7 @@ $handler->exceptionHandler(new LogicException('terrible things')); --EXPECTF-- array(1) { [0]=> - object(Exception)#15 (7) { + object(Exception)#%d (7) { ["message":protected]=> string(10) "bad things" ["string":"Exception":private]=> diff --git a/tests/phpt/handler_should_call_the_previous_exception_handler.phpt b/tests/phpt/handler_should_call_the_previous_exception_handler.phpt index b5550356..26d10de8 100644 --- a/tests/phpt/handler_should_call_the_previous_exception_handler.phpt +++ b/tests/phpt/handler_should_call_the_previous_exception_handler.phpt @@ -15,7 +15,7 @@ throw new RuntimeException('abc xyz'); var_dump('I should not be reached'); ?> --EXPECTF-- -object(RuntimeException)#15 (7) { +object(RuntimeException)#%d (7) { ["message":protected]=> string(7) "abc xyz" ["string":"Exception":private]=> diff --git a/tests/phpt/handler_should_handle_all_throwables.phpt b/tests/phpt/handler_should_handle_all_throwables.phpt index 365d316c..dc261b2d 100644 --- a/tests/phpt/handler_should_handle_all_throwables.phpt +++ b/tests/phpt/handler_should_handle_all_throwables.phpt @@ -21,7 +21,7 @@ if (PHP_MAJOR_VERSION < 7) { } ?> --EXPECTF-- -object(DivisionByZeroError)#15 (7) { +object(DivisionByZeroError)#%d (7) { ["message":protected]=> string(12) "22 / 0 = ???" ["string":"Error":private]=> diff --git a/tests/phpt/handler_should_handle_all_throwables_being_reraised.phpt b/tests/phpt/handler_should_handle_all_throwables_being_reraised.phpt index 01d2883f..a3ca1011 100644 --- a/tests/phpt/handler_should_handle_all_throwables_being_reraised.phpt +++ b/tests/phpt/handler_should_handle_all_throwables_being_reraised.phpt @@ -22,7 +22,7 @@ if (PHP_MAJOR_VERSION < 7) { } ?> --EXPECTF-- -object(DivisionByZeroError)#15 (7) { +object(DivisionByZeroError)#%d (7) { ["message":protected]=> string(12) "22 / 0 = ???" ["string":"Error":private]=> diff --git a/tests/phpt/handler_should_handle_exceptions_caused_by_previous_exception_handler.phpt b/tests/phpt/handler_should_handle_exceptions_caused_by_previous_exception_handler.phpt index 41c813de..214c10c3 100644 --- a/tests/phpt/handler_should_handle_exceptions_caused_by_previous_exception_handler.phpt +++ b/tests/phpt/handler_should_handle_exceptions_caused_by_previous_exception_handler.phpt @@ -16,7 +16,7 @@ throw new RuntimeException('abc xyz'); var_dump('I should not be reached'); ?> --EXPECTF-- -object(RuntimeException)#15 (7) { +object(RuntimeException)#%d (7) { ["message":protected]=> string(7) "abc xyz" ["string":"Exception":private]=> diff --git a/tests/phpt/handler_should_handle_previous_exception_handler_reraising.phpt b/tests/phpt/handler_should_handle_previous_exception_handler_reraising.phpt index 3eec42bb..1e40a14b 100644 --- a/tests/phpt/handler_should_handle_previous_exception_handler_reraising.phpt +++ b/tests/phpt/handler_should_handle_previous_exception_handler_reraising.phpt @@ -16,7 +16,7 @@ throw new RuntimeException('abc xyz'); var_dump('I should not be reached'); ?> --EXPECTF-- -object(RuntimeException)#15 (7) { +object(RuntimeException)#%d (7) { ["message":protected]=> string(7) "abc xyz" ["string":"Exception":private]=> diff --git a/tests/phpt/handler_should_handle_throwables_caused_by_previous_exception_handler.phpt b/tests/phpt/handler_should_handle_throwables_caused_by_previous_exception_handler.phpt index 1ca3ce35..a24206a2 100644 --- a/tests/phpt/handler_should_handle_throwables_caused_by_previous_exception_handler.phpt +++ b/tests/phpt/handler_should_handle_throwables_caused_by_previous_exception_handler.phpt @@ -22,7 +22,7 @@ if (PHP_MAJOR_VERSION < 7) { } ?> --EXPECTF-- -object(RuntimeException)#15 (7) { +object(RuntimeException)#%d (7) { ["message":protected]=> string(7) "abc xyz" ["string":"Exception":private]=> diff --git a/tests/phpt/handler_should_increase_memory_limit_by_configured_amount_on_oom.phpt b/tests/phpt/handler_should_increase_memory_limit_by_configured_amount_on_oom.phpt new file mode 100644 index 00000000..dc551117 --- /dev/null +++ b/tests/phpt/handler_should_increase_memory_limit_by_configured_amount_on_oom.phpt @@ -0,0 +1,30 @@ +--TEST-- +Bugsnag\Handler should increase the memory limit by the configured amount when an OOM happens +--FILE-- +setMemoryLimitIncrease(1024 * 1024 * 10); + +Bugsnag\Handler::register($client); + +ini_set('memory_limit', 1024 * 1024 * 5); +var_dump(ini_get('memory_limit')); + +$client->registerCallback(function () { + var_dump(ini_get('memory_limit')); +}); + +$a = str_repeat('a', 2147483647); + +echo "No OOM!\n"; +?> +--EXPECTF-- +string(7) "5242880" + +Fatal error: Allowed memory size of %d bytes exhausted (tried to allocate %d bytes) in %s on line 14 +string(8) "15728640" +Guzzle request made (1 event)! +* Method: 'POST' +* URI: 'http://localhost/notify' +* Events: + - Allowed memory size of %d bytes exhausted (tried to allocate %d bytes) diff --git a/tests/phpt/handler_should_not_increase_memory_limit_when_memory_limit_increase_is_disabled.phpt b/tests/phpt/handler_should_not_increase_memory_limit_when_memory_limit_increase_is_disabled.phpt new file mode 100644 index 00000000..496ea60f --- /dev/null +++ b/tests/phpt/handler_should_not_increase_memory_limit_when_memory_limit_increase_is_disabled.phpt @@ -0,0 +1,32 @@ +--TEST-- +Bugsnag\Handler should not increase the memory limit when memoryLimitIncrease is disabled +--FILE-- +setMemoryLimitIncrease(null); + +ini_set('memory_limit', '5M'); +var_dump(ini_get('memory_limit')); + +$client->registerCallback(function () { + // This should be the same as the first var_dump, because we should not have + // increase the memory limit + var_dump(ini_get('memory_limit')); +}); + +Bugsnag\Handler::register($client); + +$a = str_repeat('a', 2147483647); + +echo "No OOM!\n"; +?> +--EXPECTF-- +string(2) "5M" + +Fatal error: Allowed memory size of %d bytes exhausted (tried to allocate %d bytes) in %s on line 16 +string(2) "5M" +Guzzle request made (1 event)! +* Method: 'POST' +* URI: 'http://localhost/notify' +* Events: + - Allowed memory size of %d bytes exhausted (tried to allocate %d bytes) diff --git a/tests/phpt/handler_should_report_oom_from_large_allocation.phpt b/tests/phpt/handler_should_report_oom_from_large_allocation.phpt new file mode 100644 index 00000000..e475c65c --- /dev/null +++ b/tests/phpt/handler_should_report_oom_from_large_allocation.phpt @@ -0,0 +1,21 @@ +--TEST-- +Bugsnag\Handler should report OOMs triggered by single large allocations +--FILE-- + +--EXPECTF-- +Fatal error: Allowed memory size of %d bytes exhausted (tried to allocate %d bytes) in %s on line 8 +Guzzle request made (1 event)! +* Method: 'POST' +* URI: 'http://localhost/notify' +* Events: + - Allowed memory size of %d bytes exhausted (tried to allocate %d bytes) diff --git a/tests/phpt/handler_should_report_oom_from_small_allocations.phpt b/tests/phpt/handler_should_report_oom_from_small_allocations.phpt new file mode 100644 index 00000000..e7af2dda --- /dev/null +++ b/tests/phpt/handler_should_report_oom_from_small_allocations.phpt @@ -0,0 +1,35 @@ +--TEST-- +Bugsnag\Handler should report OOMs triggered by many small allocations +--FILE-- +b = $a; +} + +echo "No OOM!\n"; +?> +--SKIPIF-- + +--EXPECTF-- +Fatal error: Allowed memory size of %d bytes exhausted (tried to allocate %d bytes) in %s on line %d +Guzzle request made (1 event)! +* Method: 'POST' +* URI: 'http://localhost/notify' +* Events: + - Allowed memory size of %d bytes exhausted (tried to allocate %d bytes) diff --git a/tests/phpt/php5/handler_should_report_oom_from_small_allocations.phpt b/tests/phpt/php5/handler_should_report_oom_from_small_allocations.phpt new file mode 100644 index 00000000..eed45f34 --- /dev/null +++ b/tests/phpt/php5/handler_should_report_oom_from_small_allocations.phpt @@ -0,0 +1,34 @@ +--TEST-- +Bugsnag\Handler should report OOMs triggered by many small allocations +--FILE-- + +--SKIPIF-- + +--EXPECTF-- +Fatal error: Allowed memory size of %d bytes exhausted (tried to allocate %d bytes) in %s on line 14 +Guzzle request made (1 event)! +* Method: 'POST' +* URI: 'http://localhost/notify' +* Events: + - Allowed memory size of %d bytes exhausted (tried to allocate %d bytes) diff --git a/tests/phpt/php7/handler_should_report_parse_errors_with_previous_handler.phpt b/tests/phpt/php7/handler_should_report_parse_errors_with_previous_handler.phpt index 23ff759b..8b77e794 100644 --- a/tests/phpt/php7/handler_should_report_parse_errors_with_previous_handler.phpt +++ b/tests/phpt/php7/handler_should_report_parse_errors_with_previous_handler.phpt @@ -22,7 +22,7 @@ if (PHP_MAJOR_VERSION !== 7) { } ?> --EXPECTF-- -object(ParseError)#15 (7) { +object(ParseError)#%d (7) { ["message":protected]=> string(28) "syntax error, unexpected '{'" ["string":"Error":private]=> diff --git a/tests/phpt/php8/handler_should_report_parse_errors_with_previous_handler.phpt b/tests/phpt/php8/handler_should_report_parse_errors_with_previous_handler.phpt index 43b0ed3c..001a03ee 100644 --- a/tests/phpt/php8/handler_should_report_parse_errors_with_previous_handler.phpt +++ b/tests/phpt/php8/handler_should_report_parse_errors_with_previous_handler.phpt @@ -22,7 +22,7 @@ if (PHP_MAJOR_VERSION !== 8) { } ?> --EXPECTF-- -object(ParseError)#15 (7) { +object(ParseError)#%d (7) { ["message":protected]=> string(34) "syntax error, unexpected token "}"" ["string":"Error":private]=>