Home | History | Annotate | Download | only in histograms
      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.
      5 
      6 """Pretty-prints the histograms.xml file, alphabetizing tags, wrapping text
      7 at 80 chars, enforcing standard attribute ordering, and standardizing
      8 indentation.
      9 
     10 This is quite a bit more complicated than just calling tree.toprettyxml();
     11 we need additional customization, like special attribute ordering in tags
     12 and wrapping text nodes, so we implement our own full custom XML pretty-printer.
     13 """
     14 
     15 from __future__ import with_statement
     16 
     17 import logging
     18 import os
     19 import shutil
     20 import sys
     21 import xml.dom.minidom
     22 
     23 import print_style
     24 
     25 sys.path.insert(1, os.path.join(sys.path[0], '..', '..', 'python'))
     26 from google import path_utils
     27 
     28 # Import the metrics/common module.
     29 sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'common'))
     30 import diff_util
     31 
     32 # Tags whose children we want to alphabetize. The key is the parent tag name,
     33 # and the value is a pair of the tag name of the children we want to sort,
     34 # and a key function that maps each child node to the desired sort key.
     35 ALPHABETIZATION_RULES = {
     36   'histograms': ('histogram', lambda n: n.attributes['name'].value.lower()),
     37   'enums': ('enum', lambda n: n.attributes['name'].value.lower()),
     38   'enum': ('int', lambda n: int(n.attributes['value'].value)),
     39   'histogram_suffixes_list': (
     40       'histogram_suffixes', lambda n: n.attributes['name'].value.lower()),
     41   'histogram_suffixes': ('affected-histogram',
     42                          lambda n: n.attributes['name'].value.lower()),
     43   # TODO(yiyaoliu): Remove fieldtrial related pieces when it is not used.
     44   'fieldtrials': ('fieldtrial', lambda n: n.attributes['name'].value.lower()),
     45   'fieldtrial': ('affected-histogram',
     46                  lambda n: n.attributes['name'].value.lower()),
     47 }
     48 
     49 
     50 class Error(Exception):
     51   pass
     52 
     53 
     54 def unsafeAppendChild(parent, child):
     55   """Append child to parent's list of children, ignoring the possibility that it
     56   is already in another node's childNodes list.  Requires that the previous
     57   parent of child is discarded (to avoid non-tree DOM graphs).
     58   This can provide a significant speedup as O(n^2) operations are removed (in
     59   particular, each child insertion avoids the need to traverse the old parent's
     60   entire list of children)."""
     61   child.parentNode = None
     62   parent.appendChild(child)
     63   child.parentNode = parent
     64 
     65 
     66 def TransformByAlphabetizing(node):
     67   """Transform the given XML by alphabetizing specific node types according to
     68   the rules in ALPHABETIZATION_RULES.
     69 
     70   Args:
     71     node: The minidom node to transform.
     72 
     73   Returns:
     74     The minidom node, with children appropriately alphabetized. Note that the
     75     transformation is done in-place, i.e. the original minidom tree is modified
     76     directly.
     77   """
     78   if node.nodeType != xml.dom.minidom.Node.ELEMENT_NODE:
     79     for c in node.childNodes: TransformByAlphabetizing(c)
     80     return node
     81 
     82   # Element node with a tag name that we alphabetize the children of?
     83   if node.tagName in ALPHABETIZATION_RULES:
     84     # Put subnodes in a list of node,key pairs to allow for custom sorting.
     85     subtag, key_function = ALPHABETIZATION_RULES[node.tagName]
     86     subnodes = []
     87     last_key = -1
     88     for c in node.childNodes:
     89       if (c.nodeType == xml.dom.minidom.Node.ELEMENT_NODE and
     90           c.tagName == subtag):
     91         last_key = key_function(c)
     92       # Subnodes that we don't want to rearrange use the last node's key,
     93       # so they stay in the same relative position.
     94       subnodes.append( (c, last_key) )
     95 
     96     # Sort the subnode list.
     97     subnodes.sort(key=lambda pair: pair[1])
     98 
     99     # Re-add the subnodes, transforming each recursively.
    100     while node.firstChild:
    101       node.removeChild(node.firstChild)
    102     for (c, _) in subnodes:
    103       unsafeAppendChild(node, TransformByAlphabetizing(c))
    104     return node
    105 
    106   # Recursively handle other element nodes and other node types.
    107   for c in node.childNodes: TransformByAlphabetizing(c)
    108   return node
    109 
    110 
    111 def PrettyPrint(raw_xml):
    112   """Pretty-print the given XML.
    113 
    114   Args:
    115     raw_xml: The contents of the histograms XML file, as a string.
    116 
    117   Returns:
    118     The pretty-printed version.
    119   """
    120   tree = xml.dom.minidom.parseString(raw_xml)
    121   tree = TransformByAlphabetizing(tree)
    122   return print_style.GetPrintStyle().PrettyPrintNode(tree)
    123 
    124 
    125 def main():
    126   logging.basicConfig(level=logging.INFO)
    127 
    128   presubmit = ('--presubmit' in sys.argv)
    129 
    130   histograms_filename = 'histograms.xml'
    131   histograms_backup_filename = 'histograms.before.pretty-print.xml'
    132 
    133   # If there is a histograms.xml in the current working directory, use that.
    134   # Otherwise, use the one residing in the same directory as this script.
    135   histograms_dir = os.getcwd()
    136   if not os.path.isfile(os.path.join(histograms_dir, histograms_filename)):
    137     histograms_dir = path_utils.ScriptDir()
    138 
    139   histograms_pathname = os.path.join(histograms_dir, histograms_filename)
    140   histograms_backup_pathname = os.path.join(histograms_dir,
    141                                             histograms_backup_filename)
    142 
    143   logging.info('Loading %s...' % os.path.relpath(histograms_pathname))
    144   with open(histograms_pathname, 'rb') as f:
    145     xml = f.read()
    146 
    147   # Check there are no CR ('\r') characters in the file.
    148   if '\r' in xml:
    149     logging.info('DOS-style line endings (CR characters) detected - these are '
    150                  'not allowed. Please run dos2unix %s' % histograms_filename)
    151     sys.exit(1)
    152 
    153   logging.info('Pretty-printing...')
    154   try:
    155     pretty = PrettyPrint(xml)
    156   except Error:
    157     logging.error('Aborting parsing due to fatal errors.')
    158     sys.exit(1)
    159 
    160   if xml == pretty:
    161     logging.info('%s is correctly pretty-printed.' % histograms_filename)
    162     sys.exit(0)
    163   if presubmit:
    164     logging.info('%s is not formatted correctly; run pretty_print.py to fix.' %
    165                  histograms_filename)
    166     sys.exit(1)
    167   if not diff_util.PromptUserToAcceptDiff(
    168       xml, pretty,
    169       'Is the prettified version acceptable?'):
    170     logging.error('Aborting')
    171     return
    172 
    173   logging.info('Creating backup file %s' % histograms_backup_filename)
    174   shutil.move(histograms_pathname, histograms_backup_pathname)
    175 
    176   logging.info('Writing new %s file' % histograms_filename)
    177   with open(histograms_pathname, 'wb') as f:
    178     f.write(pretty)
    179 
    180 
    181 if __name__ == '__main__':
    182   main()
    183