[BUGFIX] Sorting of packages by dependency 52/24852/18
authorMarkus Klein <klein.t3@mfc-linz.at>
Wed, 16 Oct 2013 19:56:27 +0000 (21:56 +0200)
committerJigal van Hemert <jigal.van.hemert@typo3.org>
Sun, 20 Oct 2013 18:32:53 +0000 (20:32 +0200)
Implement proper dependency handling in Flow PackageManager.
The code ensures that all framework packages are loaded first.

Releases: 6.2
Resolves: #52828
Change-Id: I6717756efae28b1f8e13fc3a251328f69f54b4d4
Reviewed-on: https://review.typo3.org/24852
Reviewed-by: Helmut Hummel
Tested-by: Helmut Hummel
Tested-by: Anja Leichsenring
Reviewed-by: Stefan Neufeind
Reviewed-by: Jigal van Hemert
Tested-by: Jigal van Hemert
typo3/sysext/core/Classes/Package/PackageManager.php
typo3/sysext/core/Tests/Unit/Package/PackageManagerTest.php
typo3/sysext/frontend/composer.json
typo3/sysext/openid/composer.json
typo3/sysext/openid/ext_emconf.php

index e7ef089..8ec3ea8 100644 (file)
@@ -59,6 +59,20 @@ class PackageManager extends \TYPO3\Flow\Package\PackageManager implements \TYPO
        protected $packageAliasMap = array();
 
        /**
+        * Adjacency matrix for the dependency graph (DAG)
+        *
+        * Example structure is:
+        *    A => (A => FALSE, B => TRUE,  C => FALSE)
+        *    B => (A => FALSE, B => FALSE, C => FALSE)
+        *    C => (A => TRUE,  B => FALSE, C => FALSE)
+        *
+        *    A depends on B, C depends on A, B is independent
+        *
+        * @var array<array<boolean>>
+        */
+       protected $dependencyGraph;
+
+       /**
         * Constructor
         */
        public function __construct() {
@@ -558,4 +572,188 @@ class PackageManager extends \TYPO3\Flow\Package\PackageManager implements \TYPO
                parent::refreezePackage($package->getPackageKey());
        }
 
