358 lines · python
1import os2 3from clang.cindex import (4 Config,5 Cursor,6 CursorKind,7 File,8 Index,9 SourceLocation,10 SourceRange,11 TranslationUnit,12 TranslationUnitLoadError,13 TranslationUnitSaveError,14)15 16if "CLANG_LIBRARY_PATH" in os.environ:17 Config.set_library_path(os.environ["CLANG_LIBRARY_PATH"])18 19import gc20import tempfile21import unittest22from contextlib import contextmanager23from pathlib import Path24 25from .util import get_cursor, get_tu26 27INPUTS_DIR = os.path.join(os.path.dirname(__file__), "INPUTS")28 29 30@contextmanager31def 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.name39 40 41@contextmanager42def 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(Path(t.name))49 yield t.name50 51 52class TestTranslationUnit(unittest.TestCase):53 def test_spelling(self):54 path = os.path.join(INPUTS_DIR, "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(INPUTS_DIR, "hello.cpp")60 tu = get_tu(path)61 c = tu.cursor62 self.assertIsInstance(c, Cursor)63 self.assertIs(c.kind, CursorKind.TRANSLATION_UNIT)64 65 def test_parse_arguments(self):66 path = os.path.join(INPUTS_DIR, "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(INPUTS_DIR, "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(82 "fake.c",83 ["-I./"],84 unsaved_files=[85 (86 "fake.c",87 """88#include "fake.h"89int x;90int SOME_DEFINE;91""",92 ),93 (94 "./fake.h",95 """96#define SOME_DEFINE y97""",98 ),99 ],100 )101 spellings = [c.spelling for c in tu.cursor.get_children()]102 self.assertEqual(spellings[-2], "x")103 self.assertEqual(spellings[-1], "y")104 105 def test_unsaved_files_2(self):106 from io import StringIO107 108 tu = TranslationUnit.from_source(109 "fake.c", unsaved_files=[("fake.c", StringIO("int x;"))]110 )111 spellings = [c.spelling for c in tu.cursor.get_children()]112 self.assertEqual(spellings[-1], "x")113 114 def test_from_source_accepts_pathlike(self):115 tu = TranslationUnit.from_source(116 Path("fake.c"),117 ["-Iincludes"],118 unsaved_files=[119 (120 Path("fake.c"),121 """122#include "fake.h"123 int x;124 int SOME_DEFINE;125 """,126 ),127 (128 Path("includes/fake.h"),129 """130#define SOME_DEFINE y131 """,132 ),133 ],134 )135 spellings = [c.spelling for c in tu.cursor.get_children()]136 self.assertEqual(spellings[-2], "x")137 self.assertEqual(spellings[-1], "y")138 139 def assert_normpaths_equal(self, path1, path2):140 """Compares two paths for equality after normalizing them with141 os.path.normpath142 """143 self.assertEqual(os.path.normpath(path1), os.path.normpath(path2))144 145 def test_includes(self):146 def eq(expected, actual):147 if not actual.is_input_file:148 self.assert_normpaths_equal(expected[0], actual.source.name)149 self.assert_normpaths_equal(expected[1], actual.include.name)150 else:151 self.assert_normpaths_equal(expected[1], actual.include.name)152 153 src = os.path.join(INPUTS_DIR, "include.cpp")154 h1 = os.path.join(INPUTS_DIR, "header1.h")155 h2 = os.path.join(INPUTS_DIR, "header2.h")156 h3 = os.path.join(INPUTS_DIR, "header3.h")157 inc = [(src, h1), (h1, h3), (src, h2), (h2, h3)]158 159 tu = TranslationUnit.from_source(src)160 for i in zip(inc, tu.get_includes()):161 eq(i[0], i[1])162 163 def test_inclusion_directive(self):164 src = os.path.join(INPUTS_DIR, "include.cpp")165 h1 = os.path.join(INPUTS_DIR, "header1.h")166 h2 = os.path.join(INPUTS_DIR, "header2.h")167 h3 = os.path.join(INPUTS_DIR, "header3.h")168 inc = [h1, h3, h2, h3, h1]169 170 tu = TranslationUnit.from_source(171 src, options=TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD172 )173 inclusion_directive_files = [174 c.get_included_file().name175 for c in tu.cursor.get_children()176 if c.kind == CursorKind.INCLUSION_DIRECTIVE177 ]178 for i in zip(inc, inclusion_directive_files):179 self.assert_normpaths_equal(i[0], i[1])180 181 def test_save(self):182 """Ensure TranslationUnit.save() works."""183 184 tu = get_tu("int foo();")185 186 with save_tu(tu) as path:187 self.assertTrue(os.path.exists(path))188 self.assertGreater(os.path.getsize(path), 0)189 190 def test_save_pathlike(self):191 """Ensure TranslationUnit.save() works with PathLike filename."""192 193 tu = get_tu("int foo();")194 195 with save_tu_pathlike(tu) as path:196 self.assertTrue(os.path.exists(path))197 self.assertGreater(os.path.getsize(path), 0)198 199 def test_save_translation_errors(self):200 """Ensure that saving to an invalid directory raises."""201 202 tu = get_tu("int foo();")203 204 path = "/does/not/exist/llvm-test.ast"205 self.assertFalse(os.path.exists(os.path.dirname(path)))206 207 with self.assertRaises(TranslationUnitSaveError) as cm:208 tu.save(path)209 ex = cm.exception210 expected = TranslationUnitSaveError.ERROR_UNKNOWN211 self.assertEqual(ex.save_error, expected)212 213 def test_load(self):214 """Ensure TranslationUnits can be constructed from saved files."""215 216 tu = get_tu("int foo();")217 self.assertEqual(len(tu.diagnostics), 0)218 with save_tu(tu) as path:219 self.assertTrue(os.path.exists(path))220 self.assertGreater(os.path.getsize(path), 0)221 222 tu2 = TranslationUnit.from_ast_file(filename=path)223 self.assertEqual(len(tu2.diagnostics), 0)224 225 foo = get_cursor(tu2, "foo")226 self.assertIsNotNone(foo)227 228 # Just in case there is an open file descriptor somewhere.229 del tu2230 231 def test_load_pathlike(self):232 """Ensure TranslationUnits can be constructed from saved files -233 PathLike variant."""234 tu = get_tu("int foo();")235 self.assertEqual(len(tu.diagnostics), 0)236 with save_tu(tu) as path:237 tu2 = TranslationUnit.from_ast_file(filename=Path(path))238 self.assertEqual(len(tu2.diagnostics), 0)239 240 foo = get_cursor(tu2, "foo")241 self.assertIsNotNone(foo)242 243 # Just in case there is an open file descriptor somewhere.244 del tu2245 246 def test_index_parse(self):247 path = os.path.join(INPUTS_DIR, "hello.cpp")248 index = Index.create()249 tu = index.parse(path)250 self.assertIsInstance(tu, TranslationUnit)251 252 def test_get_file(self):253 """Ensure tu.get_file() works appropriately."""254 255 tu = get_tu("int foo();")256 257 f = tu.get_file("t.c")258 self.assertIsInstance(f, File)259 self.assertEqual(f.name, "t.c")260 261 with self.assertRaises(Exception):262 f = tu.get_file("foobar.cpp")263 264 def test_get_file_pathlike(self):265 """Ensure tu.get_file() works appropriately with PathLike filenames."""266 267 tu = get_tu("int foo();")268 269 f = tu.get_file(Path("t.c"))270 self.assertIsInstance(f, File)271 self.assertEqual(f.name, "t.c")272 273 with self.assertRaises(Exception):274 f = tu.get_file(Path("foobar.cpp"))275 276 def test_get_source_location(self):277 """Ensure tu.get_source_location() works."""278 279 tu = get_tu("int foo();")280 281 location = tu.get_location("t.c", 2)282 self.assertIsInstance(location, SourceLocation)283 self.assertEqual(location.offset, 2)284 self.assertEqual(location.file.name, "t.c")285 286 location = tu.get_location("t.c", (1, 3))287 self.assertIsInstance(location, SourceLocation)288 self.assertEqual(location.line, 1)289 self.assertEqual(location.column, 3)290 self.assertEqual(location.file.name, "t.c")291 292 def test_get_source_range(self):293 """Ensure tu.get_source_range() works."""294 295 tu = get_tu("int foo();")296 297 r = tu.get_extent("t.c", (1, 4))298 self.assertIsInstance(r, SourceRange)299 self.assertEqual(r.start.offset, 1)300 self.assertEqual(r.end.offset, 4)301 self.assertEqual(r.start.file.name, "t.c")302 self.assertEqual(r.end.file.name, "t.c")303 304 r = tu.get_extent("t.c", ((1, 2), (1, 3)))305 self.assertIsInstance(r, SourceRange)306 self.assertEqual(r.start.line, 1)307 self.assertEqual(r.start.column, 2)308 self.assertEqual(r.end.line, 1)309 self.assertEqual(r.end.column, 3)310 self.assertEqual(r.start.file.name, "t.c")311 self.assertEqual(r.end.file.name, "t.c")312 313 start = tu.get_location("t.c", 0)314 end = tu.get_location("t.c", 5)315 316 r = tu.get_extent("t.c", (start, end))317 self.assertIsInstance(r, SourceRange)318 self.assertEqual(r.start.offset, 0)319 self.assertEqual(r.end.offset, 5)320 self.assertEqual(r.start.file.name, "t.c")321 self.assertEqual(r.end.file.name, "t.c")322 323 def test_get_tokens_gc(self):324 """Ensures get_tokens() works properly with garbage collection."""325 326 tu = get_tu("int foo();")327 r = tu.get_extent("t.c", (0, 10))328 tokens = list(tu.get_tokens(extent=r))329 330 self.assertEqual(tokens[0].spelling, "int")331 gc.collect()332 self.assertEqual(tokens[0].spelling, "int")333 334 del tokens[1]335 gc.collect()336 self.assertEqual(tokens[0].spelling, "int")337 338 # May trigger segfault if we don't do our job properly.339 del tokens340 gc.collect()341 gc.collect() # Just in case.342 343 def test_fail_from_source(self):344 path = os.path.join(INPUTS_DIR, "non-existent.cpp")345 try:346 tu = TranslationUnit.from_source(path)347 except TranslationUnitLoadError:348 tu = None349 self.assertEqual(tu, None)350 351 def test_fail_from_ast_file(self):352 path = os.path.join(INPUTS_DIR, "non-existent.ast")353 try:354 tu = TranslationUnit.from_ast_file(path)355 except TranslationUnitLoadError:356 tu = None357 self.assertEqual(tu, None)358