Compare commits

..

2 Commits

Author SHA1 Message Date
Benjamin DALSASS
6a0b2bdfc7 N°8860 - Régression Export CSV clé externe, plus de critères 2026-07-16 07:32:50 +02:00
Benjamin DALSASS
a1561fad43 N°8860 - Régression Export CSV clé externe, plus de critères 2026-07-15 09:38:48 +02:00
24 changed files with 58 additions and 365 deletions

View File

@@ -8,6 +8,13 @@
use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
use Combodo\iTop\Application\WebPage\WebPage;
use Combodo\iTop\Core\AttributeDefinition\AttributeExternalField;
use Combodo\iTop\Core\AttributeDefinition\AttributeExternalKey;
use Combodo\iTop\Core\AttributeDefinition\AttributeFriendlyName;
use Combodo\iTop\Core\AttributeDefinition\AttributeHierarchicalKey;
use Combodo\iTop\Core\AttributeDefinition\AttributeLinkedSet;
use Combodo\iTop\Core\AttributeDefinition\AttributeStopWatch;
use Combodo\iTop\Core\AttributeDefinition\AttributeSubItem;
/**
* Bulk export: Tabular export: abstract base class for all "tabular" exports.
@@ -91,8 +98,8 @@ abstract class TabularBulkExport extends BulkExport
{
$aResult = [];
switch (get_class($oAttDef)) {
case 'AttributeExternalKey':
case 'AttributeHierarchicalKey':
case AttributeExternalKey::class:
case AttributeHierarchicalKey::class:
$bAddFriendlyName = true;
$oKeyAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
@@ -142,7 +149,7 @@ abstract class TabularBulkExport extends BulkExport
}
break;
case 'AttributeStopWatch':
case AttributeStopWatch::class:
foreach (MetaModel::ListAttributeDefs($sClass) as $sSubAttCode => $oSubAttDef) {
if ($oSubAttDef instanceof AttributeSubItem) {
if ($oSubAttDef->GetParentAttCode() == $sAttCode) {

View File

@@ -127,9 +127,6 @@ class DataFeatureRemovalController extends Controller
$aRemovedExtensions = $this->aExtensionsToCheck['to_be_removed'];
$aHiddenInputs['removed_extensions'] = $this->ConvertIntoSetupFormat($aRemovedExtensions);
$aExtensionsNotUninstallable = $this->aExtensionsToCheck['extensions_not_uninstallable'];
$aHiddenInputs['extensions_not_uninstallable'] = $this->ConvertIntoSetupFormat($aExtensionsNotUninstallable);
}
$aParams['aAddedExtensions'] = $aAddedExtensions;
@@ -347,7 +344,7 @@ class DataFeatureRemovalController extends Controller
'uninstallable' => $oExtension->CanBeUninstalled(),
'remote' => $oExtension->IsRemote(),
'missing' => $oExtension->bRemovedFromDisk,
'dependency_issue' => $oExtension->HasDependencyIssue(),
'cannot-be-installed' => count($oExtension->aMissingDependencies) > 0,
],
];
@@ -428,7 +425,6 @@ class DataFeatureRemovalController extends Controller
$this->aExtensionsToCheck = [
'to_be_installed' => [],
'to_be_removed' => [],
'extensions_not_uninstallable' => [],
];
foreach ($aAvailableExtensions as $sCode => &$aExtensionData) {
if (!isset($aSelectedExtensionsFromUI[$sCode])) {
@@ -442,9 +438,6 @@ class DataFeatureRemovalController extends Controller
if (! $this->bForcedUninstallation && $aExtensionData['extra_flags']['uninstallable']) {
$this->bForcedUninstallation = true;
}
if (false === $aExtensionData['extra_flags']['uninstallable'] || true === $aExtensionData['extra_flags']['remote']) {
$this->aExtensionsToCheck['extensions_not_uninstallable'][] = $sCode;
}
} elseif (!$aExtensionData['installed'] && $aSelectedExtensionsFromUI[$sCode] === 'on') {
$aExtensionData['extra_flags']['selected'] = true;
$sLabel = $aAvailableExtensions[$sCode]['label'];

View File

@@ -134,6 +134,10 @@ class DataCleanupService
/** @var DBObject $oDependentObj */
while ($oDependentObj = $oSet->Fetch()) {
$iDeletePropagationOption = $oExtKeyAttDef->GetDeletionPropagationOption();
if ($iDeletePropagationOption == DEL_MANUAL) {
$this->oObjectService->SetIssue(get_class($oDependentObj));
continue;
}
if ($oExtKeyAttDef->IsNullAllowed()) {
// Optional external key, list to reset
@@ -148,12 +152,6 @@ class DataCleanupService
return false;
}
} else {
// Mandatory external key
if ($iDeletePropagationOption == DEL_MANUAL) {
// Cannot be deleted automatically, must be handled manually
$this->oObjectService->SetIssue(get_class($oDependentObj));
continue;
}
// Propagate deletion only if not visited
if ($this->IsVisited($oDependentObj)) {
continue;

View File

@@ -59,11 +59,9 @@ class StaticDeletionPlan
{
foreach ($aClasses as $sClass) {
$oDeletionPlanItem = $this->GetInitialClassDeletionPlan($sClass);
// N°9831 Do not overwrite existing entity as it may already exist for this class if a previously processed class references it (issues/updates already accumulated must be kept)
if (false === array_key_exists($sClass, $this->aDeletionPlan)) {
$this->aDeletionPlan[$sClass] = new DeletionPlanEntity();
}
$this->aDeletionPlan[$sClass]->oDelete->Merge($oDeletionPlanItem);
$oDeletionPlanEntity = new DeletionPlanEntity();
$oDeletionPlanEntity->oDelete->Merge($oDeletionPlanItem);
$this->aDeletionPlan[$sClass] = $oDeletionPlanEntity;
$this->DeletionPlanForReferencingClasses($sClass);
}

View File

@@ -159,7 +159,7 @@ function DoInstall(WebPage $oPage)
foreach ($oExtensionsMap->GetAllExtensions() as $oExtension) {
if ($oExtension->sSource == iTopExtension::SOURCE_REMOTE) {
if ($oExtension->HasDependencyIssue()) {
if (count($oExtension->aMissingDependencies) > 0) {
$oPage->add('<div class="choice">');
$oPage->add('<input type="checkbox" disabled>&nbsp;');
$sTitle = Dict::Format('iTopHub:InstallationEffect:MissingDependencies_Details', implode(', ', $oExtension->aMissingDependencies));

View File

@@ -545,7 +545,7 @@ class iTopExtensionsMap
continue;
}
if (! $bKeepExtensionsHavingMissingDependencies && $oExtension->HasDependencyIssue()) {
if (! $bKeepExtensionsHavingMissingDependencies && count($oExtension->aMissingDependencies) > 0) {
//skip extensions with dependency issues
continue;
}
@@ -562,20 +562,19 @@ class iTopExtensionsMap
public function GetAllExtensionsOptionInfo(bool $bRemoteExtensionsShouldBeMandatory = true): array
{
$aRes = [];
foreach ($this->GetAllExtensionsToDisplayInSetup(true, $bRemoteExtensionsShouldBeMandatory) as $sCode => $oExtension) {
foreach ($this->GetAllExtensionsToDisplayInSetup(false, $bRemoteExtensionsShouldBeMandatory) as $sCode => $oExtension) {
$aRes[] = [
'extension_code' => $oExtension->sCode,
'title' => $oExtension->sLabel,
'description' => $oExtension->sDescription,
'more_info' => $oExtension->sMoreInfoUrl,
'default' => true, // by default offer to install all modules
'modules' => $oExtension->aModules,
'mandatory' => $oExtension->bMandatory,
'dependency_issue' => $oExtension->HasDependencyIssue(),
'source_label' => $oExtension->GetExtensionSourceLabel(),
'uninstallable' => $oExtension->CanBeUninstalled(),
'missing' => $oExtension->bRemovedFromDisk,
'version' => $oExtension->sVersion,
'extension_code' => $oExtension->sCode,
'title' => $oExtension->sLabel,
'description' => $oExtension->sDescription,
'more_info' => $oExtension->sMoreInfoUrl,
'default' => true, // by default offer to install all modules
'modules' => $oExtension->aModules,
'mandatory' => $oExtension->bMandatory,
'source_label' => $oExtension->GetExtensionSourceLabel(),
'uninstallable' => $oExtension->CanBeUninstalled(),
'missing' => $oExtension->bRemovedFromDisk,
'version' => $oExtension->sVersion,
];
}

View File

@@ -8,7 +8,6 @@ use DBObjectSet;
use IssueLog;
use MetaModel;
use SetupLog;
use TagSetFieldData;
require_once APPROOT.'setup/feature_removal/ModelReflectionSerializer.php';
@@ -69,10 +68,6 @@ abstract class AbstractSetupAudit
continue;
}
if (MetaModel::IsSameFamily($sClass, TagSetFieldData::class)) {
continue;
}
if (!MetaModel::IsStandaloneClass($sClass)) {
$iCount = $this->Count($sClass);
$this->aFinalClassesToCleanup[$sClass] = $iCount;

View File

@@ -176,7 +176,7 @@ class iTopExtension
};
}
public function IsRemote(): bool
public function IsRemote(): string
{
return $this->sSource === self::SOURCE_REMOTE;
}
@@ -200,9 +200,4 @@ class iTopExtension
{
return $this->sLabel !== '';
}
public function HasDependencyIssue(): bool
{
return count($this->aMissingDependencies) > 0;
}
}

View File

@@ -933,7 +933,6 @@ class RunTimeEnvironment
@chmod($sFinalConfig, 0440); // Read-only for owner and group, nothing for others
SetupUtils::rrmdir(dirname($sBuildConfig)); // Cleanup the temporary build dir if empty
MetaModel::ResetAllCaches($this->sBuildEnv);
MetaModel::ResetAllCaches($this->sFinalEnv);
if (! isset($_SESSION)) {

View File

@@ -1661,14 +1661,11 @@ JS
$aAvailableModules = $oProductionEnv->AnalyzeInstallation($oConfig, $aDirsToScan, $bAbortOnMissingDependency, $aModulesToLoad);
$bRemoteExtensionsShouldBeMandatory = !$oWizard->GetParameter('force-uninstall', false);
if ($bRemoteExtensionsShouldBeMandatory) {
foreach ($aAvailableModules as $key => $aModule) {
$bIsExtra = (array_key_exists('root_dir', $aModule) && (strpos($aModule['root_dir'], $sExtraDir) !== false)); // Some modules (root, datamodel) have no 'root_dir'
if ($bIsExtra) {
// Modules in data/production-modules/ are considered as mandatory and always installed
$aAvailableModules[$key]['visible'] = false;
}
foreach ($aAvailableModules as $key => $aModule) {
$bIsExtra = (array_key_exists('root_dir', $aModule) && (strpos($aModule['root_dir'], $sExtraDir) !== false)); // Some modules (root, datamodel) have no 'root_dir'
if ($bIsExtra) {
// Modules in data/production-modules/ are considered as mandatory and always installed
$aAvailableModules[$key]['visible'] = false;
}
}

View File

@@ -338,7 +338,7 @@ class InstallationFileService
continue;
}
if ($this->CanChooseUnpackageExtension($oExtension)) {
if ($oExtension->bVisible && !$oExtension->HasDependencyIssue()) {
if (($oExtension->bVisible) && (count($oExtension->aMissingDependencies) === 0)) {
$aCurrentModules = [];
$aUnselectableModules = [];
$bIsExtensionSelectable = true;

View File

@@ -112,22 +112,6 @@ JS);
return;
}
// When the setup reach this step, it already checked whether extensions were uninstallable (during WizStepModulesChoice or combodo-data-feature-removal). We only need to log what has been done.
if ($this->oWizard->GetParameter('force-uninstall', false)) {
SetupLog::Warning("User disabled uninstallation checks");
}
$aExtensionsRemoved = json_decode($this->oWizard->GetParameter('removed_extensions'), true) ?? [];
$aExtensionsNotUninstallable = json_decode($this->oWizard->GetParameter('extensions_not_uninstallable'), true) ?? [];
$aExtensionsForceUninstalled = [];
foreach ($aExtensionsRemoved as $sExtensionCode => $sLabel) {
if (in_array($sExtensionCode, $aExtensionsNotUninstallable)) {
$aExtensionsForceUninstalled[] = $sExtensionCode;
}
}
if (count($aExtensionsForceUninstalled)) {
SetupLog::Warning("Extensions uninstalled forcefully : ".implode(',', $aExtensionsForceUninstalled));
}
$oPage->add_ready_script(<<<JS
$("#wiz_form").data("installation_status", "not started");
ExecuteStep("");

View File

@@ -31,8 +31,8 @@ class WizStepLandingBeforeAudit extends WizStepModulesChoice
$oWizard->SetParameter('db_tls_enabled', $this->oConfig->Get('db_tls.enabled'));
$oWizard->SetParameter('db_tls_ca', $this->oConfig->Get('db_tls.ca') ?? '');
$oWizard->SetParameter('display_choices', '');
$oWizard->SetParameter('extensions_not_uninstallable', '[]');
$oWizard->SaveParameter('extensions_not_uninstallable', '[]');
$oWizard->SaveParameter('use_symbolic_links', MFCompiler::UseSymbolicLinks());
$oWizard->SaveParameter('force-uninstall', false);
$oWizard->SaveParameter('skip_wizard', false);

View File

@@ -587,7 +587,7 @@ EOF
break;
}
}
if ((isset($aChoice['mandatory']) && $aChoice['mandatory'] && (!isset($aChoice['dependency_issue']) || !$aChoice['dependency_issue'])) ||
if ((isset($aChoice['mandatory']) && $aChoice['mandatory']) ||
(isset($aSelectedChoices[$sChoiceId]) && ($aSelectedChoices[$sChoiceId] == $sChoiceId))) {
$sDisplayChoices .= '<li>'.$aChoice['title'].'</li>';
if (isset($aChoice['modules'])) {
@@ -637,7 +637,7 @@ EOF
if ($sChoiceName == null) {
$sChoiceName = $sChoiceId;
}
if ((isset($aChoice['mandatory']) && $aChoice['mandatory'] && (!isset($aChoice['dependency_issue']) || !$aChoice['dependency_issue'])) ||
if ((isset($aChoice['mandatory']) && $aChoice['mandatory']) ||
(isset($aSelectedChoices[$sChoiceName]) && ($aSelectedChoices[$sChoiceName] == $sChoiceId))) {
$sDisplayChoices .= '<li>'.$aChoice['title'].'</li>';
if ($aSelectedExtensions !== null) {
@@ -750,17 +750,13 @@ EOF
$bSelected = isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] === $sChoiceId);
$bMissingFromDisk = isset($aChoice['missing']) && $aChoice['missing'] === true;
$bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']);
$bInstalled = $bMissingFromDisk || $oITopExtension?->bInstalled ?? false;
$bDependencyIssue = $oITopExtension?->HasDependencyIssue() ?? false;
$bInstalled = $bMissingFromDisk || $oITopExtension->bInstalled;
$bChecked = $bSelected;
$bDisabled = false;
if ($bMissingFromDisk) {
$bDisabled = true;
$bChecked = false;
} elseif ($bDependencyIssue) {
$bDisabled = true;
$bChecked = false;
} elseif ($bMandatory) {
$bDisabled = true;
$bChecked = true;
@@ -791,8 +787,6 @@ EOF
return [
'uninstallable' => $bCanBeUninstalled,
'dependency_issue' => $bDependencyIssue,
'mandatory' => $bMandatory,
'missing' => $bMissingFromDisk,
'installed' => $bInstalled,
'disabled' => $bDisabled,
@@ -811,7 +805,7 @@ EOF
$sChoiceId = $sParentId.self::$SEP.$index;
$aFlags = $this->ComputeChoiceFlags($aChoice, $sChoiceId, $aSelectedComponents, $bAllDisabled, $bDisableUninstallCheck, $this->bUpgrade);
if ($aFlags['disabled'] && !$aFlags['checked'] && !$bDisableUninstallCheck && (!$aFlags['uninstallable'] || $aFlags['mandatory'])) {
if ($aFlags['disabled'] && !$aFlags['checked'] && !$aFlags['uninstallable'] && !$bDisableUninstallCheck) {
$this->bCanMoveForward = false;//Disable "Next"
}
@@ -888,9 +882,6 @@ EOF
if (!$aFlags['uninstallable']) {
$sTooltip .= '<div id="badge--'.$sId.'--not-uninstallable" class="ibo-badge ibo-block ibo-is-yellow" title="Once this extension has been installed, it should not be uninstalled." >cannot be uninstalled</div>';
}
if ($aFlags['dependency_issue']) {
$sTooltip .= '<div id="badge--'.$sId.'--cannot-be-installed" class="ibo-badge ibo-block ibo-is-orange" title="This extension cannot be installed because one or more dependencies are not satisfied." >cannot be installed</div>';
}
$sMetadata = '';
if (isset($aChoice['version']) && isset($aChoice['source_label'])) {
@@ -948,6 +939,10 @@ EOF
public function GetNextButtonLabel()
{
if (!$this->bCanMoveForward) {
return 'Non-uninstallable extension missing';
}
if ($this->GetStepInfo(1 + $this->GetStepIndex()) === null) {
return 'Check compatibility';
}

View File

@@ -116,7 +116,7 @@ class WizStepSummary extends AbstractWizStepInstall
$oPage->add('<div class="closed"><a class="title ibo-setup-summary-title" href="#" aria-label="Extensions to be uninstalled">Extensions to be uninstalled</a>');
$aExtensionsRemoved = json_decode($this->oWizard->GetParameter('removed_extensions'), true) ?? [];
$aExtensionsNotUninstallable = json_decode($this->oWizard->GetParameter('extensions_not_uninstallable'), true) ?? [];
$aExtensionsNotUninstallable = json_decode($this->oWizard->GetParameter('extensions_not_uninstallable')) ?? [];
if (count($aExtensionsRemoved) > 0) {
$sExtensionsRemoved = '<ul>';
foreach ($aExtensionsRemoved as $sExtensionCode => $sLabel) {

View File

@@ -26,11 +26,11 @@ class ExtensionDetailsUIBlockFactory extends AbstractUIBlockFactory
$aBadges = [];
$bUninstallable = $aExtraFlags['uninstallable'] ?? true;
$bMissingFromDisk = $aExtraFlags['missing'] ?? false;
$bDependencyIssue = $aExtraFlags['dependency_issue'] ?? false;
$bCannotBeInstalled = $aExtraFlags['cannot-be-installed'] ?? false;
$bSelected = $aExtraFlags['selected'] ?? true;
$bDisabled = $aExtraFlags['disabled'] ?? false;
$bRemote = $aExtraFlags['remote'] ?? false;
self::AddExtraBadges($aBadges, $bUninstallable, $bMissingFromDisk, $bDependencyIssue, $sCode);
self::AddExtraBadges($aBadges, $bUninstallable, $bMissingFromDisk, $bCannotBeInstalled, $sCode);
$oBadgeInstalled = BadgeUIBlockFactory::MakeGreen(
Dict::S('UI:Layout:ExtensionsDetails:BadgeInstalled'),
Dict::S('UI:Layout:ExtensionsDetails:BadgeInstalled+'),
@@ -48,7 +48,7 @@ class ExtensionDetailsUIBlockFactory extends AbstractUIBlockFactory
$oExtensionDetails = new ExtensionDetails($sCode, $sLabel, $sDescription, $aMetaData, $aBadges, $sAbout);
$oExtensionDetails->GetToggler()->SetIsToggled(true);
if ($bMissingFromDisk || $bDependencyIssue) {
if ($bMissingFromDisk || $bCannotBeInstalled) {
$oExtensionDetails->GetToggler()->SetIsToggled(false);
$oExtensionDetails->GetToggler()->SetIsDisabled(true);
} elseif ((!$bUninstallable || $bRemote) && !$bDisabled) {
@@ -71,10 +71,10 @@ class ExtensionDetailsUIBlockFactory extends AbstractUIBlockFactory
{
$aBadges = [];
$bUninstallable = $aExtraFlags['uninstallable'] ?? true;
$bDependencyIssue = $aExtraFlags['dependency_issue'] ?? false;
$bCannotBeInstalled = $aExtraFlags['cannot-be-installed'] ?? false;
$bSelected = $aExtraFlags['selected'] ?? false;
$bDisabled = $aExtraFlags['disabled'] ?? false;
self::AddExtraBadges($aBadges, $bUninstallable, false, $bDependencyIssue, $sCode);
self::AddExtraBadges($aBadges, $bUninstallable, false, $bCannotBeInstalled, $sCode);
$oBadgeInstalled = BadgeUIBlockFactory::MakeGrey(
Dict::S('UI:Layout:ExtensionsDetails:BadgeNotInstalled'),
Dict::S('UI:Layout:ExtensionsDetails:BadgeNotInstalled+'),
@@ -91,7 +91,7 @@ class ExtensionDetailsUIBlockFactory extends AbstractUIBlockFactory
$aBadges[] = $oBadgeToBeUninstalled;
$oExtensionDetails = new ExtensionDetails($sCode, $sLabel, $sDescription, $aMetaData, $aBadges, $sAbout);
if ($bDependencyIssue) {
if ($bCannotBeInstalled) {
$oExtensionDetails->GetToggler()->SetIsToggled(false);
$oExtensionDetails->GetToggler()->SetIsDisabled(true);
}
@@ -107,7 +107,7 @@ class ExtensionDetailsUIBlockFactory extends AbstractUIBlockFactory
return $oExtensionDetails;
}
private static function AddExtraBadges(array &$aBadges, bool $bUninstallable, bool $bMissingFromDisk, bool $bDependencyIssue, string $sCode)
private static function AddExtraBadges(array &$aBadges, bool $bUninstallable, bool $bMissingFromDisk, bool $bCannotBeInstalled, string $sCode)
{
if (!$bUninstallable) {
$aBadges[] = BadgeUIBlockFactory::MakeYellow(
@@ -123,7 +123,7 @@ class ExtensionDetailsUIBlockFactory extends AbstractUIBlockFactory
self::GetBadgeId($sCode, self::BADGE_ID_MISSING_FROM_DISK)
);
}
if ($bDependencyIssue) {
if ($bCannotBeInstalled) {
$aBadges[] = BadgeUIBlockFactory::MakeOrange(
Dict::S('UI:Layout:ExtensionsDetails:BadgeCannotBeInstalled'),
Dict::S('UI:Layout:ExtensionsDetails:BadgeCannotBeInstalled+'),

View File

@@ -163,58 +163,6 @@ class DataCleanupServiceTest extends \AbstractCleanup
$this->AssertSummaryEquals($aExpected, $aRes);
}
/**
* N°9831
*
* A nullable external key configured as DEL_MANUAL must NOT be treated as an issue: it is simply
* reset (set to null), exactly like core DBObject::MakeDeletionPlan does. DEL_MANUAL only blocks
* for mandatory keys. This mirrors the real "Hypervisor.farm_id -> Farm" case (nullable + DEL_MANUAL).
*
* Regression guard for the ordering of the IsNullAllowed()/DEL_MANUAL checks in RecursiveDeletion:
* reverting that change would make ExecuteCleanup throw here instead of resetting the object.
*/
public function testExecuteCleanup_NullableManualShouldResetAndNotThrow()
{
$this->GivenDFRTreeInDB(<<<EOF
DFRToRemoveLeaf_1 <- DFRManualNullable_1
EOF);
$aClasses = [ 'DFRToRemoveLeaf' ];
$oService = new DataCleanupService();
// Must NOT throw: the nullable external key is reset, not reported as an issue
$aRes = $oService->ExecuteCleanup($aClasses);
$aExpected = [
['DFRManualNullable', 1, 0 ],
['DFRToRemoveLeaf', 0, 1 ],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
/**
* N°9831
*
* Summary counterpart of the test above: a nullable DEL_MANUAL external key is reported as an
* update (reset), with an issue count of 0. Reverting the RecursiveDeletion change would report
* it as an issue instead.
*/
public function testGetCleanupSummary_NullableManualShouldBeUpdateNotIssue()
{
$this->GivenDFRTreeInDB(<<<EOF
DFRToRemoveLeaf_1 <- DFRManualNullable_1
EOF);
$aClasses = [ 'DFRToRemoveLeaf' ];
$oService = new DataCleanupService();
$aRes = $oService->GetCleanupSummary($aClasses);
$aExpected = [
['DFRManualNullable', 1, 0, 0 ],
['DFRToRemoveLeaf', 0, 1, 0 ],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
private function AssertSummaryEquals(array $expected, $actual, $sMessage = '')
{
$aExpected = [];

View File

@@ -9,6 +9,7 @@ namespace Combodo\iTop\Test\UnitTest\Module\DataFeatureRemoval;
use Combodo\iTop\DataFeatureRemoval\Entity\DataCleanupSummaryEntity;
use Combodo\iTop\DataFeatureRemoval\Service\StaticDeletionPlan;
use MetaModel;
require_once __DIR__."/AbstractCleanup.php";
class StaticDeletionPlanTest extends \AbstractCleanup
@@ -149,66 +150,6 @@ EOF);
$this->AssertSummaryEquals($aExpected, $aRes);
}
/**
* N°9831
*
* A DEL_MANUAL issue must still be reported when the referencing class (DFRManual) is itself part
* of the classes to remove AND is processed AFTER the class it points to (DFRToRemoveLeaf).
*
* This mirrors the real "VirtualMachine -> VirtualHost" case: the setup audit sorts the removed
* classes alphabetically and drops abstract classes, so a referencing class such as VirtualMachine
* ends up processed after its concrete targets (Farm/Hypervisor/Cloud). Here DFRToRemove is abstract
* and DFRToRemoveLeaf is its concrete child, while DFRManual points to DFRToRemove with a mandatory
* DEL_MANUAL external key.
*
* Regression: the plan entity accumulating the issue used to be overwritten when the referencing
* class was processed as a top-level class, silently dropping the issue (no blocking message on the
* summary page, then a crash at execution time).
*/
public function testGetCleanupSummary_IssueKeptWhenReferencingClassListedAfterTarget(): void
{
$this->GivenDFRObjectsInDB(<<<EOF
create DFRToRemoveLeaf (name = DFRToRemoveLeaf_1)
create DFRManual (name = DFRManual_1, extkey_id = DFRToRemoveLeaf_1)
EOF);
// Target listed BEFORE the referencing class, as produced by the alphabetically sorted audit
$aClasses = ['DFRToRemoveLeaf', 'DFRManual'];
$oService = new StaticDeletionPlan();
$aRes = $oService->GetCleanupSummary($aClasses);
$aExpected = [
'DFRToRemoveLeaf' => ['iDeleteCount' => 1],
'DFRManual' => ['iDeleteCount' => 1, 'iIssueCount' => 1],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
/**
* N°9831
*
* Control case: the same issue must also be reported when the referencing class is listed BEFORE
* its target (this order already worked, e.g. "Enclosure -> Rack"). Together with the test above,
* this locks in that issue detection is independent of the processing order.
*/
public function testGetCleanupSummary_IssueKeptWhenReferencingClassListedBeforeTarget(): void
{
$this->GivenDFRObjectsInDB(<<<EOF
create DFRToRemoveLeaf (name = DFRToRemoveLeaf_1)
create DFRManual (name = DFRManual_1, extkey_id = DFRToRemoveLeaf_1)
EOF);
$aClasses = ['DFRManual', 'DFRToRemoveLeaf'];
$oService = new StaticDeletionPlan();
$aRes = $oService->GetCleanupSummary($aClasses);
$aExpected = [
'DFRToRemoveLeaf' => ['iDeleteCount' => 1],
'DFRManual' => ['iDeleteCount' => 1, 'iIssueCount' => 1],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
public function testCircularRefsShouldNotRunInfinitely()
{
$this->GivenDFRObjectsInDB(<<<EOF

View File

@@ -319,58 +319,6 @@
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
<class id="DFRManualNullable" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrmanualnullable</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
<field id="extkey_id" xsi:type="AttributeExternalKey">
<sql>extkey_id</sql>
<filter/>
<dependencies/>
<is_null_allowed>true</is_null_allowed>
<target_class>DFRToRemove</target_class>
<on_target_delete>DEL_MANUAL</on_target_delete>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="extkey_id">
<rank>20</rank>
</item>
</items>
</details>
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
<class id="DFRLeafNotToRemove" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
@@ -770,14 +718,6 @@
<entry id="Class:DFRManual/Attribute:name+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManual/Attribute:extkey_id" _delta="define"><![CDATA[Dfrtoremove id]]></entry>
<entry id="Class:DFRManual/Attribute:extkey_id+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManualNullable/Name" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManualNullable/ComplementaryName" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManualNullable" _delta="define"><![CDATA[DFRManualNullable]]></entry>
<entry id="Class:DFRManualNullable+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManualNullable/Attribute:name" _delta="define"><![CDATA[Name]]></entry>
<entry id="Class:DFRManualNullable/Attribute:name+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManualNullable/Attribute:extkey_id" _delta="define"><![CDATA[Dfrtoremove id]]></entry>
<entry id="Class:DFRManualNullable/Attribute:extkey_id+" _delta="define"><![CDATA[]]></entry>
</entries>
</dictionary>
</dictionaries>

View File

@@ -49,8 +49,6 @@ class WizStepModulesChoiceTest extends ItopTestCase
'installed' => false,
'disabled' => false,
'checked' => false,
'dependency_issue' => false,
'mandatory' => false,
],
],
'A selected but not installed extension should be checked and enabled' => [
@@ -72,8 +70,6 @@ class WizStepModulesChoiceTest extends ItopTestCase
'installed' => false,
'disabled' => false,
'checked' => true,
'dependency_issue' => false,
'mandatory' => false,
],
],
'A missing extension should be disabled and unchecked' => [
@@ -93,8 +89,6 @@ class WizStepModulesChoiceTest extends ItopTestCase
'installed' => true,
'disabled' => true,
'checked' => false,
'dependency_issue' => false,
'mandatory' => false,
],
],
'A missing extension should always be disabled and unchecked, even when mandatory' => [
@@ -114,8 +108,6 @@ class WizStepModulesChoiceTest extends ItopTestCase
'installed' => true,
'disabled' => true,
'checked' => false,
'dependency_issue' => false,
'mandatory' => true,
],
],
'A missing extension should always be disabled and unchecked, even when non-uninstallable' => [
@@ -135,8 +127,6 @@ class WizStepModulesChoiceTest extends ItopTestCase
'installed' => true,
'disabled' => true,
'checked' => false,
'dependency_issue' => false,
'mandatory' => true,
],
],
'An installed but not selected extension should not be checked and be enabled' => [
@@ -158,8 +148,6 @@ class WizStepModulesChoiceTest extends ItopTestCase
'installed' => true,
'disabled' => false,
'checked' => false,
'dependency_issue' => false,
'mandatory' => false,
],
],
'An installed non uninstallable extension should be checked and disabled' => [
@@ -181,8 +169,6 @@ class WizStepModulesChoiceTest extends ItopTestCase
'installed' => true,
'disabled' => true,
'checked' => true,
'dependency_issue' => false,
'mandatory' => false,
],
],
'An installed non uninstallable extension should be enabled if the "disable uninstallation check" flag is set' => [
@@ -204,8 +190,6 @@ class WizStepModulesChoiceTest extends ItopTestCase
'installed' => true,
'disabled' => false,
'checked' => true,
'dependency_issue' => false,
'mandatory' => false,
],
],
'A mandatory extension should be checked and disabled' => [
@@ -227,8 +211,6 @@ class WizStepModulesChoiceTest extends ItopTestCase
'installed' => false,
'disabled' => true,
'checked' => true,
'dependency_issue' => false,
'mandatory' => true,
],
],
'A mandatory extension should be checked and disabled even if the "disable uninstallation check" flag is set' => [
@@ -250,8 +232,6 @@ class WizStepModulesChoiceTest extends ItopTestCase
'installed' => false,
'disabled' => true,
'checked' => true,
'dependency_issue' => false,
'mandatory' => true,
],
],
'An optional sub extension should not force its parent flags' => [
@@ -285,8 +265,6 @@ class WizStepModulesChoiceTest extends ItopTestCase
'installed' => false,
'disabled' => false,
'checked' => false,
'dependency_issue' => false,
'mandatory' => false,
],
],
'A mandatory sub extension should force its parent to be checked and disabled' => [
@@ -320,8 +298,6 @@ class WizStepModulesChoiceTest extends ItopTestCase
'installed' => false,
'disabled' => true,
'checked' => true,
'dependency_issue' => false,
'mandatory' => false,
],
],
'An installed non uninstallable sub extension should force its parent to be checked and disabled' => [
@@ -355,8 +331,6 @@ class WizStepModulesChoiceTest extends ItopTestCase
'installed' => true,
'disabled' => true,
'checked' => true,
'dependency_issue' => false,
'mandatory' => false,
],
],
'A non installed non uninstallable sub extension should not force its parent flags' => [
@@ -390,62 +364,6 @@ class WizStepModulesChoiceTest extends ItopTestCase
'installed' => true,
'disabled' => false,
'checked' => false,
'dependency_issue' => false,
'mandatory' => false,
],
],
'A non installed extension with missing dependencies should be not checked and disabled' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
'installed' => false,
'missing_dependencies' => [
'itop-ext1-1',
],
],
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
'missing_dependencies' => true,
],
'bCurrentSelected' => false,
'bDisableUninstallChecks' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => false,
'disabled' => true,
'checked' => false,
'dependency_issue' => true,
'mandatory' => false,
],
],
'An installed extension with missing dependencies should be not checked and disabled' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
'installed' => true,
'missing_dependencies' => [
'itop-ext1-1',
],
],
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
'missing_dependencies' => true,
],
'bCurrentSelected' => false,
'bDisableUninstallChecks' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => true,
'disabled' => true,
'checked' => false,
'dependency_issue' => true,
'mandatory' => false,
],
],
];

View File

@@ -79,7 +79,6 @@ class SetupAuditTest extends ItopCustomDatamodelTestCase
"Feature1Module1MyClass",
"FinalClassFeature2Module1MyClass",
"FinalClassFeature2Module1MyFinalClassFromLocation",
"TagSetFieldDataFor_FinalClassFeature2Module1MyFinalClassFromLocation__domains",
];
$this->assertEqualsCanonicalizing($expected, $oSetupAudit->GetRemovedClasses());

View File

@@ -64,12 +64,6 @@
<is_null_allowed>false</is_null_allowed>
<validation_pattern/>
</field>
<field id="domains" xsi:type="AttributeTagSet">
<sql>domains</sql>
<is_null_allowed>true</is_null_allowed>
<tracking_level>all</tracking_level>
<max_items>12</max_items>
</field>
</fields>
<methods/>
<presentation/>

View File

@@ -31,12 +31,6 @@
<default_value/>
<is_null_allowed>false</is_null_allowed>
</field>
<field id="domains" xsi:type="AttributeTagSet">
<sql>domains</sql>
<is_null_allowed>true</is_null_allowed>
<tracking_level>all</tracking_level>
<max_items>12</max_items>
</field>
</fields>
<methods/>
<presentation/>

View File

@@ -23,7 +23,6 @@ class iTopExtensionsMapFake extends iTopExtensionsMap
$oExtension->sVersion = $aExtension['version'] ?? '1.0.0';
$oExtension->sSource = $aExtension['source'] ?? iTopExtension::SOURCE_MANUAL;
$oExtension->aModuleInfo = $aExtension['module_info'] ?? [];
$oExtension->aMissingDependencies = $aExtension['missing_dependencies'] ?? [];
$oMap->AddExtension($oExtension);
}
return $oMap;