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