diff --git a/core/drupalci.yml b/core/drupalci.yml index 2085b9737b..dce85e6e2c 100644 --- a/core/drupalci.yml +++ b/core/drupalci.yml @@ -39,6 +39,11 @@ build: testgroups: '--all' suppress-deprecations: false halt-on-fail: false + run_tests.build: + types: 'PHPUnit-Build' + testgroups: '--all' + suppress-deprecations: false + halt-on-fail: false run_tests.javascript: concurrency: 15 types: 'PHPUnit-FunctionalJavascript' diff --git a/core/lib/Drupal/Component/FileSystem/nbproject/private/phpcsmd.xml b/core/lib/Drupal/Component/FileSystem/nbproject/private/phpcsmd.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/tests/Drupal/BuildTests/Dependencies/ComponentDependencyBoundsTest.php b/core/tests/Drupal/BuildTests/Dependencies/ComponentDependencyBoundsTest.php new file mode 100644 index 0000000000..45e6605401 --- /dev/null +++ b/core/tests/Drupal/BuildTests/Dependencies/ComponentDependencyBoundsTest.php @@ -0,0 +1,195 @@ +copyCodebase([], 'drupal'); + + // Copy the component tests out of the codebase. + $fs->mkdir($this->getWorkspaceDirectory() . '/workspace/core/tests/Drupal/Tests'); + $fs->mirror( + $this->getWorkspaceDirectory() . '/drupal/core/tests/Drupal/Tests/Component', + $this->getWorkspaceDirectory() . '/workspace/core/tests/Drupal/Tests' + ); + + // Copy over the fixture files. + // @todo Use the Composer scaffold plugin to do this. + $files = [ + 'bootstrap.php.fixture' => 'bootstrap.php', + 'phpunit.xml' => 'phpunit.xml', + ]; + foreach ($files as $source => $destination) { + $fs->copy( + __DIR__ . '/fixtures/' . $source, + $this->getWorkspaceDirectory() . '/workspace/' . $destination, + TRUE + ); + } + } + + /** + * Provider for testBounds(). + * + * Some components are not provided here because they fail. Fixes are + * available in https://www.drupal.org/project/drupal/issues/2876669. + * + * @see https://www.drupal.org/project/drupal/issues/2876669 + */ + public function componentsProvider() { + return [ + // ['package/name' , 'Group'], + ['drupal/core-assertion', 'Assertion'], + ['drupal/core-diff', 'Diff'], + ['drupal/core-http-foundation', 'HttpFoundation'], + ['drupal/core-proxy-builder', 'ProxyBuilder'], + ['drupal/core-render', 'Render'], + ]; + } + + public function allTheComponents() { + return [ + // ['package/name' , 'Group'], + ['drupal/core-annotation', 'Annotation'], + ['drupal/core-assertion', 'Assertion'], + ['drupal/core-bridge', 'Bridge'], + ['drupal/core-class-finder', 'ClassFinder'], + ['drupal/core-datetime', 'Datetime'], + ['drupal/core-dependency-injection', 'DependencyInjection'], + ['drupal/core-diff', 'Diff'], + ['drupal/core-discovery', 'Discovery'], + ['drupal/core-event-dispatcher', 'EventDispatcher'], + ['drupal/core-file-cache', 'FileCache'], + ['drupal/core-file-system', 'FileSystem'], + ['drupal/core-gettext', 'Gettext'], + ['drupal/core-graph', 'Graph'], + ['drupal/core-http-foundation', 'HttpFoundation'], + ['drupal/core-php-storage', 'PhpStorage'], + ['drupal/core-plugin', 'Plugin'], + ['drupal/core-proxy-builder', 'ProxyBuilder'], + ['drupal/core-render', 'Render'], + ['drupal/core-serialization', 'Serialization'], + ['drupal/core-transliteration', 'Transliteration'], + ['drupal/core-utility' , 'Utility'], + ['drupal/core-uuid', 'Uuid'], + ]; + } + + /** + * @dataProvider componentsProvider + */ + public function testBounds($package, $group) { + $fs = new Filesystem(); + $path = 'core/lib/Drupal/Component/' . $group; + + // Move the component to the workspace. + $fs->mirror( + $this->getWorkspaceDirectory() . '/drupal/' . $path, + $this->getWorkspaceDirectory() . '/workspace/component' + ); + + // Write out a composer.json to the workspace. + file_put_contents( + $this->getWorkspaceDirectory() . '/workspace/composer.json', + json_encode( + $this->buildComposerPackageArray($package, $group, 'component'), + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES + ) + ); + + // 'Priming' required because we use wikimedia merge plugin. + $this->assertCommand('composer install --no-progress --no-suggest --prefer-dist', 'workspace'); + + // Run the tests after updating to both the highest and lowest possible + // dependency constraint. + foreach ([' ', '--prefer-lowest'] as $argument) { + // Perform an update. + $this->assertCommand('composer update --no-progress --no-suggest --prefer-dist ' . $argument, 'workspace'); + $this->assertCommand('composer run-script drupal-phpunit-upgrade', 'workspace'); + // Run the tests. + $this->assertCommand('./vendor/bin/phpunit core/tests/Drupal/Tests/' . $group, 'workspace'); + } + } + + public function buildComposerPackageArray($component_package, $group, $path) { + $package = [ + 'name' => 'drupal_component/test_package', + // @todo: Use VCS tags for versioning. + 'version' => '8.5.0', + 'description' => 'Dummy package for the component.', + 'license' => 'GPL-2.0-or-later', + 'minimum-stability' => 'dev', + 'prefer-stable' => TRUE, + 'require' => [ + 'phpunit/phpunit' => '^4.8.35 || ^6.1', + 'wikimedia/composer-merge-plugin' => '^1.4', + ], + 'replace' => [ + $component_package => 'self.version', + ], + 'extra' => [ + 'merge-plugin' => [ + 'require' => [ + "$path/composer.json", + ], + 'ignore-duplicates' => FALSE, + 'merge-dev' => TRUE, + 'merge-extra' => FALSE, + 'merge-scripts' => FALSE, + 'recurse' => FALSE, + 'replace' => FALSE, + ], + ], + 'autoload-dev' => [ + 'psr-4' => [ + "DrupalComponentTester\\" => 'src', + "Drupal\\Tests\\Component\\$group\\" => "core/tests/Drupal/Tests/Component/$group", + ], + ], + 'repositories' => [ + [ + 'type' => 'composer', + 'url' => 'https://packages.drupal.org/8', + ], + ], + 'scripts' => [ + 'drupal-phpunit-upgrade' => '@composer update phpunit/phpunit --with-dependencies --no-progress', + ], + ]; + + // Set up the repositories so that we can require existing components. + $repositories = []; + $components_directory = $this->getWorkspaceDirectory() . '/drupal/core/lib/Component'; + $packages = $this->allTheComponents(); + foreach ($packages as $file_package) { + $repositories[] = [ + 'type' => 'path', + 'url' => $components_directory . '/' . $file_package[1], + ]; + } + + $package['repositories'] = array_merge($package['repositories'], $repositories); + + return $package; + } + +} diff --git a/core/tests/Drupal/BuildTests/Dependencies/CoreDependencyRegressionTest.php b/core/tests/Drupal/BuildTests/Dependencies/CoreDependencyRegressionTest.php new file mode 100644 index 0000000000..a01f38d845 --- /dev/null +++ b/core/tests/Drupal/BuildTests/Dependencies/CoreDependencyRegressionTest.php @@ -0,0 +1,53 @@ +copyCodebase(); + // Install using composer. We have to do this because subsequent update + // commands will not work if we haven't already installed the merge plugin. + $this->assertCommandErrorOutputContains( + 'Generating autoload files', 'composer install --no-interaction' + ); + // Remove drupal/coder. It's usually been modified in some way, so we use + // the filesystem to remove it. Otherwise Composer will balk. + $fs->remove($this->getWorkspaceDirectory() . '/vendor/drupal/coder'); + + // Downgrade to minimum versions. + $this->assertCommandErrorOutputContains('Generating autoload files', 'composer update --prefer-lowest --no-interaction'); + // Perform the PHPUnit upgrade. + $this->assertCommand('composer run-script drupal-phpunit-upgrade'); + // Run unit tests. + $this->assertCommand('./vendor/bin/phpunit -c core/ --testsuite unit'); + + // Remove drupal/coder again. + $fs->remove($this->getWorkspaceDirectory() . '/vendor/drupal/coder'); + // Update to maximum versions. + $this->assertCommandErrorOutputContains('Generating autoload files', 'composer update --no-interaction'); + // Run unit tests. + $this->assertCommand('./vendor/bin/phpunit -c core/ --testsuite unit'); + } + +} diff --git a/core/tests/Drupal/BuildTests/Dependencies/fixtures/bootstrap.php.fixture b/core/tests/Drupal/BuildTests/Dependencies/fixtures/bootstrap.php.fixture new file mode 100644 index 0000000000..fe2243afef --- /dev/null +++ b/core/tests/Drupal/BuildTests/Dependencies/fixtures/bootstrap.php.fixture @@ -0,0 +1,6 @@ + + + + + + + + + + diff --git a/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php new file mode 100644 index 0000000000..d7c93bc492 --- /dev/null +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php @@ -0,0 +1,346 @@ +phpFinder = new PhpExecutableFinder(); + // Set up the workspace directory. + // @todo Glean working directory from env vars, etc. + $this->workspaceDir = implode(DIRECTORY_SEPARATOR, [ + sys_get_temp_dir(), + 'build_workspace_' . md5($this->getName() . microtime()), + ]); + $fs = new Filesystem(); + $fs->mkdir($this->workspaceDir); + $this->assertFileExists($this->workspaceDir); + $this->initMink(); + } + + /** + * {@inheritdoc} + */ + protected function tearDown() { + parent::tearDown(); + + $fs = new Filesystem(); + $ws = $this->getWorkspaceDirectory(); + if ($fs->exists($ws)) { + $fs->chmod($ws, 0775, 0000, TRUE); + $fs->remove($ws); + } + + $this->stopServer(); + } + + protected function initMink() { + $client = new DrupalMinkClient(); + $client->followMetaRefresh(TRUE); + $driver = new GoutteDriver($client); + $session = new Session($driver); + $this->mink = new Mink(); + $this->mink->registerSession('default', $session); + $this->mink->setDefaultSessionName('default'); + $session->start(); + return $session; + } + + /** + * {@inheritdoc} + */ + public function getWorkspaceDirectory() { + return $this->workspaceDir; + } + + /** + * {@inheritdoc} + */ + public function assertCommandErrorOutputContains($expected, $command_line, $working_dir = NULL) { + $process = $this->assertCommand($command_line, $working_dir); + $this->assertContains($expected, $process->getErrorOutput()); + return $process; + } + + /** + * {@inheritdoc} + */ + public function assertCommandOutputContains($expected, $command_line, $working_dir = NULL) { + $process = $this->assertCommand($command_line, $working_dir); + $this->assertContains($expected, $process->getOutput()); + return $process; + } + + /** + * {@inheritdoc} + */ + public function assertCommand($command_line, $working_dir = NULL) { + $this->assertNotEmpty($command_line); + $process = NULL; + try { + $process = $this->executeCommand($command_line, $working_dir); + } + catch (\Exception $ex) { + $this->fail('Process failed with exit code: ' . $process->getExitCode() . ' Message: ' . $ex->getMessage()); + } + $this->assertEquals(0, $process->getExitCode(), + 'COMMAND: ' . $command_line . "\n" . + 'OUTPUT: ' . $process->getOutput() . "\n" . + 'ERROR: ' . $process->getErrorOutput() . "\n" + ); + return $process; + } + + /** + * {@inheritdoc} + */ + public function executeCommand($command_line, $working_dir = NULL) { + $working_path = implode('/', [$this->workspaceDir, $working_dir]); + $process = new Process($command_line); + $process->setWorkingDirectory($working_path) + ->setTimeout(300) + ->setIdleTimeout(300); + // Synchronously run the process. + $process->run(); + return $process; + } + + /** + * {@inheritdoc} + */ + public function assertRequest($request_uri, $expected_status_code = 200, $docroot = NULL) { + $this->visit($request_uri, $docroot); + $this->mink->assertSession()->statusCodeEquals($expected_status_code); + } + + /** + * {@inheritdoc} + */ + public function visit($request_uri, $docroot = NULL) { + // Try to make a server. + $this->standUpServer($docroot); + $this->assertTrue($this->serverProcess->isRunning(), 'PHP HTTP server not running.'); + + $request = 'http://localhost:' . $this->getPortNumber() . '/' . $request_uri; + $this->mink->getSession()->visit($request); + } + + /** + * Make a local test server using PHP's internal HTTP server. + * + * @param string|null $docroot + * (optional) Server docroot relative to the workspace filesystem. Defaults + * to the workspace directory. + */ + protected function standUpServer($docroot = NULL) { + // If the user wants to test a new docroot, we have to shut down the old + // server process and generate a new port number. + if ($docroot !== $this->serverDocroot && !empty($this->serverProcess)) { + $this->stopServer(); + } + // If there's not a server at this point, make one. + if (empty($this->serverProcess)) { + $this->serverProcess = $this->instantiateServer(static::$hostName, $this->getPortNumber(), $docroot); + if (!empty($this->serverProcess)) { + $this->serverDocroot = $docroot; + } + } + } + + /** + * Do the work of making a server process. + * + * @param string $hostname + * The hostname for the server. + * @param int $port + * The port number for the server. + * @param string|null $docroot + * Server docroot relative to the workspace filesystem. Defaults to the + * workspace directory. + * @param int $sleep_time + * (optional) The time to sleep in seconds after creating the process. This + * allows the server to come to life. Might also be voodoo. Defaults to 10. + * + * @return \Symfony\Component\Process\Process + * The server process. + */ + protected function instantiateServer($hostname, $port, $docroot, $sleep_time = 10) { + // Use implode because $docroot can be NULL. + $full_docroot = implode('/', [$this->getWorkspaceDirectory(), $docroot]); + $server = [ + $this->phpFinder->find(), + '-S', + $hostname . ':' . $port, + '-t', + $full_docroot, + ]; + $ps = new Process(implode(' ', $server), $full_docroot); + $ps->setIdleTimeout(30) + ->setTimeout(30) + ->start(); + // @todo Tweak the sleep, or find a better way to wait for this process + // to fully start up. + sleep($sleep_time); + return $ps; + } + + /** + * Stop the HTTP server, zero out all necessary variables. + */ + protected function stopServer() { + if (!empty($this->serverProcess)) { + $this->serverProcess->stop(); + } + $this->serverProcess = NULL; + $this->serverDocroot = NULL; + $this->hostPort = NULL; + $this->initMink(); + } + + /** + * Discover an available port number. + * + * @param int $min + * The lowest port number to start with. + * @param int $max + * The highest port number to allow. + * + * @return int + * The available port number that we discovered. + * + * @throws \RuntimeException + * Thrown when there are no available ports within the range. + */ + protected static function findBestPort($min = 8000, $max = 8100) { + $port = $min; + // If we can open the port, then it's unavailable to us. + while (($fp = @fsockopen(static::$hostName, $port, $errno, $errstr, 1)) !== FALSE) { + fclose($fp); + if (++$port > $max) { + throw new \RuntimeException('Unable to find a port available to run the web server.'); + } + } + return $port; + } + + /** + * Get the port number for requests. + * + * Test should never call this. Used by standUpServer(). + * + * @return int + */ + protected function getPortNumber() { + if (empty($this->hostPort)) { + $this->hostPort = static::findBestPort(); + } + return $this->hostPort; + } + + /** + * {@inheritdoc} + */ + public function copyCodebase($options = [], $working_dir = NULL) { + $options = array_merge(['override' => TRUE, 'delete' => TRUE], $options); + $full_working = implode('/', [$this->getWorkspaceDirectory(), $working_dir]); + + $fs = new Filesystem(); + $fs->mirror($this->getDrupalRoot(), $full_working, NULL, $options); + } + + protected function getDrupalRoot() { + return realpath(dirname(__DIR__, 5)); + } + +} diff --git a/core/tests/Drupal/BuildTests/Framework/BuildTestInterface.php b/core/tests/Drupal/BuildTests/Framework/BuildTestInterface.php new file mode 100644 index 0000000000..e358330879 --- /dev/null +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestInterface.php @@ -0,0 +1,172 @@ +mink to get session and assertion objects to test the result + * of this method. + * + * @param string $request_uri + * The non-host part of the URL to request. Example: some/path?foo=bar. + * @param int $expected_status_code + * (optional) Expected HTTP status code for this request. Defaults to 200. + * @param string $docroot + * (optional) Relative path within the test workspace file system that will + * be the docroot for the request. Defaults to the workspace directory. + */ + public function assertRequest($request_uri, $expected_status_code = 200, $docroot = NULL); + + /** + * Visit a URI on the HTTP server. + * + * The concept here is that there could be multiple potential docroots in the + * workspace, so you can use whichever ones you want. + * + * Use $this->mink to get session and assertion objects to test the outcome + * of this method. + * + * @param string $request_uri + * The non-host part of the URL to request. Example: some/path?foo=bar. + * @param string $docroot + * (optional) Relative path within the test workspace file system that will + * be the docroot for the request. Defaults to the workspace directory. + */ + public function visit($request_uri, $docroot = NULL); + + /** + * Copy the current working codebase into a workspace. + * + * Use this method to copy the current codebase, including any patched + * changes, into the workspace. + * + * To specify a working directory but keep default options, pass an empty + * array for options. + * + * @param bool[] $options + * (optional) Array of file handling options. Pass in an empty array for + * defaults. Valid options are: + * - $options['override'] If true, target files newer than origin files are + * overwritten. Defaults to TRUE. + * - $options['delete'] Whether to delete files that are not in the source + * directory. Defaults to TRUE. + * @param string|null $working_dir + * (optional) Relative path within the test workspace file system that will + * contain the copy of the codebase. Defaults to the workspace directory. + */ + public function copyCodebase($options = [], $working_dir = NULL); + +} diff --git a/core/tests/Drupal/BuildTests/Framework/DrupalMinkClient.php b/core/tests/Drupal/BuildTests/Framework/DrupalMinkClient.php new file mode 100644 index 0000000000..527349766f --- /dev/null +++ b/core/tests/Drupal/BuildTests/Framework/DrupalMinkClient.php @@ -0,0 +1,65 @@ +followMetaRefresh = $followMetaRefresh; + } + + /** + * Glean the meta refresh URL from the current page content. + * + * @return string|null + * Either the redirect URL that was found, or NULL if none was found. + */ + private function getMetaRefreshUrl() { + $metaRefresh = $this->getCrawler()->filter('meta[http-equiv="Refresh"], meta[http-equiv="refresh"]'); + foreach ($metaRefresh->extract(array('content')) as $content) { + if (preg_match('/^\s*0\s*;\s*URL\s*=\s*(?|\'([^\']++)|"([^"]++)|([^\'"].*))/i', $content, $m)) { + return str_replace("\t\r\n", '', rtrim($m[1])); + } + } + return null; + } + + /** + * {@inheritdoc} + */ + public function request($method, $uri, array $parameters = array(), array $files = array(), array $server = array(), $content = null, $changeHistory = true) { + $this->crawler = parent::request($method, $uri, $parameters, $files, $server, $content, $changeHistory); + // Check for meta refresh redirect and follow it. + if ($this->followMetaRefresh && null !== $redirect = $this->getMetaRefreshUrl()) { + $this->redirect = $redirect; + $this->redirects[serialize($this->history->current())] = true; + $this->crawler = $this->followRedirect(); + } + return $this->crawler; + } + +} diff --git a/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php b/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php new file mode 100644 index 0000000000..c61c886bd9 --- /dev/null +++ b/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php @@ -0,0 +1,82 @@ +executeCommand('test -f composer.json'); + $this->assertEquals(1, $process->getExitCode()); + + // We could assertCommand() and build our workspace, but we'll use code + // here, so that this test doesn't only run on *nix. + $workspace = $this->getWorkspaceDirectory(); + $test_directory = 'test_directory'; + $working_dir = $workspace . '/' . $test_directory; + + $fs = new Filesystem(); + $fs->mkdir($working_dir); + $this->assertFileExists($working_dir); + + // Load up a process with our expecations. assertCommand() would fail a call + // with an empty command line, but executeCommand() does not. + $process = $this->executeCommand('', $test_directory); + + $working_dir = $process->getWorkingDirectory(); + $this->assertNotEquals($test_directory, $working_dir); + $this->assertEquals($test_directory, basename($working_dir)); + } + + public function testCopyCodebase() { + $test_directory = 'copied_codebase'; + $this->copyCodebase([], $test_directory); + $full_path = $this->getWorkspaceDirectory() . '/' . $test_directory; + $files = [ + 'autoload.php', + 'composer.json', + 'index.php', + 'README.txt', + ]; + foreach ($files as $file) { + $this->assertFileExists($full_path . '/' . $file); + } + } + + public function testPort() { + // We should find the same port twice in a row, since we don't use it. + $this->assertEquals(static::findBestPort(), static::findBestPort()); + + // Grab a port, make a server, make sure the next port we grab is not the + // same. + $port = static::findBestPort(); + $process = $this->instantiateServer(static::$hostName, $port, NULL); + $this->assertNotEquals($port, static::findBestPort()); + } + + public function testPortMany() { + $processes = []; + foreach(range(1,15) as $instance) { + $port = static::findBestPort(); + $this->assertArrayNotHasKey($port, $processes, 'Port ' . $port . ' was already in use by a process.'); + $this->assertNotEmpty( + $processes[$port] = $this->instantiateServer(static::$hostName, $port, NULL, 1) + ); + } + foreach ($processes as $port => $process) { + $this->assertTrue($process->isRunning(), 'Process on port ' . $port . ' is not still running.'); + } + } + +} diff --git a/core/tests/Drupal/BuildTests/Framework/Tests/DrupalMinkClientTest.php b/core/tests/Drupal/BuildTests/Framework/Tests/DrupalMinkClientTest.php new file mode 100644 index 0000000000..1251185012 --- /dev/null +++ b/core/tests/Drupal/BuildTests/Framework/Tests/DrupalMinkClientTest.php @@ -0,0 +1,105 @@ +followMetaRefresh($followMetaRefresh); + $client->setNextResponse(new Response($content)); + $client->request('GET', 'http://www.example.com/foo/foobar'); + $this->assertEquals($expectedEndingUrl, $client->getRequest()->getUri()); + } + + public function getTestsForMetaRefresh() { + return [ + ['', 'http://www.example.com/redirected'], + ['', 'http://www.example.com/redirected'], + ['', 'http://www.example.com/redirected'], + ['', 'http://www.example.com/redirected'], + ['', 'http://www.example.com/redirected'], + ['', 'http://www.example.com/redirected'], + ['', 'http://www.example.com/redirected'], + ['', 'http://www.example.com/redirected'], + // Non-zero timeout should not result in a redirect. + ['', 'http://www.example.com/foo/foobar'], + ['', 'http://www.example.com/foo/foobar'], + // HTML 5 allows the meta tag to be placed in head or body. + ['', 'http://www.example.com/redirected'], + // Valid meta refresh should not be followed if disabled. + ['', 'http://www.example.com/foo/foobar', false], + 'drupal-1' => ['', 'http://www.example.com/update.php/start?id=2&op=do_nojs'], + 'drupal-2' => ['', 'http://www.example.com/update.php/start?id=2&op=do_nojs'], + ]; + } + +} + +class TestClient extends DrupalMinkClient { + + protected $nextResponse = null; + protected $nextScript = null; + + public function setNextResponse(Response $response) { + $this->nextResponse = $response; + } + + public function setNextScript($script) { + $this->nextScript = $script; + } + + protected function doRequest($request) { + if (null === $this->nextResponse) { + return new Response(); + } + + $response = $this->nextResponse; + $this->nextResponse = null; + + return $response; + } + + protected function filterResponse($response) { + if ($response instanceof SpecialResponse) { + return new Response($response->getContent(), $response->getStatus(), $response->getHeaders()); + } + + return $response; + } + + protected function getScript($request) { + $r = new \ReflectionClass('Symfony\Component\BrowserKit\Response'); + $path = $r->getFileName(); + + return <<nextScript); +EOF; + } + +} + +class SpecialResponse extends Response { + +} diff --git a/core/tests/Drupal/BuildTests/QuickStart/InstallTest.php b/core/tests/Drupal/BuildTests/QuickStart/InstallTest.php new file mode 100644 index 0000000000..df23e1b0a1 --- /dev/null +++ b/core/tests/Drupal/BuildTests/QuickStart/InstallTest.php @@ -0,0 +1,63 @@ + ['standard'], + 'minimal' => ['minimal'], + 'demo_umami' => ['demo_umami'], + ]; + } + + /** + * @dataProvider providerProfile + */ + public function testInstall($profile) { + // Get the codebase. + $this->copyCodebase(); + + // We have to remove some files from the copy. + $remove_these = [ + // Remove drupal/coder from the live site. We have to do this because + // drupal/coder is often modified and Composer complains. + $this->getWorkspaceDirectory() . '/vendor/drupal/coder', + // Remove settings. + $this->getWorkspaceDirectory() . '/sites/default/settings.php', + $this->getWorkspaceDirectory() . '/sites/default/files', + ]; + $fs = new Filesystem(); + $fs->remove($remove_these); + + // Composer tells you stuff in error output. + $this->assertCommandErrorOutputContains('Generating autoload files', 'composer install --no-dev --no-interaction'); + $this->installQuickStart($profile); + + // Visit paths with expectations. + $this->assertRequest('', 200); + $assert = $this->mink->assertSession(); + $assert->responseHeaderEquals('X-Generator', 'Drupal 8 (https://www.drupal.org)'); + $assert->elementExists('css', 'html'); + + $this->assertRequest('does-not-exist', 404); + + $this->assertRequest('admin', 403); + $this->formLogin($this->adminUsername, $this->adminPassword); + $this->assertRequest('admin', 200); + + $this->assertRequest('user/logout', 200); + $this->assertRequest('admin', 403); + } + +} diff --git a/core/tests/Drupal/BuildTests/QuickStart/QuickStartTestBase.php b/core/tests/Drupal/BuildTests/QuickStart/QuickStartTestBase.php new file mode 100644 index 0000000000..c4cfb4a6cb --- /dev/null +++ b/core/tests/Drupal/BuildTests/QuickStart/QuickStartTestBase.php @@ -0,0 +1,43 @@ +assertCommandOutputContains( + 'Username:', + $this->phpFinder->find() . ' ./core/scripts/drupal install ' . $profile, + $working_dir + ); + preg_match('/Username: (.+)\vPassword: (.+)/', $install_process->getOutput(), $matches); + $this->assertNotEmpty($this->adminUsername = $matches[1]); + $this->assertNotEmpty($this->adminPassword = $matches[2]); + } + + /** + * Helper that uses Drupal's user/login form to log in. + * + * @param string $username + * Username. + * @param string $password + * Password. + * @param string $working_dir + * (optional) A working directory within which to login. Defaults to the + * workspace directory. + */ + public function formLogin($username, $password, $working_dir = NULL) { + $this->assertRequest('user/login', 200, $working_dir); + $assert = $this->mink->assertSession(); + $assert->fieldExists('edit-name')->setValue($username); + $assert->fieldExists('edit-pass')->setValue($password); + $session = $this->mink->getSession(); + $session->getPage()->findButton('Log in')->submit(); + } + +} diff --git a/core/tests/Drupal/BuildTests/TestSiteApplication/InstallTest.php b/core/tests/Drupal/BuildTests/TestSiteApplication/InstallTest.php new file mode 100644 index 0000000000..17b7b3efb9 --- /dev/null +++ b/core/tests/Drupal/BuildTests/TestSiteApplication/InstallTest.php @@ -0,0 +1,60 @@ +markTestIncomplete("Currently does not install. Clearly there's something I don't understand."); + $this->copyCodebase(); + // Clean up after whatever was installed locally. + $sites = $this->getWorkspaceDirectory() . '/sites'; + $fs = new Filesystem(); + $fs->chmod($sites . '/default', 0775, 0000, TRUE); + $fs->remove([ + $sites . '/default/files', + $sites . '/default/settings.php', + $sites . '/simpletest', + ]); + + // Composer tells you stuff in error output. + $this->assertCommandErrorOutputContains('Generating autoload files', 'composer install --no-interaction'); + $this->assertCommand('composer run-script drupal-phpunit-upgrade'); + + // We have to stand up the server first so we can know the port number to + // pass along to the install command. + $this->standUpServer(); + + $install_command = [ + $this->phpFinder->find(), + './core/scripts/test-site.php', + 'install', + '--base-url=http://localhost:' . $this->getPortNumber(), + '--db-url=sqlite://localhost/foo.sqlite', + '--install-profile=minimal', + '--json', + ]; + $this->assertNotEmpty( + $output_json = $this->assertCommand(implode(' ', $install_command))->getOutput() + ); + $connection_details = json_decode($output_json, TRUE); + foreach (['db_prefix', 'user_agent', 'site_path'] as $key) { + $this->assertArrayHasKey($key, $connection_details); + } + + // Visit paths with expectations. + $this->assertRequest('', 200); + $assert = $this->mink->assertSession(); + // It should have the header but it can be empty. + $assert->responseHeaderEquals('X-Generator', 'Drupal 8 (https://www.drupal.org)'); + $assert->elementExists('css', 'html'); + } + +} diff --git a/core/tests/Drupal/BuildTests/Update/UpdateTest.php b/core/tests/Drupal/BuildTests/Update/UpdateTest.php new file mode 100644 index 0000000000..096907f1fe --- /dev/null +++ b/core/tests/Drupal/BuildTests/Update/UpdateTest.php @@ -0,0 +1,168 @@ +assertCommand('git clone --branch ' . $version_tag . ' --depth 1 https://git.drupalcode.org/project/drupal.git .'); + + // Install Drupal using quick start. + $this->assertCommandErrorOutputContains('Generating autoload files', 'composer install --no-dev --no-interaction'); + $this->installQuickStart('minimal'); + + // Assert that it worked. + $this->assertRequest('', 200); + $assert = $this->mink->assertSession(); + $assert->responseHeaderEquals('X-Generator', 'Drupal 8 (https://www.drupal.org)'); + $assert->elementExists('css', 'html'); + + // Log in so that we can update. + $this->formLogin($this->adminUsername, $this->adminPassword); + + // Get the 8.6.16 codebase. We have to fetch the tags for this shallow repo. + $this->assertCommand('git fetch --tags'); + $this->assertCommand('git checkout 8.6.16'); + // Composer. + $this->assertCommandErrorOutputContains('Generating autoload files', 'composer install --no-dev --no-interaction'); + + // Perform the update steps. + $this->assertRequest('update.php', 200); + + $assert->pageTextContains('Drupal database update'); + $session = $this->mink->getSession(); + $session->getPage()->clickLink('Continue'); + + // Allow for 'no pending updates' result. + $contents = $session->getPage()->getContent(); + if (strpos($contents, 'No pending updates') === NULL) { + $session->getPage()->clickLink('Apply pending updates'); + $assert->pageTextContains('Updates were attempted.'); + } + + // Request the front page again and make sure it works. + $this->assertRequest('', 200); + $assert = $this->mink->assertSession(); + $assert->responseHeaderEquals('X-Generator', 'Drupal 8 (https://www.drupal.org)'); + $assert->elementExists('css', 'html'); + } + + /** + * Test that an update works, using local code including any DrupalCI patches. + * + * Mimics the instructions from core/UPDATE.txt. + * + * @todo This test is not good. Make a new one. It is here as a prototype. + */ + public function testUpdateCode() { + $fs = new Filesystem(); + // Clone Drupal 8.7.x. This is a shallow clone to save time/bandwidth if + // subsequent assertions fail. This would be a previous tagged release of + // 8.8.x if there were one at this time. + $this->assertCommand('git clone --branch 8.7.1 --depth 1 https://git.drupalcode.org/project/drupal.git .'); + + // Install Drupal using quick start. + $this->assertCommandErrorOutputContains('Generating autoload files', 'composer install --no-dev --no-interaction'); + $this->installQuickStart('minimal'); + + // Assert that it worked. + $this->assertRequest('', 200); + $assert = $this->mink->assertSession(); + $assert->responseHeaderEquals('X-Generator', 'Drupal 8 (https://www.drupal.org)'); + $assert->elementExists('css', 'html'); + + // Log in so that we can update. + $this->formLogin($this->adminUsername, $this->adminPassword); + + // Prepare for copying the codebase. + $fs->chmod($this->getWorkspaceDirectory(), 0775, 0000, TRUE); + $fs->remove([ + $this->getWorkspaceDirectory() . '/core', + $this->getWorkspaceDirectory() . '/vendor', + ]); + $sites_dir = $this->getWorkspaceDirectory() . '/sites'; + $fs->rename($sites_dir, $sites_dir . '_backup', TRUE); + + // Copy the current 8.8.x codebase into the workspace (including whatever + // was patched in DrupalCI). Allow overwriting newer files, but don't delete + // extra files. + $this->copyCodebase(['overwrite' => TRUE, 'delete' => FALSE]); + $fs->chmod($this->getWorkspaceDirectory(), 0775, 0000, TRUE); + + // Restore the backup sites/. + $fs->rename($sites_dir . '_backup', $sites_dir, TRUE); + + // Currently, this test has to use update_free_access because the PHP HTTP + // server caches old class information. We'll need to restart the server + // process as well. + file_put_contents($sites_dir . '/default/settings.php', '$settings[\'update_free_access\'] = TRUE;', FILE_APPEND); + + // Remove existing vendor directory to get a complete install and autoload + // dump. Also avoid the drupal/coder modified problem. + $fs->remove($this->getWorkspaceDirectory() . '/vendor'); + // Install Composer dependencies. + $this->assertCommandErrorOutputContains('Generating autoload files', 'composer install --no-dev --no-interaction'); + + // Our server caches some classes that have been updated. Specifically: + // ReflectionException: + // Method Drupal\user\Controller\UserController::userEditPage() does not + // exist in [..]/core/lib/Drupal/Core/Entity/EntityResolverManager.php on + // line 136 #0 + // [..]/core/lib/Drupal/Core/Entity/EntityResolverManager.php(136): + // ReflectionMethod->__construct('\\Drupal\\user\\Co...', 'userEditPage') + $this->stopServer(); + $this->standUpServer(); + + // Perform the update steps. + $this->assertRequest('update.php', 200); + // Since we restarted the server, we're in a different session, but only + // after we've made a request. + $session = $this->mink->getSession(); + $assert = $this->mink->assertSession(); + + $assert->pageTextContains('Drupal database update'); + $session->getPage()->clickLink('Continue'); + + $assert->pageTextNotContains('The website encountered an unexpected error'); + // Allow for 'no pending updates' result. + $contents = $session->getPage()->getContent(); + if (strpos($contents, 'No pending updates') === NULL) { + $session->getPage()->clickLink('Apply pending updates'); + $assert->pageTextContains('Updates were attempted.'); + } + + // Request the front page again and make sure it works. + $this->assertRequest('', 200); + $assert = $this->mink->assertSession(); + $assert->responseHeaderEquals('X-Generator', 'Drupal 8 (https://www.drupal.org)'); + $assert->elementExists('css', 'html'); + } + +}