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,82 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests;
use Behat\Mink\Driver\BrowserKitDriver;
use Drupal\Core\Url;
use GuzzleHttp\RequestOptions;
/**
* Boilerplate for API Functional tests' HTTP requests.
*
* @internal
*/
trait ApiRequestTrait {
/**
* Performs an HTTP request. Wraps the Guzzle HTTP client.
*
* Why wrap the Guzzle HTTP client? Because we want to keep the actual test
* code as simple as possible, and hence not require them to specify the
* 'http_errors = FALSE' request option, nor do we want them to have to
* convert Drupal Url objects to strings.
*
* We also don't want to follow redirects automatically, to ensure these tests
* are able to detect when redirects are added or removed.
*
* @param string $method
* HTTP method.
* @param \Drupal\Core\Url $url
* URL to request.
* @param array $request_options
* Request options to apply.
*
* @return \Psr\Http\Message\ResponseInterface
* The response.
*
* @see \GuzzleHttp\ClientInterface::request()
*/
protected function makeApiRequest($method, Url $url, array $request_options) {
// HEAD requests do not have bodies. If one is specified, Guzzle will not
// ignore it and the request will be treated as GET with an overridden
// method string, and libcurl will expect to read a response body.
if ($method === 'HEAD' && array_key_exists('body', $request_options)) {
unset($request_options['body']);
}
$this->refreshVariables();
$request_options[RequestOptions::HTTP_ERRORS] = FALSE;
$request_options[RequestOptions::ALLOW_REDIRECTS] = FALSE;
$request_options = $this->decorateWithXdebugCookie($request_options);
$client = $this->getSession()->getDriver()->getClient()->getClient();
return $client->request($method, $url->setAbsolute(TRUE)->toString(), $request_options);
}
/**
* Adds the Xdebug cookie to the request options.
*
* @param array $request_options
* The request options.
*
* @return array
* Request options updated with the Xdebug cookie if present.
*/
protected function decorateWithXdebugCookie(array $request_options) {
$session = $this->getSession();
$driver = $session->getDriver();
if ($driver instanceof BrowserKitDriver) {
$client = $driver->getClient();
foreach ($client->getCookieJar()->all() as $cookie) {
if (isset($request_options[RequestOptions::HEADERS]['Cookie'])) {
$request_options[RequestOptions::HEADERS]['Cookie'] .= '; ' . $cookie->getName() . '=' . $cookie->getValue();
}
else {
$request_options[RequestOptions::HEADERS]['Cookie'] = $cookie->getName() . '=' . $cookie->getValue();
}
}
}
return $request_options;
}
}

View File

@@ -0,0 +1,244 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests;
use Drupal\Component\Utility\Html;
use Drupal\Core\Utility\Error;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Provides the debug functions for browser tests.
*/
trait BrowserHtmlDebugTrait {
/**
* Class name for HTML output logging.
*
* @var string
*/
protected $htmlOutputClassName;
/**
* Directory name for HTML output logging.
*
* @var string
*/
protected $htmlOutputDirectory;
/**
* Counter storage for HTML output logging.
*
* @var string
*/
protected $htmlOutputCounterStorage;
/**
* Counter for HTML output logging.
*
* @var int
*/
protected $htmlOutputCounter = 1;
/**
* HTML output enabled.
*
* @var bool
*/
protected $htmlOutputEnabled = FALSE;
/**
* The file name to write the list of URLs to.
*
* This file is read by the PHPUnit result printer.
*
* @var string
*
* @see \Drupal\Tests\Listeners\HtmlOutputPrinter
*/
protected $htmlOutputFile;
/**
* HTML output test ID.
*
* @var int
*/
protected $htmlOutputTestId;
/**
* The Base URI to use for links to the output files.
*
* @var string
*/
protected $htmlOutputBaseUrl;
/**
* Formats HTTP headers as string for HTML output logging.
*
* @param array[] $headers
* Headers that should be formatted.
*
* @return string
* The formatted HTML string.
*/
protected function formatHtmlOutputHeaders(array $headers) {
$flattened_headers = array_map(function ($header) {
if (is_array($header)) {
return implode(';', array_map('trim', $header));
}
else {
return $header;
}
}, $headers);
return '<hr />Headers: <pre>' . Html::escape(var_export($flattened_headers, TRUE)) . '</pre>';
}
/**
* Returns headers in HTML output format.
*
* @return string
* HTML output headers.
*/
protected function getHtmlOutputHeaders() {
return $this->formatHtmlOutputHeaders($this->getSession()->getResponseHeaders());
}
/**
* Logs a HTML output message in a text file.
*
* The link to the HTML output message will be printed by the results printer.
*
* @param string|null $message
* (optional) The HTML output message to be stored. If not supplied the
* current page content is used.
*
* @see \Drupal\Tests\Listeners\VerbosePrinter::printResult()
*/
protected function htmlOutput($message = NULL) {
if (!$this->htmlOutputEnabled) {
return;
}
$message = $message ?: $this->getSession()->getPage()->getContent();
$message = '<hr />ID #' . $this->htmlOutputCounter . ' (<a href="' . $this->htmlOutputClassName . '-' . ($this->htmlOutputCounter - 1) . '-' . $this->htmlOutputTestId . '.html">Previous</a> | <a href="' . $this->htmlOutputClassName . '-' . ($this->htmlOutputCounter + 1) . '-' . $this->htmlOutputTestId . '.html">Next</a>)<hr />' . $message;
$html_output_filename = $this->htmlOutputClassName . '-' . $this->htmlOutputCounter . '-' . $this->htmlOutputTestId . '.html';
file_put_contents($this->htmlOutputDirectory . '/' . $html_output_filename, $message);
file_put_contents($this->htmlOutputCounterStorage, $this->htmlOutputCounter++);
// Do not use the file_url_generator service as the module_handler service
// might not be available.
$uri = $this->htmlOutputBaseUrl . '/sites/simpletest/browser_output/' . $html_output_filename;
file_put_contents($this->htmlOutputFile, $uri . "\n", FILE_APPEND);
}
/**
* Creates the directory to store browser output.
*
* Creates the directory to store browser output in if a file to write
* URLs to has been created by \Drupal\Tests\Listeners\HtmlOutputPrinter.
*/
protected function initBrowserOutputFile() {
$browser_output_file = getenv('BROWSERTEST_OUTPUT_FILE');
$this->htmlOutputEnabled = is_string($browser_output_file) && is_file($browser_output_file);
$this->htmlOutputBaseUrl = getenv('BROWSERTEST_OUTPUT_BASE_URL') ?: $GLOBALS['base_url'];
if ($this->htmlOutputEnabled) {
$this->htmlOutputFile = $browser_output_file;
$this->htmlOutputClassName = str_replace("\\", "_", static::class);
$this->htmlOutputDirectory = DRUPAL_ROOT . '/sites/simpletest/browser_output';
// Do not use the file_system service so this method can be called before
// it is available. Checks !is_dir() twice around mkdir() because a
// concurrent test might have made the directory and caused mkdir() to
// fail. In this case we can still use the directory even though we failed
// to make it.
if (!is_dir($this->htmlOutputDirectory) && !@mkdir($this->htmlOutputDirectory, 0775, TRUE) && !is_dir($this->htmlOutputDirectory)) {
throw new \RuntimeException(sprintf('Unable to create directory: %s', $this->htmlOutputDirectory));
}
if (!file_exists($this->htmlOutputDirectory . '/.htaccess')) {
file_put_contents($this->htmlOutputDirectory . '/.htaccess', "<IfModule mod_expires.c>\nExpiresActive Off\n</IfModule>\n");
}
$this->htmlOutputCounterStorage = $this->htmlOutputDirectory . '/' . $this->htmlOutputClassName . '.counter';
$this->htmlOutputTestId = str_replace('sites/simpletest/', '', $this->siteDirectory);
if (is_file($this->htmlOutputCounterStorage)) {
$this->htmlOutputCounter = max(1, (int) file_get_contents($this->htmlOutputCounterStorage)) + 1;
}
}
}
/**
* Provides a Guzzle middleware handler to log every response received.
*
* @return callable
* The callable handler that will do the logging.
*/
protected function getResponseLogHandler() {
return function (callable $handler) {
return function (RequestInterface $request, array $options) use ($handler) {
return $handler($request, $options)
->then(function (ResponseInterface $response) use ($request) {
if ($this->htmlOutputEnabled) {
$caller = $this->getTestMethodCaller();
$html_output = 'Called from ' . $caller['function'] . ' line ' . $caller['line'];
$html_output .= '<hr />' . $request->getMethod() . ' request to: ' . $request->getUri();
/** @var \Psr\Http\Message\StreamInterface $stream */
$stream = $response->getBody();
// Get the response body as a string. The response stream is set
// to the sink, which defaults to a readable temp stream but can
// be overridden by setting $options['sink'].
$body = $stream->isReadable()
? (string) $stream
: 'Response is not readable.';
// On redirect responses (status code starting with '3') we need
// to remove the meta tag that would do a browser refresh. We
// don't want to redirect developers away when they look at the
// debug output file in their browser.
$status_code = (string) $response->getStatusCode();
if ($status_code[0] === '3') {
$body = preg_replace('#<meta http-equiv="refresh" content=.+/>#', '', $body, 1);
}
$html_output .= '<hr />' . $body;
$html_output .= $this->formatHtmlOutputHeaders($response->getHeaders());
$this->htmlOutput($html_output);
}
return $response;
});
};
};
}
/**
* Retrieves the current calling line in the class under test.
*
* @return array
* An associative array with keys 'file', 'line' and 'function'.
*/
protected function getTestMethodCaller() {
$backtrace = debug_backtrace();
// Find the test class that has the test method.
while ($caller = Error::getLastCaller($backtrace)) {
if (isset($caller['class']) && $caller['class'] === static::class) {
break;
}
// If the test method is implemented by a test class's parent then the
// class name of $this will not be part of the backtrace.
// In that case we process the backtrace until the caller is not a
// subclass of $this and return the previous caller.
if (isset($last_caller) && (!isset($caller['class']) || !is_subclass_of($this, $caller['class']))) {
// Return the last caller since that has to be the test class.
$caller = $last_caller;
break;
}
// Otherwise we have not reached our test class yet: save the last caller
// and remove an element from to backtrace to process the next call.
$last_caller = $caller;
array_shift($backtrace);
}
return $caller;
}
}

View File

