Commit b236cc00 authored by catch's avatar catch
Browse files

Revert "Issue #3497431 by mondrake, catch, larowlan, godotislate: Deprecate...

Revert "Issue #3497431 by mondrake, catch, larowlan, godotislate: Deprecate TestDiscovery test file scanning, use PHPUnit API instead"

This reverts commit f955ab18.

(cherry picked from commit 75bf2e53)
parent 4193887a
Loading
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -104,7 +104,7 @@ variables:
  script:
    - sudo -u www-data -E -H composer run-script drupal-phpunit-upgrade-check
    # Need to pass this along directly.
    - sudo -u www-data -E -H php ./core/scripts/run-tests.sh --debug-discovery --color --keep-results --types "$TESTSUITE" --concurrency "$CONCURRENCY" --repeat "1" --sqlite "./sites/default/files/tests.sqlite" --dburl $SIMPLETEST_DB --url $SIMPLETEST_BASE_URL --verbose --non-html --all --ci-parallel-node-index $CI_PARALLEL_NODE_INDEX --ci-parallel-node-total $CI_PARALLEL_NODE_TOTAL
    - sudo -u www-data -E -H php ./core/scripts/run-tests.sh --color --keep-results --types "$TESTSUITE" --concurrency "$CONCURRENCY" --repeat "1" --sqlite "./sites/default/files/tests.sqlite" --dburl $SIMPLETEST_DB --url $SIMPLETEST_BASE_URL --verbose --non-html --all --ci-parallel-node-index $CI_PARALLEL_NODE_INDEX --ci-parallel-node-total $CI_PARALLEL_NODE_TOTAL

.run-repeat-class-test: &run-repeat-class-test
  script:
@@ -234,7 +234,7 @@ variables:
  script:
    - sudo -u www-data -E -H composer run-script drupal-phpunit-upgrade-check
    # Run a small subset of tests to prove non W3C testing still works.
    - sudo -u www-data -E -H php ./core/scripts/run-tests.sh --debug-discovery --color --keep-results --types "$TESTSUITE" --concurrency "$CONCURRENCY" --repeat "1" --sqlite "./sites/default/files/tests.sqlite" --dburl $SIMPLETEST_DB --url $SIMPLETEST_BASE_URL --verbose --non-html javascript
    - sudo -u www-data -E -H php ./core/scripts/run-tests.sh --color --keep-results --types "$TESTSUITE" --concurrency "$CONCURRENCY" --repeat "1" --sqlite "./sites/default/files/tests.sqlite" --dburl $SIMPLETEST_DB --url $SIMPLETEST_BASE_URL --verbose --non-html javascript

  after_script:
    - sed -i "s#$CI_PROJECT_DIR/##" ./sites/default/files/simpletest/phpunit-*.xml || true

core/.phpunit-next.xml

deleted100644 → 0
+0 −164
Original line number Diff line number Diff line
<?xml version="1.0" encoding="UTF-8"?>


<!-- This is a near-copy of phpunit.xml.dist, used to test with upcoming
     PHPUnit versions.

     Current differences:
     * for PHPUnit 11: removed duplicate directories from <testsuites>,
       required in PHPUnit 10, but that cause duplication warnings in
       PHPUnit 11.
-->


<!-- For how to customize PHPUnit configuration, see core/tests/README.md. -->
<!-- TODO set checkForUnintentionallyCoveredCode="true" once https://www.drupal.org/node/2626832 is resolved. -->
<!-- PHPUnit expects functional tests to be run with either a privileged user
 or your current system user. See core/tests/README.md and
 https://www.drupal.org/node/2116263 for details.
