module dependency optimization - refacto + dependency new sort order

This commit is contained in:
odain
2025-03-10 15:10:22 +01:00
parent dd68017020
commit 460a3e356e
6 changed files with 990 additions and 89 deletions

View File

@@ -68,6 +68,188 @@ HTML;
}
}
class ModuleDependency {
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 <name>/<optional_operator><version>
// 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;
}
}
public function GetPotentialPrerequisites() : array
{
return array_keys($this->aPotentialPrerequisites);
}
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'<br/>";
}
return $bResult;
}
}
class Module {
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';
}
}
public function GetModuleName()
{
return $this->sModuleName;
}
public function GetVersion()
{
return $this->sVersion;
}
public function GetModuleId()
{
return $this->sModuleId;
}
public function SetDependencies(array $aAllDependencies)
{
$this->aAllDependencies = $aAllDependencies;
$this->aOngoingDependencies = [];
foreach ($aAllDependencies as $sDepString){
$this->aOngoingDependencies[$sDepString]= new ModuleDependency($sDepString);
}
}
public function IsModuleResolved(array $aModuleVersions, array $aSelectedModules) : bool
{
$aNextDependencies=[];
$bDependenciesSolved = true;
foreach($this->aOngoingDependencies as $sDepId => $oModuleDependency)
{
/** @var ModuleDependency $oModuleDependency*/
if (!$oModuleDependency->IsDependencyResolved($aModuleVersions, $aSelectedModules))
{
$aNextDependencies[$sDepId]=$oModuleDependency;
$bDependenciesSolved = false;
}
}
$this->aOngoingDependencies=$aNextDependencies;
if ($bDependenciesSolved)
{
return true;
}
return false;
}
public function GetUnresolvedDependencyModuleNames(): array
{
$aRes=[];
foreach($this->aOngoingDependencies as $sDepId => $oModuleDependency) {
/** @var ModuleDependency $oModuleDependency */
$aRes = array_merge($aRes, $oModuleDependency->GetPotentialPrerequisites());
}
return array_unique($aRes);
}
}
class ModuleDiscovery
{
static $m_aModuleArgs = array(
@@ -221,11 +403,76 @@ class ModuleDiscovery
return self::OrderModulesByDependencies(self::$m_aModules, $bAbortOnMissingDependency, $aModulesToLoad);
}
public static function SortModulesByCountOfDepencenciesDescending(array &$aOngoingDependencies) : void
public static function SortModulesByCountOfDepencenciesDescending(array &$aUnresolvedDependencyModules) : void
{
uasort($aOngoingDependencies, function (array $aDeps1, array $aDeps2){
return count($aDeps1) - count($aDeps2);
});
$aCountDepsByModuleId=[];
$aDependsOnModuleName=[];
foreach($aUnresolvedDependencyModules as $sModuleId => $oModule) {
/** @var Module $oModule */
$aDependsOnModuleName[$oModule->GetModuleName()]=[];
}
foreach ($aUnresolvedDependencyModules as $sModuleId => $oModule) {
$iDepsCount = 0;
/** @var Module $oModule */
$aUnresolvedDependencyModuleNames = $oModule->GetUnresolvedDependencyModuleNames();
foreach ($aUnresolvedDependencyModuleNames as $sModuleName) {
if (array_key_exists($sModuleName, $aDependsOnModuleName)) {
$aDependsOnModuleName[$sModuleName][] = $sModuleId;
$iDepsCount++;
}
}
$iDepsCountIncludingOutsideModules = count($oModule->GetUnresolvedDependencyModuleNames());
$aCountDepsByModuleId[$sModuleId] = [$iDepsCount, $iDepsCountIncludingOutsideModules];
}
$aRes=[];
while(count($aUnresolvedDependencyModules)>0) {
asort($aCountDepsByModuleId);
uasort($aCountDepsByModuleId, function (array $aDeps1, array $aDeps2){
//compare only
$res = $aDeps1[0] - $aDeps2[0];
if ($res != 0){
return $res;
}
return $aDeps1[1] - $aDeps2[1];
});
$bOneLoopAtLeast=false;
foreach ($aCountDepsByModuleId as $sModuleId => $iDepsCount){
$oModule=$aUnresolvedDependencyModules[$sModuleId];
if ($bOneLoopAtLeast && $iDepsCount>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];
$iDepsCount = $aDepCount[0] - 1;
$aCountDepsByModuleId[$sModuleId2] = [ $iDepsCount, $aDepCount[1]];
}
unset($aDependsOnModuleName[$oModule->GetModuleName()]);
}
$bOneLoopAtLeast=true;
}
}
$aUnresolvedDependencyModules=$aRes;
}
/**
@@ -235,72 +482,63 @@ 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)
*/
public static function OrderModulesByDependencies($aModules, $bAbortOnMissingDependency = false, $aModulesToLoad = null, ?int &$iLoopCount=0)
{
$iLoopCount=0;
// Order the modules to take into account their inter-dependencies
$aDependencies = [];
$aOngoingDependencies = [];
$aUnresolvedDependencyModules = [];
$aSelectedModules = [];
foreach($aModules as $sId => $aModule)
foreach($aModules as $sModuleId => $aModule)
{
list($sModuleName, ) = self::GetModuleName($sId);
$oModule = new Module($sModuleId);
$sModuleName = $oModule->GetModuleName();
if (is_null($aModulesToLoad) || in_array($sModuleName, $aModulesToLoad))
{
$aCurrentDependencies = $aModule['dependencies'];
$aDependencies[$sId] = $aCurrentDependencies;
$aOngoingDependencies[$sId] = $aCurrentDependencies;
$oModule->SetDependencies($aModule['dependencies']);
$aUnresolvedDependencyModules[$sModuleId]=$oModule;
$aSelectedModules[$sModuleName] = true;
}
}
self::SortModulesByCountOfDepencenciesDescending($aOngoingDependencies);
self::SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules);
$aOrderedModules = [];
$aModuleVersions=[];
$iPreviousLoopDepencyCount=-1;
$iNextLoopCount=count($aOngoingDependencies);
$iNextLoopCount=count($aUnresolvedDependencyModules);
while(($iNextLoopCount!=$iPreviousLoopDepencyCount) //stop loop when no new dependency is resolved
&& ($iNextLoopCount > 0) //still remaining dependencies
)
{
$iLoopCount++;
$iPreviousLoopDepencyCount=$iNextLoopCount;
foreach($aOngoingDependencies as $sId => $aCurrentRemainingDeps)
foreach($aUnresolvedDependencyModules as $sModuleId => $oModule)
{
$aNextDependencies=[];
$bDependenciesSolved = true;
foreach($aCurrentRemainingDeps as $sDepId)
{
if (!self::DependencyIsResolved($sDepId, $aOrderedModules, $aSelectedModules))
{
$aNextDependencies[]=$sDepId;
$bDependenciesSolved = false;
}
/** @var Module $oModule */
if ($oModule->IsModuleResolved($aModuleVersions, $aSelectedModules)){
$aOrderedModules[] = $sModuleId;
$aModuleVersions[$oModule->GetModuleName()] = $oModule->GetVersion();
unset($aUnresolvedDependencyModules[$sModuleId]);
}
if ($bDependenciesSolved)
{
$aOrderedModules[] = $sId;
unset($aDependencies[$sId]);
unset($aOngoingDependencies[$sId]);
continue;
}
$aOngoingDependencies[$sId]=$aNextDependencies;
}
$iNextLoopCount=count($aOngoingDependencies);
self::SortModulesByCountOfDepencenciesDescending($aOngoingDependencies);
$iNextLoopCount=count($aUnresolvedDependencyModules);
self::SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules);
}
if ($bAbortOnMissingDependency && count($aOngoingDependencies) > 0)
if ($bAbortOnMissingDependency && count($aUnresolvedDependencyModules) > 0)
{
self::SortModulesByCountOfDepencenciesDescending($aOngoingDependencies);
self::SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules);
$aModulesInfo = [];
$aModuleDeps = [];
foreach($aOngoingDependencies as $sId => $aCurrentRemainingDeps)
/** @var Module $oModule */
foreach($aUnresolvedDependencyModules as $sModuleId => $oModule)
{
$aModule = $aModules[$sId];
$aModule = $aModules[$sModuleId];
$aDepsWithIcons = [];
$aDeps=$aDependencies[$sId];
foreach($aDeps as $sIndex => $sDepId)
foreach($oModule->aAllDependencies as $sIndex => $sDepId)
{
if (in_array($sDepId, $aCurrentRemainingDeps))
if (array_key_exists($sDepId, $oModule->aOngoingDependencies))
{
$aDepsWithIcons[$sIndex] = '❌ ' . $sDepId;
} else
@@ -308,8 +546,8 @@ class ModuleDiscovery
$aDepsWithIcons[$sIndex] = '✅ ' . $sDepId;
}
}
$aModuleDeps[] = "{$aModule['label']} (id: $sId) depends on: ".implode(' + ', $aDepsWithIcons);
$aModulesInfo[$sId] = array('module' => $aModule, 'dependencies' => $aDepsWithIcons);
$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);
@@ -318,9 +556,9 @@ class ModuleDiscovery
}
// Return the ordered list, so that the dependencies are met...
$aResult = array();
foreach($aOrderedModules as $sId)
foreach($aOrderedModules as $sModuleId)
{
$aResult[$sId] = $aModules[$sId];
$aResult[$sModuleId] = $aModules[$sModuleId];
}
return $aResult;
}

