blob: 99cabb1ef8906ecedef0c9774afa94e1050ab747 [file] [log] [blame]
# Copyright (c) 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Draw wayland protocols with Graphviz.
Provides a GvPrinter class which is used to output one (or more) wayland
protocols as a graph, showing the relationships between interfaces, their
messages, and how those messages create new interfaces.
"""
from __future__ import absolute_import
from __future__ import print_function
import protocol_util
class GvPrinter(object):
"""Base class for printing graphviz graphs.
The base printer class, has several generic utilities for printing different
graph representations of the protocols.
Users of this class will call the "draw(protocols)" method to print a
graphviz-syntax graph.
"""
def __init__(self):
self.nodes = {}
def force_iface(self, arg):
i_name = arg.attrib.get('interface', 'unknown')
if (i_name, None) not in self.nodes:
self.out('"%s" [label="%s" shape=parallelogram]' % (i_name, i_name))
self.nodes[(i_name, None)] = i_name
return (i_name, None)
def out(self, text):
print(text)
def draw_nodes(self, protocol):
pass
def draw_edges(self, protocol):
pass
def draw(self, protocols):
"""Draw the graph.
Args:
protocols: the list of xml.etree.ElementTree.Element protocols which you
want to draw as a graph. See third_party/wayland/src/protocol/ for more
information.
"""
self.out('digraph g {')
self.out('pack=false;')
self.out('start="random";')
self.out('overlap=false;')
self.out('splines=true;')
self.out('graph [rankdir = "LR"];')
for protocol in protocols:
self.draw_nodes(protocol)
for protocol in protocols:
self.draw_edges(protocol)
self.out('}')
class InterfacesPrinter(GvPrinter):
"""Print protocol interfaces as a graphviz graph.
A specialization of the graph printer which draws interfaces and their
methods as a class diagram, where edges indicate relationships between the
methods.
"""
def __init__(self, draw_enums=False):
super(InterfacesPrinter, self).__init__()
self.draw_enums = draw_enums
def draw_edge(self, src, snk):
for n in [src, snk]:
if n not in self.nodes:
raise Exception('Unable to find node "%s" when drawing (%s, %s)' %
(n, src, snk))
self.out(self.nodes[src] + ' -> ' + self.nodes[snk] + ' [];')
def draw_edges(self, protocol):
"""Draw edges between interfaces and methods.
Draws all the edges:
- From an enum to a method that uses that enum as an argument.
- From an interface to a method that uses that interface as an argument.
- From a method to an interface that invoking that method will create.
Args:
protocol: a wayland <protocol> as an xml.etree.ElementTree.Element.
"""
for interface in protocol.findall('interface'):
i_name = interface.attrib['name']
for message in protocol_util.grab_interface_messages(interface):
m_name = message.attrib['name']
for arg in message.findall('arg'):
if self.draw_enums and 'enum' in arg.attrib:
self.draw_edge((i_name, arg.attrib['enum']), (i_name, m_name))
continue
ty = arg.attrib['type']
if ty == 'new_id':
self.draw_edge((i_name, m_name), self.force_iface(arg))
elif ty == 'object':
self.draw_edge(self.force_iface(arg), (i_name, m_name))
def draw_nodes(self, protocol):
"""Draw record nodes for each interface.
Draws a record-style node for each interface. The header is the interface
name and each subsequent entry is an event or request which that interface
defines.
As a side-effect, this method populates the |nodes| map.
Args:
protocol: a wayland <protocol> as an xml.etree.ElementTree.Element.
"""
for interface in protocol.findall('interface'):
i_name = interface.attrib['name']
i_label = '<i>'+i_name
if self.draw_enums:
for enum in interface.findall('enum'):
e_name = enum.attrib['name']
node_name = i_name + '_' + e_name
fields = [
e.attrib['name'] + ' = ' + e.attrib['value']
for e in enum.findall('entry')
]
self.out('"%s" [label="%s" shape=record]' %
(node_name, '|'.join(['<e>'+e_name]+fields)))
self.nodes[(i_name, e_name)] = node_name+':e'
for count, message in enumerate(
protocol_util.grab_interface_messages(interface)):
m_name = message.attrib['name']
is_request = message.tag == 'request'
m_label = ('<m%d>'%count) + (m_name + ' -\\>' if is_request
else '-\\> ' + m_name)
i_label += '|' + m_label
self.nodes[(i_name, m_name)] = i_name + (':m%d'%count)
self.out('"%s" [label="%s" shape=record]' % (i_name, i_label))
self.nodes[(i_name, None)] = i_name+':i'
class DepsPrinter(GvPrinter):
"""Print message dependencies as a graphviz graph.
A specialization of the graph printer for showind dependencies between
methods. Nodes in this graph are either interfaces or methods, and edges
indicate a relationship of "defines" (when the interface is the method's
'this' object), "uses" (when the interface is an argument), or "creates"
(when the result of the call is the creation of an object).
"""
def __init__(self, draw_all=False):
super(DepsPrinter, self).__init__()
self.draw_all = draw_all
def should_draw_message(self, message):
return self.draw_all or protocol_util.is_constructor(message)
def draw_nodes(self, protocol):
"""Draws a node for each message/interface.
Draws a node for every interface and all of its methods. As a side effect,
calling this method will populate the |nodes| map.
Args:
protocol: a wayland <protocol> as an xml.etree.ElementTree.Element.
"""
self.nodes[(None, None)] = '_unknown_'
self.out('"_unknown_" [label="?" shape=diamond]')
for i in protocol.findall('interface'):
i_name = i.attrib['name']
self.nodes[(i_name, None)] = i_name
self.out('"%s" [shape=box]' % (i_name))
for m in protocol_util.grab_interface_messages(i):
if self.should_draw_message(m):
m_name = m.attrib['name']
m_node = i_name + ':' + m_name
m_shape = 'hexagon' if m.tag == 'event' else 'ellipse'
self.nodes[(i_name, m_name)] = m_node
self.out('"%s" [label="%s" shape=%s]' %
(m_node, m_name, m_shape))
def get_obj(self, arg):
if 'interface' in arg.attrib:
return self.nodes[self.force_iface(arg)]
return self.nodes[(None, None)]
def draw_edges(self, protocol):
"""Draw edges showing message arguments.
Draws edges showing the relationship between the messages and the
interfaces. Bold edges signify "defines" or "creates" relationships, while
dotted edges signify "uses" relationships.
Args:
protocol: a wayland <protocol> as an xml.etree.ElementTree.Element.
"""
for i in protocol.findall('interface'):
i_name = i.attrib['name']
for m in protocol_util.grab_interface_messages(i):
if self.should_draw_message(m):
m_name = m.attrib['name']
self.out('"%s" -> "%s" [style=bold]' %
(self.nodes[(i_name, None)], self.nodes[(i_name, m_name)]))
for pa in m.findall('arg'):
if pa.attrib['type'] == 'object':
self.out('"%s" -> "%s" [style=dotted]' %
(self.get_obj(pa), self.nodes[(i_name, m_name)]))
for pa in m.findall('arg'):
if pa.attrib['type'] == 'new_id':
self.out('"%s" -> "%s" [style=bold]' %
(self.nodes[(i_name, m_name)], self.get_obj(pa)))