-->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         bootstrap="tests/bootstrap.php"
         colors="true"
         beStrictAboutTestsThatDoNotTestAnything="true"
         beStrictAboutOutputDuringTests="true"
         beStrictAboutChangesToGlobalState="true"
         failOnRisky="true"
         failOnWarning="true"
         displayDetailsOnTestsThatTriggerErrors="true"
         displayDetailsOnTestsThatTriggerWarnings="true"
         displayDetailsOnTestsThatTriggerDeprecations="true"
         cacheResult="false"
         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
         cacheDirectory=".phpunit.cache">
  <php>
    <!-- Set error reporting to E_ALL. -->
    <ini name="error_reporting" value="32767"/>
    <!-- Do not limit the amount of memory tests take to run. -->
    <ini name="memory_limit" value="-1"/>
    <!-- Example SIMPLETEST_BASE_URL value: http://localhost -->
    <env name="SIMPLETEST_BASE_URL" value=""/>
    <!-- Example SIMPLETEST_DB value: mysql://username:password@localhost/database_name#table_prefix -->
    <env name="SIMPLETEST_DB" value=""/>
    <!-- By default, browser tests will output links that use the base URL set
     in SIMPLETEST_BASE_URL. However, if your SIMPLETEST_BASE_URL is an internal
     path (such as may be the case in a virtual or Docker-based environment),
     you can set the base URL used in the browser test output links to something
     reachable from your host machine here. This will allow you to follow them
     directly and view the output. -->
    <env name="BROWSERTEST_OUTPUT_BASE_URL" value=""/>
    <!-- The environment variable SYMFONY_DEPRECATIONS_HELPER is used to configure
      the behavior of the deprecation tests.
      Drupal core's testing framework is setting this variable to its defaults.
      Projects with their own requirements need to manage this variable
      explicitly.
    -->
    <!-- To disable deprecation testing completely uncomment the next line. -->
    <!-- <env name="SYMFONY_DEPRECATIONS_HELPER" value="disabled"/> -->
    <!-- Deprecation errors can be selectively ignored by specifying a file of
      regular expression patterns for exclusion.
      Uncomment the line below to specify a custom deprecations ignore file.
      NOTE: it may be required to specify the full path to the file to run tests
      correctly.
    -->
    <!-- <env name="SYMFONY_DEPRECATIONS_HELPER" value="ignoreFile=.deprecation-ignore.txt"/> -->
    <!-- Example for changing the driver class for mink tests MINK_DRIVER_CLASS value: 'Drupal\FunctionalJavascriptTests\DrupalSelenium2Driver' -->
    <env name="MINK_DRIVER_CLASS" value=""/>
    <!-- Example for changing the driver args to mink tests MINK_DRIVER_ARGS value: '["http://127.0.0.1:8510"]' -->
    <env name="MINK_DRIVER_ARGS" value=""/>
    <!-- Example for changing the driver args to webdriver tests MINK_DRIVER_ARGS_WEBDRIVER value: '["chrome", { "goog:chromeOptions": { "w3c": false } }, "http://localhost:4444/wd/hub"]' For using the Firefox browser, replace "chrome" with "firefox" -->
    <env name="MINK_DRIVER_ARGS_WEBDRIVER" value=""/>
  </php>
  <extensions>
    <!-- Functional tests HTML output logging. -->
    <bootstrap class="Drupal\TestTools\Extension\HtmlLogging\HtmlOutputLogger">
      <!-- The directory where the browser output will be stored. If a relative
        path is specified, it will be relative to the current working directory
        of the process running the PHPUnit CLI. In CI environments, this can be
        overridden by the value set for the "BROWSERTEST_OUTPUT_DIRECTORY"
        environment variable.
      -->
      <parameter name="outputDirectory" value="sites/simpletest/browser_output"/>
      <!-- By default browser tests print the individual links in the test run
        report. To avoid overcrowding the output in CI environments, you can
        set the "verbose" parameter or the "BROWSERTEST_OUTPUT_VERBOSE"
        environment variable to "false". In GitLabCI, the output is saved
        anyway as an artifact that can be browsed or downloaded from Gitlab.
      -->
      <parameter name="verbose" value="true"/>
    </bootstrap>
    <!-- Debug dump() printer. -->
    <bootstrap class="Drupal\TestTools\Extension\Dump\DebugDump">
      <parameter name="colors" value="true"/>
      <parameter name="printCaller" value="true"/>
    </bootstrap>
  </extensions>
  <testsuites>
    <testsuite name="unit-component">
      <directory>tests/Drupal/Tests/Component</directory>
    </testsuite>
    <testsuite name="unit">
      <directory>tests/Drupal/Tests</directory>
      <exclude>tests/Drupal/Tests/Component</exclude>
      <directory>modules/**/tests/src/Unit</directory>
      <directory>profiles/**/tests/src/Unit</directory>
      <directory>themes/**/tests/src/Unit</directory>
      <directory>../modules/**/tests/src/Unit</directory>
      <directory>../profiles/**/tests/src/Unit</directory>
      <directory>../themes/**/tests/src/Unit</directory>
    </testsuite>
    <testsuite name="kernel">
      <directory>tests/Drupal/KernelTests</directory>
      <directory>modules/**/tests/src/Kernel</directory>
      <directory>recipes/*/tests/src/Kernel</directory>
      <directory>profiles/**/tests/src/Kernel</directory>
      <directory>themes/**/tests/src/Kernel</directory>
      <directory>../modules/**/tests/src/Kernel</directory>
      <directory>../profiles/**/tests/src/Kernel</directory>
      <directory>../themes/**/tests/src/Kernel</directory>
    </testsuite>
    <testsuite name="functional">
      <directory>tests/Drupal/FunctionalTests</directory>
      <directory>modules/**/tests/src/Functional</directory>
      <directory>profiles/**/tests/src/Functional</directory>
      <directory>recipes/*/tests/src/Functional</directory>
      <directory>themes/**/tests/src/Functional</directory>
      <directory>../modules/**/tests/src/Functional</directory>
      <directory>../profiles/**/tests/src/Functional</directory>
      <directory>../themes/**/tests/src/Functional</directory>
    </testsuite>
    <testsuite name="functional-javascript">
      <directory>tests/Drupal/FunctionalJavascriptTests</directory>
      <directory>modules/**/tests/src/FunctionalJavascript</directory>
      <directory>recipes/*/tests/src/FunctionalJavascript</directory>
      <directory>profiles/**/tests/src/FunctionalJavascript</directory>
      <directory>themes/**/tests/src/FunctionalJavascript</directory>
      <directory>../modules/**/tests/src/FunctionalJavascript</directory>
      <directory>../profiles/**/tests/src/FunctionalJavascript</directory>
      <directory>../themes/**/tests/src/FunctionalJavascript</directory>
    </testsuite>
    <testsuite name="build">
      <directory>tests/Drupal/BuildTests</directory>
      <directory>modules/**/tests/src/Build</directory>
    </testsuite>
  </testsuites>
  <!-- Settings for coverage reports. -->
  <source ignoreSuppressionOfDeprecations="true">
    <include>
      <directory>./includes</directory>
      <directory>./lib</directory>
      <directory>./modules</directory>
      <directory>../modules</directory>
      <directory>../sites</directory>
    </include>
    <exclude>
      <directory>./lib/Drupal/Component</directory>
      <directory>./modules/*/src/Tests</directory>
      <directory>./modules/*/tests</directory>
      <directory>../modules/*/src/Tests</directory>
      <directory>../modules/*/tests</directory>
      <directory>../modules/*/*/src/Tests</directory>
      <directory>../modules/*/*/tests</directory>
    </exclude>
  </source>
