1# Copyright 2012 the V8 project authors. All rights reserved.
2# Redistribution and use in source and binary forms, with or without
3# modification, are permitted provided that the following conditions are
4# met:
5#
6#     * Redistributions of source code must retain the above copyright
7#       notice, this list of conditions and the following disclaimer.
8#     * Redistributions in binary form must reproduce the above
9#       copyright notice, this list of conditions and the following
10#       disclaimer in the documentation and/or other materials provided
11#       with the distribution.
12#     * Neither the name of Google Inc. nor the names of its
13#       contributors may be used to endorse or promote products derived
14#       from this software without specific prior written permission.
15#
16# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
28
29import cStringIO
30import re
31
32# These outcomes can occur in a TestCase's outcomes list:
33SKIP = 'SKIP'
34FAIL = 'FAIL'
35PASS = 'PASS'
36OKAY = 'OKAY'
37TIMEOUT = 'TIMEOUT'
38CRASH = 'CRASH'
39SLOW = 'SLOW'
40FLAKY = 'FLAKY'
41# These are just for the status files and are mapped below in DEFS:
42FAIL_OK = 'FAIL_OK'
43PASS_OR_FAIL = 'PASS_OR_FAIL'
44
45KEYWORDS = {SKIP: SKIP,
46            FAIL: FAIL,
47            PASS: PASS,
48            OKAY: OKAY,
49            TIMEOUT: TIMEOUT,
50            CRASH: CRASH,
51            SLOW: SLOW,
52            FLAKY: FLAKY,
53            FAIL_OK: FAIL_OK,
54            PASS_OR_FAIL: PASS_OR_FAIL}
55
56class Expression(object):
57  pass
58
59
60class Constant(Expression):
61
62  def __init__(self, value):
63    self.value = value
64
65  def Evaluate(self, env, defs):
66    return self.value
67
68
69class Variable(Expression):
70
71  def __init__(self, name):
72    self.name = name
73
74  def GetOutcomes(self, env, defs):
75    if self.name in env: return set([env[self.name]])
76    else: return set([])
77
78  def Evaluate(self, env, defs):
79    return env[self.name]
80
81  def __str__(self):
82    return self.name
83
84  def string(self, logical):
85    return self.__str__()
86
87
88class Outcome(Expression):
89
90  def __init__(self, name):
91    self.name = name
92
93  def GetOutcomes(self, env, defs):
94    if self.name in defs:
95      return defs[self.name].GetOutcomes(env, defs)
96    else:
97      return set([self.name])
98
99  def __str__(self):
100    if self.name in KEYWORDS:
101      return "%s" % KEYWORDS[self.name]
102    return "'%s'" % self.name
103
104  def string(self, logical):
105    if logical:
106      return "%s" % self.name
107    return self.__str__()
108
109
110class Operation(Expression):
111
112  def __init__(self, left, op, right):
113    self.left = left
114    self.op = op
115    self.right = right
116
117  def Evaluate(self, env, defs):
118    if self.op == '||' or self.op == ',':
119      return self.left.Evaluate(env, defs) or self.right.Evaluate(env, defs)
120    elif self.op == 'if':
121      return False
122    elif self.op == '==':
123      return not self.left.GetOutcomes(env, defs).isdisjoint(self.right.GetOutcomes(env, defs))
124    elif self.op == '!=':
125      return self.left.GetOutcomes(env, defs).isdisjoint(self.right.GetOutcomes(env, defs))
126    else:
127      assert self.op == '&&'
128      return self.left.Evaluate(env, defs) and self.right.Evaluate(env, defs)
129
130  def GetOutcomes(self, env, defs):
131    if self.op == '||' or self.op == ',':
132      return self.left.GetOutcomes(env, defs) | self.right.GetOutcomes(env, defs)
133    elif self.op == 'if':
134      if self.right.Evaluate(env, defs): return self.left.GetOutcomes(env, defs)
135      else: return set([])
136    else:
137      assert self.op == '&&'
138      return self.left.GetOutcomes(env, defs) & self.right.GetOutcomes(env, defs)
139
140  def __str__(self):
141    return self.string(False)
142
143  def string(self, logical=False):
144    if self.op == 'if':
145      return "['%s', %s]" % (self.right.string(True), self.left.string(logical))
146    elif self.op == "||" or self.op == ",":
147      if logical:
148        return "%s or %s" % (self.left.string(True), self.right.string(True))
149      else:
150        return "%s, %s" % (self.left, self.right)
151    elif self.op == "&&":
152      return "%s and %s" % (self.left.string(True), self.right.string(True))
153    return "%s %s %s" % (self.left.string(logical), self.op,
154                         self.right.string(logical))
155
156
157def IsAlpha(string):
158  for char in string:
159    if not (char.isalpha() or char.isdigit() or char == '_'):
160      return False
161  return True
162
163
164class Tokenizer(object):
165  """A simple string tokenizer that chops expressions into variables,
166  parens and operators"""
167
168  def __init__(self, expr):
169    self.index = 0
170    self.expr = expr
171    self.length = len(expr)
172    self.tokens = None
173
174  def Current(self, length=1):
175    if not self.HasMore(length): return ""
176    return self.expr[self.index:self.index + length]
177
178  def HasMore(self, length=1):
179    return self.index < self.length + (length - 1)
180
181  def Advance(self, count=1):
182    self.index = self.index + count
183
184  def AddToken(self, token):
185    self.tokens.append(token)
186
187  def SkipSpaces(self):
188    while self.HasMore() and self.Current().isspace():
189      self.Advance()
190
191  def Tokenize(self):
192    self.tokens = [ ]
193    while self.HasMore():
194      self.SkipSpaces()
195      if not self.HasMore():
196        return None
197      if self.Current() == '(':
198        self.AddToken('(')
199        self.Advance()
200      elif self.Current() == ')':
201        self.AddToken(')')
202        self.Advance()
203      elif self.Current() == '$':
204        self.AddToken('$')
205        self.Advance()
206      elif self.Current() == ',':
207        self.AddToken(',')
208        self.Advance()
209      elif IsAlpha(self.Current()):
210        buf = ""
211        while self.HasMore() and IsAlpha(self.Current()):
212          buf += self.Current()
213          self.Advance()
214        self.AddToken(buf)
215      elif self.Current(2) == '&&':
216        self.AddToken('&&')
217        self.Advance(2)
218      elif self.Current(2) == '||':
219        self.AddToken('||')
220        self.Advance(2)
221      elif self.Current(2) == '==':
222        self.AddToken('==')
223        self.Advance(2)
224      elif self.Current(2) == '!=':
225        self.AddToken('!=')
226        self.Advance(2)
227      else:
228        return None
229    return self.tokens
230
231
232class Scanner(object):
233  """A simple scanner that can serve out tokens from a given list"""
234
235  def __init__(self, tokens):
236    self.tokens = tokens
237    self.length = len(tokens)
238    self.index = 0
239
240  def HasMore(self):
241    return self.index < self.length
242
243  def Current(self):
244    return self.tokens[self.index]
245
246  def Advance(self):
247    self.index = self.index + 1
248
249
250def ParseAtomicExpression(scan):
251  if scan.Current() == "true":
252    scan.Advance()
253    return Constant(True)
254  elif scan.Current() == "false":
255    scan.Advance()
256    return Constant(False)
257  elif IsAlpha(scan.Current()):
258    name = scan.Current()
259    scan.Advance()
260    return Outcome(name)
261  elif scan.Current() == '$':
262    scan.Advance()
263    if not IsAlpha(scan.Current()):
264      return None
265    name = scan.Current()
266    scan.Advance()
267    return Variable(name.lower())
268  elif scan.Current() == '(':
269    scan.Advance()
270    result = ParseLogicalExpression(scan)
271    if (not result) or (scan.Current() != ')'):
272      return None
273    scan.Advance()
274    return result
275  else:
276    return None
277
278
279BINARIES = ['==', '!=']
280def ParseOperatorExpression(scan):
281  left = ParseAtomicExpression(scan)
282  if not left: return None
283  while scan.HasMore() and (scan.Current() in BINARIES):
284    op = scan.Current()
285    scan.Advance()
286    right = ParseOperatorExpression(scan)
287    if not right:
288      return None
289    left = Operation(left, op, right)
290  return left
291
292
293def ParseConditionalExpression(scan):
294  left = ParseOperatorExpression(scan)
295  if not left: return None
296  while scan.HasMore() and (scan.Current() == 'if'):
297    scan.Advance()
298    right = ParseOperatorExpression(scan)
299    if not right:
300      return None
301    left = Operation(left, 'if', right)
302  return left
303
304
305LOGICALS = ["&&", "||", ","]
306def ParseLogicalExpression(scan):
307  left = ParseConditionalExpression(scan)
308  if not left: return None
309  while scan.HasMore() and (scan.Current() in LOGICALS):
310    op = scan.Current()
311    scan.Advance()
312    right = ParseConditionalExpression(scan)
313    if not right:
314      return None
315    left = Operation(left, op, right)
316  return left
317
318
319def ParseCondition(expr):
320  """Parses a logical expression into an Expression object"""
321  tokens = Tokenizer(expr).Tokenize()
322  if not tokens:
323    print "Malformed expression: '%s'" % expr
324    return None
325  scan = Scanner(tokens)
326  ast = ParseLogicalExpression(scan)
327  if not ast:
328    print "Malformed expression: '%s'" % expr
329    return None
330  if scan.HasMore():
331    print "Malformed expression: '%s'" % expr
332    return None
333  return ast
334
335
336class Section(object):
337  """A section of the configuration file.  Sections are enabled or
338  disabled prior to running the tests, based on their conditions"""
339
340  def __init__(self, condition):
341    self.condition = condition
342    self.rules = [ ]
343
344  def AddRule(self, rule):
345    self.rules.append(rule)
346
347
348class Rule(object):
349  """A single rule that specifies the expected outcome for a single
350  test."""
351
352  def __init__(self, raw_path, path, value):
353    self.raw_path = raw_path
354    self.path = path
355    self.value = value
356
357  def GetOutcomes(self, env, defs):
358    return self.value.GetOutcomes(env, defs)
359
360  def Contains(self, path):
361    if len(self.path) > len(path):
362      return False
363    for i in xrange(len(self.path)):
364      if not self.path[i].match(path[i]):
365        return False
366    return True
367
368
369HEADER_PATTERN = re.compile(r'\[([^]]+)\]')
370RULE_PATTERN = re.compile(r'\s*([^: ]*)\s*:(.*)')
371DEF_PATTERN = re.compile(r'^def\s*(\w+)\s*=(.*)$')
372PREFIX_PATTERN = re.compile(r'^\s*prefix\s+([\w\_\.\-\/]+)$')
373
374
375class ConvertNotation(object):
376  def __init__(self, path):
377    self.path = path
378    self.indent = ""
379    self.comment = []
380    self.init = False
381    self.section = False
382    self.out = cStringIO.StringIO()
383
384  def OpenGlobal(self):
385    if self.init: return
386    self.WriteComment()
387    print >> self.out, "["
388    self.init = True
389
390  def CloseGlobal(self):
391    if not self.init: self.OpenGlobal()
392    print >> self.out, "]"
393    self.init = False
394
395  def OpenSection(self, condition="ALWAYS"):
396    if self.section: return
397    self.OpenGlobal()
398    if type(condition) != str:
399      condition = "'%s'" % condition.string(True)
400    print >> self.out, "%s[%s, {" % (self.indent, condition)
401    self.indent += " " * 2
402    self.section = condition
403
404  def CloseSection(self):
405    if not self.section: return
406    self.indent = self.indent[:-2]
407    print >> self.out, "%s}],  # %s" % (self.indent, self.section)
408    self.section = False
409
410  def WriteComment(self):
411    if not self.comment: return
412    for c in self.comment:
413      if len(c.strip()) == 0:
414        print >> self.out, ""
415      else:
416        print >> self.out, "%s%s" % (self.indent, c),
417    self.comment = []
418
419  def GetOutput(self):
420    with open(self.path) as f:
421      for line in f:
422        if line[0] == '#':
423          self.comment += [line]
424          continue
425        if len(line.strip()) == 0:
426          self.comment += [line]
427          continue
428        header_match = HEADER_PATTERN.match(line)
429        if header_match:
430          condition = ParseCondition(header_match.group(1).strip())
431          self.CloseSection()
432          self.WriteComment()
433          self.OpenSection(condition)
434          continue
435        rule_match = RULE_PATTERN.match(line)
436        if rule_match:
437          self.OpenSection()
438          self.WriteComment()
439          path = rule_match.group(1).strip()
440          value_str = rule_match.group(2).strip()
441          comment = ""
442          if '#' in value_str:
443            pos = value_str.find('#')
444            comment = "  %s" % value_str[pos:].strip()
445            value_str = value_str[:pos].strip()
446          value = ParseCondition(value_str)
447          print >> self.out, ("%s'%s': [%s],%s" %
448                              (self.indent, path, value, comment))
449          continue
450        def_match = DEF_PATTERN.match(line)
451        if def_match:
452          # Custom definitions are deprecated.
453          continue
454        prefix_match = PREFIX_PATTERN.match(line)
455        if prefix_match:
456          continue
457        print "Malformed line: '%s'." % line
458    self.CloseSection()
459    self.CloseGlobal()
460    result = self.out.getvalue()
461    self.out.close()
462    return result
463