diff --git a/tests/AbstractTestCase.php b/tests/AbstractTestCase.php new file mode 100644 index 0000000..928329b --- /dev/null +++ b/tests/AbstractTestCase.php @@ -0,0 +1,23 @@ +expectException($exceptionName, $exceptionMessage, $exceptionCode); + return; + } + + parent::setExpectedException($exceptionName, $exceptionMessage, $exceptionCode); + } + } +} elseif (class_exists('PHPUnit_Framework_TestCase')) { + // phpcs:ignore PSR1.Classes.ClassDeclaration.MultipleClasses + class AbstractTestCase extends \PHPUnit_Framework_TestCase + { + } +} diff --git a/tests/LoggerInterfaceTest.php b/tests/LoggerInterfaceTest.php new file mode 100644 index 0000000..066d79b --- /dev/null +++ b/tests/LoggerInterfaceTest.php @@ -0,0 +1,151 @@ + ". + * + * Example ->error('Foo') would yield "error Foo". + * + * @return string[] + */ + abstract public function getLogs(); + + public function testImplements() + { + $this->assertInstanceOf('Psr\Log\LoggerInterface', $this->getLogger()); + } + + /** + * @dataProvider provideLevelsAndMessages + */ + public function testLogsAtAllLevels($level, $message) + { + $logger = $this->getLogger(); + $logger->{$level}($message, array('user' => 'Bob')); + $logger->log($level, $message, array('user' => 'Bob')); + + $expected = array( + "$level message of level $level with context: Bob", + "$level message of level $level with context: Bob", + ); + $this->assertEquals($expected, $this->getLogs()); + } + + public function provideLevelsAndMessages() + { + return array( + LogLevel::EMERGENCY => array(LogLevel::EMERGENCY, 'message of level emergency with context: {user}'), + LogLevel::ALERT => array(LogLevel::ALERT, 'message of level alert with context: {user}'), + LogLevel::CRITICAL => array(LogLevel::CRITICAL, 'message of level critical with context: {user}'), + LogLevel::ERROR => array(LogLevel::ERROR, 'message of level error with context: {user}'), + LogLevel::WARNING => array(LogLevel::WARNING, 'message of level warning with context: {user}'), + LogLevel::NOTICE => array(LogLevel::NOTICE, 'message of level notice with context: {user}'), + LogLevel::INFO => array(LogLevel::INFO, 'message of level info with context: {user}'), + LogLevel::DEBUG => array(LogLevel::DEBUG, 'message of level debug with context: {user}'), + ); + } + + public function testThrowsOnInvalidLevel() + { + $logger = $this->getLogger(); + + $this->setExpectedException('Psr\Log\InvalidArgumentException'); + $logger->log('invalid level', 'Foo'); + } + + public function testContextReplacement() + { + $logger = $this->getLogger(); + $logger->info('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar')); + + $expected = array('info {Message {nothing} Bob Bar a}'); + $this->assertEquals($expected, $this->getLogs()); + } + + public function testObjectCastToString() + { + $string = uniqid('DUMMY'); + $dummy = $this->createStringable($string); + $dummy->expects($this->once()) + ->method('__toString'); + + $this->getLogger()->warning($dummy); + + $expected = array("warning $string"); + $this->assertEquals($expected, $this->getLogs()); + } + + public function testContextCanContainAnything() + { + $closed = fopen('php://memory', 'r'); + fclose($closed); + + $context = array( + 'bool' => true, + 'null' => null, + 'string' => 'Foo', + 'int' => 0, + 'float' => 0.5, + 'nested' => array('with object' => $this->createStringable()), + 'object' => new \DateTime('now', new DateTimeZone('Europe/London')), + 'resource' => fopen('php://memory', 'r'), + 'closed' => $closed, + ); + + $this->getLogger()->warning('Crazy context data', $context); + + $expected = array('warning Crazy context data'); + $this->assertEquals($expected, $this->getLogs()); + } + + public function testContextExceptionKeyCanBeExceptionOrOtherValues() + { + $logger = $this->getLogger(); + $logger->warning('Random message', array('exception' => 'oops')); + $logger->critical('Uncaught Exception!', array('exception' => new \LogicException('Fail'))); + + $expected = array( + 'warning Random message', + 'critical Uncaught Exception!' + ); + $this->assertEquals($expected, $this->getLogs()); + } + + /** + * Creates a mock of a `Stringable`. + * + * @param string $string The string that must be represented by the stringable. + * @return \PHPUnit_Framework_MockObject_MockObject A mock of an object that has a `__toString()` method. + */ + protected function createStringable($string = '') + { + $mock = $this->getMockBuilder('Stringable') + ->setMethods(array('__toString')) + ->getMock(); + + $mock->method('__toString') + ->will($this->returnValue($string)); + + return $mock; + } +} diff --git a/tests/Stub/TestLogger.php b/tests/Stub/TestLogger.php new file mode 100644 index 0000000..d549726 --- /dev/null +++ b/tests/Stub/TestLogger.php @@ -0,0 +1,194 @@ +getLogLevels(), true)) { + throw new InvalidArgumentException(sprintf('Log level "%1$s" is not valid', $level)); + } + + $record = array( + 'level' => $level, + 'message' => $this->formatMessage($message, $level, $context), + 'context' => $context, + ); + + $this->recordsByLevel[$record['level']][] = $record; + $this->records[] = $record; + } + + public function interpolateContext($message, array $context) + { + return preg_replace_callback('!\{([^\}\s]*)\}!', function ($matches) use ($context) { + $key = isset($matches[1]) ? $matches[1] : null; + if (array_key_exists($key, $context)) { + return $context[$key]; + } + + return $matches[0]; + }, $message); + } + + public function formatMessage($message, $level, array $context) + { + $message = $this->interpolateContext($message, $context); + $message = "$level $message"; + + return $message; + } + + public function getLogLevels() + { + $reflection = new ReflectionClass('Psr\Log\LogLevel'); + $constants = $reflection->getConstants(); + + return $constants; + } + + public function getRecords() + { + return $this->records; + } + + public function hasRecords($level) + { + return isset($this->recordsByLevel[$level]); + } + + public function hasRecord($record, $level) + { + if (is_string($record)) { + $record = array('message' => $record); + } + return $this->hasRecordThatPasses(function ($rec) use ($record) { + if ($rec['message'] !== $record['message']) { + return false; + } + if (isset($record['context']) && $rec['context'] !== $record['context']) { + return false; + } + return true; + }, $level); + } + + public function hasRecordThatContains($message, $level) + { + return $this->hasRecordThatPasses(function ($rec) use ($message) { + return strpos($rec['message'], $message) !== false; + }, $level); + } + + public function hasRecordThatMatches($regex, $level) + { + return $this->hasRecordThatPasses(function ($rec) use ($regex) { + return preg_match($regex, $rec['message']) > 0; + }, $level); + } + + /** + * Determines whether the logger has logged matching records of the specified level. + * + * @param callable(array{level: \Psr\Log\LogLevel::*, message: string, context: array}, int): bool $predicate + * The function used to evaluate whether a record matches. + * @param \Psr\Log\LogLevel::* $level The level of the record + * @return bool True if a matching record has been logged; false otherwise. + */ + public function hasRecordThatPasses($predicate, $level) + { + if (!isset($this->recordsByLevel[$level])) { + return false; + } + foreach ($this->recordsByLevel[$level] as $i => $rec) { + if (call_user_func($predicate, $rec, $i)) { + return true; + } + } + return false; + } + + public function __call($method, $args) + { + if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) { + $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3]; + $level = strtolower($matches[2]); + if (method_exists($this, $genericMethod)) { + $args[] = $level; + return call_user_func_array(array($this, $genericMethod), $args); + } + } + throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()'); + } + + public function reset() + { + $this->records = array(); + $this->recordsByLevel = array(); + } +} diff --git a/tests/TestLoggerTest.php b/tests/TestLoggerTest.php new file mode 100644 index 0000000..4c61be0 --- /dev/null +++ b/tests/TestLoggerTest.php @@ -0,0 +1,42 @@ +getMockBuilder('Psr\\Log\\Util\\Tests\\Stub\\TestLogger') + ->enableProxyingToOriginalMethods() + ->getMock(); + + return $mock; + } + + public function getLogger() + { + if (! $this->logger instanceof LoggerInterface) { + $this->logger = $this->createSubject(); + } + + return $this->logger; + } + + public function getLogs() + { + $records = $this->logger->getRecords(); + $messages = array_map(function ($record) { + return isset($record['message']) ? $record['message'] : null; + }, $records); + + return $messages; + } +}