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