1from distutils.errors import * 2import errno 3import glob 4import hashlib 5import imp 6import inspect 7import os 8import re 9import shutil 10import sys 11import tempfile 12import unittest 13 14import antlr3 15 16def unlink(path): 17 try: 18 os.unlink(path) 19 except OSError as exc: 20 if exc.errno != errno.ENOENT: 21 raise 22 23 24class GrammarCompileError(Exception): 25 """Grammar failed to compile.""" 26 pass 27 28 29# At least on MacOSX tempdir (/tmp) is a symlink. It's sometimes dereferences, 30# sometimes not, breaking the inspect.getmodule() function. 31testbasedir = os.path.join( 32 os.path.realpath(tempfile.gettempdir()), 33 'antlr3-test') 34 35 36class BrokenTest(unittest.TestCase.failureException): 37 def __repr__(self): 38 name, reason = self.args 39 return '{}: {}: {} works now'.format( 40 (self.__class__.__name__, name, reason)) 41 42 43def broken(reason, *exceptions): 44 '''Indicates a failing (or erroneous) test case fails that should succeed. 45 If the test fails with an exception, list the exception type in args''' 46 def wrapper(test_method): 47 def replacement(*args, **kwargs): 48 try: 49 test_method(*args, **kwargs) 50 except exceptions or unittest.TestCase.failureException: 51 pass 52 else: 53 raise BrokenTest(test_method.__name__, reason) 54 replacement.__doc__ = test_method.__doc__ 55 replacement.__name__ = 'XXX_' + test_method.__name__ 56 replacement.todo = reason 57 return replacement 58 return wrapper 59 60 61dependencyCache = {} 62compileErrorCache = {} 63 64# setup java CLASSPATH 65if 'CLASSPATH' not in os.environ: 66 cp = [] 67 68 baseDir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 69 libDir = os.path.join(baseDir, 'lib') 70 71 jar = os.path.join(libDir, 'ST-4.0.5.jar') 72 if not os.path.isfile(jar): 73 raise DistutilsFileError( 74 "Missing file '{}'. Grab it from a distribution package.".format(jar) 75 ) 76 cp.append(jar) 77 78 jar = os.path.join(libDir, 'antlr-3.4.1-SNAPSHOT.jar') 79 if not os.path.isfile(jar): 80 raise DistutilsFileError( 81 "Missing file '{}'. Grab it from a distribution package.".format(jar) 82 ) 83 cp.append(jar) 84 85 jar = os.path.join(libDir, 'antlr-runtime-3.4.jar') 86 if not os.path.isfile(jar): 87 raise DistutilsFileError( 88 "Missing file '{}'. Grab it from a distribution package.".format(jar) 89 ) 90 cp.append(jar) 91 92 cp.append(os.path.join(baseDir, 'runtime', 'Python', 'build')) 93 94 classpath = '-cp "' + ':'.join([os.path.abspath(p) for p in cp]) + '"' 95 96else: 97 classpath = '' 98 99 100class ANTLRTest(unittest.TestCase): 101 def __init__(self, *args, **kwargs): 102 super().__init__(*args, **kwargs) 103 104 self.moduleName = os.path.splitext(os.path.basename(sys.modules[self.__module__].__file__))[0] 105 self.className = self.__class__.__name__ 106 self._baseDir = None 107 108 self.lexerModule = None 109 self.parserModule = None 110 111 self.grammarName = None 112 self.grammarType = None 113 114 115 @property 116 def baseDir(self): 117 if self._baseDir is None: 118 testName = 'unknownTest' 119 for frame in inspect.stack(): 120 code = frame[0].f_code 121 codeMod = inspect.getmodule(code) 122 if codeMod is None: 123 continue 124 125 # skip frames not in requested module 126 if codeMod is not sys.modules[self.__module__]: 127 continue 128 129 # skip some unwanted names 130 if code.co_name in ('nextToken', '<module>'): 131 continue 132 133 if code.co_name.startswith('test'): 134 testName = code.co_name 135 break 136 137 self._baseDir = os.path.join( 138 testbasedir, 139 self.moduleName, self.className, testName) 140 if not os.path.isdir(self._baseDir): 141 os.makedirs(self._baseDir) 142 143 return self._baseDir 144 145 146 def _invokeantlr(self, dir, file, options, javaOptions=''): 147 cmd = 'cd {}; java {} {} org.antlr.Tool -o . {} {} 2>&1'.format( 148 dir, javaOptions, classpath, options, file 149 ) 150 fp = os.popen(cmd) 151 output = '' 152 failed = False 153 for line in fp: 154 output += line 155 156 if line.startswith('error('): 157 failed = True 158 159 rc = fp.close() 160 if rc: 161 failed = True 162 163 if failed: 164 raise GrammarCompileError( 165 "Failed to compile grammar '{}':\n{}\n\n{}".format(file, cmd, output) 166 ) 167 168 169 def compileGrammar(self, grammarName=None, options='', javaOptions=''): 170 if grammarName is None: 171 grammarName = self.moduleName + '.g' 172 173 self._baseDir = os.path.join( 174 testbasedir, 175 self.moduleName) 176 if not os.path.isdir(self._baseDir): 177 os.makedirs(self._baseDir) 178 179 if self.grammarName is None: 180 self.grammarName = os.path.splitext(grammarName)[0] 181 182 grammarPath = os.path.join(os.path.dirname(os.path.abspath(__file__)), grammarName) 183 184 # get type and name from first grammar line 185 with open(grammarPath, 'r') as fp: 186 grammar = fp.read() 187 m = re.match(r'\s*((lexer|parser|tree)\s+|)grammar\s+(\S+);', grammar, re.MULTILINE) 188 self.assertIsNotNone(m, grammar) 189 self.grammarType = m.group(2) or 'combined' 190 191 self.assertIn(self.grammarType, ('lexer', 'parser', 'tree', 'combined')) 192 193 # don't try to rebuild grammar, if it already failed 194 if grammarName in compileErrorCache: 195 return 196 197 try: 198 # # get dependencies from antlr 199 # if grammarName in dependencyCache: 200 # dependencies = dependencyCache[grammarName] 201 202 # else: 203 # dependencies = [] 204 # cmd = ('cd %s; java %s %s org.antlr.Tool -o . -depend %s 2>&1' 205 # % (self.baseDir, javaOptions, classpath, grammarPath)) 206 207 # output = "" 208 # failed = False 209 210 # fp = os.popen(cmd) 211 # for line in fp: 212 # output += line 213 214 # if line.startswith('error('): 215 # failed = True 216 # elif ':' in line: 217 # a, b = line.strip().split(':', 1) 218 # dependencies.append( 219 # (os.path.join(self.baseDir, a.strip()), 220 # [os.path.join(self.baseDir, b.strip())]) 221 # ) 222 223 # rc = fp.close() 224 # if rc is not None: 225 # failed = True 226 227 # if failed: 228 # raise GrammarCompileError( 229 # "antlr -depend failed with code {} on grammar '{}':\n\n{}\n{}".format( 230 # rc, grammarName, cmd, output) 231 # ) 232 233 # # add dependencies to my .stg files 234 # templateDir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'tool', 'src', 'main', 'resources', 'org', 'antlr', 'codegen', 'templates', 'Python')) 235 # templates = glob.glob(os.path.join(templateDir, '*.stg')) 236 237 # for dst, src in dependencies: 238 # src.extend(templates) 239 240 # dependencyCache[grammarName] = dependencies 241 242 # rebuild = False 243 # for dest, sources in dependencies: 244 # if not os.path.isfile(dest): 245 # rebuild = True 246 # break 247 248 # for source in sources: 249 # if os.path.getmtime(source) > os.path.getmtime(dest): 250 # rebuild = True 251 # break 252 253 254 # if rebuild: 255 # self._invokeantlr(self.baseDir, grammarPath, options, javaOptions) 256 257 self._invokeantlr(self.baseDir, grammarPath, options, javaOptions) 258 259 except: 260 # mark grammar as broken 261 compileErrorCache[grammarName] = True 262 raise 263 264 265 def lexerClass(self, base): 266 """Optionally build a subclass of generated lexer class""" 267 268 return base 269 270 271 def parserClass(self, base): 272 """Optionally build a subclass of generated parser class""" 273 274 return base 275 276 277 def walkerClass(self, base): 278 """Optionally build a subclass of generated walker class""" 279 280 return base 281 282 283 def __load_module(self, name): 284 modFile, modPathname, modDescription = imp.find_module(name, [self.baseDir]) 285 286 with modFile: 287 return imp.load_module(name, modFile, modPathname, modDescription) 288 289 290 def getLexer(self, *args, **kwargs): 291 """Build lexer instance. Arguments are passed to lexer.__init__().""" 292 293 if self.grammarType == 'lexer': 294 self.lexerModule = self.__load_module(self.grammarName) 295 cls = getattr(self.lexerModule, self.grammarName) 296 else: 297 self.lexerModule = self.__load_module(self.grammarName + 'Lexer') 298 cls = getattr(self.lexerModule, self.grammarName + 'Lexer') 299 300 cls = self.lexerClass(cls) 301 302 lexer = cls(*args, **kwargs) 303 304 return lexer 305 306 307 def getParser(self, *args, **kwargs): 308 """Build parser instance. Arguments are passed to parser.__init__().""" 309 310 if self.grammarType == 'parser': 311 self.lexerModule = self.__load_module(self.grammarName) 312 cls = getattr(self.lexerModule, self.grammarName) 313 else: 314 self.parserModule = self.__load_module(self.grammarName + 'Parser') 315 cls = getattr(self.parserModule, self.grammarName + 'Parser') 316 cls = self.parserClass(cls) 317 318 parser = cls(*args, **kwargs) 319 320 return parser 321 322 323 def getWalker(self, *args, **kwargs): 324 """Build walker instance. Arguments are passed to walker.__init__().""" 325 326 self.walkerModule = self.__load_module(self.grammarName + 'Walker') 327 cls = getattr(self.walkerModule, self.grammarName + 'Walker') 328 cls = self.walkerClass(cls) 329 330 walker = cls(*args, **kwargs) 331 332 return walker 333 334 335 def writeInlineGrammar(self, grammar): 336 # Create a unique ID for this test and use it as the grammar name, 337 # to avoid class name reuse. This kinda sucks. Need to find a way so 338 # tests can use the same grammar name without messing up the namespace. 339 # Well, first I should figure out what the exact problem is... 340 id = hashlib.md5(self.baseDir.encode('utf-8')).hexdigest()[-8:] 341 grammar = grammar.replace('$TP', 'TP' + id) 342 grammar = grammar.replace('$T', 'T' + id) 343 344 # get type and name from first grammar line 345 m = re.match(r'\s*((lexer|parser|tree)\s+|)grammar\s+(\S+);', grammar, re.MULTILINE) 346 self.assertIsNotNone(m, grammar) 347 grammarType = m.group(2) or 'combined' 348 grammarName = m.group(3) 349 350 self.assertIn(grammarType, ('lexer', 'parser', 'tree', 'combined')) 351 352 grammarPath = os.path.join(self.baseDir, grammarName + '.g') 353 354 # dump temp grammar file 355 with open(grammarPath, 'w') as fp: 356 fp.write(grammar) 357 358 return grammarName, grammarPath, grammarType 359 360 361 def writeFile(self, name, contents): 362 testDir = os.path.dirname(os.path.abspath(__file__)) 363 path = os.path.join(self.baseDir, name) 364 365 with open(path, 'w') as fp: 366 fp.write(contents) 367 368 return path 369 370 371 def compileInlineGrammar(self, grammar, options='', javaOptions='', 372 returnModule=False): 373 # write grammar file 374 grammarName, grammarPath, grammarType = self.writeInlineGrammar(grammar) 375 376 # compile it 377 self._invokeantlr( 378 os.path.dirname(grammarPath), 379 os.path.basename(grammarPath), 380 options, 381 javaOptions 382 ) 383 384 if grammarType == 'combined': 385 lexerMod = self.__load_module(grammarName + 'Lexer') 386 parserMod = self.__load_module(grammarName + 'Parser') 387 if returnModule: 388 return lexerMod, parserMod 389 390 lexerCls = getattr(lexerMod, grammarName + 'Lexer') 391 lexerCls = self.lexerClass(lexerCls) 392 parserCls = getattr(parserMod, grammarName + 'Parser') 393 parserCls = self.parserClass(parserCls) 394 395 return lexerCls, parserCls 396 397 if grammarType == 'lexer': 398 lexerMod = self.__load_module(grammarName) 399 if returnModule: 400 return lexerMod 401 402 lexerCls = getattr(lexerMod, grammarName) 403 lexerCls = self.lexerClass(lexerCls) 404 405 return lexerCls 406 407 if grammarType == 'parser': 408 parserMod = self.__load_module(grammarName) 409 if returnModule: 410 return parserMod 411 412 parserCls = getattr(parserMod, grammarName) 413 parserCls = self.parserClass(parserCls) 414 415 return parserCls 416 417 if grammarType == 'tree': 418 walkerMod = self.__load_module(grammarName) 419 if returnModule: 420 return walkerMod 421 422 walkerCls = getattr(walkerMod, grammarName) 423 walkerCls = self.walkerClass(walkerCls) 424 425 return walkerCls 426