| 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', '<module>'): |
| 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 |