View File

@@ -0,0 +1,138 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Setup;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
class ModuleDependencyTest extends ItopTestCase
{
public function setUp(): void {
parent::setUp();
$this->RequireOnceItopFile('setup/modulediscovery.class.inc.php');
}
public function testModuleDependencyInit_Invalid()
{
$oModuleDependency = new \ModuleDependency('||');
$this->assertEquals(true, $this->GetNonPublicProperty($oModuleDependency, 'bAlwaysUnresolved'));
}
public function testModuleDependencyInit()
{
$oModuleDependency = new \ModuleDependency('itop-config-mgmt/2.4.0');
$this->assertEquals(['itop-config-mgmt/2.4.0' => [ 'itop-config-mgmt', '>=', '2.4.0']], $this->GetNonPublicProperty($oModuleDependency, 'aParamsPerModuleId'));
$this->assertEquals(false, $this->GetNonPublicProperty($oModuleDependency, 'bAlwaysUnresolved'));
$this->assertEquals(['itop-config-mgmt'], $oModuleDependency->GetPotentialPrerequisites());
}
public static function WithOperatorProvider()
{
$aUsecases=[];
foreach (['>', '>=', '<', '<='] as $sOperator){
$aUsecases[$sOperator]=[$sOperator];
}
return $aUsecases;
}
/**
* @dataProvider WithOperatorProvider
*/
public function testModuleDependencyInit_WithOperator($sOperator)
{
$sDepId = "itop-config-mgmt/{$sOperator}2.4.0";
$oModuleDependency = new \ModuleDependency($sDepId);
$this->assertEquals([$sDepId => [ 'itop-config-mgmt', $sOperator, '2.4.0']], $this->GetNonPublicProperty($oModuleDependency, 'aParamsPerModuleId'));
$this->assertEquals(false, $this->GetNonPublicProperty($oModuleDependency, 'bAlwaysUnresolved'));
$this->assertEquals(['itop-config-mgmt'], $oModuleDependency->GetPotentialPrerequisites());
}
public static function WithOperatorOperand()
{
$aUsecases=[];
foreach (['&&', '||'] as $sOperand){
$aUsecases[$sOperand]=[$sOperand, "itop-structure/3.0.0 $sOperand itop-portal/<3.2.1"];
$aUsecases["$sOperand + parenthesis"]=[$sOperand, "(itop-structure/3.0.0 $sOperand itop-portal/<3.2.1)"];
}
return $aUsecases;
}
/**
* @dataProvider WithOperatorOperand
*/
public function testModuleDependencyInit_WithOperand($sOperand, $sDepId)
{
$sDepId = "itop-structure/3.0.0 $sOperand itop-portal/<3.2.1";
$oModuleDependency = new \ModuleDependency($sDepId);
$this->assertEquals(['itop-structure/3.0.0' => [ 'itop-structure', ">=", '3.0.0'], 'itop-portal/<3.2.1' => [ 'itop-portal', "<", '3.2.1']], $this->GetNonPublicProperty($oModuleDependency, 'aParamsPerModuleId'));
$this->assertEquals(false, $this->GetNonPublicProperty($oModuleDependency, 'bAlwaysUnresolved'));
$this->assertEquals(['itop-structure', 'itop-portal'], $oModuleDependency->GetPotentialPrerequisites());
}
public function testModuleIsDependencyResolved_SimpleCase_UnresolvedDueToMissingModule()
{
$oModuleDependency = new \ModuleDependency('itop-config-mgmt/2.4.0');
$this->assertEquals(false, $oModuleDependency->IsDependencyResolved([], ['itop-config-mgmt' => true]));
}
public function testModuleIsDependencyResolved_SimpleCase_UnresolvedDueToWrongModuleVersion()
{
$oModuleDependency = new \ModuleDependency('itop-config-mgmt/2.4.0');
$this->assertEquals(false, $oModuleDependency->IsDependencyResolved(['itop-config-mgmt' => '1.2.3'], ['itop-config-mgmt' => true]));
}
public function testModuleIsDependencyResolved_SimpleCase_Resolved()
{
$oModuleDependency = new \ModuleDependency('itop-config-mgmt/2.4.0');
$this->assertEquals(['itop-config-mgmt'], $oModuleDependency->GetPotentialPrerequisites());
$this->assertEquals(true, $oModuleDependency->IsDependencyResolved(['itop-config-mgmt' => '2.4.1'], ['itop-config-mgmt' => true]));
$this->assertEquals([], $oModuleDependency->GetPotentialPrerequisites());
}
public function testIsDependencyResolved_AndOperand_UnresolvedDueToMissingModule()
{
$sDepId = "itop-structure/3.0.0 && itop-portal/3.2.1";
$oModuleDependency = new \ModuleDependency($sDepId);
$this->assertEquals(['itop-structure/3.0.0' => [ 'itop-structure', ">=", '3.0.0'], 'itop-portal/3.2.1' => [ 'itop-portal', ">=", '3.2.1']], $this->GetNonPublicProperty($oModuleDependency, 'aParamsPerModuleId'));
$this->assertEquals(false, $this->GetNonPublicProperty($oModuleDependency, 'bAlwaysUnresolved'));
$this->assertEquals(['itop-structure', 'itop-portal'], $oModuleDependency->GetPotentialPrerequisites());
$this->assertEquals(false, $oModuleDependency->IsDependencyResolved(['itop-structure' => '3.0.0'], ['itop-structure' => true, 'itop-portal' => true]));
$this->assertEquals(['itop-portal'], $oModuleDependency->GetPotentialPrerequisites());
}
public function testIsDependencyResolved_AndOperand_UnresolvedDueToWrongModuleVersion()
{
$sDepId = "itop-structure/3.0.0 && itop-portal/3.2.1";
$oModuleDependency = new \ModuleDependency($sDepId);
$this->assertEquals(['itop-structure/3.0.0' => [ 'itop-structure', ">=", '3.0.0'], 'itop-portal/3.2.1' => [ 'itop-portal', ">=", '3.2.1']], $this->GetNonPublicProperty($oModuleDependency, 'aParamsPerModuleId'));
$this->assertEquals(false, $this->GetNonPublicProperty($oModuleDependency, 'bAlwaysUnresolved'));
$this->assertEquals(['itop-structure', 'itop-portal'], $oModuleDependency->GetPotentialPrerequisites());
$this->assertEquals(false, $oModuleDependency->IsDependencyResolved(['itop-structure' => '3.0.0', 'itop-portal' => '1.0.0'], ['itop-structure' => true, 'itop-portal' => true]));
$this->assertEquals(['itop-portal'], $oModuleDependency->GetPotentialPrerequisites());
}
public function testIsDependencyResolved_AndOperand_Resolved()
{
$sDepId = "itop-structure/3.0.0 && itop-portal/3.2.1";
$oModuleDependency = new \ModuleDependency($sDepId);
$this->assertEquals(['itop-structure/3.0.0' => [ 'itop-structure', ">=", '3.0.0'], 'itop-portal/3.2.1' => [ 'itop-portal', ">=", '3.2.1']], $this->GetNonPublicProperty($oModuleDependency, 'aParamsPerModuleId'));
$this->assertEquals(false, $this->GetNonPublicProperty($oModuleDependency, 'bAlwaysUnresolved'));
$this->assertEquals(['itop-structure', 'itop-portal'], $oModuleDependency->GetPotentialPrerequisites());
$this->assertEquals(false, $oModuleDependency->IsDependencyResolved(['itop-structure' => '3.0.0'], ['itop-structure' => true]));
$this->assertEquals(['itop-portal'], $oModuleDependency->GetPotentialPrerequisites());
}
public function testIsDependencyResolved_OrOperand_ResolvedDueToMissingModule()
{
$sDepId = "itop-structure/3.0.0 || itop-portal/3.2.1";
$oModuleDependency = new \ModuleDependency($sDepId);
$this->assertEquals(['itop-structure/3.0.0' => [ 'itop-structure', ">=", '3.0.0'], 'itop-portal/3.2.1' => [ 'itop-portal', ">=", '3.2.1']], $this->GetNonPublicProperty($oModuleDependency, 'aParamsPerModuleId'));
$this->assertEquals(false, $this->GetNonPublicProperty($oModuleDependency, 'bAlwaysUnresolved'));
$this->assertEquals(['itop-structure', 'itop-portal'], $oModuleDependency->GetPotentialPrerequisites());
$this->assertEquals(true, $oModuleDependency->IsDependencyResolved(['itop-structure' => '3.0.0'], ['itop-structure' => true]));
$this->assertEquals(['itop-portal'], $oModuleDependency->GetPotentialPrerequisites());
}
}

