Initial Drupal 11 with DDEV setup
This commit is contained in:
		@ -0,0 +1,49 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\TestTools\Comparator;
 | 
			
		||||
 | 
			
		||||
use Drupal\Component\Render\MarkupInterface;
 | 
			
		||||
use SebastianBergmann\Comparator\Comparator;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Compares MarkupInterface objects for equality.
 | 
			
		||||
 */
 | 
			
		||||
class MarkupInterfaceComparator extends Comparator {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function accepts($expected, $actual): bool {
 | 
			
		||||
    // If at least one argument is a MarkupInterface object, we take over and
 | 
			
		||||
    // convert to strings before comparing.
 | 
			
		||||
    return ($expected instanceof MarkupInterface && $actual instanceof MarkupInterface) ||
 | 
			
		||||
      ($expected instanceof MarkupInterface && is_scalar($actual)) ||
 | 
			
		||||
      (is_scalar($expected) && $actual instanceof MarkupInterface);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = FALSE, $ignoreCase = FALSE): void {
 | 
			
		||||
    if (is_scalar($expected) && is_scalar($actual)) {
 | 
			
		||||
      throw new \LogicException(__METHOD__ . '() should not be called directly. Use TestCase::assertEquals() instead');
 | 
			
		||||
    }
 | 
			
		||||
    if (is_array($expected) || is_array($actual)) {
 | 
			
		||||
      throw new \InvalidArgumentException('Expected and actual arguments passed to ' . __METHOD__ . '() must not be arrays');
 | 
			
		||||
    }
 | 
			
		||||
    $expected_safe = (string) $expected;
 | 
			
		||||
    $actual_safe = (string) $actual;
 | 
			
		||||
    $expected_safe_stripped = strip_tags($expected_safe);
 | 
			
		||||
    $actual_safe_stripped = strip_tags($actual_safe);
 | 
			
		||||
    if (!($expected instanceof MarkupInterface && $actual instanceof MarkupInterface)) {
 | 
			
		||||
      if ($expected_safe !== $expected_safe_stripped && $actual_safe !== $actual_safe_stripped) {
 | 
			
		||||
        throw new \RuntimeException("Cannot compare markup between MarkupInterface objects and plain strings");
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    $comparator = $this->factory()->getComparatorFor($expected_safe_stripped, $actual_safe_stripped);
 | 
			
		||||
    $comparator->assertEquals($expected_safe_stripped, $actual_safe_stripped, $delta, $canonicalize, $ignoreCase);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,84 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\TestTools\ErrorHandler;
 | 
			
		||||
 | 
			
		||||
use Drupal\TestTools\Extension\DeprecationBridge\DeprecationHandler;
 | 
			
		||||
use PHPUnit\Event\Code\NoTestCaseObjectOnCallStackException;
 | 
			
		||||
use PHPUnit\Runner\ErrorHandler as PhpUnitErrorHandler;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Drupal's PHPUnit base error handler.
 | 
			
		||||
 *
 | 
			
		||||
 * This code works in coordination with DeprecationHandler.
 | 
			
		||||
 *
 | 
			
		||||
 * This error handler is registered during PHPUnit's runner bootstrap, and is
 | 
			
		||||
 * essentially used to capture deprecations occurring before tests are run (for
 | 
			
		||||
 * example, deprecations triggered by the DebugClassloader). When test runs
 | 
			
		||||
 * are prepared, a test specific TestErrorHandler is activated instead.
 | 
			
		||||
 *
 | 
			
		||||
 * @see \Drupal\TestTools\Extension\DeprecationBridge\DeprecationHandler
 | 
			
		||||
 *
 | 
			
		||||
 * @internal
 | 
			
		||||
 */
 | 
			
		||||
final class BootstrapErrorHandler {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param \PHPUnit\Runner\ErrorHandler $phpUnitErrorHandler
 | 
			
		||||
   *   An instance of PHPUnit's runner own error handler. Any error not
 | 
			
		||||
   *   managed here will fall back to it.
 | 
			
		||||
   */
 | 
			
		||||
  public function __construct(
 | 
			
		||||
    private readonly PhpUnitErrorHandler $phpUnitErrorHandler,
 | 
			
		||||
  ) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Executes when the object is called as a function.
 | 
			
		||||
   *
 | 
			
		||||
   * @param int $errorNumber
 | 
			
		||||
   *   The level of the error raised.
 | 
			
		||||
   * @param string $errorString
 | 
			
		||||
   *   The error message.
 | 
			
		||||
   * @param string $errorFile
 | 
			
		||||
   *   The filename that the error was raised in.
 | 
			
		||||
   * @param int $errorLine
 | 
			
		||||
   *   The line number the error was raised at.
 | 
			
		||||
   *
 | 
			
		||||
   * @return bool
 | 
			
		||||
   *   TRUE to stop error handling, FALSE to let the normal error handler
 | 
			
		||||
   *   continue.
 | 
			
		||||
   */
 | 
			
		||||
  public function __invoke(int $errorNumber, string $errorString, string $errorFile, int $errorLine): bool {
 | 
			
		||||
    if (!DeprecationHandler::isEnabled()) {
 | 
			
		||||
      throw new \RuntimeException(__METHOD__ . '() must not be called if the deprecation handler is not enabled.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // We collect a deprecation no matter what.
 | 
			
		||||
    if (E_USER_DEPRECATED === $errorNumber || E_DEPRECATED === $errorNumber) {
 | 
			
		||||
      $prefix = (error_reporting() & $errorNumber) ? 'Unsilenced deprecation: ' : '';
 | 
			
		||||
      DeprecationHandler::collectActualDeprecation($prefix . $errorString);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If the deprecation handled is one of those in the ignore list, we keep
 | 
			
		||||
    // running.
 | 
			
		||||
    if ((E_USER_DEPRECATED === $errorNumber || E_DEPRECATED === $errorNumber) && DeprecationHandler::isIgnoredDeprecation($errorString)) {
 | 
			
		||||
      return TRUE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // In all other cases (errors, warnings, deprecations to be reported), we
 | 
			
		||||
    // fall back to PHPUnit's error handler, an instance of which was created
 | 
			
		||||
    // when this error handler was created.
 | 
			
		||||
    try {
 | 
			
		||||
      call_user_func($this->phpUnitErrorHandler, $errorNumber, $errorString, $errorFile, $errorLine);
 | 
			
		||||
    }
 | 
			
		||||
    catch (NoTestCaseObjectOnCallStackException) {
 | 
			
		||||
      // If we end up here, it's likely because a test's processing has
 | 
			
		||||
      // finished already and we are processing an error that occurred while
 | 
			
		||||
      // dealing with STDOUT rewinding or truncating. Do nothing.
 | 
			
		||||
    }
 | 
			
		||||
    return TRUE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,75 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\TestTools\ErrorHandler;
 | 
			
		||||
 | 
			
		||||
use Drupal\TestTools\Extension\DeprecationBridge\DeprecationHandler;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Drupal's PHPUnit test level error handler.
 | 
			
		||||
 *
 | 
			
		||||
 * This code works in coordination with DeprecationHandler.
 | 
			
		||||
 *
 | 
			
		||||
 * This error handler is registered during the preparation of a PHPUnit's test,
 | 
			
		||||
 * and is essentially used to capture deprecations occurring during test
 | 
			
		||||
 * executions. When test runs are torn down, the more generic
 | 
			
		||||
 * BootstrapErrorHandler is restored.
 | 
			
		||||
 *
 | 
			
		||||
 * @see \Drupal\TestTools\Extension\DeprecationBridge\DeprecationHandler
 | 
			
		||||
 *
 | 
			
		||||
 * @internal
 | 
			
		||||
 */
 | 
			
		||||
final class TestErrorHandler {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param \Drupal\TestTools\ErrorHandler\BootstrapErrorHandler $parentHandler
 | 
			
		||||
   *   The parent error handler.
 | 
			
		||||
   * @param \PHPUnit\Framework\TestCase $testCase
 | 
			
		||||
   *   The test case being executed.
 | 
			
		||||
   */
 | 
			
		||||
  public function __construct(
 | 
			
		||||
    private readonly BootstrapErrorHandler $parentHandler,
 | 
			
		||||
    private readonly TestCase $testCase,
 | 
			
		||||
  ) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Executes when the object is called as a function.
 | 
			
		||||
   *
 | 
			
		||||
   * @param int $errorNumber
 | 
			
		||||
   *   The level of the error raised.
 | 
			
		||||
   * @param string $errorString
 | 
			
		||||
   *   The error message.
 | 
			
		||||
   * @param string $errorFile
 | 
			
		||||
   *   The filename that the error was raised in.
 | 
			
		||||
   * @param int $errorLine
 | 
			
		||||
   *   The line number the error was raised at.
 | 
			
		||||
   *
 | 
			
		||||
   * @return bool
 | 
			
		||||
   *   TRUE to stop error handling, FALSE to let the normal error handler
 | 
			
		||||
   *   continue.
 | 
			
		||||
   */
 | 
			
		||||
  public function __invoke(int $errorNumber, string $errorString, string $errorFile, int $errorLine): bool {
 | 
			
		||||
    if (!DeprecationHandler::isEnabled()) {
 | 
			
		||||
      throw new \RuntimeException(__METHOD__ . '() must not be called if the deprecation handler is not enabled.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // We are within a test execution. If we have a deprecation and the test is
 | 
			
		||||
    // a deprecation test, than we just collect the deprecation and return to
 | 
			
		||||
    // execution, since deprecations are expected.
 | 
			
		||||
    if ((E_USER_DEPRECATED === $errorNumber || E_DEPRECATED === $errorNumber) && DeprecationHandler::isDeprecationTest($this->testCase)) {
 | 
			
		||||
      $prefix = (error_reporting() & $errorNumber) ? 'Unsilenced deprecation: ' : '';
 | 
			
		||||
      DeprecationHandler::collectActualDeprecation($prefix . $errorString);
 | 
			
		||||
      return TRUE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // In all other cases (errors, warnings, deprecations in normal tests), we
 | 
			
		||||
    // fall back to the parent error handler, which is the one that was
 | 
			
		||||
    // registered in the test runner bootstrap (BootstrapErrorHandler).
 | 
			
		||||
    call_user_func($this->parentHandler, $errorNumber, $errorString, $errorFile, $errorLine);
 | 
			
		||||
    return TRUE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,254 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\TestTools\Extension\DeprecationBridge;
 | 
			
		||||
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Drupal's PHPUnit extension to manage code deprecation.
 | 
			
		||||
 *
 | 
			
		||||
 * This class is a replacement for symfony/phpunit-bridge that does not
 | 
			
		||||
 * support PHPUnit 10. In the future this extension might be dropped if
 | 
			
		||||
 * PHPUnit adds support for all deprecation management needs.
 | 
			
		||||
 *
 | 
			
		||||
 * @internal
 | 
			
		||||
 */
 | 
			
		||||
final class DeprecationHandler {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Indicates if the extension is enabled.
 | 
			
		||||
   */
 | 
			
		||||
  private static bool $enabled = FALSE;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A list of deprecation messages that should be ignored if detected.
 | 
			
		||||
   *
 | 
			
		||||
   * @var list<string>
 | 
			
		||||
   */
 | 
			
		||||
  private static array $deprecationIgnorePatterns = [];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A list of expected deprecation messages.
 | 
			
		||||
   *
 | 
			
		||||
   * @var list<string>
 | 
			
		||||
   */
 | 
			
		||||
  private static array $expectedDeprecations = [];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A list of deprecation messages collected during test run.
 | 
			
		||||
   *
 | 
			
		||||
   * @var list<string>
 | 
			
		||||
   */
 | 
			
		||||
  private static array $collectedDeprecations = [];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * This class should not be instantiated.
 | 
			
		||||
   */
 | 
			
		||||
  private function __construct() {
 | 
			
		||||
    throw new \LogicException(__CLASS__ . ' should not be instantiated');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the extension configuration.
 | 
			
		||||
   *
 | 
			
		||||
   * For historical reasons, the configuration is stored in the
 | 
			
		||||
   * SYMFONY_DEPRECATIONS_HELPER environment variable.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array|false
 | 
			
		||||
   *   An array of configuration variables, of FALSE if the extension is
 | 
			
		||||
   *   disabled.
 | 
			
		||||
   */
 | 
			
		||||
  public static function getConfiguration(): array|FALSE {
 | 
			
		||||
    $environmentVariable = getenv('SYMFONY_DEPRECATIONS_HELPER');
 | 
			
		||||
    if ($environmentVariable === 'disabled') {
 | 
			
		||||
      return FALSE;
 | 
			
		||||
    }
 | 
			
		||||
    if ($environmentVariable === FALSE) {
 | 
			
		||||
      // Ensure ignored deprecation patterns listed in .deprecation-ignore.txt
 | 
			
		||||
      // are considered in testing.
 | 
			
		||||
      $relativeFilePath = __DIR__ . "/../../../../../.deprecation-ignore.txt";
 | 
			
		||||
      $deprecationIgnoreFilename = realpath($relativeFilePath);
 | 
			
		||||
      if (empty($deprecationIgnoreFilename)) {
 | 
			
		||||
        throw new \InvalidArgumentException(sprintf('The ignoreFile "%s" does not exist.', $relativeFilePath));
 | 
			
		||||
      }
 | 
			
		||||
      $environmentVariable = "ignoreFile=$deprecationIgnoreFilename";
 | 
			
		||||
    }
 | 
			
		||||
    parse_str($environmentVariable, $configuration);
 | 
			
		||||
 | 
			
		||||
    $environmentVariable = getenv('PHPUNIT_FAIL_ON_PHPUNIT_DEPRECATION');
 | 
			
		||||
    $phpUnitDeprecationVariable = $environmentVariable !== FALSE ? $environmentVariable : TRUE;
 | 
			
		||||
    $configuration['failOnPhpunitDeprecation'] = filter_var($phpUnitDeprecationVariable, \FILTER_VALIDATE_BOOLEAN);
 | 
			
		||||
 | 
			
		||||
    return $configuration;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Determines if the extension is enabled.
 | 
			
		||||
   *
 | 
			
		||||
   * @return bool
 | 
			
		||||
   *   TRUE if enabled, FALSE if disabled.
 | 
			
		||||
   */
 | 
			
		||||
  public static function isEnabled(): bool {
 | 
			
		||||
    return self::$enabled;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Initializes the extension.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string|null $ignoreFile
 | 
			
		||||
   *   The path to a file containing ignore patterns for deprecations.
 | 
			
		||||
   */
 | 
			
		||||
  public static function init(?string $ignoreFile = NULL): void {
 | 
			
		||||
    if (self::isEnabled()) {
 | 
			
		||||
      throw new \LogicException(__CLASS__ . ' is already initialized');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Load the deprecation ignore patterns from the specified file.
 | 
			
		||||
    if ($ignoreFile && !self::$deprecationIgnorePatterns) {
 | 
			
		||||
      if (!is_file($ignoreFile)) {
 | 
			
		||||
        throw new \InvalidArgumentException(sprintf('The ignoreFile "%s" does not exist.', $ignoreFile));
 | 
			
		||||
      }
 | 
			
		||||
      set_error_handler(static function ($t, $m) use ($ignoreFile, &$line) {
 | 
			
		||||
        throw new \RuntimeException(sprintf('Invalid pattern found in "%s" on line "%d"', $ignoreFile, 1 + $line) . substr($m, 12));
 | 
			
		||||
      });
 | 
			
		||||
      try {
 | 
			
		||||
        foreach (file($ignoreFile) as $line => $pattern) {
 | 
			
		||||
          if ((trim($pattern)[0] ?? '#') !== '#') {
 | 
			
		||||
            preg_match($pattern, '');
 | 
			
		||||
            self::$deprecationIgnorePatterns[] = $pattern;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      finally {
 | 
			
		||||
        restore_error_handler();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Mark the extension as enabled.
 | 
			
		||||
    self::$enabled = TRUE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Resets the extension.
 | 
			
		||||
   *
 | 
			
		||||
   * The extension should be reset at the beginning of each test run to ensure
 | 
			
		||||
   * matching of expected and actual deprecations.
 | 
			
		||||
   */
 | 
			
		||||
  public static function reset(): void {
 | 
			
		||||
    if (!self::isEnabled()) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    self::$expectedDeprecations = [];
 | 
			
		||||
    self::$collectedDeprecations = [];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Adds an expected deprecation.
 | 
			
		||||
   *
 | 
			
		||||
   * Tests will expect deprecations during the test execution; at the end of
 | 
			
		||||
   * each test run, collected deprecations are checked against the expected
 | 
			
		||||
   * ones.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $message
 | 
			
		||||
   *   The expected deprecation message.
 | 
			
		||||
   */
 | 
			
		||||
  public static function expectDeprecation(string $message): void {
 | 
			
		||||
    if (!self::isEnabled()) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    self::$expectedDeprecations[] = $message;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns all expected deprecations.
 | 
			
		||||
   *
 | 
			
		||||
   * @return list<string>
 | 
			
		||||
   *   The expected deprecation messages.
 | 
			
		||||
   */
 | 
			
		||||
  public static function getExpectedDeprecations(): array {
 | 
			
		||||
    if (!self::isEnabled()) {
 | 
			
		||||
      throw new \LogicException(__CLASS__ . ' is not initialized');
 | 
			
		||||
    }
 | 
			
		||||
    return self::$expectedDeprecations;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Collects an actual deprecation.
 | 
			
		||||
   *
 | 
			
		||||
   * Tests will expect deprecations during the test execution; at the end of
 | 
			
		||||
   * each test run, collected deprecations are checked against the expected
 | 
			
		||||
   * ones.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $message
 | 
			
		||||
   *   The actual deprecation message triggered via trigger_error().
 | 
			
		||||
   */
 | 
			
		||||
  public static function collectActualDeprecation(string $message): void {
 | 
			
		||||
    if (!self::isEnabled()) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    self::$collectedDeprecations[] = $message;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns all collected deprecations.
 | 
			
		||||
   *
 | 
			
		||||
   * @return list<string>
 | 
			
		||||
   *   The collected deprecation messages.
 | 
			
		||||
   */
 | 
			
		||||
  public static function getCollectedDeprecations(): array {
 | 
			
		||||
    if (!self::isEnabled()) {
 | 
			
		||||
      throw new \LogicException(__CLASS__ . ' is not initialized');
 | 
			
		||||
    }
 | 
			
		||||
    return self::$collectedDeprecations;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Determines if an actual deprecation should be ignored.
 | 
			
		||||
   *
 | 
			
		||||
   * Deprecations that match the patterns included in the ignore file should
 | 
			
		||||
   * be ignored.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $deprecationMessage
 | 
			
		||||
   *   The actual deprecation message triggered via trigger_error().
 | 
			
		||||
   */
 | 
			
		||||
  public static function isIgnoredDeprecation(string $deprecationMessage): bool {
 | 
			
		||||
    if (!self::$deprecationIgnorePatterns) {
 | 
			
		||||
      return FALSE;
 | 
			
		||||
    }
 | 
			
		||||
    $result = @preg_filter(self::$deprecationIgnorePatterns, '$0', $deprecationMessage);
 | 
			
		||||
    if (preg_last_error() !== \PREG_NO_ERROR) {
 | 
			
		||||
      throw new \RuntimeException(preg_last_error_msg());
 | 
			
		||||
    }
 | 
			
		||||
    return (bool) $result;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Determines if a test case is a deprecation test.
 | 
			
		||||
   *
 | 
			
		||||
   * Deprecation tests are those that are annotated with '@group legacy' or
 | 
			
		||||
   * that have a '#[IgnoreDeprecations]' attribute.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \PHPUnit\Framework\TestCase $testCase
 | 
			
		||||
   *   The test case being executed.
 | 
			
		||||
   */
 | 
			
		||||
  public static function isDeprecationTest(TestCase $testCase): bool {
 | 
			
		||||
    return $testCase->valueObjectForEvents()->metadata()->isIgnoreDeprecations()->isNotEmpty() || self::isTestInLegacyGroup($testCase);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Determines if a test case is part of the 'legacy' group.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \PHPUnit\Framework\TestCase $testCase
 | 
			
		||||
   *   The test case being executed.
 | 
			
		||||
   */
 | 
			
		||||
  private static function isTestInLegacyGroup(TestCase $testCase): bool {
 | 
			
		||||
    $groups = [];
 | 
			
		||||
    foreach ($testCase->valueObjectForEvents()->metadata()->isGroup() as $metadata) {
 | 
			
		||||
      $groups[] = $metadata->groupName();
 | 
			
		||||
    }
 | 
			
		||||
    return in_array('legacy', $groups, TRUE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,97 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\TestTools\Extension\DeprecationBridge;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Utility\Error;
 | 
			
		||||
use Drupal\TestTools\ErrorHandler\TestErrorHandler;
 | 
			
		||||
use PHPUnit\Framework\Attributes\After;
 | 
			
		||||
use PHPUnit\Framework\Attributes\Before;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A trait to include in Drupal tests to manage expected deprecations.
 | 
			
		||||
 *
 | 
			
		||||
 * This code works in coordination with DeprecationHandler.
 | 
			
		||||
 *
 | 
			
		||||
 * This trait is a replacement for symfony/phpunit-bridge that is not
 | 
			
		||||
 * supporting PHPUnit 10. In the future this extension might be dropped if
 | 
			
		||||
 * PHPUnit will support all deprecation management needs.
 | 
			
		||||
 *
 | 
			
		||||
 * @see \Drupal\TestTools\Extension\DeprecationBridge\DeprecationHandler
 | 
			
		||||
 *
 | 
			
		||||
 * @internal
 | 
			
		||||
 */
 | 
			
		||||
trait ExpectDeprecationTrait {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Sets up the test error handler.
 | 
			
		||||
   *
 | 
			
		||||
   * This method is run before each test's ::setUp() method, and when the
 | 
			
		||||
   * DeprecationHandler is active, resets the extension to be able to collect
 | 
			
		||||
   * the test's deprecations, and sets TestErrorHandler as the current error
 | 
			
		||||
   * handler.
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\TestTools\ErrorHandler\TestErrorHandler
 | 
			
		||||
   */
 | 
			
		||||
  #[Before]
 | 
			
		||||
  public function setUpErrorHandler(): void {
 | 
			
		||||
    if (!DeprecationHandler::isEnabled()) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    DeprecationHandler::reset();
 | 
			
		||||
    set_error_handler(new TestErrorHandler(Error::currentErrorHandler(), $this));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tears down the test error handler.
 | 
			
		||||
   *
 | 
			
		||||
   * This method is run after each test's ::tearDown() method, and checks if
 | 
			
		||||
   * collected deprecations match the expectations; it also resets the error
 | 
			
		||||
   * handler to the one set prior of the change made by ::setUpErrorHandler().
 | 
			
		||||
   */
 | 
			
		||||
  #[After]
 | 
			
		||||
  public function tearDownErrorHandler(): void {
 | 
			
		||||
    if (!DeprecationHandler::isEnabled()) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // We expect that the current error handler is the one set by
 | 
			
		||||
    // ::setUpErrorHandler() prior to the start of the test execution. If not,
 | 
			
		||||
    // the error handler was changed during the test execution but not properly
 | 
			
		||||
    // restored during ::tearDown().
 | 
			
		||||
    $handler = Error::currentErrorHandler();
 | 
			
		||||
    if (!$handler instanceof TestErrorHandler) {
 | 
			
		||||
      throw new \RuntimeException(sprintf('%s registered its own error handler without restoring the previous one before or during tear down. This can cause unpredictable test results. Ensure the test cleans up after itself.', $this->name()));
 | 
			
		||||
    }
 | 
			
		||||
    restore_error_handler();
 | 
			
		||||
 | 
			
		||||
    // Checks if collected deprecations match the expectations.
 | 
			
		||||
    if (DeprecationHandler::getExpectedDeprecations()) {
 | 
			
		||||
      $prefix = "@expectedDeprecation:\n";
 | 
			
		||||
      $expDep = $prefix . '%A  ' . implode("\n%A  ", DeprecationHandler::getExpectedDeprecations()) . "\n%A";
 | 
			
		||||
      $actDep = $prefix . '  ' . implode("\n  ", DeprecationHandler::getCollectedDeprecations()) . "\n";
 | 
			
		||||
      $this->assertStringMatchesFormat($expDep, $actDep);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Adds an expected deprecation.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $message
 | 
			
		||||
   *   The expected deprecation message.
 | 
			
		||||
   */
 | 
			
		||||
  public function expectDeprecation(string $message): void {
 | 
			
		||||
    if (!DeprecationHandler::isDeprecationTest($this)) {
 | 
			
		||||
      throw new \RuntimeException('expectDeprecation() can only be called from tests marked with #[IgnoreDeprecations] or \'@group legacy\'');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!DeprecationHandler::isEnabled()) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    DeprecationHandler::expectDeprecation($message);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										244
									
								
								web/core/tests/Drupal/TestTools/Extension/Dump/DebugDump.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										244
									
								
								web/core/tests/Drupal/TestTools/Extension/Dump/DebugDump.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,244 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\TestTools\Extension\Dump;
 | 
			
		||||
 | 
			
		||||
use PHPUnit\Event\TestRunner\Finished as TestRunnerFinished;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use PHPUnit\Runner\Extension\Extension;
 | 
			
		||||
use PHPUnit\Runner\Extension\Facade;
 | 
			
		||||
use PHPUnit\Runner\Extension\ParameterCollection;
 | 
			
		||||
use PHPUnit\TextUI\Configuration\Configuration;
 | 
			
		||||
use Symfony\Component\VarDumper\Cloner\VarCloner;
 | 
			
		||||
use Symfony\Component\VarDumper\Dumper\CliDumper;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Drupal's extension for printing dump() output results.
 | 
			
		||||
 *
 | 
			
		||||
 * @internal
 | 
			
		||||
 */
 | 
			
		||||
final class DebugDump implements Extension {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The path to the dump staging file.
 | 
			
		||||
   */
 | 
			
		||||
  private static string $stagingFilePath;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Whether colors should be used for printing.
 | 
			
		||||
   */
 | 
			
		||||
  private static bool $colors = FALSE;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Whether the caller of dump should be included in the report.
 | 
			
		||||
   */
 | 
			
		||||
  private static bool $printCaller = FALSE;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function bootstrap(
 | 
			
		||||
    Configuration $configuration,
 | 
			
		||||
    Facade $facade,
 | 
			
		||||
    ParameterCollection $parameters,
 | 
			
		||||
  ): void {
 | 
			
		||||
    // Determine staging file path.
 | 
			
		||||
    self::$stagingFilePath = tempnam(sys_get_temp_dir(), 'dpd');
 | 
			
		||||
 | 
			
		||||
    // Determine color output.
 | 
			
		||||
    $colors = $parameters->has('colors') ? $parameters->get('colors') : FALSE;
 | 
			
		||||
    self::$colors = filter_var($colors, \FILTER_VALIDATE_BOOLEAN);
 | 
			
		||||
 | 
			
		||||
    // Print caller.
 | 
			
		||||
    $printCaller = $parameters->has('printCaller') ? $parameters->get('printCaller') : FALSE;
 | 
			
		||||
    self::$printCaller = filter_var($printCaller, \FILTER_VALIDATE_BOOLEAN);
 | 
			
		||||
 | 
			
		||||
    // Set the environment variable with the configuration.
 | 
			
		||||
    $config = json_encode([
 | 
			
		||||
      'stagingFilePath' => self::$stagingFilePath,
 | 
			
		||||
      'colors' => self::$colors,
 | 
			
		||||
      'printCaller' => self::$printCaller,
 | 
			
		||||
    ]);
 | 
			
		||||
    putenv('DRUPAL_PHPUNIT_DUMPER_CONFIG=' . $config);
 | 
			
		||||
 | 
			
		||||
    $facade->registerSubscriber(new TestRunnerFinishedSubscriber($this));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Determines if the extension is enabled.
 | 
			
		||||
   *
 | 
			
		||||
   * @return bool
 | 
			
		||||
   *   TRUE if enabled, FALSE if disabled.
 | 
			
		||||
   */
 | 
			
		||||
  public static function isEnabled(): bool {
 | 
			
		||||
    return getenv('DRUPAL_PHPUNIT_DUMPER_CONFIG') !== FALSE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A CLI handler for \Symfony\Component\VarDumper\VarDumper.
 | 
			
		||||
   *
 | 
			
		||||
   * @param mixed $var
 | 
			
		||||
   *   The variable to be dumped.
 | 
			
		||||
   */
 | 
			
		||||
  public static function cliHandler(mixed $var): void {
 | 
			
		||||
    if (!self::isEnabled()) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    $config = (array) json_decode(getenv('DRUPAL_PHPUNIT_DUMPER_CONFIG'));
 | 
			
		||||
 | 
			
		||||
    $caller = self::getCaller();
 | 
			
		||||
 | 
			
		||||
    $cloner = new VarCloner();
 | 
			
		||||
    $dumper = new CliDumper();
 | 
			
		||||
    $dumper->setColors($config['colors']);
 | 
			
		||||
    $dump = [];
 | 
			
		||||
    $dumper->dump(
 | 
			
		||||
      $cloner->cloneVar($var),
 | 
			
		||||
      function ($line, $depth, $indent_pad) use (&$dump) {
 | 
			
		||||
        // A negative depth means "end of dump".
 | 
			
		||||
        if ($depth >= 0) {
 | 
			
		||||
          // Adds a two spaces indentation to the line.
 | 
			
		||||
          $dump[] = str_repeat($indent_pad, $depth) . $line;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    file_put_contents(
 | 
			
		||||
      $config['stagingFilePath'],
 | 
			
		||||
      self::encodeDump($caller['test']->id(), $caller['file'], $caller['line'], $dump) . "\n",
 | 
			
		||||
      FILE_APPEND,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Encodes the dump for storing.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $testId
 | 
			
		||||
   *   The id of the test from where the dump was called.
 | 
			
		||||
   * @param string|null $file
 | 
			
		||||
   *   The path of the file from where the dump was called.
 | 
			
		||||
   * @param int|null $line
 | 
			
		||||
   *   The line number from where the dump was called.
 | 
			
		||||
   * @param array $dump
 | 
			
		||||
   *   The dump as an array of lines.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   An encoded string.
 | 
			
		||||
   */
 | 
			
		||||
  private static function encodeDump(string $testId, ?string $file, ?int $line, array $dump): string {
 | 
			
		||||
    $data = [
 | 
			
		||||
      'test' => $testId,
 | 
			
		||||
      'file' => $file,
 | 
			
		||||
      'line' => $line,
 | 
			
		||||
      'dump' => $dump,
 | 
			
		||||
    ];
 | 
			
		||||
    $jsonData = json_encode($data);
 | 
			
		||||
    return base64_encode($jsonData);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Decodes a dump retrieved from storage.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $encodedData
 | 
			
		||||
   *   An encoded string.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array{test: string, file: string|null, line: int|null, dump: string[]}
 | 
			
		||||
   *   An encoded string.
 | 
			
		||||
   */
 | 
			
		||||
  private static function decodeDump(string $encodedData): array {
 | 
			
		||||
    $jsonData = base64_decode($encodedData);
 | 
			
		||||
    return (array) json_decode($jsonData);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns information about the caller of dump().
 | 
			
		||||
   *
 | 
			
		||||
   * @return array{test: \PHPUnit\Framework\Event\Code\TestMethod, file: string|null, line: int|null}
 | 
			
		||||
   *   Caller information.
 | 
			
		||||
   */
 | 
			
		||||
  private static function getCaller(): array {
 | 
			
		||||
    $backtrace = debug_backtrace();
 | 
			
		||||
 | 
			
		||||
    while (!isset($backtrace[0]['function']) || $backtrace[0]['function'] !== 'dump') {
 | 
			
		||||
      array_shift($backtrace);
 | 
			
		||||
    }
 | 
			
		||||
    $call['file'] = $backtrace[1]['file'] ?? NULL;
 | 
			
		||||
    $call['line'] = $backtrace[1]['line'] ?? NULL;
 | 
			
		||||
 | 
			
		||||
    while (!isset($backtrace[0]['object']) || !($backtrace[0]['object'] instanceof TestCase)) {
 | 
			
		||||
      array_shift($backtrace);
 | 
			
		||||
    }
 | 
			
		||||
    $call['test'] = $backtrace[0]['object']->valueObjectForEvents();
 | 
			
		||||
 | 
			
		||||
    return $call;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Retrieves dumps from storage.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array{string, array{file: string|null, line: int|null, dump: string[]}}
 | 
			
		||||
   *   Caller information.
 | 
			
		||||
   */
 | 
			
		||||
  public static function getDumps(): array {
 | 
			
		||||
    if (!self::isEnabled()) {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
    $config = (array) json_decode(getenv('DRUPAL_PHPUNIT_DUMPER_CONFIG'));
 | 
			
		||||
    $contents = rtrim(file_get_contents($config['stagingFilePath']));
 | 
			
		||||
    if (empty($contents)) {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
    $encodedDumps = explode("\n", $contents);
 | 
			
		||||
    $dumps = [];
 | 
			
		||||
    foreach ($encodedDumps as $encodedDump) {
 | 
			
		||||
      $dump = self::decodeDump($encodedDump);
 | 
			
		||||
      $test = $dump['test'];
 | 
			
		||||
      unset($dump['test']);
 | 
			
		||||
      $dumps[$test][] = $dump;
 | 
			
		||||
    }
 | 
			
		||||
    return $dumps;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Prints the dumps generated during the test.
 | 
			
		||||
   */
 | 
			
		||||
  public function testRunnerFinished(TestRunnerFinished $event): void {
 | 
			
		||||
    $dumps = self::getDumps();
 | 
			
		||||
 | 
			
		||||
    // Cleanup.
 | 
			
		||||
    unlink(self::$stagingFilePath);
 | 
			
		||||
    putenv('DRUPAL_PHPUNIT_DUMPER_CONFIG');
 | 
			
		||||
 | 
			
		||||
    if ($dumps === []) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    print "\n\n";
 | 
			
		||||
 | 
			
		||||
    print "dump() output\n";
 | 
			
		||||
    print "-------------\n\n";
 | 
			
		||||
 | 
			
		||||
    foreach ($dumps as $testId => $testDumps) {
 | 
			
		||||
      if (self::$printCaller) {
 | 
			
		||||
        print $testId . "\n";
 | 
			
		||||
      }
 | 
			
		||||
      foreach ($testDumps as $dump) {
 | 
			
		||||
        if (self::$printCaller) {
 | 
			
		||||
          print "in " . $dump['file'] . ", line " . $dump['line'] . ":\n";
 | 
			
		||||
        }
 | 
			
		||||
        foreach ($dump['dump'] as $line) {
 | 
			
		||||
          print $line . "\n";
 | 
			
		||||
        }
 | 
			
		||||
        if (self::$printCaller) {
 | 
			
		||||
          print "\n";
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (self::$printCaller) {
 | 
			
		||||
        print "\n";
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,26 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\TestTools\Extension\Dump;
 | 
			
		||||
 | 
			
		||||
use PHPUnit\Event\TestRunner\Finished;
 | 
			
		||||
use PHPUnit\Event\TestRunner\FinishedSubscriber;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Event subscriber notifying end of test runner execution to HTML logging.
 | 
			
		||||
 *
 | 
			
		||||
 * @internal
 | 
			
		||||
 */
 | 
			
		||||
final class TestRunnerFinishedSubscriber implements FinishedSubscriber {
 | 
			
		||||
 | 
			
		||||
  public function __construct(
 | 
			
		||||
    private readonly DebugDump $dump,
 | 
			
		||||
  ) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public function notify(Finished $event): void {
 | 
			
		||||
    $this->dump->testRunnerFinished($event);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,158 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\TestTools\Extension\HtmlLogging;
 | 
			
		||||
 | 
			
		||||
use PHPUnit\Event\TestRunner\Finished as TestRunnerFinished;
 | 
			
		||||
use PHPUnit\Event\TestRunner\Started as TestRunnerStarted;
 | 
			
		||||
use PHPUnit\Runner\Extension\Extension;
 | 
			
		||||
use PHPUnit\Runner\Extension\Facade;
 | 
			
		||||
use PHPUnit\Runner\Extension\ParameterCollection;
 | 
			
		||||
use PHPUnit\TextUI\Configuration\Configuration;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Drupal's extension for providing HTML output results for functional tests.
 | 
			
		||||
 *
 | 
			
		||||
 * @internal
 | 
			
		||||
 */
 | 
			
		||||
final class HtmlOutputLogger implements Extension {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The status of the extension.
 | 
			
		||||
   */
 | 
			
		||||
  private bool $enabled = FALSE;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A file with list of links to HTML pages generated.
 | 
			
		||||
   */
 | 
			
		||||
  private ?string $browserOutputFile = NULL;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A file with list of links to HTML pages generated.
 | 
			
		||||
   */
 | 
			
		||||
  private string $outputDirectory;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verbosity of the final report.
 | 
			
		||||
   *
 | 
			
		||||
   * If TRUE, a list of links generated will be output at the end of the test
 | 
			
		||||
   * run; if FALSE, only a summary with the count of pages generated.
 | 
			
		||||
   */
 | 
			
		||||
  private bool $outputVerbose;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function bootstrap(
 | 
			
		||||
    Configuration $configuration,
 | 
			
		||||
    Facade $facade,
 | 
			
		||||
    ParameterCollection $parameters,
 | 
			
		||||
  ): void {
 | 
			
		||||
    // Determine output directory.
 | 
			
		||||
    $envDirectory = getenv('BROWSERTEST_OUTPUT_DIRECTORY');
 | 
			
		||||
    if ($envDirectory === "") {
 | 
			
		||||
      print "HTML output disabled by BROWSERTEST_OUTPUT_DIRECTORY = ''.\n\n";
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    elseif ($envDirectory !== FALSE) {
 | 
			
		||||
      $directory = $envDirectory;
 | 
			
		||||
    }
 | 
			
		||||
    elseif ($parameters->has('outputDirectory')) {
 | 
			
		||||
      $directory = $parameters->get('outputDirectory');
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      print "HTML output directory not specified.\n\n";
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    $realDirectory = realpath($directory);
 | 
			
		||||
    if ($realDirectory === FALSE || !is_dir($realDirectory) || !is_writable($realDirectory)) {
 | 
			
		||||
      print "HTML output directory {$directory} is not a writable directory.\n\n";
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    $this->outputDirectory = $realDirectory;
 | 
			
		||||
 | 
			
		||||
    // Determine output verbosity.
 | 
			
		||||
    $envVerbose = getenv('BROWSERTEST_OUTPUT_VERBOSE');
 | 
			
		||||
    if ($envVerbose !== FALSE) {
 | 
			
		||||
      $verbose = $envVerbose;
 | 
			
		||||
    }
 | 
			
		||||
    elseif ($parameters->has('verbose')) {
 | 
			
		||||
      $verbose = $parameters->get('verbose');
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $verbose = FALSE;
 | 
			
		||||
    }
 | 
			
		||||
    $this->outputVerbose = filter_var($verbose, \FILTER_VALIDATE_BOOLEAN);
 | 
			
		||||
 | 
			
		||||
    $facade->registerSubscriber(new TestRunnerStartedSubscriber($this));
 | 
			
		||||
    $facade->registerSubscriber(new TestRunnerFinishedSubscriber($this));
 | 
			
		||||
 | 
			
		||||
    $this->enabled = TRUE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Logs a link to a generated HTML page.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $logEntry
 | 
			
		||||
   *   A link to a generated HTML page, should not contain a trailing newline.
 | 
			
		||||
   *
 | 
			
		||||
   * @throws \RuntimeException
 | 
			
		||||
   */
 | 
			
		||||
  public static function log(string $logEntry): void {
 | 
			
		||||
    $browserOutputFile = getenv('BROWSERTEST_OUTPUT_FILE');
 | 
			
		||||
    if ($browserOutputFile === FALSE) {
 | 
			
		||||
      throw new \RuntimeException("HTML output is not enabled");
 | 
			
		||||
    }
 | 
			
		||||
    file_put_contents($browserOutputFile, $logEntry . "\n", FILE_APPEND);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Empties the list of the HTML output created during the test run.
 | 
			
		||||
   */
 | 
			
		||||
  public function testRunnerStarted(TestRunnerStarted $event): void {
 | 
			
		||||
    if (!$this->enabled) {
 | 
			
		||||
      throw new \RuntimeException("HTML output is not enabled");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Convert to a canonicalized absolute pathname just in case the current
 | 
			
		||||
    // working directory is changed.
 | 
			
		||||
    $this->browserOutputFile = tempnam($this->outputDirectory, 'browser_output_');
 | 
			
		||||
    if ($this->browserOutputFile) {
 | 
			
		||||
      touch($this->browserOutputFile);
 | 
			
		||||
      putenv('BROWSERTEST_OUTPUT_FILE=' . $this->browserOutputFile);
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      // Remove any environment variable.
 | 
			
		||||
      putenv('BROWSERTEST_OUTPUT_FILE');
 | 
			
		||||
      throw new \RuntimeException("Unable to create a temporary file in {$this->outputDirectory}.");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Prints the list of HTML output generated during the test.
 | 
			
		||||
   */
 | 
			
		||||
  public function testRunnerFinished(TestRunnerFinished $event): void {
 | 
			
		||||
    if (!$this->enabled) {
 | 
			
		||||
      throw new \RuntimeException("HTML output is not enabled");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $contents = file_get_contents($this->browserOutputFile);
 | 
			
		||||
    if ($contents) {
 | 
			
		||||
      print "\n\n";
 | 
			
		||||
      if ($this->outputVerbose) {
 | 
			
		||||
        print "HTML output was generated.\n";
 | 
			
		||||
        print $contents;
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        print "HTML output was generated, " . count(explode("\n", $contents)) . " page(s).\n";
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // No need to keep the file around any more.
 | 
			
		||||
    unlink($this->browserOutputFile);
 | 
			
		||||
    putenv('BROWSERTEST_OUTPUT_FILE');
 | 
			
		||||
    $this->browserOutputFile = NULL;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,23 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\TestTools\Extension\HtmlLogging;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Base class for PHPUnit event subscribers related to HTML logging.
 | 
			
		||||
 *
 | 
			
		||||
 * @internal
 | 
			
		||||
 */
 | 
			
		||||
abstract class SubscriberBase {
 | 
			
		||||
 | 
			
		||||
  public function __construct(
 | 
			
		||||
    private readonly HtmlOutputLogger $logger,
 | 
			
		||||
  ) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected function logger(): HtmlOutputLogger {
 | 
			
		||||
    return $this->logger;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,21 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\TestTools\Extension\HtmlLogging;
 | 
			
		||||
 | 
			
		||||
use PHPUnit\Event\TestRunner\Finished;
 | 
			
		||||
use PHPUnit\Event\TestRunner\FinishedSubscriber;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Event subscriber notifying end of test runner execution to HTML logging.
 | 
			
		||||
 *
 | 
			
		||||
 * @internal
 | 
			
		||||
 */
 | 
			
		||||
final class TestRunnerFinishedSubscriber extends SubscriberBase implements FinishedSubscriber {
 | 
			
		||||
 | 
			
		||||
  public function notify(Finished $event): void {
 | 
			
		||||
    $this->logger()->testRunnerFinished($event);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,21 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\TestTools\Extension\HtmlLogging;
 | 
			
		||||
 | 
			
		||||
use PHPUnit\Event\TestRunner\Started;
 | 
			
		||||
use PHPUnit\Event\TestRunner\StartedSubscriber;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Event subscriber notifying beginning of test runner to HTML logging.
 | 
			
		||||
 *
 | 
			
		||||
 * @internal
 | 
			
		||||
 */
 | 
			
		||||
final class TestRunnerStartedSubscriber extends SubscriberBase implements StartedSubscriber {
 | 
			
		||||
 | 
			
		||||
  public function notify(Started $event): void {
 | 
			
		||||
    $this->logger()->testRunnerStarted($event);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,39 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\TestTools\Extension;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Serialization\Yaml;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Writes the info file and ensures the mtime changes.
 | 
			
		||||
 *
 | 
			
		||||
 * @see \Drupal\Component\FileCache\FileCache
 | 
			
		||||
 * @see \Drupal\Core\Extension\InfoParser
 | 
			
		||||
 */
 | 
			
		||||
trait InfoWriterTrait {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Writes the info file and ensures the mtime changes.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $file_path
 | 
			
		||||
   *   The info file path.
 | 
			
		||||
   * @param array $info
 | 
			
		||||
   *   The info array.
 | 
			
		||||
   *
 | 
			
		||||
   * @return void
 | 
			
		||||
   *   No return value.
 | 
			
		||||
   */
 | 
			
		||||
  private function writeInfoFile(string $file_path, array $info): void {
 | 
			
		||||
    $mtime = file_exists($file_path) ? filemtime($file_path) : FALSE;
 | 
			
		||||
 | 
			
		||||
    file_put_contents($file_path, Yaml::encode($info));
 | 
			
		||||
 | 
			
		||||
    // Ensure mtime changes.
 | 
			
		||||
    if ($mtime === filemtime($file_path)) {
 | 
			
		||||
      touch($file_path, max($mtime + 1, time()));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,22 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\TestTools\Extension;
 | 
			
		||||
 | 
			
		||||
use PHPUnit\Framework\Attributes\BeforeClass;
 | 
			
		||||
use Symfony\Component\Process\ExecutableFinder;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Ensures Composer executable is available, skips test otherwise.
 | 
			
		||||
 */
 | 
			
		||||
trait RequiresComposerTrait {
 | 
			
		||||
 | 
			
		||||
  #[BeforeClass]
 | 
			
		||||
  public static function requiresComposer(): void {
 | 
			
		||||
    if (!((new ExecutableFinder())->find('composer'))) {
 | 
			
		||||
      static::markTestSkipped('This test requires the Composer executable to be accessible.');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,38 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\TestTools\Extension;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Extension\ModuleHandlerInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides methods to access modules' schema.
 | 
			
		||||
 */
 | 
			
		||||
class SchemaInspector {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the module's schema specification.
 | 
			
		||||
   *
 | 
			
		||||
   * This function can be used to retrieve a schema specification provided by
 | 
			
		||||
   * hook_schema(), so it allows you to derive your tables from existing
 | 
			
		||||
   * specifications.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $handler
 | 
			
		||||
   *   The module handler to use for calling schema hook.
 | 
			
		||||
   * @param string $module
 | 
			
		||||
   *   The module to which the table belongs.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array
 | 
			
		||||
   *   An array of schema definition provided by hook_schema().
 | 
			
		||||
   *
 | 
			
		||||
   * @see \hook_schema()
 | 
			
		||||
   */
 | 
			
		||||
  public static function getTablesSpecification(ModuleHandlerInterface $handler, string $module): array {
 | 
			
		||||
    if ($handler->loadInclude($module, 'install')) {
 | 
			
		||||
      return $handler->invoke($module, 'schema') ?? [];
 | 
			
		||||
    }
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,28 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\TestTools\PhpUnitCompatibility\PhpUnit10;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Drupal's forward compatibility layer with multiple versions of PHPUnit.
 | 
			
		||||
 *
 | 
			
		||||
 * @internal
 | 
			
		||||
 */
 | 
			
		||||
trait TestCompatibilityTrait {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets @covers defined on the test class.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string[]
 | 
			
		||||
   *   An array of classes listed with the @covers annotation.
 | 
			
		||||
   */
 | 
			
		||||
  public function getTestClassCovers(): array {
 | 
			
		||||
    $ret = [];
 | 
			
		||||
    foreach ($this->valueObjectForEvents()->metadata()->isCovers()->isClassLevel() as $metadata) {
 | 
			
		||||
      $ret[] = $metadata->target();
 | 
			
		||||
    }
 | 
			
		||||
    return $ret;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,13 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\TestTools\PhpUnitCompatibility\PhpUnit11;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Drupal's forward compatibility layer with multiple versions of PHPUnit.
 | 
			
		||||
 *
 | 
			
		||||
 * @internal
 | 
			
		||||
 */
 | 
			
		||||
trait TestCompatibilityTrait {
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,32 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\TestTools\PhpUnitCompatibility;
 | 
			
		||||
 | 
			
		||||
use PHPUnit\Runner\Version;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Helper class to determine information about running PHPUnit version.
 | 
			
		||||
 *
 | 
			
		||||
 * This class contains static methods only and is not meant to be instantiated.
 | 
			
		||||
 */
 | 
			
		||||
final class RunnerVersion {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * This class should not be instantiated.
 | 
			
		||||
   */
 | 
			
		||||
  private function __construct() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the major version of the PHPUnit runner being used.
 | 
			
		||||
   *
 | 
			
		||||
   * @return int
 | 
			
		||||
   *   The major version of the PHPUnit runner being used.
 | 
			
		||||
   */
 | 
			
		||||
  public static function getMajor() {
 | 
			
		||||
    return (int) explode('.', Version::id())[0];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,17 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\TestTools;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Enumeration of JUnit test result statuses.
 | 
			
		||||
 */
 | 
			
		||||
enum PhpUnitTestCaseJUnitResult: string {
 | 
			
		||||
 | 
			
		||||
  case Pass = 'pass';
 | 
			
		||||
  case Fail = 'fail';
 | 
			
		||||
  case Error = 'error';
 | 
			
		||||
  case Skip = 'skipped';
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										125
									
								
								web/core/tests/Drupal/TestTools/Random.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								web/core/tests/Drupal/TestTools/Random.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,125 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\TestTools;
 | 
			
		||||
 | 
			
		||||
use Drupal\Component\Utility\Random as RandomUtility;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides random generator utility static methods.
 | 
			
		||||
 */
 | 
			
		||||
abstract class Random {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The random generator.
 | 
			
		||||
   */
 | 
			
		||||
  protected static RandomUtility $randomGenerator;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets the random generator for the utility methods.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Component\Utility\Random
 | 
			
		||||
   *   The random generator.
 | 
			
		||||
   */
 | 
			
		||||
  public static function getGenerator(): RandomUtility {
 | 
			
		||||
    if (!isset(static::$randomGenerator)) {
 | 
			
		||||
      static::$randomGenerator = new RandomUtility();
 | 
			
		||||
    }
 | 
			
		||||
    return static::$randomGenerator;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Generates a pseudo-random string of ASCII characters of codes 32 to 126.
 | 
			
		||||
   *
 | 
			
		||||
   * Do not use this method when special characters are not possible (e.g., in
 | 
			
		||||
   * machine or file names that have already been validated); instead, use
 | 
			
		||||
   * \Drupal\Tests\RandomGeneratorTrait::randomMachineName(). If $length is
 | 
			
		||||
   * greater than 3 the random string will include at least one ampersand ('&')
 | 
			
		||||
   * and at least one greater than ('>') character to ensure coverage for
 | 
			
		||||
   * special characters and avoid the introduction of random test failures.
 | 
			
		||||
   *
 | 
			
		||||
   * @param int $length
 | 
			
		||||
   *   Length of random string to generate.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   Pseudo-randomly generated unique string including special characters.
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\Component\Utility\Random::string()
 | 
			
		||||
   */
 | 
			
		||||
  public static function string(int $length = 8): string {
 | 
			
		||||
    if ($length < 4) {
 | 
			
		||||
      return static::getGenerator()->string($length, TRUE, [static::class, 'stringValidate']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // To prevent the introduction of random test failures, ensure that the
 | 
			
		||||
    // returned string contains a character that needs to be escaped in HTML by
 | 
			
		||||
    // injecting an ampersand into it.
 | 
			
		||||
    $replacement_pos = intval($length / 2);
 | 
			
		||||
    // Remove 2 from the length to account for the ampersand and greater than
 | 
			
		||||
    // characters.
 | 
			
		||||
    $string = static::getGenerator()->string($length - 2, TRUE, [static::class, 'stringValidate']);
 | 
			
		||||
    return substr_replace($string, '>&', $replacement_pos, 0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Callback for random string validation.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $string
 | 
			
		||||
   *   The random string to validate.
 | 
			
		||||
   *
 | 
			
		||||
   * @return bool
 | 
			
		||||
   *   TRUE if the random string is valid, FALSE if not.
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\Component\Utility\Random::string()
 | 
			
		||||
   */
 | 
			
		||||
  public static function stringValidate(string $string): bool {
 | 
			
		||||
    // Consecutive spaces causes issues for link validation.
 | 
			
		||||
    if (preg_match('/\s{2,}/', $string)) {
 | 
			
		||||
      return FALSE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Starting or ending with a space means that length might not be what is
 | 
			
		||||
    // expected.
 | 
			
		||||
    if (preg_match('/^\s|\s$/', $string)) {
 | 
			
		||||
      return FALSE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return TRUE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Generates a unique random string containing letters and numbers.
 | 
			
		||||
   *
 | 
			
		||||
   * Do not use this method when testing non validated user input. Instead, use
 | 
			
		||||
   * \Drupal\Tests\RandomGeneratorTrait::randomString().
 | 
			
		||||
   *
 | 
			
		||||
   * @param int $length
 | 
			
		||||
   *   Length of random string to generate.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   Randomly generated unique string.
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\Component\Utility\Random::name()
 | 
			
		||||
   */
 | 
			
		||||
  public static function machineName(int $length = 8): string {
 | 
			
		||||
    return static::getGenerator()->machineName($length, TRUE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Generates a random PHP object.
 | 
			
		||||
   *
 | 
			
		||||
   * @param int $size
 | 
			
		||||
   *   The number of random keys to add to the object.
 | 
			
		||||
   *
 | 
			
		||||
   * @return object
 | 
			
		||||
   *   The generated object, with the specified number of random keys. Each key
 | 
			
		||||
   *   has a random string value.
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\Component\Utility\Random::object()
 | 
			
		||||
   */
 | 
			
		||||
  public static function object(int $size = 4): \stdClass {
 | 
			
		||||
    return static::getGenerator()->object($size);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user