Commit 7ff27245 authored by catch's avatar catch

Issue #2422019 by alexpott, dawehner: Don't use reflection for parsing test annotations

parent 4709679e
......@@ -14,6 +14,7 @@
* Tests the migrate entity.
*
* @coversDefaultClass \Drupal\migrate\Entity\Migration
* @group migrate
*/
class MigrationTest extends UnitTestCase {
......
......@@ -144,7 +144,7 @@ function simpletest_run_tests($test_list) {
// Get the info for the first test being run.
$first_test = reset($test_list);
$info = TestDiscovery::getTestInfo(new \ReflectionClass($first_test));
$info = TestDiscovery::getTestInfo($first_test);
$batch = array(
'title' => t('Running tests'),
......@@ -320,7 +320,7 @@ function _simpletest_batch_operation($test_list_init, $test_id, &$context) {
$test = new $test_class($test_id);
$test->run();
$size = count($test_list);
$info = TestDiscovery::getTestInfo(new \ReflectionClass($test));
$info = TestDiscovery::getTestInfo($test_class);
\Drupal::moduleHandler()->invokeAll('test_finished', array($test->results));
......
<?php
/**
* @file
* Contains \Drupal\simpletest\Exception\MissingGroupException.
*/
namespace Drupal\simpletest\Exception;
/**
* Exception thrown when a simpletest class is missing an @group annotation.
*/
class MissingGroupException extends \LogicException {
}
<?php
/**
* @file
* Contains \Drupal\simpletest\Exception\MissingSummaryLineException.
*/
namespace Drupal\simpletest\Exception;
/**
* Exception thrown when a simpletest class is missing a summary line.
*/
class MissingSummaryLineException extends \LogicException {
}
......@@ -153,7 +153,7 @@ public function buildForm(array $form, FormStateInterface $form_state, $test_id
$form['result']['results'] = array();
foreach ($results as $group => $assertions) {
// Create group details with summary information.
$info = TestDiscovery::getTestInfo(new \ReflectionClass($group));
$info = TestDiscovery::getTestInfo($group);
$form['result']['results'][$group] = array(
'#type' => 'details',
'#title' => $info['name'],
......
......@@ -8,9 +8,14 @@
namespace Drupal\simpletest;
use Composer\Autoload\ClassLoader;
use Doctrine\Common\Annotations\SimpleAnnotationReader;
use Doctrine\Common\Reflection\StaticReflectionParser;
use Drupal\Component\Annotation\Reflection\MockFileFinder;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\simpletest\Exception\MissingGroupException;
use Drupal\simpletest\Exception\MissingSummaryLineException;
use PHPUnit_Util_Test;
/**
......@@ -128,6 +133,9 @@ public function registerTestNamespaces() {
* @todo Add base class groups 'Kernel' + 'Web', complementing 'PHPUnit'.
*/
public function getTestClasses($extension = NULL) {
$reader = new SimpleAnnotationReader();
$reader->addNamespace('Drupal\\simpletest\\Annotation');
if (!isset($extension)) {
if ($this->cacheBackend && $cache = $this->cacheBackend->get('simpletest:discovery:classes')) {
return $cache->data;
......@@ -144,24 +152,16 @@ public function getTestClasses($extension = NULL) {
$this->classLoader->addClassMap($classmap);
foreach ($classmap as $classname => $pathname) {
$finder = MockFileFinder::create($pathname);
$parser = new StaticReflectionParser($classname, $finder, TRUE);
try {
$class = new \ReflectionClass($classname);
}
catch (\ReflectionException $e) {
// Re-throw with expected pathname.
$message = $e->getMessage() . " in expected $pathname";
throw new \ReflectionException($message, $e->getCode(), $e);
}
// Skip interfaces, abstract classes, and traits.
if (!$class->isInstantiable()) {
continue;
$info = static::getTestInfo($classname, $parser->getDocComment());
}
// Skip non-test classes.
if (!$class->isSubclassOf('Drupal\simpletest\TestBase') && !$class->isSubclassOf('PHPUnit_Framework_TestCase')) {
catch (\LogicException $e) {
// If the class is missing a summary line or an @group annotation just
// skip it. Most likely it is an abstract class, trait or test fixture.
continue;
}
$info = static::getTestInfo($class);
// Skip this test class if it requires unavailable modules.
// @todo PHPUnit skips tests with unmet requirements when executing a test
// (instead of excluding them upfront). Refactor test runner to follow
......@@ -275,8 +275,11 @@ public static function scanDirectory($namespace_prefix, $path) {
/**
* Retrieves information about a test class for UI purposes.
*
* @param \ReflectionClass $class
* The reflected test class.
* @param string $class
* The test classname.
* @param string $doc_comment
* (optional) The class PHPDoc comment. If not passed in reflection will be
* used but this is very expensive when parsing all the test classes.
*
* @return array
* An associative array containing:
......@@ -287,79 +290,56 @@ public static function scanDirectory($namespace_prefix, $path) {
* PHPDoc annotations:
* - module: List of Drupal module extension names the test depends on.
*
* @throws \LogicException
* @throws \Drupal\simpletest\Exception\MissingSummaryLineException
* If the class does not have a PHPDoc summary line or @coversDefaultClass
* annotation.
* @throws \LogicException
* @throws \Drupal\simpletest\Exception\MissingGroupException
* If the class does not have a @group annotation.
*/
public static function getTestInfo(\ReflectionClass $class) {
$classname = $class->getName();
public static function getTestInfo($classname, $doc_comment = NULL) {
if (!$doc_comment) {
$reflection = new \ReflectionClass($classname);
$doc_comment = $reflection->getDocComment();
}
$info = array(
'name' => $classname,
);
$annotations = array();
preg_match_all('/^ \* \@([^\s]*) (.*$)/m', $doc_comment, $matches);
if (isset($matches[1])) {
foreach ($matches[1] as $key => $annotation) {
if (!empty($annotations[$annotation])) {
// Only have the first match per annotation. This deals with
// multiple @group annotations.
continue;
}
$annotations[$annotation] = $matches[2][$key];
}
}
// Automatically convert @coversDefaultClass into summary.
$annotations = static::parseTestClassAnnotations($class);
if (isset($annotations['coversDefaultClass'][0])) {
$info['description'] = 'Tests ' . $annotations['coversDefaultClass'][0] . '.';
if (empty($annotations['group'])) {
// Concrete tests must have a group.
throw new MissingGroupException(sprintf('Missing @group annotation in %s', $classname));
}
elseif ($summary = static::parseTestClassSummary($class)) {
$info['description'] = $summary;
// Force all PHPUnit tests into the same group.
if (strpos($classname, 'Drupal\\Tests\\') === 0) {
$info['group'] = 'PHPUnit';
}
else {
throw new \LogicException(sprintf('Missing PHPDoc summary line on %s in %s.', $classname, $class->getFileName()));
$info['group'] = $annotations['group'];
}
// Reduce to @group and @requires.
$info += array_intersect_key($annotations, array('group' => 1, 'requires' => 1));
// @todo Remove legacy getInfo() methods.
if (method_exists($classname, 'getInfo')) {
$legacy_info = $classname::getInfo();
// Derive the primary @group from the namespace to ensure that legacy
// tests are not located in different groups than converted tests.
$classparts = explode('\\', $classname);
if ($classparts[1] === 'Tests') {
if ($classparts[2] === 'Component' || $classparts[2] === 'Core') {
// Drupal\Tests\Component\{group}\...
$info['group'][] = $classparts[3];
}
else {
// Drupal\Tests\{group}\...
$info['group'][] = $classparts[2];
}
}
elseif ($classparts[1] === 'system' && $classparts[3] !== 'System') {
// Drupal\system\Tests\{group}\...
$info['group'][] = $classparts[3];
}
else {
// Drupal\{group}\Tests\...
$info['group'][] = $classparts[1];
}
if (isset($legacy_info['dependencies'])) {
$info += array('requires' => array());
$info['requires'] += array('module' => array());
$info['requires']['module'] = array_merge($info['requires']['module'], $legacy_info['dependencies']);
}
}
// Process @group information.
// @todo Support multiple @groups + change UI to expose a group select
// dropdown to filter tests by group instead of collapsible table rows.
// @see https://www.drupal.org/node/2296615
// @todo Replace single enforced PHPUnit group with base class groups.
if ($class->isSubclassOf('PHPUnit_Framework_TestCase')) {
$info['group'] = 'PHPUnit';
if (!empty($annotations['coversDefaultClass'])) {
$info['description'] = 'Tests ' . $annotations['coversDefaultClass'] . '.';
}
else {
if (empty($info['group'])) {
throw new \LogicException("Missing @group for $classname.");
$info['description'] = static::parseTestClassSummary($doc_comment);
if (empty($info['description'])) {
throw new MissingSummaryLineException(sprintf('Missing PHPDoc summary line in %s', $classname));
}
$info['group'] = reset($info['group']);
}
if (isset($annotations['dependencies'])) {
$info['requires']['module'] = array_map('trim', explode(',', $annotations['dependencies']));
}
return $info;
......@@ -368,32 +348,27 @@ public static function getTestInfo(\ReflectionClass $class) {
/**
* Parses the phpDoc summary line of a test class.
*
* @param \ReflectionClass $class
* The reflected test class.
* @param string $doc_comment.
*
* @return string
* The parsed phpDoc summary line.
* The parsed phpDoc summary line. An empty string is returned if no summary
* line can be parsed.
*/
public static function parseTestClassSummary(\ReflectionClass $class) {
$phpDoc = $class->getDocComment();
public static function parseTestClassSummary($doc_comment) {
// Normalize line endings.
$phpDoc = preg_replace('/\r\n|\r/', '\n', $phpDoc);
$doc_comment = preg_replace('/\r\n|\r/', '\n', $doc_comment);
// Strip leading and trailing doc block lines.
//$phpDoc = trim($phpDoc, "* /\n");
$phpDoc = substr($phpDoc, 4, -4);
$doc_comment = substr($doc_comment, 4, -4);
// Extract actual phpDoc content.
$phpDoc = explode("\n", $phpDoc);
array_walk($phpDoc, function (&$value) {
$value = trim($value, "* /\n");
});
// Extract summary; allowed to it wrap and continue on next line.
list($summary) = explode("\n\n", implode("\n", $phpDoc));
if ($summary === '') {
throw new \LogicException(sprintf('Missing phpDoc on %s.', $class->getName()));
$lines = explode("\n", $doc_comment);
$summary = [];
foreach ($lines as $line) {
if ($line == ' *' || preg_match('/^ \* \@/', $line)) {
break;
}
$summary[] = trim($line, ' *');
}
return $summary;
return implode(' ', $summary);
}
/**
......
......@@ -13,7 +13,7 @@
* This test should not load since it requires a module that is not found.
*
* @group simpletest
* @requires module simpletest_missing_module
* @dependencies simpletest_missing_module
*/
class MissingDependentModuleUnitTest extends KernelTestBase {
......
<?php
namespace Drupal\Tests\simpletest\Unit;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\simpletest\TestDiscovery
* @group simpletest
*/
class TestInfoParsingTest extends UnitTestCase {
/**
* @covers ::getTestInfo
* @dataProvider infoParserProvider
*/
public function testTestInfoParser($expected, $classname, $doc_comment = NULL) {
$info = \Drupal\simpletest\TestDiscovery::getTestInfo($classname, $doc_comment);
$this->assertEquals($expected, $info);
}
public function infoParserProvider() {
$tests[] = [
// Expected result.
[
'name' => 'Drupal\Tests\simpletest\Unit\TestInfoParsingTest',
'group' => 'PHPUnit',
'description' => 'Tests \Drupal\simpletest\TestDiscovery.',
],
// Classname.
'Drupal\Tests\simpletest\Unit\TestInfoParsingTest',
];
$tests[] = [
// Expected result.
[
'name' => 'Drupal\field\Tests\BulkDeleteTest',
'group' => 'field',
'description' => 'Bulk delete storages and fields, and clean up afterwards.',
],
// Classname.
'Drupal\field\Tests\BulkDeleteTest',
// Doc block.
"/**
* Bulk delete storages and fields, and clean up afterwards.
*
* @group field
*/
",
];
// Multiple @group annotations.
$tests[] = [
// Expected result.
[
'name' => 'Drupal\field\Tests\BulkDeleteTest',
'group' => 'Test',
'description' => 'Bulk delete storages and fields, and clean up afterwards.',
],
// Classname.
'Drupal\field\Tests\BulkDeleteTest',
// Doc block.
"/**
* Bulk delete storages and fields, and clean up afterwards.
*
* @group Test
* @group field
*/
",
];
// @dependencies annotation.
$tests[] = [
// Expected result.
[
'name' => 'Drupal\field\Tests\BulkDeleteTest',
'group' => 'field',
'description' => 'Bulk delete storages and fields, and clean up afterwards.',
'requires' => ['module' => ['test']],
],
// Classname.
'Drupal\field\Tests\BulkDeleteTest',
// Doc block.
"/**
* Bulk delete storages and fields, and clean up afterwards.
*
* @dependencies test
* @group field
*/
",
];
// Multiple @dependencies annotation.
$tests[] = [
// Expected result.
[
'name' => 'Drupal\field\Tests\BulkDeleteTest',
'group' => 'field',
'description' => 'Bulk delete storages and fields, and clean up afterwards.',
'requires' => ['module' => ['test', 'test1', 'test2']],
],
// Classname.
'Drupal\field\Tests\BulkDeleteTest',
// Doc block.
"/**
* Bulk delete storages and fields, and clean up afterwards.
*
* @dependencies test, test1,test2
* @group field
*/
",
];
// Multi-line summary line.
$tests[] = [
// Expected result.
[
'name' => 'Drupal\field\Tests\BulkDeleteTest',
'group' => 'field',
'description' => 'Bulk delete storages and fields, and clean up afterwards. And the summary line continues and there is no gap to the annotation.',
],
// Classname.
'Drupal\field\Tests\BulkDeleteTest',
// Doc block.
"/**
* Bulk delete storages and fields, and clean up afterwards. And the summary
* line continues and there is no gap to the annotation.
* @group field
*/
",
];
return $tests;
}
/**
* @covers ::getTestInfo
* @expectedException \Drupal\simpletest\Exception\MissingGroupException
* @expectedExceptionMessage Missing @group annotation in Drupal\field\Tests\BulkDeleteTest
*/
public function testTestInfoParserMissingGroup() {
$classname = 'Drupal\field\Tests\BulkDeleteTest';
$doc_comment = <<<EOT
/**
* Bulk delete storages and fields, and clean up afterwards.
*/
EOT;
\Drupal\simpletest\TestDiscovery::getTestInfo($classname, $doc_comment);
}
/**
* @covers ::getTestInfo
* @expectedException \Drupal\simpletest\Exception\MissingSummaryLineException
* @expectedExceptionMessage Missing PHPDoc summary line in Drupal\field\Tests\BulkDeleteTest
*/
public function testTestInfoParserMissingSummary() {
$classname = 'Drupal\field\Tests\BulkDeleteTest';
$doc_comment = <<<EOT
/**
* @group field
*/
EOT;
\Drupal\simpletest\TestDiscovery::getTestInfo($classname, $doc_comment);
}
}
......@@ -26,6 +26,8 @@
* synchronizations.
*
* @see https://github.com/symfony/symfony/pull/12521
*
* @group Symfony
*/
class ContainerAwareEventDispatcherTest extends AbstractEventDispatcherTest
{
......
......@@ -12,6 +12,7 @@
/**
* @coversDefaultClass \Drupal\Component\Plugin\DefaultFactory
* @group Plugin
*/
class DefaultFactoryTest extends UnitTestCase {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment