N°6974 - Flatten classes in datamodel - Allow _delta="merge" in strict mode

This commit is contained in:
Eric Espie
2024-03-22 10:11:25 +01:00
parent 3653a4bc90
commit 90458f6048
3 changed files with 201 additions and 52 deletions

View File

@@ -339,6 +339,26 @@ class DesignElement extends \DOMElement
return false; return false;
} }
/**
* True if the node is contained in a _delta="merge" tree
* @return bool
*/
public function IsInSpecifiedMerge(): bool
{
// Iterate through the parents: reset the flag if any of them has a flag set
for ($oParent = $this; $oParent instanceof MFElement; $oParent = $oParent->parentNode) {
$sDeltaSpec = $oParent->getAttribute('_delta');
if ($sDeltaSpec === 'merge') {
return true;
}
if (in_array($sDeltaSpec, ['define', 'define_if_not_exists', 'force', 'redefine'])) {
return false;
}
}
return false;
}
/** /**
* Find the child node matching the given node. * Find the child node matching the given node.
* UNSAFE: may return nodes marked as _alteration="removed" * UNSAFE: may return nodes marked as _alteration="removed"

View File

@@ -832,6 +832,7 @@ class ModelFactory
case '': case '':
$bMustExist = ($sDeltaSpec === 'must_exist'); $bMustExist = ($sDeltaSpec === 'must_exist');
$bIfExists = ($sDeltaSpec === 'if_exists'); $bIfExists = ($sDeltaSpec === 'if_exists');
$bSpecifiedMerge = $oSourceNode->IsInSpecifiedMerge();
/** @var MFElement $oTargetNode */ /** @var MFElement $oTargetNode */
$oTargetNode = $oTargetParentNode->_FindChildNode($oSourceNode, $sSearchId); $oTargetNode = $oTargetParentNode->_FindChildNode($oSourceNode, $sSearchId);
@@ -847,7 +848,7 @@ class ModelFactory
// Do not continue deeper // Do not continue deeper
$oTargetNode = null; $oTargetNode = null;
} else { } else {
if ($sMode === self::LOAD_DELTA_MODE_STRICT && ($sSearchId !== '' || is_null($oSourceNode->firstElementChild))) { if (!$bSpecifiedMerge && $sMode === self::LOAD_DELTA_MODE_STRICT && ($sSearchId !== '' || is_null($oSourceNode->firstElementChild))) {
$iLine = ModelFactory::GetXMLLineNumber($oSourceNode); $iLine = ModelFactory::GetXMLLineNumber($oSourceNode);
$sItopNodePath = DesignDocument::GetItopNodePath($oSourceNode); $sItopNodePath = DesignDocument::GetItopNodePath($oSourceNode);
throw new MFException($sItopNodePath.' at line '.$iLine.': could not be found or marked as removed (strict mode)', throw new MFException($sItopNodePath.' at line '.$iLine.': could not be found or marked as removed (strict mode)',
@@ -867,12 +868,15 @@ class ModelFactory
// Do not continue deeper everything is already copied // Do not continue deeper everything is already copied
$oTargetNode = null; $oTargetNode = null;
} else { } else {
// copy the node with attributes and continue deeper // copy the node with attributes (except _delta) and continue deeper
$oTargetNode = $oTargetDocument->importNode($oSourceNode, false); $oTargetNode = $oTargetDocument->importNode($oSourceNode, false);
foreach ($oSourceNode->attributes as $oAttributeNode) { foreach ($oSourceNode->attributes as $oAttributeNode) {
$oTargetNode->setAttribute($oAttributeNode->name, $oAttributeNode->value); $oTargetNode->setAttribute($oAttributeNode->name, $oAttributeNode->value);
} }
if ($sSearchId !== '') { if ($oTargetNode->hasAttribute('_delta')) {
$oTargetNode->removeAttribute('_delta');
}
if ($sSearchId !== '' || $bSpecifiedMerge) {
// Add the node by default // Add the node by default
$oTargetParentNode->AddChildNode($oTargetNode); $oTargetParentNode->AddChildNode($oTargetNode);
} else { } else {

View File

@@ -75,10 +75,12 @@ class ModelFactoryTest extends ItopTestCase
// Canonicalize the expected XML (to cope with indentation) // Canonicalize the expected XML (to cope with indentation)
$oExpectedDocument = new DOMDocument(); $oExpectedDocument = new DOMDocument();
$oExpectedDocument->preserveWhiteSpace = false; $oExpectedDocument->preserveWhiteSpace = false;
$oExpectedDocument->loadXML($sXML);
$oExpectedDocument->formatOutput = true; $oExpectedDocument->formatOutput = true;
$oExpectedDocument->loadXML($sXML);
return $oExpectedDocument->C14N(false, true); $sSavedXML = $oExpectedDocument->SaveXML();
return str_replace(' encoding="UTF-8"', '', $sSavedXML);
} }
/** /**
@@ -665,7 +667,7 @@ class ModelFactoryTest extends ItopTestCase
$oFactory = $this->MakeVanillaModelFactory($sInitialXML); $oFactory = $this->MakeVanillaModelFactory($sInitialXML);
$oFactoryDocument = $this->GetNonPublicProperty($oFactory, 'oDOMDocument'); $oFactoryDocument = $this->GetNonPublicProperty($oFactory, 'oDOMDocument');
$sExpectedXML = null; $sExpectedXML = null;
if (\utils::StartsWith($sExpectedXMLOrErrorMessage, '<')) { if (\utils::StartsWith(trim($sExpectedXMLOrErrorMessage), '<')) {
$sExpectedXML = $sExpectedXMLOrErrorMessage; $sExpectedXML = $sExpectedXMLOrErrorMessage;
} }
@@ -3026,14 +3028,16 @@ XML
, ,
], ],
'Conditionally deleted class' => [ 'Conditionally deleted class' => [
'sInitialXMLInternal' => '<itop_design> 'sInitialXMLInternal' => '
<itop_design>
<classes> <classes>
<class id="cmdbAbstractObject"/> <class id="cmdbAbstractObject"/>
<class id="C_1_1" _alteration="removed"/> <class id="C_1_1" _alteration="removed"/>
<class id="C_1" _alteration="remove_needed"/> <class id="C_1" _alteration="remove_needed"/>
</classes> </classes>
</itop_design>', </itop_design>',
'sExpectedXMLDelta' => '<itop_design> 'sExpectedXMLDelta' => '
<itop_design>
<classes> <classes>
<class id="C_1_1" _delta="delete"/> <class id="C_1_1" _delta="delete"/>
<class id="C_1" _delta="delete_if_exists"/> <class id="C_1" _delta="delete_if_exists"/>
@@ -3411,15 +3415,21 @@ XML
$oDocument->loadXML($sDeltaXML); $oDocument->loadXML($sDeltaXML);
/* @var MFElement $oDeltaRoot */ /* @var MFElement $oDeltaRoot */
$oDeltaRoot = $oDocument->firstChild; $oDeltaRoot = $oDocument->firstChild;
$sExpectedXML = null;
if (\utils::StartsWith(trim($sExpectedXMLInLaxMode), '<')) {
$sExpectedXML = $sExpectedXMLInLaxMode;
}
try { try {
$oFactory->LoadDelta($oDeltaRoot, $oFactoryDocument, ModelFactory::LOAD_DELTA_MODE_LAX); $oFactory->LoadDelta($oDeltaRoot, $oFactoryDocument, ModelFactory::LOAD_DELTA_MODE_LAX);
$this->AssertEqualModels($sExpectedXMLInLaxMode, $oFactory, 'LoadDelta(lax) did not produce the expected result'); $this->assertNotNull($sExpectedXML, "LoadDelta(lax) should have failed with exception: $sExpectedXMLInLaxMode");
$this->AssertEqualModels($sExpectedXML, $oFactory, 'LoadDelta(lax) did not produce the expected result');
} }
catch (ExpectationFailedException $e) { catch (ExpectationFailedException $e) {
throw $e; throw $e;
} }
catch (\Exception $e) { catch (\Exception $e) {
$this->assertNull($sExpectedXMLInLaxMode, 'LoadDelta(lax) should not have failed with exception: '.$e->getMessage()); $this->assertNull($sExpectedXML, 'LoadDelta(lax) should not have failed with exception: '.$e->getMessage());
$this->assertEquals($sExpectedXMLInLaxMode, $e->getMessage());
} }
// Load in Strict mode // Load in Strict mode
@@ -3430,118 +3440,233 @@ XML
$oDocument->loadXML($sDeltaXML); $oDocument->loadXML($sDeltaXML);
/* @var MFElement $oDeltaRoot */ /* @var MFElement $oDeltaRoot */
$oDeltaRoot = $oDocument->firstChild; $oDeltaRoot = $oDocument->firstChild;
$sExpectedXML = null;
if (\utils::StartsWith(trim($sExpectedXMLInStrictMode), '<')) {
$sExpectedXML = $sExpectedXMLInStrictMode;
}
try { try {
$oFactory->LoadDelta($oDeltaRoot, $oFactoryDocument, ModelFactory::LOAD_DELTA_MODE_STRICT); $oFactory->LoadDelta($oDeltaRoot, $oFactoryDocument, ModelFactory::LOAD_DELTA_MODE_STRICT);
$this->AssertEqualModels($sExpectedXMLInStrictMode, $oFactory, 'LoadDelta(strict) did not produce the expected result'); $this->assertNotNull($sExpectedXML, "LoadDelta(lax) should have failed with exception: $sExpectedXMLInStrictMode");
$this->AssertEqualModels($sExpectedXML, $oFactory, 'LoadDelta(strict) did not produce the expected result');
} }
catch (ExpectationFailedException $e) { catch (ExpectationFailedException $e) {
throw $e; throw $e;
} }
catch (\Exception $e) { catch (\Exception $e) {
$this->assertNull($sExpectedXMLInStrictMode, 'LoadDelta(strict) should not have failed with exception: '.$e->getMessage()); $this->assertNull($sExpectedXML, 'LoadDelta(strict) should not have failed with exception: '.$e->getMessage());
$this->assertEquals($sExpectedXMLInStrictMode, $e->getMessage());
} }
} }
public function LoadDeltaModeProvider() public function LoadDeltaModeProvider()
{ {
return [ return [
'merge delta have different behavior depending on the mode' => [ 'default no _delta have different behavior depending on the mode' => [
'sInitialXML' => ' 'sInitialXML' => '
<itop_design> <itop_design>
<nodeA> <nodeA/>
</nodeA>
</itop_design>', </itop_design>',
'sDeltaXML' => '<itop_design> 'sDeltaXML' => '
<itop_design>
<nodeA> <nodeA>
<nodeB id="C_1"> <nodeB id="C_1">
<parent>cmdbAbstractObject</parent> <parent>cmdbAbstractObject</parent>
</nodeB> </nodeB>
</nodeA> </nodeA>
</itop_design>', </itop_design>',
'sExpectedXMLInLaxMode' => '<itop_design> 'sExpectedXMLInLaxMode' => '
<itop_design>
<nodeA> <nodeA>
<nodeB id="C_1" _alteration="added"> <nodeB id="C_1" _alteration="added">
<parent>cmdbAbstractObject</parent> <parent>cmdbAbstractObject</parent>
</nodeB> </nodeB>
</nodeA> </nodeA>
</itop_design>', </itop_design>',
'sExpectedXMLInStrictMode' => null, 'sExpectedXMLInStrictMode' => '/itop_design/nodeA/nodeB[C_1] at line 4: could not be found or marked as removed (strict mode)',
], ],
'mode specified in delta takes precedence' => [ 'mode specified in delta takes precedence' => [
'sInitialXML' => ' 'sInitialXML' => '
<itop_design> <itop_design>
<nodeA> <nodeA/>
</nodeA>
</itop_design>', </itop_design>',
'sDeltaXML' => '<itop_design load="strict"> 'sDeltaXML' => '
<itop_design load="strict">
<nodeA> <nodeA>
<nodeB id="C_1"> <nodeB id="C_1">
<parent>cmdbAbstractObject</parent> <parent>cmdbAbstractObject</parent>
</nodeB> </nodeB>
</nodeA> </nodeA>
</itop_design>', </itop_design>',
'sExpectedXMLInLaxMode' => null, 'sExpectedXMLInLaxMode' => '/itop_design/nodeA/nodeB[C_1] at line 4: could not be found or marked as removed (strict mode)',
'sExpectedXMLInStrictMode' => null, 'sExpectedXMLInStrictMode' => '/itop_design/nodeA/nodeB[C_1] at line 4: could not be found or marked as removed (strict mode)',
], ],
'merge leaf nodes have different behavior depending on the mode' => [
'default no _delta leaf nodes have different behavior depending on the mode' => [
'sInitialXML' => ' 'sInitialXML' => '
<itop_design> <itop_design>
<nodeA>Test</nodeA> <nodeA>Test</nodeA>
</itop_design>', </itop_design>',
'sDeltaXML' => '<itop_design> 'sDeltaXML' => '
<itop_design>
<nodeA>Taste</nodeA> <nodeA>Taste</nodeA>
</itop_design>', </itop_design>',
'sExpectedXMLInLaxMode' => '<itop_design> 'sExpectedXMLInLaxMode' => '
<nodeA _alteration="replaced">Taste</nodeA> <itop_design>
<nodeA _alteration="replaced">Taste</nodeA>
</itop_design>', </itop_design>',
'sExpectedXMLInStrictMode' => null, 'sExpectedXMLInStrictMode' => '/itop_design/nodeA at line 3: cannot be modified without _delta flag (strict mode)',
], ],
'merge existing leaf nodes without text have same behavior' => [
'default no _delta on existing leaf nodes without text have same behavior' => [
'sInitialXML' => ' 'sInitialXML' => '
<itop_design> <itop_design>
<nodeA/>
</itop_design>',
'sDeltaXML' => '<itop_design>
<nodeA/> <nodeA/>
</itop_design>', </itop_design>',
'sExpectedXMLInLaxMode' => '<itop_design> 'sDeltaXML' => '
<nodeA/>
</itop_design>',
'sExpectedXMLInStrictMode' => '<itop_design>
<nodeA/>
</itop_design>',
],
'merge non-existing leaf nodes without text have different behavior' => [
'sInitialXML' => '
<itop_design> <itop_design>
</itop_design>',
'sDeltaXML' => '<itop_design>
<nodeA/> <nodeA/>
</itop_design>', </itop_design>',
'sExpectedXMLInLaxMode' => '<itop_design> 'sExpectedXMLInLaxMode' => '
<nodeA/> <itop_design>
<nodeA/>
</itop_design>',
'sExpectedXMLInStrictMode' => '
<itop_design>
<nodeA/>
</itop_design>', </itop_design>',
'sExpectedXMLInStrictMode' => null,
], ],
'merge non-existing nodes with sub-nodes defined' => [
'default no _delta on non-existing leaf nodes without text have different behavior' => [
'sInitialXML' => ' 'sInitialXML' => '
<itop_design> <itop_design>
</itop_design>', </itop_design>',
'sDeltaXML' => '<itop_design> 'sDeltaXML' => '
<itop_design>
<nodeA/>
</itop_design>',
'sExpectedXMLInLaxMode' => '
<itop_design>
<nodeA/>
</itop_design>',
'sExpectedXMLInStrictMode' => '/itop_design/nodeA at line 3: could not be found or marked as removed (strict mode)',
],
'default no _delta on non-existing nodes with sub-nodes defined' => [
'sInitialXML' => '
<itop_design>
</itop_design>',
'sDeltaXML' => '
<itop_design>
<nodeA> <nodeA>
<nodeB _delta="define"/> <nodeB _delta="define"/>
</nodeA> </nodeA>
</itop_design>', </itop_design>',
'sExpectedXMLInLaxMode' => '<itop_design> 'sExpectedXMLInLaxMode' => '
<itop_design>
<nodeA> <nodeA>
<nodeB _alteration="added"/> <nodeB _alteration="added"/>
</nodeA> </nodeA>
</itop_design>', </itop_design>',
'sExpectedXMLInStrictMode' => '<itop_design> 'sExpectedXMLInStrictMode' => '
<itop_design>
<nodeA> <nodeA>
<nodeB _alteration="added"/> <nodeB _alteration="added"/>
</nodeA> </nodeA>
</itop_design>',
],
'merge _delta on non-existing node must create node' => [
'sInitialXML' => '
<itop_design>
<nodeA>
<nodeB/>
</nodeA>
</itop_design>',
'sDeltaXML' => '
<itop_design load="strict">
<nodeA>
<nodeB>
<nodeC id="C_1" _delta="merge">
<items>
<item id="I_1" _delta="define">Test</item>
</items>
</nodeC>
</nodeB>
</nodeA>
</itop_design>',
'sExpectedXMLInLaxMode' => '
<itop_design>
<nodeA>
<nodeB>
<nodeC id="C_1" _alteration="added">
<items>
<item id="I_1">Test</item>
</items>
</nodeC>
</nodeB>
</nodeA>
</itop_design>',
'sExpectedXMLInStrictMode' => '
<itop_design>
<nodeA>
<nodeB>
<nodeC id="C_1" _alteration="added">
<items>
<item id="I_1">Test</item>
</items>
</nodeC>
</nodeB>
</nodeA>
</itop_design>',
],
'merge _delta on existing tree must merge...' => [
'sInitialXML' => '
<itop_design>
<nodeA>
<nodeB>
<nodeC id="C_1">
<items/>
</nodeC>
</nodeB>
</nodeA>
</itop_design>',
'sDeltaXML' => '
<itop_design load="strict">
<nodeA>
<nodeB>
<nodeC id="C_1" _delta="merge">
<items>
<item id="I_1" _delta="define">Test</item>
</items>
</nodeC>
</nodeB>
</nodeA>
</itop_design>',
'sExpectedXMLInLaxMode' => '
<itop_design>
<nodeA>
<nodeB>
<nodeC id="C_1">
<items>
<item id="I_1" _alteration="added">Test</item>
</items>
</nodeC>
</nodeB>
</nodeA>
</itop_design>',
'sExpectedXMLInStrictMode' => '
<itop_design>
<nodeA>
<nodeB>
<nodeC id="C_1">
<items>
<item id="I_1" _alteration="added">Test</item>
</items>
</nodeC>
</nodeB>
</nodeA>
</itop_design>', </itop_design>',
], ],
]; ];