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