Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

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 attribute.notFound
#[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): void {
$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.
*
* Run this test in a separate process as it includes code that might have side
* effects.
*
* @group DCOM58
* @runTestsInSeparateProcesses
*/
class DCOM58Test extends TestCase
{
protected function setUp(): void
{
// Some class named Entity in the global namespace.
include __DIR__ .'/DCOM58Entity.php';
}
public function testIssueGlobalNamespace(): void
{
$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(): void
{
$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(): void
{
$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(): void
{
$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(): void
{
$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,108 @@
<?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 {
/**
* A default property for testing.
*
* @var string
*/
protected $defaultProperty = 'test_value';
}

View File

@ -0,0 +1,255 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Assertion;
use PHPUnit\Framework\TestCase;
use Drupal\Component\Assertion\Inspector;
use Drupal\TestTools\Extension\DeprecationBridge\ExpectDeprecationTrait;
/**
* @coversDefaultClass \Drupal\Component\Assertion\Inspector
* @group Assertion
*/
class InspectorTest extends TestCase {
use ExpectDeprecationTrait;
/**
* 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,944 @@
<?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');
$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.
[
'input1' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00', new \DateTimeZone('Australia/Sydney')),
'input2' => 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.
[
'input1' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '1970-01-01 00:00:00', new \DateTimeZone('Australia/Sydney')),
'input2' => 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::createFromTimestamp(0, new \DateTimeZone('UTC')),
'absolute' => FALSE,
'expected' => $negative_1_hour,
],
[
'input1' => DateTimePlus::createFromTimestamp(3600),
'input2' => DateTimePlus::createFromTimestamp(0),
'absolute' => FALSE,
'expected' => $negative_1_hour,
],
[
'input1' => DateTimePlus::createFromTimestamp(3600),
'input2' => \DateTime::createFromFormat('U', '0'),
'absolute' => FALSE,
'expected' => $negative_1_hour,
],
[
'input1' => DateTimePlus::createFromTimestamp(3600),
'input2' => DateTimePlus::createFromTimestamp(0),
'absolute' => TRUE,
'expected' => $positive_1_hour,
],
[
'input1' => DateTimePlus::createFromTimestamp(3600),
'input2' => \DateTime::createFromFormat('U', '0'),
'absolute' => TRUE,
'expected' => $positive_1_hour,
],
[
'input1' => DateTimePlus::createFromTimestamp(0),
'input2' => DateTimePlus::createFromTimestamp(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::createFromTimestamp(3600),
'input2' => '1970-01-01 00:00:00',
'absolute' => FALSE,
],
[
'input1' => DateTimePlus::createFromTimestamp(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.
$date_time_plus = DateTimePlus::createFromFormat('Y-m-d H:i:s', '2017-07-13 22:40:00', $new_york, ['langcode' => 'en']);
$this->assertEquals(1500000000, $date_time_plus->getTimestamp());
$this->assertEquals('America/New_York', $date_time_plus->getTimezone()->getName());
$datetime = $date_time_plus->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, $date_time_plus->getTimestamp());
$this->assertEquals('America/New_York', $date_time_plus->getTimezone()->getName());
}
}

View File

@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Datetime;
use Drupal\Component\Datetime\Time;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests the Time class.
*
* Isolate the tests to prevent side effects from altering system time.
*
* @coversDefaultClass \Drupal\Component\Datetime\Time
* @group Datetime
* @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
* The fixed integer timestamp used for testing purposes.
*/
function time() {
return 12345678;
}
/**
* Shadow microtime system call.
*
* @return float
* The fixed float timestamp used for testing purposes.
*/
function microtime(bool $as_float = FALSE) {
return 1234567.89;
}

View File

@ -0,0 +1,62 @@
<?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
* @group #slow
* @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,755 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\DependencyInjection\Dumper;
use Drupal\Component\Utility\Crypt;
use Drupal\TestTools\Extension\DeprecationBridge\ExpectDeprecationTrait;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophet;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\ExpressionLanguage\Expression;
/**
* @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 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): array {
return $service_definition;
}
/**
* {@inheritdoc}
*/
protected static function getServiceCall($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE): string {
if ($invalid_behavior !== ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
return sprintf('@?%s', $id);
}
return sprintf('@%s', $id);
}
/**
* {@inheritdoc}
*/
protected static function getParameterCall($name): string {
return '%' . $name . '%';
}
/**
* {@inheritdoc}
*/
protected static function getCollection($collection, $resolve = TRUE) {
return $collection;
}
}

View File

@ -0,0 +1,18 @@
<?php
/**
* @file
* Contains a test function for container 'file' include testing.
*/
declare(strict_types=1);
/**
* 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): string {
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): string {
return '%' . $name . '%';
}
/**
* Helper function to return a machine-optimized '@notation'.
*/
protected function getCollection($collection, $resolve = TRUE) {
return $collection;
}
}

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\DependencyInjection;
use Drupal\Component\DependencyInjection\ReverseContainer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Tests the ReverseContainer class.
*
* The reverse container uses a static to maintain information across
* container rebuilds.
*
* @runTestsInSeparateProcesses
* @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,59 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Diff\Engine;
// cspell:ignore HWLDFWordAccumulator
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,167 @@
<?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,163 @@
<?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
* An associative array where the keys are component names and the values
* are arrays containing the corresponding component path.
*/
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): array {
$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) {
$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,186 @@
<?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 to help test the file cache class.
*/
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,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,66 @@
<?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'));
}
}

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,372 @@
<?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();
[, $new_plural] = $p->parsePluralForms($plural);
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,204 @@
<?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 array $graph
* A graph array processed by \Drupal\Component\Graph\Graph::searchAndSort()
*
* @return array
* The normalized version of a graph.
*/
protected function normalizeGraph($graph): array {
$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 array $paths
* An array containing a list of vertices.
* @param bool $keys
* (optional) Whether to output the keys of $paths instead of the values.
*/
protected function displayArray($paths, $keys = FALSE): string {
if (!empty($paths)) {
return implode(', ', $keys ? array_keys($paths) : $paths);
}
else {
return '(empty)';
}
}
}

View File

@ -0,0 +1,61 @@
<?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());
}
}
/**
* Test class for safe redirects.
*/
class SecuredRedirectStub extends SecuredRedirectResponse {
/**
* {@inheritdoc}
*/
protected function isSafe($url): bool {
// Empty implementation for testing.
return TRUE;
}
}

