diff --git a/composer.lock b/composer.lock index a934df60e4..97d48d4f1f 100644 --- a/composer.lock +++ b/composer.lock @@ -2060,7 +2060,7 @@ }, { "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" + "email": "backendtea@gmail.com" } ], "description": "Symfony polyfill for ctype functions", @@ -4879,6 +4879,105 @@ "homepage": "https://symfony.com", "time": "2019-02-23T15:06:07+00:00" }, + { + "name": "symfony/filesystem", + "version": "v3.4.27", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "acf99758b1df8e9295e6b85aa69f294565c9fedb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/acf99758b1df8e9295e6b85aa69f294565c9fedb", + "reference": "acf99758b1df8e9295e6b85aa69f294565c9fedb", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-ctype": "~1.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Filesystem Component", + "homepage": "https://symfony.com", + "time": "2019-02-04T21:34:32+00:00" + }, + { + "name": "symfony/finder", + "version": "v3.4.27", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "61af5ce0b34b942d414fe8f1b11950d0e9a90e98" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/61af5ce0b34b942d414fe8f1b11950d0e9a90e98", + "reference": "61af5ce0b34b942d414fe8f1b11950d0e9a90e98", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Finder Component", + "homepage": "https://symfony.com", + "time": "2019-04-02T19:54:57+00:00" + }, { "name": "symfony/phpunit-bridge", "version": "v3.4.26", diff --git a/core/composer.json b/core/composer.json index 810ec27e59..9adb92b919 100644 --- a/core/composer.json +++ b/core/composer.json @@ -63,6 +63,8 @@ "phpunit/phpunit": "^4.8.35 || ^6.5", "phpspec/prophecy": "^1.7", "symfony/css-selector": "^3.4.0", + "symfony/filesystem": "^3.4", + "symfony/finder": "~3.4", "symfony/phpunit-bridge": "^3.4.3", "symfony/debug": "^3.4.0", "justinrainbow/json-schema": "^5.2" 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/modules/simpletest/src/Form/SimpletestResultsForm.php b/core/modules/simpletest/src/Form/SimpletestResultsForm.php index a2241571eb..b034288986 100644 --- a/core/modules/simpletest/src/Form/SimpletestResultsForm.php +++ b/core/modules/simpletest/src/Form/SimpletestResultsForm.php @@ -93,6 +93,7 @@ protected static function buildStatusImageMap() { 'pass' => $image_pass, 'fail' => $image_fail, 'exception' => $image_exception, + 'error' => $image_exception, 'debug' => $image_debug, ]; } @@ -278,6 +279,7 @@ public static function addResultForm(array &$form, array $results) { '#pass' => 0, '#fail' => 0, '#exception' => 0, + '#error' => 0, '#debug' => 0, ]; diff --git a/core/modules/simpletest/src/TestDiscovery.php b/core/modules/simpletest/src/TestDiscovery.php index a9cbe15db7..9e11dd9c52 100644 --- a/core/modules/simpletest/src/TestDiscovery.php +++ b/core/modules/simpletest/src/TestDiscovery.php @@ -92,6 +92,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']; diff --git a/core/phpunit.xml.dist b/core/phpunit.xml.dist index 4f81a2d533..b63985b663 100644 --- a/core/phpunit.xml.dist +++ b/core/phpunit.xml.dist @@ -48,6 +48,9 @@ ./tests/TestSuites/FunctionalJavascriptTestSuite.php + + ./tests/TestSuites/BuildTestSuite.php + diff --git a/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php new file mode 100644 index 0000000000..ab422463c5 --- /dev/null +++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php @@ -0,0 +1,375 @@ +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) { + // @todo Determine if there's a use case for options other than the defaults. + $options = array_merge([ + 'override' => TRUE, + 'delete' => FALSE, + ], $options); + // Use implode() because $working_dir can be NULL. + $full_working = implode('/', [$this->getWorkspaceDirectory(), $working_dir]); + + // Filesystem::mirror() uses the iterator differently if we set the delete + // option, so we have to handle that special case. See + // https://github.com/symfony/symfony/issues/14068 + // @todo Figure out how to manage this so that we can provide a mirrored + // codebase without the paths we want to exclude, while also deleting the + // extra files. + $iterator = NULL; + if (!$options['delete']) { + $finder = new Finder(); + $finder->files() + ->in($this->getDrupalRoot()) + ->exclude([ + 'sites/default/files', + 'sites/default/settings.php', + 'sites/simpletest', + 'vendor', + ]); + $finder->ignoreDotFiles(FALSE); + $iterator = $finder->getIterator(); + } + + $fs = new Filesystem(); + $fs->mirror($this->getDrupalRoot(), $full_working, $iterator, $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..420a156b93 --- /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. + * + * The copy will exclude sites/default/settings.php, sites/default/files, and + * vendor/. + * + * @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 FALSE. + * @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/ExternalCommandRequirementsTrait.php b/core/tests/Drupal/BuildTests/Framework/ExternalCommandRequirementsTrait.php new file mode 100644 index 0000000000..b9d9eafbc2 --- /dev/null +++ b/core/tests/Drupal/BuildTests/Framework/ExternalCommandRequirementsTrait.php @@ -0,0 +1,96 @@ +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)); + $required_commands[$command] = $command; + } + } + + // Figure out which commands are not available. + $unavailable = []; + foreach (array_keys($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 \PHPUnit_Framework_SkippedTestError('Required external commands: ' . implode(', ', $unavailable)); + } + } + + private function externalCommandIsAvailable($command) { + // @todo Figure out the Windows version of this. + $process = new Process('which ' . $command); + $process->run(); + return $process->getExitCode() == 0; + } + +} 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/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 82a1830547..7573b7c07c 100644 --- a/core/tests/bootstrap.php +++ b/core/tests/bootstrap.php @@ -124,10 +124,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__);