Initial Drupal 11 with DDEV setup
This commit is contained in:
@ -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 [];
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user