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