Commit 8524be36 authored by alexpott's avatar alexpott

Issue #3031379 by heddn, Mile23, greg.1.anderson, Charlie ChX Negyesi,...

Issue #3031379 by heddn, Mile23, greg.1.anderson, Charlie ChX Negyesi, alexpott, Mixologic, jibran, catch, Lendude: Add a new test type to do real update testing
parent 0fed097a
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "38c8384dea91c895efa0d3119c037f84",
"content-hash": "7c19b29738cf44de507d0baa3bd31665",
"packages": [
{
"name": "asm89/stack-cors",
......@@ -3652,9 +3652,6 @@
"ext-zip": "Enabling the zip extension allows you to unzip archives",
"ext-zlib": "Allow gzip compression of HTTP requests"
},
"bin": [
"bin/composer"
],
"type": "library",
"extra": {
"branch-alias": {
......@@ -5793,16 +5790,16 @@
},
{
"name": "symfony/filesystem",
"version": "v3.4.28",
"version": "v3.4.31",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "acf99758b1df8e9295e6b85aa69f294565c9fedb"
"reference": "00e3a6ddd723b8bcfe4f2a1b6f82b98eeeb51516"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/acf99758b1df8e9295e6b85aa69f294565c9fedb",
"reference": "acf99758b1df8e9295e6b85aa69f294565c9fedb",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/00e3a6ddd723b8bcfe4f2a1b6f82b98eeeb51516",
"reference": "00e3a6ddd723b8bcfe4f2a1b6f82b98eeeb51516",
"shasum": ""
},
"require": {
......@@ -5839,20 +5836,20 @@
],
"description": "Symfony Filesystem Component",
"homepage": "https://symfony.com",
"time": "2019-02-04T21:34:32+00:00"
"time": "2019-08-20T13:31:17+00:00"
},
{
"name": "symfony/finder",
"version": "v3.4.28",
"version": "v3.4.31",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "fa5d962a71f2169dfe1cbae217fa5a2799859f6c"
"reference": "1fcad80b440abcd1451767349906b6f9d3961d37"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/fa5d962a71f2169dfe1cbae217fa5a2799859f6c",
"reference": "fa5d962a71f2169dfe1cbae217fa5a2799859f6c",
"url": "https://api.github.com/repos/symfony/finder/zipball/1fcad80b440abcd1451767349906b6f9d3961d37",
"reference": "1fcad80b440abcd1451767349906b6f9d3961d37",
"shasum": ""
},
"require": {
......@@ -5888,7 +5885,69 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"time": "2019-05-24T12:25:55+00:00"
"time": "2019-08-14T09:39:58+00:00"
},
{
"name": "symfony/lock",
"version": "v3.4.31",
"source": {
"type": "git",
"url": "https://github.com/symfony/lock.git",
"reference": "e434629a79538238cce74ef62fa870b149e1b3b8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/lock/zipball/e434629a79538238cce74ef62fa870b149e1b3b8",
"reference": "e434629a79538238cce74ef62fa870b149e1b3b8",
"shasum": ""
},
"require": {
"php": "^5.5.9|>=7.0.8",
"psr/log": "~1.0",
"symfony/polyfill-php70": "~1.0"
},
"require-dev": {
"predis/predis": "~1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.4-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Lock\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jérémy Derussé",
"email": "jeremy@derusse.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Lock Component",
"homepage": "https://symfony.com",
"keywords": [
"cas",
"flock",
"locking",
"mutex",
"redlock",
"semaphore"
],
"time": "2019-08-14T11:59:53+00:00"
},
{
"name": "symfony/phpunit-bridge",
......
......@@ -34,6 +34,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.functional:
types: 'PHPUnit-Functional'
testgroups: '--all'
......
......@@ -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
......
......@@ -45,6 +45,9 @@
<testsuite name="functional-javascript">
<file>./tests/TestSuites/FunctionalJavascriptTestSuite.php</file>
</testsuite>
<testsuite name="build">
<file>./tests/TestSuites/BuildTestSuite.php</file>
</testsuite>
</testsuites>
<listeners>
<listener class="\Drupal\Tests\Listeners\DrupalListener">
......
......@@ -18,6 +18,7 @@
use Drupal\Core\Test\TestDiscovery;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem;
use Symfony\Component\HttpFoundation\Request;
// Define some colors for display.
......
This diff is collapsed.
<?php
namespace Drupal\BuildTests\Framework;
use Behat\Mink\Driver\Goutte\Client;
use Symfony\Component\BrowserKit\Client as SymfonyClient;
/**
* Extend the Mink client for Drupal use-cases.
*
* This is adapted from https://github.com/symfony/symfony/pull/27118.
*
* @todo Update this client when Drupal starts using Symfony 4.2.0+.
* https://www.drupal.org/project/drupal/issues/3077785
*/
class DrupalMinkClient extends Client {
/**
* Whether to follow meta redirects or not.
*
* @var bool
*
* @see \Drupal\BuildTests\Framework\DrupalMinkClient::followMetaRefresh()
*/
protected $followMetaRefresh;
/**
* Sets whether to automatically follow meta refresh redirects or not.
*
* @param bool $followMetaRefresh
* (optional) Whether to follow meta redirects. Defaults to TRUE.
*/
public function followMetaRefresh($followMetaRefresh = TRUE) {
$this->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 is private on the BrowserKit client, so we have to use
// reflection to manage the redirects stack.
$ref_redirects = new \ReflectionProperty(SymfonyClient::class, 'redirects');
$ref_redirects->setAccessible(TRUE);
$redirects = $ref_redirects->getValue($this);
$redirects[serialize($this->history->current())] = TRUE;
$ref_redirects->setValue($this, $redirects);
$this->crawler = $this->followRedirect();
}
return $this->crawler;
}
}
<?php
namespace Drupal\BuildTests\Framework;
use PHPUnit\Framework\SkippedTestError;
use PHPUnit\Util\Test;
use Symfony\Component\Process\ExecutableFinder;
/**
* Allows test classes to require external command line applications.
*
* Use annotation such as '(at)requires externalCommand git'.
*/
trait ExternalCommandRequirementsTrait {
/**
* A list of existing external commands we've already discovered.
*
* @var string[]
*/
private static $existingCommands = [];
/**
* Checks whether required external commands are available per test class.
*
* @throws \PHPUnit\Framework\SkippedTestError
* Thrown when the requirements are not met, and this test should be
* skipped. Callers should not catch this exception.
*/
private static function checkClassCommandRequirements() {
$annotations = Test::parseTestMethodAnnotations(static::class);
if (!empty($annotations['class']['requires'])) {
static::checkExternalCommandRequirements($annotations['class']['requires']);
}
}
/**
* Checks whether required external commands are available per method.
*
* @throws \PHPUnit\Framework\SkippedTestError
* Thrown when the requirements are not met, and this test should be
* skipped. Callers should not catch this exception.
*/
private static function checkMethodCommandRequirements($name) {
$annotations = Test::parseTestMethodAnnotations(static::class, $name);
if (!empty($annotations['method']['requires'])) {
static::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 static 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, self::$existingCommands)) {
if (static::externalCommandIsAvailable($required_command)) {
// Cache existing commands so we don't have to ask again.
self::$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 static function externalCommandIsAvailable($command) {
$finder = new ExecutableFinder();
return (bool) $finder->find($command);
}
}
<?php
namespace Drupal\BuildTests\Framework\Tests;
use Drupal\BuildTests\Framework\BuildTestBase;
use org\bovigo\vfs\vfsStream;
use Symfony\Component\Finder\Finder;
/**
* @coversDefaultClass \Drupal\BuildTests\Framework\BuildTestBase
* @group Build
*/
class BuildTestTest extends BuildTestBase {
/**
* Ensure that workspaces work.
*/
public function testWorkspace() {
$test_directory = 'test_directory';
// Execute an empty command through the shell to build out a working
// directory.
$process = $this->executeCommand('', $test_directory);
$this->assertCommandSuccessful();
// Assert that our working directory exists and is in use by the process.
$workspace = $this->getWorkspaceDirectory();
$working_path = $workspace . '/' . $test_directory;
$this->assertDirectoryExists($working_path);
$this->assertEquals($working_path, $process->getWorkingDirectory());
}
/**
* @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',
'.ht.router.php',
];
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' => '<?php $settings = stuff;',
],
'simpletest' => [
'simpletest_hash' => [
'some_results.xml' => '<xml/>',
],
],
],
'vendor' => [
'composer' => [
'composer' => [
'installed.json' => '"items": {"things"}',
],
],
],
]);
// Mock BuildTestBase so that it thinks our VFS is the Drupal root.
/** @var \PHPUnit\Framework\MockObject\MockBuilder|\Drupal\BuildTests\Framework\BuildTestBase $base */
$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)
);
$base->tearDown();
}
/**
* @covers ::findAvailablePort
*/
public function testPortMany() {
$iterator = (new Finder())->in($this->getDrupalRoot())
->ignoreDotFiles(FALSE)
->exclude(['sites/simpletest'])
->path('/^.ht.router.php$/')
->getIterator();
$this->copyCodebase($iterator);
/** @var \Symfony\Component\Process\Process[] $processes */
$processes = [];
$count = 15;
for ($i = 0; $i <= $count; $i++) {
$port = $this->findAvailablePort();
$this->assertArrayNotHasKey($port, $processes, 'Port ' . $port . ' was already in use by a process.');
$processes[$port] = $this->instantiateServer($port);
$this->assertNotEmpty($processes[$port]);
$this->assertTrue($processes[$port]->isRunning(), 'Process on port ' . $port . ' is not still running.');
$this->assertFalse($this->checkPortIsAvailable($port));
}
}
}
<?php
namespace Drupal\BuildTests\Framework\Tests;
use Drupal\BuildTests\Framework\DrupalMinkClient;
use PHPUnit\Framework\TestCase;
use Symfony\Component\BrowserKit\Response;
/**
* Test \Drupal\BuildTests\Framework\DrupalMinkClient.
*
* This test is adapted from \Symfony\Component\BrowserKit\Tests\ClientTest.
*
* @coversDefaultClass \Drupal\BuildTests\Framework\DrupalMinkClient
*
* @group Build
*/
class DrupalMinkClientTest extends TestCase {
/**
* @dataProvider getTestsForMetaRefresh
* @covers ::getMetaRefreshUrl
*/
public function testFollowMetaRefresh(string $content, string $expectedEndingUrl, bool $followMetaRefresh = TRUE) {
$client = new TestClient();
$client->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 [
['<html><head><meta http-equiv="Refresh" content="4" /><meta http-equiv="refresh" content="0; URL=http://www.example.com/redirected"/></head></html>', 'http://www.example.com/redirected'],
['<html><head><meta http-equiv="refresh" content="0;URL=http://www.example.com/redirected"/></head></html>', 'http://www.example.com/redirected'],
['<html><head><meta http-equiv="refresh" content="0;URL=\'http://www.example.com/redirected\'"/></head></html>', 'http://www.example.com/redirected'],
['<html><head><meta http-equiv="refresh" content=\'0;URL="http://www.example.com/redirected"\'/></head></html>', 'http://www.example.com/redirected'],
['<html><head><meta http-equiv="refresh" content="0; URL = http://www.example.com/redirected"/></head></html>', 'http://www.example.com/redirected'],
['<html><head><meta http-equiv="refresh" content="0;URL= http://www.example.com/redirected "/></head></html>', 'http://www.example.com/redirected'],
['<html><head><meta http-equiv="refresh" content="0;url=http://www.example.com/redirected "/></head></html>', 'http://www.example.com/redirected'],
['<html><head><noscript><meta http-equiv="refresh" content="0;URL=http://www.example.com/redirected"/></noscript></head></head></html>', 'http://www.example.com/redirected'],
// Non-zero timeout should not result in a redirect.
['<html><head><meta http-equiv="refresh" content="4; URL=http://www.example.com/redirected"/></head></html>', 'http://www.example.com/foo/foobar'],
['<html><body></body></html>', 'http://www.example.com/foo/foobar'],
// HTML 5 allows the meta tag to be placed in head or body.
['<html><body><meta http-equiv="refresh" content="0;url=http://www.example.com/redirected"/></body></html>', 'http://www.example.com/redirected'],
// Valid meta refresh should not be followed if disabled.
['<html><head><meta http-equiv="refresh" content="0;URL=http://www.example.com/redirected"/></head></html>', 'http://www.example.com/foo/foobar', FALSE],
'drupal-1' => ['<html><head><meta http-equiv="Refresh" content="0; URL=/update.php/start?id=2&op=do_nojs" /></body></html>', 'http://www.example.com/update.php/start?id=2&op=do_nojs'],
'drupal-2' => ['<html><head><noscript><meta http-equiv="Refresh" content="0; URL=/update.php/start?id=2&op=do_nojs" /></noscript></body></html>', 'http://www.example.com/update.php/start?id=2&op=do_nojs'],
];
}
/**
* @covers ::request
*/
public function testBackForwardMetaRefresh() {
$client = new TestClient();
$client->followMetaRefresh();
// First request.
$client->request('GET', 'http://www.example.com/first-page');
$content = '<html><head><meta http-equiv="Refresh" content="0; URL=/refreshed" /></body></html>';
$client->setNextResponse(new Response($content, 200));
$client->request('GET', 'http://www.example.com/refresh-from-here');
$this->assertEquals('http://www.example.com/refreshed', $client->getRequest()->getUri());
$client->back();
$this->assertEquals('http://www.example.com/first-page', $client->getRequest()->getUri());
$client->forward();
$this->assertEquals('http://www.example.com/refreshed', $client->getRequest()->getUri());
}
}
/**
* 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;
}
}
<?php
namespace Drupal\BuildTests\Framework\Tests;
use Drupal\BuildTests\Framework\ExternalCommandRequirementsTrait;
use PHPUnit\Framework\SkippedTestError;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\BuildTests\Framework\ExternalCommandRequirementsTrait
* @group Build
*/
class ExternalCommandRequirementTest extends TestCase {
/**
* @covers ::checkExternalCommandRequirements
*/
public function testCheckExternalCommandRequirementsNotAvailable() {
$requires = new UsesCommandRequirements();
$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();
$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(sprintf('The external command should be available: %s', $exception->getMessage()));
}
}
/**
* @covers ::checkClassCommandRequirements
*/
public function testClassRequiresAvailable() {
$requires = new ClassRequiresAvailable();
$ref_check = new \ReflectionMethod($requires, 'checkClassCommandRequirements');
$ref_check->setAccessible(TRUE);
// Use a try/catch block because otherwise PHPUnit might think this test is
// legitimately skipped.
try {
$this->assertNull($ref_check->invoke($requires));
}
catch (SkippedTestError $exception) {
$this->fail(sprintf('The external command should be available: %s', $exception->getMessage()));
}
}
/**
* @covers ::checkClassCommandRequirements
*/
public function testClassRequiresUnavailable() {
$requires = new ClassRequiresUnavailable();
$ref_check = new \ReflectionMethod($requires, 'checkClassCommandRequirements');
$ref_check->setAccessible(TRUE);
// Use a try/catch block because otherwise PHPUnit might think this test is
// legitimately skipped.
try {
$this->assertNull($ref_check->invoke($requires));
$this->fail('Unavailable external command requirement should throw a skipped test error exception.');
}
catch (SkippedTestError $exception) {
$this->assertEquals('Required external commands: unavailable_command', $exception->getMessage());
}
}
/**
* @covers ::checkMethodCommandRequirements
*/
public function testMethodRequiresAvailable() {