From b194a0b17c9064e4d0534b402f2b8711b4c3feed Mon Sep 17 00:00:00 2001 From: odain Date: Tue, 9 Sep 2025 16:52:41 +0200 Subject: [PATCH] revert to previous legacy order + gather new module computation classes in a dedicated folder --- .../ItopCoreModuleDependency.class.inc.php | 118 +++++ setup/module/iTopCoreModule.class.inc.php | 109 +++++ ...iTopCoreModuleDependencySort.class.inc.php | 207 +++++++++ setup/modulediscovery.class.inc.php | 417 ++---------------- ...t.php => ItopCoreModuleDependencyTest.php} | 4 +- .../setup/ModuleDiscoveryTest.php | 11 +- ...{ModuleTest.php => iTopCoreModuleTest.php} | 17 +- 7 files changed, 497 insertions(+), 386 deletions(-) create mode 100644 setup/module/ItopCoreModuleDependency.class.inc.php create mode 100644 setup/module/iTopCoreModule.class.inc.php create mode 100644 setup/module/iTopCoreModuleDependencySort.class.inc.php rename tests/php-unit-tests/unitary-tests/setup/{ModuleDependencyTest.php => ItopCoreModuleDependencyTest.php} (98%) rename tests/php-unit-tests/unitary-tests/setup/{ModuleTest.php => iTopCoreModuleTest.php} (85%) diff --git a/setup/module/ItopCoreModuleDependency.class.inc.php b/setup/module/ItopCoreModuleDependency.class.inc.php new file mode 100644 index 000000000..079b5aa6c --- /dev/null +++ b/setup/module/ItopCoreModuleDependency.class.inc.php @@ -0,0 +1,118 @@ +sDepString = $sDepString; + $this->aParamsPerModuleId = []; + $this->aPotentialPrerequisites = []; + + if (preg_match_all('/([^\(\)&| ]+)/', $sDepString, $aMatches)) + { + foreach($aMatches as $aMatch) + { + foreach($aMatch as $sModuleId) + { + if (! array_key_exists($sModuleId, $this->aParamsPerModuleId)) { + // $sModuleId in the dependency string is made of a / + // where the operator is < <= = > >= (by default >=) + $aModuleMatches = array(); + if (preg_match('|^([^/]+)/(?=?)([^><=]+)$|', $sModuleId, $aModuleMatches)) { + $sModuleName = $aModuleMatches[1]; + $this->aPotentialPrerequisites[$sModuleName] = true; + $sOperator = $aModuleMatches[2]; + if ($sOperator == '') { + $sOperator = '>='; + } + $sExpectedVersion = $aModuleMatches[3]; + $this->aParamsPerModuleId[$sModuleId] = [$sModuleName, $sOperator, $sExpectedVersion]; + } + } + } + } + } else { + $this->bAlwaysUnresolved=true; + } + } + + /** + * Return module names potentially required by current dependency + * @return array + */ + public function GetPotentialPrerequisiteModuleNames() : array + { + return array_keys($this->aPotentialPrerequisites); + } + + /** + * Check if dependency is resolved with current list of module versions + * @param array $aModuleVersions: versions by module names dict + * @param array $aSelectedModules: modules names dict + * + * @return bool + */ + public function IsDependencyResolved(array $aModuleVersions, array $aSelectedModules) : bool + { + if ($this->bAlwaysUnresolved){ + return false; + } + + $aReplacements=[]; + foreach ($this->aParamsPerModuleId as $sModuleId => list($sModuleName, $sOperator, $sExpectedVersion)){ + if (array_key_exists($sModuleName, $aModuleVersions)) + { + // module is present, check the version + $sCurrentVersion = $aModuleVersions[$sModuleName]; + if (version_compare($sCurrentVersion, $sExpectedVersion, $sOperator)) + { + if (array_key_exists($sModuleName, $this->aPotentialPrerequisites)) { + unset($this->aPotentialPrerequisites[$sModuleName]); + } + $aReplacements[$sModuleId] = '(true)'; // Add parentheses to protect against invalid condition causing + // a function call that results in a runtime fatal error + } + else + { + $aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing + // a function call that results in a runtime fatal error + } + } + else + { + // module is not present + $aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing + // a function call that results in a runtime fatal error + } + } + + foreach ($this->aPotentialPrerequisites as $sModuleName) + { + if (array_key_exists($sModuleName, $aSelectedModules)) + { + // This module is actually a prerequisite + if (!array_key_exists($sModuleName, $aModuleVersions)) + { + return false; + } + } + } + + $bResult=false; + $sBooleanExpr = str_replace(array_keys($aReplacements), array_values($aReplacements), $this->sDepString); + $bOk = @eval('$bResult = '.$sBooleanExpr.'; return true;'); + if ($bOk == false) + { + SetupLog::Warning("Eval of '$sBooleanExpr' returned false"); + echo "Failed to parse the boolean Expression = '$sBooleanExpr'
"; + } + return $bResult; + } +} \ No newline at end of file diff --git a/setup/module/iTopCoreModule.class.inc.php b/setup/module/iTopCoreModule.class.inc.php new file mode 100644 index 000000000..1557670bf --- /dev/null +++ b/setup/module/iTopCoreModule.class.inc.php @@ -0,0 +1,109 @@ +sModuleId = $sModuleId; + list($this->sModuleName, $this->sVersion) = ModuleDiscovery::GetModuleName($sModuleId); + if (strlen($this->sVersion) == 0) { + // No version number found, assume 1.0.0 + $this->sVersion = '1.0.0'; + } + } + + /** + * @return string + */ + public function GetModuleName() + { + return $this->sModuleName; + } + + /** + * @return string + */ + public function GetVersion() + { + return $this->sVersion; + } + + /** + * @return string + */ + public function GetModuleId() + { + return $this->sModuleId; + } + + /** + * @param array $aAllDependencies: list of dependencies (string) + * + * @return void + */ + public function SetDependencies(array $aAllDependencies): void + { + $this->aAllDependencies = $aAllDependencies; + $this->aOngoingDependencies = []; + + foreach ($aAllDependencies as $sDepString){ + $this->aOngoingDependencies[$sDepString]= new iTopCoreModuleDependency($sDepString); + } + } + + /** + * Check if module dependencies are resolved with current list of module versions + * @param array $aModuleVersions : versions by module names dict + * @param array $aSelectedModules : modules names dict + * + * @return bool + */ + public function IsModuleResolved(array $aModuleVersions, array $aSelectedModules) : bool + { + $aNextDependencies=[]; + $bDependenciesSolved = true; + foreach($this->aOngoingDependencies as $sDepId => $oModuleDependency) + { + /** @var iTopCoreModuleDependency $oModuleDependency*/ + if (!$oModuleDependency->IsDependencyResolved($aModuleVersions, $aSelectedModules)) + { + $aNextDependencies[$sDepId]=$oModuleDependency; + $bDependenciesSolved = false; + } + } + + $this->aOngoingDependencies=$aNextDependencies; + + if ($bDependenciesSolved) + { + return true; + } + + return false; + } + + /** + * @return array: list of unique module names + */ + public function GetUnresolvedDependencyModuleNames(): array + { + $aRes=[]; + foreach($this->aOngoingDependencies as $sDepId => $oModuleDependency) { + /** @var iTopCoreModuleDependency $oModuleDependency */ + $aRes = array_merge($aRes, $oModuleDependency->GetPotentialPrerequisiteModuleNames()); + } + + return array_unique($aRes); + } +} \ No newline at end of file diff --git a/setup/module/iTopCoreModuleDependencySort.class.inc.php b/setup/module/iTopCoreModuleDependencySort.class.inc.php new file mode 100644 index 000000000..2da91bf13 --- /dev/null +++ b/setup/module/iTopCoreModuleDependencySort.class.inc.php @@ -0,0 +1,207 @@ + $oModule) { + /** @var iTopCoreModule $oModule */ + $aDependsOnModuleName[$oModule->GetModuleName()]=[]; + } + + foreach ($aUnresolvedDependencyModules as $sModuleId => $oModule) { + $iInDegreeCounter = 0; + /** @var iTopCoreModule $oModule */ + $aUnresolvedDependencyModuleNames = $oModule->GetUnresolvedDependencyModuleNames(); + foreach ($aUnresolvedDependencyModuleNames as $sModuleName) { + if (array_key_exists($sModuleName, $aDependsOnModuleName)) { + $aDependsOnModuleName[$sModuleName][] = $sModuleId; + $iInDegreeCounter++; + } + } + //include all modules + $iInDegreeCounterIncludingOutsideModules = count($oModule->GetUnresolvedDependencyModuleNames()); + $aCountDepsByModuleId[$sModuleId] = [$iInDegreeCounter, $iInDegreeCounterIncludingOutsideModules, $sModuleId]; + } + + $aRes=[]; + while(count($aUnresolvedDependencyModules)>0) { + asort($aCountDepsByModuleId); + + uasort($aCountDepsByModuleId, function (array $aDeps1, array $aDeps2){ + //compare $iInDegreeCounter + $res = $aDeps1[0] - $aDeps2[0]; + if ($res != 0){ + return $res; + } + + //compare $iInDegreeCounterIncludingOutsideModules + $res = $aDeps1[1] - $aDeps2[1]; + if ($res != 0){ + return $res; + } + + //alphabetical order at least + return strcmp($aDeps1[2], $aDeps2[2]); + }); + + $bOneLoopAtLeast=false; + foreach ($aCountDepsByModuleId as $sModuleId => $iInDegreeCounter){ + $oModule=$aUnresolvedDependencyModules[$sModuleId]; + + if ($bOneLoopAtLeast && $iInDegreeCounter>0){ + break; + } + + unset($aUnresolvedDependencyModules[$sModuleId]); + unset($aCountDepsByModuleId[$sModuleId]); + + $aRes[$sModuleId]=$oModule; + + //when 2 versions of the same module (name) below array has been removed already + if (array_key_exists($oModule->GetModuleName(), $aDependsOnModuleName)) { + foreach ($aDependsOnModuleName[$oModule->GetModuleName()] as $sModuleId2) { + if (! array_key_exists($sModuleId2, $aCountDepsByModuleId)){ + continue; + } + $aDepCount = $aCountDepsByModuleId[$sModuleId2]; + $iInDegreeCounter = $aDepCount[0] - 1; + $iInDegreeCounterIncludingOutsideModules = $aDepCount[1]; + $aCountDepsByModuleId[$sModuleId2] = [$iInDegreeCounter, $iInDegreeCounterIncludingOutsideModules, $sModuleId2]; + } + + unset($aDependsOnModuleName[$oModule->GetModuleName()]); + } + + $bOneLoopAtLeast=true; + } + } + + $aUnresolvedDependencyModules=$aRes; + } + + /** + * Arrange an list of modules, based on their (inter) dependencies + * @param array $aModules The list of modules to process: 'id' => $aModuleInfo + * @param bool $bAbortOnMissingDependency ... + * @param array $aModulesToLoad List of modules to search for, defaults to all if omitted + * @param int $iLoopCount: used to count loop count for testing purpose (see if algo is optimized) + * @return array + * @throws \MissingDependencyException + */ + public function OrderModulesByDependencies($aModules, $bAbortOnMissingDependency = false, $aModulesToLoad = null, ?int &$iLoopCount=0) + { + $iLoopCount=0; + + // Order the modules to take into account their inter-dependencies + $aUnresolvedDependencyModules = []; + $aSelectedModules = []; + foreach($aModules as $sModuleId => $aModule) + { + $oModule = new iTopCoreModule($sModuleId); + $sModuleName = $oModule->GetModuleName(); + if (is_null($aModulesToLoad) || in_array($sModuleName, $aModulesToLoad)) + { + $oModule->SetDependencies($aModule['dependencies']); + $aUnresolvedDependencyModules[$sModuleId]=$oModule; + $aSelectedModules[$sModuleName] = true; + } + } + self::SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); + $aOrderedModules = []; + $aModuleVersions=[]; + $iPreviousLoopDepencyCount=-1; + $iNextLoopCount=count($aUnresolvedDependencyModules); + while(($iNextLoopCount!=$iPreviousLoopDepencyCount) //stop loop when no new dependency is resolved + && ($iNextLoopCount > 0) //still remaining dependencies + ) + { + $iLoopCount++; + $iPreviousLoopDepencyCount=$iNextLoopCount; + foreach($aUnresolvedDependencyModules as $sModuleId => $oModule) + { + /** @var iTopCoreModule $oModule */ + if ($oModule->IsModuleResolved($aModuleVersions, $aSelectedModules)){ + $aOrderedModules[] = $sModuleId; + $aModuleVersions[$oModule->GetModuleName()] = $oModule->GetVersion(); + unset($aUnresolvedDependencyModules[$sModuleId]); + } + } + + $iNextLoopCount=count($aUnresolvedDependencyModules); + self::SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); + } + + if ($bAbortOnMissingDependency && count($aUnresolvedDependencyModules) > 0) + { + self::SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); + $aModulesInfo = []; + $aModuleDeps = []; + /** @var iTopCoreModule $oModule */ + foreach($aUnresolvedDependencyModules as $sModuleId => $oModule) + { + $aModule = $aModules[$sModuleId]; + $aDepsWithIcons = []; + foreach($oModule->aAllDependencies as $sIndex => $sDepId) + { + if (array_key_exists($sDepId, $oModule->aOngoingDependencies)) + { + $aDepsWithIcons[$sIndex] = '❌ ' . $sDepId; + } else + { + $aDepsWithIcons[$sIndex] = '✅ ' . $sDepId; + } + } + $aModuleDeps[] = "{$aModule['label']} (id: $sModuleId) depends on: ".implode(' + ', $aDepsWithIcons); + $aModulesInfo[$sModuleId] = array('module' => $aModule, 'dependencies' => $aDepsWithIcons); + } + $sMessage = "The following modules have unmet dependencies:\n".implode(",\n", $aModuleDeps); + $oException = new MissingDependencyException($sMessage); + $oException->aModulesInfo = $aModulesInfo; + throw $oException; + } + // Return the ordered list, so that the dependencies are met... + $aResult = array(); + foreach($aOrderedModules as $sModuleId) + { + $aResult[$sModuleId] = $aModules[$sModuleId]; + } + return $aResult; + } +} \ No newline at end of file diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php index 8ccb5d285..0757c9d5a 100755 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -19,9 +19,9 @@ * */ -use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator; - require_once(APPROOT.'setup/modulediscovery/ModuleFileReader.php'); +require_once(__DIR__ . '/module/iTopCoreModuleDependencySort.class.inc.php'); +use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator; class MissingDependencyException extends CoreException { @@ -68,229 +68,6 @@ HTML; } } -/** - * Class that handles a module dependency - */ -class iTopCoreModuleDependency { - private array $aPotentialPrerequisites; - private array $aParamsPerModuleId; - private string $sDepString; - private bool $bAlwaysUnresolved=false; - - public function __construct(string $sDepString) - { - $this->sDepString = $sDepString; - $this->aParamsPerModuleId = []; - $this->aPotentialPrerequisites = []; - - if (preg_match_all('/([^\(\)&| ]+)/', $sDepString, $aMatches)) - { - foreach($aMatches as $aMatch) - { - foreach($aMatch as $sModuleId) - { - if (! array_key_exists($sModuleId, $this->aParamsPerModuleId)) { - // $sModuleId in the dependency string is made of a / - // where the operator is < <= = > >= (by default >=) - $aModuleMatches = array(); - if (preg_match('|^([^/]+)/(?=?)([^><=]+)$|', $sModuleId, $aModuleMatches)) { - $sModuleName = $aModuleMatches[1]; - $this->aPotentialPrerequisites[$sModuleName] = true; - $sOperator = $aModuleMatches[2]; - if ($sOperator == '') { - $sOperator = '>='; - } - $sExpectedVersion = $aModuleMatches[3]; - $this->aParamsPerModuleId[$sModuleId] = [$sModuleName, $sOperator, $sExpectedVersion]; - } - } - } - } - } else { - $this->bAlwaysUnresolved=true; - } - } - - /** - * Return module names potentially required by current dependency - * @return array - */ - public function GetPotentialPrerequisiteModuleNames() : array - { - return array_keys($this->aPotentialPrerequisites); - } - - /** - * Check if dependency is resolved with current list of module versions - * @param array $aModuleVersions: versions by module names dict - * @param array $aSelectedModules: modules names dict - * - * @return bool - */ - public function IsDependencyResolved(array $aModuleVersions, array $aSelectedModules) : bool - { - if ($this->bAlwaysUnresolved){ - return false; - } - - $aReplacements=[]; - foreach ($this->aParamsPerModuleId as $sModuleId => list($sModuleName, $sOperator, $sExpectedVersion)){ - if (array_key_exists($sModuleName, $aModuleVersions)) - { - // module is present, check the version - $sCurrentVersion = $aModuleVersions[$sModuleName]; - if (version_compare($sCurrentVersion, $sExpectedVersion, $sOperator)) - { - if (array_key_exists($sModuleName, $this->aPotentialPrerequisites)) { - unset($this->aPotentialPrerequisites[$sModuleName]); - } - $aReplacements[$sModuleId] = '(true)'; // Add parentheses to protect against invalid condition causing - // a function call that results in a runtime fatal error - } - else - { - $aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing - // a function call that results in a runtime fatal error - } - } - else - { - // module is not present - $aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing - // a function call that results in a runtime fatal error - } - } - - foreach ($this->aPotentialPrerequisites as $sModuleName) - { - if (array_key_exists($sModuleName, $aSelectedModules)) - { - // This module is actually a prerequisite - if (!array_key_exists($sModuleName, $aModuleVersions)) - { - return false; - } - } - } - - $bResult=false; - $sBooleanExpr = str_replace(array_keys($aReplacements), array_values($aReplacements), $this->sDepString); - $bOk = @eval('$bResult = '.$sBooleanExpr.'; return true;'); - if ($bOk == false) - { - SetupLog::Warning("Eval of '$sBooleanExpr' returned false"); - echo "Failed to parse the boolean Expression = '$sBooleanExpr'
"; - } - return $bResult; - } -} - -/** - * Class that handles a modules and all its dependencies - */ -class iTopCoreModule { - private string $sModuleId; - private string $sModuleName; - private string $sVersion; - - public array $aAllDependencies; - public array $aOngoingDependencies; - - public function __construct(string $sModuleId) - { - $this->sModuleId = $sModuleId; - list($this->sModuleName, $this->sVersion) = ModuleDiscovery::GetModuleName($sModuleId); - if (strlen($this->sVersion) == 0) { - // No version number found, assume 1.0.0 - $this->sVersion = '1.0.0'; - } - } - - /** - * @return string - */ - public function GetModuleName() - { - return $this->sModuleName; - } - - /** - * @return string - */ - public function GetVersion() - { - return $this->sVersion; - } - - /** - * @return string - */ - public function GetModuleId() - { - return $this->sModuleId; - } - - /** - * @param array $aAllDependencies: list of dependencies (string) - * - * @return void - */ - public function SetDependencies(array $aAllDependencies): void - { - $this->aAllDependencies = $aAllDependencies; - $this->aOngoingDependencies = []; - - foreach ($aAllDependencies as $sDepString){ - $this->aOngoingDependencies[$sDepString]= new iTopCoreModuleDependency($sDepString); - } - } - - /** - * Check if module dependencies are resolved with current list of module versions - * @param array $aModuleVersions : versions by module names dict - * @param array $aSelectedModules : modules names dict - * - * @return bool - */ - public function IsModuleResolved(array $aModuleVersions, array $aSelectedModules) : bool - { - $aNextDependencies=[]; - $bDependenciesSolved = true; - foreach($this->aOngoingDependencies as $sDepId => $oModuleDependency) - { - /** @var iTopCoreModuleDependency $oModuleDependency*/ - if (!$oModuleDependency->IsDependencyResolved($aModuleVersions, $aSelectedModules)) - { - $aNextDependencies[$sDepId]=$oModuleDependency; - $bDependenciesSolved = false; - } - } - - $this->aOngoingDependencies=$aNextDependencies; - - if ($bDependenciesSolved) - { - return true; - } - - return false; - } - - /** - * @return array: list of unique module names - */ - public function GetUnresolvedDependencyModuleNames(): array - { - $aRes=[]; - foreach($this->aOngoingDependencies as $sDepId => $oModuleDependency) { - /** @var iTopCoreModuleDependency $oModuleDependency */ - $aRes = array_merge($aRes, $oModuleDependency->GetPotentialPrerequisiteModuleNames()); - } - - return array_unique($aRes); - } -} - class ModuleDiscovery { static $m_aModuleArgs = array( @@ -409,7 +186,7 @@ class ModuleDiscovery $sDir = dirname($sFilePath); $aDirs = [ $sDir => self::$m_sModulePath, - $sDir.'/dictionaries' => self::$m_sModulePath.'/dictionaries', + $sDir.'/dictionaries' => self::$m_sModulePath.'/dictionaries' ]; foreach ($aDirs as $sRootDir => $sPath) { @@ -444,173 +221,71 @@ class ModuleDiscovery return self::OrderModulesByDependencies(self::$m_aModules, $bAbortOnMissingDependency, $aModulesToLoad); } - /** - * This method is key as it sorts modules by their dependencies (topological sort). - * Modules with less dependencies are first. - * When module A depends from module B with same amount of dependencies, moduleB is first. - * This order can deal with - * - cyclic dependencies - * - further versions of same module (name) - * - * @param array $aUnresolvedDependencyModules: dict of Module objects by moduleId key - * - * @return void - */ - public static function SortModulesByCountOfDepencenciesDescending(array &$aUnresolvedDependencyModules) : void - { - $aCountDepsByModuleId=[]; - $aDependsOnModuleName=[]; - - foreach($aUnresolvedDependencyModules as $sModuleId => $oModule) { - /** @var iTopCoreModule $oModule */ - $aDependsOnModuleName[$oModule->GetModuleName()]=[]; - } - - foreach ($aUnresolvedDependencyModules as $sModuleId => $oModule) { - $iInDegreeCounter = 0; - /** @var iTopCoreModule $oModule */ - $aUnresolvedDependencyModuleNames = $oModule->GetUnresolvedDependencyModuleNames(); - foreach ($aUnresolvedDependencyModuleNames as $sModuleName) { - if (array_key_exists($sModuleName, $aDependsOnModuleName)) { - $aDependsOnModuleName[$sModuleName][] = $sModuleId; - $iInDegreeCounter++; - } - } - //include all modules - $iInDegreeCounterIncludingOutsideModules = count($oModule->GetUnresolvedDependencyModuleNames()); - $aCountDepsByModuleId[$sModuleId] = [$iInDegreeCounter, $iInDegreeCounterIncludingOutsideModules, $sModuleId]; - } - - $aRes=[]; - while(count($aUnresolvedDependencyModules)>0) { - asort($aCountDepsByModuleId); - - uasort($aCountDepsByModuleId, function (array $aDeps1, array $aDeps2){ - //compare $iInDegreeCounter - $res = $aDeps1[0] - $aDeps2[0]; - if ($res != 0){ - return $res; - } - - //compare $iInDegreeCounterIncludingOutsideModules - $res = $aDeps1[1] - $aDeps2[1]; - if ($res != 0){ - return $res; - } - - //alphabetical order at least - return strcmp($aDeps1[2], $aDeps2[2]); - }); - - $bOneLoopAtLeast=false; - foreach ($aCountDepsByModuleId as $sModuleId => $iInDegreeCounter){ - $oModule=$aUnresolvedDependencyModules[$sModuleId]; - - if ($bOneLoopAtLeast && $iInDegreeCounter>0){ - break; - } - - unset($aUnresolvedDependencyModules[$sModuleId]); - unset($aCountDepsByModuleId[$sModuleId]); - - $aRes[$sModuleId]=$oModule; - - //when 2 versions of the same module (name) below array has been removed already - if (array_key_exists($oModule->GetModuleName(), $aDependsOnModuleName)) { - foreach ($aDependsOnModuleName[$oModule->GetModuleName()] as $sModuleId2) { - if (! array_key_exists($sModuleId2, $aCountDepsByModuleId)){ - continue; - } - $aDepCount = $aCountDepsByModuleId[$sModuleId2]; - $iInDegreeCounter = $aDepCount[0] - 1; - $iInDegreeCounterIncludingOutsideModules = $aDepCount[1]; - $aCountDepsByModuleId[$sModuleId2] = [$iInDegreeCounter, $iInDegreeCounterIncludingOutsideModules, $sModuleId2]; - } - - unset($aDependsOnModuleName[$oModule->GetModuleName()]); - } - - $bOneLoopAtLeast=true; - } - } - - $aUnresolvedDependencyModules=$aRes; - } - /** * Arrange an list of modules, based on their (inter) dependencies * @param array $aModules The list of modules to process: 'id' => $aModuleInfo * @param bool $bAbortOnMissingDependency ... * @param array $aModulesToLoad List of modules to search for, defaults to all if omitted - * @param int $iLoopCount: used to count loop count for testing purpose (see if algo is optimized) * @return array * @throws \MissingDependencyException */ - public static function OrderModulesByDependencies($aModules, $bAbortOnMissingDependency = false, $aModulesToLoad = null, ?int &$iLoopCount=0) + public static function OrderModulesByDependencies($aModules, $bAbortOnMissingDependency = false, $aModulesToLoad = null) { - $iLoopCount=0; - // Order the modules to take into account their inter-dependencies - $aUnresolvedDependencyModules = []; + $aDependencies = []; $aSelectedModules = []; - foreach($aModules as $sModuleId => $aModule) + foreach($aModules as $sId => $aModule) { - $oModule = new iTopCoreModule($sModuleId); - $sModuleName = $oModule->GetModuleName(); + list($sModuleName, ) = self::GetModuleName($sId); if (is_null($aModulesToLoad) || in_array($sModuleName, $aModulesToLoad)) { - $oModule->SetDependencies($aModule['dependencies']); - $aUnresolvedDependencyModules[$sModuleId]=$oModule; + $aDependencies[$sId] = $aModule['dependencies']; $aSelectedModules[$sModuleName] = true; } } - self::SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); + ksort($aDependencies); $aOrderedModules = []; - $aModuleVersions=[]; - $iPreviousLoopDepencyCount=-1; - $iNextLoopCount=count($aUnresolvedDependencyModules); - while(($iNextLoopCount!=$iPreviousLoopDepencyCount) //stop loop when no new dependency is resolved - && ($iNextLoopCount > 0) //still remaining dependencies - ) + $iLoopCount = 1; + while(($iLoopCount < count($aModules)) && (count($aDependencies) > 0) ) { - $iLoopCount++; - $iPreviousLoopDepencyCount=$iNextLoopCount; - foreach($aUnresolvedDependencyModules as $sModuleId => $oModule) + foreach($aDependencies as $sId => $aRemainingDeps) { - /** @var iTopCoreModule $oModule */ - if ($oModule->IsModuleResolved($aModuleVersions, $aSelectedModules)){ - $aOrderedModules[] = $sModuleId; - $aModuleVersions[$oModule->GetModuleName()] = $oModule->GetVersion(); - unset($aUnresolvedDependencyModules[$sModuleId]); - } - } - - $iNextLoopCount=count($aUnresolvedDependencyModules); - self::SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); - } - - if ($bAbortOnMissingDependency && count($aUnresolvedDependencyModules) > 0) - { - self::SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); - $aModulesInfo = []; - $aModuleDeps = []; - /** @var iTopCoreModule $oModule */ - foreach($aUnresolvedDependencyModules as $sModuleId => $oModule) - { - $aModule = $aModules[$sModuleId]; - $aDepsWithIcons = []; - foreach($oModule->aAllDependencies as $sIndex => $sDepId) + $bDependenciesSolved = true; + foreach($aRemainingDeps as $sDepId) { - if (array_key_exists($sDepId, $oModule->aOngoingDependencies)) + if (!self::DependencyIsResolved($sDepId, $aOrderedModules, $aSelectedModules)) { - $aDepsWithIcons[$sIndex] = '❌ ' . $sDepId; - } else - { - $aDepsWithIcons[$sIndex] = '✅ ' . $sDepId; + $bDependenciesSolved = false; } } - $aModuleDeps[] = "{$aModule['label']} (id: $sModuleId) depends on: ".implode(' + ', $aDepsWithIcons); - $aModulesInfo[$sModuleId] = array('module' => $aModule, 'dependencies' => $aDepsWithIcons); + if ($bDependenciesSolved) + { + $aOrderedModules[] = $sId; + unset($aDependencies[$sId]); + } + } + $iLoopCount++; + } + if ($bAbortOnMissingDependency && count($aDependencies) > 0) + { + $aModulesInfo = []; + $aModuleDeps = []; + foreach($aDependencies as $sId => $aDeps) + { + $aModule = $aModules[$sId]; + $aDepsWithIcons = []; + foreach($aDeps as $sIndex => $sDepId) + { + if (self::DependencyIsResolved($sDepId, $aOrderedModules, $aSelectedModules)) + { + $aDepsWithIcons[$sIndex] = '✅ ' . $sDepId; + } else + { + $aDepsWithIcons[$sIndex] = '❌ ' . $sDepId; + } + } + $aModuleDeps[] = "{$aModule['label']} (id: $sId) depends on: ".implode(' + ', $aDepsWithIcons); + $aModulesInfo[$sId] = array('module' => $aModule, 'dependencies' => $aDepsWithIcons); } $sMessage = "The following modules have unmet dependencies:\n".implode(",\n", $aModuleDeps); $oException = new MissingDependencyException($sMessage); @@ -619,9 +294,9 @@ class ModuleDiscovery } // Return the ordered list, so that the dependencies are met... $aResult = array(); - foreach($aOrderedModules as $sModuleId) + foreach($aOrderedModules as $sId) { - $aResult[$sModuleId] = $aModules[$sModuleId]; + $aResult[$sId] = $aModules[$sId]; } return $aResult; } diff --git a/tests/php-unit-tests/unitary-tests/setup/ModuleDependencyTest.php b/tests/php-unit-tests/unitary-tests/setup/ItopCoreModuleDependencyTest.php similarity index 98% rename from tests/php-unit-tests/unitary-tests/setup/ModuleDependencyTest.php rename to tests/php-unit-tests/unitary-tests/setup/ItopCoreModuleDependencyTest.php index 8faec380e..1e8f93cf3 100644 --- a/tests/php-unit-tests/unitary-tests/setup/ModuleDependencyTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/ItopCoreModuleDependencyTest.php @@ -5,11 +5,11 @@ namespace Combodo\iTop\Test\UnitTest\Setup; use Combodo\iTop\Test\UnitTest\ItopTestCase; use iTopCoreModuleDependency; -class ModuleDependencyTest extends ItopTestCase +class ItopCoreModuleDependencyTest extends ItopTestCase { public function setUp(): void { parent::setUp(); - $this->RequireOnceItopFile('setup/modulediscovery.class.inc.php'); + $this->RequireOnceItopFile('setup/module/ItopCoreModuleDependency.class.inc.php'); } public function testModuleDependencyInit_Invalid() diff --git a/tests/php-unit-tests/unitary-tests/setup/ModuleDiscoveryTest.php b/tests/php-unit-tests/unitary-tests/setup/ModuleDiscoveryTest.php index 72680941f..6884c2320 100644 --- a/tests/php-unit-tests/unitary-tests/setup/ModuleDiscoveryTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/ModuleDiscoveryTest.php @@ -3,6 +3,7 @@ namespace Combodo\iTop\Test\UnitTest\Setup; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; +use iTopCoreModuleDependencySort; use ModuleDiscovery; class ModuleDiscoveryTest extends ItopDataTestCase @@ -257,7 +258,7 @@ MSG; foreach (['c', 'b', 'a'] as $sModuleId){ $this->AddModule($aUnresolvedDependencyModules, $sModuleId, []); } - ModuleDiscovery::SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); + iTopCoreModuleDependencySort::GetInstance()->SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); $this->assertEquals(['a', 'b', 'c'], array_keys($aUnresolvedDependencyModules)); } @@ -268,7 +269,7 @@ MSG; $this->AddModule($aUnresolvedDependencyModules, 'itop-config-mgmt/123', ['itop-structure/2.7.1']); $this->AddModule($aUnresolvedDependencyModules, 'itop-structure/2.7.1', []); - ModuleDiscovery::SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); + iTopCoreModuleDependencySort::GetInstance()->SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); $this->assertEquals( [ 'itop-structure/2.7.1', @@ -284,7 +285,7 @@ MSG; $this->AddModule($aUnresolvedDependencyModules, 'itop-tickets/2.0.0', ['itop-structure/2.7.1']); $this->AddModule($aUnresolvedDependencyModules, 'itop-config-mgmt/123', ['itop-structure/2.7.1']); - ModuleDiscovery::SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); + iTopCoreModuleDependencySort::GetInstance()->SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); $this->assertEquals( [ 'itop-config-mgmt/123', @@ -301,7 +302,7 @@ MSG; $this->AddModule($aUnresolvedDependencyModules, 'moduleB/1', ['moduleA/1']); $this->AddModule($aUnresolvedDependencyModules, 'moduleC/1', []); - ModuleDiscovery::SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); + iTopCoreModuleDependencySort::GetInstance()->SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); $this->assertEquals( [ 'moduleA/1', @@ -325,7 +326,7 @@ MSG; $this->AddModule($aUnresolvedDependencyModules, $sModuleId, $aModuleData['dependencies']); } - ModuleDiscovery::SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); + iTopCoreModuleDependencySort::GetInstance()->SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); $aExpected = json_decode(file_get_contents(__DIR__ . '/ressources/expected_ordered_module_ids.json'), true); $this->assertEquals( diff --git a/tests/php-unit-tests/unitary-tests/setup/ModuleTest.php b/tests/php-unit-tests/unitary-tests/setup/iTopCoreModuleTest.php similarity index 85% rename from tests/php-unit-tests/unitary-tests/setup/ModuleTest.php rename to tests/php-unit-tests/unitary-tests/setup/iTopCoreModuleTest.php index a1fc4393a..2a6410ec2 100644 --- a/tests/php-unit-tests/unitary-tests/setup/ModuleTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/iTopCoreModuleTest.php @@ -3,17 +3,18 @@ namespace Combodo\iTop\Test\UnitTest\Setup; use Combodo\iTop\Test\UnitTest\ItopTestCase; +use iTopCoreModule; -class ModuleTest extends ItopTestCase +class iTopCoreModuleTest extends ItopTestCase { public function setUp(): void { parent::setUp(); - $this->RequireOnceItopFile('setup/modulediscovery.class.inc.php'); + $this->RequireOnceItopFile('setup/module/iTopCoreModule.class.inc.php'); } public function testModuleInit() { - $oModule = new \iTopCoreModule("itop-config-mgmt/2.4.0"); + $oModule = new iTopCoreModule("itop-config-mgmt/2.4.0"); $this->assertEquals("itop-config-mgmt", $oModule->GetModuleName()); $this->assertEquals("2.4.0", $oModule->GetVersion()); $this->assertEquals("itop-config-mgmt/2.4.0", $oModule->GetModuleId()); @@ -21,7 +22,7 @@ class ModuleTest extends ItopTestCase public function testModuleInit_NoVersion() { - $oModule = new \iTopCoreModule("itop-config-mgmt"); + $oModule = new iTopCoreModule("itop-config-mgmt"); $this->assertEquals("itop-config-mgmt", $oModule->GetModuleName()); $this->assertEquals("1.0.0", $oModule->GetVersion()); $this->assertEquals("itop-config-mgmt", $oModule->GetModuleId()); @@ -29,7 +30,7 @@ class ModuleTest extends ItopTestCase public function testIsResolved_Unresolved() { - $oModule = new \iTopCoreModule("itop-bridge-cmdb-ticket"); + $oModule = new iTopCoreModule("itop-bridge-cmdb-ticket"); $oModule->SetDependencies(['itop-config-mgmt/2.7.1', 'itop-tickets/2.7.0']); $this->assertEquals(['itop-config-mgmt', 'itop-tickets'], $oModule->GetUnresolvedDependencyModuleNames()); @@ -40,7 +41,7 @@ class ModuleTest extends ItopTestCase public function testSetDependencies() { - $oModule = new \iTopCoreModule("itop-bridge-datacenter-mgmt-services"); + $oModule = new iTopCoreModule("itop-bridge-datacenter-mgmt-services"); $oModule->SetDependencies([ 'itop-config-mgmt/2.7.1', 'itop-service-mgmt/2.7.1 || itop-service-mgmt-provider/2.7.1', @@ -58,7 +59,7 @@ class ModuleTest extends ItopTestCase public function testIsResolved_PartialResolution() { - $oModule = new \iTopCoreModule("itop-bridge-cmdb-ticket"); + $oModule = new iTopCoreModule("itop-bridge-cmdb-ticket"); $oModule->SetDependencies(['itop-config-mgmt/2.7.1', 'itop-tickets/2.7.0']); $this->assertEquals(['itop-config-mgmt', 'itop-tickets'], $oModule->GetUnresolvedDependencyModuleNames()); @@ -69,7 +70,7 @@ class ModuleTest extends ItopTestCase public function testIsResolved_OK() { - $oModule = new \iTopCoreModule("itop-bridge-cmdb-ticket"); + $oModule = new iTopCoreModule("itop-bridge-cmdb-ticket"); $oModule->SetDependencies(['itop-config-mgmt/2.7.1', 'itop-tickets/2.7.0']); $this->assertEquals(['itop-config-mgmt', 'itop-tickets'], $oModule->GetUnresolvedDependencyModuleNames());