Issue #2605284 by mondrake, david_garcia, GoZ, Jo Fitzgerald, alexpott, Pradnya Pingat, daffie, dawehner, amateescu, David Strauss, larowlan: Testing framework does not work with contributed database drivers

merge-requests/1654/head
Alex Pott 2018-07-10 15:13:08 +01:00
parent f5c6c3b719
commit 61cbaba042
No known key found for this signature in database
GPG Key ID: 31905460D4A69276
8 changed files with 440 additions and 99 deletions

View File

@ -1472,4 +1472,116 @@ abstract class Connection {
throw new \LogicException('The database connection is not serializable. This probably means you are serializing an object that has an indirect reference to the database connection. Adjust your code so that is not necessary. Alternatively, look at DependencySerializationTrait as a temporary solution.');
}
/**
* Creates an array of database connection options from a URL.
*
* @internal
* This method should not be called. Use
* \Drupal\Core\Database\Database::convertDbUrlToConnectionInfo() instead.
*
* @param string $url
* The URL.
* @param string $root
* The root directory of the Drupal installation. Some database drivers,
* like for example SQLite, need this information.
*
* @return array
* The connection options.
*
* @throws \InvalidArgumentException
* Exception thrown when the provided URL does not meet the minimum
* requirements.
*
* @see \Drupal\Core\Database\Database::convertDbUrlToConnectionInfo()
*/
public static function createConnectionOptionsFromUrl($url, $root) {
$url_components = parse_url($url);
if (!isset($url_components['scheme'], $url_components['host'], $url_components['path'])) {
throw new \InvalidArgumentException('Minimum requirement: driver://host/database');
}
$url_components += [
'user' => '',
'pass' => '',
'fragment' => '',
];
// Remove leading slash from the URL path.
if ($url_components['path'][0] === '/') {
$url_components['path'] = substr($url_components['path'], 1);
}
// Use reflection to get the namespace of the class being called.
$reflector = new \ReflectionClass(get_called_class());
$database = [
'driver' => $url_components['scheme'],
'username' => $url_components['user'],
'password' => $url_components['pass'],
'host' => $url_components['host'],
'database' => $url_components['path'],
'namespace' => $reflector->getNamespaceName(),
];
if (isset($url_components['port'])) {
$database['port'] = $url_components['port'];
}
if (!empty($url_components['fragment'])) {
$database['prefix']['default'] = $url_components['fragment'];
}
return $database;
}
/**
* Creates a URL from an array of database connection options.
*
* @internal
* This method should not be called. Use
* \Drupal\Core\Database\Database::getConnectionInfoAsUrl() instead.
*
* @param array $connection_options
* The array of connection options for a database connection.
*
* @return string
* The connection info as a URL.
*
* @throws \InvalidArgumentException
* Exception thrown when the provided array of connection options does not
* meet the minimum requirements.
*
* @see \Drupal\Core\Database\Database::getConnectionInfoAsUrl()
*/
public static function createUrlFromConnectionOptions(array $connection_options) {
if (!isset($connection_options['driver'], $connection_options['database'])) {
throw new \InvalidArgumentException("As a minimum, the connection options array must contain at least the 'driver' and 'database' keys");
}
$user = '';
if (isset($connection_options['username'])) {
$user = $connection_options['username'];
if (isset($connection_options['password'])) {
$user .= ':' . $connection_options['password'];
}
$user .= '@';
}
$host = empty($connection_options['host']) ? 'localhost' : $connection_options['host'];
$db_url = $connection_options['driver'] . '://' . $user . $host;
if (isset($connection_options['port'])) {
$db_url .= ':' . $connection_options['port'];
}
$db_url .= '/' . $connection_options['database'];
if (isset($connection_options['prefix']['default']) && $connection_options['prefix']['default'] !== '') {
$db_url .= '#' . $connection_options['prefix']['default'];
}
return $db_url;
}
}

View File

