Commit 212fb30e authored by Łukasz Nowak's avatar Łukasz Nowak

Merge branch 'master' into promises

parents 73bdb764 6d3bed94
......@@ -29,6 +29,13 @@ from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions
from Products.ERP5.Document.Item import Item
from lxml import etree
import collections
class DisconnectedSoftwareTree(Exception):
pass
class CyclicSoftwareTree(Exception):
pass
class SoftwareInstance(Item):
"""
......@@ -60,3 +67,44 @@ class SoftwareInstance(Item):
value = element.text
result_dict[key] = value
return result_dict
security.declareProtected(Permissions.AccessContentsInformation,
'checkNotCyclic')
def checkNotCyclic(self, graph):
# see http://neopythonic.blogspot.com/2009/01/detecting-cycles-in-directed-graph.html
todo = set(graph.keys())
while todo:
node = todo.pop()
stack = [node]
while stack:
top = stack[-1]
for node in graph[top]:
if node in stack:
raise CyclicSoftwareTree
if node in todo:
stack.append(node)
todo.remove(node)
break
else:
node = stack.pop()
return True
security.declareProtected(Permissions.AccessContentsInformation,
'checkConnected')
def checkConnected(self, graph, root):
size = len(graph)
visited = set()
to_crawl = collections.deque(graph[root])
while to_crawl:
current = to_crawl.popleft()
if current in visited:
continue
visited.add(current)
node_children = set(graph[current])
to_crawl.extend(node_children - visited)
# add one to visited, as root won't be visited, only children
# this is false positive in case of cyclic graphs, but they are
# anyway wrong in Software Instance trees
if size != len(visited) + 1:
raise DisconnectedSoftwareTree
return True
......@@ -64,9 +64,12 @@ instance_xml = kwargs["instance_xml"]\n
sla_xml = kwargs["sla_xml"]\n
state = kwargs["state"]\n
\n
# Get root software instance\n
# graph allows to "simulate" tree change after requested operation\n
graph = {}\n
# Get root software instance and create initial graph\n
predecessor_software_instance = software_instance\n
while (predecessor_software_instance is not None):\n
graph[predecessor_software_instance.getUid()] = predecessor_software_instance.getPredecessorUidList()\n
root_software_instance = predecessor_software_instance\n
predecessor_software_instance = predecessor_software_instance.getPredecessorRelatedValue(\n
portal_type="Software Instance")\n
......@@ -84,6 +87,11 @@ request_software_instance = software_instance.portal_catalog.getResultValue(\n
root_uid=root_software_instance.getUid(),\n
)\n
\n
# above query does not find root software instance, but as this case is easy\n
# to find, just check if such request does not come and raise\n
if root_software_instance.getTitle() == requested_partition_reference:\n
raise ValueError(\'It is disallowed to request root software instance\')\n
\n
if (request_software_instance is None):\n
if (portal.portal_activities.countMessageWithTag(tag) > 0):\n
# The software instance is already under creation but can not be fetched from catalog\n
......@@ -130,6 +138,8 @@ else:\n
predecessor_software_instance.edit(\n
predecessor_uid_list=predecessor_uid_list,\n
activate_kw={\'tag\': tag},)\n
graph[predecessor_software_instance.getUid()] = predecessor_uid_list\n
\n
if state == \'started\':\n
request_software_instance.startRequested()\n
request_software_instance.activate(after_tag=tag).requestStartComputerPartition()\n
......@@ -139,7 +149,16 @@ else:\n
else:\n
raise ValueError(\'State %r is not supported\' % state)\n
predecessor_list = software_instance.getPredecessorList() + [request_software_instance.getRelativeUrl()]\n
# Add requested software instance to graph if does not exists there yet\n
if not request_software_instance.getUid() in graph:\n
graph[request_software_instance.getUid()] = []\n
\n
# update graph to reflect requested operation\n
graph[software_instance.getUid()] = software_instance.getPredecessorUidList() + [request_software_instance.getUid()]\n
\n
# check if all elements are still connected\n
software_instance.checkNotCyclic(graph)\n
software_instance.checkConnected(graph, root_software_instance.getUid())\n
\n
software_instance.edit(\n
predecessor_list=predecessor_list,\n
......
246
\ No newline at end of file
252
\ No newline at end of file
......@@ -361,6 +361,7 @@ class TestVifibSlapWebService(testVifibMixin):
self.assertEqual(1, len(software_instance_list))
software_instance = software_instance_list[0]
sequence.edit(
root_software_instance_title=software_title,
software_instance_uid=software_instance.getUid(),
software_instance_reference=software_instance.getReference(),
hosting_subscription_uid=software_instance.getAggregateRelatedValue(
......@@ -1187,6 +1188,10 @@ class TestVifibSlapWebService(testVifibMixin):
def stepSelectRequestedReferenceChildrenBChild(self, sequence, **kw):
sequence.edit(requested_reference='children_b_child')
def stepSelectRequestedReferenceRootSoftwareInstanceTitle(self, sequence,
**kw):
sequence.edit(requested_reference=sequence['root_software_instance_title'])
def stepSelectRequestedReferenceB(self, sequence, **kw):
sequence.edit(requested_reference='b')
......@@ -3110,7 +3115,7 @@ class TestVifibSlapWebService(testVifibMixin):
def stepCheckSoftwareInstanceAndRelatedComputerPartition(self,
sequence, **kw):
self.stepCheckSoftwareInstanceAndRelatedComputerPartitionNoPackingList(sequence, **kw)
self.stepCheckSoftwareInstanceAndRelatedComputerPartitionNoPackingListCheck(sequence, **kw)
self._checkSoftwareInstanceAndRelatedPartition(software_instance)
def stepCheckSoftwareInstanceAndRelatedComputerPartitionNoPackingListCheck(self,
......@@ -8049,12 +8054,13 @@ class TestVifibSlapWebService(testVifibMixin):
sequence_list.addSequenceString(sequence_string)
sequence_list.play(self)
def stepDirectRequestComputerPartitionRaisesValueError(self,
def stepDirectRequestComputerPartitionRaisesDisconnectedSoftwareTree(self,
sequence, **kw):
software_instance = self.portal.portal_catalog.getResultValue(
uid = sequence['software_instance_uid'])
requested_reference = sequence['requested_reference']
self.assertRaises(ValueError,
from erp5.document.SoftwareInstance import DisconnectedSoftwareTree
self.assertRaises(DisconnectedSoftwareTree,
software_instance.requestSoftwareInstance,
software_release=sequence['software_release_uri'],
software_type=sequence['requested_reference'],
......@@ -8192,12 +8198,502 @@ class TestVifibSlapWebService(testVifibMixin):
# Try to: from C request B and prove that it raises
SelectRequestedReferenceB
LoginDefaultUser # login as superuser in order to work in erp5
DirectRequestComputerPartitionRaisesDisconnectedSoftwareTree
"""
sequence_list.addSequenceString(sequence_string)
sequence_list.play(self)
def stepDirectRequestComputerPartitionRaisesCyclicSoftwareTree(self,
sequence, **kw):
software_instance = self.portal.portal_catalog.getResultValue(
uid = sequence['software_instance_uid'])
requested_reference = sequence['requested_reference']
from erp5.document.SoftwareInstance import CyclicSoftwareTree
self.assertRaises(CyclicSoftwareTree,
software_instance.requestSoftwareInstance,
software_release=sequence['software_release_uri'],
software_type=sequence['requested_reference'],
partition_reference=sequence['requested_reference'],
shared=False,
instance_xml=self.minimal_correct_xml,
sla_xml=self.minimal_correct_xml,
state='started'
)
def test_bug_cyclic_software_instance(self):
"""Check that no cyclic Software Instance trees would be created
In below scenario system shall behave like mentioned:
OpenOrder.request(SR, A) | SR(A)
A.request(SR, B) | SR(A) <- SR(B)
B.request(SR, A) | SR(A) <- SR(B) <- SR(C)
C.request(SR, B) raises immediately, because the result would be:
SR(A)
SR(B) <-> SR(C)
so B and C would be cyclic
"""
# Setup sufficient amount of CP
self.computer_partition_amount = 3
sequence_list = SequenceList()
sequence_string = """
# Prepare software release
LoginTestVifibDeveloper
SelectNewSoftwareReleaseUri
CreateSoftwareRelease
Tic
SubmitSoftwareRelease
Tic
CreateSoftwareProduct
Tic
ValidateSoftwareProduct
Tic
SetSoftwareProductToSoftwareRelease
PublishByActionSoftwareRelease
Logout
# Create the computer
LoginTestVifibAdmin
CreateComputer
Tic
Logout
SlapLoginCurrentComputer
FormatComputer
Tic
SlapLogout
StoreCurrentComputerReferenceBufferA
StoreCurrentComputerUidBufferA
# Install the software release
LoginTestVifibAdmin
RequestSoftwareInstallation
Tic
Logout
SlapLoginCurrentComputer
ComputerSoftwareReleaseAvailable
Tic
SlapLogout
# Create Software Instance A (originates from Open Order)
LoginTestVifibCustomer
PersonRequestSoftwareInstance
Tic
Logout
LoginDefaultUser
ConfirmOrderedSaleOrderActiveSense
Tic
SetSelectedComputerPartition
SelectCurrentlyUsedSalePackingListUid
Logout
LoginDefaultUser
CheckComputerPartitionInstanceSetupSalePackingListConfirmed
Logout
# From root request B
SelectRequestedReferenceB
SlapLoginCurrentSoftwareInstance
RequestComputerPartitionNotReadyResponse
Tic
SlapLogout
SlapLoginCurrentSoftwareInstance
RequestComputerPartition
Tic
SlapLogout
LoginDefaultUser
CheckSoftwareInstanceAndRelatedComputerPartitionNoPackingListCheck
CheckRequestedSoftwareInstanceAndRelatedComputerPartition
Logout
LoginDefaultUser
SetCurrentSoftwareInstanceRequested
SetSelectedComputerPartition
SelectCurrentlyUsedSalePackingListUid
Logout
SlapLoginCurrentSoftwareInstance
CheckRequestedComputerPartitionCleanParameterList
Logout
# From B request C
SelectRequestedReferenceC
SlapLoginCurrentSoftwareInstance
RequestComputerPartitionNotReadyResponse
Tic
SlapLogout
SlapLoginCurrentSoftwareInstance
RequestComputerPartition
Tic
SlapLogout
LoginDefaultUser
CheckSoftwareInstanceAndRelatedComputerPartitionNoPackingListCheck
CheckRequestedSoftwareInstanceAndRelatedComputerPartition
Logout
LoginDefaultUser
SetCurrentSoftwareInstanceRequested
SetSelectedComputerPartition
SelectCurrentlyUsedSalePackingListUid
Logout
SlapLoginCurrentSoftwareInstance
CheckRequestedComputerPartitionCleanParameterList
Logout
# Try to: from C request B and prove that it raises
SelectRequestedReferenceB
LoginDefaultUser # login as superuser in order to work in erp5
DirectRequestComputerPartitionRaisesCyclicSoftwareTree
"""
sequence_list.addSequenceString(sequence_string)
sequence_list.play(self)
def stepDirectRequestComputerPartitionRaisesValueError(self,
sequence, **kw):
software_instance = self.portal.portal_catalog.getResultValue(
uid = sequence['software_instance_uid'])
requested_reference = sequence['requested_reference']
self.assertRaises(ValueError,
software_instance.requestSoftwareInstance,
software_release=sequence['software_release_uri'],
software_type=sequence['requested_reference'],
partition_reference=sequence['requested_reference'],
shared=False,
instance_xml=self.minimal_correct_xml,
sla_xml=self.minimal_correct_xml,
state='started'
)
def test_bug_cyclic_software_instance_small_tree(self):
"""Check that no cyclic Software Instance trees would be created
In below scenario system shall behave like mentioned:
OpenOrder.request(SR, A) | SR(A)
A.request(SR, B) | SR(A) <- SR(B)
B.request(SR, A) raises immediately, because the result would be:
SR(A) <-> SR(B)
so B and A would be cyclic
"""
# Setup sufficient amount of CP
self.computer_partition_amount = 2
sequence_list = SequenceList()
sequence_string = """
# Prepare software release
LoginTestVifibDeveloper
SelectNewSoftwareReleaseUri
CreateSoftwareRelease
Tic
SubmitSoftwareRelease
Tic
CreateSoftwareProduct
Tic
ValidateSoftwareProduct
Tic
SetSoftwareProductToSoftwareRelease
PublishByActionSoftwareRelease
Logout
# Create the computer
LoginTestVifibAdmin
CreateComputer
Tic
Logout
SlapLoginCurrentComputer
FormatComputer
Tic
SlapLogout
StoreCurrentComputerReferenceBufferA
StoreCurrentComputerUidBufferA
# Install the software release
LoginTestVifibAdmin
RequestSoftwareInstallation
Tic
Logout
SlapLoginCurrentComputer
ComputerSoftwareReleaseAvailable
Tic
SlapLogout
# Create Software Instance A (originates from Open Order)
LoginTestVifibCustomer
PersonRequestSoftwareInstance
Tic
Logout
LoginDefaultUser
ConfirmOrderedSaleOrderActiveSense
Tic
SetSelectedComputerPartition
SelectCurrentlyUsedSalePackingListUid
Logout
LoginDefaultUser
CheckComputerPartitionInstanceSetupSalePackingListConfirmed
Logout
# From root request B
SelectRequestedReferenceB
SlapLoginCurrentSoftwareInstance
RequestComputerPartitionNotReadyResponse
Tic
SlapLogout
SlapLoginCurrentSoftwareInstance
RequestComputerPartition
Tic
SlapLogout
LoginDefaultUser
CheckSoftwareInstanceAndRelatedComputerPartitionNoPackingListCheck
CheckRequestedSoftwareInstanceAndRelatedComputerPartition
Logout
LoginDefaultUser
SetCurrentSoftwareInstanceRequested
SetSelectedComputerPartition
SelectCurrentlyUsedSalePackingListUid
Logout
SlapLoginCurrentSoftwareInstance
CheckRequestedComputerPartitionCleanParameterList
Logout
# Try to: From B request root
SelectRequestedReferenceRootSoftwareInstanceTitle
LoginDefaultUser # login as superuser in order to work in erp5
DirectRequestComputerPartitionRaisesValueError
"""
sequence_list.addSequenceString(sequence_string)
sequence_list.play(self)
########################################
# Software Instance graph helpers
########################################
def _test_si_tree(self):
software_instance = self.portal.software_instance_module.newContent(
portal_type='Software Instance')
self.checkConnected = software_instance.checkConnected
self.checkNotCyclic = software_instance.checkNotCyclic
def test_si_tree_simple_connected(self):
"""Graph of one element is connected
A
"""
self._test_si_tree()
graph = {'A': []}
root = 'A'
self.assertEqual(True, self.checkConnected(graph, root))
def test_si_tree_simple_list_connected(self):
"""Graph of list is connected
B->C->A
"""
self._test_si_tree()
graph = {'A': [], 'B': ['C'], 'C': ['A']}
root = 'B'
self.assertEqual(True, self.checkConnected(graph, root))
def test_si_tree_complex_connected(self):
"""Tree is connected
B --> A
\-> C --> D
\-> E --> F
"""
self._test_si_tree()
graph = {
'A': [],
'B': ['A', 'C'],
'C': ['D', 'E'],
'D': [],
'E': ['F'],
'F': [],
}
root = 'B'
self.assertEqual(True, self.checkConnected(graph, root))
def test_si_tree_simple_list_disconnected(self):
"""Two lists are disconnected
A->B
C
"""
self._test_si_tree()
graph = {'A': ['B'], 'B': [], 'C': []}
root = 'A'
from erp5.document.SoftwareInstance import DisconnectedSoftwareTree
self.assertRaises(DisconnectedSoftwareTree, self.checkConnected, graph,
root)
# For now limitation of implementation gives false positive
@expectedFailure
def test_si_tree_cyclic_connected(self):
"""Cyclic is connected
A<->B
"""
self._test_si_tree()
graph = {'A': ['B'], 'B': ['A']}
root = 'B'
self.assertEqual(True, self.checkConnected(graph, root))
def test_si_tree_cyclic_disconnected(self):
"""Two trees, where one is cyclic are disconnected
B --> A
\-> H
C --> D --> G
^ \-> E --> F \
\------------/
"""
self._test_si_tree()
graph = {
'A': [],
'B': ['A', 'H'],
'C': ['D', 'E'],
'D': ['G'],
'E': ['F'],
'F': ['C'],
'G': [],
'H': [],
}
root = 'B'
from erp5.document.SoftwareInstance import DisconnectedSoftwareTree
self.assertRaises(DisconnectedSoftwareTree, self.checkConnected, graph,
root)
def test_si_tree_simple_not_cyclic(self):
"""Graph of one element is not cyclic
A
"""
self._test_si_tree()
graph = {'A': []}
self.assertEqual(True, self.checkNotCyclic(graph))
def test_si_tree_simple_list_not_cyclic(self):
"""Graph of list is not cyclic
B->C->A
"""
self._test_si_tree()
graph = {'A': [], 'B': ['C'], 'C': ['A']}
self.assertEqual(True, self.checkNotCyclic(graph))
def test_si_tree_simple_list_cyclic(self):
"""Graph of cyclic list is cyclic
B->C->A-\
^-------/
"""
self._test_si_tree()
graph = {'A': ['B'], 'B': ['C'], 'C': ['A']}
from erp5.document.SoftwareInstance import CyclicSoftwareTree
self.assertRaises(CyclicSoftwareTree, self.checkNotCyclic, graph)
def test_si_tree_simple_list_cyclic(self):
"""Graph of cyclic list is cyclic
B->C->D->A-\
^-------/
"""
self._test_si_tree()
graph = {'A': ['C'], 'B': ['C'], 'C': ['D'], 'D': ['A']}
from erp5.document.SoftwareInstance import CyclicSoftwareTree
self.assertRaises(CyclicSoftwareTree, self.checkNotCyclic, graph)
def test_si_tree_complex_not_cyclic(self):
"""Tree is not cyclic
B --> A
\-> C --> D
\-> E --> F
"""
self._test_si_tree()
graph = {
'A': [],
'B': ['A', 'C'],
'C': ['D', 'E'],
'D': [],
'E': ['F'],
'F': [],
}
self.assertEqual(True, self.checkNotCyclic(graph))
def test_si_tree_complex_cyclic(self):
"""Tree is not cyclic
B --> A
\-> C --> D
^ \-> E --> F -\
\-------------/
"""
self._test_si_tree()
graph = {
'A': [],
'B': ['A', 'C'],
'C': ['D', 'E'],
'D': [],
'E': ['F'],
'F': ['C'],
}
from erp5.document.SoftwareInstance import CyclicSoftwareTree
self.assertRaises(CyclicSoftwareTree, self.checkNotCyclic, graph)
def test_si_tree_simple_list_disconnected_not_cyclic(self):
"""Two lists are disconnected
A->B
C
"""
self._test_si_tree()
graph = {'A': ['B'], 'B': [], 'C': []}
self.assertEqual(True, self.checkNotCyclic(graph))
def test_si_tree_cyclic(self):
"""Cyclic is connected
A<->B
"""
self._test_si_tree()
graph = {'A': ['B'], 'B': ['A']}
from erp5.document.SoftwareInstance import CyclicSoftwareTree
self.assertRaises(CyclicSoftwareTree, self.checkNotCyclic, graph)
def test_si_tree_cyclic_disconnected_cyclic(self):
"""Two trees, where one is cyclic are disconnected
B --> A
\-> H
C --> D --> G
^ \-> E --> F \
\------------/
"""
self._test_si_tree()
graph = {
'A': [],
'B': ['A', 'H'],
'C': ['D', 'E'],
'D': ['G'],
'E': ['F'],
'F': ['C'],
'G': [],
'H': ['A'],
}
from erp5.document.SoftwareInstance import CyclicSoftwareTree
self.assertRaises(CyclicSoftwareTree, self.checkNotCyclic, graph)
########################################
# Other tests
########################################
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment