diff --git a/core/lib/Drupal/Core/Asset/AssetGraph.php b/core/lib/Drupal/Core/Asset/AssetGraph.php index f261a0a..04557ad 100644 --- a/core/lib/Drupal/Core/Asset/AssetGraph.php +++ b/core/lib/Drupal/Core/Asset/AssetGraph.php @@ -40,7 +40,28 @@ class AssetGraph extends DirectedAdjacencyGraph { protected $before = array(); protected $after = array(); protected $verticesById = array(); + protected $process; + /** + * Creates a new AssetGraph object. + * + * AssetGraphs are a specialization of DirectedAdjacencyGraph that is tailored + * to handling the sequencing information carried by AssetOrderingInterface + * instances. + * + * @param bool $process + * Whether or not to automatically process sequencing as vertices are added. + * This should be left as TRUE in most every user-facing case; its primary + * audience is for the creation of a graph transpose. + */ + public function __construct($process = TRUE) { + parent::__construct(); + $this->process = $process; + } + + /** + * {@inheritdoc} + */ public function addVertex($vertex) { if (!$vertex instanceof AssetInterface) { throw new InvalidVertexTypeException('AssetGraph requires vertices to implement AssetInterface.'); @@ -49,7 +70,10 @@ public function addVertex($vertex) { if (!$this->hasVertex($vertex)) { $this->vertices[$vertex] = new \SplObjectStorage(); $this->verticesById[$vertex->id()] = $vertex; - $this->processNewVertex($vertex); + + if ($this->process) { + $this->processNewVertex($vertex); + } } } @@ -83,7 +107,7 @@ protected function processNewVertex(AssetInterface $vertex) { $predecessor = is_string($predecessor) ? $predecessor : $predecessor->id(); if (isset($this->verticesById[$predecessor])) { - $this->addDirectedEdge($this->verticesById[$predecessor], $vertex); + $this->addDirectedEdge($vertex, $this->verticesById[$predecessor]); } else { if (!isset($this->before[$predecessor])) { @@ -98,7 +122,7 @@ protected function processNewVertex(AssetInterface $vertex) { $successor = is_string($successor) ? $successor : $successor->id(); if (isset($this->verticesById[$successor])) { - $this->addDirectedEdge($vertex, $this->verticesById[$successor]); + $this->addDirectedEdge($this->verticesById[$successor], $vertex); } else { if (!isset($this->before[$successor])) { @@ -111,10 +135,28 @@ protected function processNewVertex(AssetInterface $vertex) { } /** + * Remove a vertex from the graph. Unsupported in AssetGraph. + * + * Vertex removals are unsupported because it would necessitate permanent + * bookkeeping on sequencing data. With forty or fifty assets, each having + * only a few dependencies, there would be a fair bit of pointless iterating. + * + * @throws \LogicException + * This exception will always be thrown. + */ + public function removeVertex($vertex) { + throw new \LogicException('AssetGraph does not support vertex removals.'); + } + + /** * {@inheritdoc} */ public function transpose() { - // TODO super-important - have to rewrite transpose so that it correctly inverts edge direction - return parent::transpose(); + $graph = new self(FALSE); + $this->eachEdge(function($edge) use (&$graph) { + $graph->addDirectedEdge($edge[1], $edge[0]); + }); + + return $graph; } } diff --git a/core/lib/Drupal/Core/Asset/Factory/AssetCollector.php b/core/lib/Drupal/Core/Asset/Factory/AssetCollector.php index 92dca24..5e5e5a7 100644 --- a/core/lib/Drupal/Core/Asset/Factory/AssetCollector.php +++ b/core/lib/Drupal/Core/Asset/Factory/AssetCollector.php @@ -118,6 +118,9 @@ public function add(AssetInterface $asset) { * Thrown if an invalid asset type or source type is passed. */ public function create($asset_type, $source_type, $data, $options = array(), $filters = array()) { + // TODO this normalization points to a deeper modeling problem. + $source_type = $source_type == 'inline' ? 'string' : $source_type; + if (!isset($this->classMap[$asset_type])) { throw new \InvalidArgumentException(sprintf('Only assets of type "js" or "css" are allowed, "%s" requested.', $asset_type)); } diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetGraphTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetGraphTest.php new file mode 100644 index 0000000..a2f8bba --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetGraphTest.php @@ -0,0 +1,258 @@ + 'Asset graph test', + 'description' => 'Tests that custom additions in the asset graph work correctly.', + 'group' => 'Asset', + ); + } + + public function setUp() { + parent::setUp(); + $this->graph = new AssetGraph(); + } + + /** + * Generates a simple mock asset object. + * + * @param string $id + * An id to give the asset; it will returned from the mocked + * AssetInterface::id() method. + * + * @return \PHPUnit_Framework_MockObject_MockObject + * A mock of a BaseAsset object. + */ + public function createBasicAssetMock($id = 'foo') { + $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $mock = $this->getMockBuilder('\\Drupal\\Core\\Asset\\BaseAsset') + ->setConstructorArgs(array($mockmeta)) + ->getMock(); + + $mock->expects($this->any()) + ->method('id') + ->will($this->returnValue($id)); + + $mock->expects($this->once()) + ->method('getPredecessors') + ->will($this->returnValue(array())); + + $mock->expects($this->once()) + ->method('getSuccessors') + ->will($this->returnValue(array())); + + return $mock; + } + + public function doCheckVertexCount($count, AssetGraph $graph = NULL) { + $found = array(); + $graph = is_null($graph) ? $this->graph : $graph; + + $graph->eachVertex(function ($vertex) use (&$found) { + $found[] = $vertex; + }); + + $this->assertCount($count, $found); + } + + public function doCheckVerticesEqual($vertices, AssetGraph $graph = NULL) { + $found = array(); + $graph = is_null($graph) ? $this->graph : $graph; + + $graph->eachVertex(function ($vertex) use (&$found) { + $found[] = $vertex; + }); + + $this->assertEquals($vertices, $found); + } + + public function testAddSingleVertex() { + $mock = $this->createBasicAssetMock(); + + $mock->expects($this->exactly(2)) + ->method('id') + ->will($this->returnValue('foo')); + + $this->graph->addVertex($mock); + + $this->doCheckVerticesEqual(array($mock)); + } + + /** + * @expectedException \Gliph\Exception\InvalidVertexTypeException + */ + public function testAddInvalidVertexType() { + $this->graph->addVertex(new \stdClass()); + } + + /** + * @expectedException \LogicException + */ + public function testExceptionOnRemoval() { + $mock = $this->createBasicAssetMock(); + $this->graph->addVertex($mock); + $this->graph->removeVertex($mock); + } + + public function testAddUnconnectedVertices() { + $foo = $this->createBasicAssetMock('foo'); + $bar = $this->createBasicAssetMock('bar'); + + $this->graph->addVertex($foo); + $this->graph->addVertex($bar); + + $this->doCheckVerticesEqual(array($foo, $bar)); + } + + /** + * Tests that edges are automatically created correctly when assets have + * sequencing information. + */ + public function testAddConnectedVertices() { + $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $foo = $this->getMockBuilder('\\Drupal\\Core\\Asset\\BaseAsset') + ->setConstructorArgs(array($mockmeta)) + ->getMock(); + + $foo->expects($this->exactly(3)) + ->method('id') + ->will($this->returnValue('foo')); + + $foo->expects($this->once()) + ->method('getPredecessors') + ->will($this->returnValue(array('bar'))); + + $foo->expects($this->once()) + ->method('getSuccessors') + ->will($this->returnValue(array('baz'))); + + $bar = $this->createBasicAssetMock('bar'); + $baz = $this->createBasicAssetMock('baz'); + + $this->graph->addVertex($foo); + $this->graph->addVertex($bar); + $this->graph->addVertex($baz); + + $this->doCheckVerticesEqual(array($foo, $bar, $baz)); + + $lister = function($vertex) use (&$out) { + $out[] = $vertex; + }; + + $out = array(); + $this->graph->eachAdjacent($foo, $lister); + $this->assertEquals(array($bar), $out); + + $out = array(); + $this->graph->eachAdjacent($baz, $lister); + $this->assertEquals(array($foo), $out); + + $out = array(); + $this->graph->eachAdjacent($bar, $lister); + $this->assertEmpty($out); + + // Now add another vertex with sequencing info that targets already-inserted + // vertices. + + $qux = $this->getMockBuilder('\\Drupal\\Core\\Asset\\BaseAsset') + ->setConstructorArgs(array($mockmeta)) + ->getMock(); + + $qux->expects($this->exactly(2)) + ->method('id') + ->will($this->returnValue('qux')); + + // Do this one with the foo vertex itself, not its string id. + $qux->expects($this->once()) + ->method('getPredecessors') + ->will($this->returnValue(array($foo))); + + $qux->expects($this->once()) + ->method('getSuccessors') + ->will($this->returnValue(array('bar', 'baz'))); + + $this->graph->addVertex($qux); + + $this->doCheckVerticesEqual(array($foo, $bar, $baz, $qux)); + + $out = array(); + $this->graph->eachAdjacent($qux, $lister); + $this->assertEquals(array($foo), $out); + + $out = array(); + $this->graph->eachAdjacent($bar, $lister); + $this->assertEquals(array($qux), $out); + + $out = array(); + $this->graph->eachAdjacent($baz, $lister); + $this->assertEquals(array($foo, $qux), $out); + } + + public function testTranspose() { + $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $foo = $this->getMockBuilder('\\Drupal\\Core\\Asset\\BaseAsset') + ->setConstructorArgs(array($mockmeta)) + ->getMock(); + + $foo->expects($this->exactly(3)) + ->method('id') + ->will($this->returnValue('foo')); + + $foo->expects($this->once()) + ->method('getPredecessors') + ->will($this->returnValue(array('bar'))); + + $foo->expects($this->once()) + ->method('getSuccessors') + ->will($this->returnValue(array('baz'))); + + $bar = $this->createBasicAssetMock('bar'); + $baz = $this->createBasicAssetMock('baz'); + + $this->graph->addVertex($foo); + $this->graph->addVertex($bar); + $this->graph->addVertex($baz); + + $transpose = $this->graph->transpose(); + $this->doCheckVerticesEqual(array($foo, $bar, $baz), $transpose); + + // Verify that the transpose has a fully inverted edge set. + $lister = function($vertex) use (&$out) { + $out[] = $vertex; + }; + + $out = array(); + $transpose->eachAdjacent($bar, $lister); + $this->assertEquals(array($foo), $out); + + $out = array(); + $transpose->eachAdjacent($foo, $lister); + $this->assertEquals(array($baz), $out); + + $out = array(); + $transpose->eachAdjacent($baz, $lister); + $this->assertEmpty($out); + } +}