Issue #2867788 by mondrake, daffie, dawehner, alexpott, larowlan: Log::findCaller fails to report the correct caller function with non-core drivers

merge-requests/2419/head
Alex Pott 2020-02-21 14:30:50 +00:00
parent 2267926607
commit 97bac4f16a
No known key found for this signature in database
GPG Key ID: 31905460D4A69276
2 changed files with 254 additions and 17 deletions

View File

@ -38,6 +38,13 @@ class Log {
*/ */
protected $connectionKey = 'default'; protected $connectionKey = 'default';
/**
* The PHP namespace of the database driver that this object is logging.
*
* @var string
*/
protected $driverNamespace;
/** /**
* Constructor. * Constructor.
* *
@ -124,11 +131,13 @@ class Log {
/** /**
* Determine the routine that called this query. * Determine the routine that called this query.
* *
* We define "the routine that called this query" as the first entry in * Traversing the call stack from the very first call made during the
* the call stack that is not inside the includes/Drupal/Database directory * request, we define "the routine that called this query" as the last entry
* and does have a file (which excludes call_user_func_array(), anonymous * in the call stack that is not any method called from the namespace of the
* functions and similar). That makes the climbing logic very simple, and * database driver, is not inside the Drupal\Core\Database namespace and does
* handles the variable stack depth caused by the query builders. * have a file (which excludes call_user_func_array(), anonymous functions
* and similar). That makes the climbing logic very simple, and handles the
* variable stack depth caused by the query builders.
* *
* See the @link http://php.net/debug_backtrace debug_backtrace() @endlink * See the @link http://php.net/debug_backtrace debug_backtrace() @endlink
* function. * function.
@ -141,26 +150,62 @@ class Log {
* database call itself. * database call itself.
*/ */
public function findCaller() { public function findCaller() {
$stack = debug_backtrace(); $stack = $this->getDebugBacktrace();
for ($i = 0, $stack_count = count($stack); $i < $stack_count; ++$i) {
// If the call was made from a function, 'class' will be empty. It's // Starting from the very first entry processed during the request, find
// just easier to give it a default value than to try and integrate // the first function call that can be identified as a call to a
// that into the if statement below. // method/function in the database layer.
if (empty($stack[$i + 1]['class'])) { for ($n = count($stack) - 1; $n >= 0; $n--) {
$stack[$i + 1]['class'] = ''; // If the call was made from a function, 'class' will be empty. We give
// it a default empty string value in that case.
$class = $stack[$n]['class'] ?? '';
if (strpos($class, __NAMESPACE__, 0) === 0 || strpos($class, $this->getDriverNamespace(), 0) === 0) {
break;
} }
if (strpos($stack[$i + 1]['class'], __NAMESPACE__) === FALSE && !empty($stack[$i]['file'])) { }
$stack[$i] += ['file' => '?', 'line' => '?', 'args' => []];
// Return the previous function call whose stack entry has a 'file' key,
// that is, it is not a callback or a closure.
for ($i = $n; $i < count($stack); $i++) {
if (!empty($stack[$i]['file'])) {
return [ return [
'file' => $stack[$i]['file'], 'file' => $stack[$i]['file'],
'line' => $stack[$i]['line'], 'line' => $stack[$i]['line'],
'function' => $stack[$i + 1]['function'], 'function' => $stack[$i + 1]['function'],
'class' => $stack[$i + 1]['class'], 'class' => $stack[$i + 1]['class'] ?? NULL,
'type' => isset($stack[$i + 1]['type']) ? $stack[$i + 1]['type'] : NULL, 'type' => $stack[$i + 1]['type'] ?? NULL,
'args' => $stack[$i + 1]['args'], 'args' => $stack[$i + 1]['args'] ?? [],
]; ];
} }
} }
} }
/**
* Gets the namespace of the database driver.
*
* @return string|null
* Namespace of the database driver, or NULL if the connection is
* missing.
*/
protected function getDriverNamespace() {
if (!isset($this->driverNamespace)) {
$this->driverNamespace = (new \ReflectionObject(Database::getConnection('default', $this->connectionKey)))->getNamespaceName();
}
return $this->driverNamespace;
}
/**
* Gets the debug backtrace.
*
* Wraps the debug_backtrace function to allow mocking results in PHPUnit
* tests.
*
* @return array[]
* The debug backtrace.
*/
protected function getDebugBacktrace() {
return debug_backtrace();
}
} }

View File

