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