View File

@ -0,0 +1,111 @@
<?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 Drupal\TestTools\Extension\DeprecationBridge\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 ::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,115 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\PhpStorage;
use Drupal\Component\PhpStorage\FileStorage;
use Drupal\Component\Utility\Random;
use Drupal\TestTools\Extension\DeprecationBridge\ExpectDeprecationTrait;
use org\bovigo\vfs\vfsStreamDirectory;
/**
* @coversDefaultClass \Drupal\Component\PhpStorage\FileStorage
* @group Drupal
* @group PhpStorage
*/
class FileStorageTest extends PhpStorageTestBase {
use 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 ::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,35 @@
<?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.
*
* @var bool[]
*/
protected array $expected = [TRUE, FALSE];
/**
* The PHP storage class to test.
*
* @var class-string
*/
protected $storageClass = 'Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage';
}

View File

@ -0,0 +1,137 @@
<?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,34 @@
<?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.
*
* @var bool[]
*/
protected array $expected = [FALSE, FALSE];
/**
* The PHP storage class to test.
*
* @var class-string
*/
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,156 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Plugin\Attribute;
use Composer\Autoload\ClassLoader;
use Drupal\Component\Plugin\Discovery\AttributeClassDiscovery;
use Drupal\Component\FileCache\FileCacheFactory;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Plugin\Discovery\AttributeClassDiscovery
* @covers \Drupal\Component\Discovery\MissingClassDetectionClassLoader
* @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/plugins/CustomPlugin.php';
$additionalClassLoader = new ClassLoader();
$additionalClassLoader->addPsr4("com\\example\\PluginNamespace\\", __DIR__ . "/../../../../../fixtures/plugins/Plugin/PluginNamespace");
$additionalClassLoader->register(TRUE);
}
/**
* 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/Plugin";
// File path that should be discovered within that directory.
$file_path = $discovery_path . '/PluginNamespace/AttributeDiscoveryTest1.php';
// Define file paths within the directory that should not be discovered.
$non_discoverable_file_paths = [
$discovery_path . '/PluginNamespace/AttributeDiscoveryTest2.php',
$discovery_path . '/PluginNamespace/AttributeDiscoveryTestMissingInterface.php',
$discovery_path . '/PluginNamespace/AttributeDiscoveryTestMissingTrait.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.
$ref_file_cache = new \ReflectionProperty($discovery, 'fileCache');
/** @var \Drupal\Component\FileCache\FileCacheInterface $file_cache */
$file_cache = $ref_file_cache->getValue($discovery);
// The valid plugin definition should be cached.
$this->assertEquals([
'id' => 'discovery_test_1',
'class' => 'com\example\PluginNamespace\AttributeDiscoveryTest1',
], unserialize($file_cache->get($file_path)['content']));
// The plugins that extend a missing class, implement a missing interface,
// and use a missing trait should not be cached.
foreach ($non_discoverable_file_paths as $non_discoverable_file_path) {
$this->assertTrue(file_exists($non_discoverable_file_path));
$this->assertNull($file_cache->get($non_discoverable_file_path));
}
// Change the file cache entry.
// 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());
}
/**
* Tests discovery with missing traits.
*
* @covers ::getDefinitions
*/
public function testGetDefinitionsMissingTrait(): void {
// Path to the classes which we'll discover and parse annotation.
$discovery_path = __DIR__ . "/../../../../../fixtures/plugins/Plugin";
// Define file paths within the directory that should not be discovered.
$non_discoverable_file_paths = [
$discovery_path . '/PluginNamespace/AttributeDiscoveryTest2.php',
$discovery_path . '/PluginNamespace/AttributeDiscoveryTestMissingInterface.php',
$discovery_path . '/PluginNamespace/AttributeDiscoveryTestMissingTrait.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.
$ref_file_cache = new \ReflectionProperty($discovery, 'fileCache');
/** @var \Drupal\Component\FileCache\FileCacheInterface $file_cache */
$file_cache = $ref_file_cache->getValue($discovery);
// The plugins that extend a missing class, implement a missing interface,
// and use a missing trait should not be cached.
foreach ($non_discoverable_file_paths as $non_discoverable_file_path) {
$this->assertTrue(file_exists($non_discoverable_file_path));
$this->assertNull($file_cache->get($non_discoverable_file_path));
}
$discovery = new AttributeClassDiscovery(['com\example' => [$discovery_path], 'Drupal\a_module_that_does_not_exist' => [$discovery_path]]);
$this->assertEquals([
'discovery_test_1' => [
'id' => 'discovery_test_1',
'class' => 'com\example\PluginNamespace\AttributeDiscoveryTest1',
],
'discovery_test_missing_trait' => [
'id' => 'discovery_test_missing_trait',
'class' => 'com\example\PluginNamespace\AttributeDiscoveryTestMissingTrait',
'title' => 'Discovery test plugin missing trait',
],
], $discovery->getDefinitions());
// The plugins that extend a missing class, implement a missing interface,
// and use a missing trait should not be cached. This is the case even for
// the plugin that was just discovered.
foreach ($non_discoverable_file_paths as $non_discoverable_file_path) {
$this->assertTrue(file_exists($non_discoverable_file_path));
$this->assertNull($file_cache->get($non_discoverable_file_path));
}
}
}

