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)