def test__given_recursive_function_call__parse_tu_contains_recursion(): access = ClangCallGraphAccess() with generate_file('two-functions.cpp', 'void f();\nvoid f() {f();}') as file_name: access.parse_tu(tu_file_name=file_name, compiler_arguments='') expected = ['f()'] actual = get_names_of_calls(access.get_calls_of('f()')) assert expected == actual
def test_given_unknown_entry_point__get_empty_calls_list(): access = ClangCallGraphAccess() with generate_file('one-function.cpp', 'void f() {}') as file_name: access.parse_tu(tu_file_name=file_name, compiler_arguments='') expected = [] actual = access.get_calls_of('g()') assert expected == actual
def test__given_parsed_tu_with_one_function__get_callable_returns_callable_with_expected_name(): access = ClangCallGraphAccess() with generate_file('one-function.cpp', 'void f() {}') as file_name: access.parse_tu(tu_file_name=file_name, compiler_arguments='') callable = access.get_callable('f()') expected = 'f()' actual = callable.name assert expected == actual
class CallTreeManager(object): ''' Manages call-tree related use cases ''' def __init__(self): super(CallTreeManager, self).__init__() self.extra_arguments_ = '' self.tu_access_ = None self.call_graph_access_ = None self.include_system_headers_ = False self.loaded_files_ = set() self.included_ = set() self.root_ = None self.state_ = CallTreeManagerState.INITIALIZED def set_extra_arguments(self, extra_arguments, include_system_headers=False): if self.state_ not in [CallTreeManagerState.INITIALIZED, CallTreeManagerState.EXTRA_ARGUMENTS_INITIALIZED]: warnings.warn('Unsupported state transition from {} to {}'.format( self.state_, CallTreeManagerState.EXTRA_ARGUMENTS_INITIALIZED)) return self.extra_arguments_ = extra_arguments self.include_system_headers_ = include_system_headers self.state_ = CallTreeManagerState.EXTRA_ARGUMENTS_INITIALIZED def open(self, file_name): if self.state_ not in [CallTreeManagerState.INITIALIZED, CallTreeManagerState.EXTRA_ARGUMENTS_INITIALIZED, CallTreeManagerState.READY_TO_SELECT_TU]: warnings.warn('Unsupported state transition from {} to {}'.format( self.state_, CallTreeManagerState.READY_TO_SELECT_TU)) return self.tu_access_ = ClangTUAccess(file_name=file_name, extra_arguments=self.extra_arguments_) set_global_common_path(find_common_path(list(self.tu_access_.files))) self.state_ = CallTreeManagerState.READY_TO_SELECT_TU return self.tu_access_.files.keys() def select_tu(self, file_name): if self.state_ not in [CallTreeManagerState.READY_TO_SELECT_TU, CallTreeManagerState.READY_TO_SELECT_ROOT]: warnings.warn('Unsupported state transition from {} to {}'.format( self.state_, CallTreeManagerState.READY_TO_SELECT_ROOT)) return if file_name not in self.tu_access_.files: warnings.warn('File {} not found in compilation database'.format(file_name)) return compiler_arguments = self.tu_access_.files[file_name] self.call_graph_access_ = ClangCallGraphAccess(include_system_headers=self.include_system_headers_) self.call_graph_access_.parse_tu(tu_file_name=file_name, compiler_arguments=compiler_arguments) self.loaded_files_.add(file_name) self.state_ = CallTreeManagerState.READY_TO_SELECT_ROOT return self.call_graph_access_.get_callables_in(file_name) def select_root(self, callable_name): if self.state_ not in [CallTreeManagerState.READY_TO_SELECT_ROOT, CallTreeManagerState.READY_FOR_INTERACTIONS]: warnings.warn('Unsupported state transition from {} to {}'.format( self.state_, CallTreeManagerState.READY_FOR_INTERACTIONS)) return self.root_ = self.call_graph_access_.get_callable(callable_name) self.state_ = CallTreeManagerState.READY_FOR_INTERACTIONS return self.root_ def load_definition(self, callable_name): for file_name, compiler_arguments in self.list_tu_candidates_(callable_name).items(): if file_name not in self.loaded_files_: self.call_graph_access_.parse_tu(tu_file_name=file_name, compiler_arguments=compiler_arguments) self.loaded_files_.add(file_name) callable = self.call_graph_access_.get_callable(callable_name) if callable and callable.is_definition(): return callable def get_calls_of(self, callable_name): return self.call_graph_access_.get_calls_of(callable_name) def include(self, callable_name): self.included_.add(callable_name) def exclude(self, callable_name): if callable_name in self.included_: self.included_.remove(callable_name) def export(self): call_tree = '' if self.root_.name in self.included_: call_tree = ' -> ' + quote(self.root_.participant) + ': ' + self.root_.callable + '\n' call_tree += 'activate {}\n'.format(quote(self.root_.participant)) call_tree += self.export_calls_(parent=self.root_, included_parent_name='') if self.root_.name in self.included_: call_tree += 'deactivate {}\n'.format(quote(self.root_.participant)) return '@startuml\n\n{}\n@enduml'.format(call_tree) def export_calls_(self, parent, included_parent_name): call_tree = '' if parent is None: return call_tree parent_name = included_parent_name if parent.name in self.included_: parent_name = parent.participant for call in self.call_graph_access_.get_calls_of(parent.name): if call.name in self.included_: call_tree += quote(parent_name) + ' -> ' + quote(call.participant) + ': ' + call.callable + '\n' # TODO(KNR): avoid redundant activations call_tree += 'activate {}\n'.format(quote(call.participant)) call_tree += self.export_calls_(parent=call, included_parent_name=parent_name) if call.name in self.included_: call_tree += 'deactivate {}\n'.format(quote(call.participant)) return call_tree def dump(self, file_name, entry_point, include_system_headers=False, extra_arguments=None): tu_access = ClangTUAccess(file_name=file_name, extra_arguments=extra_arguments) self.call_graph_access_ = ClangCallGraphAccess(include_system_headers=include_system_headers) for file_name, compiler_arguments in tu_access.files.items(): self.call_graph_access_.parse_tu(tu_file_name=file_name, compiler_arguments=compiler_arguments) root = self.call_graph_access_.get_callable(entry_point) return self.dump_callable_(root, 0) def list_tu_candidates_(self, callable_name): callable = self.call_graph_access_.get_callable(callable_name) assert callable # TODO(KNR): be nicer search_key = callable.get_spelling() tu_candidates = {file_name: compiler_arguments for file_name, compiler_arguments in self.tu_access_.files.items() if find_text_in_file_(file_name=file_name, text=search_key)} # TODO(KNR): figure out how to avoid Decorate-Sort-Undecorate idiom decorated = [(rate_path_commonality_(callable.used_in_file, file_name), file_name, compiler_arguments) for file_name, compiler_arguments in tu_candidates.items()] decorated.sort(reverse=True) tu_candidates = {file_name: compiler_arguments for _, file_name, compiler_arguments in decorated} return tu_candidates def dump_callable_(self, callable, level): indentation = level * ' ' call_tree = '{}{}\n'.format(indentation, callable.name) for call in self.call_graph_access_.get_calls_of(callable.name): call_tree += self.dump_callable_(call, level + 1) return call_tree
def test__given_parsed_tu_with_one_function__get_callables_in_returns_one_callable(): access = ClangCallGraphAccess() with generate_file('one-function.cpp', 'void f() {}') as file_name: access.parse_tu(tu_file_name=file_name, compiler_arguments='') callables = access.get_callables_in(file_name) assert len(callables) == 1
def test__given_parsed_tu_with_one_function__callables_contains_one_item(): access = ClangCallGraphAccess() with generate_file('syntax-error.cpp', 'void f() {}') as file_name: access.parse_tu(tu_file_name=file_name, compiler_arguments='') callables = access.callables assert len(callables) == 1
def test__given_file_with_syntax_error__parse_tu_throws(): access = ClangCallGraphAccess() with generate_file('syntax-error.cpp', 'void f() {') as file_name: with pytest.raises(SyntaxError): access.parse_tu(tu_file_name=file_name, compiler_arguments='')
def test__given_non_existing_file__parse_tu_throws(): access = ClangCallGraphAccess() with pytest.raises(FileNotFoundError): access.parse_tu(tu_file_name='a-file-that-doesnt-exist', compiler_arguments='')