@@ -0,0 +1,724 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests;
use Behat\Mink\Driver\BrowserKitDriver;
use Behat\Mink\Element\Element;
use Behat\Mink\Mink;
use Behat\Mink\Selector\SelectorsHandler;
use Behat\Mink\Session;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Database\Database;
use Drupal\Core\Test\FunctionalTestSetupTrait;
use Drupal\Core\Test\TestSetupTrait;
use Drupal\Core\Url;
use Drupal\Core\Utility\Error;
use Drupal\Tests\block\Traits\BlockCreationTrait;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\node\Traits\NodeCreationTrait;
use Drupal\Tests\Traits\PhpUnitWarnings;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\TestTools\Comparator\MarkupInterfaceComparator;
use Drupal\TestTools\TestVarDumper;
use GuzzleHttp\Cookie\CookieJar;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
use Symfony\Component\VarDumper\VarDumper;
/**
* Provides a test case for functional Drupal tests.
*
* Tests extending BrowserTestBase must exist in the
* Drupal\Tests\your_module\Functional namespace and live in the
* modules/your_module/tests/src/Functional directory.
*
* Tests extending this base class should only translate text when testing
* translation functionality. For example, avoid wrapping test text with t()
* or TranslatableMarkup().
*
* Using Symfony's dump() function in functional test test code will produce
* output on the command line; using dump() in site code will produce output in
* the requested web page, which can then be inspected in the HTML output from
* the test.
*
* @ingroup testing
*/
abstract class BrowserTestBase extends TestCase {
use FunctionalTestSetupTrait;
use UiHelperTrait {
FunctionalTestSetupTrait::refreshVariables insteadof UiHelperTrait;
}
use TestSetupTrait;
use BlockCreationTrait {
placeBlock as drupalPlaceBlock;
}
use RandomGeneratorTrait;
use NodeCreationTrait {
getNodeByTitle as drupalGetNodeByTitle;
createNode as drupalCreateNode;
}
use ContentTypeCreationTrait {
createContentType as drupalCreateContentType;
}
use ConfigTestTrait;
use TestRequirementsTrait;
use UserCreationTrait {
createRole as drupalCreateRole;
createUser as drupalCreateUser;
}
use XdebugRequestTrait;
use PhpUnitWarnings;
use PhpUnitCompatibilityTrait;
use ExpectDeprecationTrait;
use ExtensionListTestTrait;
/**
* Time limit in seconds for the test.
*
* @var int
*/
protected $timeLimit = 500;
/**
* The translation file directory for the test environment.
*
* This is set in BrowserTestBase::prepareEnvironment().
*
* @var string
*/
protected $translationFilesDirectory;
/**
* The config importer that can be used in a test.
*
* @var \Drupal\Core\Config\ConfigImporter
*/
protected $configImporter;
/**
* Modules to enable.
*
* The test runner will merge the $modules lists from this class, the class
* it extends, and so on up the class hierarchy. It is not necessary to
* include modules in your list that a parent class has already declared.
*
* @var string[]
*
* @see \Drupal\Tests\BrowserTestBase::installDrupal()
*/
protected static $modules = [];
/**
* The profile to install as a basis for testing.
*
* @var string
*/
protected $profile = 'testing';
/**
* The theme to install as the default for testing.
*
* Defaults to the install profile's default theme, if it specifies any.
*
* @var string
*/
protected $defaultTheme;
/**
* An array of custom translations suitable for SettingsEditor::rewrite().
*
* @var array
*
* @see \Drupal\Core\Site\SettingsEditor::rewrite()
*/
protected $customTranslations;
/**
* Mink class for the default driver to use.
*
* Should be a fully-qualified class name that implements
* Behat\Mink\Driver\DriverInterface.
*
* Value can be overridden using the environment variable MINK_DRIVER_CLASS.
*
* @var string
*/
protected $minkDefaultDriverClass = BrowserKitDriver::class;
/**
* Mink default driver params.
*
* If it's an array its contents are used as constructor params when default
* Mink driver class is instantiated.
*
* Can be overridden using the environment variable MINK_DRIVER_ARGS. In this
* case that variable should be a JSON array, for example:
* '["firefox", null, "http://localhost:4444/wd/hub"]'.
*
* @var array
*/
protected $minkDefaultDriverArgs;
/**
* Mink session manager.
*
* This will not be initialized if there was an error during the test setup.
*
* @var \Behat\Mink\Mink|null
*/
protected $mink;
/**
* {@inheritdoc}
*
* Browser tests are run in separate processes to prevent collisions between
* code that may be loaded by tests.
*/
protected $runTestInSeparateProcess = TRUE;
/**
* {@inheritdoc}
*/
protected $preserveGlobalState = FALSE;
/**
* The base URL.
*
* @var string
*/
protected $baseUrl;
/**
* The original array of shutdown function callbacks.
*
* @var array
*/
protected $originalShutdownCallbacks = [];
/**
* The original container.
*
* Move this to \Drupal\Core\Test\FunctionalTestSetupTrait once TestBase no
* longer provides the same value.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $originalContainer;
/**
* {@inheritdoc}
*/
public static function setUpBeforeClass(): void {
parent::setUpBeforeClass();
VarDumper::setHandler(TestVarDumper::class . '::cliHandler');
}
/**
* Initializes Mink sessions.
*/
protected function initMink() {
$driver = $this->getDefaultDriverInstance();
if ($driver instanceof BrowserKitDriver) {
// Turn off curl timeout. Having a timeout is not a problem in a normal
// test running, but it is a problem when debugging. Also, disable SSL
// peer verification so that testing under HTTPS always works.
/** @var \GuzzleHttp\Client $client */
$client = $this->container->get('http_client_factory')->fromOptions([
'timeout' => NULL,
'verify' => FALSE,
]);
// Inject a Guzzle middleware to generate debug output for every request
// performed in the test.
$handler_stack = $client->getConfig('handler');
$handler_stack->push($this->getResponseLogHandler());
$driver->getClient()->setClient($client);
}
$selectors_handler = new SelectorsHandler([
'hidden_field_selector' => new HiddenFieldSelector(),
]);
$session = new Session($driver, $selectors_handler);
$this->mink = new Mink();
$this->mink->registerSession('default', $session);
$this->mink->setDefaultSessionName('default');
$this->registerSessions();
$this->initFrontPage();
// Copies cookies from the current environment, for example, XDEBUG_SESSION
// in order to support Xdebug.
// @see BrowserTestBase::initFrontPage()
$cookies = $this->extractCookiesFromRequest(\Drupal::request());
foreach ($cookies as $cookie_name => $values) {
foreach ($values as $value) {
$session->setCookie($cookie_name, $value);
}
}
return $session;
}
/**
* Visits the front page when initializing Mink.
*
* According to the W3C WebDriver specification a cookie can only be set if
* the cookie domain is equal to the domain of the active document. When the
* browser starts up the active document is not our domain but 'about:blank'
* or similar. To be able to set our User-Agent and Xdebug cookies at the
* start of the test we now do a request to the front page so the active
* document matches the domain.
*
* @see https://w3c.github.io/webdriver/webdriver-spec.html#add-cookie
* @see https://www.w3.org/Bugs/Public/show_bug.cgi?id=20975
*/
protected function initFrontPage() {
$session = $this->getSession();
$session->visit($this->baseUrl);
}
/**
* Gets an instance of the default Mink driver.
*
* @return \Behat\Mink\Driver\DriverInterface
* Instance of default Mink driver.
*
* @throws \InvalidArgumentException
* When provided default Mink driver class can't be instantiated.
*/
protected function getDefaultDriverInstance() {
// Get default driver params from environment if available.
if ($arg_json = $this->getMinkDriverArgs()) {
$this->minkDefaultDriverArgs = json_decode($arg_json, TRUE);
}
// Get and check default driver class from environment if available.
if ($minkDriverClass = getenv('MINK_DRIVER_CLASS')) {
if (class_exists($minkDriverClass)) {
$this->minkDefaultDriverClass = $minkDriverClass;
}
else {
throw new \InvalidArgumentException("Can't instantiate provided $minkDriverClass class by environment as default driver class.");
}
}
if ($this->minkDefaultDriverClass === BrowserKitDriver::class) {
$driver = new $this->minkDefaultDriverClass(new DrupalTestBrowser());
}
elseif (is_array($this->minkDefaultDriverArgs)) {
// Use ReflectionClass to instantiate class with received params.
$reflector = new \ReflectionClass($this->minkDefaultDriverClass);
$driver = $reflector->newInstanceArgs($this->minkDefaultDriverArgs);
}
else {
$driver = new $this->minkDefaultDriverClass();
}
return $driver;
}
/**
* Gets the Mink driver args from an environment variable.
*
* The environment variable can be overridden in a derived class so it is
* possible to use a different value for a subset of tests, e.g. the
* JavaScript tests.
*
* @return string|false
* The JSON-encoded argument string. False if it is not set.
*/
protected function getMinkDriverArgs() {
return getenv('MINK_DRIVER_ARGS');
}
/**
* Registers additional Mink sessions.
*
* Tests wishing to use a different driver or change the default driver should
* override this method.
*
* @code
* // Register a new session that uses the MinkPonyDriver.
* $pony = new MinkPonyDriver();
* $session = new Session($pony);
* $this->mink->registerSession('pony', $session);
* @endcode
*/
protected function registerSessions() {}
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpAppRoot();
// Allow tests to compare MarkupInterface objects via assertEquals().
$this->registerComparator(new MarkupInterfaceComparator());
$this->setupBaseUrl();
// Install Drupal test site.
$this->prepareEnvironment();
$this->installDrupal();
// Setup Mink.
$this->initMink();
// Set up the browser test output file.
$this->initBrowserOutputFile();
// Ensure that the test is not marked as risky because of no assertions. In
// PHPUnit 6 tests that only make assertions using $this->assertSession()
// can be marked as risky.
$this->addToAssertionCount(1);
}
/**
* {@inheritdoc}
*/
public function __get(string $name) {
if ($name === 'randomGenerator') {
@trigger_error('Accessing the randomGenerator property is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use getRandomGenerator() instead. See https://www.drupal.org/node/3358445', E_USER_DEPRECATED);
return $this->getRandomGenerator();
}
}
/**
* Sets up the root application path.
*/
protected function setUpAppRoot(): void {
if ($this->root === NULL) {
$this->root = dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__)), 2);
}
}
/**
* Ensures test files are deletable.
*
* Some tests chmod generated files to be read only. During
* BrowserTestBase::cleanupEnvironment() and other cleanup operations,
* these files need to get deleted too.
*
* @param string $path
* The file path.
*
* @see \Drupal\Core\File\FileSystemInterface::deleteRecursive()
*/
public static function filePreDeleteCallback($path) {
// When the webserver runs with the same system user as phpunit, we can
// make read-only files writable again. If not, chmod will fail while the
// file deletion still works if file permissions have been configured
// correctly. Thus, we ignore any problems while running chmod.
@chmod($path, 0700);
}
/**
* Clean up the test environment.
*/
protected function cleanupEnvironment() {
// Remove all prefixed tables.
$original_connection_info = Database::getConnectionInfo('simpletest_original_default');
$original_prefix = $original_connection_info['default']['prefix'];
$test_connection_info = Database::getConnectionInfo('default');
$test_prefix = $test_connection_info['default']['prefix'];
if ($original_prefix != $test_prefix) {
$tables = Database::getConnection()->schema()->findTables('%');
foreach ($tables as $table) {
if (Database::getConnection()->schema()->dropTable($table)) {
unset($tables[$table]);
}
}
}
// Delete test site directory.
\Drupal::service('file_system')->deleteRecursive($this->siteDirectory, [$this, 'filePreDeleteCallback']);
}
/**
* {@inheritdoc}
*/
protected function tearDown(): void {
parent::tearDown();
if ($this->container) {
// Cleanup mock session started in DrupalKernel::preHandle().
try {
/** @var \Symfony\Component\HttpFoundation\Session\Session $session */
$session = $this->container->get('request_stack')->getSession();
$session->clear();
$session->save();
}
catch (SessionNotFoundException) {
@trigger_error('Pushing requests without a session onto the request_stack is deprecated in drupal:10.3.0 and an error will be thrown from drupal:11.0.0. See https://www.drupal.org/node/3337193', E_USER_DEPRECATED);
}
}
// Destroy the testing kernel.
if (isset($this->kernel)) {
$this->cleanupEnvironment();
$this->kernel->shutdown();
}
// Ensure that internal logged in variable is reset.
$this->loggedInUser = FALSE;
if ($this->mink) {
$this->mink->stopSessions();
}
// Restore original shutdown callbacks.
if (function_exists('drupal_register_shutdown_function')) {
$callbacks = &drupal_register_shutdown_function();
$callbacks = $this->originalShutdownCallbacks;
}
}
/**
* Returns Mink session.
*
* @param string $name
* (optional) Name of the session. Defaults to the active session.
*
* @return \Behat\Mink\Session
* The active Mink session object.
*/
public function getSession($name = NULL) {
return $this->mink->getSession($name);
}
/**
* Get session cookies from current session.
*
* @return \GuzzleHttp\Cookie\CookieJar
* A cookie jar with the current session.
*/
protected function getSessionCookies() {
$domain = parse_url($this->getUrl(), PHP_URL_HOST);
$session_id = $this->getSession()->getCookie($this->getSessionName());
$cookies = CookieJar::fromArray([$this->getSessionName() => $session_id], $domain);
return $cookies;
}
/**
* Obtain the HTTP client for the system under test.
*
* Use this method for arbitrary HTTP requests to the site under test. For
* most tests, you should not get the HTTP client and instead use navigation
* methods such as drupalGet() and clickLink() in order to benefit from
* assertions.
*
* Subclasses which substitute a different Mink driver should override this
* method and provide a Guzzle client if the Mink driver provides one.
*
* @return \GuzzleHttp\ClientInterface
* The client with BrowserTestBase configuration.
*
* @throws \RuntimeException
* If the Mink driver does not support a Guzzle HTTP client, throw an
* exception.
*/
protected function getHttpClient() {
/** @var \Behat\Mink\Driver\DriverInterface $mink_driver */
$mink_driver = $this->getSession()->getDriver();
if ($this->isTestUsingGuzzleClient()) {
return $mink_driver->getClient()->getClient();
}
throw new \RuntimeException('The Mink client type ' . get_class($mink_driver) . ' does not support getHttpClient().');
}
/**
* Helper function to get the options of select field.
*
* @param \Behat\Mink\Element\NodeElement|string $select
* Name, ID, or Label of select field to assert.
* @param \Behat\Mink\Element\Element $container
* (optional) Container element to check against. Defaults to current page.
*
* @return array
* Associative array of option keys and values.
*/
protected function getOptions($select, ?Element $container = NULL) {
if (is_string($select)) {
$select = $this->assertSession()->selectExists($select, $container);
}
$options = [];
/** @var \Behat\Mink\Element\NodeElement $option */
foreach ($select->findAll('xpath', '//option') as $option) {
$label = $option->getText();
$value = $option->getAttribute('value') ?: $label;
$options[$value] = $label;
}
return $options;
}
/**
* Installs Drupal into the test site.
*/
public function installDrupal() {
$this->initUserSession();
$this->prepareSettings();
$this->doInstall();
$this->initSettings();
$this->container = $container = $this->initKernel(\Drupal::request());
$this->initConfig($container);
$this->installDefaultThemeFromClassProperty($container);
$this->installModulesFromClassProperty($container);
// Clear the static cache so that subsequent cache invalidations will work
// as expected.
$this->container->get('cache_tags.invalidator')->resetChecksums();
// Generate a route to prime the URL generator with the correct base URL.
// @todo Remove in https://www.drupal.org/project/drupal/issues/3207896.
Url::fromRoute('<front>')->setAbsolute()->toString();
// Explicitly call register() again on the container registered in \Drupal.
// @todo This should already be called through
// DrupalKernel::prepareLegacyRequest() -> DrupalKernel::boot() but that
// appears to be calling a different container.
$this->container->get('stream_wrapper_manager')->register();
}
/**
* Prevents serializing any properties.
*
* Browser tests are run in a separate process. To do this PHPUnit creates a
* script to run the test. If it fails, the test result object will contain a
* stack trace which includes the test object. It will attempt to serialize
* it. Returning an empty array prevents it from serializing anything it
* should not.
*
* @return array
* An empty array.
*
* @see vendor/phpunit/phpunit/src/Util/PHP/Template/TestCaseMethod.tpl.dist
*/
public function __sleep() {
return [];
}
/**
* Performs an xpath search on the contents of the internal browser.
*
* The search is relative to the root element (HTML tag normally) of the page.
*
* @param string $xpath
* The xpath string to use in the search.
* @param array $arguments
* An array of arguments with keys in the form ':name' matching the
* placeholders in the query. The values may be either strings or numeric
* values.
*
* @return \Behat\Mink\Element\NodeElement[]
* The list of elements matching the xpath expression.
*/
protected function xpath($xpath, array $arguments = []) {
$xpath = $this->assertSession()->buildXPathQuery($xpath, $arguments);
return $this->getSession()->getPage()->findAll('xpath', $xpath);
}
/**
* Configuration accessor for tests. Returns non-overridden configuration.
*
* @param string $name
* Configuration name.
*
* @return \Drupal\Core\Config\Config
* The configuration object with original configuration data.
*/
protected function config($name) {
return $this->container->get('config.factory')->getEditable($name);
}
/**
* Gets the JavaScript drupalSettings variable for the currently-loaded page.
*
* @return array
* The JSON decoded drupalSettings value from the current page.
*/
protected function getDrupalSettings() {
$html = $this->getSession()->getPage()->getContent();
if (preg_match('@<script type="application/json" data-drupal-selector="drupal-settings-json">([^<]*)</script>@', $html, $matches)) {
$settings = Json::decode($matches[1]);
if (isset($settings['ajaxPageState']['libraries'])) {
$settings['ajaxPageState']['libraries'] = UrlHelper::uncompressQueryParameter($settings['ajaxPageState']['libraries']);
}
return $settings;
}
return [];
}
/**
* Retrieves the current calling line in the class under test.
*
* @return array
* An associative array with keys 'file', 'line' and 'function'.
*/
protected function getTestMethodCaller() {
$backtrace = debug_backtrace();
// Find the test class that has the test method.
while ($caller = Error::getLastCaller($backtrace)) {
// If we match PHPUnit's TestCase::runTest, then the previously processed
// caller entry is where our test method sits.
if (isset($last_caller) && isset($caller['function']) && $caller['function'] === 'PHPUnit\Framework\TestCase->runTest()') {
// Return the last caller since that has to be the test class.
$caller = $last_caller;
break;
}
// If the test method is implemented by a test class's parent then the
// class name of $this will not be part of the backtrace.
// In that case we process the backtrace until the caller is not a
// subclass of $this and return the previous caller.
if (isset($last_caller) && (!isset($caller['class']) || !is_subclass_of($this, $caller['class']))) {
// Return the last caller since that has to be the test class.
$caller = $last_caller;
break;
}
if (isset($caller['class']) && $caller['class'] === static::class) {
break;
}
// Otherwise we have not reached our test class yet: save the last caller
// and remove an element from to backtrace to process the next call.
$last_caller = $caller;
array_shift($backtrace);
}
return $caller;
}
/**
* Transforms a nested array into a flat array suitable for submitForm().
*
* @param array $values
* A multi-dimensional form values array to convert.
*
* @return array
* The flattened $edit array suitable for BrowserTestBase::submitForm().
*/
protected function translatePostValues(array $values) {
$edit = [];
// The easiest and most straightforward way to translate values suitable for
// BrowserTestBase::submitForm() is to actually build the POST data
// string and convert the resulting key/value pairs back into a flat array.
$query = http_build_query($values);
foreach (explode('&', $query) as $item) {
[$key, $value] = explode('=', $item);
$edit[urldecode($key)] = urldecode($value);
}
return $edit;
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\CSpell;
use PHPUnit\Framework\TestCase;
/**
* Tests that the dictionary.txt file is properly sorted.
*
* @group cspell
*/
class SortTest extends TestCase {
/**
* The path to the dictionary file.
*/
private string $filePath;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->filePath = dirname(__DIR__, 5) . '/core/misc/cspell/dictionary.txt';
}
/**
* Tests that the file exists.
*/
public function testFileExists(): void {
$this->assertFileExists($this->filePath);
}
/**
* Tests that the file is properly sorted.
*/
public function testSorted(): void {
$content = file_get_contents($this->filePath);
$this->assertIsString($content);
$current_dictionary = explode("\n", rtrim($content));
$this->assertIsArray($current_dictionary);
$sorted_dictionary = $current_dictionary;
sort($current_dictionary);
$this->assertSame($current_dictionary, $sorted_dictionary);
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation;
use Drupal\Component\Annotation\Plugin\Discovery\AnnotatedClassDiscovery;
use Drupal\Component\FileCache\FileCacheFactory;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Annotation\Plugin\Discovery\AnnotatedClassDiscovery
* @group Annotation
* @runTestsInSeparateProcesses
*/
class AnnotatedClassDiscoveryCachedTest extends TestCase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Ensure FileCacheFactory::DISABLE_CACHE is *not* set, since we're testing
// integration with the file cache.
FileCacheFactory::setConfiguration([]);
// Ensure that FileCacheFactory has a prefix.
FileCacheFactory::setPrefix('prefix');
}
/**
* Tests that getDefinitions() retrieves the file cache correctly.
*
* @covers ::getDefinitions
*/
public function testGetDefinitions(): void {
// Path to the classes which we'll discover and parse annotation.
$discovery_path = __DIR__ . '/Fixtures';
// File path that should be discovered within that directory.
$file_path = $discovery_path . '/PluginNamespace/DiscoveryTest1.php';
$discovery = new AnnotatedClassDiscovery(['com\example' => [$discovery_path]]);
$this->assertEquals([
'discovery_test_1' => [
'id' => 'discovery_test_1',
'class' => 'com\example\PluginNamespace\DiscoveryTest1',
],
], $discovery->getDefinitions());
// Gain access to the file cache so we can change it.
$ref_file_cache = new \ReflectionProperty($discovery, 'fileCache');
/** @var \Drupal\Component\FileCache\FileCacheInterface $file_cache */
$file_cache = $ref_file_cache->getValue($discovery);
// The file cache is keyed by the file path, and we'll add some known
// content to test against.
$file_cache->set($file_path, [
'id' => 'wrong_id',
'content' => serialize(['an' => 'array']),
]);
// Now perform the same query and check for the cached results.
$this->assertEquals([
'wrong_id' => [
'an' => 'array',
],
], $discovery->getDefinitions());
}
}

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation;
use Drupal\Component\Annotation\Plugin;
use Drupal\Component\Annotation\Plugin\Discovery\AnnotatedClassDiscovery;
use Drupal\Component\FileCache\FileCacheFactory;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Annotation\Plugin\Discovery\AnnotatedClassDiscovery
* @group Annotation
* @runTestsInSeparateProcesses
*/
class AnnotatedClassDiscoveryTest extends TestCase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Ensure the file cache is disabled.
FileCacheFactory::setConfiguration([FileCacheFactory::DISABLE_CACHE => TRUE]);
// Ensure that FileCacheFactory has a prefix.
FileCacheFactory::setPrefix('prefix');
}
/**
* @covers ::__construct
* @covers ::getPluginNamespaces
*/
public function testGetPluginNamespaces(): void {
$discovery = new AnnotatedClassDiscovery(['com/example' => [__DIR__]]);
$reflection = new \ReflectionMethod($discovery, 'getPluginNamespaces');
$result = $reflection->invoke($discovery);
$this->assertEquals(['com/example' => [__DIR__]], $result);
}
/**
* @covers ::getDefinitions
* @covers ::prepareAnnotationDefinition
* @covers ::getAnnotationReader
*/
public function testGetDefinitions(): void {
$discovery = new AnnotatedClassDiscovery(['com\example' => [__DIR__ . '/Fixtures']]);
$this->assertEquals([
'discovery_test_1' => [
'id' => 'discovery_test_1',
'class' => 'com\example\PluginNamespace\DiscoveryTest1',
],
], $discovery->getDefinitions());
$custom_annotation_discovery = new AnnotatedClassDiscovery(['com\example' => [__DIR__ . '/Fixtures']], CustomPlugin::class, ['Drupal\Tests\Component\Annotation']);
$this->assertEquals([
'discovery_test_1' => [
'id' => 'discovery_test_1',
'class' => 'com\example\PluginNamespace\DiscoveryTest1',
'title' => 'Discovery test plugin',
],
], $custom_annotation_discovery->getDefinitions());
$empty_discovery = new AnnotatedClassDiscovery(['com\example' => [__DIR__ . '/Fixtures']], CustomPlugin2::class, ['Drupal\Tests\Component\Annotation']);
$this->assertEquals([], $empty_discovery->getDefinitions());
}
}
/**
* Custom plugin annotation.
*
* @Annotation
*/
class CustomPlugin extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The plugin title.
*
* @var string
*
* @ingroup plugin_translatable
*/
public $title = '';
}
/**
* Custom plugin annotation.
*
* @Annotation
*/
class CustomPlugin2 extends Plugin {}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation;
use Drupal\Component\Annotation\AnnotationBase;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Annotation\AnnotationBase
* @group Annotation
*/
class AnnotationBaseTest extends TestCase {
/**
* @covers ::getProvider
* @covers ::setProvider
*/
public function testSetProvider(): void {
$plugin = new AnnotationBaseStub();
$plugin->setProvider('example');
$this->assertEquals('example', $plugin->getProvider());
}
/**
* @covers ::getId
*/
public function testGetId(): void {
$plugin = new AnnotationBaseStub();
// Doctrine sets the public prop directly.
$plugin->id = 'example';
$this->assertEquals('example', $plugin->getId());
}
/**
* @covers ::getClass
* @covers ::setClass
*/
public function testSetClass(): void {
$plugin = new AnnotationBaseStub();
$plugin->setClass('example');
$this->assertEquals('example', $plugin->getClass());
}
}
/**
* {@inheritdoc}
*/
class AnnotationBaseStub extends AnnotationBase {
/**
* {@inheritdoc}
*/
public function get() {}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation;
use Drupal\Component\Annotation\Doctrine\DocParser;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Annotation\Doctrine\DocParser
*
* @group Annotation
*/
class DocParserIgnoredClassesTest extends TestCase {
/**
* Ensure annotations can be ignored when namespaces are present.
*
* Drupal's DocParser should never use class_exists() on an ignored
* annotation, including cases where namespaces are set.
*/
public function testIgnoredAnnotationSkippedBeforeReflection(): void {
$annotation = 'neverReflectThis';
$parser = new DocParser();
$parser->setIgnoredAnnotationNames([$annotation => TRUE]);
$parser->addNamespace('\\Arbitrary\\Namespace');
// Register our class loader which will fail if the parser tries to
// autoload disallowed annotations.
$autoloader = function ($class_name) use ($annotation) {
$name_array = explode('\\', $class_name);
$name = array_pop($name_array);
if ($name == $annotation) {
$this->fail('Attempted to autoload an ignored annotation: ' . $name);
}
};
spl_autoload_register($autoloader, TRUE, TRUE);
// Perform the parse.
$this->assertEmpty($parser->parse('@neverReflectThis'));
// Clean up after ourselves.
spl_autoload_unregister($autoloader);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures\Annotation;
/** @Annotation */
class AnnotWithDefaultValue
{
/** @var string */
public $foo = 'bar';
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures\Annotation;
/**
* @Annotation
*/
class Autoload
{
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures\Annotation;
/** @Annotation */
class Route
{
/** @var string @Required */
public $pattern;
public $name;
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures\Annotation;
/** @Annotation */
class Secure
{
private $roles;
public function __construct(array $values)
{
if (is_string($values['value'])) {
$values['value'] = array($values['value']);
}
$this->roles = $values['value'];
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures\Annotation;
/** @Annotation */
class Template
{
private $name;
public function __construct(array $values)
{
$this->name = isset($values['value']) ? $values['value'] : null;
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures\Annotation;
/**
* @Annotation
* @Target("PROPERTY")
*/
final class Version
{
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
/**
* @Annotation
* @Target("ALL")
*/
final class AnnotationEnum
{
const ONE = 'ONE';
const TWO = 'TWO';
const THREE = 'THREE';
/**
* @var mixed
*
* @Enum({"ONE","TWO","THREE"})
*/
public $value;
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
/**
* @Annotation
* @Target("ALL")
*/
final class AnnotationEnumInvalid
{
/**
* @var mixed
*
* @Enum({1, 2, "foo", "bar", {"foo":"bar"}})
*/
public $value;
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
use Drupal\Tests\Component\Annotation\Doctrine\Fixtures\AnnotationEnumLiteral as SelfEnum;
/**
* @Annotation
* @Target("ALL")
*/
final class AnnotationEnumLiteral
{
const ONE = 1;
const TWO = 2;
const THREE = 3;
/**
* @var mixed
*
* @Enum(
* value = {
* 1,
* 2,
* 3,
* },
* literal = {
* 1 : "AnnotationEnumLiteral::ONE",
* 2 : "AnnotationEnumLiteral::TWO",
* 3 : "AnnotationEnumLiteral::THREE",
* }
* )
*/
public $value;
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
/**
* @Annotation
* @Target("ALL")
*/
final class AnnotationEnumLiteralInvalid
{
const ONE = 1;
const TWO = 2;
const THREE = 3;
/**
* @var mixed
*
* @Enum(
* value = {
* 1,
* 2
* },
* literal = {
* 1 : "AnnotationEnumLiteral::ONE",
* 2 : "AnnotationEnumLiteral::TWO",
* 3 : "AnnotationEnumLiteral::THREE"
* }
* )
*/
public $value;
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
/**
* @Annotation
* @Target("ALL")
*/
class AnnotationTargetAll
{
public $data;
public $name;
public $target;
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
/**
* @Annotation
* @Target({ "ANNOTATION" })
*/
final class AnnotationTargetAnnotation
{
public $data;
public $name;
public $target;
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
/**
* @Annotation
* @Target("CLASS")
*/
final class AnnotationTargetClass
{
public $data;
public $name;
public $target;
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
/**
* @Annotation
* @Target({ "METHOD", "PROPERTY" })
*/
final class AnnotationTargetPropertyMethod
{
public $data;
public $name;
public $target;
}

View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
/**
* @Annotation
* @Target("ALL")
* @Attributes({
@Attribute("mixed", type = "mixed"),
@Attribute("boolean", type = "boolean"),
@Attribute("bool", type = "bool"),
@Attribute("float", type = "float"),
@Attribute("string", type = "string"),
@Attribute("integer", type = "integer"),
@Attribute("array", type = "array"),
@Attribute("arrayOfIntegers", type = "array<integer>"),
@Attribute("arrayOfStrings", type = "string[]"),
@Attribute("annotation", type = "Drupal\Tests\Component\Annotation\Doctrine\Fixtures\AnnotationTargetAll"),
@Attribute("arrayOfAnnotations", type = "array<Drupal\Tests\Component\Annotation\Doctrine\Fixtures\AnnotationTargetAll>"),
})
*/
final class AnnotationWithAttributes
{
public final function __construct(array $data)
{
foreach ($data as $key => $value) {
$this->$key = $value;
}
}
private $mixed;
private $boolean;
private $bool;
private $float;
private $string;
private $integer;
private $array;
private $annotation;
private $arrayOfIntegers;
private $arrayOfStrings;
private $arrayOfAnnotations;
/**
* @return mixed
*/
public function getMixed()
{
return $this->mixed;
}
/**
* @return boolean
*/
public function getBoolean()
{
return $this->boolean;
}
/**
* @return bool
*/
public function getBool()
{
return $this->bool;
}
/**
* @return float
*/
public function getFloat()
{
return $this->float;
}
/**
* @return string
*/
public function getString()
{
return $this->string;
}
public function getInteger()
{
return $this->integer;
}
/**
* @return array
*/
public function getArray()
{
return $this->array;
}
/**
* @return Drupal\Tests\Component\Annotation\Doctrine\Fixtures\AnnotationTargetAll
*/
public function getAnnotation()
{
return $this->annotation;
}
/**
* @return string[]
*/
public function getArrayOfStrings()
{
return $this->arrayOfIntegers;
}
/**
* @return array<integer>
*/
public function getArrayOfIntegers()
{
return $this->arrayOfIntegers;
}
/**
* @return array<Drupal\Tests\Component\Annotation\Doctrine\Fixtures\AnnotationTargetAll>
*/
public function getArrayOfAnnotations()
{
return $this->arrayOfAnnotations;
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
/**
* @Annotation
* @Target("ALL")
*/
final class AnnotationWithConstants
{
const INTEGER = 1;
const FLOAT = 1.2;
const STRING = '1.2.3';
/**
* @var mixed
*/
public $value;
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
/**
* @Annotation
* @Target("ALL")
* @Attributes({
@Attribute("value", required = true , type = "string"),
@Attribute("annot", required = true , type = "Drupal\Tests\Component\Annotation\Doctrine\Fixtures\AnnotationTargetAnnotation"),
})
*/
final class AnnotationWithRequiredAttributes
{
public final function __construct(array $data)
{
foreach ($data as $key => $value) {
$this->$key = $value;
}
}
/**
* @var string
*/
private $value;
/**
*
* @var Drupal\Tests\Component\Annotation\Doctrine\Fixtures\AnnotationTargetAnnotation
*/
private $annot;
/**
* @return string
*/
public function getValue()
{
return $this->value;
}
/**
* @return Drupal\Tests\Component\Annotation\Doctrine\Fixtures\AnnotationTargetAnnotation
*/
public function getAnnot()
{
return $this->annot;
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
/**
* @Annotation
* @Target("ALL")
*/
final class AnnotationWithRequiredAttributesWithoutContructor
{
/**
* @Required
* @var string
*/
public $value;
/**
* @Required
* @var Drupal\Tests\Component\Annotation\Doctrine\Fixtures\AnnotationTargetAnnotation
*/
public $annot;
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
/**
* @Annotation
* @Target(@)
*/
final class AnnotationWithTargetSyntaxError
{
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
/**
* @Annotation
* @Target("ALL")
*/
final class AnnotationWithVarType
{
/**
* @var mixed
*/
public $mixed;
/**
* @var boolean
*/
public $boolean;
/**
* @var bool
*/
public $bool;
/**
* @var float
*/
public $float;
/**
* @var string
*/
public $string;
/**
* @var integer
*/
public $integer;
/**
* @var array
*/
public $array;
/**
* @var Drupal\Tests\Component\Annotation\Doctrine\Fixtures\AnnotationTargetAll
*/
public $annotation;
/**
* @var array<integer>
*/
public $arrayOfIntegers;
/**
* @var string[]
*/
public $arrayOfStrings;
/**
* @var array<Drupal\Tests\Component\Annotation\Doctrine\Fixtures\AnnotationTargetAll>
*/
public $arrayOfAnnotations;
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures\Attribute;
#[\Attribute]
final class AttributeClass
{
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures\Attribute;
#[/* Comment */\Drupal\Tests\Component\Annotation\Doctrine\Fixtures\ExtraAttributes\ExampleAttribute]
final class FullyQualified
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures\Attribute;
#[\Attribute /* Comment */]
#[/* Comment */AttributeClass]
final class MultipleAttributes
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures\Attribute;
// @phpstan-ignore-next-line
#[NonexistentAttribute]
final class Nonexistent
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures\Attribute;
use Drupal\Tests\Component\Annotation\Doctrine\Fixtures\ExtraAttributes;
#[/* Comment */ExtraAttributes\ExampleAttribute]
final class Qualified
{
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures\Attribute;
#[/* Comment */SubDir\SubDirAttribute]
final class Relative
{
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures\Attribute\SubDir;
#[\Attribute]
final class SubDirAttribute
{
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures\Attribute;
use Drupal\Tests\Component\Annotation\Doctrine\Fixtures\ExtraAttributes\ExampleAttribute;
#[/* Comment */ExampleAttribute]
final class Used
{
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures\Attribute;
use Drupal\Tests\Component\Annotation\Doctrine\Fixtures\ExtraAttributes\ExampleAttribute as ClassAttribute;
#[/* Comment */ClassAttribute]
final class UsedAs
{
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures\Attribute;
use Drupal\Tests\Component\Annotation\Doctrine\Fixtures\ExtraAttributes as ClassAttributes;
#[/* Comment */ClassAttributes\ExampleAttribute]
final class UsedAsQualified
{
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
class ClassWithConstants
{
const SOME_VALUE = 'ClassWithConstants.SOME_VALUE';
const SOME_KEY = 'ClassWithConstants.SOME_KEY';
const OTHER_KEY_ = 'ClassWithConstants.OTHER_KEY_';
const OTHER_KEY_2 = 'ClassWithConstants.OTHER_KEY_2';
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
use Drupal\Tests\Component\Annotation\Doctrine\Fixtures\AnnotationTargetPropertyMethod;
/**
* @AnnotationTargetPropertyMethod("Some data")
*/
class ClassWithInvalidAnnotationTargetAtClass
{
/**
* @AnnotationTargetPropertyMethod("Bar")
*/
public $foo;
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
use Drupal\Tests\Component\Annotation\Doctrine\Fixtures\AnnotationTargetClass;
/**
* @AnnotationTargetClass("Some data")
*/
class ClassWithInvalidAnnotationTargetAtMethod
{
/**
* @AnnotationTargetClass("functionName")
*/
public function functionName($param)
{
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
use Drupal\Tests\Component\Annotation\Doctrine\Fixtures\AnnotationTargetClass;
use Drupal\Tests\Component\Annotation\Doctrine\Fixtures\AnnotationTargetAnnotation;
/**
* @AnnotationTargetClass("Some data")
*/
class ClassWithInvalidAnnotationTargetAtProperty
{
/**
* @AnnotationTargetClass("Bar")
*/
public $foo;
/**
* @AnnotationTargetAnnotation("Foo")
*/
public $bar;
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
use Drupal\Tests\Component\Annotation\Doctrine\Fixtures\AnnotationTargetClass;
use Drupal\Tests\Component\Annotation\Doctrine\Fixtures\AnnotationTargetAll;
use Drupal\Tests\Component\Annotation\Doctrine\Fixtures\AnnotationTargetPropertyMethod;
/**
* @AnnotationTargetClass("Some data")
*/
class ClassWithValidAnnotationTarget
{
/**
* @AnnotationTargetPropertyMethod("Some data")
*/
public $foo;
/**
* @AnnotationTargetAll("Some data",name="Some name")
*/
public $name;
/**
* @AnnotationTargetPropertyMethod("Some data",name="Some name")
*/
public function someFunction()
{
}
/**
* @AnnotationTargetAll(@AnnotationTargetAnnotation)
*/
public $nested;
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures\ExtraAttributes;
#[\Attribute]
final class ExampleAttribute extends ExampleParentAttribute
{
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures\ExtraAttributes;
#[\Attribute]
class ExampleParentAttribute {
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Fixtures;
interface IntefaceWithConstants
{
const SOME_VALUE = 'IntefaceWithConstants.SOME_VALUE';
const SOME_KEY = 'IntefaceWithConstants.SOME_KEY';
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Drupal\Tests\Component\Annotation\Doctrine;
use Drupal\Component\Annotation\Doctrine\StaticReflectionParser;
use Drupal\Component\Annotation\Reflection\MockFileFinder;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Annotation\Doctrine\StaticReflectionParser
*
* @group Annotation
*/
class StaticReflectionParserTest extends TestCase {
/**
* @testWith ["AttributeClass", "\\Attribute", true]
* ["AttributeClass", "attribute", true]
* ["AttributeClass", "Attribute", true]
* ["AttributeClass", "\\DoesNotExist", false]
* ["Nonexistent", "NonexistentAttribute", false]
* ["MultipleAttributes", "Attribute", true]
* ["MultipleAttributes", "Drupal\\Tests\\Component\\Annotation\\Doctrine\\Fixtures\\Attribute\\AttributeClass", true]
* ["MultipleAttributes", "DoesNotExist", false]
* ["FullyQualified", "Drupal\\Tests\\Component\\Annotation\\Doctrine\\Fixtures\\ExtraAttributes\\ExampleAttribute", true]
* ["Used", "Drupal\\Tests\\Component\\Annotation\\Doctrine\\Fixtures\\ExtraAttributes\\ExampleAttribute", true]
* ["UsedAs", "Drupal\\Tests\\Component\\Annotation\\Doctrine\\Fixtures\\ExtraAttributes\\ExampleAttribute", true]
* ["UsedAsQualified", "Drupal\\Tests\\Component\\Annotation\\Doctrine\\Fixtures\\ExtraAttributes\\ExampleAttribute", true]
* ["Qualified", "Drupal\\Tests\\Component\\Annotation\\Doctrine\\Fixtures\\ExtraAttributes\\ExampleAttribute", true]
* ["Relative", "Drupal\\Tests\\Component\\Annotation\\Doctrine\\Fixtures\\Attribute\\SubDir\\SubDirAttribute", true]
* ["FullyQualified", "Drupal\\Tests\\Component\\Annotation\\Doctrine\\Fixtures\\ExtraAttributes\\ExampleParentAttribute", true]
* ["Used", "Drupal\\Tests\\Component\\Annotation\\Doctrine\\Fixtures\\ExtraAttributes\\ExampleParentAttribute", true]
* ["UsedAs", "Drupal\\Tests\\Component\\Annotation\\Doctrine\\Fixtures\\ExtraAttributes\\ExampleParentAttribute", true]
* ["UsedAsQualified", "Drupal\\Tests\\Component\\Annotation\\Doctrine\\Fixtures\\ExtraAttributes\\ExampleParentAttribute", true]
* ["Qualified", "Drupal\\Tests\\Component\\Annotation\\Doctrine\\Fixtures\\ExtraAttributes\\ExampleParentAttribute", true]
*/
public function testAttribute(string $class, string $attribute_class, bool $expected) {
$finder = MockFileFinder::create(__DIR__ . '/Fixtures/Attribute/' . $class . '.php');
$parser = new StaticReflectionParser('\\Drupal\\Tests\\Component\\Annotation\\Doctrine\\Fixtures\\Attribute\\' . $class, $finder);
$this->assertSame($expected, $parser->hasClassAttribute($attribute_class), "'$class' has attribute that is a '$attribute_class'");
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
// Some class named Entity in the global namespace
/**
* This class is a near-copy of
* tests/Doctrine/Tests/Common/Annotations/Ticket/DCOM58Entity.php, which is
* part of the Doctrine project: <http://www.doctrine-project.org>. It was
* copied from version 1.2.7.
*
* @Annotation
*/
class Entity
{
}

View File

@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Doctrine\Ticket;
use Drupal\Component\Annotation\Doctrine\DocParser;
use Drupal\Component\Annotation\Doctrine\SimpleAnnotationReader;
use PHPUnit\Framework\TestCase;
/**
* This class is a near-copy of
* \Doctrine\Tests\Common\Annotations\Ticket\DCOM58Test, which is part of the
* Doctrine project: <http://www.doctrine-project.org>. It was copied from
* version 1.2.7.
*
* @group DCOM58
*
* Run this test in a separate process as it includes code that might have side
* effects.
* @runTestsInSeparateProcesses
*/
class DCOM58Test extends TestCase
{
protected function setUp(): void
{
// Some class named Entity in the global namespace.
include __DIR__ .'/DCOM58Entity.php';
}
public function testIssueGlobalNamespace()
{
$docblock = "@Entity";
$parser = new DocParser();
$parser->setImports(array(
"__NAMESPACE__" =>"Drupal\Tests\Component\Annotation\Doctrine\Ticket\Doctrine\ORM\Mapping"
));
$annots = $parser->parse($docblock);
$this->assertCount(1, $annots);
$this->assertInstanceOf("Drupal\Tests\Component\Annotation\Doctrine\Ticket\Doctrine\ORM\Mapping\Entity", $annots[0]);
}
public function testIssueNamespaces()
{
$docblock = "@Entity";
$parser = new DocParser();
$parser->addNamespace("Drupal\Tests\Component\Annotation\Doctrine\Ticket\Doctrine\ORM");
$annots = $parser->parse($docblock);
$this->assertCount(1, $annots);
$this->assertInstanceOf("Drupal\Tests\Component\Annotation\Doctrine\Ticket\Doctrine\ORM\Entity", $annots[0]);
}
public function testIssueMultipleNamespaces()
{
$docblock = "@Entity";
$parser = new DocParser();
$parser->addNamespace("Drupal\Tests\Component\Annotation\Doctrine\Ticket\Doctrine\ORM\Mapping");
$parser->addNamespace("Drupal\Tests\Component\Annotation\Doctrine\Ticket\Doctrine\ORM");
$annots = $parser->parse($docblock);
$this->assertCount(1, $annots);
$this->assertInstanceOf("Drupal\Tests\Component\Annotation\Doctrine\Ticket\Doctrine\ORM\Mapping\Entity", $annots[0]);
}
public function testIssueWithNamespacesOrImports()
{
$docblock = "@Entity";
$parser = new DocParser();
$annots = $parser->parse($docblock);
$this->assertCount(1, $annots);
$this->assertInstanceOf("Entity", $annots[0]);
$this->assertCount(1, $annots);
}
public function testIssueSimpleAnnotationReader()
{
$reader = new SimpleAnnotationReader();
$reader->addNamespace('Drupal\Tests\Component\Annotation\Doctrine\Ticket\Doctrine\ORM\Mapping');
$annots = $reader->getClassAnnotations(new \ReflectionClass(__NAMESPACE__."\MappedClass"));
$this->assertCount(1, $annots);
$this->assertInstanceOf("Drupal\Tests\Component\Annotation\Doctrine\Ticket\Doctrine\ORM\Mapping\Entity", $annots[0]);
}
}
/**
* @Entity
*/
class MappedClass
{
}
namespace Drupal\Tests\Component\Annotation\Doctrine\Ticket\Doctrine\ORM\Mapping;
/**
* @Annotation
*/
class Entity
{
}
namespace Drupal\Tests\Component\Annotation\Doctrine\Ticket\Doctrine\ORM;
/**
* @Annotation
*/
class Entity
{
}

View File

@@ -0,0 +1,16 @@
All files within this directory where copied from the Doctrine project: <http://www.doctrine-project.org>
They were copied from version 1.2.7.
Original copyright:
Copyright (c) 2006-2013 Doctrine Project
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace com\example\PluginNamespace;
/**
* Provides a custom test plugin.
*
* @Plugin(
* id = "discovery_test_1"
* )
* @CustomPlugin(
* id = "discovery_test_1",
* title = "Discovery test plugin"
* )
*/
class DiscoveryTest1 {}

View File

@@ -0,0 +1,2 @@
# This should not be loaded by our annotated class discovery.
id:discovery_test_2

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation;
use Drupal\Component\Annotation\Reflection\MockFileFinder;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Annotation\Reflection\MockFileFinder
* @group Annotation
*/
class MockFileFinderTest extends TestCase {
/**
* @covers ::create
* @covers ::findFile
*/
public function testFindFile(): void {
$tmp = MockFileFinder::create('test_filename.txt');
$this->assertEquals('test_filename.txt', $tmp->findFile('n/a'));
$this->assertEquals('test_filename.txt', $tmp->findFile('SomeClass'));
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation\Plugin\Discovery;
use Drupal\Component\Annotation\Plugin;
use Drupal\Component\Annotation\Plugin\Discovery\AnnotationBridgeDecorator;
use Drupal\Component\Plugin\Definition\PluginDefinition;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
/**
* @coversDefaultClass \Drupal\Component\Annotation\Plugin\Discovery\AnnotationBridgeDecorator
* @group Plugin
*/
class AnnotationBridgeDecoratorTest extends TestCase {
use ProphecyTrait;
/**
* @covers ::getDefinitions
*/
public function testGetDefinitions(): void {
$definitions = [];
$definitions['object'] = new ObjectDefinition(['id' => 'foo']);
$definitions['array'] = ['id' => 'bar'];
$discovery = $this->prophesize(DiscoveryInterface::class);
$discovery->getDefinitions()->willReturn($definitions);
$decorator = new AnnotationBridgeDecorator($discovery->reveal(), TestAnnotation::class);
$expected = [
'object' => new ObjectDefinition(['id' => 'foo']),
'array' => new ObjectDefinition(['id' => 'bar']),
];
$this->assertEquals($expected, $decorator->getDefinitions());
}
}
/**
* {@inheritdoc}
*/
class TestAnnotation extends Plugin {
/**
* {@inheritdoc}
*/
public function get() {
return new ObjectDefinition($this->definition);
}
}
/**
* {@inheritdoc}
*/
class ObjectDefinition extends PluginDefinition {
/**
* ObjectDefinition constructor.
*
* @param array $definition
* An array of definition values.
*/
public function __construct(array $definition) {
foreach ($definition as $property => $value) {
$this->{$property} = $value;
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation;
use Drupal\Component\Annotation\PluginID;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Annotation\PluginId
* @group Annotation
*/
class PluginIdTest extends TestCase {
/**
* @covers ::get
*/
public function testGet(): void {
// Assert plugin starts empty.
$plugin = new PluginID();
$this->assertEquals([
'id' => NULL,
'class' => NULL,
'provider' => NULL,
], $plugin->get());
// Set values and ensure we can retrieve them.
$plugin->value = 'foo';
$plugin->setClass('bar');
$plugin->setProvider('baz');
$this->assertEquals([
'id' => 'foo',
'class' => 'bar',
'provider' => 'baz',
], $plugin->get());
}
/**
* @covers ::getId
*/
public function testGetId(): void {
$plugin = new PluginID();
$plugin->value = 'example';
$this->assertEquals('example', $plugin->getId());
}
}

View File

@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Annotation;
use Drupal\Component\Annotation\Plugin;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Annotation\Plugin
* @group Annotation
*/
class PluginTest extends TestCase {
/**
* @covers ::__construct
* @covers ::parse
* @covers ::get
*/
public function testGet(): void {
// Assert all values are accepted through constructor and default value is
// used for non existent but defined property.
$plugin = new PluginStub([
1 => 'oak',
'foo' => 'bar',
'biz' => [
'baz' => 'boom',
],
'nestedAnnotation' => new Plugin([
'foo' => 'bar',
]),
]);
$this->assertEquals([
// This property wasn't in our definition but is defined as a property on
// our plugin class.
'defaultProperty' => 'test_value',
1 => 'oak',
'foo' => 'bar',
'biz' => [
'baz' => 'boom',
],
'nestedAnnotation' => [
'foo' => 'bar',
],
], $plugin->get());
// Without default properties, we get a completely empty plugin definition.
$plugin = new Plugin([]);
$this->assertEquals([], $plugin->get());
}
/**
* @covers ::getProvider
*/
public function testGetProvider(): void {
$plugin = new Plugin(['provider' => 'example']);
$this->assertEquals('example', $plugin->getProvider());
}
/**
* @covers ::setProvider
*/
public function testSetProvider(): void {
$plugin = new Plugin([]);
$plugin->setProvider('example');
$this->assertEquals('example', $plugin->getProvider());
}
/**
* @covers ::getId
*/
public function testGetId(): void {
$plugin = new Plugin(['id' => 'example']);
$this->assertEquals('example', $plugin->getId());
}
/**
* @covers ::getClass
*/
public function testGetClass(): void {
$plugin = new Plugin(['class' => 'example']);
$this->assertEquals('example', $plugin->getClass());
}
/**
* @covers ::setClass
*/
public function testSetClass(): void {
$plugin = new Plugin([]);
$plugin->setClass('example');
$this->assertEquals('example', $plugin->getClass());
}
}
/**
* {@inheritdoc}
*/
class PluginStub extends Plugin {
protected $defaultProperty = 'test_value';
}

View File

@@ -0,0 +1,270 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Assertion;
use PHPUnit\Framework\TestCase;
use Drupal\Component\Assertion\Inspector;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
/**
* @coversDefaultClass \Drupal\Component\Assertion\Inspector
* @group Assertion
*/
class InspectorTest extends TestCase {
use ExpectDeprecationTrait;
/**
* Tests asserting argument is an array or traversable object.
*
* @covers ::assertTraversable
*
* @group legacy
*/
public function testAssertTraversable(): void {
$this->expectDeprecation('Drupal\Component\Assertion\Inspector::assertTraversable() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use is_iterable() instead. See https://www.drupal.org/node/3422775');
$this->assertTrue(Inspector::assertTraversable([]));
$this->assertTrue(Inspector::assertTraversable(new \ArrayObject()));
$this->assertFalse(Inspector::assertTraversable(new \stdClass()));
$this->assertFalse(Inspector::assertTraversable('foo'));
}
/**
* Tests asserting all members are strings.
*
* @covers ::assertAllStrings
* @dataProvider providerTestAssertAllStrings
*/
public function testAssertAllStrings($input, $expected): void {
$this->assertSame($expected, Inspector::assertAllStrings($input));
}
public static function providerTestAssertAllStrings() {
$data = [
'empty-array' => [[], TRUE],
'array-with-strings' => [['foo', 'bar'], TRUE],
'string' => ['foo', FALSE],
'array-with-strings-with-colon' => [['foo', 'bar', 'llama:2001988', 'baz', 'llama:14031991'], TRUE],
'with-FALSE' => [[FALSE], FALSE],
'with-TRUE' => [[TRUE], FALSE],
'with-string-and-boolean' => [['foo', FALSE], FALSE],
'with-NULL' => [[NULL], FALSE],
'string-with-NULL' => [['foo', NULL], FALSE],
'integer' => [[1337], FALSE],
'string-and-integer' => [['foo', 1337], FALSE],
'double' => [[3.14], FALSE],
'string-and-double' => [['foo', 3.14], FALSE],
'array' => [[[]], FALSE],
'string-and-array' => [['foo', []], FALSE],
'string-and-nested-array' => [['foo', ['bar']], FALSE],
'object' => [[new \stdClass()], FALSE],
'string-and-object' => [['foo', new StringObject()], FALSE],
];
return $data;
}
/**
* Tests asserting all members are strings or objects with __toString().
*
* @covers ::assertAllStringable
*/
public function testAssertAllStringable(): void {
$this->assertTrue(Inspector::assertAllStringable([]));
$this->assertTrue(Inspector::assertAllStringable(['foo', 'bar']));
$this->assertFalse(Inspector::assertAllStringable('foo'));
$this->assertTrue(Inspector::assertAllStringable(['foo', new StringObject()]));
}
/**
* Tests asserting all members are arrays.
*
* @covers ::assertAllArrays
*/
public function testAssertAllArrays(): void {
$this->assertTrue(Inspector::assertAllArrays([]));
$this->assertTrue(Inspector::assertAllArrays([[], []]));
$this->assertFalse(Inspector::assertAllArrays([[], 'foo']));
}
/**
* Tests asserting array is 0-indexed - the strict definition of array.
*
* @covers ::assertStrictArray
*/
public function testAssertStrictArray(): void {
$this->assertTrue(Inspector::assertStrictArray([]));
$this->assertTrue(Inspector::assertStrictArray(['bar', 'foo']));
$this->assertFalse(Inspector::assertStrictArray(['foo' => 'bar', 'bar' => 'foo']));
}
/**
* Tests asserting all members are strict arrays.
*
* @covers ::assertAllStrictArrays
*/
public function testAssertAllStrictArrays(): void {
$this->assertTrue(Inspector::assertAllStrictArrays([]));
$this->assertTrue(Inspector::assertAllStrictArrays([[], []]));
$this->assertFalse(Inspector::assertAllStrictArrays([['foo' => 'bar', 'bar' => 'foo']]));
}
/**
* Tests asserting all members have specified keys.
*
* @covers ::assertAllHaveKey
*/
public function testAssertAllHaveKey(): void {
$this->assertTrue(Inspector::assertAllHaveKey([]));
$this->assertTrue(Inspector::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']]));
$this->assertTrue(Inspector::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']], 'foo'));
$this->assertTrue(Inspector::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']], 'bar', 'foo'));
$this->assertFalse(Inspector::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']], 'bar', 'foo', 'moo'));
}
/**
* Tests asserting all members are integers.
*
* @covers ::assertAllIntegers
*/
public function testAssertAllIntegers(): void {
$this->assertTrue(Inspector::assertAllIntegers([]));
$this->assertTrue(Inspector::assertAllIntegers([1, 2, 3]));
$this->assertFalse(Inspector::assertAllIntegers([1, 2, 3.14]));
$this->assertFalse(Inspector::assertAllIntegers([1, '2', 3]));
}
/**
* Tests asserting all members are floating point variables.
*
* @covers ::assertAllFloat
*/
public function testAssertAllFloat(): void {
$this->assertTrue(Inspector::assertAllFloat([]));
$this->assertTrue(Inspector::assertAllFloat([1.0, 2.1, 3.14]));
$this->assertFalse(Inspector::assertAllFloat([1, 2.1, 3.14]));
$this->assertFalse(Inspector::assertAllFloat([1.0, '2', 3]));
$this->assertFalse(Inspector::assertAllFloat(['Titanic']));
}
/**
* Tests asserting all members are callable.
*
* @covers ::assertAllCallable
*/
public function testAllCallable(): void {
$this->assertTrue(Inspector::assertAllCallable([
'strchr',
[$this, 'callMe'],
[__CLASS__, 'callMeStatic'],
function () {
return TRUE;
},
]));
$this->assertFalse(Inspector::assertAllCallable([
'strchr',
[$this, 'callMe'],
[__CLASS__, 'callMeStatic'],
function () {
return TRUE;
},
"I'm not callable",
]));
}
/**
* Tests asserting all members are !empty().
*
* @covers ::assertAllNotEmpty
*/
public function testAllNotEmpty(): void {
$this->assertTrue(Inspector::assertAllNotEmpty([1, 'two']));
$this->assertFalse(Inspector::assertAllNotEmpty(['']));
}
/**
* Tests asserting all arguments are numbers or strings castable to numbers.
*
* @covers ::assertAllNumeric
*/
public function testAssertAllNumeric(): void {
$this->assertTrue(Inspector::assertAllNumeric([1, '2', 3.14]));
$this->assertFalse(Inspector::assertAllNumeric([1, 'two', 3.14]));
}
/**
* Tests asserting strstr() or stristr() match.
*
* @covers ::assertAllMatch
*/
public function testAssertAllMatch(): void {
$this->assertTrue(Inspector::assertAllMatch('f', ['fee', 'fi', 'fo']));
$this->assertTrue(Inspector::assertAllMatch('F', ['fee', 'fi', 'fo']));
$this->assertTrue(Inspector::assertAllMatch('f', ['fee', 'fi', 'fo'], TRUE));
$this->assertFalse(Inspector::assertAllMatch('F', ['fee', 'fi', 'fo'], TRUE));
$this->assertFalse(Inspector::assertAllMatch('e', ['fee', 'fi', 'fo']));
$this->assertFalse(Inspector::assertAllMatch('1', [12]));
}
/**
* Tests asserting regular expression match.
*
* @covers ::assertAllRegularExpressionMatch
*/
public function testAssertAllRegularExpressionMatch(): void {
$this->assertTrue(Inspector::assertAllRegularExpressionMatch('/f/i', ['fee', 'fi', 'fo']));
$this->assertTrue(Inspector::assertAllRegularExpressionMatch('/F/i', ['fee', 'fi', 'fo']));
$this->assertTrue(Inspector::assertAllRegularExpressionMatch('/f/', ['fee', 'fi', 'fo']));
$this->assertFalse(Inspector::assertAllRegularExpressionMatch('/F/', ['fee', 'fi', 'fo']));
$this->assertFalse(Inspector::assertAllRegularExpressionMatch('/e/', ['fee', 'fi', 'fo']));
$this->assertFalse(Inspector::assertAllRegularExpressionMatch('/1/', [12]));
}
/**
* Tests asserting all members are objects.
*
* @covers ::assertAllObjects
*/
public function testAssertAllObjects(): void {
$this->assertTrue(Inspector::assertAllObjects([new \ArrayObject(), new \ArrayObject()]));
$this->assertFalse(Inspector::assertAllObjects([new \ArrayObject(), new \ArrayObject(), 'foo']));
$this->assertTrue(Inspector::assertAllObjects([new \ArrayObject(), new \ArrayObject()], '\\Traversable'));
$this->assertFalse(Inspector::assertAllObjects([new \ArrayObject(), new \ArrayObject(), 'foo'], '\\Traversable'));
$this->assertFalse(Inspector::assertAllObjects([new \ArrayObject(), new StringObject()], '\\Traversable'));
$this->assertTrue(Inspector::assertAllObjects([new \ArrayObject(), new StringObject()], '\\Traversable', '\\Drupal\\Tests\\Component\\Assertion\\StringObject'));
$this->assertFalse(Inspector::assertAllObjects([new \ArrayObject(), new StringObject(), new \stdClass()], '\\ArrayObject', '\\Drupal\\Tests\\Component\\Assertion\\StringObject'));
}
/**
* Defines a test method referenced by ::testAllCallable().
*/
public function callMe() {
return TRUE;
}
/**
* Defines a test method referenced by ::testAllCallable().
*/
public static function callMeStatic() {
return TRUE;
}
}
/**
* Quick class for testing for objects with __toString.
*/
class StringObject {
/**
* {@inheritdoc}
*/
public function __toString() {
return 'foo';
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\ClassFinder;
use Composer\Autoload\ClassLoader;
use Drupal\Component\ClassFinder\ClassFinder;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\ClassFinder\ClassFinder
* @group ClassFinder
*/
class ClassFinderTest extends TestCase {
/**
* @covers ::findFile
*/
public function testFindFile(): void {
$finder = new ClassFinder();
// The full path is returned therefore only tests with
// assertStringEndsWith() so the test is portable.
$expected_path = str_replace('/', DIRECTORY_SEPARATOR, 'core/tests/Drupal/Tests/Component/ClassFinder/ClassFinderTest.php');
$this->assertStringEndsWith($expected_path, $finder->findFile(ClassFinderTest::class));
$class = 'Not\\A\\Class';
$this->assertNull($finder->findFile($class));
// Register an autoloader that can find this class.
$loader = new ClassLoader();
$loader->addClassMap([$class => __FILE__]);
$loader->register();
$this->assertEquals(__FILE__, $finder->findFile($class));
// This shouldn't prevent us from finding the original file.
$this->assertStringEndsWith($expected_path, $finder->findFile(ClassFinderTest::class));
// Clean up the additional autoloader after the test.
$loader->unregister();
}
}

View File

@@ -0,0 +1,942 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Datetime;
use Drupal\Component\Datetime\DateTimePlus;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Datetime\DateTimePlus
* @group Datetime
*/
class DateTimePlusTest extends TestCase {
/**
* Tests creating dates from string and array input.
*
* @param mixed $input
* Input argument for DateTimePlus.
* @param string $timezone
* Timezone argument for DateTimePlus.
* @param string $expected
* Expected output from DateTimePlus::format().
*
* @dataProvider providerTestDates
*/
public function testDates($input, $timezone, $expected): void {
$date = new DateTimePlus($input, $timezone);
$value = $date->format('c');
if (is_array($input)) {
$input = var_export($input, TRUE);
}
$this->assertEquals($expected, $value, sprintf("Test new DateTimePlus(%s, %s): should be %s, found %s.", $input, $timezone, $expected, $value));
}
/**
* Tests creating dates from string and array input.
*
* @param mixed $input
* Input argument for DateTimePlus.
* @param string $timezone
* Timezone argument for DateTimePlus.
* @param string $expected
* Expected output from DateTimePlus::format().
*
* @dataProvider providerTestDateArrays
*/
public function testDateArrays($input, $timezone, $expected): void {
$date = DateTimePlus::createFromArray($input, $timezone);
$value = $date->format('c');
if (is_array($input)) {
$input = var_export($input, TRUE);
}
$this->assertEquals($expected, $value, sprintf("Test new DateTimePlus(%s, %s): should be %s, found %s.", $input, $timezone, $expected, $value));
}
/**
* Tests date diffs.
*
* @param mixed $input1
* A DateTimePlus object.
* @param mixed $input2
* Date argument for DateTimePlus::diff method.
* @param bool $absolute
* Absolute flag for DateTimePlus::diff method.
* @param \DateInterval $expected
* The expected result of the DateTimePlus::diff operation.
*
* @dataProvider providerTestDateDiff
*/
public function testDateDiff($input1, $input2, $absolute, \DateInterval $expected): void {
$interval = $input1->diff($input2, $absolute);
$this->assertEquals($interval, $expected);
}
/**
* Tests date diff exception caused by invalid input.
*
* @param mixed $input1
* A DateTimePlus object.
* @param mixed $input2
* Date argument for DateTimePlus::diff method.
* @param bool $absolute
* Absolute flag for DateTimePlus::diff method.
*
* @dataProvider providerTestInvalidDateDiff
*/
public function testInvalidDateDiff($input1, $input2, $absolute): void {
$this->expectException(\BadMethodCallException::class);
$this->expectExceptionMessage('Method Drupal\Component\Datetime\DateTimePlus::diff expects parameter 1 to be a \DateTime or \Drupal\Component\Datetime\DateTimePlus object');
$interval = $input1->diff($input2, $absolute);
}
/**
* Tests creating dates from invalid array input.
*
* @param mixed $input
* Input argument for DateTimePlus.
* @param string $timezone
* Timezone argument for DateTimePlus.
* @param string $class
* The Exception subclass to expect to be thrown.
*
* @dataProvider providerTestInvalidDateArrays
*/
public function testInvalidDateArrays($input, $timezone, $class): void {
$this->expectException($class);
$this->assertInstanceOf(
'\Drupal\Component\DateTimePlus',
DateTimePlus::createFromArray($input, $timezone)
);
}
/**
* Tests DateTimePlus::checkArray().
*
* @param array $array
* Input argument for DateTimePlus::checkArray().
* @param bool $expected
* The expected result of DateTimePlus::checkArray().
*
* @dataProvider providerTestCheckArray
*/
public function testCheckArray(array $array, $expected): void {
$this->assertSame(
$expected,
DateTimePlus::checkArray($array)
);
}
/**
* Tests creating dates from timestamps, and manipulating timezones.
*
* @param int $input
* Input argument for DateTimePlus::createFromTimestamp().
* @param array $initial
* An array containing:
* - 'timezone_initial' - Timezone argument for DateTimePlus.
* - 'format_initial' - Format argument for DateTimePlus.
* - 'expected_initial_date' - Expected output from DateTimePlus::format().
* - 'expected_initial_timezone' - Expected output from
* DateTimePlus::getTimeZone()::getName().
* - 'expected_initial_offset' - Expected output from DateTimePlus::getOffset().
* @param array $transform
* An array containing:
* - 'timezone_transform' - Argument to transform date to another timezone via
* DateTimePlus::setTimezone().
* - 'format_transform' - Format argument to use when transforming date to
* another timezone.
* - 'expected_transform_date' - Expected output from DateTimePlus::format(),
* after timezone transform.
* - 'expected_transform_timezone' - Expected output from
* DateTimePlus::getTimeZone()::getName(), after timezone transform.
* - 'expected_transform_offset' - Expected output from
* DateTimePlus::getOffset(), after timezone transform.
*
* @dataProvider providerTestTimestamp
*/
public function testTimestamp($input, array $initial, array $transform): void {
// Initialize a new date object.
$date = DateTimePlus::createFromTimestamp($input, $initial['timezone']);
$this->assertDateTimestamp($date, $input, $initial, $transform);
}
/**
* Tests creating dates from datetime strings.
*
* @param string $input
* Input argument for DateTimePlus().
* @param array $initial
* @see testTimestamp()
* @param array $transform
* @see testTimestamp()
*
* @dataProvider providerTestDateTimestamp
*/
public function testDateTimestamp($input, array $initial, array $transform): void {
// Initialize a new date object.
$date = new DateTimePlus($input, $initial['timezone']);
$this->assertDateTimestamp($date, $input, $initial, $transform);
}
/**
* Asserts a DateTimePlus value.
*
* @param \Drupal\Component\Datetime\DateTimePlus $date
* DateTimePlus to test.
* @param string|int $input
* The original input passed to the test method.
* @param array $initial
* @see testTimestamp()
* @param array $transform
* @see testTimestamp()
*
* @internal
*/
public function assertDateTimestamp(DateTimePlus $date, string|int $input, array $initial, array $transform): void {
// Check format.
$value = $date->format($initial['format']);
$this->assertEquals($initial['expected_date'], $value, sprintf("Test new DateTimePlus(%s, %s): should be %s, found %s.", $input, $initial['timezone'], $initial['expected_date'], $value));
// Check timezone name.
$value = $date->getTimeZone()->getName();
$this->assertEquals($initial['expected_timezone'], $value, sprintf("The current timezone is %s: should be %s.", $value, $initial['expected_timezone']));
// Check offset.
$value = $date->getOffset();
$this->assertEquals($initial['expected_offset'], $value, sprintf("The current offset is %s: should be %s.", $value, $initial['expected_offset']));
// Transform the date to another timezone.
$date->setTimezone(new \DateTimeZone($transform['timezone']));
// Check transformed format.
$value = $date->format($transform['format']);
$this->assertEquals($transform['expected_date'], $value, sprintf("Test \$date->setTimezone(new \\DateTimeZone(%s)): should be %s, found %s.", $transform['timezone'], $transform['expected_date'], $value));
// Check transformed timezone.
$value = $date->getTimeZone()->getName();
$this->assertEquals($transform['expected_timezone'], $value, sprintf("The current timezone should be %s, found %s.", $transform['expected_timezone'], $value));
// Check transformed offset.
$value = $date->getOffset();
$this->assertEquals($transform['expected_offset'], $value, sprintf("The current offset should be %s, found %s.", $transform['expected_offset'], $value));
}
/**
* Tests creating dates from format strings.
*
* @param string $input
* Input argument for DateTimePlus.
* @param string $timezone
* Timezone argument for DateTimePlus.
* @param string $format
* PHP date() type format for parsing the input.
* @param string $format_date
* Format argument for DateTimePlus::format().
* @param string $expected
* Expected output from DateTimePlus::format().
*
* @dataProvider providerTestDateFormat
*/
public function testDateFormat($input, $timezone, $format, $format_date, $expected): void {
$date = DateTimePlus::createFromFormat($format, $input, $timezone);
$value = $date->format($format_date);
$this->assertEquals($expected, $value, sprintf("Test new DateTimePlus(%s, %s, %s): should be %s, found %s.", $input, $timezone, $format, $expected, $value));
}
/**
* Tests invalid date handling.
*
* @param mixed $input
* Input argument for DateTimePlus.
* @param string $timezone
* Timezone argument for DateTimePlus.
* @param string $format
* Format argument for DateTimePlus.
* @param string $message
* Message to print if no errors are thrown by the invalid dates.
* @param string $class
* The Exception subclass to expect to be thrown.
*
* @dataProvider providerTestInvalidDates
*/
public function testInvalidDates($input, $timezone, $format, $message, $class): void {
$this->expectException($class);
DateTimePlus::createFromFormat($format, $input, $timezone);
}
/**
* Tests that DrupalDateTime can detect the right timezone to use.
*
* When specified or not.
*
* @param mixed $input
* Input argument for DateTimePlus.
* @param mixed $timezone
* Timezone argument for DateTimePlus.
* @param string $expected_timezone
* Expected timezone returned from DateTimePlus::getTimezone::getName().
* @param string $message
* Message to print on test failure.
*
* @dataProvider providerTestDateTimezone
*/
public function testDateTimezone($input, $timezone, $expected_timezone, $message): void {
$date = new DateTimePlus($input, $timezone);
$timezone = $date->getTimezone()->getName();
$this->assertEquals($timezone, $expected_timezone, $message);
}
/**
* Tests that DrupalDateTime can detect the correct timezone to use.
*
* But only when the DrupalDateTime is constructed from a datetime object.
*/
public function testDateTimezoneWithDateTimeObject(): void {
// Create a date object with another date object.
$input = new \DateTime('now', new \DateTimeZone('Pacific/Midway'));
$timezone = NULL;
$expected_timezone = 'Pacific/Midway';
$message = 'DateTimePlus uses the specified timezone if provided.';
$date = DateTimePlus::createFromDateTime($input, $timezone);
$timezone = $date->getTimezone()->getName();
$this->assertEquals($timezone, $expected_timezone, $message);
}
/**
* Provides data for date tests.
*
* @return array
* An array of arrays, each containing the input parameters for
* DateTimePlusTest::testDates().
*
* @see DateTimePlusTest::testDates()
*/
public static function providerTestDates() {
$dates = [
// String input.
// Create date object from datetime string.
['2009-03-07 10:30', 'America/Chicago', '2009-03-07T10:30:00-06:00'],
// Same during daylight savings time.
['2009-06-07 10:30', 'America/Chicago', '2009-06-07T10:30:00-05:00'],
// Create date object from date string.
['2009-03-07', 'America/Chicago', '2009-03-07T00:00:00-06:00'],
// Same during daylight savings time.
['2009-06-07', 'America/Chicago', '2009-06-07T00:00:00-05:00'],
// Create date object from date string.
['2009-03-07 10:30', 'Australia/Canberra', '2009-03-07T10:30:00+11:00'],
// Same during daylight savings time.
['2009-06-07 10:30', 'Australia/Canberra', '2009-06-07T10:30:00+10:00'],
];
// On 32-bit systems, timestamps are limited to 1901-2038.
if (PHP_INT_SIZE > 4) {
// Create a date object in the distant past.
// @see https://www.drupal.org/node/2795489#comment-12127088
// Note that this date is after the United States standardized its
// timezones.
$dates[] = ['1883-11-19 10:30', 'America/Chicago', '1883-11-19T10:30:00-06:00'];
// Create a date object in the far future.
$dates[] = ['2345-01-02 02:04', 'UTC', '2345-01-02T02:04:00+00:00'];
}
return $dates;
}
/**
* Provides data for date tests.
*
* @return array
* An array of arrays, each containing the input parameters for
* DateTimePlusTest::testDates().
*
* @see DateTimePlusTest::testDates()
*/
public static function providerTestDateArrays() {
$dates = [
// Array input.
// Create date object from date array, date only.
[['year' => 2010, 'month' => 2, 'day' => 28], 'America/Chicago', '2010-02-28T00:00:00-06:00'],
// Create date object from date array with hour.
[['year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 10], 'America/Chicago', '2010-02-28T10:00:00-06:00'],
// Create date object from date array, date only.
[['year' => 2010, 'month' => 2, 'day' => 28], 'Europe/Berlin', '2010-02-28T00:00:00+01:00'],
// Create date object from date array with hour.
[['year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 10], 'Europe/Berlin', '2010-02-28T10:00:00+01:00'],
];
// On 32-bit systems, timestamps are limited to 1901-2038.
if (PHP_INT_SIZE > 4) {
// Create a date object in the distant past.
// @see https://www.drupal.org/node/2795489#comment-12127088
// Note that this date is after the United States standardized its
// timezones.
$dates[] = [['year' => 1883, 'month' => 11, 'day' => 19], 'America/Chicago', '1883-11-19T00:00:00-06:00'];
// Create a date object in the far future.
$dates[] = [['year' => 2345, 'month' => 1, 'day' => 2], 'UTC', '2345-01-02T00:00:00+00:00'];
}
return $dates;
}
/**
* Provides data for testDateFormats.
*
* @return array
* An array of arrays, each containing:
* - 'input' - Input to DateTimePlus.
* - 'timezone' - Timezone for DateTimePlus.
* - 'format' - Date format for DateTimePlus.
* - 'format_date' - Date format for use in $date->format() method.
* - 'expected' - The expected return from DateTimePlus.
*
* @see testDateFormats()
*/
public static function providerTestDateFormat() {
return [
// Create a year-only date.
['2009', NULL, 'Y', 'Y', '2009'],
// Create a month and year-only date.
['2009-10', NULL, 'Y-m', 'Y-m', '2009-10'],
// Create a time-only date.
['T10:30:00', NULL, '\TH:i:s', 'H:i:s', '10:30:00'],
// Create a time-only date.
['10:30:00', NULL, 'H:i:s', 'H:i:s', '10:30:00'],
];
}
/**
* Provides data for testInvalidDates.
*
* @return array
* An array of arrays, each containing:
* - 'input' - Input for DateTimePlus.
* - 'timezone' - Timezone for DateTimePlus.
* - 'format' - Format for DateTimePlus.
* - 'message' - Message to display on failure.
*
* @see testInvalidDates
*/
public static function providerTestInvalidDates() {
return [
// Test for invalid month names when we are using a short version
// of the month.
['23 abc 2012', NULL, 'd M Y', "23 abc 2012 contains an invalid month name and did not produce errors.", \InvalidArgumentException::class],
// Test for invalid hour.
['0000-00-00T45:30:00', NULL, 'Y-m-d\TH:i:s', "0000-00-00T45:30:00 contains an invalid hour and did not produce errors.", \UnexpectedValueException::class],
// Test for invalid day.
['0000-00-99T05:30:00', NULL, 'Y-m-d\TH:i:s', "0000-00-99T05:30:00 contains an invalid day and did not produce errors.", \UnexpectedValueException::class],
// Test for invalid month.
['0000-75-00T15:30:00', NULL, 'Y-m-d\TH:i:s', "0000-75-00T15:30:00 contains an invalid month and did not produce errors.", \UnexpectedValueException::class],
// Test for invalid year.
['11-08-01T15:30:00', NULL, 'Y-m-d\TH:i:s', "11-08-01T15:30:00 contains an invalid year and did not produce errors.", \UnexpectedValueException::class],
];
}
/**
* Data provider for testInvalidDateArrays.
*
* @return array
* An array of arrays, each containing:
* - 'input' - Input for DateTimePlus.
* - 'timezone' - Timezone for DateTimePlus.
*
* @see testInvalidDateArrays
*/
public static function providerTestInvalidDateArrays() {
return [
// One year larger than the documented upper limit of checkdate().
[['year' => 32768, 'month' => 1, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
// One year smaller than the documented lower limit of checkdate().
[['year' => 0, 'month' => 1, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
// Test for invalid month from date array.
[['year' => 2010, 'month' => 27, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
// Test for invalid hour from date array.
[['year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 80, 'minute' => 0, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
// Test for invalid minute from date array.
[['year' => 2010, 'month' => 7, 'day' => 8, 'hour' => 8, 'minute' => 88, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
// Regression test for https://www.drupal.org/node/2084455.
[['hour' => 59, 'minute' => 1, 'second' => 1], 'America/Chicago', \InvalidArgumentException::class],
];
}
/**
* Data provider for testCheckArray.
*
* @return array
* An array of arrays, each containing:
* - 'array' - Input for DateTimePlus::checkArray().
* - 'expected' - Expected output for DateTimePlus::checkArray().
*
* @see testCheckArray
*/
public static function providerTestCheckArray() {
return [
'Date array, date only' => [['year' => 2010, 'month' => 2, 'day' => 28], TRUE],
'Date array with hour' => [['year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 10], TRUE],
'One year larger than the documented upper limit of checkdate()' => [['year' => 32768, 'month' => 1, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0], FALSE],
'One year smaller than the documented lower limit of checkdate()' => [['year' => 0, 'month' => 1, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0], FALSE],
'Invalid month from date array' => [['year' => 2010, 'month' => 27, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0], FALSE],
'Invalid hour from date array' => [['year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 80, 'minute' => 0, 'second' => 0], FALSE],
'Invalid minute from date array.' => [['year' => 2010, 'month' => 7, 'day' => 8, 'hour' => 8, 'minute' => 88, 'second' => 0], FALSE],
'Missing day' => [['year' => 2059, 'month' => 1, 'second' => 1], FALSE],
'Zero day' => [['year' => 2059, 'month' => 1, 'day' => 0], FALSE],
];
}
/**
* Provides data for testDateTimezone.
*
* @return array
* An array of arrays, each containing:
* - 'date' - Date string or object for DateTimePlus.
* - 'timezone' - Timezone string for DateTimePlus.
* - 'expected' - Expected return from DateTimePlus::getTimezone()::getName().
* - 'message' - Message to display on test failure.
*
* @see testDateTimezone
*/
public static function providerTestDateTimezone() {
// Use a common date for most of the tests.
$date_string = '2007-01-31 21:00:00';
// Detect the system timezone.
$system_timezone = date_default_timezone_get();
return [
// Create a date object with an unspecified timezone, which should
// end up using the system timezone.
[$date_string, NULL, $system_timezone, 'DateTimePlus uses the system timezone when there is no site timezone.'],
// Create a date object with a specified timezone name.
[$date_string, 'America/Yellowknife', 'America/Yellowknife', 'DateTimePlus uses the specified timezone if provided.'],
// Create a date object with a timezone object.
[$date_string, new \DateTimeZone('Australia/Canberra'), 'Australia/Canberra', 'DateTimePlus uses the specified timezone if provided.'],
// Create a date object with another date object.
[new DateTimePlus('now', 'Pacific/Midway'), NULL, 'Pacific/Midway', 'DateTimePlus uses the specified timezone if provided.'],
];
}
/**
* Provides data for testTimestamp.
*
* @return array
* An array of arrays, each containing the arguments required for
* self::testTimestamp().
*
* @see testTimestamp()
*/
public static function providerTestTimestamp() {
return [
// Create date object from a unix timestamp and display it in
// local time.
[
'input' => 0,
'initial' => [
'timezone' => 'UTC',
'format' => 'c',
'expected_date' => '1970-01-01T00:00:00+00:00',
'expected_timezone' => 'UTC',
'expected_offset' => 0,
],
'transform' => [
'timezone' => 'America/Los_Angeles',
'format' => 'c',
'expected_date' => '1969-12-31T16:00:00-08:00',
'expected_timezone' => 'America/Los_Angeles',
'expected_offset' => '-28800',
],
],
// Create a date using the timestamp of zero, then display its
// value both in UTC and the local timezone.
[
'input' => 0,
'initial' => [
'timezone' => 'America/Los_Angeles',
'format' => 'c',
'expected_date' => '1969-12-31T16:00:00-08:00',
'expected_timezone' => 'America/Los_Angeles',
'expected_offset' => '-28800',
],
'transform' => [
'timezone' => 'UTC',
'format' => 'c',
'expected_date' => '1970-01-01T00:00:00+00:00',
'expected_timezone' => 'UTC',
'expected_offset' => 0,
],
],
];
}
/**
* Provides data for testDateTimestamp.
*
* @return array
* An array of arrays, each containing the arguments required for
* self::testDateTimestamp().
*
* @see testDateTimestamp()
*/
public static function providerTestDateTimestamp() {
return [
// Create date object from datetime string in UTC, and convert
// it to a local date.
[
'input' => '1970-01-01 00:00:00',
'initial' => [
'timezone' => 'UTC',
'format' => 'c',
'expected_date' => '1970-01-01T00:00:00+00:00',
'expected_timezone' => 'UTC',
'expected_offset' => 0,
],
'transform' => [
'timezone' => 'America/Los_Angeles',
'format' => 'c',
'expected_date' => '1969-12-31T16:00:00-08:00',
'expected_timezone' => 'America/Los_Angeles',
'expected_offset' => '-28800',
],
],
// Convert the local time to UTC using string input.
[
'input' => '1969-12-31 16:00:00',
'initial' => [
'timezone' => 'America/Los_Angeles',
'format' => 'c',
'expected_date' => '1969-12-31T16:00:00-08:00',
'expected_timezone' => 'America/Los_Angeles',
'expected_offset' => '-28800',
],
'transform' => [
'timezone' => 'UTC',
'format' => 'c',
'expected_date' => '1970-01-01T00:00:00+00:00',
'expected_timezone' => 'UTC',
'expected_offset' => 0,
],
],
// Convert the local time to UTC using string input.
[
'input' => '1969-12-31 16:00:00',
'initial' => [
'timezone' => 'Europe/Warsaw',
'format' => 'c',
'expected_date' => '1969-12-31T16:00:00+01:00',
'expected_timezone' => 'Europe/Warsaw',
'expected_offset' => '+3600',
],
'transform' => [
'timezone' => 'UTC',
'format' => 'c',
'expected_date' => '1969-12-31T15:00:00+00:00',
'expected_timezone' => 'UTC',
'expected_offset' => 0,
],
],
];
}
/**
* Provides data for date tests.
*
* @return array
* An array of arrays, each containing the input parameters for
* DateTimePlusTest::testDateDiff().
*
* @see DateTimePlusTest::testDateDiff()
*/
public static function providerTestDateDiff() {
$empty_interval = new \DateInterval('PT0S');
$positive_19_hours = new \DateInterval('PT19H');
$positive_18_hours = new \DateInterval('PT18H');
$positive_1_hour = new \DateInterval('PT1H');
$negative_1_hour = new \DateInterval('PT1H');
$negative_1_hour->invert = 1;
return [
// There should be a 19 hour time interval between
// new years in Sydney and new years in LA in year 2000.
[
'input2' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00', new \DateTimeZone('Australia/Sydney')),
'input1' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00', new \DateTimeZone('America/Los_Angeles')),
'absolute' => FALSE,
'expected' => $positive_19_hours,
],
// In 1970 Sydney did not observe daylight savings time
// So there is only an 18 hour time interval.
[
'input2' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '1970-01-01 00:00:00', new \DateTimeZone('Australia/Sydney')),
'input1' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '1970-01-01 00:00:00', new \DateTimeZone('America/Los_Angeles')),
'absolute' => FALSE,
'expected' => $positive_18_hours,
],
[
'input1' => DateTimePlus::createFromFormat('U', 3600, new \DateTimeZone('America/Los_Angeles')),
'input2' => DateTimePlus::createFromFormat('U', 0, new \DateTimeZone('UTC')),
'absolute' => FALSE,
'expected' => $negative_1_hour,
],
[
'input1' => DateTimePlus::createFromFormat('U', 3600),
'input2' => DateTimePlus::createFromFormat('U', 0),
'absolute' => FALSE,
'expected' => $negative_1_hour,
],
[
'input1' => DateTimePlus::createFromFormat('U', 3600),
'input2' => \DateTime::createFromFormat('U', '0'),
'absolute' => FALSE,
'expected' => $negative_1_hour,
],
[
'input1' => DateTimePlus::createFromFormat('U', 3600),
'input2' => DateTimePlus::createFromFormat('U', 0),
'absolute' => TRUE,
'expected' => $positive_1_hour,
],
[
'input1' => DateTimePlus::createFromFormat('U', 3600),
'input2' => \DateTime::createFromFormat('U', '0'),
'absolute' => TRUE,
'expected' => $positive_1_hour,
],
[
'input1' => DateTimePlus::createFromFormat('U', 0),
'input2' => DateTimePlus::createFromFormat('U', 0),
'absolute' => FALSE,
'expected' => $empty_interval,
],
];
}
/**
* Provides data for date tests.
*
* @return array
* An array of arrays, each containing the input parameters for
* DateTimePlusTest::testInvalidDateDiff().
*
* @see DateTimePlusTest::testInvalidDateDiff()
*/
public static function providerTestInvalidDateDiff() {
return [
[
'input1' => DateTimePlus::createFromFormat('U', 3600),
'input2' => '1970-01-01 00:00:00',
'absolute' => FALSE,
],
[
'input1' => DateTimePlus::createFromFormat('U', 3600),
'input2' => NULL,
'absolute' => FALSE,
],
];
}
/**
* Tests invalid values passed to constructor.
*
* @param string $time
* A date/time string.
* @param string[] $errors
* An array of error messages.
*
* @covers ::__construct
*
* @dataProvider providerTestInvalidConstructor
*/
public function testInvalidConstructor($time, array $errors): void {
$date = new DateTimePlus($time);
$this->assertEquals(TRUE, $date->hasErrors());
$this->assertEquals($errors, $date->getErrors());
}
/**
* Provider for testInvalidConstructor().
*
* @return array
* An array of invalid date/time strings, and corresponding error messages.
*/
public static function providerTestInvalidConstructor() {
return [
[
'YYYY-MM-DD',
[
'The timezone could not be found in the database',
'Unexpected character',
'Double timezone specification',
],
],
[
'2017-MM-DD',
[
'Unexpected character',
'The timezone could not be found in the database',
],
],
[
'YYYY-03-DD',
[
'The timezone could not be found in the database',
'Unexpected character',
'Double timezone specification',
],
],
[
'YYYY-MM-07',
[
'The timezone could not be found in the database',
'Unexpected character',
'Double timezone specification',
],
],
[
'2017-13-55',
[
'Unexpected character',
],
],
[
'YYYY-MM-DD hh:mm:ss',
[
'The timezone could not be found in the database',
'Unexpected character',
'Double timezone specification',
],
],
[
'2017-03-07 25:70:80',
[
'Unexpected character',
'Double time specification',
],
],
[
'invalid time string',
[
'The timezone could not be found in the database',
'Double timezone specification',
],
],
];
}
/**
* Tests the $settings['validate_format'] parameter in ::createFromFormat().
*/
public function testValidateFormat(): void {
// Check that an input that does not strictly follow the input format will
// produce the desired date. In this case the year string '11' doesn't
// precisely match the 'Y' formatter parameter, but PHP will parse it
// regardless. However, when formatted with the same string, the year will
// be output with four digits. With the ['validate_format' => FALSE]
// $settings, this will not thrown an exception.
$date = DateTimePlus::createFromFormat('Y-m-d H:i:s', '11-03-31 17:44:00', 'UTC', ['validate_format' => FALSE]);
$this->assertEquals('0011-03-31 17:44:00', $date->format('Y-m-d H:i:s'));
// Parse the same date with ['validate_format' => TRUE] and make sure we
// get the expected exception.
$this->expectException(\UnexpectedValueException::class);
$date = DateTimePlus::createFromFormat('Y-m-d H:i:s', '11-03-31 17:44:00', 'UTC', ['validate_format' => TRUE]);
}
/**
* Tests setting the default time for date-only objects.
*/
public function testDefaultDateTime(): void {
$utc = new \DateTimeZone('UTC');
$date = DateTimePlus::createFromFormat('Y-m-d H:i:s', '2017-05-23 22:58:00', $utc);
$this->assertEquals('22:58:00', $date->format('H:i:s'));
$date->setDefaultDateTime();
$this->assertEquals('12:00:00', $date->format('H:i:s'));
}
/**
* Tests that object methods are chainable.
*
* @covers ::__call
*/
public function testChainable(): void {
$date = new DateTimePlus('now', 'Australia/Sydney');
$date->setTimestamp(12345678);
$rendered = $date->render();
$this->assertEquals('1970-05-24 07:21:18 Australia/Sydney', $rendered);
$date->setTimestamp(23456789);
$rendered = $date->setTimezone(new \DateTimeZone('America/New_York'))->render();
$this->assertEquals('1970-09-29 07:46:29 America/New_York', $rendered);
$date = DateTimePlus::createFromFormat('Y-m-d H:i:s', '1970-05-24 07:21:18', new \DateTimeZone('Australia/Sydney'))
->setTimezone(new \DateTimeZone('America/New_York'));
$rendered = $date->render();
$this->assertInstanceOf(DateTimePlus::class, $date);
$this->assertEquals(12345678, $date->getTimestamp());
$this->assertEquals('1970-05-23 17:21:18 America/New_York', $rendered);
}
/**
* Tests that non-chainable methods work.
*
* @covers ::__call
*/
public function testChainableNonChainable(): void {
$datetime1 = new DateTimePlus('2009-10-11 12:00:00');
$datetime2 = new DateTimePlus('2009-10-13 12:00:00');
$interval = $datetime1->diff($datetime2);
$this->assertInstanceOf(\DateInterval::class, $interval);
$this->assertEquals('+2 days', $interval->format('%R%a days'));
}
/**
* Tests that chained calls to non-existent functions throw an exception.
*
* @covers ::__call
*/
public function testChainableNonCallable(): void {
$this->expectException(\BadMethodCallException::class);
$this->expectExceptionMessage('Call to undefined method Drupal\Component\Datetime\DateTimePlus::nonexistent()');
$date = new DateTimePlus('now', 'Australia/Sydney');
$date->setTimezone(new \DateTimeZone('America/New_York'))->nonexistent();
}
/**
* @covers ::getPhpDateTime
*/
public function testGetPhpDateTime(): void {
$new_york = new \DateTimeZone('America/New_York');
$berlin = new \DateTimeZone('Europe/Berlin');
// Test retrieving a cloned copy of the wrapped \DateTime object, and that
// altering it does not change the DateTimePlus object.
$datetimeplus = DateTimePlus::createFromFormat('Y-m-d H:i:s', '2017-07-13 22:40:00', $new_york, ['langcode' => 'en']);
$this->assertEquals(1500000000, $datetimeplus->getTimestamp());
$this->assertEquals('America/New_York', $datetimeplus->getTimezone()->getName());
$datetime = $datetimeplus->getPhpDateTime();
$this->assertInstanceOf('DateTime', $datetime);
$this->assertEquals(1500000000, $datetime->getTimestamp());
$this->assertEquals('America/New_York', $datetime->getTimezone()->getName());
$datetime->setTimestamp(1400000000)->setTimezone($berlin);
$this->assertEquals(1400000000, $datetime->getTimestamp());
$this->assertEquals('Europe/Berlin', $datetime->getTimezone()->getName());
$this->assertEquals(1500000000, $datetimeplus->getTimestamp());
$this->assertEquals('America/New_York', $datetimeplus->getTimezone()->getName());
}
}

View File

@@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Datetime;
use Drupal\Component\Datetime\Time;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
/**
* @coversDefaultClass \Drupal\Component\Datetime\Time
* @group Datetime
*
* Isolate the tests to prevent side effects from altering system time.
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
class TimeTest extends TestCase {
/**
* The mocked request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack|\PHPUnit\Framework\MockObject\MockObject
*/
protected $requestStack;
/**
* The mocked time class.
*
* @var \Drupal\Component\Datetime\Time
*/
protected $time;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->requestStack = $this->getMockBuilder('Symfony\Component\HttpFoundation\RequestStack')->getMock();
$this->time = new Time($this->requestStack);
}
/**
* Tests the getRequestTime method.
*
* @covers ::getRequestTime
*/
public function testGetRequestTime(): void {
$expected = 12345678;
$request = Request::createFromGlobals();
$request->server->set('REQUEST_TIME', $expected);
// Mocks a the request stack getting the current request.
$this->requestStack->expects($this->any())
->method('getCurrentRequest')
->willReturn($request);
$this->assertEquals($expected, $this->time->getRequestTime());
}
/**
* Tests the getRequestMicroTime method.
*
* @covers ::getRequestMicroTime
*/
public function testGetRequestMicroTime(): void {
$expected = 1234567.89;
$request = Request::createFromGlobals();
$request->server->set('REQUEST_TIME_FLOAT', $expected);
// Mocks a the request stack getting the current request.
$this->requestStack->expects($this->any())
->method('getCurrentRequest')
->willReturn($request);
$this->assertEquals($expected, $this->time->getRequestMicroTime());
}
/**
* @covers ::getRequestTime
*/
public function testGetRequestTimeNoRequest(): void {
// With no request, and no global variable, we expect to get the int part
// of the microtime.
$expected = 1234567;
unset($_SERVER['REQUEST_TIME']);
$this->assertEquals($expected, $this->time->getRequestTime());
$_SERVER['REQUEST_TIME'] = 23456789;
$this->assertEquals(23456789, $this->time->getRequestTime());
}
/**
* @covers ::getRequestMicroTime
*/
public function testGetRequestMicroTimeNoRequest(): void {
$expected = 1234567.89;
unset($_SERVER['REQUEST_TIME_FLOAT']);
$this->assertEquals($expected, $this->time->getRequestMicroTime());
$_SERVER['REQUEST_TIME_FLOAT'] = 2345678.90;
$this->assertEquals(2345678.90, $this->time->getRequestMicroTime());
}
/**
* Tests the getCurrentTime method.
*
* @covers ::getCurrentTime
*/
public function testGetCurrentTime(): void {
$expected = 12345678;
$this->assertEquals($expected, $this->time->getCurrentTime());
}
/**
* Tests the getCurrentMicroTime method.
*
* @covers ::getCurrentMicroTime
*/
public function testGetCurrentMicroTime(): void {
$expected = 1234567.89;
$this->assertEquals($expected, $this->time->getCurrentMicroTime());
}
}
namespace Drupal\Component\Datetime;
/**
* Shadow time() system call.
*
* @return int
*/
function time() {
return 12345678;
}
/**
* Shadow microtime system call.
*
* @return float
*/
function microtime(bool $as_float = FALSE) {
return 1234567.89;
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Datetime;
use Drupal\Component\Datetime\Time;
use PHPUnit\Framework\TestCase;
/**
* Tests that getRequest(Micro)Time works when no underlying request exists.
*
* @coversDefaultClass \Drupal\Component\Datetime\Time
* @group Datetime
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
class TimeWithNoRequestTest extends TestCase {
/**
* The time class for testing.
*/
protected Time $time;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// We need to explicitly unset the $_SERVER variables, so that Time is
// forced to look for current time.
unset($_SERVER['REQUEST_TIME']);
unset($_SERVER['REQUEST_TIME_FLOAT']);
$this->time = new Time();
}
/**
* Tests the getRequestTime method.
*
* @covers ::getRequestTime
*/
public function testGetRequestTimeImmutable(): void {
$requestTime = $this->time->getRequestTime();
sleep(2);
$this->assertSame($requestTime, $this->time->getRequestTime());
}
/**
* Tests the getRequestMicroTime method.
*
* @covers ::getRequestMicroTime
*/
public function testGetRequestMicroTimeImmutable(): void {
$requestTime = $this->time->getRequestMicroTime();
usleep(20000);
$this->assertSame($requestTime, $this->time->getRequestMicroTime());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,789 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\DependencyInjection\Dumper {
use Drupal\Component\Utility\Crypt;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophet;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
/**
* @coversDefaultClass \Drupal\Component\DependencyInjection\Dumper\OptimizedPhpArrayDumper
* @group DependencyInjection
*/
class OptimizedPhpArrayDumperTest extends TestCase {
use ExpectDeprecationTrait;
use ProphecyTrait;
/**
* The container builder instance.
*
* @var \Symfony\Component\DependencyInjection\ContainerBuilder
*/
protected $containerBuilder;
/**
* The definition for the container to build in tests.
*
* @var array
*/
protected $containerDefinition;
/**
* Whether the dumper uses the machine-optimized format or not.
*
* @var bool
*/
protected $machineFormat = TRUE;
/**
* Stores the dumper class to use.
*
* @var string
*/
protected $dumperClass = '\Drupal\Component\DependencyInjection\Dumper\OptimizedPhpArrayDumper';
/**
* The dumper instance.
*
* @var \Symfony\Component\DependencyInjection\Dumper\DumperInterface
*/
protected $dumper;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
// Setup a mock container builder.
$this->containerBuilder = $this->prophesize('\Symfony\Component\DependencyInjection\ContainerBuilder');
$this->containerBuilder->getAliases()->willReturn([]);
$this->containerBuilder->getParameterBag()->willReturn(new ParameterBag());
$this->containerBuilder->getDefinitions()->willReturn([]);
$this->containerBuilder->isCompiled()->willReturn(TRUE);
$definition = [];
$definition['aliases'] = [];
$definition['parameters'] = [];
$definition['services'] = [];
$definition['frozen'] = TRUE;
$definition['machine_format'] = $this->machineFormat;
$this->containerDefinition = $definition;
// Create the dumper.
$this->dumper = new $this->dumperClass($this->containerBuilder->reveal());
}
/**
* Tests that an empty container works properly.
*
* @covers ::dump
* @covers ::getArray
* @covers ::supportsMachineFormat
*/
public function testDumpForEmptyContainer(): void {
$serialized_definition = $this->dumper->dump();
$this->assertEquals(serialize($this->containerDefinition), $serialized_definition);
}
/**
* Tests that alias processing works properly.
*
* @covers ::getAliases
*
* @dataProvider getAliasesDataProvider
*/
public function testGetAliases($aliases, $definition_aliases): void {
$this->containerDefinition['aliases'] = $definition_aliases;
$this->containerBuilder->getAliases()->willReturn($aliases);
$this->assertEquals($this->containerDefinition, $this->dumper->getArray(), 'Expected definition matches dump.');
}
/**
* Data provider for testGetAliases().
*
* @return array[]
* Returns data-set elements with:
* - aliases as returned by ContainerBuilder.
* - aliases as expected in the container definition.
*/
public static function getAliasesDataProvider() {
return [
[[], []],
[
['foo' => 'foo.alias'],
['foo' => 'foo.alias'],
],
[
['foo' => 'foo.alias', 'foo.alias' => 'foo.alias.alias'],
['foo' => 'foo.alias.alias', 'foo.alias' => 'foo.alias.alias'],
],
];
}
/**
* Tests that parameter processing works properly.
*
* @covers ::getParameters
* @covers ::prepareParameters
* @covers ::escape
* @covers ::dumpValue
* @covers ::getReferenceCall
*
* @dataProvider getParametersDataProvider
*/
public function testGetParameters($parameters, $definition_parameters, $is_frozen): void {
$this->containerDefinition['parameters'] = $definition_parameters;
$this->containerDefinition['frozen'] = $is_frozen;
$parameter_bag = new ParameterBag($parameters);
$this->containerBuilder->getParameterBag()->willReturn($parameter_bag);
$this->containerBuilder->isCompiled()->willReturn($is_frozen);
$this->assertEquals($this->containerDefinition, $this->dumper->getArray(), 'Expected definition matches dump.');
}
/**
* Data provider for testGetParameters().
*
* @return array[]
* Returns data-set elements with:
* - parameters as returned by ContainerBuilder.
* - parameters as expected in the container definition.
* - frozen value
*/
public static function getParametersDataProvider() {
return [
[[], [], TRUE],
[
['foo' => 'value_foo'],
['foo' => 'value_foo'],
TRUE,
],
[
['foo' => ['llama' => 'yes']],
['foo' => ['llama' => 'yes']],
TRUE,
],
[
['foo' => '%llama%', 'llama' => 'yes'],
['foo' => '%%llama%%', 'llama' => 'yes'],
TRUE,
],
[
['foo' => '%llama%', 'llama' => 'yes'],
['foo' => '%llama%', 'llama' => 'yes'],
FALSE,
],
];
}
/**
* Tests that service processing works properly.
*
* @covers ::getServiceDefinitions
* @covers ::getServiceDefinition
* @covers ::dumpMethodCalls
* @covers ::dumpCollection
* @covers ::dumpCallable
* @covers ::dumpValue
* @covers ::getPrivateServiceCall
* @covers ::getReferenceCall
* @covers ::getServiceCall
* @covers ::getServiceClosureCall
* @covers ::getParameterCall
*
* @dataProvider getDefinitionsDataProvider
*/
public function testGetServiceDefinitions($services, $definition_services): void {
$this->containerDefinition['services'] = $definition_services;
$this->containerBuilder->getDefinitions()->willReturn($services);
$bar_definition = new Definition('\stdClass');
$bar_definition->setPublic(TRUE);
$this->containerBuilder->getDefinition('bar')->willReturn($bar_definition);
$private_definition = new Definition('\stdClass');
$private_definition->setPublic(FALSE);
$this->containerBuilder->getDefinition('private_definition')->willReturn($private_definition);
$this->assertEquals($this->containerDefinition, $this->dumper->getArray(), 'Expected definition matches dump.');
}
/**
* Data provider for testGetServiceDefinitions().
*
* @return array[]
* Returns data-set elements with:
* - parameters as returned by ContainerBuilder.
* - parameters as expected in the container definition.
* - frozen value
*/
public static function getDefinitionsDataProvider() {
$base_service_definition = [
'class' => '\stdClass',
'public' => TRUE,
'file' => FALSE,
'synthetic' => FALSE,
'lazy' => FALSE,
'arguments' => [],
'arguments_count' => 0,
'properties' => [],
'calls' => [],
'shared' => TRUE,
'factory' => FALSE,
'configurator' => FALSE,
];
// Test basic flags.
$service_definitions[] = [] + $base_service_definition;
$service_definitions[] = [
'public' => FALSE,
] + $base_service_definition;
$service_definitions[] = [
'file' => 'test_include.php',
] + $base_service_definition;
$service_definitions[] = [
'synthetic' => TRUE,
] + $base_service_definition;
$service_definitions[] = [
'shared' => FALSE,
] + $base_service_definition;
$service_definitions[] = [
'lazy' => TRUE,
] + $base_service_definition;
// Test a basic public Reference.
$service_definitions[] = [
'arguments' => ['foo', new Reference('bar')],
'arguments_count' => 2,
'arguments_expected' => static::getCollection(['foo', static::getServiceCall('bar')]),
] + $base_service_definition;
// Test a public reference that should not throw an Exception.
$reference = new Reference('bar', ContainerInterface::NULL_ON_INVALID_REFERENCE);
$service_definitions[] = [
'arguments' => [$reference],
'arguments_count' => 1,
'arguments_expected' => static::getCollection([static::getServiceCall('bar', ContainerInterface::NULL_ON_INVALID_REFERENCE)]),
] + $base_service_definition;
// Test a private shared service, denoted by having a Reference.
$private_definition = [
'class' => '\stdClass',
'public' => FALSE,
'arguments_count' => 0,
];
$service_definitions[] = [
'arguments' => ['foo', new Reference('private_definition')],
'arguments_count' => 2,
'arguments_expected' => static::getCollection([
'foo',
static::getPrivateServiceCall('private_definition', $private_definition, TRUE),
]),
] + $base_service_definition;
// Test a service closure.
$service_definitions[] = [
'arguments' => [
'foo',
[
'alias-1' => new ServiceClosureArgument(new Reference('bar', ContainerInterface::NULL_ON_INVALID_REFERENCE)),
'alias-2' => new ServiceClosureArgument(new Reference('bar', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE)),
],
],
'arguments_count' => 2,
'arguments_expected' => static::getCollection([
'foo',
static::getCollection([
'alias-1' => static::getServiceClosureCall('bar', ContainerInterface::NULL_ON_INVALID_REFERENCE),
'alias-2' => static::getServiceClosureCall('bar', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE),
]),
]),
] + $base_service_definition;
// Test a private non-shared service, denoted by having a Definition.
$private_definition_object = new Definition('\stdClass');
$private_definition_object->setPublic(FALSE);
$service_definitions[] = [
'arguments' => ['foo', $private_definition_object],
'arguments_count' => 2,
'arguments_expected' => static::getCollection([
'foo',
static::getPrivateServiceCall(NULL, $private_definition),
]),
] + $base_service_definition;
// Test a deep collection without a reference.
$service_definitions[] = [
'arguments' => [[['foo']]],
'arguments_count' => 1,
] + $base_service_definition;
// Test a deep collection with a reference to resolve.
$service_definitions[] = [
'arguments' => [[new Reference('bar')]],
'arguments_count' => 1,
'arguments_expected' => static::getCollection([static::getCollection([static::getServiceCall('bar')])]),
] + $base_service_definition;
// Test an IteratorArgument collection with a reference to resolve.
$service_definitions[] = [
'arguments' => [new IteratorArgument([new Reference('bar')])],
'arguments_count' => 1,
'arguments_expected' => static::getCollection([static::getIterator([static::getServiceCall('bar')])]),
] + $base_service_definition;
// Test a collection with a variable to resolve.
$service_definitions[] = [
'arguments' => [new Parameter('llama_parameter')],
'arguments_count' => 1,
'arguments_expected' => static::getCollection([static::getParameterCall('llama_parameter')]),
] + $base_service_definition;
// Test getMethodCalls.
$calls = [
['method', static::getCollection([])],
['method2', static::getCollection([])],
];
$service_definitions[] = [
'calls' => $calls,
] + $base_service_definition;
$service_definitions[] = [
'shared' => FALSE,
] + $base_service_definition;
// Test factory.
$service_definitions[] = [
'factory' => [new Reference('bar'), 'factoryMethod'],
'factory_expected' => [static::getServiceCall('bar'), 'factoryMethod'],
] + $base_service_definition;
// Test invalid factory - needed to test deep dumpValue().
$service_definitions[] = [
'factory' => [['foo', 'llama'], 'factoryMethod'],
] + $base_service_definition;
// Test properties.
$service_definitions[] = [
'properties' => ['_value' => 'llama'],
] + $base_service_definition;
// Test configurator.
$service_definitions[] = [
'configurator' => [new Reference('bar'), 'configureService'],
'configurator_expected' => [static::getServiceCall('bar'), 'configureService'],
] + $base_service_definition;
$services_provided = [];
$services_provided[] = [
[],
[],
];
foreach ($service_definitions as $service_definition) {
$definition = (new Prophet())->prophesize('\Symfony\Component\DependencyInjection\Definition');
$definition->getClass()->willReturn($service_definition['class']);
$definition->isPublic()->willReturn($service_definition['public']);
$definition->getFile()->willReturn($service_definition['file']);
$definition->isSynthetic()->willReturn($service_definition['synthetic']);
$definition->isLazy()->willReturn($service_definition['lazy']);
$definition->getArguments()->willReturn($service_definition['arguments']);
$definition->getProperties()->willReturn($service_definition['properties']);
$definition->getMethodCalls()->willReturn($service_definition['calls']);
$definition->isShared()->willReturn($service_definition['shared']);
$definition->getDecoratedService()->willReturn(NULL);
$definition->getFactory()->willReturn($service_definition['factory']);
$definition->getConfigurator()->willReturn($service_definition['configurator']);
// Preserve order.
$filtered_service_definition = [];
foreach ($base_service_definition as $key => $value) {
$filtered_service_definition[$key] = $service_definition[$key];
unset($service_definition[$key]);
if ($key == 'class' || $key == 'arguments_count') {
continue;
}
if ($filtered_service_definition[$key] === $base_service_definition[$key]) {
unset($filtered_service_definition[$key]);
}
}
// Add remaining properties.
$filtered_service_definition += $service_definition;
// Allow to set _expected values.
foreach (['arguments', 'factory', 'configurator'] as $key) {
$expected = $key . '_expected';
if (isset($filtered_service_definition[$expected])) {
$filtered_service_definition[$key] = $filtered_service_definition[$expected];
unset($filtered_service_definition[$expected]);
}
}
if (isset($filtered_service_definition['public']) && $filtered_service_definition['public'] === FALSE) {
$services_provided[] = [
['foo_service' => $definition->reveal()],
[],
];
continue;
}
$services_provided[] = [
['foo_service' => $definition->reveal()],
['foo_service' => static::serializeDefinition($filtered_service_definition)],
];
}
return $services_provided;
}
/**
* Helper function to serialize a definition.
*
* Used to override serialization.
*/
protected static function serializeDefinition(array $service_definition) {
return serialize($service_definition);
}
/**
* Helper function to return a service definition.
*/
protected static function getServiceCall($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
return (object) [
'type' => 'service',
'id' => $id,
'invalidBehavior' => $invalid_behavior,
];
}
/**
* Helper function to return a service closure definition.
*/
protected static function getServiceClosureCall($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
return (object) [
'type' => 'service_closure',
'id' => $id,
'invalidBehavior' => $invalid_behavior,
];
}
/**
* Tests that references to aliases work correctly.
*
* @covers ::getReferenceCall
*
* @dataProvider publicPrivateDataProvider
*/
public function testGetServiceDefinitionWithReferenceToAlias($public): void {
$bar_definition = new Definition('\stdClass');
$bar_definition_php_array = [
'class' => '\stdClass',
];
if ($public) {
$bar_definition->setPublic(TRUE);
}
else {
$bar_definition->setPublic(FALSE);
$bar_definition_php_array['public'] = FALSE;
}
$bar_definition_php_array['arguments_count'] = 0;
$services['bar'] = $bar_definition;
$aliases['bar.alias'] = 'bar';
$foo = new Definition('\stdClass');
$foo->setPublic(TRUE);
$foo->addArgument(new Reference('bar.alias'));
$services['foo'] = $foo;
$this->containerBuilder->getAliases()->willReturn($aliases);
$this->containerBuilder->getDefinitions()->willReturn($services);
$this->containerBuilder->getDefinition('bar')->willReturn($bar_definition);
$dump = $this->dumper->getArray();
if ($public) {
$service_definition = static::getServiceCall('bar');
}
else {
$service_definition = static::getPrivateServiceCall('bar', $bar_definition_php_array, TRUE);
}
$data = [
'class' => '\stdClass',
'arguments' => static::getCollection([
$service_definition,
]),
'arguments_count' => 1,
];
$this->assertEquals(static::serializeDefinition($data), $dump['services']['foo'], 'Expected definition matches dump.');
}
public static function publicPrivateDataProvider() {
return [
[TRUE],
[FALSE],
];
}
/**
* Tests that getDecoratedService() is unsupported.
*
* Tests that the correct InvalidArgumentException is thrown for
* getDecoratedService().
*
* @covers ::getServiceDefinition
*/
public function testGetServiceDefinitionForDecoratedService(): void {
$bar_definition = new Definition('\stdClass');
$bar_definition->setPublic(TRUE);
$bar_definition->setDecoratedService((string) new Reference('foo'));
$services['bar'] = $bar_definition;
$this->containerBuilder->getDefinitions()->willReturn($services);
$this->expectException(InvalidArgumentException::class);
$this->dumper->getArray();
}
/**
* Tests that the correct RuntimeException is thrown for expressions.
*
* @covers ::dumpValue
*/
public function testGetServiceDefinitionForExpression(): void {
$expression = new Expression('');
$bar_definition = new Definition('\stdClass');
$bar_definition->setPublic(TRUE);
$bar_definition->addArgument($expression);
$services['bar'] = $bar_definition;
$this->containerBuilder->getDefinitions()->willReturn($services);
$this->expectException(RuntimeException::class);
$this->dumper->getArray();
}
/**
* Tests that the correct RuntimeException is thrown for dumping an object.
*
* @covers ::dumpValue
*/
public function testGetServiceDefinitionForObject(): void {
$service = new \stdClass();
$bar_definition = new Definition('\stdClass');
$bar_definition->setPublic(TRUE);
$bar_definition->addArgument($service);
$services['bar'] = $bar_definition;
$this->containerBuilder->getDefinitions()->willReturn($services);
$this->expectException(RuntimeException::class);
$this->dumper->getArray();
}
/**
* Tests that the correct RuntimeException is thrown for dumping an object.
*
* @covers ::dumpValue
* @group legacy
*/
public function testGetServiceDefinitionForObjectServiceId(): void {
$service = new \stdClass();
$service->_serviceId = 'foo';
$services['foo'] = new Definition('\stdClass');
$services['bar'] = new Definition('\stdClass');
$services['bar']->addArgument($service);
foreach ($services as $s) {
$s->setPublic(TRUE);
}
$this->containerBuilder->getDefinitions()->willReturn($services);
$this->containerBuilder->getDefinition('foo')->willReturn($services['foo']);
$this->containerBuilder->getDefinition('bar')->willReturn($services['bar']);
$this->expectDeprecation('_serviceId is deprecated in drupal:9.5.0 and is removed from drupal:11.0.0. Use \Drupal\Core\DrupalKernelInterface::getServiceIdMapping() instead. See https://www.drupal.org/node/3292540');
$a = $this->dumper->getArray();
$this->assertEquals(
static::serializeDefinition([
'class' => '\stdClass',
// Legacy code takes care of converting _serviceId into this.
'arguments' => static::getCollection([static::getServiceCall('foo')]),
'arguments_count' => 1,
]), $a['services']['bar']);
}
/**
* Tests that the correct RuntimeException is thrown for dumping a resource.
*
* @covers ::dumpValue
*/
public function testGetServiceDefinitionForResource(): void {
$resource = fopen('php://memory', 'r');
$bar_definition = new Definition('\stdClass');
$bar_definition->setPublic(TRUE);
$bar_definition->addArgument($resource);
$services['bar'] = $bar_definition;
$this->containerBuilder->getDefinitions()->willReturn($services);
$this->expectException(RuntimeException::class);
$this->dumper->getArray();
}
/**
* Tests that service arguments with escaped percents are correctly dumped.
*
* @dataProvider percentsEscapeProvider
*/
public function testPercentsEscape($expected, $argument): void {
$definition = new Definition('\stdClass', [$argument]);
$definition->setPublic(TRUE);
$this->containerBuilder->getDefinitions()->willReturn([
'test' => $definition,
]);
$dump = $this->dumper->getArray();
$this->assertEquals(static::serializeDefinition([
'class' => '\stdClass',
'arguments' => static::getCollection([
$this->getRaw($expected),
]),
'arguments_count' => 1,
]), $dump['services']['test']);
}
/**
* Data provider for testPercentsEscape().
*
* @return array[]
* Returns data-set elements with:
* - expected final value.
* - escaped value in service definition.
*/
public static function percentsEscapeProvider() {
return [
['%foo%', '%%foo%%'],
['foo%bar%', 'foo%%bar%%'],
['%foo%bar', '%%foo%%bar'],
['%', '%'],
['%', '%%'],
['%%', '%%%'],
['%%', '%%%%'],
];
}
/**
* Helper function to return a private service definition.
*/
protected static function getPrivateServiceCall($id, $service_definition, $shared = FALSE) {
if (!$id) {
$hash = Crypt::hashBase64(serialize($service_definition));
$id = 'private__' . $hash;
}
return (object) [
'type' => 'private_service',
'id' => $id,
'value' => $service_definition,
'shared' => $shared,
];
}
/**
* Helper function to return a machine-optimized collection.
*/
protected static function getCollection($collection) {
return (object) [
'type' => 'collection',
'value' => $collection,
];
}
/**
* Helper function to return a machine-optimized iterator.
*/
protected static function getIterator($collection) {
return (object) [
'type' => 'iterator',
'value' => $collection,
];
}
/**
* Helper function to return a parameter definition.
*/
protected static function getParameterCall($name) {
return (object) [
'type' => 'parameter',
'name' => $name,
];
}
/**
* Helper function to return a raw value definition.
*/
protected function getRaw($value) {
return (object) [
'type' => 'raw',
'value' => $value,
];
}
}
}
/**
* Defines a dummy ExpressionLanguage component.
*
* As Drupal Core does not ship with ExpressionLanguage component we need to
* define a dummy, else it cannot be tested.
*/
namespace Symfony\Component\ExpressionLanguage {
if (!class_exists('\Symfony\Component\ExpressionLanguage\Expression')) {
/**
* Dummy class to ensure non-existent Symfony component can be tested.
*/
class Expression {
public function __construct($expression) {
}
/**
* Gets the string representation of the expression.
*/
public function __toString() {
return 'dummy_expression';
}
}
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\DependencyInjection\Dumper;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @coversDefaultClass \Drupal\Component\DependencyInjection\Dumper\PhpArrayDumper
* @group DependencyInjection
*/
class PhpArrayDumperTest extends OptimizedPhpArrayDumperTest {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->machineFormat = FALSE;
$this->dumperClass = '\Drupal\Component\DependencyInjection\Dumper\PhpArrayDumper';
parent::setUp();
}
/**
* {@inheritdoc}
*/
protected static function serializeDefinition(array $service_definition) {
return $service_definition;
}
/**
* {@inheritdoc}
*/
protected static function getServiceCall($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
if ($invalid_behavior !== ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
return sprintf('@?%s', $id);
}
return sprintf('@%s', $id);
}
/**
* {@inheritdoc}
*/
protected static function getParameterCall($name) {
return '%' . $name . '%';
}
/**
* {@inheritdoc}
*/
protected static function getCollection($collection, $resolve = TRUE) {
return $collection;
}
}

View File

@@ -0,0 +1,16 @@
<?php
/**
* @file
* Contains a test function for container 'file' include testing.
*/
/**
* Test function for container testing.
*
* @return string
* A string just for testing.
*/
function container_test_file_service_test_service_function() {
return 'Hello Container';
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @coversDefaultClass \Drupal\Component\DependencyInjection\PhpArrayContainer
* @group DependencyInjection
*/
class PhpArrayContainerTest extends ContainerTest {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->machineFormat = FALSE;
$this->containerClass = '\Drupal\Component\DependencyInjection\PhpArrayContainer';
$this->containerDefinition = $this->getMockContainerDefinition();
$this->container = new $this->containerClass($this->containerDefinition);
}
/**
* Helper function to return a service definition.
*/
protected function getServiceCall($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
if ($invalid_behavior !== ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
return sprintf('@?%s', $id);
}
return sprintf('@%s', $id);
}
/**
* Helper function to return a service definition.
*/
protected function getParameterCall($name) {
return '%' . $name . '%';
}
/**
* Helper function to return a machine-optimized '@notation'.
*/
protected function getCollection($collection, $resolve = TRUE) {
return $collection;
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\DependencyInjection;
use Drupal\Component\DependencyInjection\ReverseContainer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* @runTestsInSeparateProcesses
* The reverse container uses a static to maintain information across
* container rebuilds.
*
* @coversDefaultClass \Drupal\Component\DependencyInjection\ReverseContainer
* @group DependencyInjection
*/
class ReverseContainerTest extends TestCase {
/**
* @covers ::getId
*/
public function testGetId(): void {
$container = new ContainerBuilder();
$service = new \stdClass();
$container->set('bar', $service);
$reverse_container = new ReverseContainer($container);
$this->assertSame('bar', $reverse_container->getId($service));
$non_service = new \stdClass();
$this->assertNull($reverse_container->getId($non_service));
$this->assertSame('service_container', $reverse_container->getId($container));
}
/**
* @covers ::recordContainer
*/
public function testRecordContainer(): void {
$container = new ContainerBuilder();
$service = new \stdClass();
$container->set('bar', $service);
$reverse_container = new ReverseContainer($container);
$reverse_container->recordContainer();
$container = new ContainerBuilder();
$reverse_container = new ReverseContainer($container);
// New container does not have a bar service.
$this->assertNull($reverse_container->getId($service));
// Add the bar service to make the lookup based on the old object work as
// expected.
$container->set('bar', new \stdClass());
$this->assertSame('bar', $reverse_container->getId($service));
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Diff;
use Drupal\Component\Diff\Diff;
use Drupal\Component\Diff\DiffFormatter;
use PHPUnit\Framework\TestCase;
/**
* Test DiffFormatter classes.
*
* @coversDefaultClass \Drupal\Component\Diff\DiffFormatter
*
* @group Diff
*/
class DiffFormatterTest extends TestCase {
/**
* @return array
* - Expected formatted diff output.
* - First array of text to diff.
* - Second array of text to diff.
*/
public static function provideTestDiff() {
return [
'empty' => ['', [], []],
'add' => [
"3a3\n> line2a\n",
['line1', 'line2', 'line3'],
['line1', 'line2', 'line2a', 'line3'],
],
'delete' => [
"3d3\n< line2a\n",
['line1', 'line2', 'line2a', 'line3'],
['line1', 'line2', 'line3'],
],
'change' => [
"3c3\n< line2a\n---\n> line2b\n",
['line1', 'line2', 'line2a', 'line3'],
['line1', 'line2', 'line2b', 'line3'],
],
];
}
/**
* Tests whether op classes returned by DiffEngine::diff() match expectations.
*
* @covers ::format
* @dataProvider provideTestDiff
*/
public function testDiff($expected, $from, $to): void {
$diff = new Diff($from, $to);
$formatter = new DiffFormatter();
$output = $formatter->format($diff);
$this->assertEquals($expected, $output);
}
}

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Diff;
use Drupal\Component\Diff\DiffOpOutputBuilder;
use Drupal\Component\Diff\Engine\DiffOpAdd;
use Drupal\Component\Diff\Engine\DiffOpCopy;
use Drupal\Component\Diff\Engine\DiffOpChange;
use Drupal\Component\Diff\Engine\DiffOpDelete;
use PHPUnit\Framework\TestCase;
use SebastianBergmann\Diff\Differ;
/**
* @coversDefaultClass \Drupal\Component\Diff\DiffOpOutputBuilder
*
* @group Diff
*/
class DiffOpOutputBuilderTest extends TestCase {
/**
* @return array
* - Expected output in terms of return class. A list of class names
* expected to be returned by DiffEngine::diff().
* - An array of strings to change from.
* - An array of strings to change to.
*/
public static function provideTestDiff(): array {
return [
'empty' => [[], [], []],
'add' => [[new DiffOpAdd(['a'])], [], ['a']],
'copy' => [[new DiffOpCopy(['a'])], ['a'], ['a']],
'change' => [[new DiffOpChange(['a'], ['b'])], ['a'], ['b']],
'copy-and-change' => [
[
new DiffOpCopy(['a']),
new DiffOpChange(['b'], ['c']),
],
['a', 'b'],
['a', 'c'],
],
'copy-change-copy' => [
[
new DiffOpCopy(['a']),
new DiffOpChange(['b'], ['c']),
new DiffOpCopy(['d']),
],
['a', 'b', 'd'],
['a', 'c', 'd'],
],
'copy-change-copy-add' => [
[
new DiffOpCopy(['a']),
new DiffOpChange(['b'], ['c']),
new DiffOpCopy(['d']),
new DiffOpAdd(['e']),
],
['a', 'b', 'd'],
['a', 'c', 'd', 'e'],
],
'copy-delete' => [
[
new DiffOpCopy(['a']),
new DiffOpDelete(['b', 'd']),
],
['a', 'b', 'd'],
['a'],
],
'change-copy' => [
[
new DiffOpChange(['aa', 'bb', 'cc'], ['a', 'c']),
new DiffOpCopy(['d']),
],
['aa', 'bb', 'cc', 'd'],
['a', 'c', 'd'],
],
'copy-change-copy-change' => [
[
new DiffOpCopy(['a']),
new DiffOpChange(['bb'], ['b', 'c']),
new DiffOpCopy(['d']),
new DiffOpChange(['ee'], ['e']),
],
['a', 'bb', 'd', 'ee'],
['a', 'b', 'c', 'd', 'e'],
],
];
}
/**
* Tests whether op classes returned match expectations.
*
* @covers ::toOpsArray
* @dataProvider provideTestDiff
*/
public function testToOpsArray(array $expected, array $from, array $to): void {
$diffOpBuilder = new DiffOpOutputBuilder();
$differ = new Differ($diffOpBuilder);
$diff = $differ->diffToArray($from, $to);
$this->assertEquals($expected, $diffOpBuilder->toOpsArray($diff));
}
/**
* @covers ::getDiff
* @dataProvider provideTestDiff
*/
public function testGetDiff(array $expected, array $from, array $to): void {
$differ = new Differ(new DiffOpOutputBuilder());
$diff = $differ->diff($from, $to);
$this->assertEquals($expected, unserialize($diff));
}
/**
* Tests that two files can be successfully diffed.
*
* @covers ::toOpsArray
*/
public function testDiffInfiniteLoop(): void {
$from = explode("\n", file_get_contents(__DIR__ . '/Engine/fixtures/file1.txt'));
$to = explode("\n", file_get_contents(__DIR__ . '/Engine/fixtures/file2.txt'));
$diffOpBuilder = new DiffOpOutputBuilder();
$differ = new Differ($diffOpBuilder);
$diff = $differ->diffToArray($from, $to);
$diffOps = $diffOpBuilder->toOpsArray($diff);
$this->assertCount(4, $diffOps);
$this->assertEquals($diffOps[0], new DiffOpAdd([' - image.style.max_325x325']));
$this->assertEquals($diffOps[1], new DiffOpCopy([' - image.style.max_650x650']));
$this->assertEquals($diffOps[2], new DiffOpChange([' - image.style.max_325x325'], ['_core:', ' default_config_hash: random_hash_string_here']));
$this->assertEquals($diffOps[3], new DiffOpCopy(['fallback_image_style: max_325x325', '']));
}
}

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Diff\Engine;
use Drupal\Component\Diff\Engine\DiffEngine;
use Drupal\Component\Diff\Engine\DiffOpAdd;
use Drupal\Component\Diff\Engine\DiffOpCopy;
use Drupal\Component\Diff\Engine\DiffOpChange;
use Drupal\Component\Diff\Engine\DiffOpDelete;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
/**
* Test DiffEngine class.
*
* @coversDefaultClass \Drupal\Component\Diff\Engine\DiffEngine
*
* @group Diff
* @group legacy
*/
class DiffEngineTest extends TestCase {
use ExpectDeprecationTrait;
/**
* @return array
* - Expected output in terms of return class. A list of class names
* expected to be returned by DiffEngine::diff().
* - An array of strings to change from.
* - An array of strings to change to.
*/
public static function provideTestDiff() {
return [
'empty' => [[], [], []],
'add' => [[DiffOpAdd::class], [], ['a']],
'copy' => [[DiffOpCopy::class], ['a'], ['a']],
'change' => [[DiffOpChange::class], ['a'], ['b']],
'copy-and-change' => [
[
DiffOpCopy::class,
DiffOpChange::class,
],
['a', 'b'],
['a', 'c'],
],
'copy-change-copy' => [
[
DiffOpCopy::class,
DiffOpChange::class,
DiffOpCopy::class,
],
['a', 'b', 'd'],
['a', 'c', 'd'],
],
'copy-change-copy-add' => [
[
DiffOpCopy::class,
DiffOpChange::class,
DiffOpCopy::class,
DiffOpAdd::class,
],
['a', 'b', 'd'],
['a', 'c', 'd', 'e'],
],
'copy-delete' => [
[
DiffOpCopy::class,
DiffOpDelete::class,
],
['a', 'b', 'd'],
['a'],
],
'change-copy' => [
[
DiffOpChange::class,
DiffOpCopy::class,
],
['aa', 'bb', 'cc', 'd'],
['a', 'c', 'd'],
],
'copy-change-copy-change' => [
[
DiffOpCopy::class,
DiffOpChange::class,
DiffOpCopy::class,
DiffOpChange::class,
],
['a', 'bb', 'd', 'ee'],
['a', 'b', 'c', 'd', 'e'],
],
];
}
/**
* Tests whether op classes returned by DiffEngine::diff() match expectations.
*
* @covers ::diff
* @dataProvider provideTestDiff
*/
public function testDiff($expected, $from, $to): void {
$this->expectDeprecation('Drupal\Component\Diff\Engine\DiffEngine is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use sebastianbergmann/diff instead. See https://www.drupal.org/node/3337942');
$diff_engine = new DiffEngine();
$diff = $diff_engine->diff($from, $to);
// Make sure we have the same number of results as expected.
$this->assertSameSize($expected, $diff);
// Make sure the diff objects match our expectations.
foreach ($expected as $index => $op_class) {
$this->assertEquals($op_class, get_class($diff[$index]));
}
}
/**
* Tests that two files can be successfully diffed.
*
* @covers ::diff
*/
public function testDiffInfiniteLoop(): void {
$this->expectDeprecation('Drupal\Component\Diff\Engine\DiffEngine is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use sebastianbergmann/diff instead. See https://www.drupal.org/node/3337942');
$from = explode("\n", file_get_contents(__DIR__ . '/fixtures/file1.txt'));
$to = explode("\n", file_get_contents(__DIR__ . '/fixtures/file2.txt'));
$diff_engine = new DiffEngine();
$diff = $diff_engine->diff($from, $to);
$this->assertCount(4, $diff);
$this->assertEquals($diff[0], new DiffOpDelete([' - image.style.max_650x650']));
$this->assertEquals($diff[1], new DiffOpCopy([' - image.style.max_325x325']));
$this->assertEquals($diff[2], new DiffOpAdd([' - image.style.max_650x650', '_core:', ' default_config_hash: random_hash_string_here']));
$this->assertEquals($diff[3], new DiffOpCopy(['fallback_image_style: max_325x325', '']));
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Diff\Engine;
use Drupal\Component\Diff\Engine\DiffOp;
use Drupal\Tests\Traits\PhpUnitWarnings;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
/**
* Test DiffOp base class.
*
* The only significant behavior here is that ::reverse() should throw an error
* if not overridden. In versions of this code in other projects, reverse() is
* marked as abstract, which enforces some of this behavior.
*
* @coversDefaultClass \Drupal\Component\Diff\Engine\DiffOp
*
* @group Diff
* @group legacy
*/
class DiffOpTest extends TestCase {
use ExpectDeprecationTrait;
use PhpUnitWarnings;
/**
* DiffOp::reverse() always throws an error.
*
* @covers ::reverse
*/
public function testReverse(): void {
$this->expectDeprecation('Drupal\Component\Diff\Engine\DiffOp::reverse() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3337942');
$this->expectError();
$op = new DiffOp();
$result = $op->reverse();
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Diff\Engine;
use Drupal\Component\Diff\Engine\HWLDFWordAccumulator;
use PHPUnit\Framework\TestCase;
// cspell:ignore wordword
/**
* Test HWLDFWordAccumulator.
*
* @coversDefaultClass \Drupal\Component\Diff\Engine\HWLDFWordAccumulator
*
* @group Diff
*/
class HWLDFWordAccumulatorTest extends TestCase {
/**
* Verify that we only get back a NBSP from an empty accumulator.
*
* @covers ::getLines
*
* @see Drupal\Component\Diff\Engine\HWLDFWordAccumulator::NBSP
*/
public function testGetLinesEmpty(): void {
$acc = new HWLDFWordAccumulator();
$this->assertEquals(['&#160;'], $acc->getLines());
}
/**
* @return array
* - Expected array of lines from getLines().
* - Array of strings for the $words parameter to addWords().
* - String tag for the $tag parameter to addWords().
*/
public static function provideAddWords() {
return [
[['wordword2'], ['word', 'word2'], 'tag'],
[['word', 'word2'], ['word', "\nword2"], 'tag'],
[['&#160;', 'word2'], ['', "\nword2"], 'tag'],
];
}
/**
* @covers ::addWords
* @dataProvider provideAddWords
*/
public function testAddWords($expected, $words, $tag): void {
$acc = new HWLDFWordAccumulator();
$acc->addWords($words, $tag);
$this->assertEquals($expected, $acc->getLines());
}
}

View File

@@ -0,0 +1,3 @@
- image.style.max_650x650
- image.style.max_325x325
fallback_image_style: max_325x325

View File

@@ -0,0 +1,5 @@
- image.style.max_325x325
- image.style.max_650x650
_core:
default_config_hash: random_hash_string_here
fallback_image_style: max_325x325

View File

@@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Discovery;
use Drupal\Component\Discovery\DiscoveryException;
use Drupal\Component\Discovery\YamlDirectoryDiscovery;
use Drupal\Component\FileCache\FileCacheFactory;
use org\bovigo\vfs\vfsStream;
use PHPUnit\Framework\TestCase;
/**
* YamlDirectoryDiscoveryTest component unit tests.
*
* @coversDefaultClass \Drupal\Component\Discovery\YamlDirectoryDiscovery
*
* @group Discovery
*/
class YamlDirectoryDiscoveryTest extends TestCase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
// Ensure that FileCacheFactory has a prefix.
FileCacheFactory::setPrefix('prefix');
}
/**
* Tests YAML directory discovery.
*
* @covers ::findAll
*/
public function testDiscovery(): void {
vfsStream::setup('modules', NULL, [
'test_1' => [
'subdir1' => [
'item_1.test.yml' => "id: item1\nname: 'test1 item 1'",
],
'subdir2' => [
'item_2.test.yml' => "id: item2\nname: 'test1 item 2'",
],
],
'test_2' => [
'subdir1' => [
'item_3.test.yml' => "id: item3\nname: 'test2 item 3'",
],
'subdir2' => [],
],
'test_3' => [],
'test_4' => [
'subdir1' => [
'item_4.test.yml' => "id: item4\nname: 'test4 item 4'",
'item_5.test.yml' => "id: item5\nname: 'test4 item 5'",
'item_6.test.yml' => "id: item6\nname: 'test4 item 6'",
],
],
]);
// Set up the directories to search.
$directories = [
// Multiple directories both with valid items.
'test_1' => [
vfsStream::url('modules/test_1/subdir1'),
vfsStream::url('modules/test_1/subdir2'),
],
// The subdir2 directory is empty.
'test_2' => [
vfsStream::url('modules/test_2/subdir1'),
vfsStream::url('modules/test_2/subdir2'),
],
// Directories that do not exist.
'test_3' => [
vfsStream::url('modules/test_3/subdir1'),
vfsStream::url('modules/test_3/subdir2'),
],
// A single directory.
'test_4' => vfsStream::url('modules/test_4/subdir1'),
];
$discovery = new YamlDirectoryDiscovery($directories, 'test');
$data = $discovery->findAll();
// The file path is dependent on the operating system, so we adjust the directory separator.
$this->assertSame(['id' => 'item1', 'name' => 'test1 item 1', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_1/subdir1' . DIRECTORY_SEPARATOR . 'item_1.test.yml'], $data['test_1']['item1']);
$this->assertSame(['id' => 'item2', 'name' => 'test1 item 2', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_1/subdir2' . DIRECTORY_SEPARATOR . 'item_2.test.yml'], $data['test_1']['item2']);
$this->assertCount(2, $data['test_1']);
$this->assertSame(['id' => 'item3', 'name' => 'test2 item 3', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_2/subdir1' . DIRECTORY_SEPARATOR . 'item_3.test.yml'], $data['test_2']['item3']);
$this->assertCount(1, $data['test_2']);
$this->assertArrayNotHasKey('test_3', $data, 'test_3 provides 0 items');
$this->assertSame(['id' => 'item4', 'name' => 'test4 item 4', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_4/subdir1' . DIRECTORY_SEPARATOR . 'item_4.test.yml'], $data['test_4']['item4']);
$this->assertSame(['id' => 'item5', 'name' => 'test4 item 5', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_4/subdir1' . DIRECTORY_SEPARATOR . 'item_5.test.yml'], $data['test_4']['item5']);
$this->assertSame(['id' => 'item6', 'name' => 'test4 item 6', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_4/subdir1' . DIRECTORY_SEPARATOR . 'item_6.test.yml'], $data['test_4']['item6']);
$this->assertCount(3, $data['test_4']);
}
/**
* Tests YAML directory discovery with an alternate ID key.
*
* @covers ::findAll
*/
public function testDiscoveryAlternateId(): void {
vfsStream::setup('modules', NULL, [
'test_1' => [
'item_1.test.yml' => "alt_id: item1\nid: ignored",
],
]);
// Set up the directories to search.
$directories = ['test_1' => vfsStream::url('modules/test_1')];
$discovery = new YamlDirectoryDiscovery($directories, 'test', 'alt_id');
$data = $discovery->findAll();
$this->assertSame(['alt_id' => 'item1', 'id' => 'ignored', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_1' . DIRECTORY_SEPARATOR . 'item_1.test.yml'], $data['test_1']['item1']);
$this->assertCount(1, $data['test_1']);
}
/**
* Tests YAML directory discovery with a missing ID key.
*
* @covers ::findAll
* @covers ::getIdentifier
*/
public function testDiscoveryNoIdException(): void {
$this->expectException(DiscoveryException::class);
$this->expectExceptionMessage('The vfs://modules/test_1' . DIRECTORY_SEPARATOR . 'item_1.test.yml contains no data in the identifier key \'id\'');
vfsStream::setup('modules', NULL, [
'test_1' => [
'item_1.test.yml' => "",
],
]);
// Set up the directories to search.
$directories = ['test_1' => vfsStream::url('modules/test_1')];
$discovery = new YamlDirectoryDiscovery($directories, 'test');
$discovery->findAll();
}
/**
* Tests YAML directory discovery with invalid YAML.
*
* @covers ::findAll
*/
public function testDiscoveryInvalidYamlException(): void {
$this->expectException(DiscoveryException::class);
$this->expectExceptionMessage('The vfs://modules/test_1' . DIRECTORY_SEPARATOR . 'item_1.test.yml contains invalid YAML');
vfsStream::setup('modules', NULL, [
'test_1' => [
'item_1.test.yml' => "id: invalid\nfoo : [bar}",
],
]);
// Set up the directories to search.
$directories = ['test_1' => vfsStream::url('modules/test_1')];
$discovery = new YamlDirectoryDiscovery($directories, 'test');
$discovery->findAll();
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Discovery;
use Drupal\Component\Discovery\YamlDiscovery;
use Drupal\Component\FileCache\FileCacheFactory;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamDirectory;
use org\bovigo\vfs\vfsStreamWrapper;
use PHPUnit\Framework\TestCase;
/**
* YamlDiscovery component unit tests.
*
* @group Discovery
*/
class YamlDiscoveryTest extends TestCase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
// Ensure that FileCacheFactory has a prefix.
FileCacheFactory::setPrefix('prefix');
}
/**
* Tests the YAML file discovery.
*/
public function testDiscovery(): void {
vfsStreamWrapper::register();
$root = new vfsStreamDirectory('modules');
vfsStreamWrapper::setRoot($root);
$url = vfsStream::url('modules');
mkdir($url . '/test_1');
file_put_contents($url . '/test_1/test_1.test.yml', 'name: test');
file_put_contents($url . '/test_1/test_2.test.yml', 'name: test');
mkdir($url . '/test_2');
file_put_contents($url . '/test_2/test_3.test.yml', 'name: test');
// Write an empty YAML file.
file_put_contents($url . '/test_2/test_4.test.yml', '');
// Set up the directories to search.
$directories = [
'test_1' => $url . '/test_1',
'test_2' => $url . '/test_1',
'test_3' => $url . '/test_2',
'test_4' => $url . '/test_2',
];
$discovery = new YamlDiscovery('test', $directories);
$data = $discovery->findAll();
$this->assertCount(4, $data);
$this->assertArrayHasKey('test_1', $data);
$this->assertArrayHasKey('test_2', $data);
$this->assertArrayHasKey('test_3', $data);
$this->assertArrayHasKey('test_4', $data);
foreach (['test_1', 'test_2', 'test_3'] as $key) {
$this->assertArrayHasKey('name', $data[$key]);
$this->assertEquals('test', $data[$key]['name']);
}
$this->assertSame([], $data['test_4']);
}
/**
* Tests if filename is output for a broken YAML file.
*/
public function testForBrokenYml(): void {
vfsStreamWrapper::register();
$root = new vfsStreamDirectory('modules');
vfsStreamWrapper::setRoot($root);
$url = vfsStream::url('modules');
mkdir($url . '/test_broken');
file_put_contents($url . '/test_broken/test_broken.test.yml', "broken:\n:");
$this->expectException(InvalidDataTypeException::class);
$this->expectExceptionMessage('vfs://modules/test_broken/test_broken.test.yml');
$directories = ['test_broken' => $url . '/test_broken'];
$discovery = new YamlDiscovery('test', $directories);
$discovery->findAll();
}
}

View File

@@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component;
use org\bovigo\vfs\vfsStream;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\TestCase;
/**
* General tests for \Drupal\Component that can't go anywhere else.
*
* @group Component
*/
class DrupalComponentTest extends TestCase {
/**
* Tests that classes in Component do not use any Core class.
*/
public function testNoCoreInComponent(): void {
$component_path = dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))) . '/lib/Drupal/Component';
foreach ($this->findPhpClasses($component_path) as $class) {
$this->assertNoCoreUsage($class);
}
}
/**
* Tests that classes in Component Tests do not use any Core class.
*/
public function testNoCoreInComponentTests(): void {
$component_path = dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))) . '/tests/Drupal/Tests/Component';
foreach ($this->findPhpClasses($component_path) as $class) {
$this->assertNoCoreUsage($class);
}
}
/**
* Tests LICENSE.txt is present and has the correct content.
*
* @param string $component_path
* The path to the component.
*
* @dataProvider getComponents
*/
public function testComponentLicense(string $component_path): void {
$this->assertFileExists($component_path . DIRECTORY_SEPARATOR . 'LICENSE.txt');
$this->assertSame('e84dac1d9fbb5a4a69e38654ce644cea769aa76b', hash_file('sha1', $component_path . DIRECTORY_SEPARATOR . 'LICENSE.txt'));
}
/**
* Data provider.
*
* @return array
*/
public static function getComponents(): array {
$root_component_path = dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))) . '/lib/Drupal/Component';
$component_paths = [];
foreach (new \DirectoryIterator($root_component_path) as $file) {
if ($file->isDir() && !$file->isDot()) {
$component_paths[$file->getBasename()] = [$file->getPathname()];
}
}
return $component_paths;
}
/**
* Searches a directory recursively for PHP classes.
*
* @param string $dir
* The full path to the directory that should be checked.
*
* @return array
* An array of class paths.
*/
protected function findPhpClasses($dir) {
$classes = [];
foreach (new \DirectoryIterator($dir) as $file) {
if ($file->isDir() && !$file->isDot()) {
$classes = array_merge($classes, $this->findPhpClasses($file->getPathname()));
}
elseif ($file->getExtension() == 'php') {
$classes[] = $file->getPathname();
}
}
return $classes;
}
/**
* Asserts that the given class is not using any class from Core namespace.
*
* @param string $class_path
* The full path to the class that should be checked.
*
* @internal
*/
protected function assertNoCoreUsage(string $class_path): void {
$contents = file_get_contents($class_path);
preg_match_all('/^.*Drupal\\\Core.*$/m', $contents, $matches);
$matches = array_filter($matches[0], function ($line) {
// Filter references that don't really matter.
return preg_match('/@see|E_USER_DEPRECATED|expectDeprecation/', $line) === 0;
});
$this->assertEmpty($matches, "Checking for illegal reference to 'Drupal\\Core' namespace in $class_path");
}
/**
* Data provider for testAssertNoCoreUsage().
*
* @return array
* Data for testAssertNoCoreUsage() in the form:
* - TRUE if the test passes, FALSE otherwise.
* - File data as a string. This will be used as a virtual file.
*/
public static function providerAssertNoCoreUsage() {
return [
[
TRUE,
'@see \\Drupal\\Core\\Something',
],
[
FALSE,
'\\Drupal\\Core\\Something',
],
[
FALSE,
"@see \\Drupal\\Core\\Something\n" .
'\\Drupal\\Core\\Something',
],
[
FALSE,
"\\Drupal\\Core\\Something\n" .
'@see \\Drupal\\Core\\Something',
],
];
}
/**
* @covers \Drupal\Tests\Component\DrupalComponentTest::assertNoCoreUsage
* @dataProvider providerAssertNoCoreUsage
*/
public function testAssertNoCoreUsage($expected_pass, $file_data): void {
// Set up a virtual file to read.
$vfs_root = vfsStream::setup('root');
vfsStream::newFile('Test.php')->at($vfs_root)->setContent($file_data);
$file_uri = vfsStream::url('root/Test.php');
try {
$pass = TRUE;
$this->assertNoCoreUsage($file_uri);
}
catch (AssertionFailedError $e) {
$pass = FALSE;
}
$this->assertEquals($expected_pass, $pass, $expected_pass ?
'Test caused a false positive' :
'Test failed to detect Core usage');
}
}

View File

@@ -0,0 +1,629 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\EventDispatcher;
use Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Contracts\EventDispatcher\Event as SymfonyEvent;
use Symfony\Component\EventDispatcher\GenericEvent;
use Drupal\Component\EventDispatcher\Event;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
/**
* Unit tests for the ContainerAwareEventDispatcher.
*
* NOTE: Most of this code is a literal copy of Symfony 3.4's
* Symfony\Component\EventDispatcher\Tests\AbstractEventDispatcherTest.
*
* This file does NOT follow Drupal coding standards, so as to simplify future
* synchronizations.
*
* @group EventDispatcher
*/
class ContainerAwareEventDispatcherTest extends TestCase {
use ExpectDeprecationTrait;
/* Some pseudo events */
const PRE_FOO = 'pre.foo';
const POST_FOO = 'post.foo';
const PRE_BAR = 'pre.bar';
const POST_BAR = 'post.bar';
/**
* @var \Symfony\Component\EventDispatcher\EventDispatcher
*/
private $dispatcher;
private $listener;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->dispatcher = $this->createEventDispatcher();
$this->listener = new TestEventListener();
}
/**
* {@inheritdoc}
*/
protected function tearDown(): void {
$this->dispatcher = NULL;
$this->listener = NULL;
}
protected function createEventDispatcher() {
$container = new Container();
return new ContainerAwareEventDispatcher($container);
}
public function testGetListenersWithCallables(): void {
// When passing in callables exclusively as listeners into the event
// dispatcher constructor, the event dispatcher must not attempt to
// resolve any services.
$container = $this->getMockBuilder(ContainerInterface::class)->getMock();
$container->expects($this->never())->method($this->anything());
$firstListener = new CallableClass();
$secondListener = function () {
};
$thirdListener = [new TestEventListener(), 'preFoo'];
$listeners = [
'test_event' => [
0 => [
['callable' => $firstListener],
['callable' => $secondListener],
['callable' => $thirdListener],
],
],
];
$dispatcher = new ContainerAwareEventDispatcher($container, $listeners);
$actualListeners = $dispatcher->getListeners();
$expectedListeners = [
'test_event' => [
$firstListener,
$secondListener,
$thirdListener,
],
];
$this->assertSame($expectedListeners, $actualListeners);
}
public function testDispatchWithCallables(): void {
// When passing in callables exclusively as listeners into the event
// dispatcher constructor, the event dispatcher must not attempt to
// resolve any services.
$container = $this->getMockBuilder(ContainerInterface::class)->getMock();
$container->expects($this->never())->method($this->anything());
$firstListener = new CallableClass();
$secondListener = function () {
};
$thirdListener = [new TestEventListener(), 'preFoo'];
$listeners = [
'test_event' => [
0 => [
['callable' => $firstListener],
['callable' => $secondListener],
['callable' => $thirdListener],
],
],
];
$dispatcher = new ContainerAwareEventDispatcher($container, $listeners);
$dispatcher->dispatch(new Event(), 'test_event');
$this->assertTrue($thirdListener[0]->preFooInvoked);
}
public function testGetListenersWithServices(): void {
$container = new ContainerBuilder();
$container->register('listener_service', TestEventListener::class);
$listeners = [
'test_event' => [
0 => [
['service' => ['listener_service', 'preFoo']],
],
],
];
$dispatcher = new ContainerAwareEventDispatcher($container, $listeners);
$actualListeners = $dispatcher->getListeners();
$listenerService = $container->get('listener_service');
$expectedListeners = [
'test_event' => [
[$listenerService, 'preFoo'],
],
];
$this->assertSame($expectedListeners, $actualListeners);
}
/**
* Tests dispatching Symfony events with core's event dispatcher.
*/
public function testSymfonyEventDispatching(): void {
$container = new ContainerBuilder();
$dispatcher = new ContainerAwareEventDispatcher($container, []);
$dispatcher->dispatch(new GenericEvent());
}
public function testDispatchWithServices(): void {
$container = new ContainerBuilder();
$container->register('listener_service', TestEventListener::class);
$listeners = [
'test_event' => [
0 => [
['service' => ['listener_service', 'preFoo']],
],
],
];
$dispatcher = new ContainerAwareEventDispatcher($container, $listeners);
$dispatcher->dispatch(new Event(), 'test_event');
$listenerService = $container->get('listener_service');
$this->assertTrue($listenerService->preFooInvoked);
}
public function testRemoveService(): void {
$container = new ContainerBuilder();
$container->register('listener_service', TestEventListener::class);
$container->register('other_listener_service', TestEventListener::class);
$listeners = [
'test_event' => [
0 => [
['service' => ['listener_service', 'preFoo']],
['service' => ['other_listener_service', 'preFoo']],
],
],
];
$dispatcher = new ContainerAwareEventDispatcher($container, $listeners);
$listenerService = $container->get('listener_service');
$dispatcher->removeListener('test_event', [$listenerService, 'preFoo']);
// Ensure that other service was not initialized during removal of the
// listener service.
$this->assertFalse($container->initialized('other_listener_service'));
$dispatcher->dispatch(new Event(), 'test_event');
$this->assertFalse($listenerService->preFooInvoked);
$otherService = $container->get('other_listener_service');
$this->assertTrue($otherService->preFooInvoked);
}
public function testGetListenerPriorityWithServices(): void {
$container = new ContainerBuilder();
$container->register('listener_service', TestEventListener::class);
$listeners = [
'test_event' => [
5 => [
['service' => ['listener_service', 'preFoo']],
],
],
];
$dispatcher = new ContainerAwareEventDispatcher($container, $listeners);
$listenerService = $container->get('listener_service');
$actualPriority = $dispatcher->getListenerPriority('test_event', [$listenerService, 'preFoo']);
$this->assertSame(5, $actualPriority);
}
public function testInitialState(): void {
$this->assertEquals([], $this->dispatcher->getListeners());
$this->assertFalse($this->dispatcher->hasListeners(self::PRE_FOO));
$this->assertFalse($this->dispatcher->hasListeners(self::POST_FOO));
}
public function testAddListener(): void {
$this->dispatcher->addListener('pre.foo', [$this->listener, 'preFoo']);
$this->dispatcher->addListener('post.foo', [$this->listener, 'postFoo']);
$this->assertTrue($this->dispatcher->hasListeners());
$this->assertTrue($this->dispatcher->hasListeners(self::PRE_FOO));
$this->assertTrue($this->dispatcher->hasListeners(self::POST_FOO));
$this->assertCount(1, $this->dispatcher->getListeners(self::PRE_FOO));
$this->assertCount(1, $this->dispatcher->getListeners(self::POST_FOO));
$this->assertCount(2, $this->dispatcher->getListeners());
}
public function testGetListenersSortsByPriority(): void {
$listener1 = new TestEventListener();
$listener2 = new TestEventListener();
$listener3 = new TestEventListener();
$listener1->name = '1';
$listener2->name = '2';
$listener3->name = '3';
$this->dispatcher->addListener('pre.foo', [$listener1, 'preFoo'], -10);
$this->dispatcher->addListener('pre.foo', [$listener2, 'preFoo'], 10);
$this->dispatcher->addListener('pre.foo', [$listener3, 'preFoo']);
$expected = [
[$listener2, 'preFoo'],
[$listener3, 'preFoo'],
[$listener1, 'preFoo'],
];
$this->assertSame($expected, $this->dispatcher->getListeners('pre.foo'));
}
public function testGetAllListenersSortsByPriority(): void {
$listener1 = new TestEventListener();
$listener2 = new TestEventListener();
$listener3 = new TestEventListener();
$listener4 = new TestEventListener();
$listener5 = new TestEventListener();
$listener6 = new TestEventListener();
$this->dispatcher->addListener('pre.foo', $listener1, -10);
$this->dispatcher->addListener('pre.foo', $listener2);
$this->dispatcher->addListener('pre.foo', $listener3, 10);
$this->dispatcher->addListener('post.foo', $listener4, -10);
$this->dispatcher->addListener('post.foo', $listener5);
$this->dispatcher->addListener('post.foo', $listener6, 10);
$expected = [
'pre.foo' => [$listener3, $listener2, $listener1],
'post.foo' => [$listener6, $listener5, $listener4],
];
$this->assertSame($expected, $this->dispatcher->getListeners());
}
public function testGetListenerPriority(): void {
$listener1 = new TestEventListener();
$listener2 = new TestEventListener();
$this->dispatcher->addListener('pre.foo', $listener1, -10);
$this->dispatcher->addListener('pre.foo', $listener2);
$this->assertSame(-10, $this->dispatcher->getListenerPriority('pre.foo', $listener1));
$this->assertSame(0, $this->dispatcher->getListenerPriority('pre.foo', $listener2));
$this->assertNull($this->dispatcher->getListenerPriority('pre.bar', $listener2));
$this->assertNull($this->dispatcher->getListenerPriority('pre.foo', function () {
}));
}
public function testDispatch(): void {
$this->dispatcher->addListener('pre.foo', [$this->listener, 'preFoo']);
$this->dispatcher->addListener('post.foo', [$this->listener, 'postFoo']);
$this->dispatcher->dispatch(new Event(), self::PRE_FOO);
$this->assertTrue($this->listener->preFooInvoked);
$this->assertFalse($this->listener->postFooInvoked);
$this->assertInstanceOf(Event::class, $this->dispatcher->dispatch(new Event(), 'no_event'));
$this->assertInstanceOf(Event::class, $this->dispatcher->dispatch(new Event(), self::PRE_FOO));
// Any kind of object can be dispatched, not only instances of Event.
$this->assertInstanceOf(\stdClass::class, $this->dispatcher->dispatch(new \stdClass(), self::PRE_FOO));
$event = new Event();
$return = $this->dispatcher->dispatch($event, self::PRE_FOO);
$this->assertSame($event, $return);
}
public function testDispatchForClosure(): void {
$invoked = 0;
$listener = function () use (&$invoked) {
++$invoked;
};
$this->dispatcher->addListener('pre.foo', $listener);
$this->dispatcher->addListener('post.foo', $listener);
$this->dispatcher->dispatch(new Event(), self::PRE_FOO);
$this->assertEquals(1, $invoked);
}
public function testStopEventPropagation(): void {
$otherListener = new TestEventListener();
// postFoo() stops the propagation, so only one listener should
// be executed
// Manually set priority to enforce $this->listener to be called first
$this->dispatcher->addListener('post.foo', [$this->listener, 'postFoo'], 10);
$this->dispatcher->addListener('post.foo', [$otherListener, 'postFoo']);
$this->dispatcher->dispatch(new Event(), self::POST_FOO);
$this->assertTrue($this->listener->postFooInvoked);
$this->assertFalse($otherListener->postFooInvoked);
}
public function testDispatchByPriority(): void {
$invoked = [];
$listener1 = function () use (&$invoked) {
$invoked[] = '1';
};
$listener2 = function () use (&$invoked) {
$invoked[] = '2';
};
$listener3 = function () use (&$invoked) {
$invoked[] = '3';
};
$this->dispatcher->addListener('pre.foo', $listener1, -10);
$this->dispatcher->addListener('pre.foo', $listener2);
$this->dispatcher->addListener('pre.foo', $listener3, 10);
$this->dispatcher->dispatch(new Event(), self::PRE_FOO);
$this->assertEquals(['3', '2', '1'], $invoked);
}
public function testRemoveListener(): void {
$this->dispatcher->addListener('pre.bar', $this->listener);
$this->assertTrue($this->dispatcher->hasListeners(self::PRE_BAR));
$this->dispatcher->removeListener('pre.bar', $this->listener);
$this->assertFalse($this->dispatcher->hasListeners(self::PRE_BAR));
$this->dispatcher->removeListener('notExists', $this->listener);
}
public function testAddSubscriber(): void {
$eventSubscriber = new TestEventSubscriber();
$this->dispatcher->addSubscriber($eventSubscriber);
$this->assertTrue($this->dispatcher->hasListeners(self::PRE_FOO));
$this->assertTrue($this->dispatcher->hasListeners(self::POST_FOO));
}
public function testAddSubscriberWithPriorities(): void {
$eventSubscriber = new TestEventSubscriber();
$this->dispatcher->addSubscriber($eventSubscriber);
$eventSubscriber = new TestEventSubscriberWithPriorities();
$this->dispatcher->addSubscriber($eventSubscriber);
$listeners = $this->dispatcher->getListeners('pre.foo');
$this->assertTrue($this->dispatcher->hasListeners(self::PRE_FOO));
$this->assertCount(2, $listeners);
$this->assertInstanceOf(TestEventSubscriberWithPriorities::class, $listeners[0][0]);
}
public function testAddSubscriberWithMultipleListeners(): void {
$eventSubscriber = new TestEventSubscriberWithMultipleListeners();
$this->dispatcher->addSubscriber($eventSubscriber);
$listeners = $this->dispatcher->getListeners('pre.foo');
$this->assertTrue($this->dispatcher->hasListeners(self::PRE_FOO));
$this->assertCount(2, $listeners);
$this->assertEquals('preFoo2', $listeners[0][1]);
}
public function testRemoveSubscriber(): void {
$eventSubscriber = new TestEventSubscriber();
$this->dispatcher->addSubscriber($eventSubscriber);
$this->assertTrue($this->dispatcher->hasListeners(self::PRE_FOO));
$this->assertTrue($this->dispatcher->hasListeners(self::POST_FOO));
$this->dispatcher->removeSubscriber($eventSubscriber);
$this->assertFalse($this->dispatcher->hasListeners(self::PRE_FOO));
$this->assertFalse($this->dispatcher->hasListeners(self::POST_FOO));
}
public function testRemoveSubscriberWithPriorities(): void {
$eventSubscriber = new TestEventSubscriberWithPriorities();
$this->dispatcher->addSubscriber($eventSubscriber);
$this->assertTrue($this->dispatcher->hasListeners(self::PRE_FOO));
$this->dispatcher->removeSubscriber($eventSubscriber);
$this->assertFalse($this->dispatcher->hasListeners(self::PRE_FOO));
}
public function testRemoveSubscriberWithMultipleListeners(): void {
$eventSubscriber = new TestEventSubscriberWithMultipleListeners();
$this->dispatcher->addSubscriber($eventSubscriber);
$this->assertTrue($this->dispatcher->hasListeners(self::PRE_FOO));
$this->assertCount(2, $this->dispatcher->getListeners(self::PRE_FOO));
$this->dispatcher->removeSubscriber($eventSubscriber);
$this->assertFalse($this->dispatcher->hasListeners(self::PRE_FOO));
}
public function testEventReceivesTheDispatcherInstanceAsArgument(): void {
$listener = new TestWithDispatcher();
$this->dispatcher->addListener('test', [$listener, 'foo']);
$this->assertNull($listener->name);
$this->assertNull($listener->dispatcher);
$this->dispatcher->dispatch(new Event(), 'test');
$this->assertEquals('test', $listener->name);
$this->assertSame($this->dispatcher, $listener->dispatcher);
}
/**
* @see https://bugs.php.net/bug.php?id=62976
*
* This bug affects:
* - The PHP 5.3 branch for versions < 5.3.18
* - The PHP 5.4 branch for versions < 5.4.8
* - The PHP 5.5 branch is not affected
*/
public function testWorkaroundForPhpBug62976(): void {
$dispatcher = $this->createEventDispatcher();
$dispatcher->addListener('bug.62976', new CallableClass());
$dispatcher->removeListener('bug.62976', function () {
});
$this->assertTrue($dispatcher->hasListeners('bug.62976'));
}
public function testHasListenersWhenAddedCallbackListenerIsRemoved(): void {
$listener = function () {
};
$this->dispatcher->addListener('foo', $listener);
$this->dispatcher->removeListener('foo', $listener);
$this->assertFalse($this->dispatcher->hasListeners());
}
public function testGetListenersWhenAddedCallbackListenerIsRemoved(): void {
$listener = function () {
};
$this->dispatcher->addListener('foo', $listener);
$this->dispatcher->removeListener('foo', $listener);
$this->assertSame([], $this->dispatcher->getListeners());
}
public function testHasListenersWithoutEventsReturnsFalseAfterHasListenersWithEventHasBeenCalled(): void {
$this->assertFalse($this->dispatcher->hasListeners('foo'));
$this->assertFalse($this->dispatcher->hasListeners());
}
public function testHasListenersIsLazy(): void {
$called = 0;
$listener = [
function () use (&$called) {
++$called;
},
'onFoo',
];
$this->dispatcher->addListener('foo', $listener);
$this->assertTrue($this->dispatcher->hasListeners());
$this->assertTrue($this->dispatcher->hasListeners('foo'));
$this->assertSame(0, $called);
}
public function testDispatchLazyListener(): void {
$called = 0;
$factory = function () use (&$called) {
++$called;
return new TestWithDispatcher();
};
$this->dispatcher->addListener('foo', [$factory, 'foo']);
$this->assertSame(0, $called);
$this->dispatcher->dispatch(new Event(), 'foo');
$this->dispatcher->dispatch(new Event(), 'foo');
$this->assertSame(1, $called);
}
public function testRemoveFindsLazyListeners(): void {
$test = new TestWithDispatcher();
$factory = function () use ($test) {
return $test;
};
$this->dispatcher->addListener('foo', [$factory, 'foo']);
$this->assertTrue($this->dispatcher->hasListeners('foo'));
$this->dispatcher->removeListener('foo', [$test, 'foo']);
$this->assertFalse($this->dispatcher->hasListeners('foo'));
$this->dispatcher->addListener('foo', [$test, 'foo']);
$this->assertTrue($this->dispatcher->hasListeners('foo'));
$this->dispatcher->removeListener('foo', [$factory, 'foo']);
$this->assertFalse($this->dispatcher->hasListeners('foo'));
}
public function testPriorityFindsLazyListeners(): void {
$test = new TestWithDispatcher();
$factory = function () use ($test) {
return $test;
};
$this->dispatcher->addListener('foo', [$factory, 'foo'], 3);
$this->assertSame(3, $this->dispatcher->getListenerPriority('foo', [$test, 'foo']));
$this->dispatcher->removeListener('foo', [$factory, 'foo']);
$this->dispatcher->addListener('foo', [$test, 'foo'], 5);
$this->assertSame(5, $this->dispatcher->getListenerPriority('foo', [$factory, 'foo']));
}
public function testGetLazyListeners(): void {
$test = new TestWithDispatcher();
$factory = function () use ($test) {
return $test;
};
$this->dispatcher->addListener('foo', [$factory, 'foo'], 3);
$this->assertSame([[$test, 'foo']], $this->dispatcher->getListeners('foo'));
$this->dispatcher->removeListener('foo', [$test, 'foo']);
$this->dispatcher->addListener('bar', [$factory, 'foo'], 3);
$this->assertSame(['bar' => [[$test, 'foo']]], $this->dispatcher->getListeners());
}
}
class CallableClass {
public function __invoke() {
}
}
class TestEventListener {
public $name;
public $preFooInvoked = FALSE;
public $postFooInvoked = FALSE;
/**
* Listener methods.
*/
public function preFoo(object $e) {
$this->preFooInvoked = TRUE;
}
public function postFoo(Event $e) {
$this->postFooInvoked = TRUE;
$e->stopPropagation();
}
}
class TestWithDispatcher {
public $name;
public $dispatcher;
public function foo(Event $e, $name, $dispatcher) {
$this->name = $name;
$this->dispatcher = $dispatcher;
}
}
class TestEventSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents(): array {
return ['pre.foo' => 'preFoo', 'post.foo' => 'postFoo'];
}
}
class TestEventSubscriberWithPriorities implements EventSubscriberInterface {
public static function getSubscribedEvents(): array {
return [
'pre.foo' => ['preFoo', 10],
'post.foo' => ['postFoo'],
];
}
}
class TestEventSubscriberWithMultipleListeners implements EventSubscriberInterface {
public static function getSubscribedEvents(): array {
return [
'pre.foo' => [
['preFoo1'],
['preFoo2', 10],
],
];
}
}
class SymfonyInheritedEvent extends SymfonyEvent {}

View File

@@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\FileCache;
use Drupal\Component\FileCache\FileCache;
use Drupal\Component\FileCache\NullFileCache;
use Drupal\Component\FileCache\FileCacheFactory;
use Drupal\Component\Utility\Random;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\FileCache\FileCacheFactory
* @group FileCache
*/
class FileCacheFactoryTest extends TestCase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$configuration = [
'test_foo_settings' => [
'collection' => 'test-23',
'cache_backend_class' => '\Drupal\Tests\Component\FileCache\StaticFileCacheBackend',
'cache_backend_configuration' => [
'bin' => 'dog',
],
],
];
FileCacheFactory::setConfiguration($configuration);
FileCacheFactory::setPrefix('prefix');
}
/**
* @covers ::get
*/
public function testGet(): void {
$file_cache = FileCacheFactory::get('test_foo_settings', []);
// Ensure the right backend and configuration is used.
$filename = __DIR__ . '/Fixtures/llama-23.txt';
$realpath = realpath($filename);
$cid = 'prefix:test-23:' . $realpath;
$file_cache->set($filename, 23);
$static_cache = new StaticFileCacheBackend(['bin' => 'dog']);
$result = $static_cache->fetch([$cid]);
$this->assertNotEmpty($result);
// Cleanup static caches.
$file_cache->delete($filename);
}
/**
* @covers ::get
*/
public function testGetNoPrefix(): void {
FileCacheFactory::setPrefix(NULL);
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Required prefix configuration is missing');
FileCacheFactory::get('test_foo_settings', []);
}
/**
* @covers ::get
*/
public function testGetDisabledFileCache(): void {
// Ensure the returned FileCache is an instance of FileCache::class.
$file_cache = FileCacheFactory::get('test_foo_settings', []);
$this->assertInstanceOf(FileCache::class, $file_cache);
$configuration = FileCacheFactory::getConfiguration();
$configuration[FileCacheFactory::DISABLE_CACHE] = TRUE;
FileCacheFactory::setConfiguration($configuration);
// Ensure the returned FileCache is now an instance of NullFileCache::class.
$file_cache = FileCacheFactory::get('test_foo_settings', []);
$this->assertInstanceOf(NullFileCache::class, $file_cache);
}
/**
* @covers ::get
*
* @dataProvider configurationDataProvider
*/
public function testGetConfigurationOverrides($configuration, $arguments, $class): void {
FileCacheFactory::setConfiguration($configuration);
$file_cache = FileCacheFactory::get('test_foo_settings', $arguments);
$this->assertInstanceOf($class, $file_cache);
}
/**
* Data provider for testGetConfigurationOverrides().
*/
public static function configurationDataProvider() {
$data = [];
// Test fallback configuration.
$data['fallback-configuration'] = [
[],
[],
FileCache::class,
];
// Test default configuration.
$data['default-configuration'] = [
['default' => ['class' => CustomFileCache::class]],
[],
CustomFileCache::class,
];
// Test specific per collection setting.
$data['collection-setting'] = [
['test_foo_settings' => ['class' => CustomFileCache::class]],
[],
CustomFileCache::class,
];
// Test default configuration plus specific per collection setting.
$data['default-plus-collection-setting'] = [
[
'default' => ['class' => '\stdClass'],
'test_foo_settings' => ['class' => CustomFileCache::class],
],
[],
CustomFileCache::class,
];
// Test default configuration plus class specific override.
$data['default-plus-class-override'] = [
['default' => ['class' => '\stdClass']],
['class' => CustomFileCache::class],
CustomFileCache::class,
];
// Test default configuration plus class specific override plus specific
// per collection setting.
$data['default-plus-class-plus-collection-setting'] = [
[
'default' => ['class' => '\stdClass'],
'test_foo_settings' => ['class' => CustomFileCache::class],
],
['class' => '\stdClass'],
CustomFileCache::class,
];
return $data;
}
/**
* @covers ::getConfiguration
* @covers ::setConfiguration
*/
public function testGetSetConfiguration(): void {
$configuration = FileCacheFactory::getConfiguration();
$configuration['test_foo_bar'] = ['bar' => 'llama'];
FileCacheFactory::setConfiguration($configuration);
$configuration = FileCacheFactory::getConfiguration();
$this->assertEquals(['bar' => 'llama'], $configuration['test_foo_bar']);
}
/**
* @covers ::getPrefix
* @covers ::setPrefix
*/
public function testGetSetPrefix(): void {
// Random generator.
$random = new Random();
$prefix = $random->name(8, TRUE);
FileCacheFactory::setPrefix($prefix);
$this->assertEquals($prefix, FileCacheFactory::getPrefix());
}
}
class CustomFileCache extends FileCache {}

View File

@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\FileCache;
use Drupal\Component\FileCache\FileCache;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\FileCache\FileCache
* @group FileCache
*/
class FileCacheTest extends TestCase {
/**
* FileCache object used for the tests.
*
* @var \Drupal\Component\FileCache\FileCacheInterface
*/
protected $fileCache;
/**
* Static FileCache object used for verification of tests.
*
* @var \Drupal\Component\FileCache\FileCacheBackendInterface
*/
protected $staticFileCache;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->fileCache = new FileCache('prefix', 'test', '\Drupal\Tests\Component\FileCache\StaticFileCacheBackend', ['bin' => 'llama']);
$this->staticFileCache = new StaticFileCacheBackend(['bin' => 'llama']);
}
/**
* @covers ::get
* @covers ::__construct
*/
public function testGet(): void {
// Test a cache miss.
$result = $this->fileCache->get(__DIR__ . DIRECTORY_SEPARATOR . 'Fixtures' . DIRECTORY_SEPARATOR . 'no-llama-42.yml');
$this->assertNull($result);
// Test a cache hit.
$filename = __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures' . DIRECTORY_SEPARATOR . 'llama-42.txt';
$realpath = realpath($filename);
$cid = 'prefix:test:' . $realpath;
$data = [
'mtime' => filemtime($realpath),
'filepath' => $realpath,
'data' => 42,
];
$this->staticFileCache->store($cid, $data);
$result = $this->fileCache->get($filename);
$this->assertEquals(42, $result);
// Cleanup static caches.
$this->fileCache->delete($filename);
}
/**
* @covers ::getMultiple
*/
public function testGetMultiple(): void {
// Test a cache miss.
$result = $this->fileCache->getMultiple([__DIR__ . DIRECTORY_SEPARATOR . 'Fixtures' . DIRECTORY_SEPARATOR . 'no-llama-42.yml']);
$this->assertEmpty($result);
// Test a cache hit.
$filename = __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures' . DIRECTORY_SEPARATOR . 'llama-42.txt';
$realpath = realpath($filename);
$cid = 'prefix:test:' . $realpath;
$data = [
'mtime' => filemtime($realpath),
'filepath' => $realpath,
'data' => 42,
];
$this->staticFileCache->store($cid, $data);
$result = $this->fileCache->getMultiple([$filename]);
$this->assertEquals([$filename => 42], $result);
// Test a static cache hit.
$file2 = __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures' . DIRECTORY_SEPARATOR . 'llama-23.txt';
$this->fileCache->set($file2, 23);
$result = $this->fileCache->getMultiple([$filename, $file2]);
$this->assertEquals([$filename => 42, $file2 => 23], $result);
// Cleanup static caches.
$this->fileCache->delete($filename);
$this->fileCache->delete($file2);
}
/**
* @covers ::set
*/
public function testSet(): void {
$filename = __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures' . DIRECTORY_SEPARATOR . 'llama-23.txt';
$realpath = realpath($filename);
$cid = 'prefix:test:' . $realpath;
$data = [
'mtime' => filemtime($realpath),
'filepath' => $realpath,
'data' => 23,
];
$this->fileCache->set($filename, 23);
$result = $this->staticFileCache->fetch([$cid]);
$this->assertEquals([$cid => $data], $result);
// Cleanup static caches.
$this->fileCache->delete($filename);
}
/**
* @covers ::delete
*/
public function testDelete(): void {
$filename = __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures' . DIRECTORY_SEPARATOR . 'llama-23.txt';
$realpath = realpath($filename);
$cid = 'prefix:test:' . $realpath;
$this->fileCache->set($filename, 23);
// Ensure data is removed after deletion.
$this->fileCache->delete($filename);
$result = $this->staticFileCache->fetch([$cid]);
$this->assertEquals([], $result);
$result = $this->fileCache->get($filename);
$this->assertNull($result);
}
}

View File

@@ -0,0 +1 @@
23

View File

@@ -0,0 +1 @@
42

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\FileCache;
use Drupal\Component\FileCache\FileCacheBackendInterface;
/**
* Allows to cache data based on file modification dates in a static cache.
*/
class StaticFileCacheBackend implements FileCacheBackendInterface {
/**
* Internal static cache.
*
* @var array
*/
protected static $cache = [];
/**
* Bin used for storing the data in the static cache.
*
* @var string
*/
protected $bin;
/**
* Constructs a PHP Storage FileCache backend.
*
* @param array $configuration
* (optional) Configuration used to configure this object.
*/
public function __construct($configuration) {
$this->bin = $configuration['bin'] ?? 'file_cache';
}
/**
* {@inheritdoc}
*/
public function fetch(array $cids) {
$result = [];
foreach ($cids as $cid) {
if (isset(static::$cache[$this->bin][$cid])) {
$result[$cid] = static::$cache[$this->bin][$cid];
}
}
return $result;
}
/**
* {@inheritdoc}
*/
public function store($cid, $data) {
static::$cache[$this->bin][$cid] = $data;
}
/**
* {@inheritdoc}
*/
public function delete($cid) {
unset(static::$cache[$this->bin][$cid]);
}
/**
* Allows tests to reset the static cache to avoid side effects.
*/
public static function reset() {
static::$cache = [];
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\FileSecurity;
use Drupal\Component\FileSecurity\FileSecurity;
use org\bovigo\vfs\vfsStream;
use PHPUnit\Framework\TestCase;
/**
* Tests the file security component.
*
* @coversDefaultClass \Drupal\Component\FileSecurity\FileSecurity
* @group FileSecurity
*/
class FileSecurityTest extends TestCase {
/**
* @covers ::writeHtaccess
*/
public function testWriteHtaccessPrivate(): void {
vfsStream::setup('root');
FileSecurity::writeHtaccess(vfsStream::url('root'));
$htaccess_file = vfsStream::url('root') . '/.htaccess';
$this->assertFileExists($htaccess_file);
$this->assertEquals('0444', substr(sprintf('%o', fileperms($htaccess_file)), -4));
$htaccess_contents = file_get_contents($htaccess_file);
$this->assertStringContainsString("Require all denied", $htaccess_contents);
}
/**
* @covers ::writeHtaccess
*/
public function testWriteHtaccessPublic(): void {
vfsStream::setup('root');
$this->assertTrue(FileSecurity::writeHtaccess(vfsStream::url('root'), FALSE));
$htaccess_file = vfsStream::url('root') . '/.htaccess';
$this->assertFileExists($htaccess_file);
$this->assertEquals('0444', substr(sprintf('%o', fileperms($htaccess_file)), -4));
$htaccess_contents = file_get_contents($htaccess_file);
$this->assertStringNotContainsString("Require all denied", $htaccess_contents);
}
/**
* @covers ::writeHtaccess
*/
public function testWriteHtaccessForceOverwrite(): void {
vfsStream::setup('root');
$htaccess_file = vfsStream::url('root') . '/.htaccess';
file_put_contents($htaccess_file, "foo");
$this->assertTrue(FileSecurity::writeHtaccess(vfsStream::url('root'), TRUE, TRUE));
$htaccess_contents = file_get_contents($htaccess_file);
$this->assertStringContainsString("Require all denied", $htaccess_contents);
$this->assertStringNotContainsString("foo", $htaccess_contents);
}
/**
* @covers ::writeHtaccess
*/
public function testWriteHtaccessFailure(): void {
vfsStream::setup('root');
$this->assertFalse(FileSecurity::writeHtaccess(vfsStream::url('root') . '/foo'));
}
/**
* @covers ::writeWebConfig
*/
public function testWriteWebConfig(): void {
vfsStream::setup('root');
$this->assertTrue(FileSecurity::writeWebConfig(vfsStream::url('root')));
$web_config_file = vfsStream::url('root') . '/web.config';
$this->assertFileExists($web_config_file);
$this->assertEquals('0444', substr(sprintf('%o', fileperms($web_config_file)), -4));
}
/**
* @covers ::writeWebConfig
*/
public function testWriteWebConfigForceOverwrite(): void {
vfsStream::setup('root');
$web_config_file = vfsStream::url('root') . '/web.config';
file_put_contents($web_config_file, "foo");
$this->assertTrue(FileSecurity::writeWebConfig(vfsStream::url('root'), TRUE));
$this->assertFileExists($web_config_file);
$this->assertEquals('0444', substr(sprintf('%o', fileperms($web_config_file)), -4));
$this->assertStringNotContainsString("foo", $web_config_file);
}
/**
* @covers ::writeWebConfig
*/
public function testWriteWebConfigFailure(): void {
vfsStream::setup('root');
$this->assertFalse(FileSecurity::writeWebConfig(vfsStream::url('root') . '/foo'));
}
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\FileSystem;
use Drupal\Component\FileSystem\RegexDirectoryIterator;
use org\bovigo\vfs\vfsStream;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\FileSystem\RegexDirectoryIterator
* @group FileSystem
*/
class RegexDirectoryIteratorTest extends TestCase {
/**
* @covers ::accept
* @dataProvider providerTestRegexDirectoryIterator
*/
public function testRegexDirectoryIterator(array $directory, $regex, array $expected): void {
vfsStream::setup('root', NULL, $directory);
$iterator = new RegexDirectoryIterator(vfsStream::url('root'), $regex);
// Create an array of filenames to assert against.
$file_list = array_map(function (\SplFileInfo $file) {
return $file->getFilename();
}, array_values(iterator_to_array($iterator)));
$this->assertSame($expected, $file_list);
}
/**
* Provider for self::testRegexDirectoryIterator().
*/
public static function providerTestRegexDirectoryIterator() {
return [
[
[
'1.yml' => '',
],
'/\.yml$/',
[
'1.yml',
],
],
[
[
'1.yml' => '',
'2.yml' => '',
'3.txt' => '',
],
'/\.yml$/',
[
'1.yml',
'2.yml',
],
],
[
[
'1.yml' => '',
'2.yml' => '',
'3.txt' => '',
],
'/\.txt/',
[
'3.txt',
],
],
[
[
'1.yml' => '',
// Ensure we don't recurse in directories even if that match the
// regex.
'2.yml' => [
'3.yml' => '',
'4.yml' => '',
],
'3.txt' => '',
],
'/\.yml$/',
[
'1.yml',
],
],
[
[
'1.yml' => '',
'2.yml' => '',
'3.txt' => '',
],
'/^\d/',
[
'1.yml',
'2.yml',
'3.txt',
],
],
[
[
'1.yml' => '',
'2.yml' => '',
'3.txt' => '',
],
'/^\D/',
[],
],
];
}
}

View File

@@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\FrontMatter;
use Drupal\Component\FrontMatter\Exception\FrontMatterParseException;
use Drupal\Component\FrontMatter\FrontMatter;
use Drupal\Component\Serialization\Yaml;
use PHPUnit\Framework\TestCase;
/**
* Tests front matter parsing helper methods.
*
* @group FrontMatter
*
* @coversDefaultClass \Drupal\Component\FrontMatter\FrontMatter
*/
class FrontMatterTest extends TestCase {
/**
* A basic source string.
*/
const SOURCE = '<div>Hello world</div>';
/**
* Creates a front matter source string.
*
* @param array|null $yaml
* The YAML array to prepend as a front matter block.
* @param string $content
* The source contents.
*
* @return string
* The new source.
*/
public static function createFrontMatterSource(?array $yaml, string $content = self::SOURCE): string {
// Encode YAML and wrap in a front matter block.
$frontMatter = '';
if (is_array($yaml)) {
$yaml = $yaml ? trim(Yaml::encode($yaml)) . "\n" : '';
$frontMatter = FrontMatter::SEPARATOR . "\n$yaml" . FrontMatter::SEPARATOR . "\n";
}
return $frontMatter . $content;
}
/**
* Tests when a passed serializer doesn't implement the proper interface.
*
* @covers ::__construct
* @covers ::create
*/
public function testFrontMatterSerializerException(): void {
$this->expectException(\AssertionError::class);
$this->expectExceptionMessage('The $serializer parameter must reference a class that implements Drupal\Component\Serialization\SerializationInterface.');
FrontMatter::create('', '');
}
/**
* Tests broken front matter.
*
* @covers ::__construct
* @covers ::create
* @covers ::parse
* @covers \Drupal\Component\FrontMatter\Exception\FrontMatterParseException
*/
public function testFrontMatterBroken(): void {
$this->expectException(FrontMatterParseException::class);
$this->expectExceptionMessage('An error occurred when attempting to parse front matter data on line 4');
$source = "---\ncollection:\n- key: foo\n foo: bar\n---\n";
FrontMatter::create($source)->getData();
}
/**
* Tests the parsed data from front matter.
*
* @param array|null $yaml
* The YAML used as front matter data to prepend the source.
* @param int $line
* The expected line number where the source code starts.
* @param string $content
* The content to use for testing purposes.
*
* @covers ::__construct
* @covers ::getContent
* @covers ::getData
* @covers ::getLine
* @covers ::create
* @covers ::parse
*
* @dataProvider providerFrontMatterData
*/
public function testFrontMatterData($yaml, $line, $content = self::SOURCE): void {
$source = static::createFrontMatterSource($yaml, $content);
$frontMatter = FrontMatter::create($source);
$this->assertEquals($content, $frontMatter->getContent());
$this->assertEquals($yaml ?? [], $frontMatter->getData());
$this->assertEquals($line, $frontMatter->getLine());
}
/**
* Provides the front matter data to test.
*
* @return array
* Array of front matter data.
*/
public static function providerFrontMatterData() {
$data['none'] = [
'yaml' => NULL,
'line' => 1,
];
$data['scalar'] = [
'yaml' => [
'string' => 'value',
'number' => 42,
'bool' => TRUE,
'null' => NULL,
],
'line' => 7,
];
$data['indexed_arrays'] = [
'yaml' => [
'brackets' => [1, 2, 3],
'items' => [
'item1',
'item2',
'item3',
],
],
'line' => 11,
];
$data['associative_arrays'] = [
'yaml' => [
'brackets' => [
'a' => 1,
'b' => 2,
'c' => 3,
],
'items' => [
'a' => 'item1',
'b' => 'item2',
'c' => 'item3',
],
],
'line' => 11,
];
$data['empty_data'] = [
'yaml' => [],
'line' => 3,
];
$data['empty_content'] = [
'yaml' => ['key' => 'value'],
'line' => 4,
'content' => '',
];
$data['empty_data_and_content'] = [
'yaml' => [],
'line' => 3,
'content' => '',
];
$data['empty_string'] = [
'yaml' => NULL,
'line' => 1,
'content' => '',
];
$data['multiple_separators'] = [
'yaml' => ['key' => '---'],
'line' => 4,
'content' => "Something\n---\nSomething more",
];
return $data;
}
}

View File

@@ -0,0 +1,373 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Gettext;
use Drupal\Component\Gettext\PoHeader;
use PHPUnit\Framework\TestCase;
/**
* Unit tests for the Gettext PO file header handling features.
*
* @see Drupal\Component\Gettext\PoHeader.
*
* @group Gettext
*/
class PoHeaderTest extends TestCase {
/**
* Tests that plural expressions are evaluated correctly.
*
* Validate that the given plural expressions is evaluated with the correct
* plural formula.
*
* @param string $plural
* The plural expression.
* @param array $expected
* Array of expected plural positions keyed by plural value.
*
* @dataProvider providerTestPluralsFormula
*/
public function testPluralsFormula($plural, $expected): void {
$p = new PoHeader();
$parsed = $p->parsePluralForms($plural);
[$nplurals, $new_plural] = $parsed;
foreach ($expected as $number => $plural_form) {
$result = $new_plural[$number] ?? $new_plural['default'];
$this->assertEquals($result, $plural_form, 'Difference found at ' . $number . ': ' . $plural_form . ' versus ' . $result);
}
}
/**
* Data provider for testPluralsFormula.
*
* Gets pairs of plural expressions and expected plural positions keyed by
* plural value.
*
* @return array
* Pairs of plural expressions and expected plural positions keyed by plural
* value.
*/
public static function providerTestPluralsFormula() {
return [
[
'nplurals=1; plural=0;',
['default' => 0],
],
[
'nplurals=2; plural=(n > 1);',
[0 => 0, 1 => 0, 'default' => 1],
],
[
'nplurals=2; plural=(n!=1);',
[1 => 0, 'default' => 1],
],
[
'nplurals=2; plural=(((n==1)||((n%10)==1))?(0):1);',
[
1 => 0,
11 => 0,
21 => 0,
31 => 0,
41 => 0,
51 => 0,
61 => 0,
71 => 0,
81 => 0,
91 => 0,
101 => 0,
111 => 0,
121 => 0,
131 => 0,
141 => 0,
151 => 0,
161 => 0,
171 => 0,
181 => 0,
191 => 0,
'default' => 1,
],
],
[
'nplurals=3; plural=((((n%10)==1)&&((n%100)!=11))?(0):(((((n%10)>=2)&&((n%10)<=4))&&(((n%100)<10)||((n%100)>=20)))?(1):2));',
[
1 => 0,
2 => 1,
3 => 1,
4 => 1,
21 => 0,
22 => 1,
23 => 1,
24 => 1,
31 => 0,
32 => 1,
33 => 1,
34 => 1,
41 => 0,
42 => 1,
43 => 1,
44 => 1,
51 => 0,
52 => 1,
53 => 1,
54 => 1,
61 => 0,
62 => 1,
63 => 1,
64 => 1,
71 => 0,
72 => 1,
73 => 1,
74 => 1,
81 => 0,
82 => 1,
83 => 1,
84 => 1,
91 => 0,
92 => 1,
93 => 1,
94 => 1,
101 => 0,
102 => 1,
103 => 1,
104 => 1,
121 => 0,
122 => 1,
123 => 1,
124 => 1,
131 => 0,
132 => 1,
133 => 1,
134 => 1,
141 => 0,
142 => 1,
143 => 1,
144 => 1,
151 => 0,
152 => 1,
153 => 1,
154 => 1,
161 => 0,
162 => 1,
163 => 1,
164 => 1,
171 => 0,
172 => 1,
173 => 1,
174 => 1,
181 => 0,
182 => 1,
183 => 1,
184 => 1,
191 => 0,
192 => 1,
193 => 1,
194 => 1,
'default' => 2,
],
],
[
'nplurals=3; plural=((n==1)?(0):(((n>=2)&&(n<=4))?(1):2));',
[
1 => 0,
2 => 1,
3 => 1,
4 => 1,
'default' => 2,
],
],
[
'nplurals=3; plural=((n==1)?(0):(((n==0)||(((n%100)>0)&&((n%100)<20)))?(1):2));',
[
0 => 1,
1 => 0,
2 => 1,
3 => 1,
4 => 1,
5 => 1,
6 => 1,
7 => 1,
8 => 1,
9 => 1,
10 => 1,
11 => 1,
12 => 1,
13 => 1,
14 => 1,
15 => 1,
16 => 1,
17 => 1,
18 => 1,
19 => 1,
101 => 1,
102 => 1,
103 => 1,
104 => 1,
105 => 1,
106 => 1,
107 => 1,
108 => 1,
109 => 1,
110 => 1,
111 => 1,
112 => 1,
113 => 1,
114 => 1,
115 => 1,
116 => 1,
117 => 1,
118 => 1,
119 => 1,
'default' => 2,
],
],
[
'nplurals=3; plural=((n==1)?(0):(((((n%10)>=2)&&((n%10)<=4))&&(((n%100)<10)||((n%100)>=20)))?(1):2));',
[
1 => 0,
2 => 1,
3 => 1,
4 => 1,
22 => 1,
23 => 1,
24 => 1,
32 => 1,
33 => 1,
34 => 1,
42 => 1,
43 => 1,
44 => 1,
52 => 1,
53 => 1,
54 => 1,
62 => 1,
63 => 1,
64 => 1,
72 => 1,
73 => 1,
74 => 1,
82 => 1,
83 => 1,
84 => 1,
92 => 1,
93 => 1,
94 => 1,
102 => 1,
103 => 1,
104 => 1,
122 => 1,
123 => 1,
124 => 1,
132 => 1,
133 => 1,
134 => 1,
142 => 1,
143 => 1,
144 => 1,
152 => 1,
153 => 1,
154 => 1,
162 => 1,
163 => 1,
164 => 1,
172 => 1,
173 => 1,
174 => 1,
182 => 1,
183 => 1,
184 => 1,
192 => 1,
193 => 1,
194 => 1,
'default' => 2,
],
],
[
'nplurals=4; plural=(((n==1)||(n==11))?(0):(((n==2)||(n==12))?(1):(((n>2)&&(n<20))?(2):3)));',
[
1 => 0,
2 => 1,
3 => 2,
4 => 2,
5 => 2,
6 => 2,
7 => 2,
8 => 2,
9 => 2,
10 => 2,
11 => 0,
12 => 1,
13 => 2,
14 => 2,
15 => 2,
16 => 2,
17 => 2,
18 => 2,
19 => 2,
'default' => 3,
],
],
[
'nplurals=4; plural=(((n%100)==1)?(0):(((n%100)==2)?(1):((((n%100)==3)||((n%100)==4))?(2):3)));',
[
1 => 0,
2 => 1,
3 => 2,
4 => 2,
101 => 0,
102 => 1,
103 => 2,
104 => 2,
'default' => 3,
],
],
[
'nplurals=5; plural=((n==1)?(0):((n==2)?(1):((n<7)?(2):((n<11)?(3):4))));',
[
0 => 2,
1 => 0,
2 => 1,
3 => 2,
4 => 2,
5 => 2,
6 => 2,
7 => 3,
8 => 3,
9 => 3,
10 => 3,
'default' => 4,
],
],
[
'nplurals=6; plural=((n==1)?(0):((n==0)?(1):((n==2)?(2):((((n%100)>=3)&&((n%100)<=10))?(3):((((n%100)>=11)&&((n%100)<=99))?(4):5)))));',
[
0 => 1,
1 => 0,
2 => 2,
3 => 3,
4 => 3,
5 => 3,
6 => 3,
7 => 3,
8 => 3,
9 => 3,
10 => 3,
100 => 5,
101 => 5,
102 => 5,
103 => 3,
104 => 3,
105 => 3,
106 => 3,
107 => 3,
108 => 3,
109 => 3,
110 => 3,
'default' => 4,
],
],
];
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Gettext;
use Drupal\Component\Gettext\PoHeader;
use Drupal\Component\Gettext\PoItem;
use Drupal\Component\Gettext\PoStreamWriter;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamFile;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
/**
* @coversDefaultClass \Drupal\Component\Gettext\PoStreamWriter
* @group Gettext
*/
class PoStreamWriterTest extends TestCase {
use ProphecyTrait;
/**
* The PO writer object under test.
*
* @var \Drupal\Component\Gettext\PoStreamWriter
*/
protected $poWriter;
/**
* The mock po file.
*
* @var \org\bovigo\vfs\vfsStreamFile
*/
protected $poFile;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$poHeader = $this->prophesize(PoHeader::class);
$poHeader->__toString()->willReturn('');
$this->poWriter = new PoStreamWriter();
$this->poWriter->setHeader($poHeader->reveal());
$root = vfsStream::setup();
$this->poFile = new vfsStreamFile('poWriter.po');
$root->addChild($this->poFile);
}
/**
* @covers ::getURI
*/
public function testGetUriException(): void {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('No URI set.');
$this->poWriter->getURI();
}
/**
* @covers ::writeItem
* @dataProvider providerWriteData
*/
public function testWriteItem($poContent, $expected, $long): void {
if ($long) {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Unable to write data:');
}
// Limit the file system quota to make the write fail on long strings.
vfsStream::setQuota(10);
$this->poWriter->setURI($this->poFile->url());
$this->poWriter->open();
$poItem = $this->prophesize(PoItem::class);
$poItem->__toString()->willReturn($poContent);
$this->poWriter->writeItem($poItem->reveal());
$this->poWriter->close();
$this->assertEquals(file_get_contents($this->poFile->url()), $expected);
}
/**
* @return array
* - Content to write.
* - Written content.
* - Content longer than 10 bytes.
*/
public static function providerWriteData() {
// cSpell:disable
return [
['', '', FALSE],
["\r\n", "\r\n", FALSE],
['write this if you can', 'write this', TRUE],
['éáíó>&', 'éáíó>&', FALSE],
['éáíó>&<', 'éáíó>&', TRUE],
['中文 890', '中文 890', FALSE],
['中文 89012', '中文 890', TRUE],
];
// cSpell:enable
}
/**
* @covers ::close
*/
public function testCloseException(): void {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Cannot close stream that is not open.');
$this->poWriter->close();
}
}

View File

@@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Graph;
use Drupal\Component\Graph\Graph;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Graph\Graph
* @group Graph
*/
class GraphTest extends TestCase {
/**
* Tests depth-first-search features.
*/
public function testDepthFirstSearch(): void {
// The sample graph used is:
// @code
// 1 --> 2 --> 3 5 ---> 6
// | ^ ^
// | | |
// | | |
// +---> 4 <-- 7 8 ---> 9
// @endcode
$graph = $this->normalizeGraph([
1 => [2],
2 => [3, 4],
3 => [],
4 => [3],
5 => [6],
7 => [4, 5],
8 => [9],
9 => [],
]);
$graph_object = new Graph($graph);
$graph = $graph_object->searchAndSort();
$expected_paths = [
1 => [2, 3, 4],
2 => [3, 4],
3 => [],
4 => [3],
5 => [6],
7 => [4, 3, 5, 6],
8 => [9],
9 => [],
];
$this->assertPaths($graph, $expected_paths);
$expected_reverse_paths = [
1 => [],
2 => [1],
3 => [2, 1, 4, 7],
4 => [2, 1, 7],
5 => [7],
7 => [],
8 => [],
9 => [8],
];
$this->assertReversePaths($graph, $expected_reverse_paths);
// Assert that DFS didn't created "missing" vertexes automatically.
$this->assertFalse(isset($graph[6]), 'Vertex 6 has not been created');
$expected_components = [
[1, 2, 3, 4, 5, 7],
[8, 9],
];
$this->assertComponents($graph, $expected_components);
$expected_weights = [
[1, 2, 3],
[2, 4, 3],
[7, 4, 3],
[7, 5],
[8, 9],
];
$this->assertWeights($graph, $expected_weights);
}
/**
* Normalizes a graph.
*
* @param $graph
* A graph array processed by \Drupal\Component\Graph\Graph::searchAndSort()
*
* @return array
* The normalized version of a graph.
*/
protected function normalizeGraph($graph) {
$normalized_graph = [];
foreach ($graph as $vertex => $edges) {
// Create vertex even if it hasn't any edges.
$normalized_graph[$vertex] = [];
foreach ($edges as $edge) {
$normalized_graph[$vertex]['edges'][$edge] = TRUE;
}
}
return $normalized_graph;
}
/**
* Verify expected paths in a graph.
*
* @param array $graph
* A graph array processed by \Drupal\Component\Graph\Graph::searchAndSort()
* @param array $expected_paths
* An associative array containing vertices with their expected paths.
*
* @internal
*/
protected function assertPaths(array $graph, array $expected_paths): void {
foreach ($expected_paths as $vertex => $paths) {
// Build an array with keys = $paths and values = TRUE.
$expected = array_fill_keys($paths, TRUE);
$result = $graph[$vertex]['paths'] ?? [];
$this->assertEquals($expected, $result, sprintf('Expected paths for vertex %s: %s, got %s', $vertex, $this->displayArray($expected, TRUE), $this->displayArray($result, TRUE)));
}
}
/**
* Verify expected reverse paths in a graph.
*
* @param array $graph
* A graph array processed by \Drupal\Component\Graph\Graph::searchAndSort()
* @param array $expected_reverse_paths
* An associative array containing vertices with their expected reverse
* paths.
*
* @internal
*/
protected function assertReversePaths(array $graph, array $expected_reverse_paths): void {
foreach ($expected_reverse_paths as $vertex => $paths) {
// Build an array with keys = $paths and values = TRUE.
$expected = array_fill_keys($paths, TRUE);
$result = $graph[$vertex]['reverse_paths'] ?? [];
$this->assertEquals($expected, $result, sprintf('Expected reverse paths for vertex %s: %s, got %s', $vertex, $this->displayArray($expected, TRUE), $this->displayArray($result, TRUE)));
}
}
/**
* Verify expected components in a graph.
*
* @param array $graph
* A graph array processed by \Drupal\Component\Graph\Graph::searchAndSort().
* @param array $expected_components
* An array containing of components defined as a list of their vertices.
*
* @internal
*/
protected function assertComponents(array $graph, array $expected_components): void {
$unassigned_vertices = array_fill_keys(array_keys($graph), TRUE);
foreach ($expected_components as $component) {
$result_components = [];
foreach ($component as $vertex) {
$result_components[] = $graph[$vertex]['component'];
unset($unassigned_vertices[$vertex]);
}
$this->assertCount(1, array_unique($result_components), sprintf('Expected one unique component for vertices %s, got %s', $this->displayArray($component), $this->displayArray($result_components)));
}
$this->assertEquals([], $unassigned_vertices, sprintf('Vertices not assigned to a component: %s', $this->displayArray($unassigned_vertices, TRUE)));
}
/**
* Verify expected order in a graph.
*
* @param array $graph
* A graph array processed by \Drupal\Component\Graph\Graph::searchAndSort()
* @param array $expected_orders
* An array containing lists of vertices in their expected order.
*
* @internal
*/
protected function assertWeights(array $graph, array $expected_orders): void {
foreach ($expected_orders as $order) {
$previous_vertex = array_shift($order);
foreach ($order as $vertex) {
$this->assertLessThan($graph[$vertex]['weight'], $graph[$previous_vertex]['weight'], sprintf("Weight of vertex %s should be less than vertex %s.", $previous_vertex, $vertex));
}
}
}
/**
* Helper function to output vertices as comma-separated list.
*
* @param $paths
* An array containing a list of vertices.
* @param $keys
* (optional) Whether to output the keys of $paths instead of the values.
*/
protected function displayArray($paths, $keys = FALSE) {
if (!empty($paths)) {
return implode(', ', $keys ? array_keys($paths) : $paths);
}
else {
return '(empty)';
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\HttpFoundation;
use Drupal\Component\HttpFoundation\SecuredRedirectResponse;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Test secure redirect base class.
*
* @group Routing
* @coversDefaultClass \Drupal\Component\HttpFoundation\SecuredRedirectResponse
*/
class SecuredRedirectResponseTest extends TestCase {
/**
* Tests copying of redirect response.
*
* @covers ::createFromRedirectResponse
* @covers ::fromResponse
*/
public function testRedirectCopy(): void {
$redirect = new RedirectResponse('/magic_redirect_url', 301, ['x-cache-foobar' => 123]);
$redirect->setProtocolVersion('2.0');
$redirect->setCharset('ibm-943_P14A-2000');
$redirect->headers->setCookie(new Cookie('name', 'value', 0, '/', NULL, FALSE, TRUE, FALSE, NULL));
// Make a cloned redirect.
$secureRedirect = SecuredRedirectStub::createFromRedirectResponse($redirect);
$this->assertEquals('/magic_redirect_url', $secureRedirect->getTargetUrl());
$this->assertEquals(301, $secureRedirect->getStatusCode());
// We pull the headers from the original redirect because there are default headers applied.
$headers1 = $redirect->headers->all();
$headers2 = $secureRedirect->headers->all();
$this->assertEquals($headers1, $headers2);
$this->assertEquals('2.0', $secureRedirect->getProtocolVersion());
$this->assertEquals('ibm-943_P14A-2000', $secureRedirect->getCharset());
$this->assertEquals($redirect->headers->getCookies(), $secureRedirect->headers->getCookies());
}
}
class SecuredRedirectStub extends SecuredRedirectResponse {
/**
* {@inheritdoc}
*/
protected function isSafe($url) {
// Empty implementation for testing.
return TRUE;
}
}

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\PhpStorage;
use Drupal\Component\PhpStorage\FileStorage;
use Drupal\Component\PhpStorage\FileReadOnlyStorage;
use Drupal\Component\Utility\Random;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
/**
* @coversDefaultClass \Drupal\Component\PhpStorage\FileReadOnlyStorage
*
* @group Drupal
* @group PhpStorage
*/
class FileStorageReadOnlyTest extends PhpStorageTestBase {
use ExpectDeprecationTrait;
/**
* Standard test settings to pass to storage instances.
*
* @var array
*/
protected $standardSettings;
/**
* Read only test settings to pass to storage instances.
*
* @var array
*/
protected $readonlyStorage;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->standardSettings = [
'directory' => $this->directory,
'bin' => 'test',
];
$this->readonlyStorage = [
'directory' => $this->directory,
// Let this read from the bin where the other instance is writing.
'bin' => 'test',
];
}
/**
* Tests writing with one class and reading with another.
*/
public function testReadOnly(): void {
// Random generator.
$random = new Random();
$php = new FileStorage($this->standardSettings);
$name = $random->name(8, TRUE) . '/' . $random->name(8, TRUE) . '.php';
// Find a global that doesn't exist.
do {
$random = 'test' . mt_rand(10000, 100000);
} while (isset($GLOBALS[$random]));
// Write out a PHP file and ensure it's successfully loaded.
$code = "<?php\n\$GLOBALS['$random'] = TRUE;";
$success = $php->save($name, $code);
$this->assertTrue($success);
$php_read = new FileReadOnlyStorage($this->readonlyStorage);
$php_read->load($name);
$this->assertTrue($GLOBALS[$random]);
// If the file was successfully loaded, it must also exist, but ensure the
// exists() method returns that correctly.
$this->assertTrue($php_read->exists($name));
// Saving and deleting should always fail.
$this->assertFalse($php_read->save($name, $code));
$this->assertFalse($php_read->delete($name));
unset($GLOBALS[$random]);
}
/**
* @covers ::writeable
* @group legacy
*/
public function testWritable(): void {
$this->expectDeprecation('Drupal\Component\PhpStorage\FileReadOnlyStorage::writeable() is deprecated in drupal:10.1.0 and will be removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3155413');
$php_read = new FileReadOnlyStorage($this->readonlyStorage);
$this->assertFalse($php_read->writeable());
}
/**
* @covers ::deleteAll
*/
public function testDeleteAll(): void {
// Random generator.
$random = new Random();
$php = new FileStorage($this->standardSettings);
$name = $random->name(8, TRUE) . '/' . $random->name(8, TRUE) . '.php';
// Find a global that doesn't exist.
do {
$random = mt_rand(10000, 100000);
} while (isset($GLOBALS[$random]));
// Write our the file so we can test deleting.
$code = "<?php\n\$GLOBALS[$random] = TRUE;";
$this->assertTrue($php->save($name, $code));
$php_read = new FileReadOnlyStorage($this->readonlyStorage);
$this->assertFalse($php_read->deleteAll());
// Make sure directory exists prior to removal.
$this->assertDirectoryExists($this->directory . '/test');
}
}

View File

@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\PhpStorage;
use Drupal\Component\PhpStorage\FileStorage;
use Drupal\Component\Utility\Random;
use Drupal\Tests\Traits\PhpUnitWarnings;
use org\bovigo\vfs\vfsStreamDirectory;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
/**
* @coversDefaultClass \Drupal\Component\PhpStorage\FileStorage
* @group Drupal
* @group PhpStorage
*/
class FileStorageTest extends PhpStorageTestBase {
use PhpUnitWarnings, ExpectDeprecationTrait;
/**
* Standard test settings to pass to storage instances.
*
* @var array
*/
protected $standardSettings;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->standardSettings = [
'directory' => $this->directory,
'bin' => 'test',
];
}
/**
* Tests basic load/save/delete operations.
*
* @covers ::load
* @covers ::save
* @covers ::exists
* @covers ::delete
*/
public function testCRUD(): void {
$php = new FileStorage($this->standardSettings);
$this->assertCRUD($php);
}
/**
* @covers ::writeable
* @group legacy
*/
public function testWritable(): void {
$this->expectDeprecation('Drupal\Component\PhpStorage\FileStorage::writeable() is deprecated in drupal:10.1.0 and will be removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3155413');
$php = new FileStorage($this->standardSettings);
$this->assertTrue($php->writeable());
}
/**
* @covers ::deleteAll
*/
public function testDeleteAll(): void {
// Random generator.
$random_generator = new Random();
// Write out some files.
$php = new FileStorage($this->standardSettings);
$name = $random_generator->name(8, TRUE) . '/' . $random_generator->name(8, TRUE) . '.php';
// Find a global that doesn't exist.
do {
$random = 'test' . mt_rand(10000, 100000);
} while (isset($GLOBALS[$random]));
// Write out a PHP file and ensure it's successfully loaded.
$code = "<?php\n\$GLOBALS['$random'] = TRUE;";
$this->assertTrue($php->save($name, $code), 'Saved php file');
$php->load($name);
$this->assertTrue($GLOBALS[$random], 'File saved correctly with correct value');
// Make sure directory exists prior to removal.
$this->assertDirectoryExists($this->directory . '/test');
$this->assertTrue($php->deleteAll(), 'Delete all reported success');
$this->assertFalse($php->load($name));
$this->assertDirectoryDoesNotExist($this->directory . '/test');
// Should still return TRUE if directory has already been deleted.
$this->assertTrue($php->deleteAll(), 'Delete all succeeds with nothing to delete');
unset($GLOBALS[$random]);
}
/**
* @covers ::createDirectory
*/
public function testCreateDirectoryFailWarning(): void {
$directory = new vfsStreamDirectory('permissionDenied', 0200);
$storage = new FileStorage([
'directory' => $directory->url(),
'bin' => 'test',
]);
$code = "<?php\n echo 'here';";
// PHPUnit 10 cannot expect warnings, so we have to catch them ourselves.
$messages = [];
set_error_handler(function (int $errno, string $errstr) use (&$messages): void {
$messages[] = [$errno, $errstr];
});
$storage->save('subdirectory/foo.php', $code);
restore_error_handler();
$this->assertCount(2, $messages);
$this->assertSame(E_USER_WARNING, $messages[0][0]);
$this->assertSame('mkdir(): Permission Denied', $messages[0][1]);
$this->assertSame(E_WARNING, $messages[1][0]);
$this->assertStringStartsWith('file_put_contents(vfs://permissionDenied/test/subdirectory/foo.php)', $messages[1][1]);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\PhpStorage;
/**
* Tests the MTimeProtectedFastFileStorage implementation.
*
* @coversDefaultClass \Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage
*
* @group Drupal
* @group PhpStorage
*/
class MTimeProtectedFastFileStorageTest extends MTimeProtectedFileStorageBase {
/**
* The expected test results for the security test.
*
* The first iteration does not change the directory mtime so this class will
* include the hacked file on the first try but the second test will change
* the directory mtime and so on the second try the file will not be included.
*/
protected array $expected = [TRUE, FALSE];
/**
* The PHP storage class to test.
*/
protected $storageClass = 'Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage';
}

View File

@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\PhpStorage;
use Drupal\Component\FileSecurity\FileSecurity;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Random;
/**
* Base test class for MTime protected storage.
*/
abstract class MTimeProtectedFileStorageBase extends PhpStorageTestBase {
/**
* The PHP storage class to test.
*
* This should be overridden by extending classes.
*
* @var string
*/
protected $storageClass;
/**
* The secret string to use for file creation.
*
* @var string
*/
protected $secret;
/**
* Test settings to pass to storage instances.
*
* @var array
*/
protected $settings;
/**
* The expected test results for the security test.
*/
protected array $expected;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Random generator.
$random = new Random();
$this->secret = $random->name(8, TRUE);
$this->settings = [
'directory' => $this->directory,
'bin' => 'test',
'secret' => $this->secret,
];
}
/**
* Tests basic load/save/delete operations.
*/
public function testCRUD(): void {
$php = new $this->storageClass($this->settings);
$this->assertCRUD($php);
}
/**
* Tests the security of the MTimeProtectedFileStorage implementation.
*
* We test two attacks: first changes the file mtime, then the directory
* mtime too.
*
* We need to delay over 1 second for mtime test.
* @medium
*/
public function testSecurity(): void {
$php = new $this->storageClass($this->settings);
$name = 'test.php';
$php->save($name, '<?php');
$expected_root_directory = $this->directory . '/test';
if (str_ends_with($name, '.php')) {
$expected_directory = $expected_root_directory . '/' . substr($name, 0, -4);
}
else {
$expected_directory = $expected_root_directory . '/' . $name;
}
$directory_mtime = filemtime($expected_directory);
$expected_filename = $expected_directory . '/' . Crypt::hmacBase64($name, $this->secret . $directory_mtime) . '.php';
// Ensure the file exists and that it and the containing directory have
// minimal permissions. fileperms() can return high bits unrelated to
// permissions, so mask with 0777.
$this->assertFileExists($expected_filename);
$this->assertSame(0444, fileperms($expected_filename) & 0777);
$this->assertSame(0777, fileperms($expected_directory) & 0777);
// Ensure the root directory for the bin has a .htaccess file denying web
// access.
$this->assertSame(file_get_contents($expected_root_directory . '/.htaccess'), FileSecurity::htaccessLines());
// Ensure that if the file is replaced with an untrusted one (due to another
// script's file upload vulnerability), it does not get loaded. Since mtime
// granularity is 1 second, we cannot prevent an attack that happens within
// a second of the initial save().
sleep(1);
for ($i = 0; $i < 2; $i++) {
$php = new $this->storageClass($this->settings);
$GLOBALS['hacked'] = FALSE;
$untrusted_code = "<?php\n" . '$GLOBALS["hacked"] = TRUE;';
chmod($expected_directory, 0700);
chmod($expected_filename, 0700);
if ($i) {
// Now try to write the file in such a way that the directory mtime
// changes and invalidates the hash.
file_put_contents($expected_filename . '.tmp', $untrusted_code);
rename($expected_filename . '.tmp', $expected_filename);
}
else {
// On the first try do not change the directory mtime but the filemtime
// is now larger than the directory mtime.
file_put_contents($expected_filename, $untrusted_code);
}
chmod($expected_filename, 0400);
chmod($expected_directory, 0100);
$this->assertSame(file_get_contents($expected_filename), $untrusted_code);
$this->assertSame($this->expected[$i], $php->exists($name));
$this->assertSame($this->expected[$i], $php->load($name));
$this->assertSame($this->expected[$i], $GLOBALS['hacked']);
}
unset($GLOBALS['hacked']);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\PhpStorage;
/**
* Tests the MTimeProtectedFileStorage implementation.
*
* @coversDefaultClass \Drupal\Component\PhpStorage\MTimeProtectedFileStorage
*
* @group Drupal
* @group PhpStorage
*/
class MTimeProtectedFileStorageTest extends MTimeProtectedFileStorageBase {
/**
* The expected test results for the security test.
*
* The default implementation protects against even the filemtime change so
* both iterations will return FALSE.
*/
protected array $expected = [FALSE, FALSE];
/**
* The PHP storage class to test.
*/
protected $storageClass = 'Drupal\Component\PhpStorage\MTimeProtectedFileStorage';
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\PhpStorage;
use Drupal\Component\PhpStorage\PhpStorageInterface;
use Drupal\Component\Utility\Random;
use org\bovigo\vfs\vfsStream;
use PHPUnit\Framework\TestCase;
/**
* Base test for PHP storages.
*/
abstract class PhpStorageTestBase extends TestCase {
/**
* A unique per test class directory path to test php storage.
*
* @var string
*/
protected $directory;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
vfsStream::setup('exampleDir');
$this->directory = vfsStream::url('exampleDir');
}
/**
* Assert that a PHP storage's load/save/delete operations work.
*/
public function assertCRUD($php) {
// Random generator.
$random_generator = new Random();
$name = $random_generator->name(8, TRUE) . '/' . $random_generator->name(8, TRUE) . '.php';
// Find a global that doesn't exist.
do {
$random = 'test' . mt_rand(10000, 100000);
} while (isset($GLOBALS[$random]));
// Write out a PHP file and ensure it's successfully loaded.
$code = "<?php\n\$GLOBALS['$random'] = TRUE;";
$success = $php->save($name, $code);
$this->assertTrue($success, 'Saved php file');
$php->load($name);
$this->assertTrue($GLOBALS[$random], 'File saved correctly with correct value');
// Run additional asserts.
$this->additionalAssertCRUD($php, $name);
// If the file was successfully loaded, it must also exist, but ensure the
// exists() method returns that correctly.
$this->assertTrue($php->exists($name), 'Exists works correctly');
// Delete the file, and then ensure exists() returns FALSE.
$this->assertTrue($php->delete($name), 'Delete succeeded');
$this->assertFalse($php->exists($name), 'Delete deleted file');
// Ensure delete() can be called on a non-existing file. It should return
// FALSE, but not trigger errors.
$this->assertFalse($php->delete($name), 'Delete fails on missing file');
unset($GLOBALS[$random]);
}
/**
* Additional asserts to be run.
*
* @param \Drupal\Component\PhpStorage\PhpStorageInterface $php
* The PHP storage object.
* @param string $name
* The name of an object. It should exist in the storage.
*/
protected function additionalAssertCRUD(PhpStorageInterface $php, $name) {
// By default do not do any additional asserts. This is a way of extending
// tests in contrib.
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Plugin\Attribute;
use Drupal\Component\Plugin\Attribute\AttributeBase;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Plugin\Attribute\AttributeBase
* @group Attribute
*/
class AttributeBaseTest extends TestCase {
/**
* @covers ::getProvider
* @covers ::setProvider
*/
public function testSetProvider(): void {
$plugin = new AttributeBaseStub(id: '1');
$plugin->setProvider('example');
$this->assertEquals('example', $plugin->getProvider());
}
/**
* @covers ::getId
*/
public function testGetId(): void {
$plugin = new AttributeBaseStub(id: 'example');
$this->assertEquals('example', $plugin->getId());
}
/**
* @covers ::getClass
* @covers ::setClass
*/
public function testSetClass(): void {
$plugin = new AttributeBaseStub(id: '1');
$plugin->setClass('example');
$this->assertEquals('example', $plugin->getClass());
}
}
/**
* {@inheritdoc}
*/
class AttributeBaseStub extends AttributeBase {
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Plugin\Attribute;
use Drupal\Component\Plugin\Discovery\AttributeClassDiscovery;
use Drupal\Component\FileCache\FileCacheFactory;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Plugin\Discovery\AttributeClassDiscovery
* @group Attribute
* @runTestsInSeparateProcesses
*/
class AttributeClassDiscoveryCachedTest extends TestCase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Ensure FileCacheFactory::DISABLE_CACHE is *not* set, since we're testing
// integration with the file cache.
FileCacheFactory::setConfiguration([]);
// Ensure that FileCacheFactory has a prefix.
FileCacheFactory::setPrefix('prefix');
// Normally the attribute classes would be autoloaded.
include_once __DIR__ . '/Fixtures/CustomPlugin.php';
include_once __DIR__ . '/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php';
}
/**
* Tests that getDefinitions() retrieves the file cache correctly.
*
* @covers ::getDefinitions
*/
public function testGetDefinitions(): void {
// Path to the classes which we'll discover and parse annotation.
$discovery_path = __DIR__ . '/Fixtures/Plugins';
// File path that should be discovered within that directory.
$file_path = $discovery_path . '/PluginNamespace/AttributeDiscoveryTest1.php';
$discovery = new AttributeClassDiscovery(['com\example' => [$discovery_path]]);
$this->assertEquals([
'discovery_test_1' => [
'id' => 'discovery_test_1',
'class' => 'com\example\PluginNamespace\AttributeDiscoveryTest1',
],
], $discovery->getDefinitions());
// Gain access to the file cache so we can change it.
$ref_file_cache = new \ReflectionProperty($discovery, 'fileCache');
$ref_file_cache->setAccessible(TRUE);
/** @var \Drupal\Component\FileCache\FileCacheInterface $file_cache */
$file_cache = $ref_file_cache->getValue($discovery);
// The file cache is keyed by the file path, and we'll add some known
// content to test against.
$file_cache->set($file_path, [
'id' => 'wrong_id',
'content' => serialize(['an' => 'array']),
]);
// Now perform the same query and check for the cached results.
$this->assertEquals([
'wrong_id' => [
'an' => 'array',
],
], $discovery->getDefinitions());
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Plugin\Attribute;
use Drupal\Component\Plugin\Discovery\AttributeClassDiscovery;
use Drupal\Component\FileCache\FileCacheFactory;
use PHPUnit\Framework\TestCase;
use com\example\PluginNamespace\CustomPlugin;
use com\example\PluginNamespace\CustomPlugin2;
/**
* @coversDefaultClass \Drupal\Component\Plugin\Discovery\AttributeClassDiscovery
* @group Attribute
* @runTestsInSeparateProcesses
*/
class AttributeClassDiscoveryTest extends TestCase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Ensure the file cache is disabled.
FileCacheFactory::setConfiguration([FileCacheFactory::DISABLE_CACHE => TRUE]);
// Ensure that FileCacheFactory has a prefix.
FileCacheFactory::setPrefix('prefix');
// Normally the attribute classes would be autoloaded.
include_once __DIR__ . '/Fixtures/CustomPlugin.php';
include_once __DIR__ . '/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php';
}
/**
* @covers ::__construct
* @covers ::getPluginNamespaces
*/
public function testGetPluginNamespaces(): void {
// Path to the classes which we'll discover and parse annotation.
$discovery = new AttributeClassDiscovery(['com/example' => [__DIR__]]);
$reflection = new \ReflectionMethod($discovery, 'getPluginNamespaces');
$reflection->setAccessible(TRUE);
$result = $reflection->invoke($discovery);
$this->assertEquals(['com/example' => [__DIR__]], $result);
}
/**
* @covers ::getDefinitions
* @covers ::prepareAttributeDefinition
*/
public function testGetDefinitions(): void {
$discovery = new AttributeClassDiscovery(['com\example' => [__DIR__ . '/Fixtures/Plugins']]);
$this->assertEquals([
'discovery_test_1' => [
'id' => 'discovery_test_1',
'class' => 'com\example\PluginNamespace\AttributeDiscoveryTest1',
],
], $discovery->getDefinitions());
$custom_annotation_discovery = new AttributeClassDiscovery(['com\example' => [__DIR__ . '/Fixtures/Plugins']], CustomPlugin::class);
$this->assertEquals([
'discovery_test_1' => [
'id' => 'discovery_test_1',
'class' => 'com\example\PluginNamespace\AttributeDiscoveryTest1',
'title' => 'Discovery test plugin',
],
], $custom_annotation_discovery->getDefinitions());
$empty_discovery = new AttributeClassDiscovery(['com\example' => [__DIR__ . '/Fixtures/Plugins']], CustomPlugin2::class);
$this->assertEquals([], $empty_discovery->getDefinitions());
}
}

Some files were not shown because too many files have changed in this diff Show More