</phpunit>
+0 −346
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\Core\Test;

use Drupal\Core\Test\Exception\MissingGroupException;
use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion;
use PHPUnit\Framework\DataProviderTestSuite;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestSuite;
use PHPUnit\TextUI\Configuration\Builder;
use PHPUnit\TextUI\Configuration\TestSuiteBuilder;

/**
 * Discovers available tests using the PHPUnit API.
 *
 * @internal
 */
class PhpUnitTestDiscovery {

  /**
   * The map of legacy test suite identifiers to phpunit.xml ones.
   *
   * @var array<string,string>
   */
  private array $map = [
    'PHPUnit-FunctionalJavascript' => 'functional-javascript',
    'PHPUnit-Functional' => 'functional',
    'PHPUnit-Kernel' => 'kernel',
    'PHPUnit-Unit' => 'unit',
    'PHPUnit-Unit-Component' => 'unit-component',
    'PHPUnit-Build' => 'build',
  ];

  /**
   * The reverse map of legacy test suite identifiers to phpunit.xml ones.
   *
   * @var array<string,string>
   */
  private array $reverseMap;

  /**
   * The warnings generated during the discovery.
   *
   * @var list<string>
   */
  private array $warnings = [];

  public function __construct(
    private string $configurationFilePath,
  ) {
    $this->reverseMap = array_flip($this->map);
  }

