diff --git a/datamodels/2.x/itop-endusers-devices/datamodel.itop-enduser-devices.xml b/datamodels/2.x/itop-endusers-devices/datamodel.itop-endusers-devices.xml old mode 100644 new mode 100755 similarity index 100% rename from datamodels/2.x/itop-endusers-devices/datamodel.itop-enduser-devices.xml rename to datamodels/2.x/itop-endusers-devices/datamodel.itop-endusers-devices.xml diff --git a/datamodels/2.x/itop-hub-connector/hubruntimeenvironment.class.inc.php b/datamodels/2.x/itop-hub-connector/hubruntimeenvironment.class.inc.php index 37feb5457..c5c722cf7 100644 --- a/datamodels/2.x/itop-hub-connector/hubruntimeenvironment.class.inc.php +++ b/datamodels/2.x/itop-hub-connector/hubruntimeenvironment.class.inc.php @@ -1,6 +1,7 @@ sTargetEnv) - { - if (is_dir(APPROOT.'/env-'.$this->sTargetEnv)) - { - SetupUtils::rrmdir(APPROOT.'/env-'.$this->sTargetEnv); - } - if (is_dir(APPROOT.'/data/'.$this->sTargetEnv.'-modules')) - { - SetupUtils::rrmdir(APPROOT.'/data/'.$this->sTargetEnv.'-modules'); - } + + if ($sEnvironment != $this->sTargetEnv) { + if (is_dir(APPROOT.'/env-'.$this->sTargetEnv)) { + SetupUtils::rrmdir(APPROOT.'/env-'.$this->sTargetEnv); + } + if (is_dir(APPROOT.'/data/'.$this->sTargetEnv.'-modules')) { + SetupUtils::rrmdir(APPROOT.'/data/'.$this->sTargetEnv.'-modules'); + } SetupUtils::copydir(APPROOT.'/data/'.$sEnvironment.'-modules', APPROOT.'/data/'.$this->sTargetEnv.'-modules'); } } - + /** * Update the includes for the target environment * @param Config $oConfig @@ -32,7 +30,7 @@ class HubRunTimeEnvironment extends RunTimeEnvironment { $oConfig->UpdateIncludes('env-'.$this->sTargetEnv); // TargetEnv != FinalEnv } - + /** * Move an extension (path to folder of this extension) to the target environment * @param string $sExtensionDirectory The folder of the extension @@ -40,21 +38,23 @@ class HubRunTimeEnvironment extends RunTimeEnvironment */ public function MoveExtension($sExtensionDirectory) { - if (!is_dir(APPROOT.'/data/'.$this->sTargetEnv.'-modules')) - { - if (!mkdir(APPROOT.'/data/'.$this->sTargetEnv.'-modules')) throw new Exception("ERROR: failed to create directory:'".(APPROOT.'/data/'.$this->sTargetEnv.'-modules')."'"); + if (!is_dir(APPROOT.'/data/'.$this->sTargetEnv.'-modules')) { + if (!mkdir(APPROOT.'/data/'.$this->sTargetEnv.'-modules')) { + throw new Exception("ERROR: failed to create directory:'".(APPROOT.'/data/'.$this->sTargetEnv.'-modules')."'"); + } } $sDestinationPath = APPROOT.'/data/'.$this->sTargetEnv.'-modules/'; - + // Make sure that the destination directory of the extension does not already exist - if (is_dir($sDestinationPath.basename($sExtensionDirectory))) - { - // Cleanup before moving... - SetupUtils::rrmdir($sDestinationPath.basename($sExtensionDirectory)); + if (is_dir($sDestinationPath.basename($sExtensionDirectory))) { + // Cleanup before moving... + SetupUtils::rrmdir($sDestinationPath.basename($sExtensionDirectory)); + } + if (!rename($sExtensionDirectory, $sDestinationPath.basename($sExtensionDirectory))) { + throw new Exception("ERROR: failed move directory:'$sExtensionDirectory' to '".$sDestinationPath.basename($sExtensionDirectory)."'"); } - if (!rename($sExtensionDirectory, $sDestinationPath.basename($sExtensionDirectory))) throw new Exception("ERROR: failed move directory:'$sExtensionDirectory' to '".$sDestinationPath.basename($sExtensionDirectory)."'"); } - + /** * Move the selected extensions located in the given directory in data/-modules * @param string $sDownloadedExtensionsDir The directory to scan @@ -63,10 +63,8 @@ class HubRunTimeEnvironment extends RunTimeEnvironment */ public function MoveSelectedExtensions($sDownloadedExtensionsDir, $aSelectedExtensionDirs) { - foreach(glob($sDownloadedExtensionsDir.'*', GLOB_ONLYDIR) as $sExtensionDir) - { - if (in_array(basename($sExtensionDir), $aSelectedExtensionDirs)) - { + foreach (glob($sDownloadedExtensionsDir.'*', GLOB_ONLYDIR) as $sExtensionDir) { + if (in_array(basename($sExtensionDir), $aSelectedExtensionDirs)) { $this->MoveExtension($sExtensionDir); } } diff --git a/datamodels/2.x/itop-oauth-client/module.itop-oauth-client.php b/datamodels/2.x/itop-oauth-client/module.itop-oauth-client.php index d1a1631d7..0cd3a1c4a 100644 --- a/datamodels/2.x/itop-oauth-client/module.itop-oauth-client.php +++ b/datamodels/2.x/itop-oauth-client/module.itop-oauth-client.php @@ -17,6 +17,7 @@ SetupWebPage::AddModule( // 'dependencies' => [ 'itop-welcome-itil/3.1.0,', + 'itop-profiles-itil/3.1.0', //SuperUser id 117 ], 'mandatory' => false, 'visible' => true, diff --git a/datamodels/2.x/itop-portal-base/module.itop-portal-base.php b/datamodels/2.x/itop-portal-base/module.itop-portal-base.php index 3cb0b401f..296bad6c9 100644 --- a/datamodels/2.x/itop-portal-base/module.itop-portal-base.php +++ b/datamodels/2.x/itop-portal-base/module.itop-portal-base.php @@ -28,6 +28,7 @@ SetupWebPage::AddModule( 'category' => 'Portal', // Setup 'dependencies' => [ + 'itop-attachments/3.2.1', //CMDBChangeOpAttachmentRemoved ], 'mandatory' => true, 'visible' => false, diff --git a/datamodels/2.x/itop-tickets/module.itop-tickets.php b/datamodels/2.x/itop-tickets/module.itop-tickets.php index 96b407a34..228aeef78 100755 --- a/datamodels/2.x/itop-tickets/module.itop-tickets.php +++ b/datamodels/2.x/itop-tickets/module.itop-tickets.php @@ -13,6 +13,7 @@ SetupWebPage::AddModule( // 'dependencies' => [ 'itop-structure/2.7.1', + 'itop-portal/3.0.0', // module_design_itop_design->module_designs->itop-portal ], 'mandatory' => false, 'visible' => true, diff --git a/lib/autoload.php b/lib/autoload.php index 9ee03077e..db10dc867 100644 --- a/lib/autoload.php +++ b/lib/autoload.php @@ -14,7 +14,10 @@ if (PHP_VERSION_ID < 50600) { echo $err; } } - throw new RuntimeException($err); + trigger_error( + $err, + E_USER_ERROR + ); } require_once __DIR__ . '/composer/autoload_real.php'; diff --git a/lib/composer/autoload_psr4.php b/lib/composer/autoload_psr4.php index 0334bd4ce..602597214 100644 --- a/lib/composer/autoload_psr4.php +++ b/lib/composer/autoload_psr4.php @@ -61,7 +61,7 @@ return array( 'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'), 'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'), 'Pelago\\Emogrifier\\' => array($vendorDir . '/pelago/emogrifier/src'), - 'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-google/src', $vendorDir . '/league/oauth2-client/src'), + 'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-client/src', $vendorDir . '/league/oauth2-google/src'), 'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'), 'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'), 'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'), diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index f237f3d1e..7c85ebdb5 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -340,8 +340,8 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f ), 'League\\OAuth2\\Client\\' => array ( - 0 => __DIR__ . '/..' . '/league/oauth2-google/src', - 1 => __DIR__ . '/..' . '/league/oauth2-client/src', + 0 => __DIR__ . '/..' . '/league/oauth2-client/src', + 1 => __DIR__ . '/..' . '/league/oauth2-google/src', ), 'GuzzleHttp\\Psr7\\' => array ( diff --git a/lib/composer/platform_check.php b/lib/composer/platform_check.php index 72145773d..dee74e173 100644 --- a/lib/composer/platform_check.php +++ b/lib/composer/platform_check.php @@ -36,7 +36,8 @@ if ($issues) { echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; } } - throw new \RuntimeException( - 'Composer detected issues in your platform: ' . implode(' ', $issues) + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR ); } diff --git a/setup/moduledependency/dependencyexpression.class.inc.php b/setup/moduledependency/dependencyexpression.class.inc.php new file mode 100644 index 000000000..07c5b6048 --- /dev/null +++ b/setup/moduledependency/dependencyexpression.class.inc.php @@ -0,0 +1,146 @@ +456) + */ +class DependencyExpression +{ + private static PhpExpressionEvaluator $oPhpExpressionEvaluator; + + private string $sDependencyExpression; + private bool $bValid = true; + private bool $bResolved = false; + + /** + * @var array $aRemainingModuleNamesToResolve + */ + private array $aRemainingModuleNamesToResolve; + + /** + * @var array $aParamsPerModuleId + */ + private array $aParamsPerModuleId; + + public function __construct(string $sDependencyExpression) + { + $this->sDependencyExpression = $sDependencyExpression; + $this->aParamsPerModuleId = []; + $this->aRemainingModuleNamesToResolve = []; + + if (preg_match_all('/([^\(\)&| ]+)/', $sDependencyExpression, $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 = []; + if (preg_match('|^([^/]+)/(?=?)([^><=]+)$|', $sModuleId, $aModuleMatches)) { + $sModuleName = $aModuleMatches[1]; + $this->aRemainingModuleNamesToResolve[$sModuleName] = true; + $sOperator = $aModuleMatches[2]; + if ($sOperator == '') { + $sOperator = '>='; + } + $sExpectedVersion = $aModuleMatches[3]; + $this->aParamsPerModuleId[$sModuleId] = [$sModuleName, $sOperator, $sExpectedVersion]; + } + } + } + } + } else { + $this->bValid = false; + } + } + + private static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator + { + if (!isset(static::$oPhpExpressionEvaluator)) { + static::$oPhpExpressionEvaluator = new PhpExpressionEvaluator([], RunTimeEnvironment::STATIC_CALL_AUTOSELECT_WHITELIST); + } + + return static::$oPhpExpressionEvaluator; + } + + /** + * Return module names potentially required by current dependency + * @return array + */ + public function GetRemainingModuleNamesToResolve(): array + { + return array_keys($this->aRemainingModuleNamesToResolve); + } + + public function IsResolved(): bool + { + return $this->bResolved; + } + + /** + * 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 void + */ + public function UpdateModuleResolutionState(array $aModuleVersions, array $aSelectedModules): void + { + if (!$this->bValid) { + return; + } + + $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->aRemainingModuleNamesToResolve)) { + unset($this->aRemainingModuleNamesToResolve[$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->aRemainingModuleNamesToResolve as $sModuleName => $c) { + if (array_key_exists($sModuleName, $aSelectedModules)) { + // This module is actually a prerequisite + if (!array_key_exists($sModuleName, $aModuleVersions)) { + return; + } + } + } + + $bResult = false; + $sBooleanExpr = str_replace(array_keys($aReplacements), array_values($aReplacements), $this->sDependencyExpression); + try { + $bResult = self::GetPhpExpressionEvaluator()->ParseAndEvaluateBooleanExpression($sBooleanExpr); + } catch (ModuleFileReaderException $e) { + //logged already + echo "Failed to parse the boolean Expression = '$sBooleanExpr'
"; + } + + $this->bResolved = $bResult; + } + + public function IsValid(): bool + { + return $this->bValid; + } +} diff --git a/setup/moduledependency/module.class.inc.php b/setup/moduledependency/module.class.inc.php new file mode 100644 index 000000000..76273fd08 --- /dev/null +++ b/setup/moduledependency/module.class.inc.php @@ -0,0 +1,129 @@ + $aInitialDependencyExpressions + */ + private array $aInitialDependencyExpressions; + + /** + * @var array $aRemainingDependenciesToResolve + */ + public array $aRemainingDependenciesToResolve; + + public function __construct(string $sModuleId) + { + $this->sModuleId = $sModuleId; + list($this->sModuleName, $this->sVersion) = ModuleDiscovery::GetModuleName($sModuleId); + } + + public function IsDependencyExpressionResolved(string $sDependencyExpression): bool + { + return ! array_key_exists($sDependencyExpression, $this->aRemainingDependenciesToResolve); + } + + public function GetDependencyResolutionFeedback(): array + { + $aDepsWithIcons = []; + + foreach ($this->aInitialDependencyExpressions as $sDependencyExpression) { + if (! $this->IsDependencyExpressionResolved($sDependencyExpression)) { + $aDepsWithIcons[] = '❌ '.$sDependencyExpression; + } + } + return $aDepsWithIcons; + } + + /** + * @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 $aAllDependencyExpressions: list of dependencies (string) + * + * @return void + */ + public function SetDependencies(array $aAllDependencyExpressions): void + { + $this->aInitialDependencyExpressions = $aAllDependencyExpressions; + $this->aRemainingDependenciesToResolve = []; + + foreach ($aAllDependencyExpressions as $sDependencyExpression) { + $this->aRemainingDependenciesToResolve[$sDependencyExpression] = new DependencyExpression($sDependencyExpression); + } + } + + public function IsResolved(): bool + { + return (0 === count($this->aRemainingDependenciesToResolve)); + } + + /** + * 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 void + */ + public function UpdateModuleResolutionState(array $aModuleVersions, array $aSelectedModules): void + { + $aNextDependencies = []; + + foreach ($this->aRemainingDependenciesToResolve as $sDependencyExpression => $oModuleDependency) { + /** @var DependencyExpression $oModuleDependency*/ + $oModuleDependency->UpdateModuleResolutionState($aModuleVersions, $aSelectedModules); + if (!$oModuleDependency->IsResolved()) { + $aNextDependencies[$sDependencyExpression] = $oModuleDependency; + } + } + + $this->aRemainingDependenciesToResolve = $aNextDependencies; + } + + /** + * @return array: list of unique module names + */ + public function GetUnresolvedDependencyModuleNames(): array + { + $aRes = []; + foreach ($this->aRemainingDependenciesToResolve as $sDependencyExpression => $oModuleDependency) { + /** @var DependencyExpression $oModuleDependency */ + $aRes = array_merge($aRes, $oModuleDependency->GetRemainingModuleNamesToResolve()); + } + + return array_unique($aRes); + } +} diff --git a/setup/moduledependency/moduledependencysort.class.inc.php b/setup/moduledependency/moduledependencysort.class.inc.php new file mode 100644 index 000000000..c8484507d --- /dev/null +++ b/setup/moduledependency/moduledependencysort.class.inc.php @@ -0,0 +1,198 @@ + $aModuleInfo + * @param bool $bAbortOnMissingDependency ... + * @return array + * @throws \MissingDependencyException + */ + public function GetModulesOrderedForInstallation($aModules, $bAbortOnMissingDependency = false) + { + // Filter modules to compute + $aUnresolvedDependencyModules = []; + $aModuleNames = []; + foreach ($aModules as $sModuleId => $aModule) { + $oModule = new Module($sModuleId); + $sModuleName = $oModule->GetModuleName(); + $oModule->SetDependencies($aModule['dependencies']); + $aUnresolvedDependencyModules[$sModuleId] = $oModule; + $aModuleNames[$sModuleName] = true; + } + + // Make sure order is deterministic (alphabtical order) + ksort($aUnresolvedDependencyModules); + + //Attempt to resolve module dependencies + $aOrderedModules = []; + $aModuleVersions = []; + $iPreviousUnresolvedCount = -1; + //loop until no dependency is resolved + while ($iPreviousUnresolvedCount !== count($aUnresolvedDependencyModules)) { + $iPreviousUnresolvedCount = count($aUnresolvedDependencyModules); + if ($iPreviousUnresolvedCount === 0) { + break; + } + + foreach ($aUnresolvedDependencyModules as $sModuleId => $oModule) { + /** @var Module $oModule */ + $oModule->UpdateModuleResolutionState($aModuleVersions, $aModuleNames); + if ($oModule->IsResolved()) { + $aOrderedModules[] = $sModuleId; + $aModuleVersions[$oModule->GetModuleName()] = $oModule->GetVersion(); + unset($aUnresolvedDependencyModules[$sModuleId]); + } + } + } + + // Report unresolved dependencies + if ($bAbortOnMissingDependency && count($aUnresolvedDependencyModules) > 0) { + $this->SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); + + $aUnresolvedModulesInfo = []; + $aModuleDeps = []; + foreach ($aUnresolvedDependencyModules as $sModuleId => $oModule) { + $aModule = $aModules[$sModuleId]; + $aDepsWithIcons = $oModule->GetDependencyResolutionFeedback(); + + $aModuleDeps[] = "{$aModule['label']} (id: $sModuleId) depends on: ".implode(' + ', $aDepsWithIcons); + $aUnresolvedModulesInfo[$sModuleId] = ['module' => $aModule, 'dependencies' => $aDepsWithIcons]; + } + $sMessage = "The following modules have unmet dependencies:\n".implode(",\n", $aModuleDeps); + $oException = new MissingDependencyException($sMessage); + $oException->aModulesInfo = $aUnresolvedModulesInfo; + throw $oException; + } + + // Return the ordered list, so that the dependencies are met... + $aResult = []; + foreach ($aOrderedModules as $sId) { + $aResult[$sId] = $aModules[$sId]; + } + return $aResult; + } + + /** + * 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 + */ + protected function SortModulesByCountOfDepencenciesDescending(array &$aUnresolvedDependencyModules): void + { + $aCountDepsByModuleId = []; + $aDependsOnModuleName = []; + + foreach ($aUnresolvedDependencyModules as $sModuleId => $oModule) { + /** @var Module $oModule */ + $aDependsOnModuleName[$oModule->GetModuleName()] = []; + } + + foreach ($aUnresolvedDependencyModules as $sModuleId => $oModule) { + $iInDegreeCounter = 0; + /** @var Module $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; + } +} diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php old mode 100644 new mode 100755 index 26dff485f..2d2e8bb72 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -21,10 +21,14 @@ */ use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator; +use Combodo\iTop\Setup\ModuleDependency\Module; use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader; use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException; require_once(APPROOT.'setup/modulediscovery/ModuleFileReader.php'); +require_once(__DIR__.'/moduledependency/moduledependencysort.class.inc.php'); + +use Combodo\iTop\Setup\ModuleDependency\ModuleDependencySort; class MissingDependencyException extends CoreException { @@ -211,76 +215,23 @@ class ModuleDiscovery * @param array $aModulesToLoad List of modules to search for, defaults to all if omitted * @return array * @throws \MissingDependencyException - */ + */ public static function OrderModulesByDependencies($aModules, $bAbortOnMissingDependency = false, $aModulesToLoad = null) { - // Order the modules to take into account their inter-dependencies - $aDependencies = []; - $aSelectedModules = []; - foreach ($aModules as $sId => $aModule) { - list($sModuleName, ) = self::GetModuleName($sId); - if (is_null($aModulesToLoad) || in_array($sModuleName, $aModulesToLoad)) { - $aDependencies[$sId] = $aModule['dependencies']; - $aSelectedModules[$sModuleName] = true; - } - } - ksort($aDependencies); - $aOrderedModules = []; - $iLoopCount = 0; - while (($iLoopCount < count($aModules)) && (count($aDependencies) > 0)) { - foreach ($aDependencies as $sId => $aRemainingDeps) { - $bDependenciesSolved = true; - foreach ($aRemainingDeps as $sDepId) { - if (!self::DependencyIsResolved($sDepId, $aOrderedModules, $aSelectedModules)) { - $bDependenciesSolved = false; - } - } - if ($bDependenciesSolved) { - $aOrderedModules[] = $sId; - unset($aDependencies[$sId]); + if (is_null($aModulesToLoad)) { + $aFilteredModules = $aModules; + } else { + $aFilteredModules = []; + foreach ($aModules as $sModuleId => $aModule) { + $oModule = new Module($sModuleId); + $sModuleName = $oModule->GetModuleName(); + if (in_array($sModuleName, $aModulesToLoad)) { + $aFilteredModules[$sModuleId] = $aModule; } } - $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] = ['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 = []; - foreach ($aOrderedModules as $sId) { - $aResult[$sId] = $aModules[$sId]; - } - return $aResult; - } - /** - * Remove the duplicate modules (i.e. modules with the same name but with a different version) from the supplied list of modules - * @param array $aModules - * @return array The ordered modules as a duplicate-free list of modules - */ - public static function RemoveDuplicateModules($aModules) - { - // No longer needed, kept only for compatibility - // The de-duplication is now done directly by the AddModule method - return $aModules; + return ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aFilteredModules, $bAbortOnMissingDependency); } private static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator @@ -292,73 +243,6 @@ class ModuleDiscovery return static::$oPhpExpressionEvaluator; } - protected static function DependencyIsResolved($sDepString, $aOrderedModules, $aSelectedModules) - { - $bResult = false; - $aModuleVersions = []; - // Separate the module names from their version for an easier comparison later - foreach ($aOrderedModules as $sModuleId) { - list($sModuleName, $sVersion) = self::GetModuleName($sModuleId); - $aModuleVersions[$sModuleName] = $sVersion; - } - if (preg_match_all('/([^\(\)&| ]+)/', $sDepString, $aMatches)) { - $aReplacements = []; - $aPotentialPrerequisites = []; - foreach ($aMatches as $aMatch) { - foreach ($aMatch as $sModuleId) { - // $sModuleId in the dependency string is made of a / - // where the operator is < <= = > >= (by default >=) - $aModuleMatches = []; - if (preg_match('|^([^/]+)/(?=?)([^><=]+)$|', $sModuleId, $aModuleMatches)) { - $sModuleName = $aModuleMatches[1]; - $aPotentialPrerequisites[$sModuleName] = true; - $sOperator = $aModuleMatches[2]; - if ($sOperator == '') { - $sOperator = '>='; - } - $sExpectedVersion = $aModuleMatches[3]; - if (array_key_exists($sModuleName, $aModuleVersions)) { - // module is present, check the version - $sCurrentVersion = $aModuleVersions[$sModuleName]; - if (version_compare($sCurrentVersion, $sExpectedVersion, $sOperator)) { - $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 - } - } - } - } - $bMissingPrerequisite = false; - foreach (array_keys($aPotentialPrerequisites) as $sModuleName) { - if (array_key_exists($sModuleName, $aSelectedModules)) { - // This module is actually a prerequisite - if (!array_key_exists($sModuleName, $aModuleVersions)) { - $bMissingPrerequisite = true; - } - } - } - if ($bMissingPrerequisite) { - $bResult = false; - } else { - $sBooleanExpr = str_replace(array_keys($aReplacements), array_values($aReplacements), $sDepString); - try { - $bResult = self::GetPhpExpressionEvaluator()->ParseAndEvaluateBooleanExpression($sBooleanExpr); - } catch (ModuleFileReaderException $e) { - //logged already - echo "Failed to parse the boolean Expression = '$sBooleanExpr'
"; - } - } - } - return $bResult; - } - /** * Search (on the disk) for all defined iTop modules, load them and returns the list (as an array) * of the possible iTop modules to install diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index e8052c434..9b86e4720 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -1,4 +1,5 @@ - /** * Manage a runtime environment * @@ -33,18 +33,16 @@ require_once APPROOT.'setup/modelfactory.class.inc.php'; require_once APPROOT.'setup/compiler.class.inc.php'; require_once APPROOT.'setup/extensionsmap.class.inc.php'; -define ('MODULE_ACTION_OPTIONAL', 1); -define ('MODULE_ACTION_MANDATORY', 2); -define ('MODULE_ACTION_IMPOSSIBLE', 3); -define ('ROOT_MODULE', '_Root_'); // Convention to store IN MEMORY the name/version of the root module i.e. application -define ('DATAMODEL_MODULE', 'datamodel'); // Convention to store the version of the datamodel - - +define('MODULE_ACTION_OPTIONAL', 1); +define('MODULE_ACTION_MANDATORY', 2); +define('MODULE_ACTION_IMPOSSIBLE', 3); +define('ROOT_MODULE', '_Root_'); // Convention to store IN MEMORY the name/version of the root module i.e. application +define('DATAMODEL_MODULE', 'datamodel'); // Convention to store the version of the datamodel class RunTimeEnvironment { - const STATIC_CALL_AUTOSELECT_WHITELIST=[ - "SetupInfo::ModuleIsSelected" + public const STATIC_CALL_AUTOSELECT_WHITELIST = [ + "SetupInfo::ModuleIsSelected", ]; /** @@ -74,13 +72,10 @@ class RunTimeEnvironment public function __construct($sEnvironment = 'production', $bAutoCommit = true) { $this->sFinalEnv = $sEnvironment; - if ($bAutoCommit) - { + if ($bAutoCommit) { // Build directly onto the requested environment $this->sTargetEnv = $sEnvironment; - } - else - { + } else { // Build into a temporary target $this->sTargetEnv = $sEnvironment.'-build'; } @@ -121,25 +116,20 @@ class RunTimeEnvironment require_once APPROOT.'/setup/moduleinstallation.class.inc.php'; $sConfigFile = $oConfig->GetLoadedFile(); - if (strlen($sConfigFile) > 0) - { + if (strlen($sConfigFile) > 0) { $this->log_info("MetaModel::Startup from $sConfigFile (ModelOnly = $bModelOnly)"); - } - else - { + } else { $this->log_info("MetaModel::Startup (ModelOnly = $bModelOnly)"); } - if (!$bUseCache) - { + if (!$bUseCache) { // Reset the cache for the first use ! MetaModel::ResetAllCaches($this->sTargetEnv); } MetaModel::Startup($oConfig, $bModelOnly, $bUseCache, false /* $bTraceSourceFiles */, $this->sTargetEnv); - if ($this->oExtensionsMap === null) - { + if ($this->oExtensionsMap === null) { $this->oExtensionsMap = new iTopExtensionsMap($this->sTargetEnv); } } @@ -178,26 +168,23 @@ class RunTimeEnvironment */ public function AnalyzeInstallation($oConfig, $modulesPath, $bAbortOnMissingDependency = false, $aModulesToLoad = null) { - $aRes = array( - ROOT_MODULE => array( + $aRes = [ + ROOT_MODULE => [ 'version_db' => '', 'name_db' => '', 'version_code' => ITOP_VERSION_FULL, 'name_code' => ITOP_APPLICATION, - ) - ); + ], + ]; - $aDirs = is_array($modulesPath) ? $modulesPath : array($modulesPath); + $aDirs = is_array($modulesPath) ? $modulesPath : [$modulesPath]; $aModules = ModuleDiscovery::GetAvailableModules($aDirs, $bAbortOnMissingDependency, $aModulesToLoad); - foreach($aModules as $sModuleId => $aModuleInfo) - { + foreach ($aModules as $sModuleId => $aModuleInfo) { list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId); - if ($sModuleName == '') - { + if ($sModuleName == '') { throw new Exception("Missing name for the module: '$sModuleId'"); } - if ($sModuleVersion == '') - { + if ($sModuleVersion == '') { // The version must not be empty (it will be used as a criteria to determine wether a module has been installed or not) //throw new Exception("Missing version for the module: '$sModuleId'"); $sModuleVersion = '1.0.0'; @@ -207,95 +194,76 @@ class RunTimeEnvironment $aModuleInfo['version_db'] = ''; $aModuleInfo['version_code'] = $sModuleVersion; - if (!in_array($sModuleAppVersion, array('1.0.0', '1.0.1', '1.0.2'))) - { + if (!in_array($sModuleAppVersion, ['1.0.0', '1.0.1', '1.0.2'])) { // This module is NOT compatible with the current version - $aModuleInfo['install'] = array( + $aModuleInfo['install'] = [ 'flag' => MODULE_ACTION_IMPOSSIBLE, - 'message' => 'the module is not compatible with the current version of the application' - ); - } - elseif ($aModuleInfo['mandatory']) - { - $aModuleInfo['install'] = array( + 'message' => 'the module is not compatible with the current version of the application', + ]; + } elseif ($aModuleInfo['mandatory']) { + $aModuleInfo['install'] = [ 'flag' => MODULE_ACTION_MANDATORY, - 'message' => 'the module is part of the application' - ); - } - else - { - $aModuleInfo['install'] = array( + 'message' => 'the module is part of the application', + ]; + } else { + $aModuleInfo['install'] = [ 'flag' => MODULE_ACTION_OPTIONAL, - 'message' => '' - ); + 'message' => '', + ]; } $aRes[$sModuleName] = $aModuleInfo; } - try - { - $aSelectInstall = array(); + try { + $aSelectInstall = []; if (! is_null($oConfig)) { CMDBSource::InitFromConfig($oConfig); $aSelectInstall = CMDBSource::QueryToArray("SELECT * FROM ".$oConfig->Get('db_subname')."priv_module_install"); } - } - catch (MySQLException $e) - { + } catch (MySQLException $e) { // No database or erroneous information } // Build the list of installed module (get the latest installation) // - $aInstallByModule = array(); // array of => array ('installed' => timestamp, 'version' => ) + $aInstallByModule = []; // array of => array ('installed' => timestamp, 'version' => ) $iRootId = 0; - foreach ($aSelectInstall as $aInstall) - { - if (($aInstall['parent_id'] == 0) && ($aInstall['name'] != 'datamodel')) - { + foreach ($aSelectInstall as $aInstall) { + if (($aInstall['parent_id'] == 0) && ($aInstall['name'] != 'datamodel')) { // Root module, what is its ID ? $iId = (int) $aInstall['id']; - if ($iId > $iRootId) - { + if ($iId > $iRootId) { $iRootId = $iId; } } } - foreach ($aSelectInstall as $aInstall) - { + foreach ($aSelectInstall as $aInstall) { //$aInstall['comment']; // unsused $iInstalled = strtotime($aInstall['installed']); $sModuleName = $aInstall['name']; $sModuleVersion = $aInstall['version']; - if ($sModuleVersion == '') - { + if ($sModuleVersion == '') { // Though the version cannot be empty in iTop 2.0, it used to be possible // therefore we have to put something here or the module will not be considered // as being installed $sModuleVersion = '0.0.0'; } - if ($aInstall['parent_id'] == 0) - { + if ($aInstall['parent_id'] == 0) { $sModuleName = ROOT_MODULE; - } - else if($aInstall['parent_id'] != $iRootId) - { + } elseif ($aInstall['parent_id'] != $iRootId) { // Skip all modules belonging to previous installations continue; } - if (array_key_exists($sModuleName, $aInstallByModule)) - { - if ($iInstalled < $aInstallByModule[$sModuleName]['installed']) - { + if (array_key_exists($sModuleName, $aInstallByModule)) { + if ($iInstalled < $aInstallByModule[$sModuleName]['installed']) { continue; } } - if ($aInstall['parent_id'] == 0) - { + if ($aInstall['parent_id'] == 0) { $aRes[$sModuleName]['version_db'] = $sModuleVersion; $aRes[$sModuleName]['name_db'] = $aInstall['name']; } @@ -306,37 +274,33 @@ class RunTimeEnvironment // Adjust the list of proposed modules // - foreach ($aInstallByModule as $sModuleName => $aModuleDB) - { - if ($sModuleName == ROOT_MODULE) continue; // Skip the main module + foreach ($aInstallByModule as $sModuleName => $aModuleDB) { + if ($sModuleName == ROOT_MODULE) { + continue; + } // Skip the main module - if (!array_key_exists($sModuleName, $aRes)) - { + if (!array_key_exists($sModuleName, $aRes)) { // A module was installed, it is not proposed in the new build... skip continue; } $aRes[$sModuleName]['version_db'] = $aModuleDB['version']; - if ($aRes[$sModuleName]['install']['flag'] == MODULE_ACTION_MANDATORY) - { - $aRes[$sModuleName]['uninstall'] = array( + if ($aRes[$sModuleName]['install']['flag'] == MODULE_ACTION_MANDATORY) { + $aRes[$sModuleName]['uninstall'] = [ 'flag' => MODULE_ACTION_IMPOSSIBLE, - 'message' => 'the module is part of the application' - ); - } - else - { - $aRes[$sModuleName]['uninstall'] = array( + 'message' => 'the module is part of the application', + ]; + } else { + $aRes[$sModuleName]['uninstall'] = [ 'flag' => MODULE_ACTION_OPTIONAL, - 'message' => '' - ); + 'message' => '', + ]; } } return $aRes; } - /** * @param Config $oConfig * @@ -359,10 +323,10 @@ class RunTimeEnvironment * Return an array with extra directories to scan for extensions/modules to install * @return string[] */ - protected function GetExtraDirsToScan($aDirs = array()) + protected function GetExtraDirsToScan($aDirs = []) { // Do nothing, overload this method if needed - return array(); + return []; } /** @@ -381,25 +345,22 @@ class RunTimeEnvironment protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir) { $sSourceDirFull = APPROOT.$sSourceDir; - if (!is_dir($sSourceDirFull)) - { + if (!is_dir($sSourceDirFull)) { throw new Exception("The source directory '$sSourceDirFull' does not exist (or could not be read)"); } - $aDirsToCompile = array($sSourceDirFull); - if (is_dir(APPROOT.'extensions')) - { + $aDirsToCompile = [$sSourceDirFull]; + if (is_dir(APPROOT.'extensions')) { $aDirsToCompile[] = APPROOT.'extensions'; } $sExtraDir = utils::GetDataPath().$this->sTargetEnv.'-modules/'; - if (is_dir($sExtraDir)) - { + if (is_dir($sExtraDir)) { $aDirsToCompile[] = $sExtraDir; } $aExtraDirs = $this->GetExtraDirsToScan($aDirsToCompile); $aDirsToCompile = array_merge($aDirsToCompile, $aExtraDirs); - $aRet = array(); + $aRet = []; // Determine the installed modules and extensions // @@ -414,10 +375,8 @@ class RunTimeEnvironment // The actual choices will be recorded by RecordInstallation below $this->oExtensionsMap = new iTopExtensionsMap($this->sTargetEnv, true, $aExtraDirs); $this->oExtensionsMap->LoadChoicesFromDatabase($oSourceConfig); - foreach($this->oExtensionsMap->GetAllExtensions() as $oExtension) - { - if($this->IsExtensionSelected($oExtension)) - { + foreach ($this->oExtensionsMap->GetAllExtensions() as $oExtension) { + if ($this->IsExtensionSelected($oExtension)) { $this->oExtensionsMap->MarkAsChosen($oExtension->sCode); } } @@ -429,28 +388,23 @@ class RunTimeEnvironment $oFactory = new ModelFactory($aDirsToCompile); $sDeltaFile = APPROOT.'core/datamodel.core.xml'; - if (file_exists($sDeltaFile)) - { + if (file_exists($sDeltaFile)) { $oCoreModule = new MFCoreModule('core', 'Core Module', $sDeltaFile); $aRet[$oCoreModule->GetName()] = $oCoreModule; } $sDeltaFile = APPROOT.'application/datamodel.application.xml'; - if (file_exists($sDeltaFile)) - { + if (file_exists($sDeltaFile)) { $oApplicationModule = new MFCoreModule('application', 'Application Module', $sDeltaFile); $aRet[$oApplicationModule->GetName()] = $oApplicationModule; } $aModules = $oFactory->FindModules(); - foreach($aModules as $oModule) - { + foreach ($aModules as $oModule) { $sModule = $oModule->GetName(); $sModuleRootDir = $oModule->GetRootDir(); $bIsExtra = $this->oExtensionsMap->ModuleIsChosenAsPartOfAnExtension($sModule, iTopExtension::SOURCE_REMOTE); - if (array_key_exists($sModule, $aAvailableModules)) - { - if (($aAvailableModules[$sModule]['version_db'] != '') || $bIsExtra && !$oModule->IsAutoSelect()) //Extra modules are always unless they are 'AutoSelect' - { + if (array_key_exists($sModule, $aAvailableModules)) { + if (($aAvailableModules[$sModule]['version_db'] != '') || $bIsExtra && !$oModule->IsAutoSelect()) { //Extra modules are always unless they are 'AutoSelect' $aRet[$oModule->GetName()] = $oModule; } } @@ -459,33 +413,27 @@ class RunTimeEnvironment $oPhpExpressionEvaluator = new PhpExpressionEvaluator([], RunTimeEnvironment::STATIC_CALL_AUTOSELECT_WHITELIST); // Now process the 'AutoSelect' modules - do - { + do { // Loop while new modules are added... $bModuleAdded = false; - foreach($aModules as $oModule) - { - if (!array_key_exists($oModule->GetName(), $aRet) && $oModule->IsAutoSelect()) - { + foreach ($aModules as $oModule) { + if (!array_key_exists($oModule->GetName(), $aRet) && $oModule->IsAutoSelect()) { SetupInfo::SetSelectedModules($aRet); - try{ + try { $bSelected = $oPhpExpressionEvaluator->ParseAndEvaluateBooleanExpression($oModule->GetAutoSelect()); - if ($bSelected) - { + if ($bSelected) { $aRet[$oModule->GetName()] = $oModule; // store the Id of the selected module $bModuleAdded = true; } - } catch(ModuleFileReaderException $e){ + } catch (ModuleFileReaderException $e) { //do nothing. logged already } } } - } - while($bModuleAdded); + } while ($bModuleAdded); $sDeltaFile = utils::GetDataPath().$this->sTargetEnv.'.delta.xml'; - if (file_exists($sDeltaFile)) - { + if (file_exists($sDeltaFile)) { $oDelta = new MFDeltaModule($sDeltaFile); $aRet[$oDelta->GetName()] = $oDelta; } @@ -514,10 +462,8 @@ class RunTimeEnvironment // $oFactory = new ModelFactory($sSourceDirFull); $aModulesToCompile = $this->GetMFModulesToCompile($sSourceEnv, $sSourceDir); - foreach ($aModulesToCompile as $oModule) - { - if ($oModule instanceof MFDeltaModule) - { + foreach ($aModulesToCompile as $oModule) { + if ($oModule instanceof MFDeltaModule) { // Just before loading the delta, let's save an image of the datamodel // in case there is no delta the operation will be done after the end of the loop $oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$this->sTargetEnv.'.xml'); @@ -525,7 +471,6 @@ class RunTimeEnvironment $oFactory->LoadModule($oModule); } - if ($oModule instanceof MFDeltaModule) { // A delta was loaded, let's save a second copy of the datamodel $oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$this->sTargetEnv.'-with-delta.xml'); @@ -560,45 +505,32 @@ class RunTimeEnvironment */ public function CreateDatabaseStructure(Config $oConfig, $sMode) { - if (strlen($oConfig->Get('db_subname')) > 0) - { + if (strlen($oConfig->Get('db_subname')) > 0) { $this->log_info("Creating the structure in '".$oConfig->Get('db_name')."' (table names prefixed by '".$oConfig->Get('db_subname')."')."); - } - else - { + } else { $this->log_info("Creating the structure in '".$oConfig->Get('db_name')."'."); } //MetaModel::CheckDefinitions(); - if ($sMode == 'install') - { - if (!MetaModel::DBExists(/* bMustBeComplete */ false)) - { - MetaModel::DBCreate(array($this, 'LogQueryCallback')); + if ($sMode == 'install') { + if (!MetaModel::DBExists(/* bMustBeComplete */ false)) { + MetaModel::DBCreate([$this, 'LogQueryCallback']); $this->log_ok("Database structure successfully created."); - } - else - { - if (strlen($oConfig->Get('db_subname')) > 0) - { + } else { + if (strlen($oConfig->Get('db_subname')) > 0) { throw new Exception("Error: found iTop tables into the database '".$oConfig->Get('db_name')."' (prefix: '".$oConfig->Get('db_subname')."'). Please, try selecting another database instance or specify another prefix to prevent conflicting table names."); - } - else - { + } else { throw new Exception("Error: found iTop tables into the database '".$oConfig->Get('db_name')."'. Please, try selecting another database instance or specify a prefix to prevent conflicting table names."); } } - } - else - { - if (MetaModel::DBExists(/* bMustBeComplete */ false)) - { + } else { + if (MetaModel::DBExists(/* bMustBeComplete */ false)) { // Have it work fine even if the DB has been set in read-only mode for the users // (fix copied from RunTimeEnvironment::RecordInstallation) $iPrevAccessMode = $oConfig->Get('access_mode'); $oConfig->Set('access_mode', ACCESS_FULL); - MetaModel::DBCreate(array($this, 'LogQueryCallback')); + MetaModel::DBCreate([$this, 'LogQueryCallback']); $this->log_ok("Database structure successfully updated."); // Check (and update only if it seems needed) the hierarchical keys @@ -626,15 +558,10 @@ class RunTimeEnvironment // Restore the previous access mode $oConfig->Set('access_mode', $iPrevAccessMode); - } - else - { - if (strlen($oConfig->Get('db_subname')) > 0) - { + } else { + if (strlen($oConfig->Get('db_subname')) > 0) { throw new Exception("Error: No previous instance of iTop found into the database '".$oConfig->Get('db_name')."' (prefix: '".$oConfig->Get('db_subname')."'). Please, try selecting another database instance."); - } - else - { + } else { throw new Exception("Error: No previous instance of iTop found into the database '".$oConfig->Get('db_name')."'. Please, try selecting another database instance."); } } @@ -651,46 +578,36 @@ class RunTimeEnvironment // Constant classes (e.g. User profiles) // - foreach (MetaModel::GetClasses() as $sClass) - { - $aPredefinedObjects = call_user_func(array( + foreach (MetaModel::GetClasses() as $sClass) { + $aPredefinedObjects = call_user_func([ $sClass, - 'GetPredefinedObjects' - )); - if ($aPredefinedObjects != null) - { - $this->log_info("$sClass::GetPredefinedObjects() returned " . count($aPredefinedObjects) . " elements."); + 'GetPredefinedObjects', + ]); + if ($aPredefinedObjects != null) { + $this->log_info("$sClass::GetPredefinedObjects() returned ".count($aPredefinedObjects)." elements."); // Create/Delete/Update objects of this class, // according to the given constant values // - $aDBIds = array(); + $aDBIds = []; $oAll = new DBObjectSet(new DBObjectSearch($sClass)); - while ($oObj = $oAll->Fetch()) - { - if (array_key_exists($oObj->GetKey(), $aPredefinedObjects)) - { + while ($oObj = $oAll->Fetch()) { + if (array_key_exists($oObj->GetKey(), $aPredefinedObjects)) { $aObjValues = $aPredefinedObjects[$oObj->GetKey()]; - foreach ($aObjValues as $sAttCode => $value) - { + foreach ($aObjValues as $sAttCode => $value) { $oObj->Set($sAttCode, $value); } $oObj->DBUpdate(); $aDBIds[$oObj->GetKey()] = true; - } - else - { + } else { $oObj->DBDelete(); } } - foreach ($aPredefinedObjects as $iRefId => $aObjValues) - { - if (! array_key_exists($iRefId, $aDBIds)) - { + foreach ($aPredefinedObjects as $iRefId => $aObjValues) { + if (! array_key_exists($iRefId, $aDBIds)) { $oNewObj = MetaModel::NewObject($sClass); $oNewObj->SetKey($iRefId); - foreach ($aObjValues as $sAttCode => $value) - { + foreach ($aObjValues as $sAttCode => $value) { $oNewObj->Set($sAttCode, $value); } $oNewObj->DBInsert(); @@ -710,22 +627,20 @@ class RunTimeEnvironment MetaModel::GetConfig()->Set('access_mode', ACCESS_FULL); //$oConfig->Set('access_mode', ACCESS_FULL); - if (CMDBSource::DBName() == '') - { + if (CMDBSource::DBName() == '') { // In case this has not yet been done CMDBSource::InitFromConfig($oConfig); } - if ($sShortComment === null) - { + if ($sShortComment === null) { $sShortComment = 'Done by the setup program'; } $sMainComment = $sShortComment."\nBuilt on ".ITOP_BUILD_DATE; // Record datamodel version - $aData = array( + $aData = [ 'source_dir' => $oConfig->Get('source_dir'), - ); + ]; $iInstallationTime = time(); // Make sure that all modules record the same installation time $oInstallRec = new ModuleInstallation(); $oInstallRec->Set('name', DATAMODEL_MODULE); @@ -744,10 +659,9 @@ class RunTimeEnvironment $oInstallRec->Set('installed', $iInstallationTime); $iMainItopRecord = $oInstallRec->DBInsertNoReload(); - // Record installed modules and extensions // - $aAvailableExtensions = array(); + $aAvailableExtensions = []; $aAvailableModules = $this->AnalyzeInstallation($oConfig, $this->GetBuildDir()); foreach ($aSelectedModuleCodes as $sModuleId) { if (!array_key_exists($sModuleId, $aAvailableModules)) { @@ -757,7 +671,7 @@ class RunTimeEnvironment $sName = $sModuleId; $sVersion = $aModuleData['version_code']; $sUninstallable = $aModuleData['uninstallable'] ?? 'yes'; - $aComments = array(); + $aComments = []; $aComments[] = $sShortComment; if ($aModuleData['mandatory']) { $aComments[] = 'Mandatory'; @@ -788,25 +702,21 @@ class RunTimeEnvironment $oInstallRec->DBInsertNoReload(); } - if ($this->oExtensionsMap) - { + if ($this->oExtensionsMap) { // Mark as chosen the selected extensions code passed to us // Note: some other extensions may already be marked as chosen - foreach($this->oExtensionsMap->GetAllExtensions() as $oExtension) - { - if (in_array($oExtension->sCode, $aSelectedExtensionCodes)) - { + foreach ($this->oExtensionsMap->GetAllExtensions() as $oExtension) { + if (in_array($oExtension->sCode, $aSelectedExtensionCodes)) { $this->oExtensionsMap->MarkAsChosen($oExtension->sCode); } } - foreach($this->oExtensionsMap->GetChoices() as $oExtension) - { + foreach ($this->oExtensionsMap->GetChoices() as $oExtension) { $oInstallRec = new ExtensionInstallation(); $oInstallRec->Set('code', $oExtension->sCode); $oInstallRec->Set('label', $oExtension->sLabel); $oInstallRec->Set('version', $oExtension->sVersion); - $oInstallRec->Set('source', $oExtension->sSource); + $oInstallRec->Set('source', $oExtension->sSource); $oInstallRec->Set('uninstallable', $oExtension->CanBeUninstalled() ? 'yes' : 'no'); $oInstallRec->Set('installed', $iInstallationTime); $oInstallRec->DBInsertNoReload(); @@ -827,14 +737,11 @@ class RunTimeEnvironment */ public function GetApplicationVersion(Config $oConfig) { - try - { + try { CMDBSource::InitFromConfig($oConfig); $sSQLQuery = "SELECT * FROM ".$oConfig->Get('db_subname')."priv_module_install"; $aSelectInstall = CMDBSource::QueryToArray($sSQLQuery); - } - catch (MySQLException $e) - { + } catch (MySQLException $e) { // No database or erroneous information $this->log_error('Can not connect to the database: host: '.$oConfig->Get('db_host').', user:'.$oConfig->Get('db_user').', pwd:'.$oConfig->Get('db_pwd').', db name:'.$oConfig->Get('db_name')); $this->log_error('Exception '.$e->getMessage()); @@ -843,37 +750,29 @@ class RunTimeEnvironment $aResult = []; // Scan the list of installed modules to get the version of the 'ROOT' module which holds the main application version - foreach ($aSelectInstall as $aInstall) - { + foreach ($aSelectInstall as $aInstall) { $sModuleVersion = $aInstall['version']; - if ($sModuleVersion == '') - { + if ($sModuleVersion == '') { // Though the version cannot be empty in iTop 2.0, it used to be possible // therefore we have to put something here or the module will not be considered // as being installed $sModuleVersion = '0.0.0'; } - if ($aInstall['parent_id'] == 0) - { - if ($aInstall['name'] == DATAMODEL_MODULE) - { + if ($aInstall['parent_id'] == 0) { + if ($aInstall['name'] == DATAMODEL_MODULE) { $aResult['datamodel_version'] = $sModuleVersion; $aComments = json_decode($aInstall['comment'], true); - if (is_array($aComments)) - { + if (is_array($aComments)) { $aResult = array_merge($aResult, $aComments); } - } - else - { + } else { $aResult['product_name'] = $aInstall['name']; $aResult['product_version'] = $sModuleVersion; } } } - if (!array_key_exists('datamodel_version', $aResult)) - { + if (!array_key_exists('datamodel_version', $aResult)) { // Versions prior to 2.0 did not record the version of the datamodel // so assume that the datamodel version is equal to the application version $aResult['datamodel_version'] = $aResult['product_version']; @@ -884,10 +783,8 @@ class RunTimeEnvironment public static function MakeDirSafe($sDir) { - if (!is_dir($sDir)) - { - if (!@mkdir($sDir)) - { + if (!is_dir($sDir)) { + if (!@mkdir($sDir)) { throw new Exception("Failed to create directory '$sDir', please check that the web server process has enough rights to create the directory."); } @chmod($sDir, 0770); // RWX for owner and group, nothing for others @@ -926,8 +823,7 @@ class RunTimeEnvironment { $sSetupQueriesFilePath = SetupUtils::GetSetupQueriesFilePath(); $hSetupQueriesFile = @fopen($sSetupQueriesFilePath, 'a'); - if ($hSetupQueriesFile !== false) - { + if ($hSetupQueriesFile !== false) { fwrite($hSetupQueriesFile, "$sQuery\n"); fclose($hSetupQueriesFile); } @@ -936,10 +832,9 @@ class RunTimeEnvironment public function GetCurrentDataModelVersion() { $oSearch = DBObjectSearch::FromOQL("SELECT ModuleInstallation WHERE name='".DATAMODEL_MODULE."'"); - $oSet = new DBObjectSet($oSearch, array('installed' => false)); + $oSet = new DBObjectSet($oSearch, ['installed' => false]); $oLatestDM = $oSet->Fetch(); - if ($oLatestDM == null) - { + if ($oLatestDM == null) { return '0.0.0'; } return $oLatestDM->Get('version'); @@ -947,12 +842,9 @@ class RunTimeEnvironment public function Commit() { - if ($this->sFinalEnv != $this->sTargetEnv) - { - if (file_exists(utils::GetDataPath().$this->sTargetEnv.'.delta.xml')) - { - if (file_exists(utils::GetDataPath().$this->sFinalEnv.'.delta.xml')) - { + if ($this->sFinalEnv != $this->sTargetEnv) { + if (file_exists(utils::GetDataPath().$this->sTargetEnv.'.delta.xml')) { + if (file_exists(utils::GetDataPath().$this->sFinalEnv.'.delta.xml')) { // Make a "previous" file copy( utils::GetDataPath().$this->sFinalEnv.'.delta.xml', @@ -1013,34 +905,24 @@ class RunTimeEnvironment */ protected function CommitFile($sSource, $sDest, $bSourceMustExist = true) { - if (file_exists($sSource)) - { + if (file_exists($sSource)) { SetupUtils::builddir(dirname($sDest)); - if (file_exists($sDest)) - { + if (file_exists($sDest)) { $bRes = @unlink($sDest); - if (!$bRes) - { + if (!$bRes) { throw new Exception('Commit - Failed to cleanup destination file: '.$sDest); } } rename($sSource, $sDest); - } - else - { + } else { // The file does not exist - if ($bSourceMustExist) - { + if ($bSourceMustExist) { throw new Exception('Commit - Missing file: '.$sSource); - } - else - { + } else { // Align the destination with the source... make sure there is NO file - if (file_exists($sDest)) - { + if (file_exists($sDest)) { $bRes = @unlink($sDest); - if (!$bRes) - { + if (!$bRes) { throw new Exception('Commit - Failed to cleanup destination file: '.$sDest); } } @@ -1059,22 +941,15 @@ class RunTimeEnvironment */ protected function CommitDir($sSource, $sDest, $bSourceMustExist = true, $bRemoveSource = true) { - if (file_exists($sSource)) - { + if (file_exists($sSource)) { SetupUtils::movedir($sSource, $sDest, $bRemoveSource); - } - else - { + } else { // The file does not exist - if ($bSourceMustExist) - { + if ($bSourceMustExist) { throw new Exception('Commit - Missing directory: '.$sSource); - } - else - { + } else { // Align the destination with the source... make sure there is NO file - if (file_exists($sDest)) - { + if (file_exists($sDest)) { SetupUtils::rrmdir($sDest); } } @@ -1083,8 +958,7 @@ class RunTimeEnvironment public function Rollback() { - if ($this->sFinalEnv != $this->sTargetEnv) - { + if ($this->sFinalEnv != $this->sTargetEnv) { SetupUtils::tidydir(APPROOT.'env-'.$this->sTargetEnv); } } @@ -1098,10 +972,8 @@ class RunTimeEnvironment */ public function CallInstallerHandlers($aAvailableModules, $aSelectedModules, $sHandlerName) { - foreach($aAvailableModules as $sModuleId => $aModule) - { - if (($sModuleId != ROOT_MODULE) && in_array($sModuleId, $aSelectedModules)) - { + foreach ($aAvailableModules as $sModuleId => $aModule) { + if (($sModuleId != ROOT_MODULE) && in_array($sModuleId, $aSelectedModules)) { $aArgs = [MetaModel::GetConfig(), $aModule['version_db'], $aModule['version_code']]; RunTimeEnvironment::CallInstallerHandler($aAvailableModules[$sModuleId], $sHandlerName, $aArgs); } @@ -1120,14 +992,13 @@ class RunTimeEnvironment public static function CallInstallerHandler(array $aModuleInfo, $sHandlerName, array $aArgs) { $sModuleInstallerClass = ModuleFileReader::GetInstance()->GetAndCheckModuleInstallerClass($aModuleInfo); - if (is_null($sModuleInstallerClass)){ + if (is_null($sModuleInstallerClass)) { return; } SetupLog::Info("Calling Module Handler: $sModuleInstallerClass::$sHandlerName", null, $aArgs); $aCallSpec = [$sModuleInstallerClass, $sHandlerName]; - if (is_callable($aCallSpec)) - { + if (is_callable($aCallSpec)) { try { call_user_func_array($aCallSpec, $aArgs); } catch (Exception $e) { @@ -1160,8 +1031,8 @@ class RunTimeEnvironment SetupLog::Info("starting data load session"); $oDataLoader->StartSession($oMyChange); - $aFiles = array(); - $aPreviouslyLoadedFiles = array(); + $aFiles = []; + $aPreviouslyLoadedFiles = []; foreach ($aAvailableModules as $sModuleId => $aModule) { if (($sModuleId != ROOT_MODULE)) { $sRelativePath = 'env-'.$this->sTargetEnv.'/'.basename($aModule['root_dir']); @@ -1172,22 +1043,15 @@ class RunTimeEnvironment if ($bSampleData) { $aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); $aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']); - } - else - { + } else { // Load only structural data $aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); } - } - else - { - if ($bSampleData) - { + } else { + if ($bSampleData) { $aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); $aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']); - } - else - { + } else { // Load only structural data $aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); } @@ -1199,12 +1063,10 @@ class RunTimeEnvironment // Simulate the load of the previously loaded files, in order to initialize // the mapping between the identifiers in the XML and the actual identifiers // in the current database - foreach($aPreviouslyLoadedFiles as $sFileRelativePath) - { + foreach ($aPreviouslyLoadedFiles as $sFileRelativePath) { $sFileName = APPROOT.$sFileRelativePath; SetupLog::Info("Loading file: $sFileName (just to get the keys mapping)"); - if (empty($sFileName) || !file_exists($sFileName)) - { + if (empty($sFileName) || !file_exists($sFileName)) { throw(new Exception("File $sFileName does not exist")); } @@ -1213,12 +1075,10 @@ class RunTimeEnvironment SetupLog::Info($sResult); } - foreach($aFiles as $sFileRelativePath) - { + foreach ($aFiles as $sFileRelativePath) { $sFileName = APPROOT.$sFileRelativePath; SetupLog::Info("Loading file: $sFileName"); - if (empty($sFileName) || !file_exists($sFileName)) - { + if (empty($sFileName) || !file_exists($sFileName)) { throw(new Exception("File $sFileName does not exist")); } @@ -1240,9 +1100,8 @@ class RunTimeEnvironment */ protected static function MergeWithRelativeDir($aSourceArray, $sBaseDir, $aFilesToMerge) { - $aToMerge = array(); - foreach($aFilesToMerge as $sFile) - { + $aToMerge = []; + foreach ($aFilesToMerge as $sFile) { $aToMerge[] = $sBaseDir.'/'.$sFile; } return array_merge($aSourceArray, $aToMerge); @@ -1258,10 +1117,8 @@ class RunTimeEnvironment { $iCount = 0; $fStart = microtime(true); - foreach(MetaModel::GetClasses() as $sClass) - { - if (false == MetaModel::HasTable($sClass) && MetaModel::IsAbstract($sClass)) - { + foreach (MetaModel::GetClasses() as $sClass) { + if (false == MetaModel::HasTable($sClass) && MetaModel::IsAbstract($sClass)) { //if a class is not persisted and is abstract, the code below would crash. Needed by the class AbstractRessource. This is tolerable to skip this because we check the setup process integrity, not the datamodel integrity. continue; } @@ -1270,24 +1127,21 @@ class RunTimeEnvironment $oSearch->SetShowObsoleteData(false); $oSQLQuery = $oSearch->GetSQLQueryStructure(null, false); $sViewName = MetaModel::DBGetView($sClass); - if (strlen($sViewName) > 64) - { + if (strlen($sViewName) > 64) { throw new Exception("Class name too long for class: '$sClass'. The name of the corresponding view ($sViewName) would exceed MySQL's limit for the name of a table (64 characters)."); } $sTableName = MetaModel::DBGetTable($sClass); - if (strlen($sTableName) > 64) - { + if (strlen($sTableName) > 64) { throw new Exception("Table name too long for class: '$sClass'. The name of the corresponding MySQL table ($sTableName) would exceed MySQL's limit for the name of a table (64 characters)."); } $iTableCount = $oSQLQuery->CountTables(); - if ($iTableCount > 61) - { + if ($iTableCount > 61) { throw new Exception("Class requiring too many tables: '$sClass'. The structure of the class ($sClass) would require a query with more than 61 JOINS (MySQL's limitation)."); } $iCount++; } $fDuration = microtime(true) - $fStart; - return sprintf("Checked %d classes in %.1f ms. No error found.\n", $iCount, $fDuration*1000.0); + return sprintf("Checked %d classes in %.1f ms. No error found.\n", $iCount, $fDuration * 1000.0); } } // End of class diff --git a/tests/php-unit-tests/module_integration.xml.dist b/tests/php-unit-tests/module_integration.xml.dist index f13e34082..2934ea0b2 100644 --- a/tests/php-unit-tests/module_integration.xml.dist +++ b/tests/php-unit-tests/module_integration.xml.dist @@ -31,6 +31,7 @@ integration-tests/DictionariesConsistencyAfterSetupTest.php integration-tests/DictionariesConsistencyTest.php + integration-tests/iTopModulesDependencyValidationServiceTest.php diff --git a/tests/php-unit-tests/unitary-tests/setup/ModuleDiscoveryTest.php b/tests/php-unit-tests/unitary-tests/setup/ModuleDiscoveryTest.php new file mode 100644 index 000000000..4e31921c9 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/ModuleDiscoveryTest.php @@ -0,0 +1,80 @@ +RequireOnceItopFile('setup/runtimeenv.class.inc.php'); + $this->RequireOnceItopFile('setup/modulediscovery.class.inc.php'); + } + + public function testOrderModulesByDependencies_RealExample() + { + $aModules = json_decode(file_get_contents(__DIR__.'/ressources/reallife_discovered_modules.json'), true); + + $aResult = ModuleDiscovery::OrderModulesByDependencies($aModules, true); + + $aExpected = json_decode(file_get_contents(__DIR__.'/ressources/reallife_expected_ordered_modules.json'), true); + $this->assertEquals($aExpected, array_keys($aResult)); + } + + public function testOrderModulesByDependencies_LoadOnlyChoosenModules() + { + $aChoices = ['id1', 'id2']; + + $aModules = [ + "id1/1" => [ + 'dependencies' => [ 'id2/2'], + 'label' => 'label1', + ], + "id2/2" => [ + 'dependencies' => [], + 'label' => 'label2', + ], + "id3/3" => [ + 'dependencies' => [], + 'label' => 'label3', + ], + ]; + + $aResult = ModuleDiscovery::OrderModulesByDependencies($aModules, true, $aChoices); + + $aExpected = [ + "id2/2", + "id1/1", + ]; + $this->assertEquals($aExpected, array_keys($aResult)); + } + + public function testOrderModulesByDependencies_FailWhenChoosenModuleDependsOnUnchoosenModule() + { + $aChoices = ['id1']; + + $aModules = [ + "id1/1" => [ + 'dependencies' => [ 'id2/2'], + 'label' => 'label1', + ], + "id2/2" => [ + 'dependencies' => [], + 'label' => 'label2', + ], + ]; + + $sExpectedMessage = <<expectException(MissingDependencyException::class); + $this->expectExceptionMessage($sExpectedMessage); + + ModuleDiscovery::OrderModulesByDependencies($aModules, true, $aChoices); + } +} diff --git a/tests/php-unit-tests/unitary-tests/setup/moduledependency/DependencyExpressionTest.php b/tests/php-unit-tests/unitary-tests/setup/moduledependency/DependencyExpressionTest.php new file mode 100644 index 000000000..bb790ca6e --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/moduledependency/DependencyExpressionTest.php @@ -0,0 +1,189 @@ +RequireOnceItopFile('setup/moduledependency/dependencyexpression.class.inc.php'); + } + + public function testModuleDependencyInit_Invalid() + { + $oModuleDependency = new DependencyExpression('||'); + $this->assertFalse($oModuleDependency->IsValid()); + $this->assertFalse($oModuleDependency->IsResolved()); + } + + public static function WithOperatorProvider() + { + return [ + "nominal case" => [ + "dep" => "itop-config-mgmt/2.4.0", + 'expected_operator' => '>=', + ], + ">" => [ + "dep" => "itop-config-mgmt/>2.4.0", + 'expected_operator' => '>', + ], + ">=" => [ + "dep" => "itop-config-mgmt/>=2.4.0", + 'expected_operator' => '>=', + ], + "<" => [ + "dep" => "itop-config-mgmt/<2.4.0", + 'expected_operator' => '<', + ], + "<=" => [ + "dep" => "itop-config-mgmt/<=2.4.0", + 'expected_operator' => '<=', + ], + ]; + } + + /** + * @dataProvider WithOperatorProvider + */ + public function testModuleDependencyInit_WithOperator($sDepId, $sExpectedOperator) + { + $oModuleDependency = new DependencyExpression($sDepId); + $this->assertEquals([$sDepId => ['itop-config-mgmt', $sExpectedOperator, '2.4.0']], $this->GetNonPublicProperty($oModuleDependency, 'aParamsPerModuleId')); + $this->assertTrue($oModuleDependency->IsValid()); + $this->assertFalse($oModuleDependency->IsResolved()); + ; + $this->assertEquals(['itop-config-mgmt'], $oModuleDependency->GetRemainingModuleNamesToResolve()); + } + + public static function WithOperatorOperandProvider() + { + $aInternalStructure = ['itop-structure/3.0.0' => [ 'itop-structure', ">=", '3.0.0'], 'itop-portal/<3.2.1' => [ 'itop-portal', "<", '3.2.1']]; + return [ + '&&' => [ + 'sDepId' => 'itop-structure/3.0.0 && itop-portal/<3.2.1', + 'expected_structure' => $aInternalStructure, + ], + '&& with parenthesis' => [ + 'sDepId' => '(itop-structure/3.0.0) && (itop-portal/<3.2.1)', + 'expected_structure' => $aInternalStructure, + ], + '||' => [ + 'sDepId' => 'itop-structure/3.0.0 || itop-portal/<3.2.1', + 'expected_structure' => $aInternalStructure, + ], + '|| with parenthesis' => [ + 'sDepId' => '(itop-structure/3.0.0) || (itop-portal/<3.2.1)', + 'expected_structure' => $aInternalStructure, + ], + ]; + } + + /** + * @dataProvider WithOperatorOperandProvider + */ + public function testModuleDependencyInit_WithOperand($sDepId, $sExpected) + { + $oModuleDependency = new DependencyExpression($sDepId); + $this->assertEquals($sExpected, $this->GetNonPublicProperty($oModuleDependency, 'aParamsPerModuleId')); + $this->assertTrue($oModuleDependency->IsValid()); + ; + $this->assertEquals(['itop-structure', 'itop-portal'], $oModuleDependency->GetRemainingModuleNamesToResolve()); + } + + public static function SimpleDependencyExpressionIsResolvedProvider() + { + return [ + 'unresolved with major version' => [ + 'expr' => 'itop-config-mgmt/2.4.0', + 'module_versions' => ['itop-config-mgmt' => '1.2.3'], + 'expected_is_resolved' => false, + ], + 'unresolved with minor version' => [ + 'expr' => 'itop-config-mgmt/2.4.1', + 'module_versions' => ['itop-config-mgmt' => '2.4.0-1'], + 'expected_is_resolved' => false, + ], + 'resolution OK with major version' => [ + 'expr' => 'itop-config-mgmt/2.4.0', + 'module_versions' => ['itop-config-mgmt' => '2.4.2'], + 'expected_is_resolved' => true, + ], + 'resolution OK with minor version' => [ + 'expr' => 'itop-config-mgmt/2.4.0', + 'module_versions' => ['itop-config-mgmt' => '2.4.0-1'], + 'expected_is_resolved' => true, + ], + 'unproper use of api' => [ + 'expr' => 'itop-config-mgmt/2.4.0', + 'module_versions' => [], + 'expected_is_resolved' => false, + ], + ]; + } + + /** + * @dataProvider SimpleDependencyExpressionIsResolvedProvider + */ + public function testSimpleDependencyExpressionIsResolved($sExpression, $aModuleVersions, $bExpectedResolved) + { + $oModuleDependency = new DependencyExpression($sExpression); + $oModuleDependency->UpdateModuleResolutionState($aModuleVersions, ['itop-config-mgmt' => true]); + $this->assertEquals($bExpectedResolved, $oModuleDependency->IsResolved()); + if ($bExpectedResolved) { + $this->assertEquals([], $oModuleDependency->GetRemainingModuleNamesToResolve()); + } + } + + public static function ComplexDependencyExpressionIsResolvedProvider() + { + return [ + 'and + unresolved due to missing itop-portal' => [ + 'expr' => 'itop-structure/3.0.0 && itop-portal/3.2.1', + 'module_versions' => ['itop-structure' => '3.0.0'], + 'expected_is_resolved' => false, + 'remaining_module_names' => ['itop-portal'], + ], + 'and + unresolved due to unsifficient itop-portal version' => [ + 'expr' => 'itop-structure/3.0.0 && itop-portal/3.2.1', + 'module_versions' => ['itop-structure' => '3.0.0', 'itop-portal' => '1.0.0'], + 'expected_is_resolved' => false, + 'remaining_module_names' => ['itop-portal'], + ], + 'and + resolved' => [ + 'expr' => 'itop-structure/3.0.0 && itop-portal/3.2.1', + 'module_versions' => ['itop-structure' => '3.0.0', 'itop-portal' => '3.3.3'], + 'expected_is_resolved' => true, + 'remaining_module_names' => [], + ], + 'or + resolved' => [ + 'expr' => 'itop-structure/3.0.0 || itop-portal/3.2.1', + 'module_versions' => ['itop-structure' => '3.0.0'], + 'expected_is_resolved' => false, + 'remaining_module_names' => ['itop-portal'], + ], + 'or + resolved with less prerequisites' => [ + 'expr' => 'itop-structure/3.0.0 || itop-portal/3.2.1', + 'module_versions' => ['itop-structure' => '3.0.0'], + 'expected_is_resolved' => true, + 'remaining_module_names' => ['itop-portal'], + 'prerequisites' => ['itop-structure' => true], + ], + ]; + } + + /** + * @dataProvider ComplexDependencyExpressionIsResolvedProvider + */ + public function testComplexDependencyExpressionIsResolved($sExpression, $aModuleVersions, $bExpectedResolved, $aRemainingModuleNames, $aPrerequisites = ['itop-structure' => true, 'itop-portal' => true]) + { + $oModuleDependency = new DependencyExpression($sExpression); + + $oModuleDependency->UpdateModuleResolutionState($aModuleVersions, $aPrerequisites); + $this->assertEquals($aRemainingModuleNames, $oModuleDependency->GetRemainingModuleNamesToResolve()); + $this->assertEquals($bExpectedResolved, $oModuleDependency->IsResolved()); + } +} diff --git a/tests/php-unit-tests/unitary-tests/setup/moduledependency/ModuleDependencySortTest.php b/tests/php-unit-tests/unitary-tests/setup/moduledependency/ModuleDependencySortTest.php new file mode 100644 index 000000000..dd047d4f1 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/moduledependency/ModuleDependencySortTest.php @@ -0,0 +1,374 @@ +RequireOnceItopFile('setup/modulediscovery.class.inc.php'); + $this->RequireOnceItopFile('setup/moduledependency/moduledependencysort.class.inc.php'); + } + + public function testOrderModulesByDependencies_CheckExceptionWhenAllModuleUnresolved() + { + $aModules = [ + "id1/123" => [ + 'dependencies' => [ 'id3/666', 'id4/666'], + 'label' => 'label1', + ], + "id2/456" => [ + 'dependencies' => ['id3/666'], + 'label' => 'label2', + ], + ]; + + $sExpectedMessage = <<expectException(MissingDependencyException::class); + $this->expectExceptionMessage($sExpectedMessage); + + ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aModules, true); + } + + public function testOrderModulesByDependencies_CheckExceptionWhenSomeModuleUnresolved() + { + $aModules = [ + "id1/123" => [ + 'dependencies' => [ 'id2/456', 'id4/666', 'id3/789'], + 'label' => 'label1', + ], + "id2/456" => [ + 'dependencies' => [], + 'label' => 'label2', + ], + "id3/789" => [ + 'dependencies' => [ 'id2/456', 'id4/666'], + 'label' => 'label3', + ], + ]; + + $sExpectedMessage = <<expectException(MissingDependencyException::class); + $this->expectExceptionMessage($sExpectedMessage); + + ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aModules, true); + } + + public function testOrderModulesByDependencies_CheckExceptionWhenCircularDependencies() + { + $aModules = [ + "id1/1" => [ + 'dependencies' => [ 'id2/2'], + 'label' => 'label1', + ], + "id2/2" => [ + 'dependencies' => ['id3/3'], + 'label' => 'label2', + ], + "id3/3" => [ + 'dependencies' => ['id4/4'], + 'label' => 'label3', + ], + "id4/4" => [ + 'dependencies' => ['id1/1'], + 'label' => 'label4', + ], + ]; + + $sExpectedMessage = <<expectException(MissingDependencyException::class); + $this->expectExceptionMessage($sExpectedMessage); + + ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aModules, true); + } + + public function testOrderModulesByDependencies_KeepGoingEvenWithFailure() + { + $aModules = [ + "id1/123" => [ + 'dependencies' => [ 'id2/456', 'id4/666', 'id3/789'], + 'label' => 'label1', + ], + "id2/456" => [ + 'dependencies' => [], + 'label' => 'label2', + ], + "id3/789" => [ + 'dependencies' => [ 'id2/456', 'id4/666'], + 'label' => 'label3', + ], + ]; + + $aResult = ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aModules, false); + + $aExpected = [ + 'id2/456', + ]; + + $this->assertEquals($aExpected, array_keys($aResult)); + } + + public function testOrderModulesByDependencies_Nominalcase() + { + $aModules = [ + "id0/1" => [ + 'dependencies' => [ 'id2/2'], + 'label' => 'label1', + ], + "id1/1" => [ + 'dependencies' => [ 'id2/2'], + 'label' => 'label1', + ], + "id2/2" => [ + 'dependencies' => ['id3/3'], + 'label' => 'label2', + ], + "id3/3" => [ + 'dependencies' => ['id4/4'], + 'label' => 'label3', + ], + "id4/4" => [ + 'dependencies' => [], + 'label' => 'label4', + ], + ]; + + $aExpected = [ + "id4/4", + "id3/3", + "id2/2", + "id0/1", + "id1/1", + ]; + + $aResult = ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aModules, true); + + $this->assertEquals($aExpected, array_keys($aResult)); + } + + //warning : tricky usecase + public function testOrderModulesByDependencies_AllTermsOfOrExpressionWillImpactTheOrder() + { + $aModules = [ + "id0/1" => [ + 'dependencies' => [ 'id2/2 || id1/1'], + 'label' => 'label1', + ], + "id1/1" => [ + 'dependencies' => [ 'id2/2'], + 'label' => 'label1', + ], + "id2/2" => [ + 'dependencies' => [], + 'label' => 'label2', + ], + ]; + + $aExpected = [ + "id2/2", + "id1/1", + "id0/1", + ]; + + $aResult = ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aModules, true); + + $this->assertEquals($aExpected, array_keys($aResult)); + } + + //WARNING: alphabetical order make setup are determinititic + public function testOrderModulesByDependencies_ResolveNoDependendenciesOrderByAlphabeticalOrder() + { + $aModules = [ + "id2/2" => [ + 'dependencies' => [], + 'label' => 'label2', + ], + "id1/1" => [ + 'dependencies' => [], + 'label' => 'label1', + ], + "id3/3" => [ + 'dependencies' => [], + 'label' => 'label3', + ], + "id4/4" => [ + 'dependencies' => [], + 'label' => 'label4', + ], + "id0/1" => [ + 'dependencies' => [], + 'label' => 'label0', + ], + ]; + + $aExpected = [ + "id0/1", + "id1/1", + "id2/2", + "id3/3", + "id4/4", + ]; + + $aResult = ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aModules, true); + + $this->assertEquals($aExpected, array_keys($aResult)); + } + + public function testOrderModulesByDependencies_AlphabeticalOrderWithDependencies() + { + $aModules = [ + "id2/2" => [ + 'dependencies' => ["id1/1"], + 'label' => 'label2', + ], + "id1/1" => [ + 'dependencies' => [], + 'label' => 'label1', + ], + "id3/3" => [ + 'dependencies' => ["id1/1"], + 'label' => 'label3', + ], + ]; + + $aExpected = [ + "id1/1", + "id2/2", + "id3/3", + ]; + + $aResult = ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aModules, true); + + $this->assertEquals($aExpected, array_keys($aResult)); + } + + public function testOrderModulesByDependencies_AlphabeticalOrderWithDependencies2() + { + $aModules = [ + "z_id2/2" => [ //difference here + 'dependencies' => ["id1/1"], + 'label' => 'label2', + ], + "id1/1" => [ + 'dependencies' => [], + 'label' => 'label1', + ], + "id3/3" => [ + 'dependencies' => ["id1/1"], + 'label' => 'label3', + ], + ]; + + $aExpected = [ + "id1/1", + "id3/3", + "z_id2/2", + ]; + + $aResult = ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aModules, true); + + $this->assertEquals($aExpected, array_keys($aResult)); + } + + public function testSortModulesByCountOfDepencenciesDescending_NoDependencies() + { + $aUnresolvedDependencyModules = []; + $this->AddModule($aUnresolvedDependencyModules, 'c', []); + $this->AddModule($aUnresolvedDependencyModules, 'b', []); + $this->AddModule($aUnresolvedDependencyModules, 'a', []); + + $this->SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); + $this->assertEquals(['a', 'b', 'c'], array_keys($aUnresolvedDependencyModules)); + } + + public function testSortModulesByCountOfDepencenciesDescending_NominalUseCase() + { + $aUnresolvedDependencyModules = []; + $this->AddModule($aUnresolvedDependencyModules, 'itop-change-mgmt/456', ['itop-config-mgmt/2.2.0', 'itop-tickets/2.0.0']); + $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']); + $this->AddModule($aUnresolvedDependencyModules, 'itop-structure/2.7.1', []); + + $this->SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); + $this->assertEquals( + [ + 'itop-structure/2.7.1', + 'itop-config-mgmt/123', + 'itop-tickets/2.0.0', + 'itop-change-mgmt/456', + ], + array_keys($aUnresolvedDependencyModules) + ); + } + + public function testSortModulesByCountOfDepencenciesDescending_NominalUseCaseWithMissingDependency() + { + $aUnresolvedDependencyModules = []; + $this->AddModule($aUnresolvedDependencyModules, 'itop-change-mgmt/456', ['itop-config-mgmt/2.2.0', 'itop-tickets/2.0.0']); + $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']); + + $this->SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); + $this->assertEquals( + [ + 'itop-config-mgmt/123', + 'itop-tickets/2.0.0', + 'itop-change-mgmt/456', + ], + array_keys($aUnresolvedDependencyModules) + ); + } + + public function testSortModulesByCountOfDepencenciesDescending_FurtherVersionsOfSameModule() + { + $aUnresolvedDependencyModules = []; + $this->AddModule($aUnresolvedDependencyModules, 'moduleA/1', []); + $this->AddModule($aUnresolvedDependencyModules, 'moduleA/2', ['moduleC/1']); + $this->AddModule($aUnresolvedDependencyModules, 'moduleB/1', ['moduleA/1']); + $this->AddModule($aUnresolvedDependencyModules, 'moduleC/1', []); + + $this->SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules); + $this->assertEquals( + [ + 'moduleA/1', + 'moduleC/1', + 'moduleA/2', + 'moduleB/1', + ], + array_keys($aUnresolvedDependencyModules) + ); + } + + private function AddModule(array &$aUnresolvedDependencyModules, string $sModuleId, array $aDeps) + { + $oModule = new Module($sModuleId); + $oModule->SetDependencies($aDeps); + $aUnresolvedDependencyModules[$sModuleId] = $oModule; + } + + private function SortModulesByCountOfDepencenciesDescending(array &$aUnresolvedDependencyModules) + { + $this->InvokeNonPublicMethod(ModuleDependencySort::class, 'SortModulesByCountOfDepencenciesDescending', ModuleDependencySort::GetInstance(), [&$aUnresolvedDependencyModules]); + } +} diff --git a/tests/php-unit-tests/unitary-tests/setup/moduledependency/ModuleTest.php b/tests/php-unit-tests/unitary-tests/setup/moduledependency/ModuleTest.php new file mode 100644 index 000000000..b0d845c5f --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/moduledependency/ModuleTest.php @@ -0,0 +1,76 @@ +RequireOnceItopFile('setup/moduledependency/module.class.inc.php'); + } + + public function testModuleInit() + { + $oModule = new Module("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()); + } + + public function testModuleInit_NoVersion() + { + $oModule = new Module("itop-config-mgmt"); + $this->assertEquals("itop-config-mgmt", $oModule->GetModuleName()); + $this->assertEquals("1.0.0", $oModule->GetVersion()); + $this->assertEquals("itop-config-mgmt", $oModule->GetModuleId()); + } + + public function testSetDependencies_ComplexExpressionsParsing() + { + $oModule = new Module("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', + 'itop-datacenter-mgmt/3.1.0 || true && false', + ]); + $this->assertEquals( + ['itop-config-mgmt', 'itop-service-mgmt', 'itop-service-mgmt-provider', 'itop-datacenter-mgmt' ], + $oModule->GetUnresolvedDependencyModuleNames() + ); + } + + public function testIsResolved_Unresolved() + { + $oModule = new Module("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(), "all dependencies are unresolved"); + $this->assertFalse($oModule->IsResolved()); + + $oModule->UpdateModuleResolutionState([], []); + $this->assertFalse($oModule->IsResolved(), "all dependencies are still unresolved"); + } + + public function testIsResolved_PartialResolution() + { + $oModule = new Module("itop-bridge-cmdb-ticket"); + $oModule->SetDependencies(['itop-config-mgmt/2.7.1', 'itop-tickets/2.7.0']); + + $oModule->UpdateModuleResolutionState(['itop-config-mgmt' => '2.7.1'], ['itop-config-mgmt' => true]); + $this->assertFalse($oModule->IsResolved(), "some dependencies are still unresolved"); + $this->assertEquals(['itop-tickets'], $oModule->GetUnresolvedDependencyModuleNames(), 'one dependency is remaining'); + } + + public function testIsResolved_OK() + { + $oModule = new Module("itop-bridge-cmdb-ticket"); + $oModule->SetDependencies(['itop-config-mgmt/2.7.1', 'itop-tickets/2.7.0']); + + $oModule->UpdateModuleResolutionState(['itop-config-mgmt' => '2.7.1', 'itop-tickets' => '2.7.0'], ['itop-config-mgmt' => true, 'itop-tickets' => true]); + $this->assertTrue($oModule->IsResolved()); + $this->assertEquals([], $oModule->GetUnresolvedDependencyModuleNames()); + } +} diff --git a/tests/php-unit-tests/unitary-tests/setup/ressources/reallife_discovered_modules.json b/tests/php-unit-tests/unitary-tests/setup/ressources/reallife_discovered_modules.json new file mode 100644 index 000000000..02946914b --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/ressources/reallife_discovered_modules.json @@ -0,0 +1,267 @@ +{ + "authent-cas\/3.2.1": { + "label": "CAS SSO", + "dependencies": [] + }, + "authent-external\/3.2.1": { + "label": "External user authentication", + "dependencies": [] + }, + "authent-ldap\/3.2.1": { + "label": "User authentication based on LDAP", + "dependencies": [] + }, + "authent-local\/3.2.1": { + "label": "User authentication based on the local DB", + "dependencies": [] + }, + "combodo-backoffice-darkmoon-theme\/3.2.1": { + "label": "Backoffice: Darkmoon theme", + "dependencies": [] + }, + "combodo-backoffice-fullmoon-high-contrast-theme\/3.2.1": { + "label": "Backoffice: Fullmoon with high contrast accessibility theme", + "dependencies": [] + }, + "combodo-backoffice-fullmoon-protanopia-deuteranopia-theme\/3.2.1": { + "label": "Backoffice: Fullmoon with protonopia & deuteranopia accessibility theme", + "dependencies": [] + }, + "combodo-backoffice-fullmoon-tritanopia-theme\/3.2.1": { + "label": "Backoffice: Fullmoon with tritanopia accessibility theme", + "dependencies": [] + }, + "combodo-db-tools\/3.2.1": { + "label": "Database maintenance tools", + "dependencies": [ + "itop-structure\/3.0.0" + ] + }, + "itop-attachments\/3.2.1": { + "label": "Tickets Attachments", + "dependencies": [] + }, + "itop-backup\/3.2.1": { + "label": "Backup utilities", + "dependencies": [] + }, + "itop-bridge-cmdb-services\/3.2.1": { + "label": "Bridge for CMDB and Services", + "dependencies": [ + "itop-config-mgmt\/2.7.1", + "itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1" + ] + }, + "itop-bridge-cmdb-ticket\/3.2.1": { + "label": "Bridge for CMDB and Ticket", + "dependencies": [ + "itop-config-mgmt\/2.7.1", + "itop-tickets\/2.7.0" + ] + }, + "itop-bridge-datacenter-mgmt-services\/3.2.1": { + "label": "Bridge for CMDB Virtualization objects and Services", + "dependencies": [ + "itop-config-mgmt\/2.7.1", + "itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1", + "itop-datacenter-mgmt\/3.1.0" + ] + }, + "itop-bridge-endusers-devices-services\/3.2.1": { + "label": "Bridge for CMDB endusers objects and Services", + "dependencies": [ + "itop-config-mgmt\/2.7.1", + "itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1", + "itop-endusers-devices\/3.1.0" + ] + }, + "itop-bridge-storage-mgmt-services\/3.2.1": { + "label": "Bridge for CMDB Virtualization objects and Services", + "dependencies": [ + "itop-config-mgmt\/2.7.1", + "itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1", + "itop-storage-mgmt\/3.1.0" + ] + }, + "itop-bridge-virtualization-mgmt-services\/3.2.1": { + "label": "Bridge for CMDB Virtualization objects and Services", + "dependencies": [ + "itop-config-mgmt\/2.7.1", + "itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1", + "itop-virtualization-mgmt\/3.1.0" + ] + }, + "itop-bridge-virtualization-storage\/3.2.1": { + "label": "Links between virtualization and storage", + "dependencies": [ + "itop-storage-mgmt\/2.2.0", + "itop-virtualization-mgmt\/2.2.0" + ] + }, + "itop-change-mgmt-itil\/3.2.1": { + "label": "Change Management ITIL", + "dependencies": [ + "itop-config-mgmt\/2.2.0", + "itop-tickets\/2.0.0" + ] + }, + "itop-change-mgmt\/3.2.1": { + "label": "Change Management", + "dependencies": [ + "itop-config-mgmt\/2.2.0", + "itop-tickets\/2.0.0" + ] + }, + "itop-config-mgmt\/3.2.1": { + "label": "Configuration Management (CMDB)", + "dependencies": [ + "itop-structure\/2.7.1" + ] + }, + "itop-config\/3.2.1": { + "label": "Configuration editor", + "dependencies": [] + }, + "itop-core-update\/3.2.1": { + "label": "iTop Core Update", + "dependencies": [ + "itop-files-information\/2.7.0", + "combodo-db-tools\/2.7.0" + ] + }, + "itop-datacenter-mgmt\/3.2.1": { + "label": "Datacenter Management", + "dependencies": [ + "itop-config-mgmt\/2.2.0" + ] + }, + "itop-endusers-devices\/3.2.1": { + "label": "End-user Devices Management", + "dependencies": [ + "itop-config-mgmt\/2.2.0" + ] + }, + "itop-faq-light\/3.2.1": { + "label": "Frequently Asked Questions Database", + "dependencies": [ + "itop-structure\/3.0.0 || itop-portal\/3.0.0" + ] + }, + "itop-files-information\/3.2.1": { + "label": "iTop files information", + "dependencies": [] + }, + "itop-full-itil\/3.2.1": { + "label": "Bridge - Request management ITIL + Incident management ITIL", + "dependencies": [ + "itop-request-mgmt-itil\/2.3.0", + "itop-incident-mgmt-itil\/2.3.0" + ] + }, + "itop-hub-connector\/3.2.1": { + "label": "iTop Hub Connector", + "dependencies": [ + "itop-config-mgmt\/2.4.0" + ] + }, + "itop-incident-mgmt-itil\/3.2.1": { + "label": "Incident Management ITIL", + "dependencies": [ + "itop-config-mgmt\/2.4.0", + "itop-tickets\/2.4.0", + "itop-profiles-itil\/2.3.0" + ] + }, + "itop-knownerror-mgmt\/3.2.1": { + "label": "Known Errors Database", + "dependencies": [ + "itop-config-mgmt\/2.2.0" + ] + }, + "itop-oauth-client\/3.2.1": { + "label": "OAuth 2.0 client", + "dependencies": [ + "itop-welcome-itil\/3.1.0," + ] + }, + "itop-portal-base\/3.2.1": { + "label": "Portal Development Library", + "dependencies": [] + }, + "itop-portal\/3.2.1": { + "label": "Enhanced Customer Portal", + "dependencies": [ + "itop-portal-base\/2.7.0" + ] + }, + "itop-problem-mgmt\/3.2.1": { + "label": "Problem Management", + "dependencies": [ + "itop-tickets\/2.0.0" + ] + }, + "itop-profiles-itil\/3.2.1": { + "label": "Create standard ITIL profiles", + "dependencies": [] + }, + "itop-request-mgmt-itil\/3.2.1": { + "label": "User request Management ITIL", + "dependencies": [ + "itop-tickets\/2.4.0" + ] + }, + "itop-request-mgmt\/3.2.1": { + "label": "Simple Ticket Management", + "dependencies": [ + "itop-tickets\/2.4.0" + ] + }, + "itop-service-mgmt-provider\/3.2.1": { + "label": "Service Management for Service Providers", + "dependencies": [ + "itop-tickets\/2.0.0" + ] + }, + "itop-service-mgmt\/3.2.1": { + "label": "Service Management", + "dependencies": [ + "itop-tickets\/2.0.0" + ] + }, + "itop-sla-computation\/3.2.1": { + "label": "SLA Computation", + "dependencies": [] + }, + "itop-storage-mgmt\/3.2.1": { + "label": "Advanced Storage Management", + "dependencies": [ + "itop-config-mgmt\/2.4.0" + ] + }, + "itop-structure\/3.2.1": { + "label": "Core iTop Structure", + "dependencies": [] + }, + "itop-themes-compat\/3.2.1": { + "label": "Light grey and Test red themes compatibility", + "dependencies": [ + "itop-structure\/3.1.0" + ] + }, + "itop-tickets\/3.2.1": { + "label": "Tickets Management", + "dependencies": [ + "itop-structure\/2.7.1" + ] + }, + "itop-virtualization-mgmt\/3.2.1": { + "label": "Virtualization Management", + "dependencies": [ + "itop-config-mgmt\/2.4.0" + ] + }, + "itop-welcome-itil\/3.2.1": { + "label": "ITIL skin", + "dependencies": [] + } +} \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/setup/ressources/reallife_expected_ordered_modules.json b/tests/php-unit-tests/unitary-tests/setup/ressources/reallife_expected_ordered_modules.json new file mode 100644 index 000000000..2766580aa --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/ressources/reallife_expected_ordered_modules.json @@ -0,0 +1 @@ +["authent-cas\/3.2.1","authent-external\/3.2.1","authent-ldap\/3.2.1","authent-local\/3.2.1","combodo-backoffice-darkmoon-theme\/3.2.1","combodo-backoffice-fullmoon-high-contrast-theme\/3.2.1","combodo-backoffice-fullmoon-protanopia-deuteranopia-theme\/3.2.1","combodo-backoffice-fullmoon-tritanopia-theme\/3.2.1","itop-attachments\/3.2.1","itop-backup\/3.2.1","itop-config\/3.2.1","itop-files-information\/3.2.1","itop-portal-base\/3.2.1","itop-portal\/3.2.1","itop-profiles-itil\/3.2.1","itop-sla-computation\/3.2.1","itop-structure\/3.2.1","itop-themes-compat\/3.2.1","itop-tickets\/3.2.1","itop-welcome-itil\/3.2.1","combodo-db-tools\/3.2.1","itop-config-mgmt\/3.2.1","itop-core-update\/3.2.1","itop-datacenter-mgmt\/3.2.1","itop-endusers-devices\/3.2.1","itop-faq-light\/3.2.1","itop-hub-connector\/3.2.1","itop-incident-mgmt-itil\/3.2.1","itop-knownerror-mgmt\/3.2.1","itop-oauth-client\/3.2.1","itop-problem-mgmt\/3.2.1","itop-request-mgmt-itil\/3.2.1","itop-request-mgmt\/3.2.1","itop-service-mgmt-provider\/3.2.1","itop-service-mgmt\/3.2.1","itop-storage-mgmt\/3.2.1","itop-virtualization-mgmt\/3.2.1","itop-bridge-cmdb-services\/3.2.1","itop-bridge-cmdb-ticket\/3.2.1","itop-bridge-datacenter-mgmt-services\/3.2.1","itop-bridge-endusers-devices-services\/3.2.1","itop-bridge-storage-mgmt-services\/3.2.1","itop-bridge-virtualization-mgmt-services\/3.2.1","itop-bridge-virtualization-storage\/3.2.1","itop-change-mgmt-itil\/3.2.1","itop-change-mgmt\/3.2.1","itop-full-itil\/3.2.1"] \ No newline at end of file