diff --git a/tests/php-unit-tests/integration-tests/XmlModule.php b/tests/php-unit-tests/integration-tests/XmlModule.php new file mode 100644 index 000000000..68ba63519 --- /dev/null +++ b/tests/php-unit-tests/integration-tests/XmlModule.php @@ -0,0 +1,66 @@ +sModuleName = $sModuleName; + } + + public function AddDependency(string $sXmlMetaInfoUID, string $sDefiningModuleName, array $aModules) + { + if ($sDefiningModuleName===$this->sModuleName){ + return; + } + + /*$aLog = ['itop-bridge-datacenter-mgmt-services', 'itop-datacenter-mgmt']; + if (in_array($sDefiningModuleName, $aLog) && in_array($this->sModuleName, $aLog)){ + echo $this->sModuleName . " => $sDefiningModuleName === " . $sXmlMetaInfoUID . "\n"; + }*/ + + if (! array_key_exists($sDefiningModuleName, $this->aXMlMetaInfosByModuleNames)){ + $this->aXMlMetaInfosByModuleNames[$sDefiningModuleName]=[$sXmlMetaInfoUID]; + } else { + if (! in_array($sXmlMetaInfoUID, $this->aXMlMetaInfosByModuleNames[$sDefiningModuleName])){ + $this->aXMlMetaInfosByModuleNames[$sDefiningModuleName][]=$sXmlMetaInfoUID; + } + } + + if (! array_key_exists($sDefiningModuleName, $this->aDependencyModulesNames)){ + /** @var XmlModule $oXmlModule */ + $oXmlModule = $aModules[$sDefiningModuleName]; + $this->aDependencyModulesNames[$sDefiningModuleName]=$oXmlModule; + } + } + + public function Depends(string $sModuleName) : bool + { + return array_key_exists($sModuleName, $this->aDependencyModulesNames) || array_key_exists($sModuleName, $this->aXMlMetaInfosByModuleNames); + } + + public function __toString(): string + { + return sprintf("%s (%s)", $this->sModuleName, implode('|', array_keys($this->aDependencyModulesNames))); + } + + public function CompleteModuleDependencies(array $aAllModules) : void + { + foreach ($this->aDependencyModulesNames as $sDirectDependency => $oXmlMod){ + /** @var \Combodo\iTop\Test\UnitTest\XmlModule $oDirectDepXmlModule */ + $oDirectDepXmlModule = $aAllModules[$sDirectDependency]; + foreach ($oDirectDepXmlModule->aDependencyModulesNames as $sDirectDependency2 => $oXmlMod2){ + if (! array_key_exists($sDirectDependency2, $this->aDependencyModulesNames) && ! in_array($sDirectDependency2, $this->aAllDependencyModulesNames)){ + $this->aAllDependencyModulesNames[]=$sDirectDependency2; + } + } + } + } + + +} \ No newline at end of file diff --git a/tests/php-unit-tests/integration-tests/XmlModuleMetaInfo.php b/tests/php-unit-tests/integration-tests/XmlModuleMetaInfo.php new file mode 100644 index 000000000..8955673fd --- /dev/null +++ b/tests/php-unit-tests/integration-tests/XmlModuleMetaInfo.php @@ -0,0 +1,29 @@ +sLastNodeId = $sLastNodeId; + $this->sNodeName = $sNodeName; + $this->sPath = $sPath; + $this->sDelta = $sDelta; + } + + public function IsDefine() : bool { + return + $this->sDelta === 'define_if_not_exists' + || $this->sDelta === 'define'; + } + + public function GetUID() : string + { + return $this->sNodeName . '_' . $this->sLastNodeId; + } +} \ No newline at end of file diff --git a/tests/php-unit-tests/integration-tests/iTopModulesDependencyTest.php b/tests/php-unit-tests/integration-tests/iTopModulesDependencyTest.php new file mode 100644 index 000000000..b5ca28e0e --- /dev/null +++ b/tests/php-unit-tests/integration-tests/iTopModulesDependencyTest.php @@ -0,0 +1,309 @@ +aFilesToRemove as $sTmpFile){ + @unlink($sTmpFile); + } + } + + private array $aAllDmFiles=[]; + public function ListDatamodelFiles() : array + { + if (count($this->aAllDmFiles)==0){ + $aGlobPAtterns = [ + APPROOT.'datamodels/2.x/*/datamodel.*.xml', + APPROOT.'application/*.xml', + APPROOT.'core/*.xml', + ]; + + foreach ($aGlobPAtterns as $sPattern) { + $this->aAllDmFiles = array_merge($this->aAllDmFiles, glob($sPattern)); + } + } + return $this->aAllDmFiles; + } + + private array $aModules=[]; + + public function FetchAllDependenciesViaDM() + { + foreach ($this->ListDatamodelFiles() as $sFile) { + $this->FetchXmlMetaInfo($sFile); + } + + foreach ($this->aDefineNodes as $sKey => $aModules){ + foreach ($aModules as $sModuleName){ + if (! array_key_exists($sModuleName, $this->aModules)){ + $this->aModules[$sModuleName] = new XmlModule($sModuleName); + } + } + } + + foreach ($this->aDependencyNodes as $sKey => $aModules){ + foreach ($aModules as $sModuleName){ + /** @var XmlModule $oCurrentXmlModule */ + $oCurrentXmlModule = $this->aModules[$sModuleName] ?? null; + if (is_null($oCurrentXmlModule)){ + $oCurrentXmlModule = new XmlModule($sModuleName); + $this->aModules[$sModuleName] = $oCurrentXmlModule; + } + + $aDefiningModules = $this->aDefineNodes[$sKey] ?? null; + if (is_null($aDefiningModules)){ + //throw new \Exception("weard behaviour $sKey"); + continue; + } + + foreach ($aDefiningModules as $sDefiningModuleName){ + $oCurrentXmlModule->AddDependency($sKey, $sDefiningModuleName, $this->aModules); + } + } + } + + $this->OrderModules(); + $this->CompleteModuleDependencies(); + } + + private array $aModulesDepsByModuleName=[]; + public function testModules() + { + $this->FetchAllDependenciesViaDM(); + + $aDirsToScan = [ + APPROOT.'datamodels/2.x', + APPROOT.'extensions', + APPROOT.'data/production-modules', + ]; + + foreach(\ModuleDiscovery::GetAvailableModules($aDirsToScan) as $sModuleId => $aData) { + list($sModuleName, $sVersion) = \ModuleDiscovery::GetModuleName($sModuleId); + $aCurrentDeps = $aData['dependencies'] ?? []; + $this->aModulesDepsByModuleName[$sModuleName] = $aCurrentDeps; + } + + $aErrors=[]; + /** @var XmlModule $oXmlModule */ + foreach ($this->aModules as $sModuleName => $oXmlModule) { + $aCurrentDeps = $this->aModulesDepsByModuleName[$sModuleName] ?? []; + $aModuleErrors=[]; + foreach ($oXmlModule->aDependencyModulesNames as $sDepModuleName => $oXmlModule2){ + if ($sDepModuleName==="core"||$sDepModuleName==="application"){ + continue; + } + + $sXmlUIDs = implode('|', $oXmlModule->aXMlMetaInfosByModuleNames[$sDepModuleName]); + foreach ($aCurrentDeps as $sDepString){ + $oModuleDependency = new \ModuleDependency($sDepString); + if (! in_array($sDepModuleName, $oModuleDependency->GetPotentialPrerequisiteModuleNames())){ + $bResolved=false; + foreach ($oModuleDependency->GetPotentialPrerequisiteModuleNames() as $sPotentialDepModuleName){ + /** @var XmlModule $oXmlModule2 */ + $oXmlModule2 = $this->aModules[$sPotentialDepModuleName]??null; + + if (! is_null($oXmlModule2) && $oXmlModule2->Depends($sDepModuleName)){ + $bResolved=true; + } + } + if (! $bResolved){ + $aModuleErrors []= "$sModuleName depends on $sDepModuleName but missing in module dependencies: " . implode('|', $aCurrentDeps) . ". ($sXmlUIDs)"; + } + } + } + } + + if (count($aModuleErrors)){ + $aErrors[$sModuleName]=$aModuleErrors; + } + } + + $this->assertEquals(0, count($aErrors), var_export($aErrors, true)); + + } + + public function testGetMetaInfo() + { + $this->FetchXmlMetaInfo(APPROOT . 'core/datamodel.core.xml'); + + $this->assertEquals("core", $this->sCurrentModule); + $this->assertEquals(22, count($this->aDefineNodes)); + $this->assertEquals(0, count($this->aDependencyNodes)); + //var_dump($this->aDefineNodes); + $this->assertEquals([], $this->aDefineNodes); + $this->assertEquals([], $this->aDependencyNodes); + } + + private string $sCurrentModule; + private array $aDefineNodes; + private array $aDependencyNodes; + + private function GetModuleSuffix($sFile) : string + { + if (! preg_match('|.*datamodel\.([^\.]+)\.xml|', $sFile, $aMatches)){ + throw new \Exception("Regexp issue: $sFile"); + } + return $aMatches[1]; + } + + public function FetchXmlMetaInfo($sFile) : void { + $oDomDoc = new \DOMDocument('1.0', 'UTF-8'); + libxml_clear_errors(); + $oDomDoc->loadXml(file_get_contents($sFile)); + $aErrors = libxml_get_errors(); + if (count($aErrors) > 0) + { + throw new \Exception("Malformed XML"); + } + + $this->sCurrentModule = $this->GetModuleSuffix($sFile); + + if (! isset($this->aDefineNodes)){ + $this->aDefineNodes=[]; + } + + if (! isset($this->aDependencyNodes)){ + $this->aDependencyNodes=[]; + } + + foreach ($oDomDoc->childNodes as $oDomNode){ + $this->FetchMetaInfo($oDomDoc->childNodes); + } + } + + private function FetchMetaInfo(\DOMNodeList $oDomNodeList, ?string $sPath=null) + { + /** @var \DOMNode $oDomNode */ + foreach ($oDomNodeList as $oDomNode) { + /** @var \DOMAttr $oDelta */ + $oDelta = $oDomNode->attributes['_delta'] ?? null; + /** @var \DOMAttr $oId */ + $oId = $oDomNode->attributes['id'] ?? null; + + if (!is_null($oDelta) && ! is_null($oId)) { + $sId = $oId->nodeValue; + $sPath = $sPath ? $sPath . "->" . $sId : $sId; + $oXmlModuleMetaInfo = new XmlModuleMetaInfo($sId, $oDomNode->nodeName, $sPath, $oDelta->nodeValue); + $sKey = $oXmlModuleMetaInfo->GetUID(); + if ($oXmlModuleMetaInfo->IsDefine()){ + if (array_key_exists($sKey, $this->aDefineNodes)){ + $this->aDefineNodes[$sKey][]=$this->sCurrentModule; + } else { + $this->aDefineNodes[$sKey]=[ $this->sCurrentModule ]; + } + } else { + if (array_key_exists($sKey, $this->aDependencyNodes)){ + $this->aDependencyNodes[$sKey][]=$this->sCurrentModule; + } else { + $this->aDependencyNodes[$sKey]=[ $this->sCurrentModule ]; + } + } + } + + $this->FetchMetaInfo($oDomNode->childNodes, $sPath); + } + } + + private function OrderModules() + { + $aModuleDepsCount = []; + /** @var XmlModule $oXmlModule */ + foreach ($this->aModules as $oXmlModule) { + $aModuleDepsCount[$oXmlModule->sModuleName] = count($oXmlModule->aDependencyModulesNames); + } + + $aOrderModules=[]; + while (count($aModuleDepsCount)>0) { + asort($aModuleDepsCount); + + foreach ($aModuleDepsCount as $sModuleName => $iCount){ + if ($iCount>0){ + /** @var XmlModule $oXmlModule */ + $oXmlModule = $this->aModules[$sModuleName]; + /*var_dump((string)$oXmlModule); + var_dump((string)$this->aModules['itop-datacenter-mgmt']); + var_dump((string)$this->aModules['itop-config-mgmt']); + var_dump($aModuleDepsCount);*/ + //var_dump($this->aModules['itop-bridge-cmdb-services']->aXMlMetaInfosByModuleNames["itop-bridge-cmdb-ticket"]); + //var_dump($this->aModules['itop-bridge-cmdb-ticket']->aXMlMetaInfosByModuleNames["itop-bridge-cmdb-services"]); + throw new \Exception("still deps with $sModuleName"); + } + + unset($aModuleDepsCount[$sModuleName]); + $aOrderModules[$sModuleName] = $this->aModules[$sModuleName]; + break; + } + + //echo "$sModuleName\n"; + foreach ($aModuleDepsCount as $sStillToProcessModuleName => $c){ + /** @var XmlModule $oXmlStillToProcessModule */ + $oXmlStillToProcessModule = $this->aModules[$sStillToProcessModuleName]; + if ($oXmlStillToProcessModule->Depends($sModuleName)){ + $aModuleDepsCount[$sStillToProcessModuleName] = $c - 1 ; + } + } + } + + $this->aModules = $aOrderModules; + } + + private function CompleteModuleDependencies() + { + /** @var XmlModule $oXmlModule */ + foreach ($this->aModules as $oXmlModule) { + $oXmlModule->CompleteModuleDependencies($this->aModules); + } + } +} + +