Files
iTop/tests/php-unit-tests/integration-tests/iTopModulesDependencyTest.php

450 lines
13 KiB
PHP

<?php
/**
* Copyright (C) 2013-2024 Combodo SAS
* This file is part of iTop.
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* iTop is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License
*/
namespace Combodo\iTop\Test\UnitTest\Integration;
use ApplicationException;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use Combodo\iTop\Test\UnitTest\XmlModule;
use Combodo\iTop\Test\UnitTest\XmlModuleMetaInfo;
use utils;
require_once __DIR__ . '/XmlModuleMetaInfo.php';
require_once __DIR__ . '/XmlModule.php';
use ModuleInstallerAPI;
/**
* @package Combodo\iTop\Test\UnitTest\Setup
*/
class iTopModulesDependencyTest extends ItopTestCase {
private array $aFilesToRemove = [];
private array $aModules=[];
private static array $aModulesDataByModuleName=[];
private string $sCurrentModule;
private array $aDefineNodes;
private array $aDependencyNodes;
private array $aAllDmFiles=[];
private array $aSoftDependencyNodes=[];
protected function setUp(): void
{
parent::setUp(); // TODO: Change the autogenerated stub
}
protected function tearDown(): void
{
parent::tearDown(); // TODO: Change the autogenerated stub
foreach ($this->aFilesToRemove as $sTmpFile){
@unlink($sTmpFile);
}
}
public function ListDatamodelFiles() : array
{
if (count($this->aAllDmFiles)==0){
$aGlobPAtterns = [
APPROOT.'datamodels/2.x',
APPROOT.'application/*.xml',
APPROOT.'core/*.xml',
APPROOT.'extensions',
APPROOT.'extensions/*',
APPROOT.'data/production-modules',
APPROOT.'data/production-modules/*',
];
foreach ($aGlobPAtterns as $sPattern) {
$this->aAllDmFiles = array_merge($this->aAllDmFiles, glob("$sPattern/datamodel.*.xml"));
}
}
return $this->aAllDmFiles;
}
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);
}
}
$aSoftlyDependentModules = $this->aSoftDependencyNodes[$sKey] ?? null;
if (is_null($aSoftlyDependentModules)){
continue;
}
foreach ($aSoftlyDependentModules as $sModuleName){
/** @var XmlModule $oCurrentXmlModule */
$oCurrentXmlModule = $this->aModules[$sModuleName] ?? null;
if (is_null($oCurrentXmlModule)){
$oCurrentXmlModule = new XmlModule($sModuleName);
$this->aModules[$sModuleName] = $oCurrentXmlModule;
}
$oCurrentXmlModule->AddDependency($sKey, $aModules, $this->aModules);
}
}
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)){
continue;
}
$oCurrentXmlModule->AddDependency($sKey, $aDefiningModules, $this->aModules);
}
}
$this->OrderModules();
$this->CompleteModuleDependencies();
}
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
require_once APPROOT."setup/modulediscovery.class.inc.php";
require_once(APPROOT.'/setup/moduleinstaller.class.inc.php');
if (count(self::$aModulesDataByModuleName)>0){
return;
}
$aDirsToScan = [
APPROOT.'datamodels/2.x',
APPROOT.'extensions',
APPROOT.'extensions/*',
APPROOT.'data/production-modules',
APPROOT.'data/production-modules/*',
];
$aFilesToRemove=[];
foreach ($aDirsToScan as $sDir){
foreach (glob("$sDir/*/module.*.php") as $sFile) {
$sContent = file_get_contents($sFile);
$sContent=str_replace('SetupWebPage::AddModule', '$aModuleData=array', $sContent);
$sTempFile = tempnam(sys_get_temp_dir(), 'modulefile_');
$aFilesToRemove[]=$sTempFile;
file_put_contents($sTempFile, $sContent);
$bDebug=false;
if ($bDebug){
echo "$sFile\n";
echo $sContent;
}
require_once $sTempFile;
if ($bDebug){
var_dump($aModuleData);
}
//replace tmp file by real module path
$aModuleData[0]=$sFile;
$sModuleId=$aModuleData[1];
list($sModuleName, $sVersion) = \ModuleDiscovery::GetModuleName($sModuleId);
self::$aModulesDataByModuleName[$sModuleName] = $aModuleData;
}
ksort(self::$aModulesDataByModuleName);
foreach ($aFilesToRemove as $sTmpFile){
@unlink($sTmpFile);
}
}
}
public function testReadModuleFileData()
{
$aFullnameClassesByModuleName=[];
foreach (self::$aModulesDataByModuleName as $sModuleName => $aModuleData){
echo "$sModuleName\n";
$aFiles = $aModuleData[2]['datamodel'] ?? [];
$sDir = dirname($aModuleData[0]);
foreach ($aFiles as $sFile){
if (preg_match("|.*model\.$sModuleName\.php|", $sFile)){
continue;
}
$aFullnameClassesByModuleName=array_merge($aFullnameClassesByModuleName, $this->ReadDependencies($sModuleName, "$sDir/$sFile"));
}
}
$this->assertEquals([], $aFullnameClassesByModuleName);
}
public function testModules()
{
$this->FetchAllDependenciesViaDM();
$aErrors=[];
/** @var XmlModule $oXmlModule */
foreach ($this->aModules as $sModuleName => $oXmlModule) {
$aCurrentDeps = self::$aModulesDataByModuleName[$sModuleName][2]['dependencies'] ?? [];
$aModuleErrors=[];
foreach ($oXmlModule->aDependencyModulesNames as $sDepModuleName => $oXmlModule2){
$sXmlUIDs = implode('|', $oXmlModule->aXMlMetaInfosByModuleNames[$sDepModuleName]);
$bResolved=false;
foreach ($aCurrentDeps as $sDepString){
$oModuleDependency = new \iTopCoreModuleDependency($sDepString);
if (in_array($sDepModuleName, $oModuleDependency->GetPotentialPrerequisiteModuleNames())) {
$bResolved=true;
break;
}
foreach ($oModuleDependency->GetPotentialPrerequisiteModuleNames() as $sPotentialDepModuleName){
/** @var XmlModule $oXmlModule2 */
$oXmlModule2 = $this->aModules[$sPotentialDepModuleName]??null;
if (! is_null($oXmlModule2) && $oXmlModule2->Depends($sDepModuleName)){
$bResolved=true;
break;
}
}
if ($bResolved) {
break;
}
}
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));
}
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($oId)) {
$sId = $oId->nodeValue;
$sCurrentPath = $sPath ? $sPath."->".$sId : $sId;
if (!is_null($oDelta)) {
$oXmlModuleMetaInfo = new XmlModuleMetaInfo($sId, $oDomNode->nodeName, $sCurrentPath, $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];
}
}
} else {
$oXmlModuleMetaInfo = new XmlModuleMetaInfo($sId, $oDomNode->nodeName, $sCurrentPath, "nodelta");
$sKey = $oXmlModuleMetaInfo->GetUID();
if (array_key_exists($sKey, $this->aSoftDependencyNodes)) {
$this->aSoftDependencyNodes[$sKey][] = $this->sCurrentModule;
} else {
$this->aSoftDependencyNodes[$sKey] = [$this->sCurrentModule];
}
}
} else if ($oDomNode instanceof \DOMElement){
$sCurrentPath = $sPath ? $sPath . '->' . $oDomNode->nodeName : $oDomNode->nodeName;
} else{
$sCurrentPath = $sPath;
}
$this->FetchMetaInfo($oDomNode->childNodes, $sCurrentPath);
}
}
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){
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);
}
}
/**
* Read declared classes/interfaces in modules.php file (either directly listed files or inside autoload)
* @param string $sModuleName: module name
* @param string : module file path
*
* @return array: dict with fullname classes as keys and module name as value
*/
public function ReadDependencies(string $sModuleName, string $sPath) : array
{
if (false !== strpos($sPath, 'autoload.php')){
return $this->ReadAutoloadDependencies($sModuleName, $sPath);
}
$aRes=[];
$sContent = file_get_contents($sPath);
$sNamespace='';
if (preg_match('|namespace (.*)[ ]*;|', $sContent, $aMatches)){
$sNamespace=trim($aMatches[1]) . '\\';
}
if (preg_match_all('|^class ([a-zA-Z]*) |m', $sContent, $aMatches)){
foreach($aMatches[1] as $sClass){
$aRes[$sNamespace.$sClass]=$sModuleName;
}
}
if (preg_match_all('|^interface ([a-zA-Z]*) |m', $sContent, $aMatches)){
foreach($aMatches[1] as $sInterface){
$aRes[$sNamespace.$sInterface]=$sModuleName;
}
}
return $aRes;
}
/**
* Read declared classes/interfaces autoload file
* @param string $sModuleName: module name
* @param string : module file path
*
* @return array: dict with fullname classes as keys and module name as value
*/
private function ReadAutoloadDependencies(string $sModuleName, string $sPath) : array
{
$sAutoloadClassMap = dirname($sPath) . "/composer/autoload_classmap.php";
//echo $sAutoloadClassMap . '\n';
if (!is_file($sAutoloadClassMap)) {
return [];
}
$sTempfile = tempnam(sys_get_temp_dir(), 'autoload_');
$this->aFilesToRemove[] = $sTempfile;
$sContent = file_get_contents($sAutoloadClassMap);
$sReplace=<<<TXT
\$aModuleFiles=
TXT;
$sContent = preg_replace('|return|', $sReplace, $sContent);
//echo $sContent;
file_put_contents($sTempfile, $sContent);
require_once $sTempfile;
$aRes=[];
foreach (array_keys($aModuleFiles) as $sClass){
if (strpos($sClass, 'InstalledVersions')){
continue;
}
$aRes[$sClass]=$sModuleName;
}
return $aRes;
}
public function testReadDependencies()
{
$this->assertEquals(['CASLoginExtension'], $this->ReadDependencies('authent-cas', APPROOT . 'datamodels/2.x/authent-cas/src/CASLoginExtension.php'));
}
}