+
+       /**
+        * Get packages of specific type
+        *
+        * @param string $type Type of package. Empty string for all types
+        * @param array $excludedTypes Array of package types to exclude
+        * @return array List of packages
+        */
+       protected function getPackageKeysOfType($type, array $excludedTypes = array()) {
+               $packageKeys = array();
+               foreach ($this->packages as $packageKey => $package) {
+                       $packageType = $package->getComposerManifest('type');
+                       if (($type === '' || $packageType === $type) && !in_array($packageType, $excludedTypes)) {
+                               $packageKeys[] = $packageKey;
+                       }
+               }
+               return $packageKeys;
+       }
+
+       /**
+        * Build the dependency graph for the given packages
+        *
+        * @param array $packageKeys
+        * @return void
+        * @throws \UnexpectedValueException
+        */
+       protected function buildDependencyGraphForPackages(array $packageKeys) {
+               // Initialize the dependencies with FALSE
+               $this->dependencyGraph = array_fill_keys($packageKeys, array_fill_keys($packageKeys, FALSE));
+               foreach ($packageKeys as $packageKey) {
+                       $dependentPackageKeys = $this->packageStatesConfiguration['packages'][$packageKey]['dependencies'];
+                       foreach ($dependentPackageKeys as $dependentPackageKey) {
+                               if (!in_array($dependentPackageKey, $packageKeys)) {
+                                       throw new \UnexpectedValueException(
+                                               'The package "' . $packageKey .'" depends on "'
+                                               . $dependentPackageKey . '" which is not present in the system.',
+                                               1382276561);
+                               }
+                               $this->dependencyGraph[$packageKey][$dependentPackageKey] = TRUE;
+                       }
+               }
+       }
+
+       /**
+        * Adds all root packages of current dependency graph as dependency
+        * to all extensions
+        *
+        * @return void
+        */
+       protected function addDependencyToFrameworkToAllExtensions() {
+               $rootPackageKeys = array();
+               foreach (array_keys($this->dependencyGraph) as $packageKey) {
+                       if (!$this->getIncomingEdgeCount($packageKey)) {
+                               $rootPackageKeys[] = $packageKey;
+                       }
+               }
+               $extensionPackageKeys = $this->getPackageKeysOfType('', array('typo3-cms-framework'));
+               $frameworkPackageKeys = $this->getPackageKeysOfType('typo3-cms-framework');
+               foreach ($extensionPackageKeys as $packageKey) {
+                       // Remove framework packages from list
+                       $packageKeysWithoutFramework = array_diff(
+                               $this->packageStatesConfiguration['packages'][$packageKey]['dependencies'],
+                               $frameworkPackageKeys
+                       );
+                       // The order of the array_merge is crucial here,
+                       // we want the framework first
+                       $this->packageStatesConfiguration['packages'][$packageKey]['dependencies'] = array_merge(
+                               $rootPackageKeys, $packageKeysWithoutFramework
+                       );
+               }
+       }
+
+       /**
+        * Builds the dependency graph for all packages
+        *
+        * This method also introduces dependencies among the dependencies
+        * to ensure the loading order is exactly as specified in the list.
+        *
+        * @return void
+        */
+       protected function buildDependencyGraph() {
+               $this->resolvePackageDependencies();
+
+               $frameworkPackageKeys = $this->getPackageKeysOfType('typo3-cms-framework');
+               $this->buildDependencyGraphForPackages($frameworkPackageKeys);
+
+               $this->addDependencyToFrameworkToAllExtensions();
+
+               $packageKeys = array_keys($this->packages);
+               $this->buildDependencyGraphForPackages($packageKeys);
+       }
+
+       /**
+        * Get the number of incoming edges in the dependency graph
+        * for given package key.
+        *
+        * @param string $packageKey
+        * @return integer
+        */
+       protected function getIncomingEdgeCount($packageKey) {
+               $incomingEdgeCount = 0;
+               foreach ($this->dependencyGraph as $dependencies) {
+                       if ($dependencies[$packageKey]) {
+                               $incomingEdgeCount++;
+                       }
+               }
+               return $incomingEdgeCount;
+       }
+
+       /**
+        * Get the loading order for packages
+        *
+        * @return array The properly sorted loading order
+        * @throws \UnexpectedValueException
+        */
+       protected function getAvailablePackageLoadingOrder() {
+               $this->buildDependencyGraph();
+
+               // This will contain our final result
+               $sortedPackageKeys = array();
+
+               $rootPackageKeys = array();
+               // Filter extensions with no incoming edge
+               foreach (array_keys($this->dependencyGraph) as $packageKey) {
+                       if (!$this->getIncomingEdgeCount($packageKey)) {
+                               $rootPackageKeys[] = $packageKey;
+                       }
+               }
+
+               while (count($rootPackageKeys)) {
+                       $currentPackageKey = array_shift($rootPackageKeys);
+                       array_push($sortedPackageKeys, $currentPackageKey);
+
+                       $dependingPackageKeys = array_keys(array_filter($this->dependencyGraph[$currentPackageKey]));
+                       foreach ($dependingPackageKeys as $dependingPackageKey) {
+                               // Remove the edge to this dependency
+                               $this->dependencyGraph[$currentPackageKey][$dependingPackageKey] = FALSE;
+                               if (!$this->getIncomingEdgeCount($dependingPackageKey)) {
+                                       // We found a new root, lets add it
+                                       array_unshift($rootPackageKeys, $dependingPackageKey);
+                               }
+                       }
+               }
+
+               // Check for remaining edges in the graph
+               $cycles = array();
+               array_walk($this->dependencyGraph, function($dependencies, $packageKeyFrom) use(&$cycles) {
+                       array_walk($dependencies, function($dependency, $packageKeyTo) use(&$cycles, $packageKeyFrom) {
+                               if ($dependency) {
+                                       $cycles[] = $packageKeyFrom . '->' . $packageKeyTo;
+                               }
+                       });
+               });
+               if (count($cycles)) {
+                       throw new \UnexpectedValueException('Your dependencies have cycles. That will not work out. Cycles found: ' . implode(', ', $cycles), 1381960493);
+               }
+
+               // We built now a list of dependencies
+               // Reverse the list to get the correct loading order
+               return array_reverse($sortedPackageKeys);
+       }
+
+       /**
+        * Orders all packages by comparing their dependencies. By this, the packages
+        * and package configurations arrays holds all packages in the correct
+        * initialization order.
+        *
+        * @return void
+        */
+       protected function sortAvailablePackagesByDependencies() {
+               $newPackages = array();
+               $newPackageStatesConfiguration = array();
+
+               $sortedPackageKeys = $this->getAvailablePackageLoadingOrder();
+
+               // Reorder the packages according to the loading order
+               foreach ($sortedPackageKeys as $packageKey) {
+                       $newPackages[$packageKey] = $this->packages[$packageKey];
+                       $newPackageStatesConfiguration[$packageKey] = $this->packageStatesConfiguration['packages'][$packageKey];
+               }
+
+               $this->packages = $newPackages;
+               $this->packageStatesConfiguration['packages'] = $newPackageStatesConfiguration;
+       }
 }
index b537328..9c3c293 100644 (file)
@@ -38,7 +38,7 @@ class PackageManagerTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                $mockCache->expects($this->any())->method('set')->will($this->returnValue(TRUE));
                $mockCache->expects($this->any())->method('getBackend')->will($this->returnValue($mockCacheBackend));
                $mockCacheBackend->expects($this->any())->method('getCacheDirectory')->will($this->returnValue('vfs://Test/Cache'));
-               $this->packageManager = new \TYPO3\CMS\Core\Package\PackageManager();
+               $this->packageManager = $this->getMock('TYPO3\\CMS\\Core\\Package\\PackageManager', array('sortAndSavePackageStates'));
 
                mkdir('vfs://Test/Packages/Application', 0700, TRUE);
                mkdir('vfs://Test/Configuration');
