gopkg.in/rethinkdb/rethinkdb-go.v6@v6.2.2/internal/gen_tests/process_polyglot.py (about)

     1  '''Finds and reads polyglot yaml tests (preferring the python tests),
     2  normalizing their quirks into something that can be translated in a
     3  sane way.
     4  
     5  The idea is that this file contains nothing Go specific, so could
     6  potentially be used to convert the tests for use with other drivers.
     7  '''
     8  
     9  import os
    10  import sys
    11  import os.path
    12  import ast
    13  import copy
    14  import logging
    15  from collections import namedtuple
    16  
    17  try:
    18      basestring
    19  except NameError:
    20      basestring = ("".__class__,)
    21  
    22  logger = logging.getLogger("process_polyglot")
    23  
    24  class EmptyTemplate(Exception):
    25      '''Raised inside templates if they have no reason to be rendered
    26      because what they're iterating over is empty'''
    27      pass
    28  
    29  class Unhandled(Exception):
    30      '''Used when a corner case is hit that probably should be handled
    31      if a test actually hits it'''
    32      pass
    33  
    34  
    35  class Skip(Exception):
    36      '''Used when skipping a test for whatever reason'''
    37      pass
    38  
    39  
    40  class FatalSkip(EmptyTemplate):
    41      '''Used when a skipped test should prevent the entire test file
    42      from rendering'''
    43      def __init__(self, msg):
    44          logger.info("Skipping rendering because %s", msg)
    45          super(FatalSkip, self).__init__(msg)
    46  
    47  
    48  Term = namedtuple("Term", 'line type ast')
    49  CustomTerm = namedtuple('CustomTerm', 'line')
    50  Query = namedtuple(
    51      'Query',
    52      ('query',
    53       'expected',
    54       'testfile',
    55       'line_num',
    56       'runopts')
    57  )
    58  Def = namedtuple('Def', 'varname term run_if_query testfile line_num runopts')
    59  CustomDef = namedtuple('CustomDef', 'line testfile line_num')
    60  Expect = namedtuple('Expect', 'bif term')
    61  
    62  
    63  class AnythingIsFine(object):
    64      def __init__(self):
    65          self.type = str
    66          self.ast = ast.Name("AnythingIsFine", None)
    67          self.line = "AnythingIsFine"
    68  
    69  
    70  class SkippedTest(object):
    71      __slots__ = ('line', 'reason')
    72  
    73      def __init__(self, line, reason):
    74          if reason == "No go, python or generic test":
    75              logger.debug("Skipped test because %s", reason)
    76          else:
    77              logger.info("Skipped test because %s", reason)
    78              logger.info(" - Skipped test was: %s", line)
    79          self.line = line
    80          self.reason = reason
    81  
    82  
    83  def flexiget(obj, keys, default):
    84      '''Like dict.get, but accepts an array of keys, matching the first
    85      that exists in the dict. If none do, it returns the default. If
    86      the object isn't a dict, it also returns the default'''
    87      if not isinstance(obj, dict):
    88          return default
    89      for key in keys:
    90          if key in obj:
    91              return obj[key]
    92      return default
    93  
    94  
    95  def py_str(py):
    96      '''Turns a python value into a string of python code
    97      representing that object'''
    98      def maybe_str(s):
    99          return s if isinstance(s, str) and '(' in s else repr(s)
   100  
   101      if type(py) is dict:
   102          return '{' + ', '.join(
   103              [repr(k) + ': ' + maybe_str(py[k]) for k in py]) + '}'
   104      if not isinstance(py, basestring):
   105          return repr(py)
   106      else:
   107          return py
   108  
   109  
   110  def _try_eval(node, context):
   111      '''For evaluating expressions given a context'''
   112      node_4_eval = copy.deepcopy(node)
   113      if type(node_4_eval) == ast.Expr:
   114          node_4_eval = node_4_eval.value
   115      node_4_eval = ast.Expression(node_4_eval)
   116      ast.fix_missing_locations(node_4_eval)
   117      compiled_value = compile(node_4_eval, '<str>', mode='eval')
   118      r = context['r']
   119      try:
   120          value = eval(compiled_value, context)
   121      except r.ReqlError:
   122          raise Skip("Java type system prevents static Reql errors")
   123      except AttributeError:
   124          raise Skip("Java type system prevents attribute errors")
   125      except Exception as err:
   126          return type(err), err
   127      else:
   128          return type(value), value
   129  
   130  
   131  def try_eval(node, context):
   132      return _try_eval(node, context)[0]
   133  
   134  
   135  def try_eval_def(parsed_define, context):
   136      '''For evaluating python definitions like x = foo'''
   137      varname = parsed_define.targets[0].id
   138      type_, value = _try_eval(parsed_define.value, context)
   139      context[varname] = value
   140      return varname, type_
   141  
   142  
   143  def all_yaml_tests(test_dir, exclusions):
   144      '''Generator for the full paths of all non-excluded yaml tests'''
   145      for root, dirs, files in os.walk(test_dir):
   146          for f in files:
   147              path = os.path.relpath(os.path.join(root, f), test_dir)
   148              if valid_filename(exclusions, path):
   149                  yield path
   150  
   151  
   152  def valid_filename(exclusions, filepath):
   153      parts = filepath.split('.')
   154      if parts[-1] != 'yaml':
   155          return False
   156      for exclusion in exclusions:
   157          if exclusion in filepath:
   158              logger.info("Skipped %s due to exclusion %r",
   159                          filepath, exclusion)
   160              return False
   161      return True
   162  
   163  def fake_type(name):
   164      def __init__(self, *args, **kwargs):
   165          pass
   166      typ = type(name, (object,), {'__init__': __init__})
   167      typ.__module__ = '?test?'
   168      return typ
   169  
   170  def create_context(r, table_var_names):
   171      '''Creates a context for evaluation of test definitions. Needs the
   172      rethinkdb driver module to use, and the variable names of
   173      predefined tables'''
   174      from datetime import datetime, tzinfo, timedelta
   175  
   176      # Both these tzinfo classes were nabbed from
   177      # test/rql_test/driver/driver.py to aid in evaluation
   178      class UTCTimeZone(tzinfo):
   179          '''UTC'''
   180  
   181          def utcoffset(self, dt):
   182              return timedelta(0)
   183  
   184          def tzname(self, dt):
   185              return "UTC"
   186  
   187          def dst(self, dt):
   188              return timedelta(0)
   189  
   190      class PacificTimeZone(tzinfo):
   191          '''Pacific timezone emulator for timestamp: 1375147296.68'''
   192  
   193          def utcoffset(self, dt):
   194              return timedelta(-1, 61200)
   195  
   196          def tzname(self, dt):
   197              return 'PDT'
   198  
   199          def dst(self, dt):
   200              return timedelta(0, 3600)
   201  
   202      # We need to keep track of the values of definitions because each
   203      # subsequent definition can depend on previous ones.
   204      context = {
   205          'r': r,
   206          'null': None,
   207          'nil': None,
   208          'sys': sys,
   209          'false': False,
   210          'true': True,
   211          'datetime': datetime,
   212          'PacificTimeZone': PacificTimeZone,
   213          'UTCTimeZone': UTCTimeZone,
   214          # mock test helper functions
   215          'len': lambda x: 1,
   216          'arrlen': fake_type("arr_len"),
   217          'uuid': fake_type("uuid"),
   218          'fetch': lambda c, limit=None: [],
   219          'int_cmp': fake_type("int_cmp"),
   220          'partial': fake_type("partial"),
   221          'float_cmp': fake_type("float_cmp"),
   222          'wait': lambda time: None,
   223          'err': fake_type('err'),
   224          'err_regex': fake_type('err_regex'),
   225          'regex': fake_type('regex'),
   226          'bag': fake_type('bag'),
   227          # py3 compatibility
   228          'xrange': range,
   229      }
   230      # Definitions can refer to these predefined table variables. Since
   231      # we're only evaluating definitions here to determine what the
   232      # type of the term will be, it doesn't need to include the db or
   233      # anything, it just needs to be a Table ast object.
   234      context.update({tbl: r.table(tbl) for tbl in table_var_names})
   235      return context
   236  
   237  
   238  class TestContext(object):
   239      '''Holds file, context and test number info before "expected" data
   240      is obtained'''
   241      def __init__(self, context, testfile, runopts):
   242          self.context = context
   243          self.testfile = testfile
   244          self.runopts = runopts
   245  
   246      @staticmethod
   247      def find_python_expected(test):
   248          '''Extract the expected result of the test. We want the python
   249          specific version if it's available, so we have to poke around
   250          a bit'''
   251          if 'ot' in test:
   252              ret = flexiget(test['ot'], ['py', 'cd'], test['ot'])
   253          elif isinstance(test.get('py'), dict) and 'ot' in test['py']:
   254              ret = test['py']['ot']
   255          else:
   256              # This is distinct from the 'ot' field having the
   257              # value None in it!
   258              return AnythingIsFine()
   259          return ret
   260  
   261      @staticmethod
   262      def find_custom_expected(test, field):
   263          '''Gets the ot field for the language if it exists. If not it returns
   264          None.'''
   265          if 'ot' in test:
   266              ret = flexiget(test['ot'], [field], None)
   267          elif field in test:
   268              ret = flexiget(test[field], ['ot'], None)
   269          else:
   270              ret = None
   271          return ret
   272  
   273      def expected_context(self, test, custom_field):
   274          custom_expected = self.find_custom_expected(test, custom_field)
   275          if custom_expected is not None:
   276              # custom version doesn't need to be evaluated, it's in the
   277              # right language already
   278              term = CustomTerm(custom_expected)
   279          else:
   280              exp = self.find_python_expected(test)
   281              if type(exp) == AnythingIsFine:
   282                  return ExpectedContext(self, AnythingIsFine())
   283              expected = py_str(exp)
   284              expected_ast = ast.parse(expected, mode="eval").body
   285              logger.debug("Evaluating: %s", expected)
   286              expected_type = try_eval(expected_ast, self.context)
   287              term = Term(
   288                  ast=expected_ast,
   289                  line=expected,
   290                  type=expected_type,
   291              )
   292          return ExpectedContext(self, term)
   293  
   294      def def_from_parsed(self, define_line, parsed_define, run_if_query):
   295          logger.debug("Evaluating: %s", define_line)
   296          varname, result_type = try_eval_def(parsed_define, self.context)
   297          return Def(
   298              varname=varname,
   299              term=Term(
   300                  line=define_line,
   301                  type=result_type,
   302                  ast=parsed_define),
   303              run_if_query=run_if_query,
   304              testfile=self.testfile,
   305              line_num=define_line.linenumber,
   306              runopts=self.runopts,
   307          )
   308  
   309      def def_from_define(self, define, run_if_query):
   310          define_line = py_str(define)
   311          parsed_define = ast.parse(define_line, mode='single').body[0]
   312          return self.def_from_parsed(define_line, parsed_define, run_if_query)
   313  
   314      def custom_def(self, line):
   315          return CustomDef(
   316              line=line, testfile=self.testfile, line_num=line.linenumber)
   317  
   318  
   319  class ExpectedContext(object):
   320      '''Holds some contextual information needed to yield queries. Used by
   321      the tests_and_defs generator'''
   322  
   323      def __init__(self, test_context, expected_term):
   324          self.testfile = test_context.testfile
   325          self.context = test_context.context
   326          self.runopts = test_context.runopts
   327          self.expected_term = expected_term
   328  
   329      def query_from_term(self, query_term, line_num=None):
   330          if type(query_term) == SkippedTest:
   331              return query_term
   332          else:
   333              return Query(
   334                  query=query_term,
   335                  expected=self.expected_term,
   336                  testfile=self.testfile,
   337                  line_num=query_term.line.linenumber,
   338                  runopts=self.runopts,
   339              )
   340  
   341      def query_from_test(self, test):
   342          return self.query_from_term(
   343              self.term_from_test(test), test.linenumber)
   344  
   345      def query_from_subtest(self, test, subline_num):
   346          return self.query_from_term(
   347              self.term_from_test(test),
   348              (test.linenumber, subline_num))
   349  
   350      def query_from_parsed(self, testline, parsed):
   351          return self.query_from_term(
   352              self.term_from_parsed(testline, parsed))
   353  
   354      def term_from_test(self, test):
   355          testline = py_str(test)
   356          return self.term_from_testline(testline)
   357  
   358      def term_from_testline(self, testline):
   359          parsed = ast.parse(testline, mode='eval').body
   360          return self.term_from_parsed(testline, parsed)
   361  
   362      def term_from_parsed(self, testline, parsed):
   363          try:
   364              logger.debug("Evaluating: %s", testline)
   365              result_type = try_eval(parsed, self.context)
   366          except Skip as s:
   367              return SkippedTest(line=testline, reason=str(s))
   368          else:
   369              return Term(ast=parsed, line=testline, type=result_type)
   370  
   371  
   372  def tests_and_defs(testfile, raw_test_data, context, custom_field=None):
   373      '''Generator of parsed python tests and definitions.
   374      `testfile`      is the name of the file being converted
   375      `raw_test_data` is the yaml data as python data structures
   376      `context`       is the evaluation context for the values. Will be modified
   377      `custom`        is the specific type of test to look for.
   378                      (falls back to 'py', then 'cd')
   379      '''
   380      for test in raw_test_data:
   381          runopts = test.get('runopts')
   382          if runopts is not None:
   383              runopts = {key: ast.parse(py_str(val), mode="eval").body
   384                         for key, val in runopts.items()}
   385          test_context = TestContext(context, testfile, runopts=runopts)
   386          if 'def' in test and flexiget(test['def'], [custom_field], False):
   387              yield test_context.custom_def(test['def'][custom_field])
   388          elif 'def' in test:
   389              # We want to yield the definition before the test itself
   390              define = flexiget(test['def'], [custom_field], None)
   391              if define is not None:
   392                  yield test_context.custom_def(define)
   393              else:
   394                  define = flexiget(test['def'], ['py', 'cd'], test['def'])
   395                  # for some reason, sometimes def is just None
   396                  if define and type(define) is not dict:
   397                      # if define is a dict, it doesn't have anything
   398                      # relevant since we already checked. if this
   399                      # happens to be a query fragment, the test
   400                      # framework should not run it, just store the
   401                      # fragment in the variable.
   402                      yield test_context.def_from_define(
   403                          define, run_if_query=False)
   404          customtest = test.get(custom_field, None)
   405          # as a backup try getting a python or generic test
   406          pytest = flexiget(test, ['py', 'cd'], None)
   407          if customtest is None and pytest is None:
   408              line = flexiget(test, ['rb', 'js'], u'¯\_(ツ)_/¯')
   409              yield SkippedTest(
   410                  line=line,
   411                  reason='No {}, python or generic test'.format(custom_field))
   412              continue
   413  
   414          expected_context = test_context.expected_context(test, custom_field)
   415          if customtest is not None:
   416              yield expected_context.query_from_term(customtest)
   417          elif isinstance(pytest, basestring):
   418              parsed = ast.parse(pytest, mode="single").body[0]
   419              if type(parsed) == ast.Expr:
   420                  yield expected_context.query_from_parsed(pytest, parsed.value)
   421              elif type(parsed) == ast.Assign:
   422                  # Second syntax for defines. Surprise, it wasn't a
   423                  # test at all, because it has an equals sign in it.
   424                  # if this happens to be a query, it will be run.
   425                  yield test_context.def_from_parsed(
   426                      pytest, parsed, run_if_query=True)
   427          elif type(pytest) is dict and 'cd' in pytest:
   428              yield expected_context.query_from_test(pytest['cd'])
   429          else:
   430              for i, subtest in enumerate(pytest, start=1):
   431                  # unroll subtests
   432                  yield expected_context.query_from_subtest(subtest, i)