View File

@@ -2,45 +2,18 @@
namespace Combodo\iTop\Test\UnitTest\Setup;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use ModuleDiscovery;
class ModuleDiscoveryTest extends ItopTestCase
class ModuleDiscoveryTest extends ItopDataTestCase
{
public function setUp(): void {
parent::setUp();
$this->RequireOnceItopFile('setup/modulediscovery.class.inc.php');
}
public function testSortModulesByCountOfDepencenciesDescending()
{
$aOngoingDependencies=[];
$aExpectedKeys=[];
for($i=5; $i>0; $i--){
$sKey = "k$i";
$aExpectedKeys[]=$sKey;
$aDeps=[];
for ($j=0; $j<$i; $j++){
$aDeps[]=$j;
}
$aOngoingDependencies[$sKey]=$aDeps;
}
sort($aExpectedKeys);
\ModuleDiscovery::SortModulesByCountOfDepencenciesDescending($aOngoingDependencies);
$this->assertEquals($aExpectedKeys, array_keys($aOngoingDependencies));
}
public function testOrderModulesByDependencies_CheckMissingDependenciesAreCorrectlyOrderedInTheException()
{
$sExpectedMessage = <<<MSG
The following modules have unmet dependencies:
label2 (id: id2/456) depends on: ❌ id3/666,
label1 (id: id1/123) depends on: ❌ id3/666 + ❌ id4/666
MSG;
$this->expectExceptionMessage($sExpectedMessage);
$aModules=[
"id1/123" => [
'dependencies' => [ 'id3/666', 'id4/666'],
@@ -51,34 +24,115 @@ MSG;
'label' => 'label2',
],
];
\ModuleDiscovery::OrderModulesByDependencies($aModules, true);
$iLoopCount=0;
try{
ModuleDiscovery::OrderModulesByDependencies($aModules, true, null, $iLoopCount);
} catch(\MissingDependencyException $e){
$sExpectedMessage = <<<MSG
The following modules have unmet dependencies:
label2 (id: id2/456) depends on: ❌ id3/666,
label1 (id: id1/123) depends on: ❌ id3/666 + ❌ id4/666
MSG;
$this->assertEquals($sExpectedMessage, $e->getMessage());
$this->assertEquals(1, $iLoopCount);
}
}
public function testOrderModulesByDependencies_ValidateExceptionWithSomeDependenciesResolved()
{
$sExpectedMessage = <<<MSG
The following modules have unmet dependencies:
label1 (id: id1/123) depends on: ✅ id2/456 + ❌ id4/666
MSG;
$this->expectExceptionMessage($sExpectedMessage);
$aModules=[
"id1/123" => [
'dependencies' => [ 'id2/456', 'id4/666'],
'dependencies' => [ 'id2/456', 'id4/666', 'id3/789'],
'label' => 'label1',
],
"id2/456" => [
'dependencies' => [],
'label' => 'label2',
],
"id3/789" => [
'dependencies' => [ 'id2/456', 'id4/666'],
'label' => 'label3',
],
];
\ModuleDiscovery::OrderModulesByDependencies($aModules, true);
$iLoopCount=0;
try{
ModuleDiscovery::OrderModulesByDependencies($aModules, true, null, $iLoopCount);
} catch(\MissingDependencyException $e){
$sExpectedMessage = <<<MSG
The following modules have unmet dependencies:
label3 (id: id3/789) depends on: ✅ id2/456 + ❌ id4/666,
label1 (id: id1/123) depends on: ✅ id2/456 + ❌ id4/666 + ❌ id3/789
MSG;
$this->assertEquals($sExpectedMessage, $e->getMessage());
$this->assertEquals(2, $iLoopCount);
}
}
public function testOrderModulesByDependencies_KeepGoingEvenWithFailure_WithSomeDependenciesResolved()
{
$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',
],
];
$iLoopCount=0;
$aResult = ModuleDiscovery::OrderModulesByDependencies($aModules, false, null, $iLoopCount);
$aExpected = [
'id2/456'
];
$this->assertEquals($aExpected, array_keys($aResult));
$this->assertEquals(2, $iLoopCount);
}
public function testOrderModulesByDependencies_UnResolveWithCircularDependency()
{
$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',
],
];
$iLoopCount=0;
try{
ModuleDiscovery::OrderModulesByDependencies($aModules, true, null, $iLoopCount);
} catch(\MissingDependencyException $e){
$sExpectedMessage = <<<MSG
The following modules have unmet dependencies:
label1 (id: id1/1) depends on: ❌ id2/2,
label4 (id: id4/4) depends on: ❌ id1/1,
label3 (id: id3/3) depends on: ❌ id4/4,
label2 (id: id2/2) depends on: ❌ id3/3
MSG;
$this->assertEquals($sExpectedMessage, $e->getMessage());
$this->assertEquals(1, $iLoopCount);
}
}
public function testOrderModulesByDependencies_ResolveOk()
{
$aModules=[
"id1/1" => [
'dependencies' => [ 'id2/2'],
@@ -97,7 +151,8 @@ MSG;
'label' => 'label4',
],
];
$aResult = \ModuleDiscovery::OrderModulesByDependencies($aModules, true);
$iLoopCount=0;
$aResult = ModuleDiscovery::OrderModulesByDependencies($aModules, true, null, $iLoopCount);
$aExpected = [
"id4/4",
@@ -106,5 +161,79 @@ MSG;
"id1/1",
];
$this->assertEquals($aExpected, array_keys($aResult));
$this->assertEquals(1, $iLoopCount);
}
public function testOrderModulesByDependencies_RealExample(){
$aModules = json_decode(file_get_contents(__DIR__ . '/ressources/module_deps.json'), true);
$iLoopCount=0;
$aResult = ModuleDiscovery::OrderModulesByDependencies($aModules, true, null, $iLoopCount);
$aExpected = json_decode(file_get_contents(__DIR__ . '/ressources/expected_ordered_module_ids.json'), true);
$this->assertEquals($aExpected, array_keys($aResult));
$this->assertEquals(1, $iLoopCount);
}
public function testSortModulesByCountOfDepencenciesDescending_NoDependencies(){
$aUnresolvedDependencyModules = [];
foreach (['a', 'b', 'c'] as $sModuleId){
$this->AddModule($aUnresolvedDependencyModules, $sModuleId, []);
}
ModuleDiscovery::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', []);
ModuleDiscovery::SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules);
$this->assertEquals(
[
'itop-structure/2.7.1',
'itop-tickets/2.0.0',
'itop-config-mgmt/123',
'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']);
ModuleDiscovery::SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules);
$this->assertEquals(
[
'itop-tickets/2.0.0',
'itop-config-mgmt/123',
'itop-change-mgmt/456',
],
array_keys($aUnresolvedDependencyModules));
}
private function AddModule(array &$aUnresolvedDependencyModules, string $sModuleId, array $aDeps){
$oModule = new \Module($sModuleId);
$oModule->SetDependencies($aDeps);
$aUnresolvedDependencyModules[$sModuleId]= $oModule;
}
public function testSortModulesByCountOfDepencenciesDescending_RealExample(){
$aUnresolvedDependencyModules = [];
$aDependencies = json_decode(file_get_contents(__DIR__ . '/ressources/module_deps.json'), true);
foreach ($aDependencies as $sModuleId => $aModuleData){
$this->AddModule($aUnresolvedDependencyModules, $sModuleId, $aModuleData['dependencies']);
}
ModuleDiscovery::SortModulesByCountOfDepencenciesDescending($aUnresolvedDependencyModules);
$aExpected = json_decode(file_get_contents(__DIR__ . '/ressources/expected_ordered_module_ids.json'), true);
$this->assertEquals(
$aExpected,
array_keys($aUnresolvedDependencyModules));
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Setup;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
class ModuleTest extends ItopTestCase
{
public function setUp(): void {
parent::setUp();
$this->RequireOnceItopFile('setup/modulediscovery.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 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());
$this->assertEquals(false, $oModule->IsModuleResolved([],[]));
$this->assertEquals(['itop-config-mgmt', 'itop-tickets'], $oModule->GetUnresolvedDependencyModuleNames());
$this->assertEquals(['itop-config-mgmt/2.7.1', 'itop-tickets/2.7.0'], array_keys($oModule->aOngoingDependencies));
}
public function testSetDependencies()
{
$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',
]);
$this->assertEquals(['itop-config-mgmt', 'itop-service-mgmt', 'itop-service-mgmt-provider', 'itop-datacenter-mgmt' ],
$oModule->GetUnresolvedDependencyModuleNames());
$this->assertEquals(false, $oModule->IsModuleResolved([],[]));
$this->assertEquals(['itop-config-mgmt', 'itop-service-mgmt', 'itop-service-mgmt-provider', 'itop-datacenter-mgmt'],
$oModule->GetUnresolvedDependencyModuleNames());
$this->assertEquals(['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'],
array_keys($oModule->aOngoingDependencies));
}
public function testIsResolved_PartialResolution()
{
$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());
$this->assertEquals(false, $oModule->IsModuleResolved(['itop-config-mgmt' => '2.7.1'],['itop-config-mgmt'=>true]));
$this->assertEquals(['itop-tickets'], $oModule->GetUnresolvedDependencyModuleNames());
$this->assertEquals(['itop-tickets/2.7.0'], array_keys($oModule->aOngoingDependencies));
}
public function testIsResolved_OK()
{
$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());
$this->assertEquals(true, $oModule->IsModuleResolved(['itop-config-mgmt' => '2.7.1', 'itop-tickets' => '2.7.0'],['itop-config-mgmt'=>true, 'itop-tickets' => true]));
$this->assertEquals([], $oModule->GetUnresolvedDependencyModuleNames());
$this->assertEquals([], array_keys($oModule->aOngoingDependencies));
}
}

View File

@@ -0,0 +1,49 @@
[
"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-profiles-itil\/3.2.1",
"itop-sla-computation\/3.2.1",
"itop-structure\/3.2.1",
"itop-welcome-itil\/3.2.1",
"itop-portal\/3.2.1",
"combodo-db-tools\/3.2.1",
"itop-config-mgmt\/3.2.1",
"itop-themes-compat\/3.2.1",
"itop-tickets\/3.2.1",
"itop-oauth-client\/3.2.1",
"itop-datacenter-mgmt\/3.2.1",
"itop-endusers-devices\/3.2.1",
"itop-hub-connector\/3.2.1",
"itop-knownerror-mgmt\/3.2.1",
"itop-storage-mgmt\/3.2.1",
"itop-virtualization-mgmt\/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-faq-light\/3.2.1",
"itop-core-update\/3.2.1",
"itop-bridge-cmdb-ticket\/3.2.1",
"itop-change-mgmt-itil\/3.2.1",
"itop-change-mgmt\/3.2.1",
"itop-bridge-virtualization-storage\/3.2.1",
"itop-incident-mgmt-itil\/3.2.1",
"itop-full-itil\/3.2.1",
"itop-bridge-cmdb-services\/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"
]

View File

@@ -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": []
}
}