1import os
2from clang.cindex import Config
3if 'CLANG_LIBRARY_PATH' in os.environ:
4    Config.set_library_path(os.environ['CLANG_LIBRARY_PATH'])
5
6from contextlib import contextmanager
7import gc
8import os
9import tempfile
10import unittest
11
12from clang.cindex import CursorKind
13from clang.cindex import Cursor
14from clang.cindex import File
15from clang.cindex import Index
16from clang.cindex import SourceLocation
17from clang.cindex import SourceRange
18from clang.cindex import TranslationUnitSaveError
19from clang.cindex import TranslationUnitLoadError
20from clang.cindex import TranslationUnit
21from .util import get_cursor
22from .util import get_tu
23from .util import skip_if_no_fspath
24from .util import str_to_path
25
26
27kInputsDir = os.path.join(os.path.dirname(__file__), 'INPUTS')
28
29
30@contextmanager
31def save_tu(tu):
32    """Convenience API to save a TranslationUnit to a file.
33
34    Returns the filename it was saved to.
35    """
36    with tempfile.NamedTemporaryFile() as t:
37        tu.save(t.name)
38        yield t.name
39
40
41@contextmanager
42def save_tu_pathlike(tu):
43    """Convenience API to save a TranslationUnit to a file.
44
45    Returns the filename it was saved to.
46    """
47    with tempfile.NamedTemporaryFile() as t:
48        tu.save(str_to_path(t.name))
49        yield t.name
50
51
52class TestTranslationUnit(unittest.TestCase):
53    def test_spelling(self):
54        path = os.path.join(kInputsDir, 'hello.cpp')
55        tu = TranslationUnit.from_source(path)
56        self.assertEqual(tu.spelling, path)
57
58    def test_cursor(self):
59        path = os.path.join(kInputsDir, 'hello.cpp')
60        tu = get_tu(path)
61        c = tu.cursor
62        self.assertIsInstance(c, Cursor)
63        self.assertIs(c.kind, CursorKind.TRANSLATION_UNIT)
64
65    def test_parse_arguments(self):
66        path = os.path.join(kInputsDir, 'parse_arguments.c')
67        tu = TranslationUnit.from_source(path, ['-DDECL_ONE=hello', '-DDECL_TWO=hi'])
68        spellings = [c.spelling for c in tu.cursor.get_children()]
69        self.assertEqual(spellings[-2], 'hello')
70        self.assertEqual(spellings[-1], 'hi')
71
72    def test_reparse_arguments(self):
73        path = os.path.join(kInputsDir, 'parse_arguments.c')
74        tu = TranslationUnit.from_source(path, ['-DDECL_ONE=hello', '-DDECL_TWO=hi'])
75        tu.reparse()
76        spellings = [c.spelling for c in tu.cursor.get_children()]
77        self.assertEqual(spellings[-2], 'hello')
78        self.assertEqual(spellings[-1], 'hi')
79
80    def test_unsaved_files(self):
81        tu = TranslationUnit.from_source('fake.c', ['-I./'], unsaved_files = [
82                ('fake.c', """
83#include "fake.h"
84int x;
85int SOME_DEFINE;
86"""),
87                ('./fake.h', """
88#define SOME_DEFINE y
89""")
90                ])
91        spellings = [c.spelling for c in tu.cursor.get_children()]
92        self.assertEqual(spellings[-2], 'x')
93        self.assertEqual(spellings[-1], 'y')
94
95    def test_unsaved_files_2(self):
96        try:
97            from StringIO import StringIO
98        except:
99            from io import StringIO
100        tu = TranslationUnit.from_source('fake.c', unsaved_files = [
101                ('fake.c', StringIO('int x;'))])
102        spellings = [c.spelling for c in tu.cursor.get_children()]
103        self.assertEqual(spellings[-1], 'x')
104
105    @skip_if_no_fspath
106    def test_from_source_accepts_pathlike(self):
107        tu = TranslationUnit.from_source(str_to_path('fake.c'), ['-Iincludes'], unsaved_files = [
108                (str_to_path('fake.c'), """
109#include "fake.h"
110    int x;
111    int SOME_DEFINE;
112    """),
113                    (str_to_path('includes/fake.h'), """
114#define SOME_DEFINE y
115    """)
116                ])
117        spellings = [c.spelling for c in tu.cursor.get_children()]
118        self.assertEqual(spellings[-2], 'x')
119        self.assertEqual(spellings[-1], 'y')
120
121    def assert_normpaths_equal(self, path1, path2):
122        """ Compares two paths for equality after normalizing them with
123            os.path.normpath
124        """
125        self.assertEqual(os.path.normpath(path1),
126                         os.path.normpath(path2))
127
128    def test_includes(self):
129        def eq(expected, actual):
130            if not actual.is_input_file:
131                self.assert_normpaths_equal(expected[0], actual.source.name)
132                self.assert_normpaths_equal(expected[1], actual.include.name)
133            else:
134                self.assert_normpaths_equal(expected[1], actual.include.name)
135
136        src = os.path.join(kInputsDir, 'include.cpp')
137        h1 = os.path.join(kInputsDir, "header1.h")
138        h2 = os.path.join(kInputsDir, "header2.h")
139        h3 = os.path.join(kInputsDir, "header3.h")
140        inc = [(src, h1), (h1, h3), (src, h2), (h2, h3)]
141
142        tu = TranslationUnit.from_source(src)
143        for i in zip(inc, tu.get_includes()):
144            eq(i[0], i[1])
145
146    def test_inclusion_directive(self):
147        src = os.path.join(kInputsDir, 'include.cpp')
148        h1 = os.path.join(kInputsDir, "header1.h")
149        h2 = os.path.join(kInputsDir, "header2.h")
150        h3 = os.path.join(kInputsDir, "header3.h")
151        inc = [h1, h3, h2, h3, h1]
152
153        tu = TranslationUnit.from_source(src, options=TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD)
154        inclusion_directive_files = [c.get_included_file().name for c in tu.cursor.get_children() if c.kind == CursorKind.INCLUSION_DIRECTIVE]
155        for i in zip(inc, inclusion_directive_files):
156            self.assert_normpaths_equal(i[0], i[1])
157
158    def test_save(self):
159        """Ensure TranslationUnit.save() works."""
160
161        tu = get_tu('int foo();')
162
163        with save_tu(tu) as path:
164            self.assertTrue(os.path.exists(path))
165            self.assertGreater(os.path.getsize(path), 0)
166
167    @skip_if_no_fspath
168    def test_save_pathlike(self):
169        """Ensure TranslationUnit.save() works with PathLike filename."""
170
171        tu = get_tu('int foo();')
172
173        with save_tu_pathlike(tu) as path:
174            self.assertTrue(os.path.exists(path))
175            self.assertGreater(os.path.getsize(path), 0)
176
177    def test_save_translation_errors(self):
178        """Ensure that saving to an invalid directory raises."""
179
180        tu = get_tu('int foo();')
181
182        path = '/does/not/exist/llvm-test.ast'
183        self.assertFalse(os.path.exists(os.path.dirname(path)))
184
185        with self.assertRaises(TranslationUnitSaveError) as cm:
186            tu.save(path)
187        ex = cm.exception
188        expected = TranslationUnitSaveError.ERROR_UNKNOWN
189        self.assertEqual(ex.save_error, expected)
190
191    def test_load(self):
192        """Ensure TranslationUnits can be constructed from saved files."""
193
194        tu = get_tu('int foo();')
195        self.assertEqual(len(tu.diagnostics), 0)
196        with save_tu(tu) as path:
197            self.assertTrue(os.path.exists(path))
198            self.assertGreater(os.path.getsize(path), 0)
199
200            tu2 = TranslationUnit.from_ast_file(filename=path)
201            self.assertEqual(len(tu2.diagnostics), 0)
202
203            foo = get_cursor(tu2, 'foo')
204            self.assertIsNotNone(foo)
205
206            # Just in case there is an open file descriptor somewhere.
207            del tu2
208
209    @skip_if_no_fspath
210    def test_load_pathlike(self):
211        """Ensure TranslationUnits can be constructed from saved files -
212        PathLike variant."""
213        tu = get_tu('int foo();')
214        self.assertEqual(len(tu.diagnostics), 0)
215        with save_tu(tu) as path:
216            tu2 = TranslationUnit.from_ast_file(filename=str_to_path(path))
217            self.assertEqual(len(tu2.diagnostics), 0)
218
219            foo = get_cursor(tu2, 'foo')
220            self.assertIsNotNone(foo)
221
222            # Just in case there is an open file descriptor somewhere.
223            del tu2
224
225    def test_index_parse(self):
226        path = os.path.join(kInputsDir, 'hello.cpp')
227        index = Index.create()
228        tu = index.parse(path)
229        self.assertIsInstance(tu, TranslationUnit)
230
231    def test_get_file(self):
232        """Ensure tu.get_file() works appropriately."""
233
234        tu = get_tu('int foo();')
235
236        f = tu.get_file('t.c')
237        self.assertIsInstance(f, File)
238        self.assertEqual(f.name, 't.c')
239
240        with self.assertRaises(Exception):
241            f = tu.get_file('foobar.cpp')
242
243    @skip_if_no_fspath
244    def test_get_file_pathlike(self):
245        """Ensure tu.get_file() works appropriately with PathLike filenames."""
246
247        tu = get_tu('int foo();')
248
249        f = tu.get_file(str_to_path('t.c'))
250        self.assertIsInstance(f, File)
251        self.assertEqual(f.name, 't.c')
252
253        with self.assertRaises(Exception):
254            f = tu.get_file(str_to_path('foobar.cpp'))
255
256    def test_get_source_location(self):
257        """Ensure tu.get_source_location() works."""
258
259        tu = get_tu('int foo();')
260
261        location = tu.get_location('t.c', 2)
262        self.assertIsInstance(location, SourceLocation)
263        self.assertEqual(location.offset, 2)
264        self.assertEqual(location.file.name, 't.c')
265
266        location = tu.get_location('t.c', (1, 3))
267        self.assertIsInstance(location, SourceLocation)
268        self.assertEqual(location.line, 1)
269        self.assertEqual(location.column, 3)
270        self.assertEqual(location.file.name, 't.c')
271
272    def test_get_source_range(self):
273        """Ensure tu.get_source_range() works."""
274
275        tu = get_tu('int foo();')
276
277        r = tu.get_extent('t.c', (1,4))
278        self.assertIsInstance(r, SourceRange)
279        self.assertEqual(r.start.offset, 1)
280        self.assertEqual(r.end.offset, 4)
281        self.assertEqual(r.start.file.name, 't.c')
282        self.assertEqual(r.end.file.name, 't.c')
283
284        r = tu.get_extent('t.c', ((1,2), (1,3)))
285        self.assertIsInstance(r, SourceRange)
286        self.assertEqual(r.start.line, 1)
287        self.assertEqual(r.start.column, 2)
288        self.assertEqual(r.end.line, 1)
289        self.assertEqual(r.end.column, 3)
290        self.assertEqual(r.start.file.name, 't.c')
291        self.assertEqual(r.end.file.name, 't.c')
292
293        start = tu.get_location('t.c', 0)
294        end = tu.get_location('t.c', 5)
295
296        r = tu.get_extent('t.c', (start, end))
297        self.assertIsInstance(r, SourceRange)
298        self.assertEqual(r.start.offset, 0)
299        self.assertEqual(r.end.offset, 5)
300        self.assertEqual(r.start.file.name, 't.c')
301        self.assertEqual(r.end.file.name, 't.c')
302
303    def test_get_tokens_gc(self):
304        """Ensures get_tokens() works properly with garbage collection."""
305
306        tu = get_tu('int foo();')
307        r = tu.get_extent('t.c', (0, 10))
308        tokens = list(tu.get_tokens(extent=r))
309
310        self.assertEqual(tokens[0].spelling, 'int')
311        gc.collect()
312        self.assertEqual(tokens[0].spelling, 'int')
313
314        del tokens[1]
315        gc.collect()
316        self.assertEqual(tokens[0].spelling, 'int')
317
318        # May trigger segfault if we don't do our job properly.
319        del tokens
320        gc.collect()
321        gc.collect() # Just in case.
322
323    def test_fail_from_source(self):
324        path = os.path.join(kInputsDir, 'non-existent.cpp')
325        try:
326            tu = TranslationUnit.from_source(path)
327        except TranslationUnitLoadError:
328            tu = None
329        self.assertEqual(tu, None)
330
331    def test_fail_from_ast_file(self):
332        path = os.path.join(kInputsDir, 'non-existent.ast')
333        try:
334            tu = TranslationUnit.from_ast_file(path)
335        except TranslationUnitLoadError:
336            tu = None
337        self.assertEqual(tu, None)
338