first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -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) {
@trigger_error("Using assert[Not]Equals() to compare markup between MarkupInterface objects and plain strings is deprecated in drupal:10.1.0 and will throw an error from drupal:11.0.0. Expected: '{$expected_safe}' - Actual '{$actual_safe}'. Use assert[Not]Same() and cast objects to string instead. See https://www.drupal.org/node/3334057", E_USER_DEPRECATED);
}
}
$comparator = $this->factory->getComparatorFor($expected_safe_stripped, $actual_safe_stripped);
$comparator->assertEquals($expected_safe_stripped, $actual_safe_stripped, $delta, $canonicalize, $ignoreCase);
}
}

View File

@@ -0,0 +1,38 @@
<?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
*/
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()));
}
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Drupal\TestTools\Extension;
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.');
}
}
}

View File

@@ -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 [];
}
}

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Drupal\TestTools\PhpUnitCompatibility;
use Composer\Autoload\ClassLoader;
/**
* Helper class to rewrite PHPUnit's TestCase class.
*
* This class contains static methods only and is not meant to be instantiated.
*
* @internal
* This should only be called by test running code. Drupal 9 will provide best
* effort to maintain this class for the Drupal 9 cycle. However if changes to
* PHP or PHPUnit make this impossible then support will be removed.
*/
final class ClassWriter {
/**
* This class should not be instantiated.
*/
private function __construct() {
}
/**
* Mutates PHPUnit classes to make it compatible with Drupal.
*
* @param \Composer\Autoload\ClassLoader $autoloader
* The autoloader.
*
* @throws \ReflectionException
*/
public static function mutateTestBase($autoloader) {
static::alterAssert($autoloader);
static::alterTestCase($autoloader);
}
/**
* Alters the Assert class.
*
* @param \Composer\Autoload\ClassLoader $autoloader
* The autoloader.
*
* @throws \ReflectionException
*/
private static function alterAssert(ClassLoader $autoloader): void {
// If the class exists already there is nothing we can do. Hopefully this
// is happening because this has been called already. The call from
// \Drupal\Core\Test\TestDiscovery::registerTestNamespaces() necessitates
// this protection.
if (class_exists('PHPUnit\Framework\Assert', FALSE)) {
return;
}
// Mutate Assert code to make it forward compatible with different PhpUnit
// versions, by adding Symfony's PHPUnit-bridge PolyfillAssertTrait.
$alteredFile = $autoloader->findFile('PHPUnit\Framework\Assert');
$phpunit_dir = dirname($alteredFile, 3);
$alteredCode = file_get_contents($alteredFile);
$alteredCode = preg_replace('/abstract class Assert[^\{]+\{/', '$0 ' . \PHP_EOL . " use \Symfony\Bridge\PhpUnit\Legacy\PolyfillAssertTrait;" . \PHP_EOL, $alteredCode, 1);
include static::flushAlteredCodeToFile('Assert.php', $alteredCode);
}
/**
* Alters the TestCase class.
*
* @param \Composer\Autoload\ClassLoader $autoloader
* The autoloader.
*
* @throws \ReflectionException
*/
private static function alterTestCase(ClassLoader $autoloader): void {
// If the class exists already there is nothing we can do. Hopefully this
// is happening because this has been called already. The call from
// \Drupal\Core\Test\TestDiscovery::registerTestNamespaces() necessitates
// this protection.
if (class_exists('PHPUnit\Framework\TestCase', FALSE)) {
return;
}
// Mutate TestCase code to make it forward compatible with different PhpUnit
// versions, by adding Symfony's PHPUnit-bridge PolyfillTestCaseTrait.
$alteredFile = $autoloader->findFile('PHPUnit\Framework\TestCase');
$phpunit_dir = dirname($alteredFile, 3);
$alteredCode = file_get_contents($alteredFile);
$alteredCode = preg_replace('/abstract class TestCase[^\{]+\{/', '$0 ' . \PHP_EOL . " use \Symfony\Bridge\PhpUnit\Legacy\PolyfillTestCaseTrait;" . \PHP_EOL, $alteredCode, 1);
$alteredCode = str_replace("__DIR__ . '/../Util/", "'$phpunit_dir/src/Util/", $alteredCode);
include static::flushAlteredCodeToFile('TestCase.php', $alteredCode);
}
/**
* Flushes altered class code to file when necessary.
*
* @param string $file_name
* The file name.
* @param string $altered_code
* The altered code.
*
* @return string
* The full path of the file to be included.
*/
private static function flushAlteredCodeToFile(string $file_name, string $altered_code): string {
$directory = __DIR__ . '/../../../../../sites/simpletest';
$full_path = $directory . '/' . $file_name;
// Only write when necessary.
if (!file_exists($full_path) || md5_file($full_path) !== md5($altered_code)) {
// Create directory when necessary.
if (!is_dir($directory) && !@mkdir($directory, 0777, TRUE) && !is_dir($directory)) {
throw new \RuntimeException('Unable to create directory: ' . $directory);
}
file_put_contents($full_path, $altered_code);
}
return $full_path;
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Drupal\TestTools\PhpUnitCompatibility\PhpUnit9;
use PHPUnit\Util\Test;
/**
* Drupal's forward compatibility layer with multiple versions of PHPUnit.
*/
trait TestCompatibilityTrait {
/**
* Get test name.
*/
public function name(): string {
return $this->getName();
}
/**
* Gets @covers defined on the test class.
*
* @return string[]
* An array of classes listed with the @covers annotation.
*/
public function getTestClassCovers(): array {
$annotations = Test::parseTestMethodAnnotations(static::class, $this->name());
return $annotations['class']['covers'] ?? [];
}
}

View File

@@ -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];
}
}

View 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.
*
* @see \Drupal\Component\Utility\Random::string()
*
* @param string $string
* The random string to validate.
*
* @return bool
* TRUE if the random string is valid, FALSE if not.
*/
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);
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Drupal\TestTools;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
/**
* Provides handlers for the Symfony VarDumper to work within tests.
*
* This allows the dump() function to produce output on the terminal without
* causing PHPUnit to complain.
*/
class TestVarDumper {
/**
* A CLI handler for \Symfony\Component\VarDumper\VarDumper.
*/
public static function cliHandler($var) {
$cloner = new VarCloner();
$dumper = new CliDumper();
fwrite(STDERR, "\n");
$dumper->setColors(TRUE);
$dumper->dump(
$cloner->cloneVar($var),
function ($line, $depth, $indent_pad) {
// A negative depth means "end of dump".
if ($depth >= 0) {
// Adds a two spaces indentation to the line.
fwrite(STDERR, str_repeat($indent_pad, $depth) . $line . "\n");
}
}
);
}
/**
* A HTML handler for \Symfony\Component\VarDumper\VarDumper.
*/
public static function htmlHandler($var) {
$cloner = new VarCloner();
$dumper = new HtmlDumper();
$dumper->dump($cloner->cloneVar($var));
}
}