diff --git a/core/lib/Drupal/Core/Test/TestDatabase.php b/core/lib/Drupal/Core/Test/TestDatabase.php index 9bfa25d77a..42a90c0fd6 100644 --- a/core/lib/Drupal/Core/Test/TestDatabase.php +++ b/core/lib/Drupal/Core/Test/TestDatabase.php @@ -62,13 +62,17 @@ public static function getConnection() { * * @param string|null $db_prefix * If not provided a new test lock is generated. + * @param bool $create_lock + * (optional) Whether or not to create a lock file. Defaults to FALSE. If + * the environment variable RUN_TESTS_CONCURRENCY is greater than 1 it will + * be overridden to TRUE regardless of its initial value. * * @throws \InvalidArgumentException * Thrown when $db_prefix does not match the regular expression. */ - public function __construct($db_prefix = NULL) { + public function __construct($db_prefix = NULL, $create_lock = FALSE) { if ($db_prefix === NULL) { - $this->lockId = $this->getTestLock(); + $this->lockId = $this->getTestLock($create_lock); $this->databasePrefix = 'test' . $this->lockId; } else { @@ -107,31 +111,48 @@ public function getDatabasePrefix() { /** * Generates a unique lock ID for the test method. * + * @param bool $create_lock + * (optional) Whether or not to create a lock file. Defaults to FALSE. + * * @return int * The unique lock ID for the test method. */ - protected function getTestLock() { + protected function getTestLock($create_lock = FALSE) { + // If we're only running with a concurrency of greater than 1 there is a + // risk the random number generated clashing. Therefore we need to create + // a lock. + if (getenv('RUN_TESTS_CONCURRENCY') > 1) { + $create_lock = TRUE; + } + // Ensure that the generated lock ID is not in use, which may happen when // tests are run concurrently. do { $lock_id = mt_rand(10000000, 99999999); - // If we're only running with a concurrency of 1 there's no need to create - // a test lock file as there is no chance of the random number generated - // clashing. - if (getenv('RUN_TESTS_CONCURRENCY') > 1 && @symlink(__FILE__, $this->getLockFile($lock_id)) === FALSE) { + if ($create_lock && @symlink(__FILE__, $this->getLockFile($lock_id)) === FALSE) { $lock_id = NULL; } } while ($lock_id === NULL); return $lock_id; } + /** + * Releases a lock. + * + * @return bool + * TRUE if successful, FALSE if not. + */ + public function releaseLock() { + return unlink($this->getLockFile($this->lockId)); + } + /** * Releases all test locks. * * This should only be called once all the test fixtures have been cleaned up. */ public static function releaseAllTestLocks() { - $tmp = file_directory_os_temp(); + $tmp = FileSystem::getOsTemporaryDirectory(); $dir = dir($tmp); while (($entry = $dir->read()) !== FALSE) { if ($entry === '.' || $entry === '..') { diff --git a/core/scripts/test-site.php b/core/scripts/test-site.php index b86a609d31..d3fb826e82 100644 --- a/core/scripts/test-site.php +++ b/core/scripts/test-site.php @@ -12,9 +12,11 @@ return; } -$autoloader = require __DIR__ . '/../../autoload.php'; -// Access to all the test classes is required as well. +// Use the PHPUnit bootstrap to prime an autoloader that works for test classes. +// Note we have to disable the SYMFONY_DEPRECATIONS_HELPER to ensure deprecation +// notices are not triggered. +putenv('SYMFONY_DEPRECATIONS_HELPER=disabled'); require_once __DIR__ . '/../tests/bootstrap.php'; -$app = new TestSiteApplication($autoloader); +$app = new TestSiteApplication('test-site', '0.1.0'); $app->run(); diff --git a/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php b/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php index 527e77118e..e61d26402a 100644 --- a/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php +++ b/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php @@ -4,6 +4,7 @@ use Drupal\Core\Database\Database; use Drupal\Core\Test\FunctionalTestSetupTrait; +use Drupal\Core\Test\TestDatabase; use Drupal\Core\Test\TestSetupTrait; use Drupal\TestSite\TestSetupInterface; use Drupal\Tests\RandomGeneratorTrait; @@ -217,4 +218,14 @@ protected function changeDatabasePrefix() { $this->changeDatabasePrefixTrait(); } + /** + * {@inheritdoc} + */ + protected function prepareDatabasePrefix() { + // Override this method so that we can force a lock to be created. + $test_db = new TestDatabase(NULL, TRUE); + $this->siteDirectory = $test_db->getTestSitePath(); + $this->databasePrefix = $test_db->getDatabasePrefix(); + } + } diff --git a/core/tests/Drupal/TestSite/Commands/TestSiteReleaseLockCommand.php b/core/tests/Drupal/TestSite/Commands/TestSiteReleaseLockCommand.php new file mode 100644 index 0000000000..b78c55f8d2 --- /dev/null +++ b/core/tests/Drupal/TestSite/Commands/TestSiteReleaseLockCommand.php @@ -0,0 +1,49 @@ +setName('release-lock') + ->setDescription('Releases all test site locks') + ->setHelp('The locks ensure test site database prefixes are not reused.') + ->addArgument('db_prefix', InputArgument::OPTIONAL, 'The database prefix for the test site') + ->addOption('all', NULL, InputOption::VALUE_NONE, 'Use to release all the test locks') + ->addUsage('test12345678') + ->addUsage('--all'); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) { + if ($input->getOption('all')) { + TestDatabase::releaseAllTestLocks(); + } + elseif ($input->getArgument('db_prefix')) { + $test_db = new TestDatabase($input->getArgument('db_prefix')); + $test_db->releaseLock(); + } + else { + throw new \RuntimeException('The release-lock command should be called with a database prefix or --all'); + } + $output->writeln("Successfully released all the test database locks"); + } + +} diff --git a/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php b/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php index 0b7b3afcaa..be4d6bda1a 100644 --- a/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php +++ b/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php @@ -19,28 +19,6 @@ */ class TestSiteTearDownCommand extends Command { - /** - * The used PHP autoloader. - * - * @var object - */ - protected $autoloader; - - /** - * Constructs a new TestSiteTearDownCommand. - * - * @param string $autoloader - * The used PHP autoloader. - * @param string|null $name - * The name of the command. Passing NULL means it must be set in - * configure(). - */ - public function __construct($autoloader, $name = NULL) { - parent::__construct($name); - - $this->autoloader = $autoloader; - } - /** * {@inheritdoc} */ diff --git a/core/tests/Drupal/TestSite/TestSiteApplication.php b/core/tests/Drupal/TestSite/TestSiteApplication.php index 542c580ef4..87a5ca56d3 100644 --- a/core/tests/Drupal/TestSite/TestSiteApplication.php +++ b/core/tests/Drupal/TestSite/TestSiteApplication.php @@ -3,6 +3,7 @@ namespace Drupal\TestSite; use Drupal\TestSite\Commands\TestSiteInstallCommand; +use Drupal\TestSite\Commands\TestSiteReleaseLockCommand; use Drupal\TestSite\Commands\TestSiteTearDownCommand; use Symfony\Component\Console\Application; @@ -16,24 +17,6 @@ */ class TestSiteApplication extends Application { - /** - * The used PHP autoloader. - * - * @var object - */ - protected $autoloader; - - /** - * TestSiteApplication constructor. - * - * @param object $autoloader - * The used PHP autoloader. - */ - public function __construct($autoloader) { - $this->autoloader = $autoloader; - parent::__construct('test-site', '0.1.0'); - } - /** * {@inheritdoc} */ @@ -41,7 +24,8 @@ protected function getDefaultCommands() { // Even though this is a single command, keep the HelpCommand (--help). $default_commands = parent::getDefaultCommands(); $default_commands[] = new TestSiteInstallCommand(); - $default_commands[] = new TestSiteTearDownCommand($this->autoloader); + $default_commands[] = new TestSiteTearDownCommand(); + $default_commands[] = new TestSiteReleaseLockCommand(); return $default_commands; } diff --git a/core/tests/Drupal/Tests/Scripts/TestSiteApplicationTest.php b/core/tests/Drupal/Tests/Scripts/TestSiteApplicationTest.php index 657dba615d..d1df6dcff1 100644 --- a/core/tests/Drupal/Tests/Scripts/TestSiteApplicationTest.php +++ b/core/tests/Drupal/Tests/Scripts/TestSiteApplicationTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\Scripts; +use Drupal\Component\FileSystem\FileSystem; use Drupal\Core\Database\Database; use Drupal\Core\Test\TestDatabase; use Drupal\TestSite\TestSiteInstallTestScript; @@ -85,10 +86,10 @@ public function testInstallScript() { $this->markTestSkipped("Requires the directory $simpletest_path to exist and be writable"); } $php_binary_finder = new PhpExecutableFinder(); - $php_inary_path = $php_binary_finder->find(); + $php_binary_path = $php_binary_finder->find(); // Install a site using the JSON output. - $command_line = $php_inary_path . ' core/scripts/test-site.php install --json --setup_class "' . TestSiteInstallTestScript::class . '" --db_url "' . getenv('SIMPLETEST_DB') . '"'; + $command_line = $php_binary_path . ' core/scripts/test-site.php install --json --setup_class "' . TestSiteInstallTestScript::class . '" --db_url "' . getenv('SIMPLETEST_DB') . '"'; $process = new Process($command_line, $this->root); // Set the timeout to a value that allows debugging. $process->setTimeout(500); @@ -115,9 +116,12 @@ public function testInstallScript() { $test_file = $this->root . DIRECTORY_SEPARATOR . $test_database->getTestSitePath() . DIRECTORY_SEPARATOR . '.htkey'; $this->assertFileExists($test_file); + // Ensure the lock file exists. + $this->assertFileExists($this->getTestLockFile($db_prefix)); + // Install another site so we can ensure tear down only removes one site at // a time. Use the regular output. - $command_line = $php_inary_path . ' core/scripts/test-site.php install --setup_class "' . TestSiteInstallTestScript::class . '" --db_url "' . getenv('SIMPLETEST_DB') . '"'; + $command_line = $php_binary_path . ' core/scripts/test-site.php install --setup_class "' . TestSiteInstallTestScript::class . '" --db_url "' . getenv('SIMPLETEST_DB') . '"'; $process = new Process($command_line, $this->root); // Set the timeout to a value that allows debugging. $process->setTimeout(500); @@ -131,8 +135,11 @@ public function testInstallScript() { $other_key = $this->addTestDatabase($other_db_prefix); $this->assertGreaterThan(0, count(Database::getConnection('default', $other_key)->schema()->findTables('%'))); + // Ensure the lock file exists for the new install. + $this->assertFileExists($this->getTestLockFile($other_db_prefix)); + // Now test the tear down process as well. - $command_line = $php_inary_path . ' core/scripts/test-site.php tear-down ' . $db_prefix . ' --db_url "' . getenv('SIMPLETEST_DB') . '"'; + $command_line = $php_binary_path . ' core/scripts/test-site.php tear-down ' . $db_prefix . ' --db_url "' . getenv('SIMPLETEST_DB') . '"'; $process = new Process($command_line, $this->root); // Set the timeout to a value that allows debugging. $process->setTimeout(500); @@ -154,7 +161,7 @@ public function testInstallScript() { // site is broken. Prove this by removing its settings.php. $test_site_settings = $this->root . DIRECTORY_SEPARATOR . $test_database->getTestSitePath() . DIRECTORY_SEPARATOR . 'settings.php'; $this->assertTrue(unlink($test_site_settings)); - $command_line = $php_inary_path . ' core/scripts/test-site.php tear-down ' . $other_db_prefix . ' --db_url "' . getenv('SIMPLETEST_DB') . '"'; + $command_line = $php_binary_path . ' core/scripts/test-site.php tear-down ' . $other_db_prefix . ' --db_url "' . getenv('SIMPLETEST_DB') . '"'; $process = new Process($command_line, $this->root); // Set the timeout to a value that allows debugging. $process->setTimeout(500); @@ -165,6 +172,22 @@ public function testInstallScript() { // Ensure that all the tables and files for this DB prefix are gone. $this->assertCount(0, Database::getConnection('default', $other_key)->schema()->findTables('%')); $this->assertFileNotExists($test_file); + + // The locks should still exist. + $this->assertFileExists($this->getTestLockFile($db_prefix)); + $this->assertFileExists($this->getTestLockFile($other_db_prefix)); + + // Test releasing a lock. We can't test releasing all locks because this + // would affect the DrupalCI test run. + $command_line = $php_binary_path . ' core/scripts/test-site.php release-lock ' . $db_prefix; + $process = new Process($command_line, $this->root); + $process->run(); + $this->assertFileNotExists($this->getTestLockFile($db_prefix)); + $this->assertFileExists($this->getTestLockFile($other_db_prefix)); + $command_line = $php_binary_path . ' core/scripts/test-site.php release-lock ' . $other_db_prefix; + $process = new Process($command_line, $this->root); + $process->run(); + $this->assertFileNotExists($this->getTestLockFile($other_db_prefix)); } /** @@ -204,6 +227,14 @@ public function testInstallInDifferentLanguage() { // Ensure that all the tables for this DB prefix are gone. $this->assertCount(0, Database::getConnection('default', $this->addTestDatabase($db_prefix))->schema()->findTables('%')); + + // Clean up the test site lock. + $this->assertFileExists($this->getTestLockFile($db_prefix)); + $command_line = $php_binary_path . ' core/scripts/test-site.php release-lock ' . $db_prefix; + $process = new Process($command_line, $this->root); + $process->run(); + $this->assertSame(0, $process->getExitCode()); + $this->assertFileNotExists($this->getTestLockFile($db_prefix)); } /** @@ -221,6 +252,20 @@ public function testTearDownDbPrefixValidation() { $this->assertContains('Invalid database prefix: not-a-valid-prefix', $process->getErrorOutput()); } + /** + * @coversNothing + */ + public function testReleaseLockValidation() { + $php_binary_finder = new PhpExecutableFinder(); + $php_binary_path = $php_binary_finder->find(); + + $command_line = $php_binary_path . ' core/scripts/test-site.php release-lock'; + $process = new Process($command_line, $this->root); + $process->run(); + $this->assertSame(1, $process->getExitCode()); + $this->assertContains('The release-lock command should be called with a database prefix or --all', $process->getErrorOutput()); + } + /** * Adds the installed test site to the database connection info. * @@ -238,4 +283,18 @@ protected function addTestDatabase($db_prefix) { return $target; } + /** + * Gets the lock file path. + * + * @param string $db_prefix + * The prefix of the installed test site. + * + * @return string + * The lock file path. + */ + protected function getTestLockFile($db_prefix) { + $lock_id = str_replace('test', '', $db_prefix); + return FileSystem::getOsTemporaryDirectory() . '/test_' . $lock_id; + } + }