@ -2,11 +2,14 @@
namespace Drupal\KernelTests\Core\Database; namespace Drupal\KernelTests\Core\Database;
use Drupal\Core\Database\Log;
use Drupal\Core\Database\Database; use Drupal\Core\Database\Database;
/** /**
* Tests the query logging facility. * Tests the query logging facility.
* *
* @coversDefaultClass \Drupal\Core\Database\Log
*
* @group Database * @group Database
*/ */
class LoggingTest extends DatabaseTestBase { class LoggingTest extends DatabaseTestBase {
@ -135,4 +138,193 @@ class LoggingTest extends DatabaseTestBase {
$this->assertEqual($result, [], 'The function getLog with a wrong key returns an empty array.'); $this->assertEqual($result, [], 'The function getLog with a wrong key returns an empty array.');
} }
/**
* Tests that a log called by a custom database driver returns proper caller.
*
* @param string $driver_namespace
* The driver namespace to be tested.
* @param string $stack
* A test debug_backtrace stack.
* @param array $expected_entry
* The expected stack entry.
*
* @covers ::findCaller
*
* @dataProvider providerContribDriverLog
*/
public function testContribDriverLog($driver_namespace, $stack, array $expected_entry) {
$mock_builder = $this->getMockBuilder(Log::class);
$log = $mock_builder
->setMethods(['getDriverNamespace', 'getDebugBacktrace'])
->getMock();
$log->expects($this->any())
->method('getDriverNamespace')
->will($this->returnValue($driver_namespace));
$log->expects($this->once())
->method('getDebugBacktrace')
->will($this->returnValue($stack));
$result = $log->findCaller($stack);
$this->assertEquals($expected_entry, $result);
}
/**
* Provides data for the testContribDriverLog test.
*
* @return array[]
* A associative array of simple arrays, each having the following elements:
* - the contrib driver PHP namespace
* - a test debug_backtrace stack
* - the stack entry expected to be returned.
*
* @see ::testContribDriverLog()
*/
public function providerContribDriverLog() {
$stack = [
[
'file' => '/var/www/core/lib/Drupal/Core/Database/Log.php',
'line' => 125,
'function' => 'findCaller',
'class' => 'Drupal\\Core\\Database\\Log',
'object' => 'test',
'type' => '->',
'args' => [
0 => 'test',
],
],
[
'file' => '/var/www/libraries/drudbal/lib/Statement.php',
'line' => 264,
'function' => 'log',
'class' => 'Drupal\\Core\\Database\\Log',
'object' => 'test',
'type' => '->',
'args' => [
0 => 'test',
],
],
[
'file' => '/var/www/libraries/drudbal/lib/Connection.php',
'line' => 213,
'function' => 'execute',
'class' => 'Drupal\\Driver\\Database\\dbal\\Statement',
'object' => 'test',
'type' => '->',
'args' => [
0 => 'test',
],
],
[
'file' => '/var/www/core/tests/Drupal/KernelTests/Core/Database/LoggingTest.php',
'line' => 23,
'function' => 'query',
'class' => 'Drupal\\Driver\\Database\\dbal\\Connection',
'object' => 'test',
'type' => '->',
'args' => [
0 => 'test',
],
],
[
'file' => '/var/www/vendor/phpunit/phpunit/src/Framework/TestCase.php',
'line' => 1154,
'function' => 'testEnableLogging',
'class' => 'Drupal\\KernelTests\\Core\\Database\\LoggingTest',
'object' => 'test',
'type' => '->',
'args' => [
0 => 'test',
],
],
[
'file' => '/var/www/vendor/phpunit/phpunit/src/Framework/TestCase.php',
'line' => 842,
'function' => 'runTest',
'class' => 'PHPUnit\\Framework\\TestCase',
'object' => 'test',
'type' => '->',
'args' => [
0 => 'test',
],
],
[
'file' => '/var/www/vendor/phpunit/phpunit/src/Framework/TestResult.php',
'line' => 693,
'function' => 'runBare',
'class' => 'PHPUnit\\Framework\\TestCase',
'object' => 'test',
'type' => '->',
'args' => [
0 => 'test',
],
],
[
'file' => '/var/www/vendor/phpunit/phpunit/src/Framework/TestCase.php',
'line' => 796,
'function' => 'run',
'class' => 'PHPUnit\\Framework\\TestResult',
'object' => 'test',
'type' => '->',
'args' => [
0 => 'test',
],
],
[
'file' => 'Standard input code',
'line' => 57,
'function' => 'run',
'class' => 'PHPUnit\\Framework\\TestCase',
'object' => 'test',
'type' => '->',
'args' => [
0 => 'test',
],
],
[
'file' => 'Standard input code',
'line' => 111,
'function' => '__phpunit_run_isolated_test',
'args' => [
0 => 'test',
],
],
];
return [
// Test that if the driver namespace is in the stack trace, the first
// non-database entry is returned.
'contrib driver namespace' => [
'Drupal\\Driver\\Database\\dbal',
$stack,
[
'class' => 'Drupal\\KernelTests\\Core\\Database\\LoggingTest',
'function' => 'testEnableLogging',
'file' => '/var/www/core/tests/Drupal/KernelTests/Core/Database/LoggingTest.php',
'line' => 23,
'type' => '->',
'args' => [
0 => 'test',
],
],
],
// Extreme case, should not happen at normal runtime - if the driver
// namespace is not in the stack trace, the first entry to a method
// in core database namespace is returned.
'missing driver namespace' => [
'Drupal\\Driver\\Database\\fake',
$stack,
[
'class' => 'Drupal\\Driver\\Database\\dbal\\Statement',
'function' => 'execute',
'file' => '/var/www/libraries/drudbal/lib/Statement.php',
'line' => 264,
'type' => '->',
'args' => [
0 => 'test',
],
],
],
];
}
} }