blob: 849baa41b4406921a7c48dbe0aa8a405091de2b6 [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:
138 main.log.debug( "Graph: graphDictA: {}".format( graphDictA ) )
139 main.log.debug( "Graph: graphDictB: {}".format( graphDictB ) )
140 return result
141 except TypeError:
142 main.log.exception( "Graph: TypeError exception found" )
143 return main.ERROR
144 except KeyError:
145 main.log.exception( "Graph: KeyError exception found" )
146 return main.ERROR
147 except Exception:
148 main.log.exception( "Graph: Uncaught exception" )
149 return main.ERROR
150
151 def getNonCutEdges( self ):
152 """
153 Get a list of non-cut-edges (non-bridges).
154 The definition of a cut-edge (bridge) is: the deletion of a cut-edge will
155 increase the number of connected component of a graph.
156 The function is realized by impelementing Schmidt's algorithm based on
157 chain decomposition.
158 Returns a list of edges, e.g.
159 [ [ vertex1, vertex2 ], [ vertex2, vertex3 ] ]
160 """
161 try:
162 if not self.depthFirstSearch():
163 return None
164 if not self.findChains():
165 return None
166 nonCutEdges = []
167 for chain in self.chains:
168 for edge in chain:
169 nonCutEdges.append( edge )
170 main.log.debug( 'Non-cut-edges: {}'.format( nonCutEdges ) )
171 return nonCutEdges
172 except Exception:
173 main.log.exception( "Graph: Uncaught exception" )
174 return None
175
176 def getNonCutVertices( self ):
177 """
178 Get a list of non-cut-vertices.
179 The definition of a cut-vertex is: the deletion of a cut-vertex will
180 increase the number of connected component of a graph.
181 The function is realized by impelementing Schmidt's algorithm based on
182 chain decomposition.
183 Returns a list of vertices, e.g. [ vertex1, vertex2, vertex3 ]
184 """
185 try:
186 nonCutEdges = self.getNonCutEdges()
187 # find all cycle chains
188 cycleChains = []
189 for chain in self.chains:
190 # if the source vertex of the first chain equals to the destination vertex of the last
191 # chain, the chain is a cycle chain
192 if chain[ 0 ][ 0 ] == chain[ -1 ][ 1 ]:
193 cycleChains.append( chain )
194 main.log.debug( 'Cycle chains: {}'.format( cycleChains ) )
195 # Get a set of vertices which are the first vertices of a cycle chain (excluding the first
196 # cycle chain), and these vertices are a subset of all cut-vertices
197 subsetOfCutVertices = []
198 if len( cycleChains ) > 1:
199 for cycleChain in cycleChains[ 1: ]:
200 subsetOfCutVertices.append( cycleChain[ 0 ][ 0 ] )
201 main.log.debug( 'Subset of cut vertices: {}'.format( subsetOfCutVertices ) )
202 nonCutVertices = []
203 assert nonCutEdges != None
204 for vertex in self.graphDict.keys():
205 if vertex in subsetOfCutVertices:
206 continue
207 vertexIsNonCut = True
208 for neighbor in self.graphDict[ vertex ][ 'edges' ].keys():
209 edge = [ vertex, neighbor ]
210 backwardEdge = [ neighbor, vertex ]
211 if not edge in nonCutEdges and not backwardEdge in nonCutEdges:
212 vertexIsNonCut = False
213 break
214 if vertexIsNonCut:
215 nonCutVertices.append( vertex )
216 main.log.debug( 'Non-cut-vertices: {}'.format( nonCutVertices ) )
217 return nonCutVertices
218 except KeyError:
219 main.log.exception( "Graph: KeyError exception found" )
220 return None
221 except AssertionError:
222 main.log.exception( "Graph: AssertionError exception found" )
223 return None
224 except Exception:
225 main.log.exception( "Graph: Uncaught exception" )
226 return None
227
228 def depthFirstSearch( self ):
229 """
230 This function runs a depth-first search and gets DFI of each vertex as well
231 as generates the back edges
232 """
233 try:
234 assert self.graphDict != None and len( self.graphDict ) != 0
235 for vertex in self.graphDict.keys():
236 self.DFI[ vertex ] = -1
237 self.parentVertexInDFS[ vertex ] = ''
238 self.parentEdgeInDFS[ vertex ] = None
239 firstVertex = self.graphDict.keys()[ 0 ]
240 self.currentDFI = 0
241 self.backEdges = {}
242 if not self.depthFirstSearchRecursive( firstVertex ):
243 return main.ERROR
244 return main.TRUE
245 except KeyError:
246 main.log.exception( "Graph: KeyError exception found" )
247 return main.ERROR
248 except AssertionError:
249 main.log.exception( "Graph: AssertionError exception found" )
250 return main.ERROR
251 except Exception:
252 main.log.exception( "Graph: Uncaught exception" )
253 return main.ERROR
254
255 def depthFirstSearchRecursive( self, vertex ):
256 """
257 Recursive function for depth-first search
258 """
259 try:
260 self.DFI[ vertex ] = self.currentDFI
261 self.currentDFI += 1
262 for neighbor in self.graphDict[ vertex ][ 'edges' ].keys():
263 edge = [ vertex, neighbor ]
264 backwardEdge = [ neighbor, vertex ]
265 if neighbor == self.parentVertexInDFS[ vertex ]:
266 continue
267 elif self.DFI[ neighbor ] == -1:
268 self.parentVertexInDFS[ neighbor ] = vertex
269 self.parentEdgeInDFS[ neighbor ] = backwardEdge
270 if not self.depthFirstSearchRecursive( neighbor ):
271 return main.ERROR
272 else:
273 key = self.DFI[ neighbor ]
274 if key in self.backEdges.keys():
275 if not edge in self.backEdges[ key ] and\
276 not backwardEdge in self.backEdges[ key ]:
277 self.backEdges[ key ].append( backwardEdge )
278 else:
279 tempKey = self.DFI[ vertex ]
280 if tempKey in self.backEdges.keys():
281 if not edge in self.backEdges[ tempKey ] and\
282 not backwardEdge in self.backEdges[ tempKey ]:
283 self.backEdges[ key ] = [ backwardEdge ]
284 else:
285 self.backEdges[ key ] = [ backwardEdge ]
286 return main.TRUE
287 except KeyError:
288 main.log.exception( "Graph: KeyError exception found" )
289 return main.ERROR
290 except Exception:
291 main.log.exception( "Graph: Uncaught exception" )
292 return main.ERROR
293
294 def findChains( self ):
295 """
296 This function finds all the chains in chain-decomposition algorithm
297 """
298 keyList = self.backEdges.keys()
299 keyList.sort()
300 vertexIsVisited = {}
301 self.chains = []
302 for vertex in self.graphDict.keys():
303 vertexIsVisited[ vertex ] = False
304 try:
305 for key in keyList:
306 backEdgeList = self.backEdges[ key ]
307 for edge in backEdgeList:
308 chain = []
309 currentEdge = edge
310 sourceVertex = edge[ 0 ]
311 while True:
312 currentVertex = currentEdge[ 0 ]
313 nextVertex = currentEdge[ 1 ]
314 vertexIsVisited[ currentVertex ] = True
315 chain.append( currentEdge )
316 if nextVertex == sourceVertex or vertexIsVisited[ nextVertex ] == True:
317 break
318 currentEdge = self.parentEdgeInDFS[ nextVertex ]
319 self.chains.append( chain )
320 return main.TRUE
321 except KeyError:
322 main.log.exception( "Graph: KeyError exception found" )
323 return main.ERROR
324 except Exception:
325 main.log.exception( "Graph: Uncaught exception" )
326 return main.ERROR