blob: e8c8a7ac192d70038e3373be17f8c186754dbf28 [file] [log] [blame]
You Wangb1a16c32016-05-23 15:30:52 -07001#!/usr/bin/env python
Jeremy Songsterae01bba2016-07-11 15:39:17 -07002"""
3Created 2016 by ON.Lab
4
5Please refer questions to either the onos test mailing list at <onos-test@onosproject.org>,
6the System Testing Plans and Results wiki page at <https://wiki.onosproject.org/x/voMg>,
7or the System Testing Guide page at <https://wiki.onosproject.org/x/WYQg>
8"""
9
10
You Wangb1a16c32016-05-23 15:30:52 -070011import time
12import random
13
14class Graph:
15 """
16 Graph class provides implementations of graph algorithms.
17 The functions currently supported include:
18 - Comparing two graphs with specified attributes for vertices and edges
19 - Getting DFI (Depth First Index) and back edges during a DFS
20 - Chain decomposition of a graph
21 - Finding (non-)cut-edges and vertices
22 """
23
24 def __init__( self ):
25 # We use a dictionary to store all information about the graph
26 self.graphDict = {}
27 # Depth-first index of each vertex
28 self.DFI = {}
29 self.currentDFI = 0
30 # Parent vertex (and edge to that vertex) of each vertex in depth-first search tree
31 self.parentVertexInDFS = {}
32 self.parentEdgeInDFS = {}
33 # Back edges of the graph generated during DFS
34 self.backEdges = {}
35 # All chains in chain decomposition algorithm
36 self.chains = []
37
38 def update( self, graphDict ):
39 """
40 Update the graph data. The current graph dictionary will be replaced by the
41 new one.
42 graphDict is in a dictionary which maps each vertex to a list of attributes.
43 An example of graphDict:
44 { vertex1: { 'edges': ..., 'name': ..., 'protocol': ... },
45 vertex2: { 'edges': ..., 'name': ..., 'protocol': ... } }
46 Each vertex should at least have an 'edges' attribute which describes the
47 adjacency information. The value of 'edges' attribute is also represented by
48 a dictionary, which maps each edge (identified by the neighbor vertex) to a
49 list of attributes.
50 An example of the edges dictionary:
51 'edges': { vertex2: { 'port': ..., 'type': ... },
52 vertex3: { 'port': ..., 'type': ... } }
53 """
54 self.graphDict = graphDict
55 return main.TRUE
56
57 def compareGraphs( self, graphDictA, graphDictB, vertexAttributes=['edges'], edgeAttributes=['port'] ):
58 """
59 Compare two graphs.
60 By default only the adjacency relationship, i.e. 'port' attribute in
61 'edges' attribute for each vertex, is compared, To get other attributes
62 included, attribute name needs to be specified in the args, e.g.
63 vertexAttributes=[ 'edges', 'protocol' ] or
64 edgeAttributes=[ 'port', 'type' ]
65 Return main.TRUE if two graphs are equal, otherwise main.FALSE
66 """
67 try:
68 result = main.TRUE
69 for vertex in set( graphDictA ).difference( graphDictB ):
70 result = main.FALSE
71 main.log.warn( "Graph: graph B: vertex {} not found".format( vertex ) )
72 for vertex in set( graphDictB ).difference( graphDictA ):
73 result = main.FALSE
74 main.log.warn( "Graph: graph A: vertex {} not found".format( vertex ) )
75 for vertex in set( graphDictA ).intersection( graphDictB ):
76 for vertexAttribute in vertexAttributes:
77 attributeFound = True
78 if vertexAttribute not in graphDictA[ vertex ]:
79 main.log.warn( "Graph: graph A -> vertex {}: attribute {} not found".format( vertex, vertexAttribute ) )
80 attributeFound = False
81 if vertexAttribute not in graphDictB[ vertex ]:
82 attributeFound = False
83 main.log.warn( "Graph: graph B -> vertex {}: attribute {} not found".format( vertex, vertexAttribute ) )
84 if not attributeFound:
85 result = main.FALSE
86 continue
87 else:
88 # Compare two attributes
89 attributeValueA = graphDictA[ vertex ][ vertexAttribute ]
90 attributeValueB = graphDictB[ vertex ][ vertexAttribute ]
91 # FIXME: the comparison may not work for (sub)attribute values that are of list type
92 # For attributes except for 'edges', we just rely on '==' for comparison
93 if not vertexAttribute == 'edges':
94 if not attributeValueA == attributeValueB:
95 result = main.FALSE
96 main.log.warn( "Graph: vertex {}: {} does not match: {} and {}".format( vertex,
97 vertexAttribute,
98 attributeValueA,
99 attributeValueB ) )
100 # The structure of 'edges' is similar to that of graphs, so we use the same method for comparison
101 else:
102 edgeDictA = attributeValueA
103 edgeDictB = attributeValueB
104 for neighbor in set( edgeDictA ).difference( edgeDictB ):
105 result = main.FALSE
106 main.log.warn( "Graph: graph B -> vertex {}: neighbor {} not found".format( vertex, neighbor ) )
107 for neighbor in set( edgeDictB ).difference( edgeDictA ):
108 result = main.FALSE
109 main.log.warn( "Graph: graph A -> vertex {}: neighbor {} not found".format( vertex, neighbor ) )
110 for neighbor in set( edgeDictA ).intersection( edgeDictB ):
111 for edgeAttribute in edgeAttributes:
112 attributeFound = True
113 if edgeAttribute not in edgeDictA[ neighbor ]:
114 attributeFound = False
115 main.log.warn( "Graph: graph A -> vertex {} -> neighbor {}: attribute {} not found".format( vertex,
116 neighbor,
117 edgeAttribute ) )
118 if edgeAttribute not in edgeDictB[ neighbor ]:
119 attributeFound = False
120 main.log.warn( "Graph: graph B -> vertex {} -> neighbor {}: attribute {} not found".format( vertex,
121 neighbor,
122 edgeAttribute ) )
123 if not attributeFound:
124 result = main.FALSE
125 continue
126 else:
127 # Compare two attributes
128 attributeValueA = edgeDictA[ neighbor ][ edgeAttribute ]
129 attributeValueB = edgeDictB[ neighbor ][ edgeAttribute ]
130 if not attributeValueA == attributeValueB:
131 result = main.FALSE
132 main.log.warn( "Graph: vertex {} -> neighbor {}: {} does not match: {} and {}".format( vertex,
133 neighbor,
134 edgeAttribute,
135 attributeValueA,
136 attributeValueB ) )
137 if not result:
You Wang9a589b92016-07-14 09:43:21 -0700138 #main.log.debug( "Graph: graphDictA: {}".format( graphDictA ) )
139 #main.log.debug( "Graph: graphDictB: {}".format( graphDictB ) )
140 pass
You Wangb1a16c32016-05-23 15:30:52 -0700141 return result
142 except TypeError:
143 main.log.exception( "Graph: TypeError exception found" )
144 return main.ERROR
145 except KeyError:
146 main.log.exception( "Graph: KeyError exception found" )
147 return main.ERROR
148 except Exception:
149 main.log.exception( "Graph: Uncaught exception" )
150 return main.ERROR
151
152 def getNonCutEdges( self ):
153 """
154 Get a list of non-cut-edges (non-bridges).
155 The definition of a cut-edge (bridge) is: the deletion of a cut-edge will
156 increase the number of connected component of a graph.
157 The function is realized by impelementing Schmidt's algorithm based on
158 chain decomposition.
159 Returns a list of edges, e.g.
160 [ [ vertex1, vertex2 ], [ vertex2, vertex3 ] ]
161 """
162 try:
163 if not self.depthFirstSearch():
164 return None
165 if not self.findChains():
166 return None
167 nonCutEdges = []
168 for chain in self.chains:
169 for edge in chain:
170 nonCutEdges.append( edge )
You Wang9a589b92016-07-14 09:43:21 -0700171 #main.log.debug( 'Non-cut-edges: {}'.format( nonCutEdges ) )
You Wangb1a16c32016-05-23 15:30:52 -0700172 return nonCutEdges
173 except Exception:
174 main.log.exception( "Graph: Uncaught exception" )
175 return None
176
177 def getNonCutVertices( self ):
178 """
179 Get a list of non-cut-vertices.
180 The definition of a cut-vertex is: the deletion of a cut-vertex will
181 increase the number of connected component of a graph.
182 The function is realized by impelementing Schmidt's algorithm based on
183 chain decomposition.
184 Returns a list of vertices, e.g. [ vertex1, vertex2, vertex3 ]
185 """
186 try:
187 nonCutEdges = self.getNonCutEdges()
188 # find all cycle chains
189 cycleChains = []
190 for chain in self.chains:
191 # if the source vertex of the first chain equals to the destination vertex of the last
192 # chain, the chain is a cycle chain
193 if chain[ 0 ][ 0 ] == chain[ -1 ][ 1 ]:
194 cycleChains.append( chain )
You Wang9a589b92016-07-14 09:43:21 -0700195 #main.log.debug( 'Cycle chains: {}'.format( cycleChains ) )
You Wangb1a16c32016-05-23 15:30:52 -0700196 # Get a set of vertices which are the first vertices of a cycle chain (excluding the first
197 # cycle chain), and these vertices are a subset of all cut-vertices
198 subsetOfCutVertices = []
199 if len( cycleChains ) > 1:
200 for cycleChain in cycleChains[ 1: ]:
201 subsetOfCutVertices.append( cycleChain[ 0 ][ 0 ] )
You Wang9a589b92016-07-14 09:43:21 -0700202 #main.log.debug( 'Subset of cut vertices: {}'.format( subsetOfCutVertices ) )
You Wangb1a16c32016-05-23 15:30:52 -0700203 nonCutVertices = []
204 assert nonCutEdges != None
205 for vertex in self.graphDict.keys():
206 if vertex in subsetOfCutVertices:
207 continue
208 vertexIsNonCut = True
209 for neighbor in self.graphDict[ vertex ][ 'edges' ].keys():
210 edge = [ vertex, neighbor ]
211 backwardEdge = [ neighbor, vertex ]
212 if not edge in nonCutEdges and not backwardEdge in nonCutEdges:
213 vertexIsNonCut = False
214 break
215 if vertexIsNonCut:
216 nonCutVertices.append( vertex )
You Wang9a589b92016-07-14 09:43:21 -0700217 #main.log.debug( 'Non-cut-vertices: {}'.format( nonCutVertices ) )
You Wangb1a16c32016-05-23 15:30:52 -0700218 return nonCutVertices
219 except KeyError:
220 main.log.exception( "Graph: KeyError exception found" )
221 return None
222 except AssertionError:
223 main.log.exception( "Graph: AssertionError exception found" )
224 return None
225 except Exception:
226 main.log.exception( "Graph: Uncaught exception" )
227 return None
228
229 def depthFirstSearch( self ):
230 """
231 This function runs a depth-first search and gets DFI of each vertex as well
232 as generates the back edges
233 """
234 try:
235 assert self.graphDict != None and len( self.graphDict ) != 0
236 for vertex in self.graphDict.keys():
237 self.DFI[ vertex ] = -1
238 self.parentVertexInDFS[ vertex ] = ''
239 self.parentEdgeInDFS[ vertex ] = None
240 firstVertex = self.graphDict.keys()[ 0 ]
241 self.currentDFI = 0
242 self.backEdges = {}
243 if not self.depthFirstSearchRecursive( firstVertex ):
244 return main.ERROR
245 return main.TRUE
246 except KeyError:
247 main.log.exception( "Graph: KeyError exception found" )
248 return main.ERROR
249 except AssertionError:
250 main.log.exception( "Graph: AssertionError exception found" )
251 return main.ERROR
252 except Exception:
253 main.log.exception( "Graph: Uncaught exception" )
254 return main.ERROR
255
256 def depthFirstSearchRecursive( self, vertex ):
257 """
258 Recursive function for depth-first search
259 """
260 try:
261 self.DFI[ vertex ] = self.currentDFI
262 self.currentDFI += 1
263 for neighbor in self.graphDict[ vertex ][ 'edges' ].keys():
264 edge = [ vertex, neighbor ]
265 backwardEdge = [ neighbor, vertex ]
266 if neighbor == self.parentVertexInDFS[ vertex ]:
267 continue
268 elif self.DFI[ neighbor ] == -1:
269 self.parentVertexInDFS[ neighbor ] = vertex
270 self.parentEdgeInDFS[ neighbor ] = backwardEdge
271 if not self.depthFirstSearchRecursive( neighbor ):
272 return main.ERROR
273 else:
274 key = self.DFI[ neighbor ]
275 if key in self.backEdges.keys():
276 if not edge in self.backEdges[ key ] and\
277 not backwardEdge in self.backEdges[ key ]:
278 self.backEdges[ key ].append( backwardEdge )
279 else:
280 tempKey = self.DFI[ vertex ]
281 if tempKey in self.backEdges.keys():
282 if not edge in self.backEdges[ tempKey ] and\
283 not backwardEdge in self.backEdges[ tempKey ]:
284 self.backEdges[ key ] = [ backwardEdge ]
285 else:
286 self.backEdges[ key ] = [ backwardEdge ]
287 return main.TRUE
288 except KeyError:
289 main.log.exception( "Graph: KeyError exception found" )
290 return main.ERROR
291 except Exception:
292 main.log.exception( "Graph: Uncaught exception" )
293 return main.ERROR
294
295 def findChains( self ):
296 """
297 This function finds all the chains in chain-decomposition algorithm
298 """
299 keyList = self.backEdges.keys()
300 keyList.sort()
301 vertexIsVisited = {}
302 self.chains = []
303 for vertex in self.graphDict.keys():
304 vertexIsVisited[ vertex ] = False
305 try:
306 for key in keyList:
307 backEdgeList = self.backEdges[ key ]
308 for edge in backEdgeList:
309 chain = []
310 currentEdge = edge
311 sourceVertex = edge[ 0 ]
312 while True:
313 currentVertex = currentEdge[ 0 ]
314 nextVertex = currentEdge[ 1 ]
315 vertexIsVisited[ currentVertex ] = True
316 chain.append( currentEdge )
317 if nextVertex == sourceVertex or vertexIsVisited[ nextVertex ] == True:
318 break
319 currentEdge = self.parentEdgeInDFS[ nextVertex ]
320 self.chains.append( chain )
321 return main.TRUE
322 except KeyError:
323 main.log.exception( "Graph: KeyError exception found" )
324 return main.ERROR
325 except Exception:
326 main.log.exception( "Graph: Uncaught exception" )
327 return main.ERROR