1#!/usr/bin/env python 2# Copyright (c) 2012 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""" Output file objects for generator. """ 7 8import difflib 9import os 10import time 11import subprocess 12import sys 13 14from idl_log import ErrOut, InfoOut, WarnOut 15from idl_option import GetOption, Option, ParseOptions 16from stat import * 17 18Option('diff', 'Generate a DIFF when saving the file.') 19 20 21# 22# IDLOutFile 23# 24# IDLOutFile provides a temporary output file. By default, the object will 25# not write the output if the file already exists, and matches what will be 26# written. This prevents the timestamp from changing to optimize cases where 27# the output files are used by a timestamp dependent build system 28# 29class IDLOutFile(object): 30 def __init__(self, filename, always_write = False, create_dir = True): 31 self.filename = filename 32 self.always_write = always_write 33 self.create_dir = create_dir 34 self.outlist = [] 35 self.open = True 36 37 # Compare the old text to the current list of output lines. 38 def IsEquivalent_(self, oldtext): 39 if not oldtext: return False 40 41 oldlines = oldtext.split('\n') 42 curlines = (''.join(self.outlist)).split('\n') 43 44 # If number of lines don't match, it's a mismatch 45 if len(oldlines) != len(curlines): 46 return False 47 48 for index in range(len(oldlines)): 49 oldline = oldlines[index] 50 curline = curlines[index] 51 52 if oldline == curline: continue 53 54 curwords = curline.split() 55 oldwords = oldline.split() 56 57 # It wasn't a perfect match. Check for changes we should ignore. 58 # Unmatched lines must be the same length 59 if len(curwords) != len(oldwords): 60 return False 61 62 # If it's not a comment then it's a mismatch 63 if curwords[0] not in ['*', '/*', '//']: 64 return False 65 66 # Ignore changes to the Copyright year which is autogenerated 67 # /* Copyright (c) 2011 The Chromium Authors. All rights reserved. 68 if len(curwords) > 4 and curwords[1] == 'Copyright': 69 if curwords[4:] == oldwords[4:]: continue 70 71 # Ignore changes to auto generation timestamp. 72 # // From FILENAME.idl modified DAY MON DATE TIME YEAR. 73 # /* From FILENAME.idl modified DAY MON DATE TIME YEAR. */ 74 # The line may be wrapped, so first deal with the first "From" line. 75 if curwords[1] == 'From': 76 if curwords[0:4] == oldwords[0:4]: continue 77 78 # Ignore changes to auto generation timestamp when line is wrapped 79 if index > 0: 80 two_line_oldwords = oldlines[index - 1].split() + oldwords[1:] 81 two_line_curwords = curlines[index - 1].split() + curwords[1:] 82 if len(two_line_curwords) > 8 and two_line_curwords[1] == 'From': 83 if two_line_curwords[0:4] == two_line_oldwords[0:4]: continue 84 85 return False 86 return True 87 88 # Return the file name 89 def Filename(self): 90 return self.filename 91 92 # Append to the output if the file is still open 93 def Write(self, string): 94 if not self.open: 95 raise RuntimeError('Could not write to closed file %s.' % self.filename) 96 self.outlist.append(string) 97 98 # Run clang-format on the buffered file contents. 99 def ClangFormat(self): 100 clang_format = subprocess.Popen(['clang-format', '-style=Chromium'], 101 stdin=subprocess.PIPE, 102 stdout=subprocess.PIPE) 103 new_output = clang_format.communicate("".join(self.outlist))[0] 104 self.outlist = [new_output] 105 106 # Close the file, flushing it to disk 107 def Close(self): 108 filename = os.path.realpath(self.filename) 109 self.open = False 110 outtext = ''.join(self.outlist) 111 oldtext = '' 112 113 if not self.always_write: 114 if os.path.isfile(filename): 115 oldtext = open(filename, 'rb').read() 116 if self.IsEquivalent_(oldtext): 117 if GetOption('verbose'): 118 InfoOut.Log('Output %s unchanged.' % self.filename) 119 return False 120 121 if GetOption('diff'): 122 for line in difflib.unified_diff(oldtext.split('\n'), outtext.split('\n'), 123 'OLD ' + self.filename, 124 'NEW ' + self.filename, 125 n=1, lineterm=''): 126 ErrOut.Log(line) 127 128 try: 129 # If the directory does not exit, try to create it, if we fail, we 130 # still get the exception when the file is openned. 131 basepath, leafname = os.path.split(filename) 132 if basepath and not os.path.isdir(basepath) and self.create_dir: 133 InfoOut.Log('Creating directory: %s\n' % basepath) 134 os.makedirs(basepath) 135 136 if not GetOption('test'): 137 outfile = open(filename, 'wb') 138 outfile.write(outtext) 139 outfile.close(); 140 InfoOut.Log('Output %s written.' % self.filename) 141 return True 142 143 except IOError as e: 144 ErrOut.Log("I/O error(%d): %s" % (e.errno, e.strerror)) 145 except: 146 ErrOut.Log("Unexpected error: %s" % sys.exc_info()[0]) 147 raise 148 149 return False 150 151 152def TestFile(name, stringlist, force, update): 153 errors = 0 154 155 # Get the old timestamp 156 if os.path.exists(name): 157 old_time = os.stat(filename)[ST_MTIME] 158 else: 159 old_time = 'NONE' 160 161 # Create the file and write to it 162 out = IDLOutFile(filename, force) 163 for item in stringlist: 164 out.Write(item) 165 166 # We wait for flush to force the timestamp to change 167 time.sleep(2) 168 169 wrote = out.Close() 170 cur_time = os.stat(filename)[ST_MTIME] 171 if update: 172 if not wrote: 173 ErrOut.Log('Failed to write output %s.' % filename) 174 return 1 175 if cur_time == old_time: 176 ErrOut.Log('Failed to update timestamp for %s.' % filename) 177 return 1 178 else: 179 if wrote: 180 ErrOut.Log('Should not have writen output %s.' % filename) 181 return 1 182 if cur_time != old_time: 183 ErrOut.Log('Should not have modified timestamp for %s.' % filename) 184 return 1 185 return 0 186 187 188def main(): 189 errors = 0 190 stringlist = ['Test', 'Testing\n', 'Test'] 191 filename = 'outtest.txt' 192 193 # Test forcibly writing a file 194 errors += TestFile(filename, stringlist, force=True, update=True) 195 196 # Test conditionally writing the file skipping 197 errors += TestFile(filename, stringlist, force=False, update=False) 198 199 # Test conditionally writing the file updating 200 errors += TestFile(filename, stringlist + ['X'], force=False, update=True) 201 202 # Clean up file 203 os.remove(filename) 204 if not errors: InfoOut.Log('All tests pass.') 205 return errors 206 207 208if __name__ == '__main__': 209 sys.exit(main()) 210