blob: b1cbdb25b8afd38bc9f36f195ad54b577004e081 [file] [log] [blame]
You Wangb1a16c32016-05-23 15:30:52 -07001#!/usr/bin/env python
Jeremy Ronquillob27ce4c2017-07-17 12:41:28 -07002
3'''
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
13 (at your option) any later version.
14
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
23'''
Jeremy Songsterae01bba2016-07-11 15:39:17 -070024
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
34 - Getting DFI (Depth First Index) and back edges during a DFS
35 - Chain decomposition of a graph
36 - Finding (non-)cut-edges and vertices
37 """
38
39 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
45 # Parent vertex (and edge to that vertex) of each vertex in depth-first search tree
46 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
63 a dictionary, which maps each edge (identified by the neighbor vertex) to a
64 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
72 def compareGraphs( self, graphDictA, graphDictB, vertexAttributes=['edges'], edgeAttributes=['port'] ):
73 """
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 ]
106 # FIXME: the comparison may not work for (sub)attribute values that are of list type
107 # 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:
You Wang9a589b92016-07-14 09:43:21 -0700153 #main.log.debug( "Graph: graphDictA: {}".format( graphDictA ) )
154 #main.log.debug( "Graph: graphDictB: {}".format( graphDictB ) )
155 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 """
169 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
171 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 )
You Wang9a589b92016-07-14 09:43:21 -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 )
You Wang9a589b92016-07-14 09:43:21 -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 ] )
You Wang9a589b92016-07-14 09:43:21 -0700217 #main.log.debug( 'Subset of cut vertices: {}'.format( subsetOfCutVertices ) )
You Wangb1a16c32016-05-23 15:30:52 -0700218 nonCutVertices = []
219 assert nonCutEdges != None
220 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 ]
227 if not edge in nonCutEdges and not backwardEdge in nonCutEdges:
228 vertexIsNonCut = False
229 break
230 if vertexIsNonCut:
231 nonCutVertices.append( vertex )
You Wang9a589b92016-07-14 09:43:21 -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:
250 assert self.graphDict != None and len( self.graphDict ) != 0
251 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():
291 if not edge in self.backEdges[ key ] and\
292 not backwardEdge in self.backEdges[ key ]:
293 self.backEdges[ key ].append( backwardEdge )
294 else:
295 tempKey = self.DFI[ vertex ]
296 if tempKey in self.backEdges.keys():
297 if not edge in self.backEdges[ tempKey ] and\
298 not backwardEdge in self.backEdges[ tempKey ]:
299 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 """
314 keyList = self.backEdges.keys()
315 keyList.sort()
316 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 )
332 if nextVertex == sourceVertex or vertexIsVisited[ nextVertex ] == True:
333 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