View File

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Plugin\Attribute;
use Composer\Autoload\ClassLoader;
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 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/plugins/CustomPlugin.php';
$additionalClassLoader = new ClassLoader();
$additionalClassLoader->addPsr4("com\\example\\PluginNamespace\\", __DIR__ . "/../../../../../fixtures/plugins/Plugin/PluginNamespace");
$additionalClassLoader->register(TRUE);
}
/**
* @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');
$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/Plugin"]]);
$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/Plugin"]], 'com\example\PluginNamespace\CustomPlugin');
$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/Plugin"]], 'com\example\PluginNamespace\CustomPlugin2');
$this->assertEquals([], $empty_discovery->getDefinitions());
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Plugin\Attribute;
use Drupal\Component\Plugin\Attribute\PluginID;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Plugin\Attribute\PluginID
* @group Attribute
*/
class PluginIdTest extends TestCase {
/**
* @covers ::get
*/
public function testGet(): void {
// Assert plugin starts with only an ID.
$plugin = new PluginID(id: 'test');
// Plugin's always have a class set by discovery.
$plugin->setClass('bar');
$this->assertEquals([
'id' => 'test',
'class' => 'bar',
'provider' => NULL,
], $plugin->get());
// Set values and ensure we can retrieve them.
$plugin->setClass('bar2');
$plugin->setProvider('baz');
$this->assertEquals([
'id' => 'test',
'class' => 'bar2',
'provider' => 'baz',
], $plugin->get());
}
}

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Plugin\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Plugin\Attribute\Plugin
* @group Attribute
*/
class PluginTest extends TestCase {
/**
* @covers ::__construct
* @covers ::get
*/
public function testGet(): void {
$plugin = new PluginStub(id: 'example', deriver: 'test');
$plugin->setClass('foo');
$this->assertEquals([
'id' => 'example',
'class' => 'foo',
'deriver' => 'test',
], $plugin->get());
}
/**
* @covers ::setProvider
* @covers ::getProvider
*/
public function testSetProvider(): void {
$plugin = new Plugin(id: 'example');
$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 ::setClass
* @covers ::getClass
*/
public function testSetClass(): void {
$plugin = new Plugin(id: 'test');
$plugin->setClass('example');
$this->assertEquals('example', $plugin->getClass());
}
}
/**
* {@inheritdoc}
*/
class PluginStub extends Plugin {
}

View File

@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Plugin\Context;
use Drupal\Component\Plugin\Context\Context;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Plugin\Context\Context
* @group Plugin
*/
class ContextTest extends TestCase {
/**
* Data provider for testGetContextValue.
*/
public static function providerGetContextValue() {
return [
['context_value', 'context_value', FALSE, 'data_type'],
[NULL, NULL, FALSE, 'data_type'],
['will throw exception', NULL, TRUE, 'data_type'],
];
}
/**
* @covers ::getContextValue
* @dataProvider providerGetContextValue
*/
public function testGetContextValue($expected, $context_value, $is_required, $data_type): void {
// Mock a Context object.
$mock_context = $this->getMockBuilder('Drupal\Component\Plugin\Context\Context')
->disableOriginalConstructor()
->onlyMethods(['getContextDefinition'])
->getMock();
// If the context value exists, getContextValue() behaves like a normal
// getter.
if ($context_value) {
// Set visibility of contextValue.
$ref_context_value = new \ReflectionProperty($mock_context, 'contextValue');
// Set contextValue to a testable state.
$ref_context_value->setValue($mock_context, $context_value);
// Exercise getContextValue().
$this->assertEquals($context_value, $mock_context->getContextValue());
}
// If no context value exists, we have to cover either returning NULL or
// throwing an exception if the definition requires it.
else {
// Create a mock definition.
$mock_definition = $this->createMock('Drupal\Component\Plugin\Context\ContextDefinitionInterface');
// Set expectation for isRequired().
$mock_definition->expects($this->once())
->method('isRequired')
->willReturn($is_required);
// Set expectation for getDataType().
$mock_definition->expects($this->exactly(
$is_required ? 1 : 0
))
->method('getDataType')
->willReturn($data_type);
// Set expectation for getContextDefinition().
$mock_context->expects($this->once())
->method('getContextDefinition')
->willReturn($mock_definition);
// Set expectation for exception.
if ($is_required) {
$this->expectException('Drupal\Component\Plugin\Exception\ContextException');
$this->expectExceptionMessage(sprintf("The %s context is required and not present.", $data_type));
}
// Exercise getContextValue().
$this->assertEquals($context_value, $mock_context->getContextValue());
}
}
/**
* Data provider for testHasContextValue.
*/
public static function providerHasContextValue() {
return [
[TRUE, FALSE],
[TRUE, 0],
[TRUE, -0],
[TRUE, 0.0],
[TRUE, -0.0],
[TRUE, ''],
[TRUE, '0'],
[TRUE, []],
[FALSE, NULL],
];
}
/**
* @covers ::hasContextValue
* @dataProvider providerHasContextValue
*/
public function testHasContextValue($has_context_value, $default_value): void {
$mock_definition = $this->createMock('Drupal\Component\Plugin\Context\ContextDefinitionInterface');
$mock_definition->expects($this->atLeastOnce())
->method('getDefaultValue')
->willReturn($default_value);
$context = new Context($mock_definition);
$this->assertSame($has_context_value, $context->hasContextValue());
$this->assertSame($default_value, $context->getContextValue());
}
/**
* @covers ::getContextValue
*/
public function testDefaultValue(): void {
$mock_definition = $this->createMock('Drupal\Component\Plugin\Context\ContextDefinitionInterface');
$mock_definition->expects($this->once())
->method('getDefaultValue')
->willReturn('test');
$context = new Context($mock_definition);
$this->assertEquals('test', $context->getContextValue());
}
}

View File

@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Plugin;
use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\Tests\Component\Plugin\Fixtures\vegetable\Broccoli;
use Drupal\Tests\Component\Plugin\Fixtures\vegetable\Corn;
use Drupal\Tests\Component\Plugin\Fixtures\vegetable\VegetableInterface;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Plugin\Factory\DefaultFactory
* @group Plugin
*/
class DefaultFactoryTest extends TestCase {
/**
* Tests getPluginClass() with a valid array plugin definition.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithValidArrayPluginDefinition(): void {
$plugin_class = Corn::class;
$class = DefaultFactory::getPluginClass('corn', ['class' => $plugin_class]);
$this->assertEquals($plugin_class, $class);
}
/**
* Tests getPluginClass() with a valid object plugin definition.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithValidObjectPluginDefinition(): void {
$plugin_class = Corn::class;
$plugin_definition = $this->getMockBuilder(PluginDefinitionInterface::class)->getMock();
$plugin_definition->expects($this->atLeastOnce())
->method('getClass')
->willReturn($plugin_class);
$class = DefaultFactory::getPluginClass('corn', $plugin_definition);
$this->assertEquals($plugin_class, $class);
}
/**
* Tests getPluginClass() with a missing class definition.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithMissingClassWithArrayPluginDefinition(): void {
$this->expectException(PluginException::class);
$this->expectExceptionMessage('The plugin (corn) did not specify an instance class.');
DefaultFactory::getPluginClass('corn', []);
}
/**
* Tests getPluginClass() with a missing class definition.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithMissingClassWithObjectPluginDefinition(): void {
$plugin_definition = $this->getMockBuilder(PluginDefinitionInterface::class)
->getMock();
$this->expectException(PluginException::class);
$this->expectExceptionMessage('The plugin (corn) did not specify an instance class.');
DefaultFactory::getPluginClass('corn', $plugin_definition);
}
/**
* Tests getPluginClass() with a non-existent class definition.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithNotExistingClassWithArrayPluginDefinition(): void {
$this->expectException(PluginException::class);
$this->expectExceptionMessage('Plugin (carrot) instance class "Drupal\Tests\Component\Plugin\Fixtures\vegetable\Carrot" does not exist.');
DefaultFactory::getPluginClass('carrot', ['class' => 'Drupal\Tests\Component\Plugin\Fixtures\vegetable\Carrot']);
}
/**
* Tests getPluginClass() with a non-existent class definition.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithNotExistingClassWithObjectPluginDefinition(): void {
$plugin_class = 'Drupal\Tests\Component\Plugin\Fixtures\vegetable\Carrot';
$plugin_definition = $this->getMockBuilder(PluginDefinitionInterface::class)->getMock();
$plugin_definition->expects($this->atLeastOnce())
->method('getClass')
->willReturn($plugin_class);
$this->expectException(PluginException::class);
DefaultFactory::getPluginClass('carrot', $plugin_definition);
}
/**
* Tests getPluginClass() with a required interface.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithInterfaceWithArrayPluginDefinition(): void {
$plugin_class = Corn::class;
$class = DefaultFactory::getPluginClass('corn', ['class' => $plugin_class], VegetableInterface::class);
$this->assertEquals($plugin_class, $class);
}
/**
* Tests getPluginClass() with a required interface.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithInterfaceWithObjectPluginDefinition(): void {
$plugin_class = Corn::class;
$plugin_definition = $this->getMockBuilder(PluginDefinitionInterface::class)->getMock();
$plugin_definition->expects($this->atLeastOnce())
->method('getClass')
->willReturn($plugin_class);
$class = DefaultFactory::getPluginClass('corn', $plugin_definition, VegetableInterface::class);
$this->assertEquals($plugin_class, $class);
}
/**
* Tests getPluginClass() with a required interface but no implementation.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithInterfaceAndInvalidClassWithArrayPluginDefinition(): void {
$this->expectException(PluginException::class);
$this->expectExceptionMessage('Plugin "corn" (Drupal\Tests\Component\Plugin\Fixtures\vegetable\Broccoli) must implement interface Drupal\Tests\Component\Plugin\Fixtures\vegetable\VegetableInterface.');
DefaultFactory::getPluginClass('corn', ['class' => Broccoli::class], VegetableInterface::class);
}
/**
* Tests getPluginClass() with a required interface but no implementation.
*
* @covers ::getPluginClass
*/
public function testGetPluginClassWithInterfaceAndInvalidClassWithObjectPluginDefinition(): void {
$plugin_class = Broccoli::class;
$plugin_definition = $this->getMockBuilder(PluginDefinitionInterface::class)->getMock();
$plugin_definition->expects($this->atLeastOnce())
->method('getClass')
->willReturn($plugin_class);
$this->expectException(PluginException::class);
DefaultFactory::getPluginClass('corn', $plugin_definition, VegetableInterface::class);
}
}

View File

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Plugin\Discovery;
use Drupal\Component\Annotation\Plugin\Discovery\AnnotatedClassDiscovery;
use Drupal\Component\FileCache\FileCacheFactory;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamDirectory;
use org\bovigo\vfs\vfsStreamWrapper;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Annotation\Plugin\Discovery\AnnotatedClassDiscovery
*
* @group Annotation
* @group Plugin
*/
class AnnotatedClassDiscoveryTest extends TestCase {
/**
* All the Drupal documentation standards tags.
*
* @var string[]
*/
public static function provideBadAnnotations() {
return [
['addtogroup'],
['code'],
['defgroup'],
['deprecated'],
['endcode'],
['endlink'],
['file'],
['ingroup'],
['group'],
['link'],
['mainpage'],
['param'],
['ref'],
['return'],
['section'],
['see'],
['subsection'],
['throws'],
['todo'],
['var'],
['{'],
['}'],
];
}
/**
* Make sure AnnotatedClassDiscovery never tries to autoload bad annotations.
*
* @dataProvider provideBadAnnotations
*
* @coversNothing
*/
public function testAutoloadBadAnnotations($annotation): void {
// Set up a class file in vfsStream.
vfsStreamWrapper::register();
$root = new vfsStreamDirectory('root');
vfsStreamWrapper::setRoot($root);
FileCacheFactory::setPrefix(__CLASS__);
// Make a directory for discovery.
$url = vfsStream::url('root');
mkdir($url . '/DrupalTest');
// Create a class docblock with our annotation.
$php_file = "<?php\nnamespace DrupalTest;\n/**\n";
$php_file .= " * @$annotation\n";
$php_file .= " */\nclass TestClass {}";
file_put_contents($url . '/DrupalTest/TestClass.php', $php_file);
// Create an AnnotatedClassDiscovery object referencing the virtual file.
$discovery = new AnnotatedClassDiscovery(
['\\DrupalTest\\TestClass' => [vfsStream::url('root/DrupalTest')]], '\\DrupalTest\\Component\\Annotation\\'
);
// Register our class loader which will fail if the annotation reader tries
// to autoload disallowed annotations.
$class_loader = function ($class_name) use ($annotation) {
$name_array = explode('\\', $class_name);
$name = array_pop($name_array);
if ($name == $annotation) {
$this->fail('Attempted to autoload a non-plugin annotation: ' . $name);
}
};
spl_autoload_register($class_loader, TRUE, TRUE);
// Now try to get plugin definitions.
$definitions = $discovery->getDefinitions();
// Unregister to clean up.
spl_autoload_unregister($class_loader);
// Assert that no annotations were loaded.
$this->assertEmpty($definitions);
}
}

View File

@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Plugin\Discovery;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Component\Plugin\Definition\PluginDefinition;
use Drupal\Component\Plugin\Discovery\AttributeBridgeDecorator;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Annotation\Plugin\Discovery\AnnotationBridgeDecorator
* @group Plugin
*/
class AttributeBridgeDecoratorTest extends TestCase {
/**
* @covers ::getDefinitions
*/
public function testGetDefinitions(): void {
// Normally the attribute classes would be autoloaded.
include_once __DIR__ . '/../../../../../fixtures/plugins/CustomPlugin.php';
include_once __DIR__ . '/../../../../../fixtures/plugins/Plugin/PluginNamespace/AttributeDiscoveryTest1.php';
$definitions = [];
$definitions['object'] = new ObjectDefinition(['id' => 'foo']);
$definitions['array'] = [
'id' => 'bar',
'class' => 'com\example\PluginNamespace\AttributeDiscoveryTest1',
];
$discovery = $this->createMock(DiscoveryInterface::class);
$discovery->expects($this->any())
->method('getDefinitions')
->willReturn($definitions);
$decorator = new AttributeBridgeDecorator($discovery, TestAttribute::class);
$expected = [
'object' => new ObjectDefinition(['id' => 'foo']),
'array' => (new ObjectDefinition(['id' => 'bar']))->setClass('com\example\PluginNamespace\AttributeDiscoveryTest1'),
];
$this->assertEquals($expected, $decorator->getDefinitions());
}
/**
* Tests that the decorator of other methods works.
*
* @covers ::__call
*/
public function testOtherMethod(): void {
// Normally the attribute classes would be autoloaded.
include_once __DIR__ . '/../../../../../fixtures/plugins/CustomPlugin.php';
include_once __DIR__ . '/../../../../../fixtures/plugins/Plugin/PluginNamespace/AttributeDiscoveryTest1.php';
$discovery = $this->createMock(ExtendedDiscoveryInterface::class);
$discovery->expects($this->exactly(2))
->method('otherMethod')
->willReturnCallback(fn($id) => $id === 'foo');
$decorator = new AttributeBridgeDecorator($discovery, TestAttribute::class);
$this->assertTrue($decorator->otherMethod('foo'));
$this->assertFalse($decorator->otherMethod('bar'));
}
}
/**
* An interface for testing the Discovery interface.
*/
interface ExtendedDiscoveryInterface extends DiscoveryInterface {
public function otherMethod(string $id): bool;
}
/**
* {@inheritdoc}
*/
class TestAttribute extends Plugin {
/**
* {@inheritdoc}
*/
public function get(): object {
return new ObjectDefinition(parent::get());
}
}
/**
* {@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,81 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Component\Plugin\Discovery;
use Drupal\Component\Plugin\Discovery\DiscoveryCachedTrait;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Component\Plugin\Discovery\DiscoveryCachedTrait
* @uses \Drupal\Component\Plugin\Discovery\DiscoveryTrait
* @group Plugin
*/
class DiscoveryCachedTraitTest extends TestCase {
/**
* Data provider for testGetDefinition().
*
* @return array
* - Expected result from getDefinition().
* - Cached definitions to be placed into self::$definitions
* - Definitions to be returned by getDefinitions().
* - Plugin name to query for.
*/
public static function providerGetDefinition() {
return [
['definition', [], ['plugin_name' => 'definition'], 'plugin_name'],
['definition', ['plugin_name' => 'definition'], [], 'plugin_name'],
[NULL, ['plugin_name' => 'definition'], [], 'bad_plugin_name'],
];
}
/**
* @covers ::getDefinition
* @dataProvider providerGetDefinition
*/
public function testGetDefinition($expected, $cached_definitions, $get_definitions, $plugin_id): void {
$trait = $this->getMockBuilder(DiscoveryCachedTraitMockableClass::class)
->onlyMethods(['getDefinitions'])
->getMock();
$reflection_definitions = new \ReflectionProperty($trait, 'definitions');
// getDefinition() needs the ::$definitions property to be set in one of two
// ways: 1) As existing cached data, or 2) as a side-effect of calling
// getDefinitions().
// If there are no cached definitions, then we have to fake the side-effect
// of getDefinitions().
if (count($cached_definitions) < 1) {
$trait->expects($this->once())
->method('getDefinitions')
// Use a callback method, so we can perform the side-effects.
->willReturnCallback(function () use ($reflection_definitions, $trait, $get_definitions) {
$reflection_definitions->setValue($trait, $get_definitions);
return $get_definitions;
});
}
else {
// Put $cached_definitions into our mocked ::$definitions.
$reflection_definitions->setValue($trait, $cached_definitions);
}
// Call getDefinition(), with $exception_on_invalid always FALSE.
$this->assertSame(
$expected,
$trait->getDefinition($plugin_id, FALSE)
);
}
}
/**
* A class using the DiscoveryCachedTrait for mocking purposes.
*/
class DiscoveryCachedTraitMockableClass {
use DiscoveryCachedTrait;
public function getDefinitions(): array {
return [];
}
}

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