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