@ -365,13 +365,8 @@ abstract class Database {
throw new DriverNotSpecifiedException('Driver not specified for this database connection: ' . $key);
}
if (!empty(self::$databaseInfo[$key][$target]['namespace'])) {
$driver_class = self::$databaseInfo[$key][$target]['namespace'] . '\\Connection';
}
else {
// Fallback for Drupal 7 settings.php.
$driver_class = "Drupal\\Core\\Database\\Driver\\{$driver}\\Connection";
}
$namespace = static::getDatabaseDriverNamespace(self::$databaseInfo[$key][$target]);
$driver_class = $namespace . '\\Connection';
$pdo_connection = $driver_class::open(self::$databaseInfo[$key][$target]);
$new_connection = new $driver_class($pdo_connection, self::$databaseInfo[$key][$target]);
@ -455,36 +450,25 @@ abstract class Database {
* requirements.
*/
public static function convertDbUrlToConnectionInfo($url, $root) {
$info = parse_url($url);
if (!isset($info['scheme'], $info['host'], $info['path'])) {
throw new \InvalidArgumentException('Minimum requirement: driver://host/database');
// Check that the URL is well formed, starting with 'scheme://', where
// 'scheme' is a database driver name.
if (preg_match('/^(.*):\/\//', $url, $matches) !== 1) {
throw new \InvalidArgumentException("Missing scheme in URL '$url'");
}
$info += [
'user' => '',
'pass' => '',
'fragment' => '',
];
$driver = $matches[1];
// A SQLite database path with two leading slashes indicates a system path.
// Otherwise the path is relative to the Drupal root.
if ($info['path'][0] === '/') {
$info['path'] = substr($info['path'], 1);
}
if ($info['scheme'] === 'sqlite' && $info['path'][0] !== '/') {
$info['path'] = $root . '/' . $info['path'];
// Discover if the URL has a valid driver scheme. Try with core drivers
// first.
$connection_class = "Drupal\\Core\\Database\\Driver\\{$driver}\\Connection";
if (!class_exists($connection_class)) {
// If the URL is not relative to a core driver, try with custom ones.
$connection_class = "Drupal\\Driver\\Database\\{$driver}\\Connection";
if (!class_exists($connection_class)) {
throw new \InvalidArgumentException("Can not convert '$url' to a database connection, class '$connection_class' does not exist");
}
}
$database = [
'driver' => $info['scheme'],
'username' => $info['user'],
'password' => $info['pass'],
'host' => $info['host'],
'database' => $info['path'],
];
if (isset($info['port'])) {
$database['port'] = $info['port'];
}
return $database;
return $connection_class::createConnectionOptionsFromUrl($url, $root);
}
/**
@ -495,32 +479,36 @@ abstract class Database {
*
* @return string
* The connection info as a URL.
*
* @throws \RuntimeException
* When the database connection is not defined.
*/
public static function getConnectionInfoAsUrl($key = 'default') {
$db_info = static::getConnectionInfo($key);
if ($db_info['default']['driver'] == 'sqlite') {
$db_url = 'sqlite://localhost/' . $db_info['default']['database'];
if (empty($db_info) || empty($db_info['default'])) {
throw new \RuntimeException("Database connection $key not defined or missing the 'default' settings");
}
else {
$user = '';
if ($db_info['default']['username']) {
$user = $db_info['default']['username'];
if ($db_info['default']['password']) {
$user .= ':' . $db_info['default']['password'];
}
$user .= '@';
}
$connection_class = static::getDatabaseDriverNamespace($db_info['default']) . '\\Connection';
return $connection_class::createUrlFromConnectionOptions($db_info['default']);
}
$db_url = $db_info['default']['driver'] . '://' . $user . $db_info['default']['host'];
if (isset($db_info['default']['port'])) {
$db_url .= ':' . $db_info['default']['port'];
}
$db_url .= '/' . $db_info['default']['database'];
/**
* Gets the PHP namespace of a database driver from the connection info.
*
* @param array $connection_info
* The database connection information, as defined in settings.php. The
* structure of this array depends on the database driver it is connecting
* to.
*
* @return string
* The PHP namespace of the driver's database.
*/
protected static function getDatabaseDriverNamespace(array $connection_info) {
if (isset($connection_info['namespace'])) {
return $connection_info['namespace'];
}
if ($db_info['default']['prefix']['default']) {
$db_url .= '#' . $db_info['default']['prefix']['default'];
}
return $db_url;
// Fallback for Drupal 7 settings.php.
return 'Drupal\\Core\\Database\\Driver\\' . $connection_info['driver'];
}
}

View File

@ -435,4 +435,50 @@ class Connection extends DatabaseConnection {
return $prefix . $table;
}
/**
* {@inheritdoc}
*/
public static function createConnectionOptionsFromUrl($url, $root) {
$database = parent::createConnectionOptionsFromUrl($url, $root);
// A SQLite database path with two leading slashes indicates a system path.
// Otherwise the path is relative to the Drupal root.
$url_components = parse_url($url);
if ($url_components['path'][0] === '/') {
$url_components['path'] = substr($url_components['path'], 1);
}
if ($url_components['path'][0] === '/') {
$database['database'] = $url_components['path'];
}
else {
$database['database'] = $root . '/' . $url_components['path'];
}
// User credentials and system port are irrelevant for SQLite.
unset(
$database['username'],
$database['password'],
$database['port']
);
return $database;
}
/**
* {@inheritdoc}
*/
public static function createUrlFromConnectionOptions(array $connection_options) {
if (!isset($connection_options['driver'], $connection_options['database'])) {
throw new \InvalidArgumentException("As a minimum, the connection options array must contain at least the 'driver' and 'database' keys");
}
$db_url = 'sqlite://localhost/' . $connection_options['database'];
if (isset($connection_options['prefix']['default']) && $connection_options['prefix']['default'] !== NULL && $connection_options['prefix']['default'] !== '') {
$db_url .= '#' . $connection_options['prefix']['default'];
}
return $db_url;
}
}

View File

@ -2,6 +2,7 @@
namespace Drupal\Tests\simpletest\Unit;
use Drupal\Core\Database\Database;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\File\FileSystemInterface;
use PHPUnit\Framework\TestCase;
@ -15,6 +16,7 @@ use PHPUnit\Framework\TestCase;
* @group simpletest
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
class SimpletestPhpunitRunCommandTest extends TestCase {
@ -85,6 +87,18 @@ class SimpletestPhpunitRunCommandTest extends TestCase {
* @dataProvider provideStatusCodes
*/
public function testSimpletestPhpUnitRunCommand($status, $label) {
// Add a default database connection in order for
// Database::getConnectionInfoAsUrl() to return valid information.
Database::addConnectionInfo('default', 'default', [
'driver' => 'mysql',
'username' => 'test_user',
'password' => 'test_pass',
'host' => 'test_host',
'database' => 'test_database',
'port' => 3306,
'namespace' => 'Drupal\Core\Database\Driver\mysql',
]
);
$test_id = basename(tempnam(sys_get_temp_dir(), 'xxx'));
putenv('SimpletestPhpunitRunCommandTestWillDie=' . $status);
$ret = simpletest_run_phpunit_tests($test_id, [SimpletestPhpunitRunCommandTestWillDie::class]);

View File

@ -58,18 +58,16 @@ class DbCommandBaseTest extends KernelTestBase {
* Test supplying database connection as a url.
*/
public function testSpecifyDbUrl() {
$connection_info = Database::getConnectionInfo('default')['default'];
$command = new DbCommandBaseTester();
$command_tester = new CommandTester($command);
$command_tester->execute([
'-db-url' => $connection_info['driver'] . '://' . $connection_info['username'] . ':' . $connection_info['password'] . '@' . $connection_info['host'] . '/' . $connection_info['database'],
'-db-url' => Database::getConnectionInfoAsUrl(),
]);
$this->assertEquals('db-tools', $command->getDatabaseConnection($command_tester->getInput())->getKey());
Database::removeConnection('db-tools');
$command_tester->execute([
'--database-url' => $connection_info['driver'] . '://' . $connection_info['username'] . ':' . $connection_info['password'] . '@' . $connection_info['host'] . '/' . $connection_info['database'],
'--database-url' => Database::getConnectionInfoAsUrl(),
]);
$this->assertEquals('db-tools', $command->getDatabaseConnection($command_tester->getInput())->getKey());
}
@ -91,9 +89,8 @@ class DbCommandBaseTest extends KernelTestBase {
]);
$this->assertEquals('extra', $command->getDatabaseConnection($command_tester->getInput())->tablePrefix());
$connection_info = Database::getConnectionInfo('default')['default'];
$command_tester->execute([
'-db-url' => $connection_info['driver'] . '://' . $connection_info['username'] . ':' . $connection_info['password'] . '@' . $connection_info['host'] . '/' . $connection_info['database'],
'-db-url' => Database::getConnectionInfoAsUrl(),
'--prefix' => 'extra2',
]);
$this->assertEquals('extra2', $command->getDatabaseConnection($command_tester->getInput())->tablePrefix());

View File

@ -460,7 +460,7 @@ abstract class KernelTestBase extends TestCase implements ServiceProviderInterfa
// Replace the full table prefix definition to ensure that no table
// prefixes of the test runner leak into the test.
$connection_info[$target]['prefix'] = [
'default' => $value['prefix']['default'] . $this->databasePrefix,
'default' => $this->databasePrefix,
];
}
}

View File

@ -2,16 +2,35 @@
namespace Drupal\Tests\Core\Database;
use Composer\Autoload\ClassLoader;
use Drupal\Core\Database\Database;
use Drupal\Tests\UnitTestCase;
/**
* Tests for database URL to/from database connection array coversions.
*
* These tests run in isolation since we don't want the database static to
* affect other tests.
*
* @coversDefaultClass \Drupal\Core\Database\Database
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*
* @group Database
*/
class UrlConversionTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$additional_class_loader = new ClassLoader();
$additional_class_loader->addPsr4("Drupal\\Driver\\Database\\fake\\", __DIR__ . "/fixtures/driver/fake");
$additional_class_loader->register(TRUE);
}
/**
* @covers ::convertDbUrlToConnectionInfo
*
@ -32,30 +51,82 @@ class UrlConversionTest extends UnitTestCase {
* - database_array: An array containing the expected results.
*/
public function providerConvertDbUrlToConnectionInfo() {
// Some valid datasets.
$root1 = '';
$url1 = 'mysql://test_user:test_pass@test_host:3306/test_database';
$database_array1 = [
'driver' => 'mysql',
'username' => 'test_user',
'password' => 'test_pass',
'host' => 'test_host',
'database' => 'test_database',
'port' => '3306',
];
$root2 = '/var/www/d8';
$url2 = 'sqlite://test_user:test_pass@test_host:3306/test_database';
$database_array2 = [
'driver' => 'sqlite',
'username' => 'test_user',
'password' => 'test_pass',
'host' => 'test_host',
'database' => $root2 . '/test_database',
'port' => 3306,
];
return [
[$root1, $url1, $database_array1],
[$root2, $url2, $database_array2],
'MySql without prefix' => [
'',
'mysql://test_user:test_pass@test_host:3306/test_database',
[
'driver' => 'mysql',
'username' => 'test_user',
'password' => 'test_pass',
'host' => 'test_host',
'database' => 'test_database',
'port' => 3306,
'namespace' => 'Drupal\Core\Database\Driver\mysql',
],
],
'SQLite, relative to root, without prefix' => [
'/var/www/d8',
'sqlite://localhost/test_database',
[
'driver' => 'sqlite',
'host' => 'localhost',
'database' => '/var/www/d8/test_database',
'namespace' => 'Drupal\Core\Database\Driver\sqlite',
],
],
'MySql with prefix' => [
'',
'mysql://test_user:test_pass@test_host:3306/test_database#bar',
[
'driver' => 'mysql',
'username' => 'test_user',
'password' => 'test_pass',
'host' => 'test_host',
'database' => 'test_database',
'prefix' => [
'default' => 'bar',
],
'port' => 3306,
'namespace' => 'Drupal\Core\Database\Driver\mysql',
],
],
'SQLite, relative to root, with prefix' => [
'/var/www/d8',
'sqlite://localhost/test_database#foo',
[
'driver' => 'sqlite',
'host' => 'localhost',
'database' => '/var/www/d8/test_database',
'prefix' => [
'default' => 'foo',
],
'namespace' => 'Drupal\Core\Database\Driver\sqlite',
],
],
'SQLite, absolute path, without prefix' => [
'/var/www/d8',
'sqlite://localhost//baz/test_database',
[
'driver' => 'sqlite',
'host' => 'localhost',
'database' => '/baz/test_database',
'namespace' => 'Drupal\Core\Database\Driver\sqlite',
],
],
'Fake custom database driver, without prefix' => [
'',
'fake://fake_user:fake_pass@fake_host:3456/fake_database',
[
'driver' => 'fake',
'username' => 'fake_user',
'password' => 'fake_pass',
'host' => 'fake_host',
'database' => 'fake_database',
'port' => 3456,
'namespace' => 'Drupal\Driver\Database\fake',
],
],
];
}
@ -64,8 +135,8 @@ class UrlConversionTest extends UnitTestCase {
*
* @dataProvider providerInvalidArgumentsUrlConversion
*/
public function testGetInvalidArgumentExceptionInUrlConversion($url, $root) {
$this->setExpectedException(\InvalidArgumentException::class);
public function testGetInvalidArgumentExceptionInUrlConversion($url, $root, $expected_exception_message) {
$this->setExpectedException(\InvalidArgumentException::class, $expected_exception_message);
Database::convertDbUrlToConnectionInfo($url, $root);
}
@ -76,32 +147,28 @@ class UrlConversionTest extends UnitTestCase {
* Array of arrays with the following elements:
* - An invalid Url string.
* - Drupal root string.
* - The expected exception message.
*/
public function providerInvalidArgumentsUrlConversion() {
return [
['foo', ''],
['foo', 'bar'],
['foo://', 'bar'],
['foo://bar', 'baz'],
['foo://bar:port', 'baz'],
['foo/bar/baz', 'bar2'],
['foo://bar:baz@test1', 'test2'],
['foo', '', "Missing scheme in URL 'foo'"],
['foo', 'bar', "Missing scheme in URL 'foo'"],
['foo://', 'bar', "Can not convert 'foo://' to a database connection, class 'Drupal\\Driver\\Database\\foo\\Connection' does not exist"],
['foo://bar', 'baz', "Can not convert 'foo://bar' to a database connection, class 'Drupal\\Driver\\Database\\foo\\Connection' does not exist"],
['foo://bar:port', 'baz', "Can not convert 'foo://bar:port' to a database connection, class 'Drupal\\Driver\\Database\\foo\\Connection' does not exist"],
['foo/bar/baz', 'bar2', "Missing scheme in URL 'foo/bar/baz'"],
['foo://bar:baz@test1', 'test2', "Can not convert 'foo://bar:baz@test1' to a database connection, class 'Drupal\\Driver\\Database\\foo\\Connection' does not exist"],
];
}
/**
* @covers ::convertDbUrlToConnectionInfo
* @covers ::getConnectionInfoAsUrl
*
* @dataProvider providerGetConnectionInfoAsUrl
*/
public function testGetConnectionInfoAsUrl(array $info, $expected_url) {
Database::addConnectionInfo('default', 'default', $info);
$url = Database::getConnectionInfoAsUrl();
// Remove the connection to not pollute subsequent datasets being tested.
Database::removeConnection('default');
$this->assertEquals($expected_url, $url);
}
@ -122,7 +189,6 @@ class UrlConversionTest extends UnitTestCase {
'prefix' => '',
'host' => 'test_host',
'port' => '3306',
'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
'driver' => 'mysql',
];
$expected_url1 = 'mysql://test_user:test_pass@test_host:3306/test_database';
@ -144,10 +210,58 @@ class UrlConversionTest extends UnitTestCase {
];
$expected_url3 = 'sqlite://localhost/test_database';
$info4 = [
'database' => 'test_database',
'driver' => 'sqlite',
'prefix' => 'pre',
];
$expected_url4 = 'sqlite://localhost/test_database#pre';
return [
[$info1, $expected_url1],
[$info2, $expected_url2],
[$info3, $expected_url3],
[$info4, $expected_url4],
];
}
/**
* Test ::getConnectionInfoAsUrl() exception for invalid arguments.
*
* @covers ::getConnectionInfoAsUrl
*
* @param array $connection_options
* The database connection information.
* @param string $expected_exception_message
* The expected exception message.
*
* @dataProvider providerInvalidArgumentGetConnectionInfoAsUrl
*/
public function testGetInvalidArgumentGetConnectionInfoAsUrl(array $connection_options, $expected_exception_message) {
Database::addConnectionInfo('default', 'default', $connection_options);
$this->setExpectedException(\InvalidArgumentException::class, $expected_exception_message);
$url = Database::getConnectionInfoAsUrl();
}
/**
* Dataprovider for testGetInvalidArgumentGetConnectionInfoAsUrl().
*
* @return array
* Array of arrays with the following elements:
* - An array mocking the database connection info. Possible keys are
* database, username, password, prefix, host, port, namespace and driver.
* - The expected exception message.
*/
public function providerInvalidArgumentGetConnectionInfoAsUrl() {
return [
'Missing database key' => [
[
'driver' => 'sqlite',
'host' => 'localhost',
'namespace' => 'Drupal\Core\Database\Driver\sqlite',
],
"As a minimum, the connection options array must contain at least the 'driver' and 'database' keys",
],
];
}

View File

@ -0,0 +1,70 @@
<?php
namespace Drupal\Driver\Database\fake;
use Drupal\Core\Database\Connection as CoreConnection;
use Drupal\Core\Database\StatementEmpty;
/**
* A fake Connection class for testing purposes.
*/
class Connection extends CoreConnection {
/**
* Public property so we can test driver loading mechanism.
*
* @var string
* @see driver().
*/
public $driver = 'fake';
/**
* {@inheritdoc}
*/
public function queryRange($query, $from, $count, array $args = [], array $options = []) {
return new StatementEmpty();
}
/**
* {@inheritdoc}
*/
public function queryTemporary($query, array $args = [], array $options = []) {
return '';
}
/**
* {@inheritdoc}
*/
public function driver() {
return $this->driver;
}
/**
* {@inheritdoc}
*/
public function databaseType() {
return 'fake';
}
/**
* {@inheritdoc}
*/
public function createDatabase($database) {
return;
}
/**
* {@inheritdoc}
*/
public function mapConditionOperator($operator) {
return NULL;
}
/**
* {@inheritdoc}
*/
public function nextId($existing_id = 0) {
return 0;
}
}