diff --git a/core/lib/Drupal/Core/Test/RunTests/TestFileParser.php b/core/lib/Drupal/Core/Test/RunTests/TestFileParser.php new file mode 100644 index 0000000000000000000000000000000000000000..afd92df5e428857e46d9713878b419dc59213d86 --- /dev/null +++ b/core/lib/Drupal/Core/Test/RunTests/TestFileParser.php @@ -0,0 +1,61 @@ +<?php + +namespace Drupal\Core\Test\RunTests; + +use Drupal\simpletest\TestBase; +use PHPUnit\Framework\TestCase; + +/** + * Parses class names from PHP files without loading them. + * + * @internal + */ +class TestFileParser { + + /** + * Gets the classes from a PHP file. + * + * @param string $file + * The path to the file to parse. + * + * @return string[] + * Array of fully qualified class names within the PHP file. + */ + public function getTestListFromFile($file) { + $test_list = $this->parseContents(file_get_contents($file)); + return array_filter($test_list, function ($class) { + return (is_subclass_of($class, TestCase::class) || is_subclass_of($class, TestBase::class)); + }); + } + + /** + * Parse class names out of PHP file contents. + * + * @param string $contents + * The contents of a PHP file. + * + * @return string[] + * Array of fully qualified class names within the PHP file contents. + */ + protected function parseContents($contents) { + // Extract a potential namespace. + $namespace = FALSE; + if (preg_match('@^\s*namespace ([^ ;]+)@m', $contents, $matches)) { + $namespace = $matches[1]; + } + $test_list = []; + // Extract all class names. Abstract classes are excluded on purpose. + preg_match_all('@^\s*(?!abstract\s+)(?:final\s+|\s*)class ([^ ]+)@m', $contents, $matches); + if (!$namespace) { + $test_list = $matches[1]; + } + else { + foreach ($matches[1] as $class_name) { + $namespace_class = $namespace . '\\' . $class_name; + $test_list[] = $namespace_class; + } + } + return $test_list; + } + +} diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh index c3f9d1a73aa8ffbd536735bfa109bbb203f6a88e..9d3f3894ebae1730b2ddd643fefe8ef0b2e86e7c 100755 --- a/core/scripts/run-tests.sh +++ b/core/scripts/run-tests.sh @@ -16,6 +16,7 @@ use Drupal\Core\StreamWrapper\PublicStream; use Drupal\Core\Test\EnvironmentCleaner; use Drupal\Core\Test\PhpUnitTestRunner; +use Drupal\Core\Test\RunTests\TestFileParser; use Drupal\Core\Test\TestDatabase; use Drupal\Core\Test\TestRunnerKernel; use Drupal\simpletest\Form\SimpletestResultsForm; @@ -1054,31 +1055,13 @@ function simpletest_script_get_test_list() { } elseif ($args['file']) { // Extract test case class names from specified files. + $parser = new TestFileParser(); foreach ($args['test_names'] as $file) { if (!file_exists($file)) { simpletest_script_print_error('File not found: ' . $file); exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); } - $content = file_get_contents($file); - // Extract a potential namespace. - $namespace = FALSE; - if (preg_match('@^namespace ([^ ;]+)@m', $content, $matches)) { - $namespace = $matches[1]; - } - // Extract all class names. - // Abstract classes are excluded on purpose. - preg_match_all('@^class ([^ ]+)@m', $content, $matches); - if (!$namespace) { - $test_list = array_merge($test_list, $matches[1]); - } - else { - foreach ($matches[1] as $class_name) { - $namespace_class = $namespace . '\\' . $class_name; - if (is_subclass_of($namespace_class, '\Drupal\simpletest\TestBase') || is_subclass_of($namespace_class, TestCase::class)) { - $test_list[] = $namespace_class; - } - } - } + $test_list = array_merge($test_list, $parser->getTestListFromFile($file)); } } elseif ($args['directory']) { @@ -1112,27 +1095,9 @@ function simpletest_script_get_test_list() { $files[$filename] = $filename; } } + $parser = new TestFileParser(); foreach ($files as $file) { - $content = file_get_contents($file); - // Extract a potential namespace. - $namespace = FALSE; - if (preg_match('@^\s*namespace ([^ ;]+)@m', $content, $matches)) { - $namespace = $matches[1]; - } - // Extract all class names. - // Abstract classes are excluded on purpose. - preg_match_all('@^\s*class ([^ ]+)@m', $content, $matches); - if (!$namespace) { - $test_list = array_merge($test_list, $matches[1]); - } - else { - foreach ($matches[1] as $class_name) { - $namespace_class = $namespace . '\\' . $class_name; - if (is_subclass_of($namespace_class, '\Drupal\simpletest\TestBase') || is_subclass_of($namespace_class, TestCase::class)) { - $test_list[] = $namespace_class; - } - } - } + $test_list = array_merge($test_list, $parser->getTestListFromFile($file)); } } else { diff --git a/core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php b/core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php new file mode 100644 index 0000000000000000000000000000000000000000..5ba0f83c489afc8f38b7a16f43382cce2b8041bc --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php @@ -0,0 +1,99 @@ +<?php + +namespace Drupal\Tests\Core\Test\RunTests; + +use Drupal\Core\Test\RunTests\TestFileParser; +use Drupal\Tests\UnitTestCase; + +/** + * @coversDefaultClass \Drupal\Core\Test\RunTests\TestFileParser + * @group Test + * @group RunTests + */ +class TestFileParserTest extends UnitTestCase { + + public function provideTestFileContents() { + return [ + 'empty' => [[], ''], + 'no-namespace' => [['ConcreteClass'], + <<< 'NO_NAMESPACE' +<?php + +class ConcreteClass {} +NO_NAMESPACE + ], + 'concrete' => [['Namespace\Is\Complex\ConcreteClass'], + <<< 'CONCRETE_CLASS' +<?php + +namespace Namespace\Is\Complex; + +class ConcreteClass {} +CONCRETE_CLASS + ], + 'abstract' => [[], + <<< 'ABSTRACT_CLASS' +<?php +namespace Namespace\Is\Complex; + +abstract class AbstractClass {} +ABSTRACT_CLASS + ], + 'final' => [['Namespace\Is\Complex\FinalClass'], + <<< 'FINAL_CLASS' +<?php +namespace Namespace\Is\Complex; + +final class FinalClass {} +FINAL_CLASS + ], + 'compound_declarations' => [[ + 'Namespace\Is\Complex\FinalClass', + 'Namespace\Is\Complex\AnotherClass', + ], + <<< 'COMPOUND' +<?php +namespace Namespace\Is\Complex; + +final class FinalClass {} + +class AnotherClass {} +COMPOUND + ], + ]; + } + + /** + * @covers ::parseContents + * @dataProvider provideTestFileContents + */ + public function testParseContents($expected, $contents) { + $parser = new TestFileParser(); + + $ref_parse = new \ReflectionMethod($parser, 'parseContents'); + $ref_parse->setAccessible(TRUE); + + $this->assertSame($expected, $ref_parse->invoke($parser, $contents)); + } + + /** + * @covers ::getTestListFromFile + */ + public function testGetTestListFromFile() { + $parser = new TestFileParser(); + $this->assertArrayEquals( + ['Drupal\Tests\Core\Test\RunTests\TestFileParserTest'], + $parser->getTestListFromFile(__FILE__) + ); + // This WebTestBase test will eventually move, so we'll need to update it. + $this->assertArrayEquals( + ['Drupal\simpletest\Tests\TimeZoneTest'], + $parser->getTestListFromFile(__DIR__ . '/../../../../../../modules/simpletest/src/Tests/TimeZoneTest.php') + ); + // Not a test. + $this->assertEmpty( + $parser->getTestListFromFile(__DIR__ . '/../../../AssertHelperTrait.php') + ); + } + +}