devtools/instrument.py
changeset 9368 10694dd136f3
child 10589 7c23b7de2b8d
equal deleted inserted replaced
9367:c8a5f7f43c03 9368:10694dd136f3
       
     1 # copyright 2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This program is free software: you can redistribute it and/or modify it under
       
     5 # the terms of the GNU Lesser General Public License as published by the Free
       
     6 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     7 # any later version.
       
     8 #
       
     9 # This program is distributed in the hope that it will be useful, but WITHOUT
       
    10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    11 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
       
    12 # details.
       
    13 #
       
    14 # You should have received a copy of the GNU Lesser General Public License along
       
    15 # with this program. If not, see <http://www.gnu.org/licenses/>.
       
    16 """Instrumentation utilities"""
       
    17 
       
    18 import os
       
    19 
       
    20 try:
       
    21     import pygraphviz
       
    22 except ImportError:
       
    23     pygraphviz = None
       
    24 
       
    25 from cubicweb.cwvreg import CWRegistryStore
       
    26 from cubicweb.devtools.devctl import DevConfiguration
       
    27 
       
    28 
       
    29 ALL_COLORS = [
       
    30     "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF", "000000",
       
    31     "800000", "008000", "000080", "808000", "800080", "008080", "808080",
       
    32     "C00000", "00C000", "0000C0", "C0C000", "C000C0", "00C0C0", "C0C0C0",
       
    33     "400000", "004000", "000040", "404000", "400040", "004040", "404040",
       
    34     "200000", "002000", "000020", "202000", "200020", "002020", "202020",
       
    35     "600000", "006000", "000060", "606000", "600060", "006060", "606060",
       
    36     "A00000", "00A000", "0000A0", "A0A000", "A000A0", "00A0A0", "A0A0A0",
       
    37     "E00000", "00E000", "0000E0", "E0E000", "E000E0", "00E0E0", "E0E0E0",
       
    38     ]
       
    39 _COLORS = {}
       
    40 def get_color(key):
       
    41     try:
       
    42         return _COLORS[key]
       
    43     except KeyError:
       
    44         _COLORS[key] = '#'+ALL_COLORS[len(_COLORS) % len(ALL_COLORS)]
       
    45         return _COLORS[key]
       
    46 
       
    47 def warn(msg, *args):
       
    48     print 'WARNING: %s' % (msg % args)
       
    49 
       
    50 def info(msg):
       
    51     print 'INFO: ' + msg
       
    52 
       
    53 
       
    54 class PropagationAnalyzer(object):
       
    55     """Abstract propagation analyzer, providing utility function to extract
       
    56     entities involved in propagation from a schema, as well as propagation
       
    57     rules from hooks (provided they use intrumentalized sets, see
       
    58     :class:`CubeTracerSet`).
       
    59 
       
    60     Concrete classes should at least define `prop_rel` class attribute and
       
    61     implements the `is_root` method.
       
    62 
       
    63     See `localperms` or `nosylist` cubes for example usage (`ccplugin` module).
       
    64     """
       
    65     prop_rel = None # name of the propagation relation
       
    66 
       
    67     def init(self, cube):
       
    68         """Initialize analyze for the given cube, returning the (already loaded)
       
    69         vregistry and a set of entities which we're interested in.
       
    70         """
       
    71         config = DevConfiguration(cube)
       
    72         schema = config.load_schema()
       
    73         vreg = CWRegistryStore(config)
       
    74         vreg.set_schema(schema) # set_schema triggers objects registrations
       
    75         eschemas = set(eschema for eschema in schema.entities()
       
    76                        if self.should_include(eschema))
       
    77         return vreg, eschemas
       
    78 
       
    79     def is_root(self, eschema):
       
    80         """Return `True` if given entity schema is a root of the graph"""
       
    81         raise NotImplementedError()
       
    82 
       
    83     def should_include(self, eschema):
       
    84         """Return `True` if given entity schema should be included by the graph.
       
    85         """
       
    86 
       
    87         if self.prop_rel in eschema.subjrels or self.is_root(eschema):
       
    88             return True
       
    89         return False
       
    90 
       
    91     def prop_edges(self, s_rels, o_rels, eschemas):
       
    92         """Return a set of edges where propagation has been detected.
       
    93 
       
    94         Each edge is defined by a 4-uple (from node, to node, rtype, package)
       
    95         where `rtype` is the relation type bringing from <from node> to <to
       
    96         node> and `package` is the cube adding the rule to the propagation
       
    97         control set (see see :class:`CubeTracerSet`).
       
    98         """
       
    99         schema = iter(eschemas).next().schema
       
   100         prop_edges = set()
       
   101         for rtype in s_rels:
       
   102             found = False
       
   103             for subj, obj in schema.rschema(rtype).rdefs:
       
   104                 if subj in eschemas and obj in eschemas:
       
   105                     found = True
       
   106                     prop_edges.add( (subj, obj, rtype, s_rels.value_cube[rtype]) )
       
   107             if not found:
       
   108                 warn('no rdef match for %s', rtype)
       
   109         for rtype in o_rels:
       
   110             found = False
       
   111             for subj, obj in schema.rschema(rtype).rdefs:
       
   112                 if subj in eschemas and obj in eschemas:
       
   113                     found = True
       
   114                     prop_edges.add( (obj, subj, rtype, o_rels.value_cube[rtype]) )
       
   115             if not found:
       
   116                 warn('no rdef match for %s', rtype)
       
   117         return prop_edges
       
   118 
       
   119     def detect_problems(self, eschemas, edges):
       
   120         """Given the set of analyzed entity schemas and edges between them,
       
   121         return a set of entity schemas where a problem has been detected.
       
   122         """
       
   123         problematic = set()
       
   124         for eschema in eschemas:
       
   125             if self.has_problem(eschema, edges):
       
   126                 problematic.add(eschema)
       
   127         not_problematic = set(eschemas).difference(problematic)
       
   128         if not_problematic:
       
   129             info('nothing problematic in: %s' %
       
   130                  ', '.join(e.type for e in not_problematic))
       
   131         return problematic
       
   132 
       
   133     def has_problem(self, eschema, edges):
       
   134         """Return `True` if the given schema is considered problematic,
       
   135         considering base propagation rules.
       
   136         """
       
   137         root = self.is_root(eschema)
       
   138         has_prop_rel = self.prop_rel in eschema.subjrels
       
   139         # root but no propagation relation
       
   140         if root and not has_prop_rel:
       
   141             warn('%s is root but miss %s', eschema, self.prop_rel)
       
   142             return True
       
   143         # propagated but without propagation relation / not propagated but
       
   144         # with propagation relation
       
   145         if not has_prop_rel and \
       
   146                 any(edge for edge in edges if edge[1] == eschema):
       
   147             warn("%s miss %s but is reached by propagation",
       
   148                  eschema, self.prop_rel)
       
   149             return True
       
   150         elif has_prop_rel and not root:
       
   151             rdef = eschema.rdef(self.prop_rel, takefirst=True)
       
   152             edges = [edge for edge in edges if edge[1] == eschema]
       
   153             if not edges:
       
   154                 warn("%s has %s but isn't reached by "
       
   155                      "propagation", eschema, self.prop_rel)
       
   156                 return True
       
   157             # require_permission relation / propagation rule not added by
       
   158             # the same cube
       
   159             elif not any(edge for edge in edges if edge[-1] == rdef.package):
       
   160                 warn('%s has %s relation / propagation rule'
       
   161                      ' not added by the same cube (%s / %s)', eschema,
       
   162                      self.prop_rel, rdef.package, edges[0][-1])
       
   163                 return True
       
   164         return False
       
   165 
       
   166     def init_graph(self, eschemas, edges, problematic):
       
   167         """Initialize and return graph, adding given nodes (entity schemas) and
       
   168         edges between them.
       
   169 
       
   170         Require pygraphviz installed.
       
   171         """
       
   172         if pygraphviz is None:
       
   173             raise RuntimeError('pygraphviz is not installed')
       
   174         graph = pygraphviz.AGraph(strict=False, directed=True)
       
   175         for eschema in eschemas:
       
   176             if eschema in problematic:
       
   177                 params = {'color': '#ff0000', 'fontcolor': '#ff0000'}
       
   178             else:
       
   179                 params = {}#'color': get_color(eschema.package)}
       
   180             graph.add_node(eschema.type, **params)
       
   181         for subj, obj, rtype, package in edges:
       
   182             graph.add_edge(str(subj), str(obj), label=rtype,
       
   183                            color=get_color(package))
       
   184         return graph
       
   185 
       
   186     def add_colors_legend(self, graph):
       
   187         """Add a legend of used colors to the graph."""
       
   188         for package, color in sorted(_COLORS.iteritems()):
       
   189             graph.add_node(package, color=color, fontcolor=color, shape='record')
       
   190 
       
   191 
       
   192 class CubeTracerSet(object):
       
   193     """Dumb set implementation whose purpose is to keep track of which cube is
       
   194     being loaded when something is added to the set.
       
   195 
       
   196     Results will be found in the `value_cube` attribute dictionary.
       
   197 
       
   198     See `localperms` or `nosylist` cubes for example usage (`hooks` module).
       
   199     """
       
   200     def __init__(self, vreg, wrapped):
       
   201         self.vreg = vreg
       
   202         self.wrapped = wrapped
       
   203         self.value_cube = {}
       
   204 
       
   205     def add(self, value):
       
   206         self.wrapped.add(value)
       
   207         cube = self.vreg.currently_loading_cube
       
   208         if value in self.value_cube:
       
   209             warn('%s is propagated by cube %s and cube %s',
       
   210                  value, self.value_cube[value], cube)
       
   211         else:
       
   212             self.value_cube[value] = cube
       
   213 
       
   214     def __iter__(self):
       
   215         return iter(self.wrapped)
       
   216 
       
   217     def __ior__(self, other):
       
   218         for value in other:
       
   219             self.add(value)
       
   220         return self
       
   221 
       
   222     def __ror__(self, other):
       
   223         other |= self.wrapped
       
   224         return other