from distutils.errors import * import errno import glob import hashlib import imp import inspect import os import re import shutil import sys import tempfile import unittest import antlr3 def unlink(path): try: os.unlink(path) except OSError as exc: if exc.errno != errno.ENOENT: raise class GrammarCompileError(Exception): """Grammar failed to compile.""" pass # At least on MacOSX tempdir (/tmp) is a symlink. It's sometimes dereferences, # sometimes not, breaking the inspect.getmodule() function. testbasedir = os.path.join( os.path.realpath(tempfile.gettempdir()), 'antlr3-test') class BrokenTest(unittest.TestCase.failureException): def __repr__(self): name, reason = self.args return '{}: {}: {} works now'.format( (self.__class__.__name__, name, reason)) def broken(reason, *exceptions): '''Indicates a failing (or erroneous) test case fails that should succeed. If the test fails with an exception, list the exception type in args''' def wrapper(test_method): def replacement(*args, **kwargs): try: test_method(*args, **kwargs) except exceptions or unittest.TestCase.failureException: pass else: raise BrokenTest(test_method.__name__, reason) replacement.__doc__ = test_method.__doc__ replacement.__name__ = 'XXX_' + test_method.__name__ replacement.todo = reason return replacement return wrapper dependencyCache = {} compileErrorCache = {} # setup java CLASSPATH if 'CLASSPATH' not in os.environ: cp = [] baseDir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) libDir = os.path.join(baseDir, 'lib') jar = os.path.join(libDir, 'ST-4.0.5.jar') if not os.path.isfile(jar): raise DistutilsFileError( "Missing file '{}'. Grab it from a distribution package.".format(jar) ) cp.append(jar) jar = os.path.join(libDir, 'antlr-3.4.1-SNAPSHOT.jar') if not os.path.isfile(jar): raise DistutilsFileError( "Missing file '{}'. Grab it from a distribution package.".format(jar) ) cp.append(jar) jar = os.path.join(libDir, 'antlr-runtime-3.4.jar') if not os.path.isfile(jar): raise DistutilsFileError( "Missing file '{}'. Grab it from a distribution package.".format(jar) ) cp.append(jar) cp.append(os.path.join(baseDir, 'runtime', 'Python', 'build')) classpath = '-cp "' + ':'.join([os.path.abspath(p) for p in cp]) + '"' else: classpath = '' class ANTLRTest(unittest.TestCase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.moduleName = os.path.splitext(os.path.basename(sys.modules[self.__module__].__file__))[0] self.className = self.__class__.__name__ self._baseDir = None self.lexerModule = None self.parserModule = None self.grammarName = None self.grammarType = None @property def baseDir(self): if self._baseDir is None: testName = 'unknownTest' for frame in inspect.stack(): code = frame[0].f_code codeMod = inspect.getmodule(code) if codeMod is None: continue # skip frames not in requested module if codeMod is not sys.modules[self.__module__]: continue # skip some unwanted names if code.co_name in ('nextToken', ''): continue if code.co_name.startswith('test'): testName = code.co_name break self._baseDir = os.path.join( testbasedir, self.moduleName, self.className, testName) if not os.path.isdir(self._baseDir): os.makedirs(self._baseDir) return self._baseDir def _invokeantlr(self, dir, file, options, javaOptions=''): cmd = 'cd {}; java {} {} org.antlr.Tool -o . {} {} 2>&1'.format( dir, javaOptions, classpath, options, file ) fp = os.popen(cmd) output = '' failed = False for line in fp: output += line if line.startswith('error('): failed = True rc = fp.close() if rc: failed = True if failed: raise GrammarCompileError( "Failed to compile grammar '{}':\n{}\n\n{}".format(file, cmd, output) ) def compileGrammar(self, grammarName=None, options='', javaOptions=''): if grammarName is None: grammarName = self.moduleName + '.g' self._baseDir = os.path.join( testbasedir, self.moduleName) if not os.path.isdir(self._baseDir): os.makedirs(self._baseDir) if self.grammarName is None: self.grammarName = os.path.splitext(grammarName)[0] grammarPath = os.path.join(os.path.dirname(os.path.abspath(__file__)), grammarName) # get type and name from first grammar line with open(grammarPath, 'r') as fp: grammar = fp.read() m = re.match(r'\s*((lexer|parser|tree)\s+|)grammar\s+(\S+);', grammar, re.MULTILINE) self.assertIsNotNone(m, grammar) self.grammarType = m.group(2) or 'combined' self.assertIn(self.grammarType, ('lexer', 'parser', 'tree', 'combined')) # don't try to rebuild grammar, if it already failed if grammarName in compileErrorCache: return try: # # get dependencies from antlr # if grammarName in dependencyCache: # dependencies = dependencyCache[grammarName] # else: # dependencies = [] # cmd = ('cd %s; java %s %s org.antlr.Tool -o . -depend %s 2>&1' # % (self.baseDir, javaOptions, classpath, grammarPath)) # output = "" # failed = False # fp = os.popen(cmd) # for line in fp: # output += line # if line.startswith('error('): # failed = True # elif ':' in line: # a, b = line.strip().split(':', 1) # dependencies.append( # (os.path.join(self.baseDir, a.strip()), # [os.path.join(self.baseDir, b.strip())]) # ) # rc = fp.close() # if rc is not None: # failed = True # if failed: # raise GrammarCompileError( # "antlr -depend failed with code {} on grammar '{}':\n\n{}\n{}".format( # rc, grammarName, cmd, output) # ) # # add dependencies to my .stg files # templateDir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'tool', 'src', 'main', 'resources', 'org', 'antlr', 'codegen', 'templates', 'Python')) # templates = glob.glob(os.path.join(templateDir, '*.stg')) # for dst, src in dependencies: # src.extend(templates) # dependencyCache[grammarName] = dependencies # rebuild = False # for dest, sources in dependencies: # if not os.path.isfile(dest): # rebuild = True # break # for source in sources: # if os.path.getmtime(source) > os.path.getmtime(dest): # rebuild = True # break # if rebuild: # self._invokeantlr(self.baseDir, grammarPath, options, javaOptions) self._invokeantlr(self.baseDir, grammarPath, options, javaOptions) except: # mark grammar as broken compileErrorCache[grammarName] = True raise def lexerClass(self, base): """Optionally build a subclass of generated lexer class""" return base def parserClass(self, base): """Optionally build a subclass of generated parser class""" return base def walkerClass(self, base): """Optionally build a subclass of generated walker class""" return base def __load_module(self, name): modFile, modPathname, modDescription = imp.find_module(name, [self.baseDir]) with modFile: return imp.load_module(name, modFile, modPathname, modDescription) def getLexer(self, *args, **kwargs): """Build lexer instance. Arguments are passed to lexer.__init__().""" if self.grammarType == 'lexer': self.lexerModule = self.__load_module(self.grammarName) cls = getattr(self.lexerModule, self.grammarName) else: self.lexerModule = self.__load_module(self.grammarName + 'Lexer') cls = getattr(self.lexerModule, self.grammarName + 'Lexer') cls = self.lexerClass(cls) lexer = cls(*args, **kwargs) return lexer def getParser(self, *args, **kwargs): """Build parser instance. Arguments are passed to parser.__init__().""" if self.grammarType == 'parser': self.lexerModule = self.__load_module(self.grammarName) cls = getattr(self.lexerModule, self.grammarName) else: self.parserModule = self.__load_module(self.grammarName + 'Parser') cls = getattr(self.parserModule, self.grammarName + 'Parser') cls = self.parserClass(cls) parser = cls(*args, **kwargs) return parser def getWalker(self, *args, **kwargs): """Build walker instance. Arguments are passed to walker.__init__().""" self.walkerModule = self.__load_module(self.grammarName + 'Walker') cls = getattr(self.walkerModule, self.grammarName + 'Walker') cls = self.walkerClass(cls) walker = cls(*args, **kwargs) return walker def writeInlineGrammar(self, grammar): # Create a unique ID for this test and use it as the grammar name, # to avoid class name reuse. This kinda sucks. Need to find a way so # tests can use the same grammar name without messing up the namespace. # Well, first I should figure out what the exact problem is... id = hashlib.md5(self.baseDir.encode('utf-8')).hexdigest()[-8:] grammar = grammar.replace('$TP', 'TP' + id) grammar = grammar.replace('$T', 'T' + id) # get type and name from first grammar line m = re.match(r'\s*((lexer|parser|tree)\s+|)grammar\s+(\S+);', grammar, re.MULTILINE) self.assertIsNotNone(m, grammar) grammarType = m.group(2) or 'combined' grammarName = m.group(3) self.assertIn(grammarType, ('lexer', 'parser', 'tree', 'combined')) grammarPath = os.path.join(self.baseDir, grammarName + '.g') # dump temp grammar file with open(grammarPath, 'w') as fp: fp.write(grammar) return grammarName, grammarPath, grammarType def writeFile(self, name, contents): testDir = os.path.dirname(os.path.abspath(__file__)) path = os.path.join(self.baseDir, name) with open(path, 'w') as fp: fp.write(contents) return path def compileInlineGrammar(self, grammar, options='', javaOptions='', returnModule=False): # write grammar file grammarName, grammarPath, grammarType = self.writeInlineGrammar(grammar) # compile it self._invokeantlr( os.path.dirname(grammarPath), os.path.basename(grammarPath), options, javaOptions ) if grammarType == 'combined': lexerMod = self.__load_module(grammarName + 'Lexer') parserMod = self.__load_module(grammarName + 'Parser') if returnModule: return lexerMod, parserMod lexerCls = getattr(lexerMod, grammarName + 'Lexer') lexerCls = self.lexerClass(lexerCls) parserCls = getattr(parserMod, grammarName + 'Parser') parserCls = self.parserClass(parserCls) return lexerCls, parserCls if grammarType == 'lexer': lexerMod = self.__load_module(grammarName) if returnModule: return lexerMod lexerCls = getattr(lexerMod, grammarName) lexerCls = self.lexerClass(lexerCls) return lexerCls if grammarType == 'parser': parserMod = self.__load_module(grammarName) if returnModule: return parserMod parserCls = getattr(parserMod, grammarName) parserCls = self.parserClass(parserCls) return parserCls if grammarType == 'tree': walkerMod = self.__load_module(grammarName) if returnModule: return walkerMod walkerCls = getattr(walkerMod, grammarName) walkerCls = self.walkerClass(walkerCls) return walkerCls