Add unit test for the Settings API Bubble.
[chromium-blink-merge.git] / tools / checkdeps / graphdeps.py
blobc04af2399b8d2959ab6f12face4b53f93caea283
1 #!/usr/bin/env python
2 # Copyright 2013 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Dumps a graph of allowed and disallowed inter-module dependencies described
7 by the DEPS files in the source tree. Supports DOT and PNG as the output format.
9 Enables filtering and differential highlighting of parts of the graph based on
10 the specified criteria. This allows for a much easier visual analysis of the
11 dependencies, including answering questions such as "if a new source must
12 depend on modules A, B, and C, what valid options among the existing modules
13 are there to put it in."
15 See builddeps.py for a detailed description of the DEPS format.
16 """
18 import os
19 import optparse
20 import pipes
21 import re
22 import sys
24 from builddeps import DepsBuilder
25 from rules import Rule
28 class DepsGrapher(DepsBuilder):
29 """Parses include_rules from DEPS files and outputs a DOT graph of the
30 allowed and disallowed dependencies between directories and specific file
31 regexps. Can generate only a subgraph of the whole dependency graph
32 corresponding to the provided inclusion and exclusion regexp filters.
33 Also can highlight fanins and/or fanouts of certain nodes matching the
34 provided regexp patterns.
35 """
37 def __init__(self,
38 base_directory,
39 verbose,
40 being_tested,
41 ignore_temp_rules,
42 ignore_specific_rules,
43 hide_disallowed_deps,
44 out_file,
45 out_format,
46 layout_engine,
47 unflatten_graph,
48 incl,
49 excl,
50 hilite_fanins,
51 hilite_fanouts):
52 """Creates a new DepsGrapher.
54 Args:
55 base_directory: OS-compatible path to root of checkout, e.g. C:\chr\src.
56 verbose: Set to true for debug output.
57 being_tested: Set to true to ignore the DEPS file at tools/graphdeps/DEPS.
58 ignore_temp_rules: Ignore rules that start with Rule.TEMP_ALLOW ("!").
59 ignore_specific_rules: Ignore rules from specific_include_rules sections.
60 hide_disallowed_deps: Hide disallowed dependencies from the output graph.
61 out_file: Output file name.
62 out_format: Output format (anything GraphViz dot's -T option supports).
63 layout_engine: Layout engine for formats other than 'dot'
64 (anything that GraphViz dot's -K option supports).
65 unflatten_graph: Try to reformat the output graph so it is narrower and
66 taller. Helps fight overly flat and wide graphs, but
67 sometimes produces a worse result.
68 incl: Include only nodes matching this regexp; such nodes' fanin/fanout
69 is also included.
70 excl: Exclude nodes matching this regexp; such nodes' fanin/fanout is
71 processed independently.
72 hilite_fanins: Highlight fanins of nodes matching this regexp with a
73 different edge and node color.
74 hilite_fanouts: Highlight fanouts of nodes matching this regexp with a
75 different edge and node color.
76 """
77 DepsBuilder.__init__(
78 self,
79 base_directory,
80 verbose,
81 being_tested,
82 ignore_temp_rules,
83 ignore_specific_rules)
85 self.ignore_temp_rules = ignore_temp_rules
86 self.ignore_specific_rules = ignore_specific_rules
87 self.hide_disallowed_deps = hide_disallowed_deps
88 self.out_file = out_file
89 self.out_format = out_format
90 self.layout_engine = layout_engine
91 self.unflatten_graph = unflatten_graph
92 self.incl = incl
93 self.excl = excl
94 self.hilite_fanins = hilite_fanins
95 self.hilite_fanouts = hilite_fanouts
97 self.deps = set()
99 def DumpDependencies(self):
100 """ Builds a dependency rule table and dumps the corresponding dependency
101 graph to all requested formats."""
102 self._BuildDepsGraph(self.base_directory)
103 self._DumpDependencies()
105 def _BuildDepsGraph(self, full_path):
106 """Recursively traverses the source tree starting at the specified directory
107 and builds a dependency graph representation in self.deps."""
108 rel_path = os.path.relpath(full_path, self.base_directory)
109 #if re.search(self.incl, rel_path) and not re.search(self.excl, rel_path):
110 rules = self.GetDirectoryRules(full_path)
111 if rules:
112 deps = rules.AsDependencyTuples(
113 include_general_rules=True,
114 include_specific_rules=not self.ignore_specific_rules)
115 self.deps.update(deps)
117 for item in os.listdir(full_path):
118 next_full_path = os.path.join(full_path, item)
119 if os.path.isdir(next_full_path):
120 self._BuildDepsGraph(next_full_path)
122 def _DumpDependencies(self):
123 """Dumps the built dependency graph to the specified file with specified
124 format."""
125 if self.out_format == 'dot' and not self.layout_engine:
126 if self.unflatten_graph:
127 pipe = pipes.Template()
128 pipe.append('unflatten -l 2 -c 3', '--')
129 out = pipe.open(self.out_file, 'w')
130 else:
131 out = open(self.out_file, 'w')
132 else:
133 pipe = pipes.Template()
134 if self.unflatten_graph:
135 pipe.append('unflatten -l 2 -c 3', '--')
136 dot_cmd = 'dot -T' + self.out_format
137 if self.layout_engine:
138 dot_cmd += ' -K' + self.layout_engine
139 pipe.append(dot_cmd, '--')
140 out = pipe.open(self.out_file, 'w')
142 self._DumpDependenciesImpl(self.deps, out)
143 out.close()
145 def _DumpDependenciesImpl(self, deps, out):
146 """Computes nodes' and edges' properties for the dependency graph |deps| and
147 carries out the actual dumping to a file/pipe |out|."""
148 deps_graph = dict()
149 deps_srcs = set()
151 # Pre-initialize the graph with src->(dst, allow) pairs.
152 for (allow, src, dst) in deps:
153 if allow == Rule.TEMP_ALLOW and self.ignore_temp_rules:
154 continue
156 deps_srcs.add(src)
157 if src not in deps_graph:
158 deps_graph[src] = []
159 deps_graph[src].append((dst, allow))
161 # Add all hierarchical parents too, in case some of them don't have their
162 # own DEPS, and therefore are missing from the list of rules. Those will
163 # be recursively populated with their parents' rules in the next block.
164 parent_src = os.path.dirname(src)
165 while parent_src:
166 if parent_src not in deps_graph:
167 deps_graph[parent_src] = []
168 parent_src = os.path.dirname(parent_src)
170 # For every node, propagate its rules down to all its children.
171 deps_srcs = list(deps_srcs)
172 deps_srcs.sort()
173 for src in deps_srcs:
174 parent_src = os.path.dirname(src)
175 if parent_src:
176 # We presort the list, so parents are guaranteed to precede children.
177 assert parent_src in deps_graph,\
178 "src: %s, parent_src: %s" % (src, parent_src)
179 for (dst, allow) in deps_graph[parent_src]:
180 # Check that this node does not explicitly override a rule from the
181 # parent that we're about to add.
182 if ((dst, Rule.ALLOW) not in deps_graph[src]) and \
183 ((dst, Rule.TEMP_ALLOW) not in deps_graph[src]) and \
184 ((dst, Rule.DISALLOW) not in deps_graph[src]):
185 deps_graph[src].append((dst, allow))
187 node_props = {}
188 edges = []
190 # 1) Populate a list of edge specifications in DOT format;
191 # 2) Populate a list of computed raw node attributes to be output as node
192 # specifications in DOT format later on.
193 # Edges and nodes are emphasized with color and line/border weight depending
194 # on how many of incl/excl/hilite_fanins/hilite_fanouts filters they hit,
195 # and in what way.
196 for src in deps_graph.keys():
197 for (dst, allow) in deps_graph[src]:
198 if allow == Rule.DISALLOW and self.hide_disallowed_deps:
199 continue
201 if allow == Rule.ALLOW and src == dst:
202 continue
204 edge_spec = "%s->%s" % (src, dst)
205 if not re.search(self.incl, edge_spec) or \
206 re.search(self.excl, edge_spec):
207 continue
209 if src not in node_props:
210 node_props[src] = {'hilite': None, 'degree': 0}
211 if dst not in node_props:
212 node_props[dst] = {'hilite': None, 'degree': 0}
214 edge_weight = 1
216 if self.hilite_fanouts and re.search(self.hilite_fanouts, src):
217 node_props[src]['hilite'] = 'lightgreen'
218 node_props[dst]['hilite'] = 'lightblue'
219 node_props[dst]['degree'] += 1
220 edge_weight += 1
222 if self.hilite_fanins and re.search(self.hilite_fanins, dst):
223 node_props[src]['hilite'] = 'lightblue'
224 node_props[dst]['hilite'] = 'lightgreen'
225 node_props[src]['degree'] += 1
226 edge_weight += 1
228 if allow == Rule.ALLOW:
229 edge_color = (edge_weight > 1) and 'blue' or 'green'
230 edge_style = 'solid'
231 elif allow == Rule.TEMP_ALLOW:
232 edge_color = (edge_weight > 1) and 'blue' or 'green'
233 edge_style = 'dashed'
234 else:
235 edge_color = 'red'
236 edge_style = 'dashed'
237 edges.append(' "%s" -> "%s" [style=%s,color=%s,penwidth=%d];' % \
238 (src, dst, edge_style, edge_color, edge_weight))
240 # Reformat the computed raw node attributes into a final DOT representation.
241 nodes = []
242 for (node, attrs) in node_props.iteritems():
243 attr_strs = []
244 if attrs['hilite']:
245 attr_strs.append('style=filled,fillcolor=%s' % attrs['hilite'])
246 attr_strs.append('penwidth=%d' % (attrs['degree'] or 1))
247 nodes.append(' "%s" [%s];' % (node, ','.join(attr_strs)))
249 # Output nodes and edges to |out| (can be a file or a pipe).
250 edges.sort()
251 nodes.sort()
252 out.write('digraph DEPS {\n'
253 ' fontsize=8;\n')
254 out.write('\n'.join(nodes))
255 out.write('\n\n')
256 out.write('\n'.join(edges))
257 out.write('\n}\n')
258 out.close()
261 def PrintUsage():
262 print """Usage: python graphdeps.py [--root <root>]
264 --root ROOT Specifies the repository root. This defaults to "../../.."
265 relative to the script file. This will be correct given the
266 normal location of the script in "<root>/tools/graphdeps".
268 --(others) There are a few lesser-used options; run with --help to show them.
270 Examples:
271 Dump the whole dependency graph:
272 graphdeps.py
273 Find a suitable place for a new source that must depend on /apps and
274 /content/browser/renderer_host. Limit potential candidates to /apps,
275 /chrome/browser and content/browser, and descendants of those three.
276 Generate both DOT and PNG output. The output will highlight the fanins
277 of /apps and /content/browser/renderer_host. Overlapping nodes in both fanins
278 will be emphasized by a thicker border. Those nodes are the ones that are
279 allowed to depend on both targets, therefore they are all legal candidates
280 to place the new source in:
281 graphdeps.py \
282 --root=./src \
283 --out=./DEPS.svg \
284 --format=svg \
285 --incl='^(apps|chrome/browser|content/browser)->.*' \
286 --excl='.*->third_party' \
287 --fanin='^(apps|content/browser/renderer_host)$' \
288 --ignore-specific-rules \
289 --ignore-temp-rules"""
292 def main():
293 option_parser = optparse.OptionParser()
294 option_parser.add_option(
295 "", "--root",
296 default="", dest="base_directory",
297 help="Specifies the repository root. This defaults "
298 "to '../../..' relative to the script file, which "
299 "will normally be the repository root.")
300 option_parser.add_option(
301 "-f", "--format",
302 dest="out_format", default="dot",
303 help="Output file format. "
304 "Can be anything that GraphViz dot's -T option supports. "
305 "The most useful ones are: dot (text), svg (image), pdf (image)."
306 "NOTES: dotty has a known problem with fonts when displaying DOT "
307 "files on Ubuntu - if labels are unreadable, try other formats.")
308 option_parser.add_option(
309 "-o", "--out",
310 dest="out_file", default="DEPS",
311 help="Output file name. If the name does not end in an extension "
312 "matching the output format, that extension is automatically "
313 "appended.")
314 option_parser.add_option(
315 "-l", "--layout-engine",
316 dest="layout_engine", default="",
317 help="Layout rendering engine. "
318 "Can be anything that GraphViz dot's -K option supports. "
319 "The most useful are in decreasing order: dot, fdp, circo, osage. "
320 "NOTE: '-f dot' and '-f dot -l dot' are different: the former "
321 "will dump a raw DOT graph and stop; the latter will further "
322 "filter it through 'dot -Tdot -Kdot' layout engine.")
323 option_parser.add_option(
324 "-i", "--incl",
325 default="^.*$", dest="incl",
326 help="Include only edges of the graph that match the specified regexp. "
327 "The regexp is applied to edges of the graph formatted as "
328 "'source_node->target_node', where the '->' part is vebatim. "
329 "Therefore, a reliable regexp should look like "
330 "'^(chrome|chrome/browser|chrome/common)->content/public/browser$' "
331 "or similar, with both source and target node regexps present, "
332 "explicit ^ and $, and otherwise being as specific as possible.")
333 option_parser.add_option(
334 "-e", "--excl",
335 default="^$", dest="excl",
336 help="Exclude dependent nodes that match the specified regexp. "
337 "See --incl for details on the format.")
338 option_parser.add_option(
339 "", "--fanin",
340 default="", dest="hilite_fanins",
341 help="Highlight fanins of nodes matching the specified regexp.")
342 option_parser.add_option(
343 "", "--fanout",
344 default="", dest="hilite_fanouts",
345 help="Highlight fanouts of nodes matching the specified regexp.")
346 option_parser.add_option(
347 "", "--ignore-temp-rules",
348 action="store_true", dest="ignore_temp_rules", default=False,
349 help="Ignore !-prefixed (temporary) rules in DEPS files.")
350 option_parser.add_option(
351 "", "--ignore-specific-rules",
352 action="store_true", dest="ignore_specific_rules", default=False,
353 help="Ignore specific_include_rules section of DEPS files.")
354 option_parser.add_option(
355 "", "--hide-disallowed-deps",
356 action="store_true", dest="hide_disallowed_deps", default=False,
357 help="Hide disallowed dependencies in the output graph.")
358 option_parser.add_option(
359 "", "--unflatten",
360 action="store_true", dest="unflatten_graph", default=False,
361 help="Try to reformat the output graph so it is narrower and taller. "
362 "Helps fight overly flat and wide graphs, but sometimes produces "
363 "inferior results.")
364 option_parser.add_option(
365 "-v", "--verbose",
366 action="store_true", default=False,
367 help="Print debug logging")
368 options, args = option_parser.parse_args()
370 if not options.out_file.endswith(options.out_format):
371 options.out_file += '.' + options.out_format
373 deps_grapher = DepsGrapher(
374 base_directory=options.base_directory,
375 verbose=options.verbose,
376 being_tested=False,
378 ignore_temp_rules=options.ignore_temp_rules,
379 ignore_specific_rules=options.ignore_specific_rules,
380 hide_disallowed_deps=options.hide_disallowed_deps,
382 out_file=options.out_file,
383 out_format=options.out_format,
384 layout_engine=options.layout_engine,
385 unflatten_graph=options.unflatten_graph,
387 incl=options.incl,
388 excl=options.excl,
389 hilite_fanins=options.hilite_fanins,
390 hilite_fanouts=options.hilite_fanouts)
392 if len(args) > 0:
393 PrintUsage()
394 return 1
396 print 'Using base directory: ', deps_grapher.base_directory
397 print 'include nodes : ', options.incl
398 print 'exclude nodes : ', options.excl
399 print 'highlight fanins of : ', options.hilite_fanins
400 print 'highlight fanouts of: ', options.hilite_fanouts
402 deps_grapher.DumpDependencies()
403 return 0
406 if '__main__' == __name__:
407 sys.exit(main())