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