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 } 44 45 46 class Error(Exception): 47 pass 48 49 50 def unsafeAppendChild(parent, child): 51 """Append child to parent's list of children, ignoring the possibility that it 52 is already in another node's childNodes list. Requires that the previous 53 parent of child is discarded (to avoid non-tree DOM graphs). 54 This can provide a significant speedup as O(n^2) operations are removed (in 55 particular, each child insertion avoids the need to traverse the old parent's 56 entire list of children).""" 57 child.parentNode = None 58 parent.appendChild(child) 59 child.parentNode = parent 60 61 62 def TransformByAlphabetizing(node): 63 """Transform the given XML by alphabetizing specific node types according to 64 the rules in ALPHABETIZATION_RULES. 65 66 Args: 67 node: The minidom node to transform. 68 69 Returns: 70 The minidom node, with children appropriately alphabetized. Note that the 71 transformation is done in-place, i.e. the original minidom tree is modified 72 directly. 73 """ 74 if node.nodeType != xml.dom.minidom.Node.ELEMENT_NODE: 75 for c in node.childNodes: TransformByAlphabetizing(c) 76 return node 77 78 # Element node with a tag name that we alphabetize the children of? 79 if node.tagName in ALPHABETIZATION_RULES: 80 # Put subnodes in a list of node,key pairs to allow for custom sorting. 81 subtag, key_function = ALPHABETIZATION_RULES[node.tagName] 82 subnodes = [] 83 sort_key = -1 84 pending_node_indices = [] 85 for c in node.childNodes: 86 if (c.nodeType == xml.dom.minidom.Node.ELEMENT_NODE and 87 c.tagName == subtag): 88 sort_key = key_function(c) 89 # Replace sort keys for delayed nodes. 90 for idx in pending_node_indices: 91 subnodes[idx][1] = sort_key 92 pending_node_indices = [] 93 else: 94 # Subnodes that we don't want to rearrange use the next node's key, 95 # so they stay in the same relative position. 96 # Therefore we delay setting key until the next node is found. 97 pending_node_indices.append(len(subnodes)) 98 99 subnodes.append( [c, sort_key] ) 100 101 # Use last sort key for trailing unknown nodes. 102 for idx in pending_node_indices: 103 subnodes[idx][1] = sort_key 104 105 # Sort the subnode list. 106 subnodes.sort(key=lambda pair: pair[1]) 107 108 # Re-add the subnodes, transforming each recursively. 109 while node.firstChild: 110 node.removeChild(node.firstChild) 111 for (c, _) in subnodes: 112 unsafeAppendChild(node, TransformByAlphabetizing(c)) 113 return node 114 115 # Recursively handle other element nodes and other node types. 116 for c in node.childNodes: TransformByAlphabetizing(c) 117 return node 118 119 120 def PrettyPrint(raw_xml): 121 """Pretty-print the given XML. 122 123 Args: 124 raw_xml: The contents of the histograms XML file, as a string. 125 126 Returns: 127 The pretty-printed version. 128 """ 129 tree = xml.dom.minidom.parseString(raw_xml) 130 tree = TransformByAlphabetizing(tree) 131 return print_style.GetPrintStyle().PrettyPrintNode(tree) 132 133 134 def main(): 135 logging.basicConfig(level=logging.INFO) 136 137 presubmit = ('--presubmit' in sys.argv) 138 139 histograms_filename = 'histograms.xml' 140 histograms_backup_filename = 'histograms.before.pretty-print.xml' 141 142 # If there is a histograms.xml in the current working directory, use that. 143 # Otherwise, use the one residing in the same directory as this script. 144 histograms_dir = os.getcwd() 145 if not os.path.isfile(os.path.join(histograms_dir, histograms_filename)): 146 histograms_dir = path_utils.ScriptDir() 147 148 histograms_pathname = os.path.join(histograms_dir, histograms_filename) 149 histograms_backup_pathname = os.path.join(histograms_dir, 150 histograms_backup_filename) 151 152 logging.info('Loading %s...' % os.path.relpath(histograms_pathname)) 153 with open(histograms_pathname, 'rb') as f: 154 xml = f.read() 155 156 # Check there are no CR ('\r') characters in the file. 157 if '\r' in xml: 158 logging.info('DOS-style line endings (CR characters) detected - these are ' 159 'not allowed. Please run dos2unix %s' % histograms_filename) 160 sys.exit(1) 161 162 logging.info('Pretty-printing...') 163 try: 164 pretty = PrettyPrint(xml) 165 except Error: 166 logging.error('Aborting parsing due to fatal errors.') 167 sys.exit(1) 168 169 if xml == pretty: 170 logging.info('%s is correctly pretty-printed.' % histograms_filename) 171 sys.exit(0) 172 if presubmit: 173 logging.info('%s is not formatted correctly; run pretty_print.py to fix.' % 174 histograms_filename) 175 sys.exit(1) 176 if not diff_util.PromptUserToAcceptDiff( 177 xml, pretty, 178 'Is the prettified version acceptable?'): 179 logging.error('Aborting') 180 return 181 182 logging.info('Creating backup file %s' % histograms_backup_filename) 183 shutil.move(histograms_pathname, histograms_backup_pathname) 184 185 logging.info('Writing new %s file' % histograms_filename) 186 with open(histograms_pathname, 'wb') as f: 187 f.write(pretty) 188 189 190 if __name__ == '__main__': 191 main() 192