  /**
   * Discovers available tests.
   *
   * @param string|null $extension
   *   (optional) The name of an extension to limit discovery to; e.g., 'node'.
   * @param list<string> $testSuites
   *   (optional) An array of PHPUnit test suites to filter the discovery for.
   * @param string|null $directory
   *   (optional) Limit discovered tests to a specific directory.
   *
   * @return array<string<array<class-string, array{name: class-string, description: string, group: string|int, groups: list<string|int>, type: string, file: string, tests_count: positive-int}>>>
   *   An array of test groups keyed by the group name. Each test group is an
   *   array of test class information arrays as returned by
   *   ::getTestClassInfo(), keyed by test class. If a test class belongs to
   *   multiple groups, it will appear under all group keys it belongs to.
   */
  public function getTestClasses(?string $extension = NULL, array $testSuites = [], ?string $directory = NULL): array {
    $this->warnings = [];

    $args = ['--configuration', $this->configurationFilePath];

    if (!empty($testSuites)) {
      // Convert $testSuites from Drupal's legacy syntax to the syntax used in
      // phpunit.xml, that is necessary to PHPUnit to be able to apply the
      // test suite filter. For example, 'PHPUnit-Unit' to 'unit'.
      $tmp = [];
      foreach ($testSuites as $i) {
        if (!is_string($i)) {
          throw new \InvalidArgumentException("Test suite must be a string");
        }
        if (str_contains($i, ' ')) {
          throw new \InvalidArgumentException("Test suite name '{$i}' is invalid");
        }
        $tmp[] = $this->map[$i] ?? $i;
      }
      $args[] = '--testsuite=' . implode(',', $tmp);
    }

    if ($directory !== NULL) {
      $args[] = $directory;
    }

    $phpUnitConfiguration = (new Builder())->build($args);

    // TestSuiteBuilder calls the test data providers during the discovery.
    // Data providers may be changing the Drupal service container, which leads
    // to potential issues. We save the current container before running the
    // discovery, and in case a change is detected, reset it and raise
    // warnings so that developers can tune their data provider code.
    if (\Drupal::hasContainer()) {
      $container = \Drupal::getContainer();
      $containerObjectId = spl_object_id($container);
    }
    $phpUnitTestSuite = (new TestSuiteBuilder())->build($phpUnitConfiguration);
    if (isset($containerObjectId) && $containerObjectId !== spl_object_id(\Drupal::getContainer())) {
      $this->warnings[] = '*** The service container was changed during the test discovery ***';
      $this->warnings[] = 'Probably a test data provider method called \\Drupal::setContainer.';
      $this->warnings[] = 'Ensure that all the data providers restore the original container before returning data.';
      assert(isset($container));
      \Drupal::setContainer($container);
    }

    $list = $directory === NULL ?
      $this->getTestList($phpUnitTestSuite, $extension) :
      $this->getTestListLimitedToDirectory($phpUnitTestSuite, $extension, $testSuites);

    // Sort the groups and tests within the groups by name.
    uksort($list, 'strnatcasecmp');
    foreach ($list as &$tests) {
      uksort($tests, 'strnatcasecmp');
    }

    return $list;
  }

