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'],
+ ['