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