@@ -429,87 +429,701 @@ class PackageManagerTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
        }
 
        /**
+        * @return array
+        */
+       public function buildDependencyGraphBuildsCorrectGraphDataProvider() {
+               return array(
+                       'TYPO3 Flow Packages' => array(
+                               array(
+                                       'TYPO3.Flow' => array(
+                                               'dependencies' => array('Symfony.Component.Yaml', 'Doctrine.Common', 'Doctrine.DBAL', 'Doctrine.ORM')
+                                       ),
+                                       'Doctrine.ORM' => array(
+                                               'dependencies' => array('Doctrine.Common', 'Doctrine.DBAL')
+                                       ),
+                                       'Doctrine.Common' => array(
+                                               'dependencies' => array()
+                                       ),
+                                       'Doctrine.DBAL' => array(
+                                               'dependencies' => array('Doctrine.Common')
+                                       ),
+                                       'Symfony.Component.Yaml' => array(
+                                               'dependencies' => array()
+                                       ),
+                               ),
+                               array(
+                                       'Doctrine.Common'
+                               ),
+                               array(
+                                       'TYPO3.Flow' => array(
+                                               'TYPO3.Flow' => FALSE,
+                                               'Doctrine.ORM' => TRUE,
+                                               'Doctrine.Common' => TRUE,
+                                               'Doctrine.DBAL' => TRUE,
+                                               'Symfony.Component.Yaml' => TRUE,
+                                       ),
+                                       'Doctrine.ORM' => array(
+                                               'TYPO3.Flow' => FALSE,
+                                               'Doctrine.ORM' => FALSE,
+                                               'Doctrine.Common' => TRUE,
+                                               'Doctrine.DBAL' => TRUE,
+                                               'Symfony.Component.Yaml' => FALSE,
+                                       ),
+                                       'Doctrine.Common' => array(
+                                               'TYPO3.Flow' => FALSE,
+                                               'Doctrine.ORM' => FALSE,
+                                               'Doctrine.Common' => FALSE,
+                                               'Doctrine.DBAL' => FALSE,
+                                               'Symfony.Component.Yaml' => FALSE,
+                                       ),
+                                       'Doctrine.DBAL' => array(
+                                               'TYPO3.Flow' => FALSE,
+                                               'Doctrine.ORM' => FALSE,
+                                               'Doctrine.Common' => TRUE,
+                                               'Doctrine.DBAL' => FALSE,
+                                               'Symfony.Component.Yaml' => FALSE,
+                                       ),
+                                       'Symfony.Component.Yaml' => array(
+                                               'TYPO3.Flow' => FALSE,
+                                               'Doctrine.ORM' => FALSE,
+                                               'Doctrine.Common' => TRUE,
+                                               'Doctrine.DBAL' => FALSE,
+                                               'Symfony.Component.Yaml' => FALSE,
+                                       ),
+                               ),
+                       ),
+                       'TYPO3 CMS Extensions' => array(
+                               array(
+                                       'core' => array(
+                                               'dependencies' => array(),
+                                       ),
+                                       'setup' => array (
+                                               'dependencies' => array('core'),
+                                       ),
+                                       'openid' => array(
+                                               'dependencies' => array('core', 'setup')
+                                       ),
+                                       'news' => array (
+                                               'dependencies' => array('extbase'),
+                                       ),
+                                       'extbase' => array (
+                                               'dependencies' => array('core'),
+                                       ),
+                                       'pt_extbase' => array (
+                                               'dependencies' => array('extbase'),
+                                       ),
+                                       'foo' => array (
+                                               'dependencies' => array(),
+                                       ),
+                               ),
+                               array(
+                                       'core', 'setup', 'openid', 'extbase'
+                               ),
+                               array(
+                                       'core' => array(
+                                               'core' => FALSE,
+                                               'setup' => FALSE,
+                                               'openid' => FALSE,
+                                               'news' => FALSE,
+                                               'extbase' => FALSE,
+                                               'pt_extbase' => FALSE,
+                                               'foo' => FALSE
+                                       ),
+                                       'setup' => array(
+                                               'core' => TRUE,
+                                               'setup' => FALSE,
+                                               'openid' => FALSE,
+                                               'news' => FALSE,
+                                               'extbase' => FALSE,
+                                               'pt_extbase' => FALSE,
+                                               'foo' => FALSE
+                                       ),
+                                       'openid' => array (
+                                               'core' => TRUE,
+                                               'setup' => TRUE,
+                                               'openid' => FALSE,
+                                               'news' => FALSE,
+                                               'extbase' => FALSE,
+                                               'pt_extbase' => FALSE,
+                                               'foo' => FALSE
+                                       ),
+                                       'news' => array (
+                                               'core' => FALSE,
+                                               'setup' => FALSE,
+                                               'openid' => TRUE,
+                                               'news' => FALSE,
+                                               'extbase' => TRUE,
+                                               'pt_extbase' => FALSE,
+                                               'foo' => FALSE
+                                       ),
+                                       'extbase' => array (
+                                               'core' => TRUE,
+                                               'setup' => FALSE,
+                                               'openid' => FALSE,
+                                               'news' => FALSE,
+                                               'extbase' => FALSE,
+                                               'pt_extbase' => FALSE,
+                                               'foo' => FALSE
+                                       ),
+                                       'pt_extbase' => array(
+                                               'core' => FALSE,
+                                               'setup' => FALSE,
+                                               'openid' => TRUE,
+                                               'news' => FALSE,
+                                               'extbase' => TRUE,
+                                               'pt_extbase' => FALSE,
+                                               'foo' => FALSE
+                                       ),
+                                       'foo' => array(
+                                               'core' => FALSE,
+                                               'setup' => FALSE,
+                                               'openid' => TRUE,
+                                               'news' => FALSE,
+                                               'extbase' => TRUE,
+                                               'pt_extbase' => FALSE,
+                                               'foo' => FALSE
+                                       ),
+                               ),
+                       ),
+                       'Dummy Packages' => array(
+                               array(
+                                       'A' => array(
+                                               'dependencies' => array('B', 'D', 'C'),
+                                       ),
+                                       'B' => array(
+                                               'dependencies' => array()
+                                       ),
+                                       'C' => array(
+                                               'dependencies' => array('E')
+                                       ),
+                                       'D' => array (
+                                               'dependencies' => array('E'),
+                                       ),
+                                       'E' => array (
+                                               'dependencies' => array(),
+                                       ),
+                                       'F' => array (
+                                               'dependencies' => array(),
+                                       ),
+                               ),
+                               array(
+                                       'B', 'C', 'E'
+                               ),
+                               array(
+                                       'A' => array(
+                                               'A' => FALSE,
+                                               'B' => TRUE,
+                                               'C' => TRUE,
+                                               'D' => TRUE,
+                                               'E' => FALSE,
+                                               'F' => FALSE,
+                                       ),
+                                       'B' => array(
+                                               'A' => FALSE,
+                                               'B' => FALSE,
+                                               'C' => FALSE,
+                                               'D' => FALSE,
+                                               'E' => FALSE,
+                                               'F' => FALSE,
+                                       ),
+                                       'C' => array(
+                                               'A' => FALSE,
+                                               'B' => FALSE,
+                                               'C' => FALSE,
+                                               'D' => FALSE,
+                                               'E' => TRUE,
+                                               'F' => FALSE,
+                                       ),
+                                       'D' => array (
+                                               'A' => FALSE,
+                                               'B' => TRUE,
+                                               'C' => TRUE,
+                                               'D' => FALSE,
+                                               'E' => FALSE,
+                                               'F' => FALSE,
+                                       ),
+                                       'E' => array (
+                                               'A' => FALSE,
+                                               'B' => FALSE,
+                                               'C' => FALSE,
+                                               'D' => FALSE,
+                                               'E' => FALSE,
+                                               'F' => FALSE,
+                                       ),
+                                       'F' => array (
+                                               'A' => FALSE,
+                                               'B' => TRUE,
+                                               'C' => TRUE,
+                                               'D' => FALSE,
+                                               'E' => FALSE,
+                                               'F' => FALSE,
+                                       ),
+                               ),
+                       ),
+               );
+       }
+
+       /**
         * @test
+        * @param array $unsorted
+        * @param array $frameworkPackageKeys
+        * @param array $expectedGraph
+        * @dataProvider buildDependencyGraphBuildsCorrectGraphDataProvider
         */
