TestSiteInstallCommand.php 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. <?php
  2. namespace Drupal\TestSite\Commands;
  3. use Drupal\Core\Database\Database;
  4. use Drupal\Core\Test\FunctionalTestSetupTrait;
  5. use Drupal\Core\Test\TestDatabase;
  6. use Drupal\Core\Test\TestSetupTrait;
  7. use Drupal\TestSite\TestPreinstallInterface;
  8. use Drupal\TestSite\TestSetupInterface;
  9. use Drupal\Tests\RandomGeneratorTrait;
  10. use Symfony\Component\Console\Command\Command;
  11. use Symfony\Component\Console\Input\InputInterface;
  12. use Symfony\Component\Console\Input\InputOption;
  13. use Symfony\Component\Console\Output\OutputInterface;
  14. use Symfony\Component\Console\Style\SymfonyStyle;
  15. /**
  16. * Command to create a test Drupal site.
  17. *
  18. * @internal
  19. */
  20. class TestSiteInstallCommand extends Command {
  21. use FunctionalTestSetupTrait {
  22. installParameters as protected installParametersTrait;
  23. }
  24. use RandomGeneratorTrait;
  25. use TestSetupTrait {
  26. changeDatabasePrefix as protected changeDatabasePrefixTrait;
  27. }
  28. /**
  29. * The install profile to use.
  30. *
  31. * @var string
  32. */
  33. protected $profile = 'testing';
  34. /**
  35. * Time limit in seconds for the test.
  36. *
  37. * Used by \Drupal\Core\Test\FunctionalTestSetupTrait::prepareEnvironment().
  38. *
  39. * @var int
  40. */
  41. protected $timeLimit = 500;
  42. /**
  43. * The database prefix of this test run.
  44. *
  45. * @var string
  46. */
  47. protected $databasePrefix;
  48. /**
  49. * The language to install the site in.
  50. *
  51. * @var string
  52. */
  53. protected $langcode = 'en';
  54. /**
  55. * {@inheritdoc}
  56. */
  57. protected function configure() {
  58. $this->setName('install')
  59. ->setDescription('Creates a test Drupal site')
  60. ->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.')
  61. ->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/TestSiteMultilingualInstallTestScript.php.')
  62. ->addOption('db-url', NULL, InputOption::VALUE_OPTIONAL, 'URL for database. Defaults to the environment variable SIMPLETEST_DB.', getenv('SIMPLETEST_DB'))
  63. ->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'))
  64. ->addOption('install-profile', NULL, InputOption::VALUE_OPTIONAL, 'Install profile to install the site in. Defaults to testing.', 'testing')
  65. ->addOption('langcode', NULL, InputOption::VALUE_OPTIONAL, 'The language to install the site in. Defaults to en.', 'en')
  66. ->addOption('json', NULL, InputOption::VALUE_NONE, 'Output test site connection details in JSON.')
  67. ->addUsage('--setup-file core/tests/Drupal/TestSite/TestSiteMultilingualInstallTestScript.php --json')
  68. ->addUsage('--install-profile demo_umami --langcode fr')
  69. ->addUsage('--base-url "http://example.com" --db-url "mysql://username:password@localhost/databasename#table_prefix"');
  70. }
  71. /**
  72. * {@inheritdoc}
  73. */
  74. protected function execute(InputInterface $input, OutputInterface $output) {
  75. // Determines and validates the setup class prior to installing a database
  76. // to avoid creating unnecessary sites.
  77. $root = dirname(dirname(dirname(dirname(dirname(__DIR__)))));
  78. chdir($root);
  79. $class_name = $this->getSetupClass($input->getOption('setup-file'));
  80. // Ensure we can install a site in the sites/simpletest directory.
  81. $this->ensureDirectory($root);
  82. $db_url = $input->getOption('db-url');
  83. $base_url = $input->getOption('base-url');
  84. putenv("SIMPLETEST_DB=$db_url");
  85. putenv("SIMPLETEST_BASE_URL=$base_url");
  86. // Manage site fixture.
  87. $this->setup($input->getOption('install-profile'), $class_name, $input->getOption('langcode'));
  88. $user_agent = drupal_generate_test_ua($this->databasePrefix);
  89. if ($input->getOption('json')) {
  90. $output->writeln(json_encode([
  91. 'db_prefix' => $this->databasePrefix,
  92. 'user_agent' => $user_agent,
  93. 'site_path' => $this->siteDirectory,
  94. ]));
  95. }
  96. else {
  97. $output->writeln('<info>Successfully installed a test site</info>');
  98. $io = new SymfonyStyle($input, $output);
  99. $io->table([], [
  100. ['Database prefix', $this->databasePrefix],
  101. ['User agent', $user_agent],
  102. ['Site path', $this->siteDirectory],
  103. ]);
  104. }
  105. return 0;
  106. }
  107. /**
  108. * Gets the setup class.
  109. *
  110. * @param string|null $file
  111. * The file to get the setup class from.
  112. *
  113. * @return string|null
  114. * The setup class contained in the provided $file.
  115. *
  116. * @throws \InvalidArgumentException
  117. * Thrown if the file does not exist, does not contain a class or the class
  118. * does not implement \Drupal\TestSite\TestSetupInterface or
  119. * \Drupal\TestSite\TestPreinstallInterface.
  120. */
  121. protected function getSetupClass($file) {
  122. if ($file === NULL) {
  123. return;
  124. }
  125. if (!file_exists($file)) {
  126. throw new \InvalidArgumentException("The file $file does not exist.");
  127. }
  128. $classes = get_declared_classes();
  129. include_once $file;
  130. $new_classes = array_values(array_diff(get_declared_classes(), $classes));
  131. if (empty($new_classes)) {
  132. throw new \InvalidArgumentException("The file $file does not contain a class.");
  133. }
  134. $class = array_pop($new_classes);
  135. if (!is_subclass_of($class, TestSetupInterface::class) && !is_subclass_of($class, TestPreinstallInterface::class)) {
  136. throw new \InvalidArgumentException("The class $class contained in $file needs to implement \Drupal\TestSite\TestSetupInterface or \Drupal\TestSite\TestPreinstallInterface");
  137. }
  138. return $class;
  139. }
  140. /**
  141. * Ensures that the sites/simpletest directory exists and is writable.
  142. *
  143. * @param string $root
  144. * The Drupal root.
  145. */
  146. protected function ensureDirectory($root) {
  147. if (!is_writable($root . '/sites/simpletest')) {
  148. if (!@mkdir($root . '/sites/simpletest')) {
  149. throw new \RuntimeException($root . '/sites/simpletest must exist and be writable to install a test site');
  150. }
  151. }
  152. }
  153. /**
  154. * Creates a test drupal installation.
  155. *
  156. * @param string $profile
  157. * (optional) The installation profile to use.
  158. * @param string $setup_class
  159. * (optional) Setup class. A PHP class to setup configuration used by the
  160. * test.
  161. * @param string $langcode
  162. * (optional) The language to install the site in.
  163. */
  164. public function setup($profile = 'testing', $setup_class = NULL, $langcode = 'en') {
  165. $this->profile = $profile;
  166. $this->langcode = $langcode;
  167. $this->setupBaseUrl();
  168. $this->prepareEnvironment();
  169. $this->executePreinstallClass($setup_class);
  170. $this->installDrupal();
  171. $this->executeSetupClass($setup_class);
  172. }
  173. /**
  174. * Installs Drupal into the test site.
  175. */
  176. protected function installDrupal() {
  177. $this->initUserSession();
  178. $this->prepareSettings();
  179. $this->doInstall();
  180. $this->initSettings();
  181. $container = $this->initKernel(\Drupal::request());
  182. $this->initConfig($container);
  183. }
  184. /**
  185. * Uses the setup file to configure Drupal.
  186. *
  187. * @param string $class
  188. * The fully qualified class name, which should set up Drupal for tests. For
  189. * example this class could create content types and fields or install
  190. * modules. The class needs to implement TestSetupInterface.
  191. *
  192. * @see \Drupal\TestSite\TestSetupInterface
  193. */
  194. protected function executeSetupClass($class) {
  195. if (is_subclass_of($class, TestSetupInterface::class)) {
  196. /** @var \Drupal\TestSite\TestSetupInterface $instance */
  197. $instance = new $class();
  198. $instance->setup();
  199. }
  200. }
  201. /**
  202. * Uses the setup file to configure the environment prior to install.
  203. *
  204. * @param string $class
  205. * The fully qualified class name, which should set up the environment prior
  206. * to installing Drupal for tests. For example this class could create
  207. * translations that are used during the installer.
  208. *
  209. * @see \Drupal\TestSite\TestPreinstallInterface
  210. */
  211. protected function executePreinstallClass($class) {
  212. if (is_subclass_of($class, TestPreinstallInterface::class)) {
  213. /** @var \Drupal\TestSite\TestPreinstallInterface $instance */
  214. $instance = new $class();
  215. $instance->preinstall($this->databasePrefix, $this->siteDirectory);
  216. }
  217. }
  218. /**
  219. * {@inheritdoc}
  220. */
  221. protected function installParameters() {
  222. $parameters = $this->installParametersTrait();
  223. $parameters['parameters']['langcode'] = $this->langcode;
  224. return $parameters;
  225. }
  226. /**
  227. * {@inheritdoc}
  228. */
  229. protected function changeDatabasePrefix() {
  230. // Ensure that we use the database from SIMPLETEST_DB environment variable.
  231. Database::removeConnection('default');
  232. $this->changeDatabasePrefixTrait();
  233. }
  234. /**
  235. * {@inheritdoc}
  236. */
  237. protected function prepareDatabasePrefix() {
  238. // Override this method so that we can force a lock to be created.
  239. $test_db = new TestDatabase(NULL, TRUE);
  240. $this->siteDirectory = $test_db->getTestSitePath();
  241. $this->databasePrefix = $test_db->getDatabasePrefix();
  242. }
  243. }