Chris@17: setName('install') Chris@17: ->setDescription('Creates a test Drupal site') Chris@17: ->setHelp('The details to connect to the test site created will be displayed upon success. It will contain the database prefix and the user agent.') Chris@17: ->addOption('setup-file', NULL, InputOption::VALUE_OPTIONAL, 'The path to a PHP file containing a class to setup configuration used by the test, for example, core/tests/Drupal/TestSite/TestSiteInstallTestScript.php.') Chris@17: ->addOption('db-url', NULL, InputOption::VALUE_OPTIONAL, 'URL for database. Defaults to the environment variable SIMPLETEST_DB.', getenv('SIMPLETEST_DB')) Chris@17: ->addOption('base-url', NULL, InputOption::VALUE_OPTIONAL, 'Base URL for site under test. Defaults to the environment variable SIMPLETEST_BASE_URL.', getenv('SIMPLETEST_BASE_URL')) Chris@17: ->addOption('install-profile', NULL, InputOption::VALUE_OPTIONAL, 'Install profile to install the site in. Defaults to testing.', 'testing') Chris@17: ->addOption('langcode', NULL, InputOption::VALUE_OPTIONAL, 'The language to install the site in. Defaults to en.', 'en') Chris@17: ->addOption('json', NULL, InputOption::VALUE_NONE, 'Output test site connection details in JSON.') Chris@17: ->addUsage('--setup-file core/tests/Drupal/TestSite/TestSiteInstallTestScript.php --json') Chris@17: ->addUsage('--install-profile demo_umami --langcode fr') Chris@17: ->addUsage('--base-url "http://example.com" --db-url "mysql://username:password@localhost/databasename#table_prefix"'); Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: protected function execute(InputInterface $input, OutputInterface $output) { Chris@17: // Determines and validates the setup class prior to installing a database Chris@17: // to avoid creating unnecessary sites. Chris@17: $root = dirname(dirname(dirname(dirname(dirname(__DIR__))))); Chris@17: chdir($root); Chris@17: $class_name = $this->getSetupClass($input->getOption('setup-file')); Chris@17: // Ensure we can install a site in the sites/simpletest directory. Chris@17: $this->ensureDirectory($root); Chris@17: Chris@17: $db_url = $input->getOption('db-url'); Chris@17: $base_url = $input->getOption('base-url'); Chris@17: putenv("SIMPLETEST_DB=$db_url"); Chris@17: putenv("SIMPLETEST_BASE_URL=$base_url"); Chris@17: Chris@17: // Manage site fixture. Chris@17: $this->setup($input->getOption('install-profile'), $class_name, $input->getOption('langcode')); Chris@17: Chris@17: $user_agent = drupal_generate_test_ua($this->databasePrefix); Chris@17: if ($input->getOption('json')) { Chris@17: $output->writeln(json_encode([ Chris@17: 'db_prefix' => $this->databasePrefix, Chris@17: 'user_agent' => $user_agent, Chris@17: 'site_path' => $this->siteDirectory, Chris@17: ])); Chris@17: } Chris@17: else { Chris@17: $output->writeln('Successfully installed a test site'); Chris@17: $io = new SymfonyStyle($input, $output); Chris@17: $io->table([], [ Chris@17: ['Database prefix', $this->databasePrefix], Chris@17: ['User agent', $user_agent], Chris@17: ['Site path', $this->siteDirectory], Chris@17: ]); Chris@17: } Chris@17: } Chris@17: Chris@17: /** Chris@17: * Gets the setup class. Chris@17: * Chris@17: * @param string|null $file Chris@17: * The file to get the setup class from. Chris@17: * Chris@17: * @return string|null Chris@17: * The setup class contained in the provided $file. Chris@17: * Chris@17: * @throws \InvalidArgumentException Chris@17: * Thrown if the file does not exist, does not contain a class or the class Chris@17: * does not implement \Drupal\TestSite\TestSetupInterface. Chris@17: */ Chris@17: protected function getSetupClass($file) { Chris@17: if ($file === NULL) { Chris@17: return; Chris@17: } Chris@17: if (!file_exists($file)) { Chris@17: throw new \InvalidArgumentException("The file $file does not exist."); Chris@17: } Chris@17: Chris@17: $classes = get_declared_classes(); Chris@17: include_once $file; Chris@17: $new_classes = array_values(array_diff(get_declared_classes(), $classes)); Chris@17: if (empty($new_classes)) { Chris@17: throw new \InvalidArgumentException("The file $file does not contain a class."); Chris@17: } Chris@17: $class = array_pop($new_classes); Chris@17: Chris@17: if (!is_subclass_of($class, TestSetupInterface::class)) { Chris@17: throw new \InvalidArgumentException("The class $class contained in $file needs to implement \Drupal\TestSite\TestSetupInterface"); Chris@17: } Chris@17: return $class; Chris@17: } Chris@17: Chris@17: /** Chris@17: * Ensures that the sites/simpletest directory exists and is writable. Chris@17: * Chris@17: * @param string $root Chris@17: * The Drupal root. Chris@17: */ Chris@17: protected function ensureDirectory($root) { Chris@17: if (!is_writable($root . '/sites/simpletest')) { Chris@17: if (!@mkdir($root . '/sites/simpletest')) { Chris@17: throw new \RuntimeException($root . '/sites/simpletest must exist and be writable to install a test site'); Chris@17: } Chris@17: } Chris@17: } Chris@17: Chris@17: /** Chris@17: * Creates a test drupal installation. Chris@17: * Chris@17: * @param string $profile Chris@17: * (optional) The installation profile to use. Chris@17: * @param string $setup_class Chris@17: * (optional) Setup class. A PHP class to setup configuration used by the Chris@17: * test. Chris@17: * @param string $langcode Chris@17: * (optional) The language to install the site in. Chris@17: */ Chris@17: public function setup($profile = 'testing', $setup_class = NULL, $langcode = 'en') { Chris@17: $this->profile = $profile; Chris@17: $this->langcode = $langcode; Chris@17: $this->setupBaseUrl(); Chris@17: $this->prepareEnvironment(); Chris@17: $this->installDrupal(); Chris@17: Chris@17: if ($setup_class) { Chris@17: $this->executeSetupClass($setup_class); Chris@17: } Chris@17: } Chris@17: Chris@17: /** Chris@17: * Installs Drupal into the test site. Chris@17: */ Chris@17: protected function installDrupal() { Chris@17: $this->initUserSession(); Chris@17: $this->prepareSettings(); Chris@17: $this->doInstall(); Chris@17: $this->initSettings(); Chris@17: $container = $this->initKernel(\Drupal::request()); Chris@17: $this->initConfig($container); Chris@17: $this->installModulesFromClassProperty($container); Chris@17: $this->rebuildAll(); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Uses the setup file to configure Drupal. Chris@17: * Chris@17: * @param string $class Chris@17: * The fully qualified class name, which should set up Drupal for tests. For Chris@17: * example this class could create content types and fields or install Chris@17: * modules. The class needs to implement TestSetupInterface. Chris@17: * Chris@17: * @see \Drupal\TestSite\TestSetupInterface Chris@17: */ Chris@17: protected function executeSetupClass($class) { Chris@17: /** @var \Drupal\TestSite\TestSetupInterface $instance */ Chris@17: $instance = new $class(); Chris@17: $instance->setup(); Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: protected function installParameters() { Chris@17: $parameters = $this->installParametersTrait(); Chris@17: $parameters['parameters']['langcode'] = $this->langcode; Chris@17: return $parameters; Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: protected function changeDatabasePrefix() { Chris@17: // Ensure that we use the database from SIMPLETEST_DB environment variable. Chris@17: Database::removeConnection('default'); Chris@17: $this->changeDatabasePrefixTrait(); Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: protected function prepareDatabasePrefix() { Chris@17: // Override this method so that we can force a lock to be created. Chris@17: $test_db = new TestDatabase(NULL, TRUE); Chris@17: $this->siteDirectory = $test_db->getTestSitePath(); Chris@17: $this->databasePrefix = $test_db->getDatabasePrefix(); Chris@17: } Chris@17: Chris@17: }