diff --git a/core/metamodel.class.php b/core/metamodel.class.php index 318fe3775..426e9de14 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -462,6 +462,20 @@ abstract class MetaModel return call_user_func([$sClass, 'GetClassDescription'], $sClass); } + + /** + * @param string $sClass + * + * @return string + * @throws \CoreException + */ + final public static function GetCreatedIn($sClass) + { + self::_check_subclass($sClass); + + return self::$m_aClassParams[$sClass]["created_in"] ?? ""; + } + /** * @param string $sClass * @@ -3145,6 +3159,7 @@ abstract class MetaModel $aMandatParams = [ "category" => "group classes by modules defining their visibility in the UI", "key_type" => "autoincrement | string", + //"created_in" => "module_name where class is defined", "name_attcode" => "define which attribute is the class name, may be an array of attributes (format specified in the dictionary as 'Class:myclass/Name' => '%1\$s %2\$s...'", "state_attcode" => "define which attribute is representing the state (object lifecycle)", "reconc_keys" => "define the attributes that will 'almost uniquely' identify an object in batch processes", diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index 918cf7f49..af7777266 100644 --- a/setup/compiler.class.inc.php +++ b/setup/compiler.class.inc.php @@ -1189,6 +1189,7 @@ EOF /** * @param \MFElement $oClass + * @param string $sModuleName * @param string $sTempTargetDir * @param string $sFinalTargetDir * @param string $sModuleRelativeDir @@ -1196,7 +1197,7 @@ EOF * @return string * @throws \DOMFormatException */ - protected function CompileClass($oClass, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir) + protected function CompileClass($oClass, $sModuleName, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir) { $sClass = $oClass->getAttribute('id'); $oProperties = $oClass->GetUniqueElement('properties'); @@ -1209,6 +1210,7 @@ EOF $aClassParams = []; $aClassParams['category'] = $this->GetPropString($oProperties, 'category', ''); $aClassParams['key_type'] = "'autoincrement'"; + $aClassParams['created_in'] = "'$sModuleName'"; if ((bool)$this->GetPropNumber($oProperties, 'is_link', 0)) { $aClassParams['is_link'] = 'true'; } diff --git a/setup/extensionsmap.class.inc.php b/setup/extensionsmap.class.inc.php index 6063ea76c..6b1a3e66e 100644 --- a/setup/extensionsmap.class.inc.php +++ b/setup/extensionsmap.class.inc.php @@ -155,10 +155,11 @@ class iTopExtensionsMap protected $aExtensions; /** * The list of all currently installed extensions - * @var array|null + * @var array */ - protected ?array $aInstalledExtensions = null; + protected array $aInstalledExtensions; + protected array $aExtensionsByCode; /** * The list of directories browsed using the ReadDir method when building the map * @var string[] @@ -168,6 +169,7 @@ class iTopExtensionsMap public function __construct($sFromEnvironment = 'production', $aExtraDirs = []) { $this->aExtensions = []; + $this->aExtensionsByCode = []; $this->aScannedDirs = []; $this->ScanDisk($sFromEnvironment); foreach ($aExtraDirs as $sDir) { @@ -261,6 +263,7 @@ class iTopExtensionsMap // This "new" extension is "newer" than the previous one, let's replace the previous one unset($this->aExtensions[$key]); $this->aExtensions[$oNewExtension->sCode.'/'.$oNewExtension->sVersion] = $oNewExtension; + $this->aExtensionsByCode[$oNewExtension->sCode] = $oNewExtension; return; } else { // This "new" extension is not "newer" than the previous one, let's ignore it @@ -270,6 +273,7 @@ class iTopExtensionsMap } // Finally it's not a duplicate, let's add it to the list $this->aExtensions[$oNewExtension->sCode.'/'.$oNewExtension->sVersion] = $oNewExtension; + $this->aExtensionsByCode[$oNewExtension->sCode] = $oNewExtension; } /** @@ -280,14 +284,33 @@ class iTopExtensionsMap */ public function GetFromExtensionCode(string $sExtensionCode): ?iTopExtension { - foreach ($this->aExtensions as $oExtension) { - if ($oExtension->sCode === $sExtensionCode) { - return $oExtension; + return $this->aExtensionsByCode[$sExtensionCode] ?? null; + } + + public function GetMissingExtensions(array $aSelectedExtensions) + { + \SetupLog::Info(__METHOD__, null, ['selected' => $aSelectedExtensions]); + $aExtensionsFromDb = array_keys($this->aExtensionsByCode); + sort($aExtensionsFromDb); + \SetupLog::Info(__METHOD__, null, ['found' => $aExtensionsFromDb]); + + $aRes = []; + foreach (array_diff($aExtensionsFromDb, $aSelectedExtensions) as $sExtensionCode) { + $oExtension = $this->Get($sExtensionCode); + if (!is_null($oExtension) && $oExtension->bVisible && $oExtension->sSource != iTopExtension::SOURCE_WIZARD) { + + \SetupLog::Info(__METHOD__."$sExtensionCode", null, ['visible' => $oExtension->bVisible, 'mandatory' => $oExtension->bMandatory]); + $aRes [] = $sExtensionCode; + } else { + \SetupLog::Info(__METHOD__." MISSING $sExtensionCode"); } } - return null; + \SetupLog::Info(__METHOD__, null, $aRes); + + return $aRes; } + /** * Read (recursively) a directory to find if it contains extensions (or modules) * @@ -465,11 +488,18 @@ class iTopExtensionsMap */ public function MarkAsChosen($sExtensionCode, $bMark = true) { - foreach ($this->aExtensions as $oExtension) { - if ($oExtension->sCode == $sExtensionCode) { - $oExtension->bMarkedAsChosen = $bMark; - break; - } + $oExtension = $this->Get($sExtensionCode); + if (!is_null($oExtension)) { + $oExtension->bMarkedAsChosen = $bMark; + } + } + + + public function MarkAsUninstallable($sExtensionCode, $bMark = true) + { + $oExtension = $this->Get($sExtensionCode); + if (!is_null($oExtension)) { + $oExtension->bUninstallable = $bMark; } } @@ -480,11 +510,11 @@ class iTopExtensionsMap */ public function IsMarkedAsChosen($sExtensionCode) { - foreach ($this->aExtensions as $oExtension) { - if ($oExtension->sCode == $sExtensionCode) { - return $oExtension->bMarkedAsChosen; - } + $oExtension = $this->Get($sExtensionCode); + if (!is_null($oExtension)) { + return $oExtension->bMarkedAsChosen; } + return false; } @@ -496,11 +526,9 @@ class iTopExtensionsMap */ protected function SetInstalledVersion($sExtensionCode, $sInstalledVersion) { - foreach ($this->aExtensions as $oExtension) { - if ($oExtension->sCode == $sExtensionCode) { - $oExtension->sInstalledVersion = $sInstalledVersion; - break; - } + $oExtension = $this->Get($sExtensionCode); + if (!is_null($oExtension)) { + $oExtension->sInstalledVersion = $sInstalledVersion; } } @@ -596,4 +624,4 @@ class iTopExtensionsMap return false; } -} +} \ No newline at end of file diff --git a/setup/feature_removal/SetupAudit.php b/setup/feature_removal/SetupAudit.php new file mode 100644 index 000000000..4104e94cb --- /dev/null +++ b/setup/feature_removal/SetupAudit.php @@ -0,0 +1,194 @@ +aExtensionToRemove = []; + $this->aClassesBeforeRemoval = []; + $this->aClassesAfterRemoval = []; + $this->aRemovedClasses = []; + $this->aFinalClassesRemoved = []; + } + + public function SetSelectedExtensions(Config $oConfig, array $aSelectedExtensions) + { + $oExtensionsMap = new \iTopExtensionsMap(); + $oExtensionsMap->LoadChoicesFromDatabase($oConfig); + + sort($aSelectedExtensions); + $this->aExtensionToRemove = $oExtensionsMap->GetMissingExtensions($aSelectedExtensions); + sort($this->aExtensionToRemove); + \SetupLog::Info(__METHOD__, null, ['aExtensionToRemove' => $this->aExtensionToRemove]); + } + + public function ComputeClassesBeforeRemoval(string $sTargetEnv) + { + $this->aClassesBeforeRemoval = $this->GetModelFromEnvironment($sTargetEnv); + } + + public function SetClassesAfterRemovalFromCurrentEnv() + { + $this->aClassesAfterRemoval = MetaModel::GetClasses(); + } + + public function SetClassesBeforeRemovalFromCurrentEnv() + { + $this->aClassesBeforeRemoval = MetaModel::GetClasses(); + } + + public function ComputeDryExtensionRemoval(array $aExtensionToRemove): void + { + $this->aExtensionToRemove = $aExtensionToRemove; + + if (count($this->aExtensionToRemove) == 0) { + //avoid time consuming setup audit when no extension removed + return; + } + + $sDryRemovalEnv = self::DRY_REMOVAL_AUDIT_ENV; + self::Cleanup($sDryRemovalEnv); + + $sSourceEnvt = MetaModel::GetEnvironment(); + + $oDryRemovalRuntimeEnvt = new RunTimeEnvironment($sDryRemovalEnv); + $oDryRemovalConfig = clone(MetaModel::GetConfig()); + $oDryRemovalConfig->ChangeModulesPath($sSourceEnvt, $sDryRemovalEnv); + + $oDryRemovalRuntimeEnvt->WriteConfigFileSafe($oDryRemovalConfig); + SetupUtils::copydir(APPROOT."/data/$sSourceEnvt-modules", APPROOT."/data/$sDryRemovalEnv-modules"); + $this->RemoveExtensionsLocally($sDryRemovalEnv, $this->aExtensionToRemove); + + $oDryRemovalRuntimeEnvt->CompileFrom($sSourceEnvt); + + $this->aClassesAfterRemoval = $this->GetModelFromEnvironment($sDryRemovalEnv); + + $oDryRemovalRuntimeEnvt->Rollback(); + self::Cleanup($sDryRemovalEnv); + } + + public static function Cleanup(string $sEnv) + { + SetupUtils::rrmdir(APPROOT."/data/$sEnv-modules"); + SetupUtils::rrmdir(APPROOT."/data/cache-$sEnv"); + SetupUtils::rrmdir(APPROOT."/env-$sEnv"); + SetupUtils::rrmdir(APPROOT."/conf/$sEnv"); + @unlink(APPROOT."/data/datamodel-$sEnv.xml"); + } + + public function GetModelFromEnvironment(string $sEnv): array + { + $sPHPExec = trim(\MetaModel::GetConfig()->Get('php_path')); + $sOutput = ""; + $iRes = 0; + exec(sprintf("$sPHPExec %s/get_model_reflection.php --env='%s'", __DIR__, $sEnv), $sOutput, $iRes); + if ($iRes != 0) { + \IssueLog::Error("Cannot get classes", null, ['code' => $iRes, "output" => $sOutput]); + throw new CoreException("Cannot get classes"); + } + + $aClasses = json_decode($sOutput[0] ?? null, true); + if (false === $aClasses) { + \IssueLog::Error("Invalid JSON", null, ["output" => $sOutput]); + throw new Exception("cannot get classes"); + } + + if (!is_array($aClasses)) { + \IssueLog::Error("not an array", null, ["classes" => $aClasses]); + throw new Exception("cannot get classes"); + } + + return $aClasses; + } + + private function RemoveExtensionsLocally(string $sTargetEnv, array $aExtensionCodes): void + { + $oExtensionsMap = new \iTopExtensionsMap($sTargetEnv); + + foreach ($aExtensionCodes as $sCode) { + /** @var \iTopExtension $oExtension */ + $oExtension = $oExtensionsMap->Get($sCode); + if (!is_null($oExtension)) { + $sDir = $oExtension->sSourceDir; + \IssueLog::Info(__METHOD__.": remove extension locally", null, [$oExtension->sCode => $sDir]); + SetupUtils::rrmdir($sDir); + } else { + \IssueLog::Warning(__METHOD__." cannot find extensions", null, ['env' => $sTargetEnv, 'code' => $sCode]); + } + } + } + + public function GetRemovedClasses(): array + { + if (count($this->aRemovedClasses) == 0) { + if (count($this->aClassesBeforeRemoval) == 0) { + return $this->aRemovedClasses; + } + + if (count($this->aClassesAfterRemoval) == 0) { + return $this->aRemovedClasses; + } + + $aExtensionsNames = array_diff($this->aClassesBeforeRemoval, $this->aClassesAfterRemoval); + $this->aRemovedClasses = []; + $aClasses = array_values($aExtensionsNames); + sort($aClasses); + + foreach ($aClasses as $i => $sClass) { + $this->aRemovedClasses[] = $sClass; + } + } + + return $this->aRemovedClasses; + } + + public function AuditExtensionsCleanupRules(bool $bStopAtFirstIssue = false): array + { + $this->aFinalClassesRemoved = []; + + foreach ($this->GetRemovedClasses() as $sClass) { + if (MetaModel::IsAbstract($sClass)) { + continue; + } + + if (!MetaModel::IsStandaloneClass($sClass)) { + $iCount = $this->Count($sClass); + $this->aFinalClassesRemoved[$sClass] = $iCount; + if ($bStopAtFirstIssue && $iCount > 0) { + //setup envt: should raise issue ASAP + throw new \Exception($sClass); + } + } + } + + return $this->aFinalClassesRemoved; + } + + private function Count($sClass): int + { + $oSearch = DBObjectSearch::FromOQL("SELECT $sClass", []); + $oSearch->AllowAllData(); + $oSet = new DBObjectSet($oSearch); + + return $oSet->Count(); + } +} \ No newline at end of file diff --git a/setup/feature_removal/get_model_reflection.php b/setup/feature_removal/get_model_reflection.php new file mode 100644 index 000000000..051b7b61e --- /dev/null +++ b/setup/feature_removal/get_model_reflection.php @@ -0,0 +1,40 @@ + $sArg) { + if (preg_match('/^--env=(.*)$/', $sArg, $aMatches)) { + $sEnv = $aMatches[1]; + } + } +} + +if (is_null($sEnv)) { + echo "No environment provided (--env) to read datamodel."; + exit(1); +} + +$sConfFile = utils::GetConfigFilePath($sEnv); + +try { + MetaModel::Startup($sConfFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv); +} +catch (\Throwable $e) { + \IssueLog::Error("Cannot read model from provided environment", null, + [ + 'env' => $sEnv, + 'error' => $e->getMessage(), + 'stack' => $e->getTraceAsString(), + ] + ); + echo "Cannot read model from provided environment"; + exit(1); +} + +$aClasses = MetaModel::GetClasses(); + +echo json_encode($aClasses); \ No newline at end of file diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index b383dbfbc..200e5faa3 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -215,6 +215,7 @@ class RunTimeEnvironment */ protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir) { + \SetupLog::Info(__METHOD__); $sSourceDirFull = APPROOT.$sSourceDir; if (!is_dir($sSourceDirFull)) { throw new Exception("The source directory '$sSourceDirFull' does not exist (or could not be read)"); @@ -1015,4 +1016,4 @@ class RunTimeEnvironment return sprintf("Checked %d classes in %.1f ms. No error found.\n", $iCount, $fDuration * 1000.0); } -} // End of class +} // End of class \ No newline at end of file diff --git a/setup/setuputils.class.inc.php b/setup/setuputils.class.inc.php index bbcadf553..a55195b44 100644 --- a/setup/setuputils.class.inc.php +++ b/setup/setuputils.class.inc.php @@ -1555,17 +1555,8 @@ JS return $sHtml; } - /** - * @param \WizardController $oWizard - * @param bool $bAbortOnMissingDependency ... - * @param array $aModulesToLoad List of modules to search for, defaults to all if ommitted - * - * @return array - * @throws Exception - */ - public static function AnalyzeInstallation($oWizard, $bAbortOnMissingDependency = false, $aModulesToLoad = null) + public static function GetConfig($oWizard) { - require_once(APPROOT.'/setup/moduleinstaller.class.inc.php'); $oConfig = new Config(); $sSourceDir = $oWizard->GetParameter('source_dir', ''); @@ -1580,7 +1571,25 @@ JS $aParamValues = $oWizard->GetParamForConfigArray(); $aParamValues['source_dir'] = $sRelativeSourceDir; $oConfig->UpdateFromParams($aParamValues, null); - $aDirsToScan = [$sSourceDir]; + + return $oConfig; + } + + /** + * @param \WizardController $oWizard + * @param bool $bAbortOnMissingDependency ... + * @param array $aModulesToLoad List of modules to search for, defaults to all if ommitted + * + * @return array + * @throws Exception + */ + public static function AnalyzeInstallation($oWizard, $bAbortOnMissingDependency = false, $aModulesToLoad = null) + { + require_once(APPROOT.'/setup/moduleinstaller.class.inc.php'); + + $oConfig = self::GetConfig($oWizard); + + $aDirsToScan = [$oWizard->GetParameter('source_dir', '')]; if (is_dir(APPROOT.'extensions')) { $aDirsToScan[] = APPROOT.'extensions'; @@ -2164,4 +2173,4 @@ class SetupInfo { return (array_key_exists($sModuleId, self::$aSelectedModules)); } -} +} \ No newline at end of file diff --git a/setup/wizardcontroller.class.inc.php b/setup/wizardcontroller.class.inc.php index 589c5c8a3..19d39979f 100644 --- a/setup/wizardcontroller.class.inc.php +++ b/setup/wizardcontroller.class.inc.php @@ -681,4 +681,4 @@ class Step3 extends WizardStep } } -End of the example */ +End of the example */ \ No newline at end of file diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php index 9d3a8010c..9e016b6f9 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -42,6 +42,7 @@ use Combodo\iTop\Application\WebPage\WebPage; use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator; +use Combodo\iTop\Setup\FeatureRemoval\SetupAudit; use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException; require_once(APPROOT.'setup/setuputils.class.inc.php'); @@ -50,6 +51,7 @@ require_once(APPROOT.'setup/applicationinstaller.class.inc.php'); require_once(APPROOT.'setup/parameters.class.inc.php'); require_once(APPROOT.'core/mutex.class.inc.php'); require_once(APPROOT.'setup/extensionsmap.class.inc.php'); +require_once APPROOT.'setup/feature_removal/SetupAudit.php'; /** * First step of the iTop Installation Wizard: Welcome screen, requirements @@ -2114,7 +2116,21 @@ class WizStepSummary extends WizardStep $this->bDependencyCheck = true; try { SetupUtils::AnalyzeInstallation($this->oWizard, true, $aSelectedModules); - } catch (MissingDependencyException $e) { + + $sInstallMode = utils::ReadParam('install_mode'); + \SetupLog::Info(__METHOD__, null, ['install_mode' => $sInstallMode]); + //if ($sInstallMode === "upgrade") { + $aExtensions = json_decode($this->oWizard->GetParameter('selected_extensions'), true); + $oSetupAudit = new SetupAudit([]); + + $oConfig = SetupUtils::GetConfig($this->oWizard); + $oSetupAudit->SetSelectedExtensions($oConfig, $aExtensions); + //$oSetupAudit->AuditExtensionsCleanupRules(true); + //} + + } + catch(MissingDependencyException $e) + { $this->bDependencyCheck = false; $this->sDependencyIssue = $e->getHtmlDesc(); } @@ -2581,4 +2597,4 @@ class WizStepDone extends WizardStep $oPage->add(file_get_contents($sBackupFile)); } } -} +} \ No newline at end of file diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopCustomDatamodelTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopCustomDatamodelTestCase.php index bb3421ebc..299a80239 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopCustomDatamodelTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopCustomDatamodelTestCase.php @@ -54,10 +54,20 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase */ abstract public function GetDatamodelDeltaAbsPath(): string; + /** + * @return array : dict extensions folders by their code + */ + public function GetAdditionalFeaturePaths(): array + { + return []; + } + protected function setUp(): void { - static::LoadRequiredItopFiles(); - $this->oEnvironment = new UnitTestRunTimeEnvironment('production', $this->GetTestEnvironment()); + static::LoadRequiredItopFiles(); + if (is_null($this->oEnvironment)) { + $this->oEnvironment = new UnitTestRunTimeEnvironment($this->GetTestEnvironment()); + } parent::setUp(); } @@ -155,26 +165,33 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase $oTestConfig->ChangeModulesPath($sSourceEnv, $sTestEnv); // - Switch DB name to a dedicated one so we don't mess with the original one $sTestEnvSanitizedForDBName = preg_replace('/[^\d\w]/', '', $sTestEnv); - $oTestConfig->Set('db_name', $oTestConfig->Get('db_name').'_'.$sTestEnvSanitizedForDBName); + $sPreviousDB = $oTestConfig->Get('db_name'); + $sNewDB = $sPreviousDB.'_'.$sTestEnvSanitizedForDBName; + $oTestConfig->Set('db_name', $sNewDB); // - Compile env. based on the existing 'production' env. - $oEnvironment = new UnitTestRunTimeEnvironment($sSourceEnv, $sTestEnv); - $oEnvironment->WriteConfigFileSafe($oTestConfig); - $oEnvironment->CompileFrom($sSourceEnv); + //$oEnvironment = new UnitTestRunTimeEnvironment($sSourceEnv, $sTestEnv); + $this->oEnvironment->WriteConfigFileSafe($oTestConfig); + $this->oEnvironment->CompileFrom($sSourceEnv); // - Force re-creating a fresh DB CMDBSource::InitFromConfig($oTestConfig); - if (CMDBSource::IsDB($oTestConfig->Get('db_name'))) { + if (CMDBSource::IsDB($sNewDB)) { CMDBSource::DropDB(); } - CMDBSource::CreateDB($oTestConfig->Get('db_name')); + CMDBSource::CreateDB($sNewDB); MetaModel::Startup($sConfFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sTestEnv); // N°7446 For some reason we need to create the DB schema before starting the MM, then only we can create the tables. MetaModel::DBCreate(); + // Make sure that runtime environment is complete + // RunTimeEnvironment::AnalyzeInstallation would not return core modules otherwise... + CMDBSource::DropTable("priv_module_install"); + CMDBSource::Query("CREATE TABLE $sNewDB.priv_module_install SELECT * FROM $sPreviousDB.priv_module_install"); + $this->debug("Custom environment '$sTestEnv' is ready!"); } parent::PrepareEnvironment(); } -} +} \ No newline at end of file diff --git a/tests/php-unit-tests/src/Service/UnitTestRunTimeEnvironment.php b/tests/php-unit-tests/src/Service/UnitTestRunTimeEnvironment.php index 8080d8abb..2daffa11c 100644 --- a/tests/php-unit-tests/src/Service/UnitTestRunTimeEnvironment.php +++ b/tests/php-unit-tests/src/Service/UnitTestRunTimeEnvironment.php @@ -26,23 +26,27 @@ use utils; */ class UnitTestRunTimeEnvironment extends RunTimeEnvironment { + /** + * @var false + */ + public bool $bUseDelta = true; + + /** + * @var true + */ + public bool $bUseAdditionalFeatures = false; + /** * @var string[] */ protected $aCustomDatamodelFiles = null; /** - * @var string + * @var string[] */ - protected $sSourceEnv; + protected $aAdditionExtensionFoldersByCode = null; - public function __construct($sSourceEnv, $sTargetEnv) - { - parent::__construct($sTargetEnv); - $this->sSourceEnv = $sSourceEnv; - } - - public function GetEnvironment(): string + public function GetEnvironment(): string { return $this->sFinalEnv; } @@ -56,6 +60,15 @@ class UnitTestRunTimeEnvironment extends RunTimeEnvironment SetupUtils::copydir(APPROOT.'/data/'.$sSourceEnv.'-modules', $sDestModulesDir, $bUseSymLinks); + if ($this->bUseAdditionalFeatures) { + foreach ($this->GetExtensionFoldersToAdd() as $sExtensionCode => $sFolderPath) { + \SetupLog::Info("ExtensionFoldersToAdd: $sExtensionCode => $sFolderPath"); + $sFolderName = basename($sFolderPath); + @mkdir($sDestModulesDir.DIRECTORY_SEPARATOR.$sFolderName); + SetupUtils::copydir($sFolderPath, $sDestModulesDir.DIRECTORY_SEPARATOR.$sFolderName, $bUseSymLinks); + } + } + parent::CompileFrom($sSourceEnv, $bUseSymLinks); } @@ -94,23 +107,43 @@ class UnitTestRunTimeEnvironment extends RunTimeEnvironment */ protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir) { + \SetupLog::Info(__METHOD__); $aRet = parent::GetMFModulesToCompile($sSourceEnv, $sSourceDir); - foreach ($this->GetCustomDatamodelFiles() as $sDeltaFile) { - $sDeltaId = preg_replace('/[^\d\w]/', '', $sDeltaFile); - $sDeltaName = basename($sDeltaFile); - $sDeltaDir = dirname($sDeltaFile); - $oDelta = new MFCoreModule($sDeltaName, "$sDeltaDir/$sDeltaName", $sDeltaFile); - $aRet[$sDeltaId] = $oDelta; + if ($this->bUseDelta) { + foreach ($this->GetCustomDatamodelFiles() as $sDeltaFile) { + $sDeltaId = preg_replace('/[^\d\w]/', '', $sDeltaFile); + $sDeltaName = basename($sDeltaFile); + $sDeltaDir = dirname($sDeltaFile); + $oDelta = new MFCoreModule($sDeltaName, "$sDeltaDir/$sDeltaName", $sDeltaFile); + $aRet[$sDeltaId] = $oDelta; + } } + return $aRet; } - public function GetCustomDatamodelFiles() + public function GetExtensionFoldersToAdd(): array { - if (!is_null($this->aCustomDatamodelFiles)) { - return $this->aCustomDatamodelFiles; + if (is_null($this->aAdditionExtensionFoldersByCode)) { + $this->InitViaItopCustomDatamodelTestCaseClasses(); } + + return $this->aAdditionExtensionFoldersByCode; + } + + public function GetCustomDatamodelFiles(): array + { + if (is_null($this->aCustomDatamodelFiles)) { + $this->InitViaItopCustomDatamodelTestCaseClasses(); + } + + return $this->aCustomDatamodelFiles; + } + + public function InitViaItopCustomDatamodelTestCaseClasses() + { + $this->aAdditionExtensionFoldersByCode = []; $this->aCustomDatamodelFiles = []; // Search for the PHP files implementing the method GetDatamodelDeltaAbsPath @@ -169,16 +202,19 @@ class UnitTestRunTimeEnvironment extends RunTimeEnvironment continue; } $sDeltaFile = $oTestClassInstance->GetDatamodelDeltaAbsPath(); - if (!is_file($sDeltaFile)) { - throw new \Exception("Unknown delta file: $sDeltaFile, from test class '$sClass'"); - } - if (!in_array($sDeltaFile, $this->aCustomDatamodelFiles)) { - $this->aCustomDatamodelFiles[] = $sDeltaFile; + if (strlen($sDeltaFile) > 0) { + if (!is_file($sDeltaFile)) { + throw new \Exception("Unknown delta file: $sDeltaFile, from test class '$sClass'"); + } + if (!in_array($sDeltaFile, $this->aCustomDatamodelFiles)) { + $this->aCustomDatamodelFiles[] = $sDeltaFile; + } } + + $aExtensionsPaths = $oTestClassInstance->GetAdditionalFeaturePaths(); + $this->aAdditionExtensionFoldersByCode = array_merge($this->aAdditionExtensionFoldersByCode, $aExtensionsPaths); } } - - return $this->aCustomDatamodelFiles; } private function FindFilesModifiedAfter(float $fReferenceTimestamp, string $sPathToScan, array &$aModifiedFiles) diff --git a/tests/php-unit-tests/unitary-tests/setup/feature_removal/SetupAuditTest.php b/tests/php-unit-tests/unitary-tests/setup/feature_removal/SetupAuditTest.php new file mode 100644 index 000000000..e0863f242 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/feature_removal/SetupAuditTest.php @@ -0,0 +1,164 @@ +oEnvironment = new UnitTestRunTimeEnvironment(self::ENVT); + $this->oEnvironment->bUseDelta = false; + $this->oEnvironment->bUseAdditionalFeatures = true; + parent::setUp(); + + $this->RequireOnceItopFile('/setup/feature_removal/SetupAudit.php'); + } + + public function GetTestEnvironment(): string + { + return self::ENVT; + } + + public function testGetModelFromEnvironment() + { + $oSetupAudit = new SetupAudit([]); + + $aExpected = \MetaModel::GetClasses(); + sort($aExpected); + + $aModel = $oSetupAudit->GetModelFromEnvironment($this->GetTestEnvironment()); + sort($aModel); + $this->assertEquals($aExpected, $aModel); + } + + public function testGetModelFromEnvironmentFailure() + { + $oSetupAudit = new SetupAudit([]); + + $aExpected = \MetaModel::GetClasses(); + sort($aExpected); + + $this->expectException(\CoreException::class); + $this->expectExceptionMessage("Cannot get classes"); + $aModel = $oSetupAudit->GetModelFromEnvironment('gabuzomeu'); + sort($aModel); + $this->assertEquals($aExpected, $aModel); + } + + public function GetDatamodelDeltaAbsPath(): string + { + //no delta: empty path provided + return ""; + } + + public function GetAdditionalFeaturePaths(): array + { + $aFeaturePaths = []; + foreach (glob(__DIR__."/additional_features/*", GLOB_ONLYDIR) as $aFeaturePath) { + $sCode = basename($aFeaturePath); + $aFeaturePaths[$sCode] = $aFeaturePath; + } + + return $aFeaturePaths; + } + + public function testComputeDryRemoval() + { + $oSetupAudit = new SetupAudit(); + $oSetupAudit->SetClassesBeforeRemovalFromCurrentEnv(); + $oSetupAudit->ComputeDryExtensionRemoval(['nominal_ext1', 'finalclass_ext2']); + $aRemovedClasses = $oSetupAudit->GetRemovedClasses(); + sort($aRemovedClasses); + $expected = [ + "Feature1Module1MyClass", + "FinalClassFeature2Module1MyClass", + "FinalClassFeature2Module1MyFinalClassFromLocation", + ]; + + sort($expected); + $this->assertEquals($expected, $aRemovedClasses); + } + + public function testComputeMTPWay() + { + $oSetupAudit = new SetupAudit(); + $oSetupAudit->ComputeClassesBeforeRemoval('production'); + $oSetupAudit->SetClassesAfterRemovalFromCurrentEnv(); + $oSetupAudit->AuditExtensionsCleanupRules(true); + + $oSetupAudit->SetClassesBeforeRemovalFromCurrentEnv(); + $oSetupAudit->ComputeDryExtensionRemoval(['nominal_ext1', 'finalclass_ext2']); + $aRemovedClasses = $oSetupAudit->GetRemovedClasses(); + sort($aRemovedClasses); + $expected = [ + "Feature1Module1MyClass", + "FinalClassFeature2Module1MyClass", + "FinalClassFeature2Module1MyFinalClassFromLocation", + ]; + + sort($expected); + $this->assertEquals($expected, $aRemovedClasses); + } + + public function testAuditExtensionsCleanupRules() + { + $sUID = "AuditExtensionsCleanupRules_".uniqid(); + $oOrg = $this->CreateOrganization($sUID); + $this->createObject('FinalClassFeature1Module1MyFinalClassFromLocation', ['org_id' => $oOrg->GetKey(), 'name' => $sUID, 'name2' => uniqid()]); + + $oSetupAudit = new SetupAudit(); + $aRemovedClasses = [ + "Feature1Module1MyClass", + "FinalClassFeature1Module1MyClass", + "FinalClassFeature1Module1MyFinalClassFromLocation", + "FinalClassFeature2Module1MyClass", + "FinalClassFeature2Module1MyFinalClassFromLocation", + ]; + + //avoid setup dry computation + $this->SetNonPublicProperty($oSetupAudit, 'aRemovedClasses', $aRemovedClasses); + + $oRules = $oSetupAudit->AuditExtensionsCleanupRules(); + asort($oRules); + + $expected = [ + "FinalClassFeature1Module1MyFinalClassFromLocation" => 1, + "FinalClassFeature2Module1MyFinalClassFromLocation" => 0, + ]; + + asort($expected); + $this->assertEquals($expected, $oRules); + } + + public function testAuditExtensionsCleanupRulesFailASAP() + { + $sUID = "AuditExtensionsCleanupRules_".uniqid(); + $oOrg = $this->CreateOrganization($sUID); + $this->createObject('FinalClassFeature1Module1MyFinalClassFromLocation', ['org_id' => $oOrg->GetKey(), 'name' => $sUID, 'name2' => uniqid()]); + $this->createObject('FinalClassFeature2Module1MyFinalClassFromLocation', ['org_id' => $oOrg->GetKey(), 'name' => $sUID, 'name2' => uniqid()]); + + $oSetupAudit = new SetupAudit(['nominal_ext1', 'finalclass_ext1', 'finalclass_ext2']); + $aRemovedClasses = [ + "Feature1Module1MyClass", + "FinalClassFeature1Module1MyClass", + "FinalClassFeature1Module1MyFinalClassFromLocation", + "FinalClassFeature2Module1MyClass", + "FinalClassFeature2Module1MyFinalClassFromLocation", + ]; + + //avoid setup dry computation + $this->SetNonPublicProperty($oSetupAudit, 'aRemovedClasses', $aRemovedClasses); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('FinalClassFeature1Module1MyFinalClassFromLocation'); + $oSetupAudit->AuditExtensionsCleanupRules(true); + } +} \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/finalclass_ext1/extension.xml b/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/finalclass_ext1/extension.xml new file mode 100644 index 000000000..492f49319 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/finalclass_ext1/extension.xml @@ -0,0 +1,22 @@ + + + finalclass_ext1 + Combodo SARL + + + + 6.6.6 + + + finalclass_ext1_module1 + tags/6.6.6 + + + 2023-07-19 + + 3.2.0 + + false + + \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/finalclass_ext1/finalclass_ext1_module1/datamodel.finalclass_ext1_module1.xml b/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/finalclass_ext1/finalclass_ext1_module1/datamodel.finalclass_ext1_module1.xml new file mode 100644 index 000000000..9b3461416 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/finalclass_ext1/finalclass_ext1_module1/datamodel.finalclass_ext1_module1.xml @@ -0,0 +1,76 @@ + + + + + + + bizmodel,searchable + false + FinalClassFeature1Module1MyFinalClassFromLocation + + + + + + + + + + + + + + + + + + + + + name2 + + false + + + + + Location + + + + bizmodel,searchable + false + FinalClassFeature1Module1MyClass + + + + + + + + + + + + + + + + + + + name + + false + + + + + + cmdbAbstractObject + + + + + + \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/finalclass_ext1/finalclass_ext1_module1/model.finalclass_ext1_module1.php b/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/finalclass_ext1/finalclass_ext1_module1/model.finalclass_ext1_module1.php new file mode 100644 index 000000000..43179ef20 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/finalclass_ext1/finalclass_ext1_module1/model.finalclass_ext1_module1.php @@ -0,0 +1,15 @@ + 'Ext For Test', + 'category' => 'business', + + // Setup + // + 'dependencies' => array( + 'itop-structure/3.2.0', + ), + 'mandatory' => false, + 'visible' => true, + 'installer' => '', + + // Components + // + 'datamodel' => array( + 'model.finalclass_ext1_module1.php', + ), + 'webservice' => array(), + 'data.struct' => array(// add your 'structure' definition XML files here, + ), + 'data.sample' => array(// add your sample data XML files here, + ), + + // Documentation + // + 'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any + 'doc.more_information' => '', // hyperlink to more information, if any + + // Default settings + // + 'settings' => array(// Module specific settings go here, if any + ), + ) +); \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/finalclass_ext2/extension.xml b/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/finalclass_ext2/extension.xml new file mode 100644 index 000000000..770e595f1 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/finalclass_ext2/extension.xml @@ -0,0 +1,22 @@ + + + finalclass_ext2 + Combodo SARL + + + + 6.6.6 + + + finalclass_ext2_module1 + tags/6.6.6 + + + 2023-07-19 + + 3.2.0 + + false + + \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/finalclass_ext2/finalclass_ext2_module1/datamodel.finalclass_ext2_module1.xml b/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/finalclass_ext2/finalclass_ext2_module1/datamodel.finalclass_ext2_module1.xml new file mode 100644 index 000000000..0fd50aac7 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/finalclass_ext2/finalclass_ext2_module1/datamodel.finalclass_ext2_module1.xml @@ -0,0 +1,76 @@ + + + + + + + bizmodel,searchable + false + FinalClassFeature2Module1MyFinalClassFromLocation + + + + + + + + + + + + + + + + + + + + + name2 + + false + + + + + Location + + + + bizmodel,searchable + false + FinalClassFeature2Module1MyClass + + + + + + + + + + + + + + + + + + + name + + false + + + + + + cmdbAbstractObject + + + + + + \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/finalclass_ext2/finalclass_ext2_module1/model.finalclass_ext2_module1.php b/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/finalclass_ext2/finalclass_ext2_module1/model.finalclass_ext2_module1.php new file mode 100644 index 000000000..43179ef20 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/finalclass_ext2/finalclass_ext2_module1/model.finalclass_ext2_module1.php @@ -0,0 +1,15 @@ + 'Ext For Test', + 'category' => 'business', + + // Setup + // + 'dependencies' => array( + 'itop-structure/3.2.0', + ), + 'mandatory' => false, + 'visible' => true, + 'installer' => '', + + // Components + // + 'datamodel' => array( + 'model.finalclass_ext2_module1.php', + ), + 'webservice' => array(), + 'data.struct' => array(// add your 'structure' definition XML files here, + ), + 'data.sample' => array(// add your sample data XML files here, + ), + + // Documentation + // + 'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any + 'doc.more_information' => '', // hyperlink to more information, if any + + // Default settings + // + 'settings' => array(// Module specific settings go here, if any + ), + ) +); \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/nominal_ext1/extension.xml b/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/nominal_ext1/extension.xml new file mode 100644 index 000000000..6d186e699 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/nominal_ext1/extension.xml @@ -0,0 +1,22 @@ + + + nominal_ext1 + Combodo SARL + + + + 6.6.6 + + + nominal_ext1_module1 + tags/6.6.6 + + + 2023-07-19 + + 3.2.0 + + false + + \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/nominal_ext1/nominal_ext1_module1/datamodel.nominal_ext1_module1.xml b/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/nominal_ext1/nominal_ext1_module1/datamodel.nominal_ext1_module1.xml new file mode 100644 index 000000000..8f3a113db --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/nominal_ext1/nominal_ext1_module1/datamodel.nominal_ext1_module1.xml @@ -0,0 +1,42 @@ + + + + + + + bizmodel,searchable + false + Feature1Module1MyClass + + + + + + + + + + + + + + + + + + + name + + false + + + + + + cmdbAbstractObject + + + + + + \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/nominal_ext1/nominal_ext1_module1/model.nominal_ext1_module1.php b/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/nominal_ext1/nominal_ext1_module1/model.nominal_ext1_module1.php new file mode 100644 index 000000000..43179ef20 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/feature_removal/additional_features/nominal_ext1/nominal_ext1_module1/model.nominal_ext1_module1.php @@ -0,0 +1,15 @@ + 'Ext For Test', + 'category' => 'business', + + // Setup + // + 'dependencies' => array( + 'itop-structure/3.2.0', + ), + 'mandatory' => false, + 'visible' => true, + 'installer' => '', + + // Components + // + 'datamodel' => array( + 'model.nominal_ext1_module1.php', + ), + 'webservice' => array(), + 'data.struct' => array(// add your 'structure' definition XML files here, + ), + 'data.sample' => array(// add your sample data XML files here, + ), + + // Documentation + // + 'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any + 'doc.more_information' => '', // hyperlink to more information, if any + + // Default settings + // + 'settings' => array(// Module specific settings go here, if any + ), + ) +); \ No newline at end of file