  /**
   * Discovers all class files in all available extensions.
   *
   * @param string|null $extension
   *   (optional) The name of an extension to limit discovery to; e.g., 'node'.
   * @param string|null $directory
   *   (optional) Limit discovered tests to a specific directory.
   *
   * @return array
   *   A classmap containing all discovered class files; i.e., a map of
   *   fully-qualified classnames to path names.
   */
  public function findAllClassFiles(?string $extension = NULL, ?string $directory = NULL): array {
    $testClasses = $this->getTestClasses($extension, [], $directory);
    $classMap = [];
    foreach ($testClasses as $group) {
      foreach ($group as $className => $info) {
        $classMap[$className] = $info['file'];
      }
    }
    return $classMap;
  }

  /**
   * Returns the warnings generated during the discovery.
   *
   * @return list<string>
   *   The warnings.
   */
  public function getWarnings(): array {
    return $this->warnings;
  }

  /**
   * Returns a list of tests from a TestSuite object.
   *
   * @param \PHPUnit\Framework\TestSuite $phpUnitTestSuite
   *   The TestSuite object returned by PHPUnit test discovery.
   * @param string|null $extension
   *   The name of an extension to limit discovery to; e.g., 'node'.
   *
   * @return array<string<array<class-string, array{name: class-string, description: string, group: string|int, groups: list<string|int>, type: string, file: string, tests_count: positive-int}>>>
   *   An array of test groups keyed by the group name. Each test group is an
   *   array of test class information arrays as returned by
   *   ::getTestClassInfo(), keyed by test class. If a test class belongs to
   *   multiple groups, it will appear under all group keys it belongs to.
   */
  private function getTestList(TestSuite $phpUnitTestSuite, ?string $extension): array {
    $list = [];
    foreach ($phpUnitTestSuite->tests() as $testSuite) {
      foreach ($testSuite->tests() as $testClass) {
        if ($extension !== NULL && !str_starts_with($testClass->name(), "Drupal\\Tests\\{$extension}\\")) {
          continue;
        }

        $item = $this->getTestClassInfo(
          $testClass,
          $this->reverseMap[$testSuite->name()] ?? $testSuite->name(),
        );

        foreach ($item['groups'] as $group) {
          $list[$group][$item['name']] = $item;
        }
      }
    }
    return $list;
  }

  /**
   * Returns a list of tests from a TestSuite object limited to a directory.
   *
   * @param \PHPUnit\Framework\TestSuite $phpUnitTestSuite
   *   The TestSuite object returned by PHPUnit test discovery.
   * @param string|null $extension
   *   The name of an extension to limit discovery to; e.g., 'node'.
   * @param list<string> $testSuites
   *   An array of PHPUnit test suites to filter the discovery for.
   *
   * @return array<string<array<class-string, array{name: class-string, description: string, group: string|int, groups: list<string|int>, type: string, file: string, tests_count: positive-int}>>>
   *   An array of test groups keyed by the group name. Each test group is an
   *   array of test class information arrays as returned by
   *   ::getTestClassInfo(), keyed by test class. If a test class belongs to
   *   multiple groups, it will appear under all group keys it belongs to.
   */
  private function getTestListLimitedToDirectory(TestSuite $phpUnitTestSuite, ?string $extension, array $testSuites): array {
    $list = [];

    // In this case, PHPUnit found a single test class to run tests for.
    if ($phpUnitTestSuite->isForTestClass()) {
      if ($extension !== NULL && !str_starts_with($phpUnitTestSuite->name(), "Drupal\\Tests\\{$extension}\\")) {
        return [];
      }

      // Take the test suite name from the class namespace.
      $testSuite = 'PHPUnit-' . TestDiscovery::getPhpunitTestSuite($phpUnitTestSuite->name());
      if (!empty($testSuites) && !in_array($testSuite, $testSuites, TRUE)) {
        return [];
      }

      $item = $this->getTestClassInfo($phpUnitTestSuite, $testSuite);

      foreach ($item['groups'] as $group) {
        $list[$group][$item['name']] = $item;
      }
      return $list;
    }

    // Multiple test classes were found.
    $list = [];
    foreach ($phpUnitTestSuite->tests() as $testClass) {
      if ($extension !== NULL && !str_starts_with($testClass->name(), "Drupal\\Tests\\{$extension}\\")) {
        continue;
      }

      // Take the test suite name from the class namespace.
      $testSuite = 'PHPUnit-' . TestDiscovery::getPhpunitTestSuite($testClass->name());
      if (!empty($testSuites) && !in_array($testSuite, $testSuites, TRUE)) {
        continue;
      }

      $item = $this->getTestClassInfo($testClass, $testSuite);

      foreach ($item['groups'] as $group) {
        $list[$group][$item['name']] = $item;
      }
    }
    return $list;

  }

