Unverified Commit 61cbaba0 authored by alexpott's avatar alexpott
Browse files

Issue #2605284 by mondrake, david_garcia, GoZ, Jo Fitzgerald, alexpott,...

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
parent f5c6c3b7
......@@ -1472,4 +1472,116 @@ public function __sleep() {
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;
}
}
......@@ -365,13 +365,8 @@ final protected static function openConnection($key, $target) {
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 @@ public static function ignoreTarget($key, $target) {
* 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' => '',
];
// 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'];
$driver = $matches[1];
// 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 @@ public static function convertDbUrlToConnectionInfo($url, $root) {
*
* @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'];
}
if ($db_info['default']['prefix']['default']) {
$db_url .= '#' . $db_info['default']['prefix']['default'];
/**
* 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'];
}
return $db_url;
// Fallback for Drupal 7 settings.php.
return 'Drupal\\Core\\Database\\Driver\\' . $connection_info['driver'];
}
}
......@@ -435,4 +435,50 @@ public function getFullQualifiedTableName($table) {
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;
}
}
......@@ -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 @@
* @group simpletest
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
class SimpletestPhpunitRunCommandTest extends TestCase {
......@@ -85,6 +87,18 @@ public function provideStatusCodes() {
* @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]);
......
......@@ -58,18 +58,16 @@ public function testSpecifyDatabaseDoesNotExist() {
* 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 @@ public function testPrefix() {
]);
$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());
......
......@@ -460,7 +460,7 @@ protected function getDatabaseConnectionInfo() {
// 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,
];
}
}
......
......@@ -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 @@ public function testDbUrltoConnectionConversion($root, $url, $database_array) {
* - 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 @@ public function providerConvertDbUrlToConnectionInfo() {
*
* @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 @@ public function testGetInvalidArgumentExceptionInUrlConversion($url, $root) {
* 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 @@ public function providerGetConnectionInfoAsUrl() {
'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 @@ public function providerGetConnectionInfoAsUrl() {
];
$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],
];
}
/**