idl_generator.py revision 5821806d5e7f356e8fa4b058a389a808ea183019
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
6import sys
7
8from idl_log import ErrOut, InfoOut, WarnOut
9from idl_option import GetOption, Option, ParseOptions
10from idl_parser import ParseFiles
11
12GeneratorList = []
13
14Option('release', 'Which release to generate.', default='')
15Option('range', 'Which ranges in the form of MIN,MAX.', default='start,end')
16
17class Generator(object):
18  """Base class for generators.
19
20  This class provides a mechanism for adding new generator objects to the IDL
21  driver.  To use this class override the GenerateRelease and GenerateRange
22  members, and instantiate one copy of the class in the same module which
23  defines it to register the generator.  After the AST is generated, call the
24  static Run member which will check every registered generator to see which
25  ones have been enabled through command-line options.  To enable a generator
26  use the switches:
27    --<sname> : To enable with defaults
28    --<sname>_opt=<XXX,YYY=y> : To enable with generator specific options.
29
30  NOTE:  Generators still have access to global options
31  """
32
33  def __init__(self, name, sname, desc):
34    self.name = name
35    self.run_switch = Option(sname, desc)
36    self.opt_switch = Option(sname + '_opt', 'Options for %s.' % sname,
37                             default='')
38    GeneratorList.append(self)
39    self.errors = 0
40    self.skip_list = []
41
42  def Error(self, msg):
43    ErrOut.Log('Error %s : %s' % (self.name, msg))
44    self.errors += 1
45
46  def GetRunOptions(self):
47    options = {}
48    option_list = self.opt_switch.Get()
49    if option_list:
50      option_list = option_list.split(',')
51      for opt in option_list:
52        offs = opt.find('=')
53        if offs > 0:
54          options[opt[:offs]] = opt[offs+1:]
55        else:
56          options[opt] = True
57      return options
58    if self.run_switch.Get():
59      return options
60    return None
61
62  def Generate(self, ast, options):
63    self.errors = 0
64
65    rangestr = GetOption('range')
66    releasestr = GetOption('release')
67
68    print "Found releases: %s" % ast.releases
69
70    # Generate list of files to ignore due to errors
71    for filenode in ast.GetListOf('File'):
72      # If this file has errors, skip it
73      if filenode.GetProperty('ERRORS') > 0:
74        self.skip_list.append(filenode)
75        continue
76
77    # Check for a range option which over-rides a release option
78    if not releasestr and rangestr:
79      range_list = rangestr.split(',')
80      if len(range_list) != 2:
81        self.Error('Failed to generate for %s, incorrect range: "%s"' %
82                   (self.name, rangestr))
83      else:
84        vmin = range_list[0]
85        vmax = range_list[1]
86
87        # Generate 'start' and 'end' represent first and last found.
88        if vmin == 'start':
89            vmin = ast.releases[0]
90        if vmax == 'end':
91            vmax = ast.releases[-1]
92
93        vmin = ast.releases.index(vmin)
94        vmax = ast.releases.index(vmax) + 1
95        releases = ast.releases[vmin:vmax]
96        InfoOut.Log('Generate range %s of %s.' % (rangestr, self.name))
97        ret = self.GenerateRange(ast, releases, options)
98        if ret < 0:
99          self.Error('Failed to generate range %s : %s.' %(vmin, vmax))
100        else:
101          InfoOut.Log('%s wrote %d files.' % (self.name, ret))
102    # Otherwise this should be a single release generation
103    else:
104      if releasestr == 'start':
105        releasestr = ast.releases[0]
106      if releasestr == 'end':
107        releasestr = ast.releases[-1]
108
109      if releasestr > ast.releases[0]:
110        InfoOut.Log('There is no unique release for %s, using last release.' %
111                    releasestr)
112        releasestr = ast.releases[-1]
113
114      if releasestr not in ast.releases:
115        self.Error('Release %s not in [%s].' %
116                   (releasestr, ', '.join(ast.releases)))
117
118      if releasestr:
119        InfoOut.Log('Generate release %s of %s.' % (releasestr, self.name))
120        ret = self.GenerateRelease(ast, releasestr, options)
121        if ret < 0:
122          self.Error('Failed to generate release %s.' % releasestr)
123        else:
124          InfoOut.Log('%s wrote %d files.' % (self.name, ret))
125
126      else:
127        self.Error('No range or release specified for %s.' % releasestr)
128    return self.errors
129
130  def GenerateRelease(self, ast, release, options):
131    __pychecker__ = 'unusednames=ast,release,options'
132    self.Error("Undefined release generator.")
133    return 0
134
135  def GenerateRange(self, ast, releases, options):
136    __pychecker__ = 'unusednames=ast,releases,options'
137    self.Error("Undefined range generator.")
138    return 0
139
140  @staticmethod
141  def Run(ast):
142    fail_count = 0
143
144    # Check all registered generators if they should run.
145    for gen in GeneratorList:
146      options = gen.GetRunOptions()
147      if options is not None:
148        if gen.Generate(ast, options):
149          fail_count += 1
150    return fail_count
151
152
153class GeneratorByFile(Generator):
154  """A simplified generator that generates one output file per IDL source file.
155
156  A subclass of Generator for use of generators which have a one to one
157  mapping between IDL sources and output files.
158
159  Derived classes should define GenerateFile.
160  """
161
162  def GenerateFile(self, filenode, releases, options):
163    """Generates an output file from the IDL source.
164
165    Returns true if the generated file is different than the previously
166    generated file.
167    """
168    __pychecker__ = 'unusednames=filenode,releases,options'
169    self.Error("Undefined release generator.")
170    return 0
171
172  def GenerateRelease(self, ast, release, options):
173    return self.GenerateRange(ast, [release], options)
174
175  def GenerateRange(self, ast, releases, options):
176    # Get list of out files
177    outlist = GetOption('out')
178    if outlist: outlist = outlist.split(',')
179
180    skipList = []
181    cnt = 0
182    for filenode in ast.GetListOf('File'):
183      # Ignore files with errors
184      if filenode in self.skip_list:
185        continue
186
187      # Skip this file if not required
188      if outlist and filenode.GetName() not in outlist:
189        continue
190
191      # Create the output file and increment out count if there was a delta
192      if self.GenerateFile(filenode, releases, options):
193        cnt = cnt + 1
194
195    for filenode in skipList:
196      errcnt = filenode.GetProperty('ERRORS')
197      ErrOut.Log('%s : Skipped because of %d errors.' % (
198          filenode.GetName(), errcnt))
199
200    if skipList:
201      return -len(skipList)
202
203    if GetOption('diff'):
204      return -cnt
205    return cnt
206
207
208check_release = 0
209check_range = 0
210
211class GeneratorReleaseTest(Generator):
212  def GenerateRelease(self, ast, release, options = {}):
213    __pychecker__ = 'unusednames=ast,release,options'
214    global check_release
215    check_map = {
216      'so_long': True,
217      'MyOpt': 'XYZ',
218      'goodbye': True
219    }
220    check_release = 1
221    for item in check_map:
222      check_item = check_map[item]
223      option_item = options.get(item, None)
224      if check_item != option_item:
225        print 'Option %s is %s, expecting %s' % (item, option_item, check_item)
226        check_release = 0
227
228    if release != 'M14':
229      check_release = 0
230    return check_release == 1
231
232  def GenerateRange(self, ast, releases, options):
233    __pychecker__ = 'unusednames=ast,releases,options'
234    global check_range
235    check_range = 1
236    return True
237
238def Test():
239  __pychecker__ = 'unusednames=args'
240  global check_release
241  global check_range
242
243  ParseOptions(['--testgen_opt=so_long,MyOpt=XYZ,goodbye'])
244  if Generator.Run('AST') != 0:
245    print 'Generate release: Failed.\n'
246    return -1
247
248  if check_release != 1 or check_range != 0:
249    print 'Gererate release: Failed to run.\n'
250    return -1
251
252  check_release = 0
253  ParseOptions(['--testgen_opt="HELLO"', '--range=M14,M16'])
254  if Generator.Run('AST') != 0:
255    print 'Generate range: Failed.\n'
256    return -1
257
258  if check_release != 0 or check_range != 1:
259    print 'Gererate range: Failed to run.\n'
260    return -1
261
262  print 'Generator test: Pass'
263  return 0
264
265
266def Main(args):
267  if not args: return Test()
268  filenames = ParseOptions(args)
269  ast = ParseFiles(filenames)
270
271  return Generator.Run(ast)
272
273
274if __name__ == '__main__':
275  GeneratorReleaseTest('Test Gen', 'testgen', 'Generator Class Test.')
276  sys.exit(Main(sys.argv[1:]))
277