-       public function sortAvailablePackagesByDependenciesMakesSureThatDependantPackagesAreStandingBeforeAPackageInTheInternalPackagesAndPackagesConfigurationArrays() {
-               $doctrineCommon = $this->getMock('\TYPO3\Flow\Package\PackageInterface');
-               $doctrineCommon->expects($this->any())->method('getPackageKey')->will($this->returnValue('Doctrine.Common'));
+       public function buildDependencyGraphBuildsCorrectGraph(array $unsorted, array $frameworkPackageKeys, array $expectedGraph) {
+               $unsortedPackageStatesConfiguration = array('packages' => $unsorted);
 
-               $doctrineDbal = $this->getMock('\TYPO3\Flow\Package\PackageInterface');
-               $doctrineDbal->expects($this->any())->method('getPackageKey')->will($this->returnValue('Doctrine.DBAL'));
+               $packageKeys = array_keys($unsorted);
+               $unsortedPackages = array();
+               foreach ($packageKeys as $packageKey) {
+                       $packageMock = $this->getMock('\TYPO3\Flow\Package\PackageInterface');
+                       $packageMock->expects($this->any())->method('getPackageKey')->will($this->returnValue($packageKey));
+                       $unsortedPackages[$packageKey] = $packageMock;
+               }
 
-               $doctrineOrm = $this->getMock('\TYPO3\Flow\Package\PackageInterface');
-               $doctrineOrm->expects($this->any())->method('getPackageKey')->will($this->returnValue('Doctrine.ORM'));
+               $typeAssignment = array(
+                       array('', array('typo3-cms-framework'), array_diff($packageKeys, $frameworkPackageKeys)),
+                       array('typo3-cms-framework', array(), $frameworkPackageKeys),
+               );
 
-               $typo3Flow = $this->getMock('\TYPO3\Flow\Package\PackageInterface');
-               $typo3Flow->expects($this->any())->method('getPackageKey')->will($this->returnValue('TYPO3.Flow'));
+               $packageManager = $this->getAccessibleMock('\TYPO3\CMS\Core\Package\PackageManager', array('resolvePackageDependencies','getPackageKeysOfType'));
+               $packageManager->expects($this->any())->method('getPackageKeysOfType')->will($this->returnValueMap($typeAssignment));
+               $packageManager->_set('packages', $unsortedPackages);
+               $packageManager->_set('packageStatesConfiguration', $unsortedPackageStatesConfiguration);
+               $packageManager->_call('buildDependencyGraph');
 
-               $symfonyComponentYaml = $this->getMock('\TYPO3\Flow\Package\PackageInterface');
-               $symfonyComponentYaml->expects($this->any())->method('getPackageKey')->will($this->returnValue('Symfony.Component.Yaml'));
+               $this->assertEquals($expectedGraph, $packageManager->_get('dependencyGraph'));
+       }
 
-               $unsortedPackageStatesConfiguration = array('packages' =>
-                       array(
-                               'Doctrine.ORM' => array(
-                                       'dependencies' => array('Doctrine.Common', 'Doctrine.DBAL')
+       /**
+        * @return array
+        */
+       public function packageSortingDataProvider() {
+               return array(
+                       'TYPO3 Flow Packages' => array(
+                               array(
+                                       'TYPO3.Flow' => array(
+                                               'dependencies' => array('Symfony.Component.Yaml', 'Doctrine.Common', 'Doctrine.DBAL', 'Doctrine.ORM')
+                                       ),
+                                       'Doctrine.ORM' => array(
+                                               'dependencies' => array('Doctrine.Common', 'Doctrine.DBAL')
+                                       ),
+                                       'Doctrine.Common' => array(
+                                               'dependencies' => array()
+                                       ),
+                                       'Doctrine.DBAL' => array(
+                                               'dependencies' => array('Doctrine.Common')
+                                       ),
+                                       'Symfony.Component.Yaml' => array(
+                                               'dependencies' => array()
+                                       ),
                                ),
-                               'Symfony.Component.Yaml' => array(
-                                       'dependencies' => array()
+                               array(
+                                       'Doctrine.Common'
                                ),
-                               'TYPO3.Flow' => array(
-                                       'dependencies' => array('Symfony.Component.Yaml', 'Doctrine.Common', 'Doctrine.DBAL', 'Doctrine.ORM')
+                               array(
+                                       'Doctrine.Common' => array(
+                                               'dependencies' => array()
+                                       ),
+                                       'Doctrine.DBAL' => array(
+                                               'dependencies' => array('Doctrine.Common')
+                                       ),
+                                       'Doctrine.ORM' => array(
+                                               'dependencies' => array('Doctrine.Common', 'Doctrine.DBAL')
+                                       ),
+                                       'Symfony.Component.Yaml' => array(
+                                               'dependencies' => array('Doctrine.Common')
+                                       ),
+                                       'TYPO3.Flow' => array(
+                                               'dependencies' => array('Doctrine.Common', 'Symfony.Component.Yaml', 'Doctrine.DBAL', 'Doctrine.ORM')
+                                       ),
                                ),
-                               'Doctrine.Common' => array(
-                                       'dependencies' => array()
+                       ),
+                       'TYPO3 CMS Extensions' => array(
+                               array(
+                                       'core' => array(
+                                               'dependencies' => array(),
+                                       ),
+                                       'setup' => array (
+                                               'dependencies' => array('core'),
+                                       ),
+                                       'openid' => array(
+                                               'dependencies' => array('core', 'setup')
+                                       ),
+                                       'news' => array (
+                                               'dependencies' => array('extbase'),
+                                       ),
+                                       'extbase' => array (
+                                               'dependencies' => array('core'),
+                                       ),
+                                       'pt_extbase' => array (
+                                               'dependencies' => array('extbase'),
+                                       ),
+                                       'foo' => array (
+                                               'dependencies' => array(),
+                                       ),
                                ),
-                               'Doctrine.DBAL' => array(
-                                       'dependencies' => array('Doctrine.Common')
-                               )
-                       )
+                               array(
+                                       'core', 'setup', 'openid', 'extbase'
+                               ),
+                               array(
+                                       'core' => array(
+                                               'dependencies' => array(),
+                                       ),
+                                       'setup' => array (
+                                               'dependencies' => array('core'),
+                                       ),
+                                       'openid' => array(
+                                               'dependencies' => array('core', 'setup')
+                                       ),
+                                       'extbase' => array (
+                                               'dependencies' => array('core'),
+                                       ),
+                                       'foo' => array (
+                                               'dependencies' => array('openid', 'extbase'),
+                                       ),
+                                       'pt_extbase' => array (
+                                               'dependencies' => array('openid', 'extbase'),
+                                       ),
+                                       'news' => array (
+                                               'dependencies' => array('openid', 'extbase'),
+                                       ),
+                               ),
+                       ),
+                       'Dummy Packages' => array(
+                               array(
+                                       'A' => array(
+                                               'dependencies' => array('B', 'D', 'C'),
+                                       ),
+                                       'B' => array(
+                                               'dependencies' => array()
+                                       ),
+                                       'C' => array(
+                                               'dependencies' => array('E')
+                                       ),
+                                       'D' => array (
+                                               'dependencies' => array('E'),
+                                       ),
+                                       'E' => array (
+                                               'dependencies' => array(),
+                                       ),
+                                       'F' => array (
+                                               'dependencies' => array(),
+                                       ),
+                               ),
+                               array(
+                                       'B', 'C', 'E'
+                               ),
+                               array(
+                                       'B' => array(
+                                               'dependencies' => array(),
+                                       ),
+                                       'E' => array (
+                                               'dependencies' => array(),
+                                       ),
+                                       'C' => array (
+                                               'dependencies' => array('E'),
+                                       ),
+                                       'F' => array (
+                                               'dependencies' => array('B', 'C'),
+                                       ),
+                                       'D' => array(
+                                               'dependencies' => array('B', 'C'),
+                                       ),
+                                       'A' => array(
+                                               'dependencies' => array('B', 'C', 'D'),
+                                       ),
+                               ),
+                       ),
                );
+       }
+
+       /**
+        * @test
+        * @dataProvider packageSortingDataProvider
+        */
+       public function sortAvailablePackagesByDependenciesMakesSureThatDependantPackagesAreStandingBeforeAPackageInTheInternalPackagesAndPackagesConfigurationArrays($unsorted, $frameworkPackageKeys, $expected) {
+               $unsortedPackageStatesConfiguration = array('packages' => $unsorted);
+               $expectedSortedPackageStatesConfiguration = array('packages' => $expected);
+
+               $unsortedPackages = array();
+               $packageKeys = array_keys($unsorted);
+               foreach ($packageKeys as $packageKey) {
+                       $packageMock = $this->getMock('\TYPO3\Flow\Package\PackageInterface');
+                       $packageMock->expects($this->any())->method('getPackageKey')->will($this->returnValue($packageKey));
+                       $unsortedPackages[$packageKey] = $packageMock;
+               }
 
-               $unsortedPackages = array(
-                       'Doctrine.ORM' => $doctrineOrm,
-                       'Symfony.Component.Yaml' => $symfonyComponentYaml,
-                       'TYPO3.Flow' => $typo3Flow,
-                       'Doctrine.Common' => $doctrineCommon,
-                       'Doctrine.DBAL' => $doctrineDbal
+               $typeAssignment = array(
+                       array('', array('typo3-cms-framework'), array_diff($packageKeys, $frameworkPackageKeys)),
+                       array('typo3-cms-framework', array(), $frameworkPackageKeys),
                );
 
-               $packageManager = $this->getAccessibleMock('\TYPO3\Flow\Package\PackageManager', array('resolvePackageDependencies'));
+               $packageManager = $this->getAccessibleMock('\TYPO3\CMS\Core\Package\PackageManager', array('resolvePackageDependencies','getPackageKeysOfType'));
+               $packageManager->expects($this->any())->method('getPackageKeysOfType')->will($this->returnValueMap($typeAssignment));
                $packageManager->_set('packages', $unsortedPackages);
                $packageManager->_set('packageStatesConfiguration', $unsortedPackageStatesConfiguration);
                $packageManager->_call('sortAvailablePackagesByDependencies');
 
-               $expectedSortedPackageKeys = array(
-                       'Doctrine.Common',
-                       'Doctrine.DBAL',
-                       'Doctrine.ORM',
-                       'Symfony.Component.Yaml',
-                       'TYPO3.Flow'
-               );
+               $expectedSortedPackageKeys = array_keys($expected);
 
-               $expectedSortedPackageStatesConfiguration = array('packages' =>
-                       array(
-                               'Doctrine.Common' => array(
-                                       'dependencies' => array()
+               $this->assertEquals($expectedSortedPackageKeys, array_keys($packageManager->_get('packages')), 'The packages have not been ordered according to their dependencies!');
+               $this->assertEquals($expectedSortedPackageStatesConfiguration, $packageManager->_get('packageStatesConfiguration'), 'The package states configurations have not been ordered according to their dependencies!');
+       }
+
+       /**
+        * @return array
+        */
+       public function buildDependencyGraphForPackagesBuildsCorrectGraphDataProvider() {
+               return array(
+                       'TYPO3 Flow Packages' => array(
+                               array(
+                                       'TYPO3.Flow' => array(
+                                               'dependencies' => array('Symfony.Component.Yaml', 'Doctrine.Common', 'Doctrine.DBAL', 'Doctrine.ORM')
+                                       ),
+                                       'Doctrine.ORM' => array(
+                                               'dependencies' => array('Doctrine.Common', 'Doctrine.DBAL')
+                                       ),
+                                       'Doctrine.Common' => array(
+                                               'dependencies' => array()
+                                       ),
+                                       'Doctrine.DBAL' => array(
+                                               'dependencies' => array('Doctrine.Common')
+                                       ),
+                                       'Symfony.Component.Yaml' => array(
+                                               'dependencies' => array()
+                                       ),
                                ),
-                               'Doctrine.DBAL' => array(
-                                       'dependencies' => array('Doctrine.Common')
+                               array(
+                                       'TYPO3.Flow' => array(
+                                               'TYPO3.Flow' => FALSE,
+                                               'Doctrine.ORM' => TRUE,
+                                               'Doctrine.Common' => TRUE,
+                                               'Doctrine.DBAL' => TRUE,
+                                               'Symfony.Component.Yaml' => TRUE,
+                                       ),
+                                       'Doctrine.ORM' => array(
+                                               'TYPO3.Flow' => FALSE,
+                                               'Doctrine.ORM' => FALSE,
+                                               'Doctrine.Common' => TRUE,
+                                               'Doctrine.DBAL' => TRUE,
+                                               'Symfony.Component.Yaml' => FALSE,
+                                       ),
+                                       'Doctrine.Common' => array(
+                                               'TYPO3.Flow' => FALSE,
+                                               'Doctrine.ORM' => FALSE,
+                                               'Doctrine.Common' => FALSE,
+                                               'Doctrine.DBAL' => FALSE,
+                                               'Symfony.Component.Yaml' => FALSE,
+                                       ),
+                                       'Doctrine.DBAL' => array(
+                                               'TYPO3.Flow' => FALSE,
+                                               'Doctrine.ORM' => FALSE,
+                                               'Doctrine.Common' => TRUE,
+                                               'Doctrine.DBAL' => FALSE,
+                                               'Symfony.Component.Yaml' => FALSE,
+                                       ),
+                                       'Symfony.Component.Yaml' => array(
+                                               'TYPO3.Flow' => FALSE,
+                                               'Doctrine.ORM' => FALSE,
+                                               'Doctrine.Common' => FALSE,
+                                               'Doctrine.DBAL' => FALSE,
+                                               'Symfony.Component.Yaml' => FALSE,
+                                       ),
                                ),
-                               'Doctrine.ORM' => array(
-                                       'dependencies' => array('Doctrine.Common', 'Doctrine.DBAL')
+                       ),
+                       'TYPO3 CMS Extensions' => array(
+                               array(
+                                       'core' => array(
+                                               'dependencies' => array(),
+                                       ),
+                                       'openid' => array(
+                                               'dependencies' => array('core', 'setup')
+                                       ),
+                                       'scheduler' => array (
+                                               'dependencies' => array('core'),
+                                       ),
+                                       'setup' => array (
+                                               'dependencies' => array('core'),
+                                       ),
+                                       'sv' => array (
+                                               'dependencies' => array('core'),
+                                       ),
                                ),
-                               'Symfony.Component.Yaml' => array(
-                                       'dependencies' => array()
+                               array(
+                                       'core' => array(
+                                               'core' => FALSE,
+                                               'setup' => FALSE,
+                                               'sv' => FALSE,
+                                               'scheduler' => FALSE,
+                                               'openid' => FALSE,
+                                       ),
+                                       'openid' => array(
+                                               'core' => TRUE,
+                                               'setup' => TRUE,
+                                               'sv' => FALSE,
+                                               'scheduler' => FALSE,
+                                               'openid' => FALSE,
+                                       ),
+                                       'scheduler' => array (
+                                               'core' => TRUE,
+                                               'setup' => FALSE,
+                                               'sv' => FALSE,
+                                               'scheduler' => FALSE,
+                                               'openid' => FALSE,
+                                       ),
+                                       'setup' => array (
+                                               'core' => TRUE,
+                                               'setup' => FALSE,
+                                               'sv' => FALSE,
+                                               'scheduler' => FALSE,
+                                               'openid' => FALSE,
+                                       ),
+                                       'sv' => array (
+                                               'core' => TRUE,
+                                               'setup' => FALSE,
+                                               'sv' => FALSE,
+                                               'scheduler' => FALSE,
+                                               'openid' => FALSE,
+                                       ),
                                ),
-                               'TYPO3.Flow' => array(
-                                       'dependencies' => array('Symfony.Component.Yaml', 'Doctrine.Common', 'Doctrine.DBAL', 'Doctrine.ORM')
-                               )
-                       )
+                       ),
+                       'Dummy Packages' => array(
+                               array(
+                                       'A' => array(
+                                               'dependencies' => array('B', 'D', 'C'),
+                                       ),
+                                       'B' => array(
+                                               'dependencies' => array()
+                                       ),
+                                       'C' => array(
+                                               'dependencies' => array('E')
+                                       ),
+                                       'D' => array (
+                                               'dependencies' => array('E'),
+                                       ),
+                                       'E' => array (
+                                               'dependencies' => array(),
+                                       ),
+                                       'F' => array (
+                                               'dependencies' => array(),
+                                       ),
+                               ),
+                               array(
+                                       'A' => array(
+                                               'A' => FALSE,
+                                               'B' => TRUE,
+                                               'C' => TRUE,
+                                               'D' => TRUE,
+                                               'E' => FALSE,
+                                               'F' => FALSE,
+                                       ),
+                                       'B' => array(
+                                               'A' => FALSE,
+                                               'B' => FALSE,
+                                               'C' => FALSE,
+                                               'D' => FALSE,
+                                               'E' => FALSE,
+                                               'F' => FALSE,
+                                       ),
+                                       'C' => array(
+                                               'A' => FALSE,
+                                               'B' => FALSE,
+                                               'C' => FALSE,
+                                               'D' => FALSE,
+                                               'E' => TRUE,
+                                               'F' => FALSE,
+                                       ),
+                                       'D' => array (
+                                               'A' => FALSE,
+                                               'B' => FALSE,
+                                               'C' => FALSE,
+                                               'D' => FALSE,
+                                               'E' => TRUE,
+                                               'F' => FALSE,
+                                       ),
+                                       'E' => array (
+                                               'A' => FALSE,
+                                               'B' => FALSE,
+                                               'C' => FALSE,
+                                               'D' => FALSE,
+                                               'E' => FALSE,
+                                               'F' => FALSE,
+                                       ),
+                                       'F' => array (
+                                               'A' => FALSE,
+                                               'B' => FALSE,
+                                               'C' => FALSE,
+                                               'D' => FALSE,
+                                               'E' => FALSE,
+                                               'F' => FALSE,
+                                       ),
+                               ),
+                       ),
                );
+       }
 
-               $this->assertEquals($expectedSortedPackageKeys, array_keys($packageManager->_get('packages')), 'The packages have not been ordered according to their dependencies!');
-               $this->assertEquals($expectedSortedPackageStatesConfiguration, $packageManager->_get('packageStatesConfiguration'), 'The package states configurations have not been ordered according to their dependencies!');
+       /**
+        * @test
+        * @dataProvider buildDependencyGraphForPackagesBuildsCorrectGraphDataProvider
+        */
+       public function buildDependencyGraphForPackagesBuildsCorrectGraph($packages, $expectedGraph) {
+               $packageManager = $this->getAccessibleMock('\TYPO3\CMS\Core\Package\PackageManager', array('dummy'));
+               $packageManager->_set('packageStatesConfiguration', array('packages' => $packages));
+               $packageManager->_call('buildDependencyGraphForPackages', array_keys($packages));
+
+               $this->assertEquals($expectedGraph, $packageManager->_get('dependencyGraph'));
+       }
+
+
+       /**
+        * @test
+        * @expectedException \UnexpectedValueException
+        */
+       public function getAvailablePackageLoadingOrderThrowsExceptionWhenCycleDetected() {
+               $unsorted = array(
+                       'A' => array(
+                               'dependencies' => array('B'),
+                       ),
+                       'B' => array(
+                               'dependencies' => array('A')
+                       ),
+               );
+               $unsortedPackageStatesConfiguration = array('packages' => $unsorted);
+
+               $unsortedPackages = array();
+               $packageKeys = array_keys($unsorted);
+               foreach ($packageKeys as $packageKey) {
+                       $packageMock = $this->getMock('\TYPO3\Flow\Package\PackageInterface');
+                       $packageMock->expects($this->any())->method('getPackageKey')->will($this->returnValue($packageKey));
+                       $unsortedPackages[$packageKey] = $packageMock;
+               }
+
+               $typeAssignment = array(
+                       array('', array('typo3-cms-framework'), $packageKeys),
+                       array('typo3-cms-framework', array(), array()),
+               );
+
+               $packageManager = $this->getAccessibleMock('\TYPO3\CMS\Core\Package\PackageManager', array('resolvePackageDependencies','getPackageKeysOfType'));
+               $packageManager->expects($this->any())->method('getPackageKeysOfType')->will($this->returnValueMap($typeAssignment));
+               $packageManager->_set('packages', $unsortedPackages);
+               $packageManager->_set('packageStatesConfiguration', $unsortedPackageStatesConfiguration);
+               $packageManager->_call('getAvailablePackageLoadingOrder');
+       }
+
+       /**
+        * @test
+        * @expectedException \UnexpectedValueException
+        */
+       public function buildDependencyGraphForPackagesThrowsExceptionWhenDependencyOnUnavailablePackageDetected() {
+               $packages = array(
+                       'A' => array(
+                               'dependencies' => array('B'),
+                       )
+               );
+               $packageManager = $this->getAccessibleMock('\TYPO3\CMS\Core\Package\PackageManager', array('dummy'));
+               $packageManager->_set('packageStatesConfiguration', array('packages' => $packages));
+               $packageManager->_call('buildDependencyGraphForPackages', array_keys($packages));
        }
 
        /**
@@ -529,7 +1143,6 @@ class PackageManagerTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
         * @dataProvider composerNamesAndPackageKeys
         */
        public function getPackageKeyFromComposerNameIgnoresCaseDifferences($composerName, $packageKey) {
-
                $packageStatesConfiguration = array('packages' =>
                        array(
                                'TYPO3.Flow' => array(
@@ -546,5 +1159,4 @@ class PackageManagerTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
 
                $this->assertEquals($packageKey, $packageManager->_call('getPackageKeyFromComposerName', $composerName));
        }
-
 }
index e2e04af..512997d 100644 (file)
@@ -7,7 +7,6 @@
        "version": "6.2.0",
 
        "require": {
-               "typo3/cms/core": "*",
                "typo3/cms/cms": "*"
        },
        "replace": {
index c6da29d..c2ce7b1 100644 (file)
@@ -7,7 +7,9 @@
        "version": "6.2.0",
 
        "require": {
-               "typo3/cms/core": "*"
+               "typo3/cms/core": "*",
+               "typo3/cms/sv": "*",
+               "typo3/cms/setup": "*"
        },
        "replace": {
                "openid": "*"
index 0c97a01..07402b0 100644 (file)
@@ -31,6 +31,8 @@ $EM_CONF[$_EXTKEY] = array(
        'constraints' => array(
                'depends' => array(
                        'typo3' => '6.2.0-6.2.99',
+                       'sv' => '6.2.0-6.2.99',
+                       'setup' => '6.2.0-6.2.99',
                ),
                'conflicts' => array(
                        'naw_openid' => '',