diff --git a/core/composer.json b/core/composer.json index eb54ebfa81..03286dcc10 100644 --- a/core/composer.json +++ b/core/composer.json @@ -22,6 +22,8 @@ "symfony/console": "~3.4.0", "symfony/dependency-injection": "~3.4.26", "symfony/event-dispatcher": "~3.4.0", + "symfony/filesystem": "~3.4.0", + "symfony/finder": "~3.4.0", "symfony/http-foundation": "~3.4.27", "symfony/http-kernel": "~3.4.14", "symfony/routing": "~3.4.0", 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/Core/Test/TestDiscovery.php b/core/lib/Drupal/Core/Test/TestDiscovery.php index 7bb08ac4a3..552a72acf9 100644 --- a/core/lib/Drupal/Core/Test/TestDiscovery.php +++ b/core/lib/Drupal/Core/Test/TestDiscovery.php @@ -81,6 +81,7 @@ public function registerTestNamespaces() { // Add PHPUnit test namespaces of Drupal core. $this->testNamespaces['Drupal\\Tests\\'] = [$this->root . '/core/tests/Drupal/Tests']; + $this->testNamespaces['Drupal\\BuildTests\\'] = [$this->root . '/core/tests/Drupal/BuildTests']; $this->testNamespaces['Drupal\\KernelTests\\'] = [$this->root . '/core/tests/Drupal/KernelTests']; $this->testNamespaces['Drupal\\FunctionalTests\\'] = [$this->root . '/core/tests/Drupal/FunctionalTests']; $this->testNamespaces['Drupal\\FunctionalJavascriptTests\\'] = [$this->root . '/core/tests/Drupal/FunctionalJavascriptTests']; @@ -102,6 +103,7 @@ public function registerTestNamespaces() { $this->testNamespaces["Drupal\\Tests\\$name\\Unit\\"][] = "$base_path/tests/src/Unit"; $this->testNamespaces["Drupal\\Tests\\$name\\Kernel\\"][] = "$base_path/tests/src/Kernel"; $this->testNamespaces["Drupal\\Tests\\$name\\Functional\\"][] = "$base_path/tests/src/Functional"; + $this->testNamespaces["Drupal\\Tests\\$name\\Build\\"][] = "$base_path/tests/src/Build"; $this->testNamespaces["Drupal\\Tests\\$name\\FunctionalJavascript\\"][] = "$base_path/tests/src/FunctionalJavascript"; // Add discovery for traits which are shared between different test diff --git a/core/modules/simpletest/src/Form/SimpletestResultsForm.php b/core/modules/simpletest/src/Form/SimpletestResultsForm.php index 5db8846353..c5d5337780 100644 --- a/core/modules/simpletest/src/Form/SimpletestResultsForm.php +++ b/core/modules/simpletest/src/Form/SimpletestResultsForm.php @@ -92,6 +92,7 @@ protected static function buildStatusImageMap() { 'pass' => $image_pass, 'fail' => $image_fail, 'exception' => $image_exception, + 'error' => $image_exception, 'debug' => $image_debug, ]; } @@ -277,6 +278,7 @@ public static function addResultForm(array &$form, array $results) { '#pass' => 0, '#fail' => 0, '#exception' => 0, + '#error' => 0, '#debug' => 0, ]; diff --git a/core/phpunit.xml.dist b/core/phpunit.xml.dist index c24d4008d0..eb63541812 100644 --- a/core/phpunit.xml.dist +++ b/core/phpunit.xml.dist @@ -45,6 +45,9 @@ ./tests/TestSuites/FunctionalJavascriptTestSuite.php + + ./tests/TestSuites/BuildTestSuite.php + diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh index f1cb4c0ce4..ebe0615526 100755 --- a/core/scripts/run-tests.sh +++ b/core/scripts/run-tests.sh @@ -849,9 +849,12 @@ function simpletest_script_run_one_test($test_id, $test_class) { * The assembled command string. */ function simpletest_script_command($test_id, $test_class) { - global $args, $php; + global $args, $php, $tmp_dir; - $command = escapeshellarg($php) . ' ' . escapeshellarg('./core/scripts/' . $args['script']); + $tmp_dir = FileSystem::getOsTemporaryDirectory() . '/run_tests_' . rand(10000,99999) . microtime(TRUE); + mkdir($tmp_dir); + $command = escapeshellarg($php) . ' -d sys_temp_dir=' . escapeshellarg($tmp_dir); + $command .= ' ' . escapeshellarg('./core/scripts/' . $args['script']); $command .= ' --url ' . escapeshellarg($args['url']); if (!empty($args['sqlite'])) { $command .= ' --sqlite ' . escapeshellarg($args['sqlite']); @@ -893,6 +896,10 @@ function simpletest_script_command($test_id, $test_class) { * @see simpletest_script_run_one_test() */ function simpletest_script_cleanup($test_id, $test_class, $exitcode) { + global $tmp_dir; + if (is_dir($tmp_dir)) { + simpletest_rmdir(); + } if (is_subclass_of($test_class, TestCase::class)) { // PHPUnit test, move on. return; @@ -977,6 +984,27 @@ function simpletest_script_cleanup($test_id, $test_class, $exitcode) { } } +/** + * Recursively delete a directory tree. + * + * @param $dir + * Path to directory to remove. + * @return bool + * 'true' if directory successfully removed. + */ +function simpletest_rmdir($dir) +{ + $files = array_diff(scandir($dir), ['.','..']); + foreach ($files as $file) { + if (is_dir("$dir/$file") && !is_link("$dir/$file")) { + simpletest_rmdir("$dir/$file"); + } else { + unlink("$dir/$file"); + } + } + return rmdir($dir); +} + /** * Get list of tests based on arguments. * diff --git a/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php new file mode 100644 index 0000000000..63e6a2444c --- /dev/null +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php @@ -0,0 +1,384 @@ +phpFinder = new PhpExecutableFinder(); + // Set up the workspace directory. + // @todo Glean working directory from env vars, etc. + $this->workspaceDir = sys_get_temp_dir() . '/build_workspace_' . md5($this->getName() . microtime(TRUE)); + $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 ($this->destroyBuild && $fs->exists($ws)) { + try { + $fs->chmod($ws, 0775, 0000, TRUE); + } + catch (IOException $exception) { + // Don't fail build if chmod doesn't work. + } + $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 getMink() { + return $this->mink; + } + + /** + * {@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 assertVisit($request_uri = '', $expected_status_code = 200, $docroot = NULL) { + $this->visit($request_uri, $docroot); + $actual_code = $this->mink->getSession()->getStatusCode(); + $message = sprintf('Current response status code is %d, but %d expected for "%s".', $actual_code, $expected_status_code, $request_uri); + $this->assertEquals((int) $expected_status_code, (int) $actual_code, $message); + } + + /** + * {@inheritdoc} + */ + public function visit($request_uri = '', $docroot = NULL) { + $request_uri = trim($request_uri, " \t\n\r\0\x0B/"); + $message = 'URI should be relative. Example: some/path?foo=bar'; + $this->assertStringStartsNotWith('http://', $request_uri, $message); + $this->assertStringStartsNotWith('https://', $request_uri, $message); + // 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. + * + * @return \Symfony\Component\Process\Process + * The server process. + */ + protected function instantiateServer($hostname, $port, $docroot) { + // 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($server, $full_docroot); + $ps->setIdleTimeout(30) + ->setTimeout(30) + ->start(); + // Wait until the web server has started. It is started if the port is no + // longer available. + while ($available_port = static::findAvailablePort($port)) { + if ($available_port !== $port) { + 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 findAvailablePort($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::findAvailablePort(); + } + return $this->hostPort; + } + + /** + * {@inheritdoc} + */ + public function copyCodebase(\Iterator $iterator = NULL, $working_dir = NULL) { + // Use implode() because $working_dir can be NULL. + $full_working = implode('/', [$this->getWorkspaceDirectory(), $working_dir]); + + if ($iterator === NULL) { + $finder = new Finder(); + $finder->files() + ->in($this->getDrupalRoot()) + ->exclude([ + 'sites/default/files', + 'sites/simpletest', + 'vendor', + ]) + ->notPath('/sites\/default\/settings\..*php/') + ->ignoreDotFiles(FALSE) + ->ignoreVCS(FALSE); + $iterator = $finder->getIterator(); + } + + $fs = new Filesystem(); + $options = ['override' => TRUE, 'delete' => FALSE]; + $fs->mirror($this->getDrupalRoot(), $full_working, $iterator, $options); + } + + /** + * Get the root path of this Drupal codebase. + * + * @return string + * The full path to the root of this Drupal codebase. + */ + 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..26c246af90 --- /dev/null +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestInterface.php @@ -0,0 +1,177 @@ +getMink() to get session and assertion objects to test the + * result of this method. + * + * @param string $request_uri + * (optional) The non-host part of the URL. Example: some/path?foo=bar. + * Defaults to visiting the homepage. + * @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 assertVisit($request_uri = '', $expected_status_code = 200, $docroot = NULL); + + /** + * Get the Mink instance. + * + * Use the Mink object to perform assertions against the content returned by a + * request. + * + * @return \Behat\Mink\Mink + * The Mink object for the last request. + */ + public function getMink(); + + /** + * 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->getMink() to get session and assertion objects to test the + * outcome of this method. + * + * @param string $request_uri + * (optional) The non-host part of the URL. Example: some/path?foo=bar. + * Defaults to visiting the homepage. + * @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. + * + * By default, the copy will exclude sites/default/settings.php, + * sites/default/files, and vendor/. Use the $iterator parameter to override + * this behavior. + * + * @param \Iterator|null $iterator + * (optional) An iterator of all the files to copy. Default behavior is to + * exclude site-specific directories and files. + * @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(\Iterator $iterator = NULL, $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..b68a0b8291 --- /dev/null +++ b/core/tests/Drupal/BuildTests/Framework/DrupalMinkClient.php @@ -0,0 +1,66 @@ +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(['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 $files = [], array $server = [], $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/ExternalCommandRequirementsTrait.php b/core/tests/Drupal/BuildTests/Framework/ExternalCommandRequirementsTrait.php new file mode 100644 index 0000000000..2a81e1766a --- /dev/null +++ b/core/tests/Drupal/BuildTests/Framework/ExternalCommandRequirementsTrait.php @@ -0,0 +1,115 @@ +getAnnotations(); + if (!empty($annotations['class']['requires'])) { + $this->checkExternalCommandRequirements($annotations['class']['requires']); + } + if (!empty($annotations['method']['requires'])) { + $this->checkExternalCommandRequirements($annotations['method']['requires']); + } + } + + /** + * Checks missing external command requirements. + * + * @param string[] $annotations + * A list of requires annotations from either a method or class annotation. + * + * @throws \PHPUnit_Framework_SkippedTestError + * Thrown when the requirements are not met, and this test should be + * skipped. Callers should not catch this exception. + */ + private function checkExternalCommandRequirements(array $annotations) { + // Make a list of required commands. + $required_commands = []; + foreach ($annotations as $requirement) { + if (strpos($requirement, 'externalCommand ') === 0) { + $command = trim(str_replace('externalCommand ', '', $requirement)); + // Use named keys to avoid duplicates. + $required_commands[$command] = $command; + } + } + + // Figure out which commands are not available. + $unavailable = []; + foreach ($required_commands as $required_command) { + if (!in_array($required_command, static::$existingCommands)) { + if ($this->externalCommandIsAvailable($required_command)) { + // Cache existing commands so we don't have to ask again. + static::$existingCommands[] = $required_command; + } + else { + $unavailable[] = $required_command; + } + } + } + + // Skip the test if there were some we couldn't find. + if (!empty($unavailable)) { + throw new SkippedTestError('Required external commands: ' . implode(', ', $unavailable)); + } + } + + /** + * Determine if an external command is available. + * + * @param $command + * The external command. + * + * @return bool + * TRUE if external command is available, else FALSE. + */ + private function externalCommandIsAvailable($command) { + $command = $this->isWindows() ? "where $command" : "which $command"; + $process = new Process($command); + $process->run(); + return $process->isSuccessful(); + } + + /** + * Determine if the operating system is Windows. + * + * @return bool + * TRUE if operating system is Windows, else FALSE. + */ + private function isWindows() { + return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; + } + +} 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..3449b12d63 --- /dev/null +++ b/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php @@ -0,0 +1,190 @@ +executeCommand('test -f composer.json'); + $this->assertEquals(1, $process->getExitCode()); + + // We could assertCommand() and build our workspace with a subdirectory, 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); + + // The getWorkingDirectory() method can return NULL if there is a problem. + $this->assertNotNull( + $process_working_dir = $process->getWorkingDirectory() + ); + // Ensure the process' working directory has the correct path prefix. + $this->assertStringStartsWith($workspace, $process_working_dir); + $this->assertEquals($test_directory, basename($process_working_dir)); + } + + /** + * @covers ::copyCodebase + * @covers ::assertCommand + * @covers ::visit + */ + public function testSiteUpdate() { + $this->copyCodebase(); + $this->executeCommand('git fetch --unshallow --tags'); + $this->assertCommand('git config user.email "drupalci@no-reply.drupal.org"'); + $this->assertCommand('git config user.name "Drupal CI"'); + $this->assertCommand('git stash'); + $this->assertCommand('git checkout 8.0.0'); + $this->assertCommand('COMPOSER_DISCARD_CHANGES=true composer install -n'); + // Install will work, but gives PHP warnings on later versions of PHP. + // Therefore we are using executeCommand instead of assertCommand. + $this->executeCommand('drush site-install --db-url=sqlite://db.sqlite -y'); + $this->assertVisit('node'); + $fs = new Filesystem(); + $fs->chmod($this->getWorkspaceDirectory() . '/sites/default', 0700, 0000); + $this->assertCommand('git checkout - -f'); + $this->assertCommand('git reset HEAD --hard'); + $this->assertCommand('git stash pop'); + $this->assertCommand('COMPOSER_DISCARD_CHANGES=true composer install -n'); + $this->assertCommand('drush updb -y'); + $this->assertVisit('node'); + $process = $this->assertCommand('drush uli --no-browser -l http://localhost'); + $url = substr($process->getOutput(), 17); + $this->assertVisit('user/login'); + $this->assertVisit($url); + } + + /** + * @covers ::copyCodebase + */ + public function testCopyCodebase() { + $test_directory = 'copied_codebase'; + $this->copyCodebase(NULL, $test_directory); + $full_path = $this->getWorkspaceDirectory() . '/' . $test_directory; + $files = [ + 'autoload.php', + 'composer.json', + 'index.php', + 'README.txt', + '.git', + ]; + foreach ($files as $file) { + $this->assertFileExists($full_path . '/' . $file); + } + } + + /** + * Ensure we're not copying directories we wish to exclude. + * + * @covers ::copyCodebase + */ + public function testCopyCodebaseExclude() { + // Create a virtual file system containing only items that should be + // excluded. + vfsStream::setup('drupal', NULL, [ + 'sites' => [ + 'default' => [ + 'files' => [ + 'a_file.txt' => 'some file.', + ], + 'settings.php' => ' [ + 'simpletest_hash' => [ + 'some_results.xml' => '', + ], + ], + ], + 'vendor' => [ + 'composer' => [ + 'composer' => [ + 'installed.json' => '"items": {"things"}', + ], + ], + ], + ]); + + // Mock BuildTestBase so that it thinks our VFS is the Drupal root. + $base = $this->getMockBuilder(BuildTestBase::class) + ->setMethods(['getDrupalRoot']) + ->getMockForAbstractClass(); + $base->expects($this->exactly(2)) + ->method('getDrupalRoot') + ->willReturn(vfsStream::url('drupal')); + + $base->setUp(); + + // Perform the copy. + $test_directory = 'copied_codebase'; + $base->copyCodebase(NULL, $test_directory); + $full_path = $base->getWorkspaceDirectory() . '/' . $test_directory; + + $this->assertDirectoryExists($full_path); + // Use scandir() to determine if our target directory is empty. It should + // only contain the system dot directories. + $this->assertTrue( + ($files = @scandir($full_path)) && count($files) <= 2, + 'Directory is not empty: ' . implode(', ', $files) + ); + + // Clean up after our mocked test base. + $base->tearDown(); + } + + /** + * @covers ::findAvailablePort + */ + public function testPort() { + // We should find the same port twice in a row, since we don't use it. + $this->assertEquals(static::findAvailablePort(), static::findAvailablePort()); + + // Grab a port, make a server, make sure the next port we grab is not the + // same. + $port = static::findAvailablePort(); + $process = $this->instantiateServer(static::$hostName, $port, NULL); + $this->assertNotEquals($port, static::findAvailablePort()); + } + + /** + * @covers ::findAvailablePort + */ + public function testPortMany() { + $processes = []; + $count = 15; + foreach (range(1, $count) as $instance) { + $port = static::findAvailablePort(); + $this->assertArrayNotHasKey($port, $processes, 'Port ' . $port . ' was already in use by a process.'); + $processes[$port] = $this->instantiateServer(static::$hostName, $port, NULL, 1); + $this->assertNotEmpty($processes[$port]); + } + 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..ac3cf2ade2 --- /dev/null +++ b/core/tests/Drupal/BuildTests/Framework/Tests/DrupalMinkClientTest.php @@ -0,0 +1,78 @@ +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'], + ]; + } + +} + +/** + * Special client that can return a given response on the first doRequest(). + */ +class TestClient extends DrupalMinkClient { + + protected $nextResponse = NULL; + + public function setNextResponse(Response $response) { + $this->nextResponse = $response; + } + + protected function doRequest($request) { + if (NULL === $this->nextResponse) { + return new Response(); + } + + $response = $this->nextResponse; + $this->nextResponse = NULL; + + return $response; + } + +} diff --git a/core/tests/Drupal/BuildTests/Framework/Tests/ExternalCommandRequirementTest.php b/core/tests/Drupal/BuildTests/Framework/Tests/ExternalCommandRequirementTest.php new file mode 100644 index 0000000000..e7e28ec969 --- /dev/null +++ b/core/tests/Drupal/BuildTests/Framework/Tests/ExternalCommandRequirementTest.php @@ -0,0 +1,78 @@ +setAvailableCommands(['available_command']); + + $ref_check_requirements = new \ReflectionMethod($requires, 'checkExternalCommandRequirements'); + $ref_check_requirements->setAccessible(TRUE); + + // Use a try/catch block because otherwise PHPUnit might think this test is + // legitimately skipped. + try { + $ref_check_requirements->invokeArgs($requires, [ + ['externalCommand not_available', 'externalCommand available_command'], + ]); + $this->fail('Unavailable external command requirement should throw a skipped test error exception.'); + } + catch (SkippedTestError $exception) { + $this->assertEquals('Required external commands: not_available', $exception->getMessage()); + } + } + + /** + * @covers ::checkExternalCommandRequirements + */ + public function testCheckExternalCommandRequirementsAvailable() { + $requires = new UsesCommandRequirements(); + + $requires->setAvailableCommands(['available_command']); + + $ref_check_requirements = new \ReflectionMethod($requires, 'checkExternalCommandRequirements'); + $ref_check_requirements->setAccessible(TRUE); + + // Use a try/catch block because otherwise PHPUnit might think this test is + // legitimately skipped. + try { + $this->assertNull( + $ref_check_requirements->invokeArgs($requires, [['externalCommand available_command']]) + ); + } + catch (SkippedTestError $exception) { + $this->fail('The external command should be available, so we should not have skipped.'); + } + } + +} + +class UsesCommandRequirements { + + use ExternalCommandRequirementsTrait; + + protected $availableCommands = []; + + public function setAvailableCommands($available_commands) { + $this->availableCommands = $available_commands; + } + + private function externalCommandIsAvailable($command) { + return in_array($command, $this->availableCommands); + } + +} diff --git a/core/tests/TestSuites/BuildTestSuite.php b/core/tests/TestSuites/BuildTestSuite.php new file mode 100644 index 0000000000..1116438f2e --- /dev/null +++ b/core/tests/TestSuites/BuildTestSuite.php @@ -0,0 +1,27 @@ +addTestsBySuiteNamespace($root, 'Build'); + + return $suite; + } + +} diff --git a/core/tests/bootstrap.php b/core/tests/bootstrap.php index 3ea857115e..d1763e844c 100644 --- a/core/tests/bootstrap.php +++ b/core/tests/bootstrap.php @@ -133,10 +133,12 @@ function drupal_phpunit_get_extension_namespaces($dirs) { */ function drupal_phpunit_populate_class_loader() { - /** @var \Composer\Autoload\ClassLoader $loader */ + /* @var \Composer\Autoload\ClassLoader $loader */ $loader = require __DIR__ . '/../../autoload.php'; // Start with classes in known locations. + $loader->add('Drupal\\BuildTests', __DIR__); + $loader->add('Drupal\\BuildTests\\Framework', __DIR__); $loader->add('Drupal\\Tests', __DIR__); $loader->add('Drupal\\TestSite', __DIR__); $loader->add('Drupal\\KernelTests', __DIR__);