diff --git a/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregate.php b/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregate.php new file mode 100644 index 0000000..53599db --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregate.php @@ -0,0 +1,293 @@ +metadata = $metadata; + parent::__construct($assets); + $this->filters = new FilterCollection($filters); + } + + /** + * {@inheritdoc} + */ + public function id() { + if (empty($this->id)) { + $this->calculateId(); + } + + return $this->id; + } + + /** + * {@inheritdoc} + */ + public function getAssetType() { + return $this->metadata->getType(); + } + + /** + * Calculates and stores an id for this aggregate from the contained assets. + * + * @return void + */ + protected function calculateId() { + $id = ''; + foreach ($this->eachLeaf() as $asset) { + $id .= $asset->id(); + } + // TODO come up with something stabler/more serialization friendly than object hash + $this->id = hash('sha256', $id ?: spl_object_hash($this)); + } + + /** + * {@inheritdoc} + */ + public function getMetadata() { + // TODO should this immutable? doable if we further granulate the interfaces + return $this->metadata; + } + + /** + * {@inheritdoc} + */ + public function removeLeaf(AsseticAssetInterface $needle, $graceful = FALSE) { + if (!$needle instanceof AssetInterface) { + throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset provided; Drupal aggregates require Drupal-flavored assets.'); + } + + return $this->doRemove($needle, $graceful); + } + + /** + * {@inheritdoc} + */ + public function replaceLeaf(AsseticAssetInterface $needle, AsseticAssetInterface $replacement, $graceful = FALSE) { + if (!($needle instanceof AssetInterface && $replacement instanceof AssetInterface)) { + throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset(s) provided; Drupal aggregates require Drupal-flavored assets.'); + } + + $this->ensureCorrectType($replacement); + if ($this->contains($replacement)) { + throw new \LogicException('Asset to be swapped in is already present in the collection.'); + } + + return $this->doReplace($needle, $replacement, $graceful); + } + + /** + * {@inheritdoc} + */ + protected function ensureCorrectType(AssetInterface $asset) { + if ($asset->getAssetType() != $this->getAssetType()) { + throw new AssetTypeMismatchException(sprintf('Aggregate/asset incompatibility, aggregate of type "%s", asset of type "%s". Aggregates and their contained assets must be of the same type.', $this->getAssetType(), $asset->getAssetType())); + } + } + + /** + * {@inheritdoc} + * + * Aggregate assets are inherently eligible for preprocessing, so this is + * always true. + */ + public function isPreprocessable() { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function load(FilterInterface $additionalFilter = NULL) { + // loop through leaves and load each asset + $parts = array(); + foreach ($this as $asset) { + $asset->load($additionalFilter); + $parts[] = $asset->getContent(); + } + + $this->content = implode("\n", $parts); + } + + /** + * {@inheritdoc} + */ + public function dump(FilterInterface $additionalFilter = NULL) { + // loop through leaves and dump each asset + $parts = array(); + foreach ($this as $asset) { + $parts[] = $asset->dump($additionalFilter); + } + + return implode("\n", $parts); + } + + /** + * {@inheritdoc} + */ + public function getContent() { + return $this->content; + } + + /** + * {@inheritdoc} + */ + public function setContent($content) { + $this->content = $content; + } + + /** + * {@inheritdoc} + */ + public function getLastModified() { + // TODO: Implement getLastModified() method. + } + + /** + * {@inheritdoc} + */ + public function getSourceRoot() { + // Drupal doesn't use this in general, and especially not for aggregates. + return NULL; + } + + /** + * {@inheritdoc} + */ + public function getSourcePath() { + return $this->sourcePath; + } + + /** + * {@inheritdoc} + */ + public function getTargetPath() { + return $this->targetPath; + } + + /** + * {@inheritdoc} + */ + public function setTargetPath($targetPath) { + $this->targetPath = $targetPath; + } + + /** + * {@inheritdoc} + */ + public function ensureFilter(FilterInterface $filter) { + $this->filters->ensure($filter); + } + + /** + * {@inheritdoc} + */ + public function getFilters() { + $this->filters->all(); + } + + /** + * {@inheritdoc} + */ + public function clearFilters() { + $this->filters->clear(); + } + + /** + * @throws \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException + */ + public final function getVars() { + throw new UnsupportedAsseticBehaviorException("Drupal does not use or support Assetic's 'vars' concept."); + } + + /** + * @throws \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException + */ + public final function setValues(array $values) { + throw new UnsupportedAsseticBehaviorException("Drupal does not use or support Assetic's 'values' concept."); + } + + /** + * @throws \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException + */ + public final function getValues() { + throw new UnsupportedAsseticBehaviorException("Drupal does not use or support Assetic's 'values' concept."); + } +} diff --git a/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregateInterface.php b/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregateInterface.php index 7b40820..1a71540 100644 --- a/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregateInterface.php +++ b/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregateInterface.php @@ -35,42 +35,4 @@ */ interface AssetAggregateInterface extends AssetInterface, AssetCollectionBasicInterface, AsseticAssetCollectionInterface { - /** - * Replaces an existing asset in the aggregate with a new one. - * - * This maintains ordering of the assets within the aggregate; the new asset - * will occupy the same position as the old asset. - * - * @param AssetInterface|string $needle - * Either an AssetInterface instance, or the string id of an asset. - * @param AssetInterface $replacement - * The new asset to swap into place. - * @param bool $graceful - * Whether failure should return FALSE or throw an exception. - * - * @return bool - * - * @throws \OutOfBoundsException - */ - public function replace($needle, AssetInterface $replacement, $graceful = FALSE); - - /** - * Adds an asset to this aggregate. - * - * @param AsseticAssetInterface $asset - * The asset to add. Note that, despite the type requirements, it must - * conform to Drupal's AssetInterface. - * - * @return bool - * TRUE if the asset was added successfully, FALSE if it was already present - * in the aggregate. - * - * @throws UnsupportedAsseticBehaviorException - * Thrown if a vanilla Assetic asset is provided. - * - * @throws AssetTypeMismatchException - * Thrown if the provided asset is not the correct type for the aggregate - * (e.g., CSS file in a JS aggregate). - */ - public function add(AsseticAssetInterface $asset); } \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Aggregate/BaseAggregateAsset.php b/core/lib/Drupal/Core/Asset/Aggregate/BaseAggregateAsset.php deleted file mode 100644 index c795a0e..0000000 --- a/core/lib/Drupal/Core/Asset/Aggregate/BaseAggregateAsset.php +++ /dev/null @@ -1,365 +0,0 @@ -metadata = $metadata; - $this->assetStorage = new \SplObjectStorage(); - $this->nestedStorage = new \SplObjectStorage(); - - foreach ($assets as $asset) { - $this->add($asset); - } - } - - /** - * {@inheritdoc} - */ - public function id() { - if (empty($this->id)) { - $this->calculateId(); - } - - return $this->id; - } - - /** - * {@inheritdoc} - */ - public function getAssetType() { - return $this->metadata->getType(); - } - - /** - * Calculates and stores an id for this aggregate from the contained assets. - * - * @return void - */ - protected function calculateId() { - $id = ''; - foreach ($this as $asset) { - $id .= $asset->id(); - } - // TODO come up with something stabler/more serialization friendly than object hash - $this->id = hash('sha256', $id ?: spl_object_hash($this)); - } - - /** - * {@inheritdoc} - */ - public function getMetadata() { - // TODO should this immutable? doable if we further granulate the interfaces - return $this->metadata; - } - - /** - * {@inheritdoc} - */ - public function add(AsseticAssetInterface $asset) { - if (!$asset instanceof AssetInterface) { - throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset provided; Drupal aggregates require Drupal-flavored assets.'); - } - $this->ensureCorrectType($asset); - - if ($this->contains($asset) || $this->getById($asset->id())) { - return FALSE; - } - - $this->assetStorage->attach($asset); - $this->assetIdMap[$asset->id()] = $asset; - - if ($asset instanceof AssetAggregateInterface) { - $this->nestedStorage->attach($asset); - } - - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function contains(AssetInterface $asset) { - if ($this->assetStorage->contains($asset)) { - return TRUE; - } - - foreach ($this->nestedStorage as $aggregate) { - if ($aggregate->contains($asset)) { - return TRUE; - } - } - - return FALSE; - } - - /** - * {@inheritdoc} - */ - public function getById($id, $graceful = TRUE) { - if (isset($this->assetIdMap[$id])) { - return $this->assetIdMap[$id]; - } - else { - // Recursively search for the id - foreach ($this->nestedStorage as $aggregate) { - if ($found = $aggregate->getById($id)) { - return $found; - } - } - } - - if ($graceful) { - return FALSE; - } - - throw new \OutOfBoundsException(sprintf('This aggregate does not contain an asset with id %s.', $id)); - } - - /** - * {@inheritdoc} - */ - public function remove($needle, $graceful = TRUE) { - if (is_string($needle)) { - if (!$needle = $this->getById($needle, $graceful)) { - return FALSE; - } - } - - return $this->removeLeaf($needle, $graceful); - } - - /** - * {@inheritdoc} - */ - public function removeLeaf(AsseticAssetInterface $needle, $graceful = FALSE) { - if (!$needle instanceof AssetInterface) { - throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset provided; Drupal aggregates require Drupal-flavored assets.'); - } - $this->ensureCorrectType($needle); - - foreach ($this->assetIdMap as $id => $asset) { - if ($asset === $needle) { - unset($this->assetStorage[$asset], $this->assetIdMap[$id], $this->nestedStorage[$asset]); - - return TRUE; - } - - if ($asset instanceof AssetAggregateInterface && $asset->removeLeaf($needle, $graceful)) { - return TRUE; - } - } - - if ($graceful) { - return FALSE; - } - - throw new \OutOfBoundsException('Asset not found.'); - } - - /** - * {@inheritdoc} - */ - public function replace($needle, AssetInterface $replacement, $graceful = TRUE) { - if (is_string($needle)) { - if (!$needle = $this->getById($needle, $graceful)) { - return FALSE; - } - } - - return $this->replaceLeaf($needle, $replacement, $graceful); - } - - /** - * {@inheritdoc} - */ - public function replaceLeaf(AsseticAssetInterface $needle, AsseticAssetInterface $replacement, $graceful = FALSE) { - if (!($needle instanceof AssetInterface && $replacement instanceof AssetInterface)) { - throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset(s) provided; Drupal aggregates require Drupal-flavored assets.'); - } - $this->ensureCorrectType($needle); - $this->ensureCorrectType($replacement); - - if ($this->contains($replacement)) { - throw new \LogicException('Asset provided for replacement is already present in the aggregate.'); - } - - $i = 0; - foreach ($this->assetIdMap as $id => $asset) { - if ($asset === $needle) { - unset($this->assetStorage[$asset], $this->nestedStorage[$asset]); - - array_splice($this->assetIdMap, $i, 1, array($replacement->id() => $replacement)); - $this->assetStorage->attach($replacement); - if ($replacement instanceof AssetAggregateInterface) { - $this->nestedStorage->attach($replacement); - } - - return TRUE; - } - - if ($asset instanceof AssetAggregateInterface && $asset->replaceLeaf($needle, $replacement, $graceful)) { - return TRUE; - } - $i++; - } - - if ($graceful) { - return FALSE; - } - - throw new \OutOfBoundsException('Asset not found.'); - } - - /** - * {@inheritdoc} - * - * Aggregate assets are inherently eligible for preprocessing, so this is - * always true. - */ - public function isPreprocessable() { - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function all() { - return $this->assetIdMap; - } - - /** - * {@inheritdoc} - */ - public function load(FilterInterface $additionalFilter = NULL) { - // loop through leaves and load each asset - $parts = array(); - foreach ($this as $asset) { - $asset->load($additionalFilter); - $parts[] = $asset->getContent(); - } - - $this->content = implode("\n", $parts); - } - - /** - * {@inheritdoc} - */ - public function dump(FilterInterface $additionalFilter = NULL) { - // loop through leaves and dump each asset - $parts = array(); - foreach ($this as $asset) { - $parts[] = $asset->dump($additionalFilter); - } - - return implode("\n", $parts); - } - - /** - * {@inheritdoc} - */ - public function getContent() { - return $this->content; - } - - /** - * {@inheritdoc} - */ - public function setContent($content) { - $this->content = $content; - } - - /** - * TODO Assetic uses their iterator to clone, then populate values and return here; is that a good model for us? - */ - public function getIterator() { - return new \RecursiveIteratorIterator(new AssetAggregateIterator($this)); - } - - /** - * Indicates whether this collection contains any assets. - * - * @return bool - * TRUE if contained assets are present, FALSE otherwise. - */ - public function isEmpty() { - return $this->assetStorage->count() === 0; - } - - /** - * Ensures that the asset is of the correct subtype (e.g., css vs. js). - * - * @param AssetInterface $asset - * - * @throws \Drupal\Core\Asset\Exception\AssetTypeMismatchException - */ - abstract protected function ensureCorrectType(AssetInterface $asset); -} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Aggregate/CssAggregateAsset.php b/core/lib/Drupal/Core/Asset/Aggregate/CssAggregateAsset.php deleted file mode 100644 index 10ca555..0000000 --- a/core/lib/Drupal/Core/Asset/Aggregate/CssAggregateAsset.php +++ /dev/null @@ -1,40 +0,0 @@ -getAssetType() !== 'css') { - throw new AssetTypeMismatchException('CSS aggregates can only work with CSS assets.'); - } - } -} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Aggregate/Iterator/AssetAggregateIterator.php b/core/lib/Drupal/Core/Asset/Aggregate/Iterator/AssetAggregateIterator.php deleted file mode 100644 index ed9eec1..0000000 --- a/core/lib/Drupal/Core/Asset/Aggregate/Iterator/AssetAggregateIterator.php +++ /dev/null @@ -1,23 +0,0 @@ -all()); - } - - public function hasChildren() { - return $this->current() instanceof AssetAggregateInterface; - } -} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Aggregate/JsAggregateAsset.php b/core/lib/Drupal/Core/Asset/Aggregate/JsAggregateAsset.php deleted file mode 100644 index 2c9f86d..0000000 --- a/core/lib/Drupal/Core/Asset/Aggregate/JsAggregateAsset.php +++ /dev/null @@ -1,40 +0,0 @@ -getAssetType() !== 'js') { - throw new AssetTypeMismatchException('JS aggregates can only work with JS assets.'); - } - } -} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/AsseticAdapterAsset.php b/core/lib/Drupal/Core/Asset/AsseticAdapterAsset.php index de2980c..a1b28fc 100644 --- a/core/lib/Drupal/Core/Asset/AsseticAdapterAsset.php +++ b/core/lib/Drupal/Core/Asset/AsseticAdapterAsset.php @@ -7,7 +7,7 @@ namespace Drupal\Core\Asset; -use Assetic\Asset\AssetInterface; +use Assetic\Asset\AssetInterface as AsseticAssetInterface; use Assetic\Asset\BaseAsset as AsseticBaseAsset; use Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException; @@ -15,7 +15,7 @@ * A class that reduces boilerplate code by centrally disabling the Assetic * properties and methods Drupal does not support. */ -abstract class AsseticAdapterAsset extends AsseticBaseAsset implements AssetInterface { +abstract class AsseticAdapterAsset extends AsseticBaseAsset implements AsseticAssetInterface { /** * @throws \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException diff --git a/core/lib/Drupal/Core/Asset/Collection/AssetCollection.php b/core/lib/Drupal/Core/Asset/Collection/AssetCollection.php index afaca2e..e795a95 100644 --- a/core/lib/Drupal/Core/Asset/Collection/AssetCollection.php +++ b/core/lib/Drupal/Core/Asset/Collection/AssetCollection.php @@ -6,6 +6,7 @@ */ namespace Drupal\Core\Asset\Collection; +use Assetic\Asset\AssetInterface as AsseticAssetInterface; use Drupal\Core\Asset\Collection\AssetCollectionInterface; use Drupal\Core\Asset\AssetInterface; use Drupal\Core\Asset\Collection\Iterator\AssetSubtypeFilterIterator; @@ -18,91 +19,16 @@ * * TODO With PHP5.4, refactor out AssetCollectionBasicInterface into a trait. */ -class AssetCollection implements \IteratorAggregate, AssetCollectionInterface { - - protected $assetStorage; - - protected $assetIdMap = array(); +class AssetCollection extends BasicAssetCollection implements AssetCollectionInterface { protected $frozen = FALSE; - public function __construct() { - $this->assetStorage = new \SplObjectStorage(); - } - - /** - * {@inheritdoc} - */ - public function add(AssetInterface $asset) { - $this->attemptWrite(); - - if ($this->contains($asset) || $this->getById($asset->id())) { - return FALSE; - } - - $this->assetStorage->attach($asset); - $this->assetIdMap[$asset->id()] = $asset; - - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function contains(AssetInterface $asset) { - // TODO decide whether to do this by id or object instance - return $this->assetStorage->contains($asset); - } - - /** - * {@inheritdoc} - */ - public function getById($id, $graceful = TRUE) { - if (isset($this->assetIdMap[$id])) { - return $this->assetIdMap[$id]; - } - else if ($graceful) { - return FALSE; - } - - throw new \OutOfBoundsException(sprintf('This collection does not contain an asset with id %s.', $id)); - } - /** * {@inheritdoc} */ - public function remove($needle, $graceful = TRUE) { - // TODO fix horrible complexity of conditionals, exceptions, and returns. + public function add(AsseticAssetInterface $asset) { $this->attemptWrite(); - - // Validate and normalize type to AssetInterface - if (is_string($needle)) { - if (!$needle = $this->getById($needle, $graceful)) { - // Asset couldn't be found but we're in graceful mode - return FALSE. - return FALSE; - } - } - else if (!$needle instanceof AssetInterface) { - throw new \InvalidArgumentException('Invalid type provided to AssetCollection::remove(); must provide either a string asset id or AssetInterface instance.'); - } - - // Check for membership - if ($this->contains($needle)) { - unset($this->assetIdMap[$needle->id()], $this->assetStorage[$needle]); - return TRUE; - } - else if (!$graceful) { - throw new \OutOfBoundsException(sprintf('This collection does not contain an asset with id %s.', $needle->id())); - } - - return FALSE; - } - - /** - * {@inheritdoc} - */ - public function all() { - return $this->assetIdMap; + return parent::add($asset); } /** @@ -127,29 +53,31 @@ public function mergeCollection(AssetCollectionInterface $collection, $freeze = /** * {@inheritdoc} */ - public function freeze() { - $this->frozen = TRUE; + public function remove($needle, $graceful = FALSE) { + $this->attemptWrite(); + return parent::remove($needle, $graceful); } /** * {@inheritdoc} */ - public function isFrozen() { - return $this->frozen; + public function replace($needle, AssetInterface $replacement, $graceful = FALSE) { + $this->attemptWrite(); + return parent::replace($needle, $replacement, $graceful); } /** * {@inheritdoc} */ - public function getIterator() { - return new \ArrayIterator($this->assetIdMap); + public function freeze() { + $this->frozen = TRUE; } /** * {@inheritdoc} */ - public function isEmpty() { - return empty($this->assetIdMap); + public function isFrozen() { + return $this->frozen; } /** @@ -157,7 +85,7 @@ public function isEmpty() { */ public function getCss() { $collection = new self(); - foreach (new AssetSubtypeFilterIterator($this->getIterator(), 'css') as $asset) { + foreach (new AssetSubtypeFilterIterator(new \ArrayIterator($this->all()), 'css') as $asset) { $collection->add($asset); } @@ -169,7 +97,7 @@ public function getCss() { */ public function getJs() { $collection = new self(); - foreach (new AssetSubtypeFilterIterator($this->getIterator(), 'js') as $asset) { + foreach (new AssetSubtypeFilterIterator(new \ArrayIterator($this->all()), 'js') as $asset) { $collection->add($asset); } diff --git a/core/lib/Drupal/Core/Asset/Collection/AssetCollectionBasicInterface.php b/core/lib/Drupal/Core/Asset/Collection/AssetCollectionBasicInterface.php index b837b6e..e24ef3e 100644 --- a/core/lib/Drupal/Core/Asset/Collection/AssetCollectionBasicInterface.php +++ b/core/lib/Drupal/Core/Asset/Collection/AssetCollectionBasicInterface.php @@ -7,6 +7,9 @@ namespace Drupal\Core\Asset\Collection; use Drupal\Core\Asset\AssetInterface; +use Assetic\Asset\AssetInterface as AsseticAssetInterface; +use Drupal\Core\Asset\Exception\AssetTypeMismatchException; +use Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException; /** * Describes an asset collection: a container for assets. @@ -21,31 +24,36 @@ * @see \Drupal\Core\Asset\Aggregate\AssetAggregateInterface * @see \Drupal\Core\Asset\Collection\AssetCollectionInterface */ -interface AssetCollectionBasicInterface extends \Traversable { +interface AssetCollectionBasicInterface extends \Traversable, \Countable { /** - * Removes an asset from the aggregate. + * Adds an asset to this aggregate. * - * Wraps Assetic's AssetCollection::removeLeaf() to ease removal of keys. - * - * @param AssetInterface|string $needle - * Either an AssetInterface instance, or the string id of an asset. - * @param bool $graceful - * Whether failure should return FALSE or throw an exception. + * @param AsseticAssetInterface $asset + * The asset to add. Note that, despite the type requirements, it must + * conform to Drupal's AssetInterface. * * @return bool + * TRUE if the asset was added successfully, FALSE if it was already present + * in the aggregate. * - * @throws \OutOfBoundsException + * @throws UnsupportedAsseticBehaviorException + * Thrown if a vanilla Assetic asset is provided. + * + * @throws AssetTypeMismatchException + * Thrown if the provided asset is not the correct type for the aggregate + * (e.g., CSS file in a JS aggregate). */ - public function remove($needle, $graceful = FALSE); + public function add(AsseticAssetInterface $asset); /** - * Indicates whether this collection contains the provided asset. - * + * Indicates whether this collection contains the given asset. + * * @param AssetInterface $asset - * Either an AssetInterface instance, or the string id of an asset. + * The asset to check for membership in the collection. * * @return bool + * TRUE if the asset is present in the collection, FALSE otherwise. */ public function contains(AssetInterface $asset); @@ -69,10 +77,95 @@ public function contains(AssetInterface $asset); public function getById($id, $graceful = TRUE); /** - * Indicates whether this collection contains any assets. + * Removes an asset from the collection. + * + * @param AssetInterface|string $needle + * Either an AssetInterface instance, or the string id of an asset. + * @param bool $graceful + * Whether failure should return FALSE or throw an exception. * * @return bool - * TRUE if contained assets are present, FALSE otherwise. + * TRUE on success, FALSE on failure to locate the given asset (or an + * exception, depending on the value of $graceful). + * + * @throws \OutOfBoundsException + * Thrown if $needle could not be located and $graceful = FALSE. + */ + public function remove($needle, $graceful = FALSE); + + /** + * Replaces an existing asset in the aggregate with a new one. + * + * This preserves ordering of the assets within the collection: the new asset + * will occupy the same position as the old asset. + * + * @param AssetInterface|string $needle + * Either an AssetInterface instance, or the string id of an asset. + * @param AssetInterface $replacement + * The new asset to swap into place. + * @param bool $graceful + * Whether failure should return FALSE or throw an exception. + * + * @return bool + * TRUE on success, FALSE on failure to locate the given asset (or an + * exception, depending on the value of $graceful). + * + * @throws \OutOfBoundsException + * Thrown if $needle could not be located and $graceful = FALSE. + */ + public function replace($needle, AssetInterface $replacement, $graceful = FALSE); + + /** + * Indicates whether the collection contains any assets. + * + * Note that this will only return TRUE if leaf assets are present - that is, + * assets that do NOT implement AssetCollectionBasicInterface. + * + * @return bool + * TRUE if the collection is devoid of any leaf assets, FALSE otherwise. */ public function isEmpty(); -} \ No newline at end of file + + /** + * Returns all top-level child assets as an array. + * + * To retrieve assets regardless of nesting level, see the iterators: + * + * @see AssetCollectionBasicInterface::each() + * @see AssetCollectionBasicInterface::eachLeaf() + * + * @return AssetInterface[] + */ + public function all(); + + /** + * Returns the total number of leaf assets in this collection. + * + * Non-leaf assets - objects implementing AssetCollectionBasicInterface - are + * not included in the count. + * + * @return int + */ + public function count(); + + /** + * Retrieves a traversable that will return all contained assets. + * + * 'All' assets includes both AssetCollectionBasicInterface objects and plain + * AssetInterface objects. + * + * @return \Traversable + */ + public function each(); + + /** + * Retrieves a traversable that returns only contained leaf assets. + * + * Leaf assets are objects that only implement AssetInterface, not + * AssetCollectionBasicInterface. + * + * @return \Traversable + */ + public function eachLeaf(); +} + diff --git a/core/lib/Drupal/Core/Asset/Collection/AssetCollectionInterface.php b/core/lib/Drupal/Core/Asset/Collection/AssetCollectionInterface.php index 8fb6f1a..6b7a8ce 100644 --- a/core/lib/Drupal/Core/Asset/Collection/AssetCollectionInterface.php +++ b/core/lib/Drupal/Core/Asset/Collection/AssetCollectionInterface.php @@ -12,32 +12,13 @@ /** * Describes an asset collection. * - * TODO we need a few more methods here to deal with asset type disambiguation and library resolution + * TODO we need a few more methods here to deal with library resolution * * @see \Drupal\Core\Asset\Collection\AssetCollectionBasicInterface */ interface AssetCollectionInterface extends AssetCollectionBasicInterface { /** - * Returns all assets contained in this collection. - * - * @return AssetInterface[] - */ - public function all(); - - /** - * Adds an asset to the collection. - * - * @param \Drupal\Core\Asset\AssetInterface $asset - * The asset to add. - * - * @return bool - * TRUE if the asset was added successfully, FALSE if it was already present - * in the collection. - */ - public function add(AssetInterface $asset); - - /** * Merges another asset collection into this one. * * If an asset is present in both collections, as identified by diff --git a/core/lib/Drupal/Core/Asset/Collection/BasicAssetCollection.php b/core/lib/Drupal/Core/Asset/Collection/BasicAssetCollection.php new file mode 100644 index 0000000..034ea07 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Collection/BasicAssetCollection.php @@ -0,0 +1,321 @@ +assetStorage = new \SplObjectStorage(); + $this->nestedStorage = new \SplObjectStorage(); + + foreach ($assets as $asset) { + $this->add($asset); + } + } + + /** + * {@inheritdoc} + */ + public function add(AsseticAssetInterface $asset) { + if (!$asset instanceof AssetInterface) { + throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset provided; Drupal collections require Drupal-flavored assets.'); + } + $this->ensureCorrectType($asset); + + if ($this->contains($asset) || $this->getById($asset->id())) { + return FALSE; + } + + $this->assetStorage->attach($asset); + $this->assetIdMap[$asset->id()] = $asset; + + if ($asset instanceof AssetCollectionBasicInterface) { + $this->nestedStorage->attach($asset); + } + + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function contains(AssetInterface $asset) { + if ($this->assetStorage->contains($asset)) { + return TRUE; + } + + foreach ($this->nestedStorage as $aggregate) { + if ($aggregate->contains($asset)) { + return TRUE; + } + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getById($id, $graceful = TRUE) { + if (isset($this->assetIdMap[$id])) { + return $this->assetIdMap[$id]; + } + else { + // Recursively search for the id + foreach ($this->nestedStorage as $aggregate) { + if ($found = $aggregate->getById($id)) { + return $found; + } + } + } + + if ($graceful) { + return FALSE; + } + + throw new \OutOfBoundsException(sprintf('This collection does not contain an asset with id %s.', $id)); + } + + /** + * {@inheritdoc} + */ + public function remove($needle, $graceful = FALSE) { + if (is_string($needle)) { + if (!$needle = $this->getById($needle, $graceful)) { + return FALSE; + } + } + else if (!$needle instanceof AssetInterface) { + throw new \InvalidArgumentException('Invalid type provided to AssetCollectionBasicInterface::replace(); must provide either a string asset id or AssetInterface instance.'); + } + + return $this->doRemove($needle, $graceful); + } + + /** + * Performs the actual work of removing an asset from the collection. + * + * @param AssetInterface|string $needle + * Either an AssetInterface instance, or the string id of an asset. + * @param bool $graceful + * Whether failure should return FALSE or throw an exception. + * + * @return bool + * TRUE on success, FALSE on failure to locate the given asset (or an + * exception, depending on the value of $graceful). + * + * @throws \OutOfBoundsException + * Thrown if $needle could not be located and $graceful = FALSE. + */ + protected function doRemove(AssetInterface $needle, $graceful) { + foreach ($this->assetIdMap as $id => $asset) { + if ($asset === $needle) { + unset($this->assetStorage[$asset], $this->assetIdMap[$id], $this->nestedStorage[$asset]); + + return TRUE; + } + + if ($asset instanceof AssetCollectionBasicInterface && $asset->doRemove($needle, TRUE)) { + return TRUE; + } + } + + if ($graceful) { + return FALSE; + } + + throw new \OutOfBoundsException('Provided asset was not found in the collection.'); + } + + /** + * {@inheritdoc} + */ + public function replace($needle, AssetInterface $replacement, $graceful = FALSE) { + if (is_string($needle)) { + if (!$needle = $this->getById($needle, $graceful)) { + return FALSE; + } + } + else if (!$needle instanceof AssetInterface) { + throw new \InvalidArgumentException('Invalid type provided to AssetCollectionBasicInterface::replace(); must provide either a string asset id or AssetInterface instance.'); + } + + $this->ensureCorrectType($replacement); + if ($this->contains($replacement)) { + throw new \LogicException('Asset to be swapped in is already present in the collection.'); + } + + return $this->doReplace($needle, $replacement, $graceful); + } + + /** + * Performs the actual work of replacing one asset with another. + * + * @param AssetInterface $needle + * The AssetInterface instance to swap out. + * @param AssetInterface $replacement + * The new asset to swap in. + * @param bool $graceful + * Whether failure should return FALSE or throw an exception. + * + * @return bool + * TRUE on success, FALSE on failure to locate the given asset (or an + * exception, depending on the value of $graceful). + * + * @throws \OutOfBoundsException + */ + protected function doReplace(AssetInterface $needle, AssetInterface $replacement, $graceful) { + $i = 0; + foreach ($this->assetIdMap as $id => $asset) { + if ($asset === $needle) { + unset($this->assetStorage[$asset], $this->nestedStorage[$asset]); + + array_splice($this->assetIdMap, $i, 1, array($replacement->id() => $replacement)); + $this->assetStorage->attach($replacement); + if ($replacement instanceof AssetCollectionBasicInterface) { + $this->nestedStorage->attach($replacement); + } + + return TRUE; + } + + if ($asset instanceof AssetCollectionBasicInterface && $asset->doReplace($needle, $replacement, TRUE)) { + return TRUE; + } + $i++; + } + + if ($graceful) { + return FALSE; + } + + throw new \OutOfBoundsException('Provided asset was not found in the collection.'); + } + + /** + * {@inheritdoc} + */ + public function all() { + return $this->assetIdMap; + } + + /** + * {@inheritdoc} + * TODO Assetic uses their iterator to clone, then populate values and return here; is that a good model for us? + */ + public function getIterator() { + return new \RecursiveIteratorIterator(new RecursiveBasicCollectionIterator($this), \RecursiveIteratorIterator::SELF_FIRST); + } + + /** + * {@inheritdoc} + */ + public function each() { + return $this->getIterator(); + } + + /** + * {@inheritdoc} + */ + public function eachLeaf() { + return new \RecursiveIteratorIterator(new RecursiveBasicCollectionIterator($this)); + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + $maincount = $this->assetStorage->count(); + if ($maincount === 0) { + return TRUE; + } + + $i = 0; + foreach ($this->nestedStorage as $aggregate) { + if (!$aggregate->isEmpty()) { + return FALSE; + } + $i++; + } + + return $i === $maincount; + } + + /** + * {@inheritdoc} + */ + public function count() { + if ($this->nestedStorage->count() === 0) { + return $this->assetStorage->count(); + } + + $c = $i = 0; + foreach ($this->nestedStorage as $collection) { + $c += $collection->count(); + $i++; + } + + return $this->assetStorage->count() - $i + $c; + } + + /** + * Ensures that the asset is the correct type for this collection. + * + * "Type" here refers to 'css' vs. 'js'. + * + * BasicAssetCollection's implementation has no body because it has no type + * restrictions; only aggregates do. + * + * @param AssetInterface $asset + * + * @throws \Drupal\Core\Asset\Exception\AssetTypeMismatchException + */ + protected function ensureCorrectType(AssetInterface $asset) {} +} + diff --git a/core/lib/Drupal/Core/Asset/Collection/Iterator/RecursiveBasicCollectionIterator.php b/core/lib/Drupal/Core/Asset/Collection/Iterator/RecursiveBasicCollectionIterator.php new file mode 100644 index 0000000..2557a03 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Collection/Iterator/RecursiveBasicCollectionIterator.php @@ -0,0 +1,24 @@ +all()); + } + + public function hasChildren() { + return $this->current() instanceof AssetCollectionBasicInterface; + } +} \ No newline at end of file diff --git a/core/tests/Drupal/Tests/Core/Asset/Aggregate/AssetAggregateTest.php b/core/tests/Drupal/Tests/Core/Asset/Aggregate/AssetAggregateTest.php new file mode 100644 index 0000000..e6a90a7 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Aggregate/AssetAggregateTest.php @@ -0,0 +1,324 @@ + 'Asset aggregate tests', + 'description' => 'Unit tests on AssetAggregate', + 'group' => 'Asset', + ); + } + + /** + * Generates a AssetAggregate mock with three leaf assets. + */ + public function getThreeLeafAggregate() { + $aggregate = $this->getAggregate(); + $nested_aggregate = $this->getAggregate(); + + foreach (array('foo', 'bar', 'baz') as $var) { + $$var = $this->createStubFileAsset('css', $var); + } + + $nested_aggregate->add($foo); + $nested_aggregate->add($bar); + $aggregate->add($nested_aggregate); + $aggregate->add($baz); + + return array($aggregate, $foo, $bar, $baz, $nested_aggregate); + } + + /** + * Returns an AssetAggregate, the base collection type for this unit test. + * + * @return AssetCollectionBasicInterface + */ + public function getCollection() { + return $this->getAggregate(); + } + + public function testGetAssetType() { + $mockmeta = $this->getMock('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag', array(), array(), '', FALSE); + $mockmeta->expects($this->once()) + ->method('getType') + ->will($this->returnValue('unicorns')); + $aggregate = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Aggregate\\AssetAggregate', array($mockmeta)); + + $this->assertEquals('unicorns', $aggregate->getAssetType()); + } + + public function testGetMetadata() { + $mockmeta = $this->createStubAssetMetadata(); + $aggregate = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Aggregate\\AssetAggregate', array($mockmeta)); + + $this->assertSame($mockmeta, $aggregate->getMetadata()); + } + + /** + * This uses PHPUnit's reflection-based assertions rather than assertContains + * so that this test can honestly sit at the root of the test method + * dependency tree. + * + * @covers ::add + */ + public function testAdd() { + $aggregate = $this->getAggregate(); + $asset = $this->createStubFileAsset(); + $this->assertTrue($aggregate->add($asset)); + + $this->assertAttributeContains($asset, 'assetStorage', $aggregate); + $this->assertAttributeContains($asset, 'assetIdMap', $aggregate); + + // Nesting: add an aggregate to the first aggregate. + $nested_aggregate = $this->getAggregate(); + $aggregate->add($nested_aggregate); + + $this->assertAttributeContains($nested_aggregate, 'assetStorage', $aggregate); + $this->assertAttributeContains($nested_aggregate, 'assetIdMap', $aggregate); + $this->assertAttributeContains($nested_aggregate, 'nestedStorage', $aggregate); + } + + /** + * @depends testAdd + * @covers ::ensureCorrectType + * @expectedException \Drupal\Core\Asset\Exception\AssetTypeMismatchException + */ + public function testAddEnsureCorrectType() { + $aggregate = $this->getAggregate(); + $aggregate->add($this->createStubFileAsset('js')); + } + + /** + * @depends testAdd + * @covers ::each + * @covers ::getIterator + * @covers \Drupal\Core\Asset\Collection\Iterator\RecursiveBasicCollectionIterator + */ + public function testEach() { + list($aggregate, $foo, $bar, $baz, $nested_aggregate) = $this->getThreeLeafAggregate(); + + $contained = array(); + foreach ($aggregate->each() as $leaf) { + $contained[] = $leaf; + } + $this->assertEquals(array($nested_aggregate, $foo, $bar, $baz), $contained); + } + + /** + * @depends testAdd + * @depends testEach + * @covers ::__construct + */ + public function testCreateWithAssets() { + $asset1 = $this->createStubFileAsset(); + $asset2 = $this->createStubFileAsset(); + $meta = $this->createStubAssetMetadata(); + $collection = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Aggregate\\AssetAggregate', array($meta, array($asset1, $asset2))); + + $this->assertContains($asset1, $collection); + $this->assertContains($asset2, $collection); + } + + /** + * @depends testAdd + * @covers ::id + * @covers ::calculateId + */ + public function testId() { + // Simple case - test with one contained asset first. + $aggregate = $this->getAggregate(); + $asset1 = $this->createStubFileAsset(); + $aggregate->add($asset1); + + $this->assertEquals(hash('sha256', $asset1->id()), $aggregate->id()); + + // Now use two contained assets, one nested in another aggregate. + $aggregate = $this->getAggregate(); + $aggregate->add($asset1); + + $aggregate2 = $this->getAggregate(); + $asset2 = $this->createStubFileAsset(); + $aggregate2->add($asset2); + + $aggregate->add($aggregate2); + + // The aggregate only uses leaf, non-aggregate assets to determine its id. + $this->assertEquals(hash('sha256', $asset1->id() . $asset2->id()), $aggregate->id()); + } + + public function testIsPreprocessable() { + $this->assertTrue($this->getAggregate()->isPreprocessable()); + } + + /** + * @depends testEach + * @covers ::removeLeaf + * @expectedException \OutOfBoundsException + */ + public function testRemoveNonexistentNeedle() { + list($aggregate) = $this->getThreeLeafAggregate(); + // Nonexistent leaf removal returns FALSE in graceful mode + $this->assertFalse($aggregate->removeLeaf($this->createStubFileAsset(), TRUE)); + + // In non-graceful mode, an exception is thrown. + $aggregate->removeLeaf($this->createStubFileAsset()); + } + + /** + * @covers ::removeLeaf + * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException + */ + public function testRemoveLeafVanillaAsseticAsset() { + $aggregate = $this->getAggregate(); + $vanilla = $this->getMock('\\Assetic\\Asset\\BaseAsset', array(), array(), '', FALSE); + $aggregate->removeLeaf($vanilla); + } + + /** + * @depends testAdd + * @covers ::ensureCorrectType + * @expectedException \Drupal\Core\Asset\Exception\AssetTypeMismatchException + */ + public function testReplaceLeafEnsureCorrectType() { + $aggregate = $this->getAggregate(); + $asset1 = $this->createStubFileAsset(); + $aggregate->add($asset1); + + $asset2 = $this->createStubFileAsset('js'); + $aggregate->replaceLeaf($asset1, $asset2); + } + + /** + * @depends testAdd + * @covers ::ensureCorrectType + * @expectedException \Drupal\Core\Asset\Exception\AssetTypeMismatchException + */ + public function testReplaceEnsureCorrectType() { + $aggregate = $this->getAggregate(); + $asset1 = $this->createStubFileAsset(); + $aggregate->add($asset1); + + $asset2 = $this->createStubFileAsset('js'); + $aggregate->replace($asset1, $asset2); + } + + /** + * @depends testEach + * @covers ::replaceLeaf + * @expectedException \OutOfBoundsException + */ + public function testReplaceLeafNonexistentNeedle() { + list($aggregate) = $this->getThreeLeafAggregate(); + // Nonexistent leaf replacement returns FALSE in graceful mode + $qux = $this->createStubFileAsset(); + $this->assertFalse($aggregate->replaceLeaf($this->createStubFileAsset(), $qux, TRUE)); + $this->assertNotContains($qux, $aggregate); + + // In non-graceful mode, an exception is thrown. + $aggregate->replaceLeaf($this->createStubFileAsset(), $qux); + } + + /** + * @depends testEach + * @covers ::replaceLeaf + * @expectedException \LogicException + */ + public function testReplaceLeafWithAlreadyPresentAsset() { + list($aggregate, $foo) = $this->getThreeLeafAggregate(); + $aggregate->replaceLeaf($this->createStubFileAsset(), $foo); + } + + /** + * @depends testAdd + * @depends testReplaceLeafWithAlreadyPresentAsset + * @covers ::replace + * @expectedException \LogicException + * + * This fails on the same check that testReplaceLeafWithAlreadyPresentAsset, + * but it is demonstrated as its own test for clarity. + */ + public function testReplaceLeafWithSelf() { + list($aggregate, $foo) = $this->getThreeLeafAggregate(); + $aggregate->replaceLeaf($foo, $foo); + } + + /** + * @depends testAdd + * @covers ::replaceLeaf + */ + public function testReplaceLeafVanillaAsseticAsset() { + $aggregate = $this->getAggregate(); + $vanilla = $this->getMock('\\Assetic\\Asset\\BaseAsset', array(), array(), '', FALSE); + $drupally = $this->createStubFileAsset(); + + try { + $aggregate->replaceLeaf($vanilla, $drupally); + $this->fail('AssetAggregate::removeLeaf() did not throw an UnsupportedAsseticBehaviorException when provided a vanilla asset leaf.'); + } catch (UnsupportedAsseticBehaviorException $e) {} + + try { + $aggregate->replaceLeaf($vanilla, $vanilla); + $this->fail('AssetAggregate::removeLeaf() did not throw an UnsupportedAsseticBehaviorException when provided a vanilla asset leaf.'); + } catch (UnsupportedAsseticBehaviorException $e) {} + + try { + $aggregate->replaceLeaf($drupally, $vanilla); + $this->fail('AssetAggregate::removeLeaf() did not throw an UnsupportedAsseticBehaviorException when provided a vanilla asset leaf.'); + } catch (UnsupportedAsseticBehaviorException $e) {} + } + + /** + * @depends testAdd + * @covers ::load + */ + public function testLoad() { + $this->fail(); + } + + /** + * @depends testAdd + * @covers ::dump + */ + public function testDump() { + $this->fail(); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException + */ + public function testGetVars() { + $this->getAggregate()->getVars(); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException + */ + public function testSetValues() { + $this->getAggregate()->setValues(array()); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException + */ + public function testGetValues() { + $this->getAggregate()->getValues(); + } +} + diff --git a/core/tests/Drupal/Tests/Core/Asset/Aggregate/BaseAggregateAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/Aggregate/BaseAggregateAssetTest.php deleted file mode 100644 index ff8ba4a..0000000 --- a/core/tests/Drupal/Tests/Core/Asset/Aggregate/BaseAggregateAssetTest.php +++ /dev/null @@ -1,377 +0,0 @@ - 'Asset aggregate tests', - 'description' => 'Unit tests on BaseAggregateAsset', - 'group' => 'Asset', - ); - } - - /** - * Generates a simple BaseAggregateAsset mock. - * - * @param array $defaults - * Defaults to inject into the aggregate's metadata bag. - * - * @return BaseAggregateAsset - */ - public function getAggregate($defaults = array()) { - $mockmeta = $this->createStubAssetMetadata(); - return $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Aggregate\\BaseAggregateAsset', array($mockmeta)); - } - - /** - * Generates a BaseAggregateAsset mock with three leaf assets. - */ - public function getThreeLeafAggregate() { - $aggregate = $this->getAggregate(); - $nested_aggregate = $this->getAggregate(); - - foreach (array('foo', 'bar', 'baz') as $var) { - $$var = $this->getMock('Drupal\\Core\\Asset\\FileAsset', array(), array(), '', FALSE); - $$var->expects($this->any()) - ->method('id') - ->will($this->returnValue($var)); - } - - $aggregate->add($foo); - $nested_aggregate->add($bar); - $nested_aggregate->add($baz); - $aggregate->add($nested_aggregate); - - return array($aggregate, $foo, $bar, $baz); - } - - public function testGetAssetType() { - $mockmeta = $this->getMock('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag', array(), array(), '', FALSE); - $mockmeta->expects($this->once()) - ->method('getType') - ->will($this->returnValue('unicorns')); - $aggregate = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Aggregate\\BaseAggregateAsset', array($mockmeta)); - - $this->assertEquals('unicorns', $aggregate->getAssetType()); - } - - public function testGetMetadata() { - $mockmeta = $this->createStubAssetMetadata(); - $aggregate = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Aggregate\\BaseAggregateAsset', array($mockmeta)); - - $this->assertSame($mockmeta, $aggregate->getMetadata()); - } - - /** - * @covers ::add - */ - public function testAdd() { - $aggregate = $this->getAggregate(); - $asset = $this->createMockFileAsset('css'); - $this->assertTrue($aggregate->add($asset)); - - $this->assertContains($asset, $aggregate); - - // Nesting: add an aggregate to the first aggregate. - $nested_aggregate = $this->getAggregate(); - $nested_asset = $this->createMockFileAsset('css'); - - $nested_aggregate->add($nested_asset); - $aggregate->add($nested_asset); - - $this->assertContains($nested_asset, $aggregate); - } - - /** - * Tests that adding the same asset twice is disallowed. - * - * @depends testAdd - * @covers ::add - */ - public function testDoubleAdd() { - $aggregate = $this->getAggregate(); - $asset = $this->createMockFileAsset('css'); - $this->assertTrue($aggregate->add($asset)); - - // Test by object identity - $this->assertFalse($aggregate->add($asset)); - // Test by id - $asset2 = $this->getMock('Drupal\\Core\\Asset\\FileAsset', array(), array(), '', FALSE); - $asset2->expects($this->once()) - ->method('id') - ->will($this->returnValue($asset->id())); - - $this->assertFalse($aggregate->add($asset2)); - } - - /** - * @depends testAdd - * @covers ::contains - */ - public function testContains() { - $aggregate = $this->getAggregate(); - $asset = $this->createMockFileAsset('css'); - $aggregate->add($asset); - - $this->assertTrue($aggregate->contains($asset)); - - // Nesting: add an aggregate to the first aggregate. - $nested_aggregate = $this->getAggregate(); - $nested_asset = $this->createMockFileAsset('css'); - - $nested_aggregate->add($nested_asset); - $aggregate->add($nested_aggregate); - - $this->assertTrue($aggregate->contains($nested_asset)); - } - - /** - * @depends testAdd - * @covers ::id - * @covers ::calculateId - */ - public function testId() { - // Simple case - test with one contained asset first. - $aggregate = $this->getAggregate(); - $asset1 = $this->createMockFileAsset('css'); - $aggregate->add($asset1); - - $this->assertEquals(hash('sha256', $asset1->id()), $aggregate->id()); - - // Now use two contained assets, one nested in another aggregate. - $aggregate = $this->getAggregate(); - $aggregate->add($asset1); - - $aggregate2 = $this->getAggregate(); - $asset2 = $this->createMockFileAsset('css'); - $aggregate2->add($asset2); - - $aggregate->add($aggregate2); - - // The aggregate only uses leaf, non-aggregate assets to determine its id. - $this->assertEquals(hash('sha256', $asset1->id() . $asset2->id()), $aggregate->id()); - } - - /** - * @covers ::getById - * @expectedException \OutOfBoundsException - */ - public function testGetById() { - $aggregate = $this->getAggregate(); - - $asset = $this->createMockFileAsset('css'); - $aggregate->add($asset); - $this->assertSame($asset, $aggregate->getById($asset->id())); - - // Nonexistent asset - $this->assertFalse($aggregate->getById('bar')); - - // Nonexistent asset, non-graceful - $aggregate->getById('bar', FALSE); - } - - public function testIsPreprocessable() { - $this->assertTrue($this->getAggregate()->isPreprocessable()); - } - - /** - * @depends testAdd - * @covers ::all - */ - public function testAll() { - $aggregate = $this->getAggregate(); - - $asset1 = $this->createMockFileAsset('css'); - $asset2 = $this->createMockFileAsset('css'); - $aggregate->add($asset1); - $aggregate->add($asset2); - - $output = array( - $asset1->id() => $asset1, - $asset2->id() => $asset2, - ); - - $this->assertEquals($output, $aggregate->all()); - } - - /** - * @depends testAdd - * @covers ::replace - * @expectedException \OutOfBoundsException - */ - public function testIsEmpty() { - $aggregate = $this->getAggregate(); - $this->assertTrue($aggregate->isEmpty()); - - // Aggregates containing only empty aggregates are considered empty. - $aggregate->add($this->getAggregate()); - $this->assertTrue($aggregate->isEmpty()); - } - - /** - * remove() and removeLeaf() are conjoined; test them both here. - * - * @depends testAdd - * @covers ::remove - * @covers ::removeLeaf - */ - public function testRemove() { - list($aggregate, $foo, $bar, $baz) = $this->getThreeLeafAggregate(); - $this->assertTrue($aggregate->remove('foo')); - - $this->assertNotContains($foo, $aggregate); - $this->assertContains($bar, $aggregate); - $this->assertContains($baz, $aggregate); - - $this->assertTrue($aggregate->remove($bar)); - - $this->assertNotContains($bar, $aggregate); - $this->assertContains($baz, $aggregate); - } - - /** - * @depends testAdd - * @covers ::removeLeaf - * @expectedException \OutOfBoundsException - */ - public function testRemoveNonexistentNeedle() { - list($aggregate,,,) = $this->getThreeLeafAggregate(); - // Nonexistent leaf removal returns FALSE in graceful mode - $this->assertFalse($aggregate->removeLeaf($this->createMockFileAsset('css'))); - - // In non-graceful mode, an exception is thrown. - $aggregate->removeLeaf($this->createMockFileAsset('css'), FALSE); - } - - /** - * @covers ::removeLeaf - * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException - */ - public function testRemoveLeafVanillaAsseticAsset() { - $aggregate = $this->getAggregate(); - $vanilla = $this->getMock('\\Assetic\\Asset\\BaseAsset', array(), array(), '', FALSE); - $aggregate->removeLeaf($vanilla); - } - - /** - * replace() and replaceLeaf() are conjoined; test them both here. - * - * @depends testAdd - * @covers ::replace - * @covers ::replaceLeaf - */ - public function testReplace() { - list($aggregate, $foo, $bar, $baz) = $this->getThreeLeafAggregate(); - $qux = $this->getMock('Drupal\\Core\\Asset\\FileAsset', array(), array(), '', FALSE); - $qux->expects($this->any()) - ->method('id') - ->will($this->returnValue('qux')); - - $this->assertTrue($aggregate->replace('foo', $qux)); - - $this->assertContains($qux, $aggregate); - $this->assertNotContains($foo, $aggregate); - - $contained = array(); - foreach ($aggregate as $leaf) { - $contained[] = $leaf; - } - $this->assertEquals(array($qux, $bar, $baz), $contained); - - $this->assertTrue($aggregate->replace($bar, $foo)); - - $this->assertContains($foo, $aggregate); - $this->assertNotContains($bar, $aggregate); - - $contained = array(); - foreach ($aggregate as $leaf) { - $contained[] = $leaf; - } - $this->assertEquals(array($qux, $foo, $baz), $contained); - } - - /** - * @depends testAdd - * @covers ::replaceLeaf - * @expectedException \OutOfBoundsException - */ - public function testReplaceLeafNonexistentNeedle() { - list($aggregate,,,) = $this->getThreeLeafAggregate(); - // Nonexistent leaf replacement returns FALSE in graceful mode - $qux = $this->createMockFileAsset('css'); - $this->assertFalse($aggregate->replaceLeaf($this->createMockFileAsset('css'), $qux)); - $this->assertNotContains($qux, $aggregate); - - // In non-graceful mode, an exception is thrown. - $aggregate->replaceLeaf($this->createMockFileAsset('css'), $qux, FALSE); - } - - /** - * @depends testAdd - * @covers ::replaceLeaf - * @expectedException \LogicException - */ - public function testReplaceLeafWithAlreadyPresentAsset() { - list($aggregate, $foo, $bar, $baz) = $this->getThreeLeafAggregate(); - $aggregate->replaceLeaf($foo, $foo); - } - - /** - * @depends testAdd - * @covers ::replaceLeaf - */ - public function testReplaceLeafVanillaAsseticAsset() { - $aggregate = $this->getAggregate(); - $vanilla = $this->getMock('\\Assetic\\Asset\\BaseAsset', array(), array(), '', FALSE); - $drupally = $this->createMockFileAsset('css'); - - try { - $aggregate->replaceLeaf($vanilla, $drupally); - $this->fail('BaseAggregateAsset::removeLeaf() did not throw an UnsupportedAsseticBehaviorException when provided a vanilla asset leaf.'); - } catch (UnsupportedAsseticBehaviorException $e) {} - - try { - $aggregate->replaceLeaf($vanilla, $vanilla); - $this->fail('BaseAggregateAsset::removeLeaf() did not throw an UnsupportedAsseticBehaviorException when provided a vanilla asset leaf.'); - } catch (UnsupportedAsseticBehaviorException $e) {} - - try { - $aggregate->replaceLeaf($drupally, $vanilla); - $this->fail('BaseAggregateAsset::removeLeaf() did not throw an UnsupportedAsseticBehaviorException when provided a vanilla asset leaf.'); - } catch (UnsupportedAsseticBehaviorException $e) {} - } - - /** - * @depends testAdd - * @covers ::load - */ - public function testLoad() { - $this->fail(); - } - - /** - * @depends testAdd - * @covers ::dump - */ - public function testDump() { - $this->fail(); - } -} - diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetUnitTest.php index f2bc236..c34b382 100644 --- a/core/tests/Drupal/Tests/Core/Asset/AssetUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Asset/AssetUnitTest.php @@ -7,6 +7,7 @@ namespace Drupal\Tests\Core\Asset; +use Drupal\Core\Asset\Aggregate\AssetAggregate; use Drupal\Core\Asset\FileAsset; use Drupal\Core\Asset\Metadata\AssetMetadataBag; use Drupal\Tests\UnitTestCase; @@ -23,11 +24,15 @@ * id(), with a randomly generated name. * * @param string $type - * 'css' or 'js'. + * 'css' or 'js'. Defaults to 'css' if not given. + * + * @param string $id + * A string id for the asset, to return from AssetInterface::id(). Defaults + * to a random string if not given. * * @return FileAsset */ - public function createMockFileAsset($type) { + public function createStubFileAsset($type = 'css', $id = '') { $asset = $this->getMock('Drupal\\Core\\Asset\\FileAsset', array(), array(), '', FALSE); $asset->expects($this->any()) ->method('getAssetType') @@ -35,7 +40,7 @@ public function createMockFileAsset($type) { $asset->expects($this->any()) ->method('id') - ->will($this->returnValue($this->randomName())); + ->will($this->returnValue($id ?: $this->randomName())); return $asset; } @@ -49,9 +54,29 @@ public function createMockFileAsset($type) { * @return AssetMetadataBag */ public function createStubAssetMetadata($type = 'css', $values = array()) { - return $this->getMockBuilder('Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag') + $stub = $this->getMockBuilder('Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag') ->setConstructorArgs(array($type, $values)) - ->setMethods(array()) // mock nothing ->getMock(); + + $stub->expects($this->any()) + ->method('getType') + ->will($this->returnValue($type)); + + return $stub; + } + + + /** + * Generates a simple AssetAggregate mock. + * + * @param array $defaults + * Defaults to inject into the aggregate's metadata bag. + * + * @return AssetAggregate + */ + public function getAggregate($defaults = array()) { + $mockmeta = $this->createStubAssetMetadata(); + return $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Aggregate\\AssetAggregate', array($mockmeta)); } + } \ No newline at end of file diff --git a/core/tests/Drupal/Tests/Core/Asset/Collection/AssetCollectionTest.php b/core/tests/Drupal/Tests/Core/Asset/Collection/AssetCollectionTest.php index d1b2fcc..a4a16ad 100644 --- a/core/tests/Drupal/Tests/Core/Asset/Collection/AssetCollectionTest.php +++ b/core/tests/Drupal/Tests/Core/Asset/Collection/AssetCollectionTest.php @@ -8,13 +8,13 @@ namespace Drupal\Tests\Core\Asset\Collection; use Drupal\Core\Asset\Collection\AssetCollection; -use Drupal\Tests\Core\Asset\AssetUnitTest; +use Drupal\Core\Asset\Collection\AssetCollectionBasicInterface; /** * @coversDefaultClass \Drupal\Core\Asset\Collection\AssetCollection * @group Asset */ -class AssetCollectionTest extends AssetUnitTest { +class AssetCollectionTest extends BasicAssetCollectionTest { /** * @var AssetCollection @@ -34,17 +34,26 @@ public function setUp() { } /** + * Returns an AssetCollection, the base collection type for this unit test. + * + * @return AssetCollectionBasicInterface + */ + public function getCollection() { + return new AssetCollection(); + } + + /** * @covers ::add */ public function testAdd() { - $css = $this->createMockFileAsset('css'); - $js = $this->createMockFileAsset('js'); + $asset1 = $this->createStubFileAsset(); + $asset2 = $this->createStubFileAsset(); - $this->assertTrue($this->collection->add($css)); - $this->assertTrue($this->collection->add($js)); + $this->assertTrue($this->collection->add($asset1)); + $this->assertTrue($this->collection->add($asset2)); - $this->assertContains($css, $this->collection); - $this->assertContains($js, $this->collection); + $this->assertContains($asset1, $this->collection); + $this->assertContains($asset2, $this->collection); } /** @@ -54,7 +63,7 @@ public function testAdd() { * @covers ::add */ public function testDoubleAdd() { - $asset = $this->createMockFileAsset('css'); + $asset = $this->createStubFileAsset(); $this->assertTrue($this->collection->add($asset)); $this->assertTrue($this->collection->contains($asset)); @@ -75,18 +84,33 @@ public function testDoubleAdd() { * @covers ::contains */ public function testContains() { - $css = $this->createMockFileAsset('css'); - $this->collection->add($css); - $this->assertTrue($this->collection->contains($css)); + $asset = $this->createStubFileAsset(); + $this->collection->add($asset); + $this->assertTrue($this->collection->contains($asset)); + } + + /** + * @depends testAdd + * @depends testContains + * @covers ::__construct + */ + public function testCreateWithAssets() { + $asset1 = $this->createStubFileAsset(); + $asset2 = $this->createStubFileAsset(); + $collection = new AssetCollection(array($asset1, $asset2)); + + $this->assertContains($asset1, $collection); + $this->assertContains($asset2, $collection); } + /** * @depends testAdd * @covers ::getCss */ public function testGetCss() { - $css = $this->createMockFileAsset('css'); - $js = $this->createMockFileAsset('js'); + $css = $this->createStubFileAsset('css'); + $js = $this->createStubFileAsset('js'); $this->collection->add($css); $this->collection->add($js); @@ -104,8 +128,8 @@ public function testGetCss() { * @covers ::getJs */ public function testGetJs() { - $css = $this->createMockFileAsset('css'); - $js = $this->createMockFileAsset('js'); + $css = $this->createStubFileAsset('css'); + $js = $this->createStubFileAsset('js'); $this->collection->add($css); $this->collection->add($js); @@ -123,8 +147,8 @@ public function testGetJs() { * @covers ::all */ public function testAll() { - $css = $this->createMockFileAsset('css'); - $js = $this->createMockFileAsset('js'); + $css = $this->createStubFileAsset('css'); + $js = $this->createStubFileAsset('js'); $this->collection->add($css); $this->collection->add($js); @@ -137,7 +161,7 @@ public function testAll() { * @covers ::remove */ public function testRemoveByAsset() { - $stub = $this->createMockFileAsset('css'); + $stub = $this->createStubFileAsset(); $this->collection->add($stub); $this->collection->remove($stub); @@ -150,7 +174,7 @@ public function testRemoveByAsset() { * @covers ::remove */ public function testRemoveById() { - $stub = $this->createMockFileAsset('css'); + $stub = $this->createStubFileAsset(); $this->collection->add($stub); $this->collection->remove($stub->id()); @@ -162,27 +186,17 @@ public function testRemoveById() { * @expectedException \OutOfBoundsException */ public function testRemoveNonexistentId() { - $this->assertFalse($this->collection->remove('foo')); - $this->collection->remove('foo', FALSE); + $this->assertFalse($this->collection->remove('foo', TRUE)); + $this->collection->remove('foo'); } /** * @expectedException \OutOfBoundsException */ public function testRemoveNonexistentAsset() { - $stub = $this->createMockFileAsset('css'); - $this->assertFalse($this->collection->remove($stub)); - $this->collection->remove($stub, FALSE); - } - - public function testRemoveInvalidType() { - $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass); - try { - foreach ($invalid as $val) { - $this->collection->remove($val); - $this->fail('AssetCollection::remove() did not throw exception on invalid argument type.'); - } - } catch (\InvalidArgumentException $e) {} + $stub = $this->createStubFileAsset(); + $this->assertFalse($this->collection->remove($stub, TRUE)); + $this->collection->remove($stub); } /** @@ -191,8 +205,8 @@ public function testRemoveInvalidType() { */ public function testMergeCollection() { $coll2 = new AssetCollection(); - $stub1 = $this->createMockFileAsset('css'); - $stub2 = $this->createMockFileAsset('js'); + $stub1 = $this->createStubFileAsset(); + $stub2 = $this->createStubFileAsset(); $coll2->add($stub1); $this->collection->mergeCollection($coll2); @@ -219,7 +233,7 @@ public function testMergeCollection() { * correctly trigger an exception. */ public function testExceptionOnWriteWhenFrozen() { - $stub = $this->createMockFileAsset('css'); + $stub = $this->createStubFileAsset(); $write_protected = array( 'add' => $stub, 'remove' => $stub, @@ -230,10 +244,15 @@ public function testExceptionOnWriteWhenFrozen() { foreach ($write_protected as $method => $arg) { try { $this->collection->$method($arg); - $this->fail('Was able to run writable method on frozen AssetCollection'); - } - catch (\LogicException $e) {} + $this->fail(sprintf('Was able to run write method "%s" on frozen AssetCollection', $method)); + } catch (\LogicException $e) {} } + + // Do replace method separately, it needs more args + try { + $this->collection->replace($stub, $this->createStubFileAsset()); + $this->fail('Was able to run write method "replace" on frozen AssetCollection'); + } catch (\LogicException $e) {} } /** @@ -259,18 +278,14 @@ public function testGetById() { $this->collection->getById('bar', FALSE); } - public function testIsEmpty() { - $this->assertTrue($this->collection->isEmpty()); - } - /** * @depends testAdd * @covers ::sort */ public function testSort() { - $stub1 = $this->createMockFileAsset('css'); - $stub2 = $this->createMockFileAsset('js'); - $stub3 = $this->createMockFileAsset('css'); + $stub1 = $this->createStubFileAsset(); + $stub2 = $this->createStubFileAsset(); + $stub3 = $this->createStubFileAsset(); $this->collection->add($stub1); $this->collection->add($stub2); @@ -296,9 +311,9 @@ public function testSort() { * @covers ::ksort */ public function testKsort() { - $stub1 = $this->createMockFileAsset('css'); - $stub2 = $this->createMockFileAsset('js'); - $stub3 = $this->createMockFileAsset('css'); + $stub1 = $this->createStubFileAsset(); + $stub2 = $this->createStubFileAsset(); + $stub3 = $this->createStubFileAsset(); $this->collection->add($stub1); $this->collection->add($stub2); @@ -315,3 +330,4 @@ public function testKsort() { $this->assertEquals($assets, $this->collection->all()); } } + diff --git a/core/tests/Drupal/Tests/Core/Asset/Collection/BasicAssetCollectionTest.php b/core/tests/Drupal/Tests/Core/Asset/Collection/BasicAssetCollectionTest.php new file mode 100644 index 0000000..f657657 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Collection/BasicAssetCollectionTest.php @@ -0,0 +1,457 @@ + 'BasicAssetCollection unit tests', + 'description' => 'Unit tests for BasicAssetCollection', + 'group' => 'Asset', + ); + } + + /** + * Generates a simple BasicAssetCollection mock. + * + * @return BasicAssetCollection + */ + public function getBasicCollection() { + return $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Collection\\BasicAssetCollection'); + } + + /** + * Method to return the appropriate collection type for the current test. + * + * @return AssetCollectionBasicInterface + */ + public function getCollection() { + return $this->getBasicCollection(); + } + + /** + * Generates a AssetAggregate mock with three leaf assets. + */ + public function getThreeLeafBasicCollection() { + $collection = $this->getCollection(); + $nested_aggregate = $this->getAggregate(); + + foreach (array('foo', 'bar', 'baz') as $var) { + $$var = $this->createStubFileAsset('css', $var); + } + + $nested_aggregate->add($foo); + $nested_aggregate->add($bar); + $collection->add($nested_aggregate); + $collection->add($baz); + + return array($collection, $foo, $bar, $baz, $nested_aggregate); + } + + /** + * This uses PHPUnit's reflection-based assertions rather than assertContains + * so that this test can honestly sit at the root of the test method + * dependency tree. + * + * @covers ::add + */ + public function testAdd() { + $collection = $this->getCollection(); + $asset = $this->createStubFileAsset(); + $this->assertTrue($collection->add($asset)); + + $this->assertAttributeContains($asset, 'assetStorage', $collection); + $this->assertAttributeContains($asset, 'assetIdMap', $collection); + + // Nesting: add an aggregate to the first aggregate. + $nested_aggregate = $this->getAggregate(); + $collection->add($nested_aggregate); + + $this->assertAttributeContains($nested_aggregate, 'assetStorage', $collection); + $this->assertAttributeContains($nested_aggregate, 'assetIdMap', $collection); + $this->assertAttributeContains($nested_aggregate, 'nestedStorage', $collection); + } + + /** + * @depends testAdd + * @depends testEach + * @covers ::__construct + */ + public function testCreateWithAssets() { + $asset1 = $this->createStubFileAsset(); + $asset2 = $this->createStubFileAsset(); + $collection = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Collection\\BasicAssetCollection', array(array($asset1, $asset2))); + + $this->assertContains($asset1, $collection); + $this->assertContains($asset2, $collection); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException + * @covers ::add + */ + public function testVanillaAsseticAdd() { + $vanilla = $this->getMock('\\Assetic\\Asset\\BaseAsset', array(), array(), '', FALSE); + $this->getCollection()->add($vanilla); + } + + /** + * @depends testAdd + * @covers ::each + * @covers ::getIterator + * @covers \Drupal\Core\Asset\Collection\Iterator\RecursiveBasicCollectionIterator + */ + public function testEach() { + list($collection, $foo, $bar, $baz, $nested_aggregate) = $this->getThreeLeafBasicCollection(); + + $contained = array(); + foreach ($collection->each() as $leaf) { + $contained[] = $leaf; + } + $this->assertEquals(array($nested_aggregate, $foo, $bar, $baz), $contained); + } + + /** + * @depends testAdd + * @covers ::eachLeaf + * @covers \Drupal\Core\Asset\Collection\Iterator\RecursiveBasicCollectionIterator + */ + public function testEachLeaf() { + list($collection, $foo, $bar, $baz) = $this->getThreeLeafBasicCollection(); + + $contained = array(); + foreach ($collection->eachLeaf() as $leaf) { + $contained[] = $leaf; + } + $this->assertEquals(array($foo, $bar, $baz), $contained); + } + + /** + * Tests that adding the same asset twice is disallowed. + * + * @depends testAdd + * @covers ::add + */ + public function testDoubleAdd() { + $collection = $this->getCollection(); + $asset = $this->createStubFileAsset(); + $this->assertTrue($collection->add($asset)); + + // Test by object identity + $this->assertFalse($collection->add($asset)); + // Test by id + $asset2 = $this->createStubFileAsset('css', $asset->id()); + + $this->assertFalse($collection->add($asset2)); + } + + /** + * @depends testAdd + * @covers ::contains + */ + public function testContains() { + $collection = $this->getCollection(); + $asset = $this->createStubFileAsset(); + $collection->add($asset); + + $this->assertTrue($collection->contains($asset)); + + // Nesting: add an aggregate to the first aggregate. + $nested_aggregate = $this->getAggregate(); + $nested_asset = $this->createStubFileAsset(); + + $nested_aggregate->add($nested_asset); + $collection->add($nested_aggregate); + + $this->assertTrue($collection->contains($nested_asset)); + } + + /** + * @covers ::getById + * @expectedException \OutOfBoundsException + */ + public function testGetById() { + $collection = $this->getCollection(); + + $asset = $this->createStubFileAsset(); + $collection->add($asset); + $this->assertSame($asset, $collection->getById($asset->id())); + + $nested_aggregate = $this->getAggregate(); + $nested_asset = $this->createStubFileAsset(); + + $nested_aggregate->add($nested_asset); + $collection->add($nested_aggregate); + + $this->assertSame($nested_asset, $collection->getById($nested_asset->id())); + + // Nonexistent asset + $this->assertFalse($collection->getById('bar')); + + // Nonexistent asset, non-graceful + $collection->getById('bar', FALSE); + } + + /** + * @depends testAdd + * @covers ::all + */ + public function testAll() { + $collection = $this->getCollection(); + + $asset1 = $this->createStubFileAsset(); + $asset2 = $this->createStubFileAsset(); + $collection->add($asset1); + $collection->add($asset2); + + $output = array( + $asset1->id() => $asset1, + $asset2->id() => $asset2, + ); + + $this->assertEquals($output, $collection->all()); + + // Ensure that only top-level assets are returned. + $nested_aggregate = $this->getAggregate(); + $nested_aggregate->add($this->createStubFileAsset()); + $collection->add($nested_aggregate); + + $output[$nested_aggregate->id()] = $nested_aggregate; + $this->assertEquals($output, $collection->all()); + } + + /** + * @depends testEach + * @covers ::remove + * @covers ::doRemove + */ + public function testRemove() { + list($collection, $foo, $bar, $baz, $nested_aggregate) = $this->getThreeLeafBasicCollection(); + $this->assertFalse($collection->remove('arglebargle', TRUE)); + $this->assertTrue($collection->remove('foo')); + + $this->assertNotContains($foo, $collection); + $this->assertContains($bar, $collection); + $this->assertContains($baz, $collection); + + $this->assertTrue($collection->remove($bar)); + + $this->assertNotContains($bar, $collection); + $this->assertContains($baz, $collection); + + $this->assertTrue($collection->remove($nested_aggregate)); + $this->assertNotContains($nested_aggregate, $collection); + } + + /** + * @depends testEach + * @covers ::remove + * @covers ::doRemove + * @expectedException \OutOfBoundsException + */ + public function testRemoveNonexistentNeedle() { + list($collection) = $this->getThreeLeafBasicCollection(); + // Nonexistent leaf removal returns FALSE in graceful mode + $this->assertFalse($collection->remove($this->createStubFileAsset(), TRUE)); + + // In non-graceful mode, an exception is thrown. + $collection->remove($this->createStubFileAsset()); + } + + /** + * @depends testEach + * @depends testEachLeaf + * @covers ::replace + * @covers ::doReplace + */ + public function testReplace() { + list($collection, $foo, $bar, $baz, $nested_aggregate) = $this->getThreeLeafBasicCollection(); + $qux = $this->createStubFileAsset('css', 'qux'); + + $this->assertFalse($collection->replace('arglebargle', $qux, TRUE)); + $this->assertTrue($collection->replace('foo', $qux)); + + $this->assertContains($qux, $collection); + $this->assertNotContains($foo, $collection); + + $contained = array(); + foreach ($collection->eachLeaf() as $leaf) { + $contained[] = $leaf; + } + $this->assertEquals(array($qux, $bar, $baz), $contained); + + $this->assertTrue($collection->replace($bar, $foo)); + + $this->assertContains($foo, $collection); + $this->assertNotContains($bar, $collection); + + $contained = array(); + foreach ($collection->eachLeaf() as $leaf) { + $contained[] = $leaf; + } + $this->assertEquals(array($qux, $foo, $baz), $contained); + + $aggregate2 = $this->getAggregate(); + $this->assertTrue($collection->replace($baz, $aggregate2)); + + $this->assertContains($aggregate2, $collection); + $this->assertNotContains($baz, $collection); + + $contained = array(); + foreach ($collection->eachLeaf() as $leaf) { + $contained[] = $leaf; + } + $this->assertEquals(array($qux, $foo), $contained); + + $contained = array(); + foreach ($collection->each() as $leaf) { + $contained[] = $leaf; + } + $this->assertEquals(array($nested_aggregate, $qux, $foo, $aggregate2), $contained); + } + + /** + * @depends testEach + * @covers ::replace + * @covers ::doReplace + * @expectedException \OutOfBoundsException + */ + public function testReplaceNonexistentNeedle() { + list($collection) = $this->getThreeLeafBasicCollection(); + // Nonexistent leaf replacement returns FALSE in graceful mode + $qux = $this->createStubFileAsset(); + $this->assertFalse($collection->replace($this->createStubFileAsset(), $qux, TRUE)); + $this->assertNotContains($qux, $collection); + + // In non-graceful mode, an exception is thrown. + $collection->replace($this->createStubFileAsset(), $qux); + } + + /** + * @depends testEach + * @covers ::replace + * @expectedException \LogicException + */ + public function testReplaceWithAlreadyPresentAsset() { + list($aggregate, $foo) = $this->getThreeLeafBasicCollection(); + $aggregate->replace($this->createStubFileAsset(), $foo); + } + + /** + * @depends testAdd + * @depends testReplaceWithAlreadyPresentAsset + * @covers ::replace + * @expectedException \LogicException + * + * This fails on the same check that testReplaceWithAlreadyPresentAsset, + * but it is demonstrated as its own test for clarity. + */ + public function testReplaceWithSelf() { + list($collection, $foo) = $this->getThreeLeafBasicCollection(); + $collection->replace($foo, $foo); + } + + /** + * @depends testAdd + * @depends testRemove + * @covers ::isEmpty + */ + public function testIsEmpty() { + $collection = $this->getCollection(); + $this->assertTrue($collection->isEmpty()); + + // Collections containing only empty collections are considered empty. + $collection->add($this->getAggregate()); + $this->assertTrue($collection->isEmpty()); + + $aggregate = $this->getAggregate(); + $asset = $this->createStubFileAsset(); + $aggregate->add($asset); + $collection->add($aggregate); + $this->assertFalse($collection->isEmpty()); + + $collection->remove($aggregate); + $this->assertTrue($collection->isEmpty()); + + $collection->add($asset); + $this->assertFalse($collection->isEmpty()); + + $collection->remove($asset); + $this->assertTrue($collection->isEmpty()); + } + + /** + * @depends testAdd + * @depends testRemove + * @covers ::count + */ + public function testCount() { + $collection = $this->getCollection(); + $this->assertCount(0, $collection); + + $collection->add($this->getAggregate()); + $this->assertCount(0, $collection); + + $aggregate = $this->getAggregate(); + $asset = $this->createStubFileAsset(); + $aggregate->add($asset); + $collection->add($aggregate); + $this->assertCount(1, $collection); + + $collection->remove($aggregate); + $this->assertCount(0, $collection); + + $collection->add($asset); + $this->assertCount(1, $collection); + + $collection->remove($asset); + $this->assertCount(0, $collection); + } + + /** + * @covers ::remove + */ + public function testRemoveInvalidNeedle() { + $collection = $this->getCollection(); + $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass); + + try { + foreach ($invalid as $val) { + $collection->remove($val); + $this->fail('BasicAssetCollection::remove() did not throw exception on invalid argument type for $needle.'); + } + } catch (\InvalidArgumentException $e) {} + } + + /** + * @covers ::replace + */ + public function testReplaceInvalidNeedle() { + $collection = $this->getCollection(); + $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass); + + try { + foreach ($invalid as $val) { + $collection->replace($val, $this->createStubFileAsset()); + $this->fail('BasicAssetCollection::replace() did not throw exception on invalid argument type for $needle.'); + } + } catch (\InvalidArgumentException $e) {} + } + +} + diff --git a/core/tests/Drupal/Tests/Core/Asset/Factory/AssetCollectorTest.php b/core/tests/Drupal/Tests/Core/Asset/Factory/AssetCollectorTest.php index 723de5d..437d09a 100644 --- a/core/tests/Drupal/Tests/Core/Asset/Factory/AssetCollectorTest.php +++ b/core/tests/Drupal/Tests/Core/Asset/Factory/AssetCollectorTest.php @@ -50,6 +50,19 @@ public function setUp() { } /** + * Tests that constructor-injected params end up in the right place. + */ + public function testConstructorInjection() { + $factory = $this->getMock('Drupal\\Core\\Asset\\Metadata\\DefaultAssetMetadataFactory'); + $collection = $this->getMock('Drupal\\Core\\Asset\\Collection\\AssetCollection'); + + $collector = new AssetCollector($collection, $factory); + + $this->assertAttributeSame($collection, 'collection', $collector); + $this->assertAttributeSame($factory, 'metadataFactory', $collector); + } + + /** * Tests that the collector injects provided metadata to created assets. */ public function testMetadataInjection() { @@ -97,7 +110,7 @@ public function testAddAssetExplicitly() { $collection = new AssetCollection(); $this->collector->setCollection($collection); - $mock = $this->createMockFileAsset('css'); + $mock = $this->createStubFileAsset('css'); $this->collector->add($mock); $this->assertContains($mock, $collection);