1
0
mirror of https://github.com/Seldaek/monolog.git synced 2025-08-08 14:16:42 +02:00
Files
php-monolog/tests/Monolog/Formatter/NormalizerFormatterTest.php
2025-03-16 13:30:20 +01:00

495 lines
15 KiB
PHP
Raw Permalink Blame History

<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
*
* (c) Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Monolog\Formatter;
use Monolog\Level;
/**
* @covers Monolog\Formatter\NormalizerFormatter
*/
class NormalizerFormatterTest extends \Monolog\Test\MonologTestCase
{
public function testFormat()
{
$formatter = new NormalizerFormatter('Y-m-d');
$formatted = $formatter->format($this->getRecord(
Level::Error,
'foo',
channel: 'meh',
extra: ['foo' => new TestFooNorm, 'bar' => new TestBarNorm, 'baz' => [], 'res' => fopen('php://memory', 'rb')],
context: [
'foo' => 'bar',
'baz' => 'qux',
'inf' => INF,
'-inf' => -INF,
'nan' => acos(4),
],
));
$this->assertEquals([
'level_name' => Level::Error->getName(),
'level' => Level::Error->value,
'channel' => 'meh',
'message' => 'foo',
'datetime' => date('Y-m-d'),
'extra' => [
'foo' => ['Monolog\\Formatter\\TestFooNorm' => ["foo" => "fooValue"]],
'bar' => ['Monolog\\Formatter\\TestBarNorm' => 'bar'],
'baz' => [],
'res' => '[resource(stream)]',
],
'context' => [
'foo' => 'bar',
'baz' => 'qux',
'inf' => 'INF',
'-inf' => '-INF',
'nan' => 'NaN',
],
], $formatted);
}
public function testFormatExceptions()
{
$formatter = new NormalizerFormatter('Y-m-d');
$e = new \LogicException('bar');
$e2 = new \RuntimeException('foo', 0, $e);
$formatted = $formatter->normalizeValue([
'exception' => $e2,
]);
$this->assertGreaterThan(5, \count($formatted['exception']['trace']));
$this->assertTrue(isset($formatted['exception']['previous']));
unset($formatted['exception']['trace'], $formatted['exception']['previous']);
$this->assertEquals([
'exception' => [
'class' => \get_class($e2),
'message' => $e2->getMessage(),
'code' => $e2->getCode(),
'file' => $e2->getFile().':'.$e2->getLine(),
],
], $formatted);
}
public function testFormatExceptionWithBasePath(): void
{
$formatter = new NormalizerFormatter('Y-m-d');
$formatter->setBasePath(\dirname(\dirname(\dirname(__DIR__))));
$e = new \LogicException('bar');
$formatted = $formatter->normalizeValue([
'exception' => $e,
]);
self::assertSame('tests/Monolog/Formatter/NormalizerFormatterTest.php:' . (__LINE__ - 5), $formatted['exception']['file']);
self::assertStringStartsWith('vendor/phpunit/phpunit/src/Framework/TestCase.php:', $formatted['exception']['trace'][0]);
self::assertStringStartsWith('vendor/phpunit/phpunit/src/Framework/TestCase.php:', $formatted['exception']['trace'][1]);
}
public function testFormatSoapFaultException()
{
if (!class_exists('SoapFault')) {
$this->markTestSkipped('Requires the soap extension');
}
$formatter = new NormalizerFormatter('Y-m-d');
$e = new \SoapFault('foo', 'bar', 'hello', 'world');
$formatted = $formatter->normalizeValue([
'exception' => $e,
]);
unset($formatted['exception']['trace']);
$this->assertEquals([
'exception' => [
'class' => 'SoapFault',
'message' => 'bar',
'code' => 0,
'file' => $e->getFile().':'.$e->getLine(),
'faultcode' => 'foo',
'faultactor' => 'hello',
'detail' => 'world',
],
], $formatted);
$formatter = new NormalizerFormatter('Y-m-d');
$e = new \SoapFault('foo', 'bar', 'hello', (object) ['bar' => (object) ['biz' => 'baz'], 'foo' => 'world']);
$formatted = $formatter->normalizeValue([
'exception' => $e,
]);
unset($formatted['exception']['trace']);
$this->assertEquals([
'exception' => [
'class' => 'SoapFault',
'message' => 'bar',
'code' => 0,
'file' => $e->getFile().':'.$e->getLine(),
'faultcode' => 'foo',
'faultactor' => 'hello',
'detail' => '{"bar":{"biz":"baz"},"foo":"world"}',
],
], $formatted);
}
public function testFormatToStringExceptionHandle()
{
$formatter = new NormalizerFormatter('Y-m-d');
$formatted = $formatter->format($this->getRecord(context: [
'myObject' => new TestToStringError(),
]));
$this->assertEquals(
[
'level_name' => Level::Warning->getName(),
'level' => Level::Warning->value,
'channel' => 'test',
'message' => 'test',
'context' => [
'myObject' => [
TestToStringError::class => [],
],
],
'datetime' => date('Y-m-d'),
'extra' => [],
],
$formatted
);
}
public function testBatchFormat()
{
$formatter = new NormalizerFormatter('Y-m-d');
$formatted = $formatter->formatBatch([
$this->getRecord(Level::Critical, 'bar', channel: 'test'),
$this->getRecord(Level::Warning, 'foo', channel: 'log'),
]);
$this->assertEquals([
[
'level_name' => Level::Critical->getName(),
'level' => Level::Critical->value,
'channel' => 'test',
'message' => 'bar',
'context' => [],
'datetime' => date('Y-m-d'),
'extra' => [],
],
[
'level_name' => Level::Warning->getName(),
'level' => Level::Warning->value,
'channel' => 'log',
'message' => 'foo',
'context' => [],
'datetime' => date('Y-m-d'),
'extra' => [],
],
], $formatted);
}
/**
* Test issue #137
*/
public function testIgnoresRecursiveObjectReferences()
{
// set up the recursion
$foo = new \stdClass();
$bar = new \stdClass();
$foo->bar = $bar;
$bar->foo = $foo;
// set an error handler to assert that the error is not raised anymore
$that = $this;
set_error_handler(function ($level, $message, $file, $line, $context) use ($that) {
if (error_reporting() & $level) {
restore_error_handler();
$that->fail("$message should not be raised");
}
return true;
});
$formatter = new NormalizerFormatter();
$reflMethod = new \ReflectionMethod($formatter, 'toJson');
$res = $reflMethod->invoke($formatter, [$foo, $bar], true);
restore_error_handler();
$this->assertEquals('[{"bar":{"foo":null}},{"foo":{"bar":null}}]', $res);
}
public function testCanNormalizeReferences()
{
$formatter = new NormalizerFormatter();
$x = ['foo' => 'bar'];
$y = ['x' => &$x];
$x['y'] = &$y;
$formatter->normalizeValue($y);
}
public function testToJsonIgnoresInvalidTypes()
{
// set up the invalid data
$resource = fopen(__FILE__, 'r');
// set an error handler to assert that the error is not raised anymore
$that = $this;
set_error_handler(function ($level, $message, $file, $line, $context) use ($that) {
if (error_reporting() & $level) {
restore_error_handler();
$that->fail("$message should not be raised");
}
return true;
});
$formatter = new NormalizerFormatter();
$reflMethod = new \ReflectionMethod($formatter, 'toJson');
$res = $reflMethod->invoke($formatter, [$resource], true);
restore_error_handler();
$this->assertEquals('[null]', $res);
}
public function testNormalizeHandleLargeArraysWithExactly1000Items()
{
$formatter = new NormalizerFormatter();
$largeArray = range(1, 1000);
$res = $formatter->format($this->getRecord(
Level::Critical,
'bar',
channel: 'test',
context: [$largeArray],
));
$this->assertCount(1000, $res['context'][0]);
$this->assertArrayNotHasKey('...', $res['context'][0]);
}
public function testNormalizeHandleLargeArrays()
{
$formatter = new NormalizerFormatter();
$largeArray = range(1, 2000);
$res = $formatter->format($this->getRecord(
Level::Critical,
'bar',
channel: 'test',
context: [$largeArray],
));
$this->assertCount(1001, $res['context'][0]);
$this->assertEquals('Over 1000 items (2000 total), aborting normalization', $res['context'][0]['...']);
}
public function testIgnoresInvalidEncoding()
{
$formatter = new NormalizerFormatter();
$reflMethod = new \ReflectionMethod($formatter, 'toJson');
// send an invalid unicode sequence as a object that can't be cleaned
$record = new \stdClass;
$record->message = "\xB1\x31";
$this->assertsame('{"message":"<22>1"}', $reflMethod->invoke($formatter, $record));
}
public function testConvertsInvalidEncodingAsLatin9()
{
$formatter = new NormalizerFormatter();
$reflMethod = new \ReflectionMethod($formatter, 'toJson');
$res = $reflMethod->invoke($formatter, ['message' => "\xA4\xA6\xA8\xB4\xB8\xBC\xBD\xBE"]);
$this->assertSame('{"message":"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"}', $res);
}
public function testMaxNormalizeDepth()
{
$formatter = new NormalizerFormatter();
$formatter->setMaxNormalizeDepth(1);
$throwable = new \Error('Foo');
$message = $this->formatRecordWithExceptionInContext($formatter, $throwable);
$this->assertEquals(
'Over 1 levels deep, aborting normalization',
$message['context']['exception']
);
}
public function testMaxNormalizeItemCountWith0ItemsMax()
{
$formatter = new NormalizerFormatter();
$formatter->setMaxNormalizeDepth(9);
$formatter->setMaxNormalizeItemCount(0);
$throwable = new \Error('Foo');
$message = $this->formatRecordWithExceptionInContext($formatter, $throwable);
$this->assertEquals(
["..." => "Over 0 items (7 total), aborting normalization"],
$message
);
}
public function testMaxNormalizeItemCountWith2ItemsMax()
{
$formatter = new NormalizerFormatter();
$formatter->setMaxNormalizeDepth(9);
$formatter->setMaxNormalizeItemCount(2);
$throwable = new \Error('Foo');
$message = $this->formatRecordWithExceptionInContext($formatter, $throwable);
unset($message['context']['exception']['trace']);
unset($message['context']['exception']['file']);
$this->assertEquals(
[
"message" => "foobar",
"context" => ['exception' => [
'class' => 'Error',
'message' => 'Foo',
'code' => 0,
]],
"..." => "Over 2 items (7 total), aborting normalization",
],
$message
);
}
public function testExceptionTraceWithArgs()
{
try {
// This will contain $resource and $wrappedResource as arguments in the trace item
$resource = fopen('php://memory', 'rw+');
fwrite($resource, 'test_resource');
$wrappedResource = new TestFooNorm;
$wrappedResource->foo = $resource;
// Just do something stupid with a resource/wrapped resource as argument
$arr = [$wrappedResource, $resource];
// modifying the array inside throws a "usort(): Array was modified by the user comparison function"
usort($arr, function ($a, $b) {
throw new \ErrorException('Foo');
});
} catch (\Throwable $e) {
}
$formatter = new NormalizerFormatter();
$record = $this->getRecord(context: ['exception' => $e]);
$result = $formatter->format($record);
// See https://github.com/php/php-src/issues/8810 fixed in PHP 8.2
$offset = PHP_VERSION_ID >= 80200 ? 13 : 11;
$this->assertSame(
__FILE__.':'.(__LINE__ - $offset),
$result['context']['exception']['trace'][0]
);
}
private function formatRecordWithExceptionInContext(NormalizerFormatter $formatter, \Throwable $exception): array
{
$message = $formatter->format($this->getRecord(
Level::Critical,
'foobar',
channel: 'core',
context: ['exception' => $exception],
));
return $message;
}
public function testExceptionTraceDoesNotLeakCallUserFuncArgs()
{
try {
$arg = new TestInfoLeak;
\call_user_func([$this, 'throwHelper'], $arg, $dt = new \DateTime());
} catch (\Exception $e) {
}
$formatter = new NormalizerFormatter();
$record = $this->getRecord(context: ['exception' => $e]);
$result = $formatter->format($record);
$this->assertSame(
__FILE__ .':'.(__LINE__-9),
$result['context']['exception']['trace'][0]
);
}
public function testCanNormalizeIncompleteObject(): void
{
$serialized = "O:17:\"Monolog\TestClass\":1:{s:23:\"\x00Monolog\TestClass\x00name\";s:4:\"test\";}";
$object = unserialize($serialized);
$formatter = new NormalizerFormatter();
$record = $this->getRecord(context: ['object' => $object]);
$result = $formatter->format($record);
$this->assertEquals([
'__PHP_Incomplete_Class' => 'Monolog\\TestClass',
], $result['context']['object']);
}
private function throwHelper($arg)
{
throw new \RuntimeException('Thrown');
}
}
class TestFooNorm
{
public $foo = 'fooValue';
}
class TestBarNorm
{
public function __toString()
{
return 'bar';
}
}
class TestStreamFoo
{
public $foo;
public $resource;
public function __construct($resource)
{
$this->resource = $resource;
$this->foo = 'BAR';
}
public function __toString()
{
fseek($this->resource, 0);
return $this->foo . ' - ' . (string) stream_get_contents($this->resource);
}
}
class TestToStringError
{
public function __toString()
{
throw new \RuntimeException('Could not convert to string');
}
}
class TestInfoLeak
{
public function __toString()
{
return 'Sensitive information';
}
}