  /**
   * Returns the test class information.
   *
   * @param \PHPUnit\Framework\Test $testClass
   *   The test class.
   * @param string $testSuite
   *   The test suite of this test class.
   *
   * @return array{name: class-string, description: string, group: string|int, groups: list<string|int>, type: string, file: string, tests_count: positive-int}
   *   The test class information.
   */
  private function getTestClassInfo(Test $testClass, string $testSuite): array {
    $reflection = new \ReflectionClass($testClass->name());

    // Let PHPUnit API return the groups, as it will deal transparently with
    // annotations or attributes, but skip groups generated by PHPUnit
    // internally and starting with a double underscore prefix.
    if (RunnerVersion::getMajor() < 11) {
      $groups = array_filter($testClass->groups(), function (string $value): bool {
        return !str_starts_with($value, '__phpunit');
      });
    }
    else {
      // In PHPUnit 11+, we need to coalesce the groups from individual tests
      // as they may not be available from the test class level (when tests are
      // backed by data providers).
      $tmp = [];
      foreach ($testClass as $test) {
        if ($test instanceof DataProviderTestSuite) {
          foreach ($test as $testWithData) {
            $tmp = array_merge($tmp, $testWithData->groups());
          }
        }
        else {
          $tmp = array_merge($tmp, $test->groups());
        }
      }
      $groups = array_filter(array_unique($tmp), function (string $value): bool {
        return !str_starts_with($value, '__phpunit');
      });
    }
    if (empty($groups)) {
      throw new MissingGroupException(sprintf('Missing group metadata in test class %s', $testClass->name()));
    }

    // Let PHPUnit API return the class coverage information.
    $test = $testClass;
    while (!$test instanceof TestCase) {
      $test = $test->tests()[0];
    }
    if (($metadata = $test->valueObjectForEvents()->metadata()->isCoversClass()) && $metadata->isNotEmpty()) {
      $description = sprintf('Tests %s.', $metadata->asArray()[0]->className());
    }
    elseif (($metadata = $test->valueObjectForEvents()->metadata()->isCoversDefaultClass()) && $metadata->isNotEmpty()) {
      $description = sprintf('Tests %s.', $metadata->asArray()[0]->className());
    }
    else {
      $description = TestDiscovery::parseTestClassSummary($reflection->getDocComment());
    }

    // Find the test cases count.
    $count = 0;
    foreach ($testClass->tests() as $testCase) {
      if ($testCase instanceof TestCase) {
        // If it's a straight test method, counts 1.
        $count++;
      }
      else {
        // It's a data provider test suite, count 1 per data set provided.
        $count += count($testCase->tests());
      }
    }

    return [
      'name' => $testClass->name(),
      'group' => $groups[0],
      'groups' => $groups,
      'type' => $testSuite,
      'description' => $description,
      'file' => $reflection->getFileName(),
      'tests_count' => $count,
    ];
  }

}
+0 −7
Original line number Diff line number Diff line
@@ -4,16 +4,9 @@

use PHPUnit\Framework\TestCase;

@trigger_error('Drupal\Core\Test\RunTests\TestFileParser is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3447698', E_USER_DEPRECATED);

/**
 * Parses class names from PHP files without loading them.
 *
 * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no
 *   replacement.
 *
 * @see https://www.drupal.org/node/3447698
 *
 * @internal
 */
class TestFileParser {
+1 −40

File changed.

Preview size limit exceeded, changes collapsed.

Loading