blob: 7c2988e182e578369811066fa214da18ccce9219 [file] [log] [blame]
Jeremy Ronquillo696f4262017-10-17 10:56:26 -07001# !/usr/bin/env python
Jeremy Ronquillob27ce4c2017-07-17 12:41:28 -07002
Jeremy Ronquillo4d5f1d02017-10-13 20:23:57 +00003'''
4Copyright 2016 Open Networking Foundation (ONF)
Jeremy Songsterae01bba2016-07-11 15:39:17 -07005
6Please refer questions to either the onos test mailing list at <onos-test@onosproject.org>,
7the System Testing Plans and Results wiki page at <https://wiki.onosproject.org/x/voMg>,
8or the System Testing Guide page at <https://wiki.onosproject.org/x/WYQg>
Jeremy Ronquillob27ce4c2017-07-17 12:41:28 -07009
10 TestON is free software: you can redistribute it and/or modify
11 it under the terms of the GNU General Public License as published by
12 the Free Software Foundation, either version 2 of the License, or
Jeremy Ronquillo4d5f1d02017-10-13 20:23:57 +000013 (at your option) any later version.
Jeremy Ronquillob27ce4c2017-07-17 12:41:28 -070014
15 TestON is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with TestON. If not, see <http://www.gnu.org/licenses/>.
22
Jeremy Ronquillo4d5f1d02017-10-13 20:23:57 +000023'''
24
25
You Wangb1a16c32016-05-23 15:30:52 -070026import time
27import random
28
29class Graph:
30 """
31 Graph class provides implementations of graph algorithms.
32 The functions currently supported include:
33 - Comparing two graphs with specified attributes for vertices and edges
Jeremy Ronquillo4d5f1d02017-10-13 20:23:57 +000034 - Getting DFI (Depth First Index) and back edges during a DFS
You Wangb1a16c32016-05-23 15:30:52 -070035 - Chain decomposition of a graph
Jeremy Ronquillo4d5f1d02017-10-13 20:23:57 +000036 - Finding (non-)cut-edges and vertices
You Wangb1a16c32016-05-23 15:30:52 -070037 """
Jeremy Ronquillo4d5f1d02017-10-13 20:23:57 +000038
You Wangb1a16c32016-05-23 15:30:52 -070039 def __init__( self ):
40 # We use a dictionary to store all information about the graph
41 self.graphDict = {}
42 # Depth-first index of each vertex
43 self.DFI = {}
44 self.currentDFI = 0
Jeremy Ronquillo4d5f1d02017-10-13 20:23:57 +000045 # Parent vertex (and edge to that vertex) of each vertex in depth-first search tree
You Wangb1a16c32016-05-23 15:30:52 -070046 self.parentVertexInDFS = {}
47 self.parentEdgeInDFS = {}
48 # Back edges of the graph generated during DFS
49 self.backEdges = {}
50 # All chains in chain decomposition algorithm
51 self.chains = []
52
53 def update( self, graphDict ):
54 """
55 Update the graph data. The current graph dictionary will be replaced by the
56 new one.
57 graphDict is in a dictionary which maps each vertex to a list of attributes.
58 An example of graphDict:
59 { vertex1: { 'edges': ..., 'name': ..., 'protocol': ... },
60 vertex2: { 'edges': ..., 'name': ..., 'protocol': ... } }
61 Each vertex should at least have an 'edges' attribute which describes the
62 adjacency information. The value of 'edges' attribute is also represented by
Jeremy Ronquillo4d5f1d02017-10-13 20:23:57 +000063 a dictionary, which maps each edge (identified by the neighbor vertex) to a
You Wangb1a16c32016-05-23 15:30:52 -070064 list of attributes.
65 An example of the edges dictionary:
66 'edges': { vertex2: { 'port': ..., 'type': ... },
67 vertex3: { 'port': ..., 'type': ... } }
68 """
69 self.graphDict = graphDict
70 return main.TRUE
71
Jeremy Ronquillo696f4262017-10-17 10:56:26 -070072 def compareGraphs( self, graphDictA, graphDictB, vertexAttributes=[ 'edges' ], edgeAttributes=[ 'port' ] ):
You Wangb1a16c32016-05-23 15:30:52 -070073 """
74 Compare two graphs.
75 By default only the adjacency relationship, i.e. 'port' attribute in
76 'edges' attribute for each vertex, is compared, To get other attributes
77 included, attribute name needs to be specified in the args, e.g.
78 vertexAttributes=[ 'edges', 'protocol' ] or
79 edgeAttributes=[ 'port', 'type' ]
80 Return main.TRUE if two graphs are equal, otherwise main.FALSE
81 """
82 try:
83 result = main.TRUE
84 for vertex in set( graphDictA ).difference( graphDictB ):
85 result = main.FALSE
86 main.log.warn( "Graph: graph B: vertex {} not found".format( vertex ) )
87 for vertex in set( graphDictB ).difference( graphDictA ):
88 result = main.FALSE
89 main.log.warn( "Graph: graph A: vertex {} not found".format( vertex ) )
90 for vertex in set( graphDictA ).intersection( graphDictB ):
91 for vertexAttribute in vertexAttributes:
92 attributeFound = True
93 if vertexAttribute not in graphDictA[ vertex ]:
94 main.log.warn( "Graph: graph A -> vertex {}: attribute {} not found".format( vertex, vertexAttribute ) )
95 attributeFound = False
96 if vertexAttribute not in graphDictB[ vertex ]:
97 attributeFound = False
98 main.log.warn( "Graph: graph B -> vertex {}: attribute {} not found".format( vertex, vertexAttribute ) )
99 if not attributeFound:
100 result = main.FALSE
101 continue
102 else:
103 # Compare two attributes
104 attributeValueA = graphDictA[ vertex ][ vertexAttribute ]
105 attributeValueB = graphDictB[ vertex ][ vertexAttribute ]
Jeremy Ronquillo4d5f1d02017-10-13 20:23:57 +0000106 # FIXME: the comparison may not work for (sub)attribute values that are of list type
You Wangb1a16c32016-05-23 15:30:52 -0700107 # For attributes except for 'edges', we just rely on '==' for comparison
108 if not vertexAttribute == 'edges':
109 if not attributeValueA == attributeValueB:
110 result = main.FALSE
111 main.log.warn( "Graph: vertex {}: {} does not match: {} and {}".format( vertex,
112 vertexAttribute,
113 attributeValueA,
114 attributeValueB ) )
115 # The structure of 'edges' is similar to that of graphs, so we use the same method for comparison
116 else:
117 edgeDictA = attributeValueA
118 edgeDictB = attributeValueB
119 for neighbor in set( edgeDictA ).difference( edgeDictB ):
120 result = main.FALSE
121 main.log.warn( "Graph: graph B -> vertex {}: neighbor {} not found".format( vertex, neighbor ) )
122 for neighbor in set( edgeDictB ).difference( edgeDictA ):
123 result = main.FALSE
124 main.log.warn( "Graph: graph A -> vertex {}: neighbor {} not found".format( vertex, neighbor ) )
125 for neighbor in set( edgeDictA ).intersection( edgeDictB ):
126 for edgeAttribute in edgeAttributes:
127 attributeFound = True
128 if edgeAttribute not in edgeDictA[ neighbor ]:
129 attributeFound = False
130 main.log.warn( "Graph: graph A -> vertex {} -> neighbor {}: attribute {} not found".format( vertex,
131 neighbor,
132 edgeAttribute ) )
133 if edgeAttribute not in edgeDictB[ neighbor ]:
134 attributeFound = False
135 main.log.warn( "Graph: graph B -> vertex {} -> neighbor {}: attribute {} not found".format( vertex,
136 neighbor,
137 edgeAttribute ) )
138 if not attributeFound:
139 result = main.FALSE
140 continue
141 else:
142 # Compare two attributes
143 attributeValueA = edgeDictA[ neighbor ][ edgeAttribute ]
144 attributeValueB = edgeDictB[ neighbor ][ edgeAttribute ]
145 if not attributeValueA == attributeValueB:
146 result = main.FALSE
147 main.log.warn( "Graph: vertex {} -> neighbor {}: {} does not match: {} and {}".format( vertex,
148 neighbor,
149 edgeAttribute,
150 attributeValueA,
151 attributeValueB ) )
152 if not result:
Jeremy Ronquillo696f4262017-10-17 10:56:26 -0700153 # main.log.debug( "Graph: graphDictA: {}".format( graphDictA ) )
154 # main.log.debug( "Graph: graphDictB: {}".format( graphDictB ) )
You Wang9a589b92016-07-14 09:43:21 -0700155 pass
You Wangb1a16c32016-05-23 15:30:52 -0700156 return result
157 except TypeError:
158 main.log.exception( "Graph: TypeError exception found" )
159 return main.ERROR
160 except KeyError:
161 main.log.exception( "Graph: KeyError exception found" )
162 return main.ERROR
163 except Exception:
164 main.log.exception( "Graph: Uncaught exception" )
165 return main.ERROR
166
167 def getNonCutEdges( self ):
168 """
Jeremy Ronquillo4d5f1d02017-10-13 20:23:57 +0000169 Get a list of non-cut-edges (non-bridges).
170 The definition of a cut-edge (bridge) is: the deletion of a cut-edge will
You Wangb1a16c32016-05-23 15:30:52 -0700171 increase the number of connected component of a graph.
172 The function is realized by impelementing Schmidt's algorithm based on
173 chain decomposition.
174 Returns a list of edges, e.g.
175 [ [ vertex1, vertex2 ], [ vertex2, vertex3 ] ]
176 """
177 try:
178 if not self.depthFirstSearch():
179 return None
180 if not self.findChains():
181 return None
182 nonCutEdges = []
183 for chain in self.chains:
184 for edge in chain:
185 nonCutEdges.append( edge )
Jeremy Ronquillo696f4262017-10-17 10:56:26 -0700186 # main.log.debug( 'Non-cut-edges: {}'.format( nonCutEdges ) )
You Wangb1a16c32016-05-23 15:30:52 -0700187 return nonCutEdges
188 except Exception:
189 main.log.exception( "Graph: Uncaught exception" )
190 return None
191
192 def getNonCutVertices( self ):
193 """
194 Get a list of non-cut-vertices.
195 The definition of a cut-vertex is: the deletion of a cut-vertex will
196 increase the number of connected component of a graph.
197 The function is realized by impelementing Schmidt's algorithm based on
198 chain decomposition.
199 Returns a list of vertices, e.g. [ vertex1, vertex2, vertex3 ]
200 """
201 try:
202 nonCutEdges = self.getNonCutEdges()
203 # find all cycle chains
204 cycleChains = []
205 for chain in self.chains:
206 # if the source vertex of the first chain equals to the destination vertex of the last
207 # chain, the chain is a cycle chain
208 if chain[ 0 ][ 0 ] == chain[ -1 ][ 1 ]:
209 cycleChains.append( chain )
Jeremy Ronquillo696f4262017-10-17 10:56:26 -0700210 # main.log.debug( 'Cycle chains: {}'.format( cycleChains ) )
You Wangb1a16c32016-05-23 15:30:52 -0700211 # Get a set of vertices which are the first vertices of a cycle chain (excluding the first
212 # cycle chain), and these vertices are a subset of all cut-vertices
213 subsetOfCutVertices = []
214 if len( cycleChains ) > 1:
215 for cycleChain in cycleChains[ 1: ]:
216 subsetOfCutVertices.append( cycleChain[ 0 ][ 0 ] )
Jeremy Ronquillo696f4262017-10-17 10:56:26 -0700217 # main.log.debug( 'Subset of cut vertices: {}'.format( subsetOfCutVertices ) )
You Wangb1a16c32016-05-23 15:30:52 -0700218 nonCutVertices = []
Jeremy Ronquillo696f4262017-10-17 10:56:26 -0700219 assert nonCutEdges is not None
You Wangb1a16c32016-05-23 15:30:52 -0700220 for vertex in self.graphDict.keys():
221 if vertex in subsetOfCutVertices:
222 continue
223 vertexIsNonCut = True
224 for neighbor in self.graphDict[ vertex ][ 'edges' ].keys():
225 edge = [ vertex, neighbor ]
226 backwardEdge = [ neighbor, vertex ]
Jeremy Ronquillo696f4262017-10-17 10:56:26 -0700227 if edge not in nonCutEdges and backwardEdge not in nonCutEdges:
You Wangb1a16c32016-05-23 15:30:52 -0700228 vertexIsNonCut = False
229 break
230 if vertexIsNonCut:
231 nonCutVertices.append( vertex )
Jeremy Ronquillo696f4262017-10-17 10:56:26 -0700232 # main.log.debug( 'Non-cut-vertices: {}'.format( nonCutVertices ) )
You Wangb1a16c32016-05-23 15:30:52 -0700233 return nonCutVertices
234 except KeyError:
235 main.log.exception( "Graph: KeyError exception found" )
236 return None
237 except AssertionError:
238 main.log.exception( "Graph: AssertionError exception found" )
239 return None
240 except Exception:
241 main.log.exception( "Graph: Uncaught exception" )
242 return None
243
244 def depthFirstSearch( self ):
245 """
246 This function runs a depth-first search and gets DFI of each vertex as well
247 as generates the back edges
248 """
249 try:
Jeremy Ronquillo696f4262017-10-17 10:56:26 -0700250 assert self.graphDict is not None and len( self.graphDict ) != 0
You Wangb1a16c32016-05-23 15:30:52 -0700251 for vertex in self.graphDict.keys():
252 self.DFI[ vertex ] = -1
253 self.parentVertexInDFS[ vertex ] = ''
254 self.parentEdgeInDFS[ vertex ] = None
255 firstVertex = self.graphDict.keys()[ 0 ]
256 self.currentDFI = 0
257 self.backEdges = {}
258 if not self.depthFirstSearchRecursive( firstVertex ):
259 return main.ERROR
260 return main.TRUE
261 except KeyError:
262 main.log.exception( "Graph: KeyError exception found" )
263 return main.ERROR
264 except AssertionError:
265 main.log.exception( "Graph: AssertionError exception found" )
266 return main.ERROR
267 except Exception:
268 main.log.exception( "Graph: Uncaught exception" )
269 return main.ERROR
270
271 def depthFirstSearchRecursive( self, vertex ):
272 """
273 Recursive function for depth-first search
274 """
275 try:
276 self.DFI[ vertex ] = self.currentDFI
277 self.currentDFI += 1
278 for neighbor in self.graphDict[ vertex ][ 'edges' ].keys():
279 edge = [ vertex, neighbor ]
280 backwardEdge = [ neighbor, vertex ]
281 if neighbor == self.parentVertexInDFS[ vertex ]:
282 continue
283 elif self.DFI[ neighbor ] == -1:
284 self.parentVertexInDFS[ neighbor ] = vertex
285 self.parentEdgeInDFS[ neighbor ] = backwardEdge
286 if not self.depthFirstSearchRecursive( neighbor ):
287 return main.ERROR
288 else:
289 key = self.DFI[ neighbor ]
290 if key in self.backEdges.keys():
Jeremy Ronquillo696f4262017-10-17 10:56:26 -0700291 if edge not in self.backEdges[ key ] and \
292 backwardEdge not in self.backEdges[ key ]:
You Wangb1a16c32016-05-23 15:30:52 -0700293 self.backEdges[ key ].append( backwardEdge )
294 else:
295 tempKey = self.DFI[ vertex ]
296 if tempKey in self.backEdges.keys():
Jeremy Ronquillo696f4262017-10-17 10:56:26 -0700297 if edge not in self.backEdges[ tempKey ] and\
298 backwardEdge not in self.backEdges[ tempKey ]:
You Wangb1a16c32016-05-23 15:30:52 -0700299 self.backEdges[ key ] = [ backwardEdge ]
300 else:
301 self.backEdges[ key ] = [ backwardEdge ]
302 return main.TRUE
303 except KeyError:
304 main.log.exception( "Graph: KeyError exception found" )
305 return main.ERROR
306 except Exception:
307 main.log.exception( "Graph: Uncaught exception" )
308 return main.ERROR
309
310 def findChains( self ):
311 """
312 This function finds all the chains in chain-decomposition algorithm
313 """
Jeremy Ronquillo4d5f1d02017-10-13 20:23:57 +0000314 keyList = self.backEdges.keys()
315 keyList.sort()
You Wangb1a16c32016-05-23 15:30:52 -0700316 vertexIsVisited = {}
317 self.chains = []
318 for vertex in self.graphDict.keys():
319 vertexIsVisited[ vertex ] = False
320 try:
321 for key in keyList:
322 backEdgeList = self.backEdges[ key ]
323 for edge in backEdgeList:
324 chain = []
325 currentEdge = edge
326 sourceVertex = edge[ 0 ]
327 while True:
328 currentVertex = currentEdge[ 0 ]
329 nextVertex = currentEdge[ 1 ]
330 vertexIsVisited[ currentVertex ] = True
331 chain.append( currentEdge )
Jeremy Ronquillo696f4262017-10-17 10:56:26 -0700332 if nextVertex == sourceVertex or vertexIsVisited[ nextVertex ] is True:
You Wangb1a16c32016-05-23 15:30:52 -0700333 break
334 currentEdge = self.parentEdgeInDFS[ nextVertex ]
335 self.chains.append( chain )
336 return main.TRUE
337 except KeyError:
338 main.log.exception( "Graph: KeyError exception found" )
339 return main.ERROR
340 except Exception:
341 main.log.exception( "Graph: Uncaught exception" )
342 return main.ERROR