diff --git a/libscanbuild/__init__.py b/libscanbuild/__init__.py index ccd55b6..6c598f3 100644 --- a/libscanbuild/__init__.py +++ b/libscanbuild/__init__.py @@ -21,6 +21,9 @@ Execution = collections.namedtuple('Execution', ['pid', 'cwd', 'cmd']) +CtuConfig = collections.namedtuple('CtuConfig', ['collect', 'analyze', 'dir', + 'func_map_cmd']) + def shell_split(string): # type: (str) -> List[str] diff --git a/libscanbuild/analyze.py b/libscanbuild/analyze.py index c8c374b..6685747 100644 --- a/libscanbuild/analyze.py +++ b/libscanbuild/analyze.py @@ -23,18 +23,20 @@ import platform import contextlib import datetime +import shutil +import glob import argparse # noqa: ignore=F401 from typing import Any, Dict, List, Callable, Iterable, Generator # noqa: ignore=F401 from libscanbuild import command_entry_point, wrapper_entry_point, \ - wrapper_environment, run_build, run_command + wrapper_environment, run_build, run_command, CtuConfig from libscanbuild.arguments import parse_args_for_scan_build, \ parse_args_for_analyze_build from libscanbuild.intercept import capture from libscanbuild.report import document from libscanbuild.compilation import Compilation, classify_source, \ CompilationDatabase -from libscanbuild.clang import get_version, get_arguments +from libscanbuild.clang import get_version, get_arguments, get_triple_arch from libscanbuild import Execution # noqa: ignore=F401 __all__ = ['scan_build', 'analyze_build', 'analyze_compiler_wrapper'] @@ -43,6 +45,9 @@ COMPILER_WRAPPER_CXX = 'analyze-c++' ENVIRONMENT_KEY = 'ANALYZE_BUILD' +CTU_FUNCTION_MAP_FILENAME = 'externalFnMap.txt' +CTU_TEMP_FNMAP_FOLDER = 'tmpExternalFnMaps' + @command_entry_point def scan_build(): @@ -61,7 +66,7 @@ def scan_build(): exit_code, compilations = capture(args) if need_analyzer(args.build): # run the analyzer against the captured commands - run_analyzer_parallel(compilations, args) + run_analyzer_with_ctu(compilations, args) else: # run build command and analyzer with compiler wrappers environment = setup_environment(args) @@ -82,7 +87,7 @@ def analyze_build(): with report_directory(args.output, args.keep_empty) as args.output: # run the analyzer against a compilation db compilations = CompilationDatabase.load(args.cdb) - run_analyzer_parallel(compilations, args) + run_analyzer_with_ctu(compilations, args) # cover report generation and bug counting number_of_bugs = document(args) # set exit status as it was requested @@ -103,6 +108,28 @@ def need_analyzer(args): return len(args) > 0 and not re.search('configure|autogen', args[0]) +def prefix_with(constant, pieces): + # type: (Any, List[Any]) -> List[Any] + """ From a sequence create another sequence where every second element + is from the original sequence and the odd elements are the prefix. + + eg.: prefix_with(0, [1,2,3]) creates [0, 1, 0, 2, 0, 3] """ + + return [elem for piece in pieces for elem in [constant, piece]] + + +def get_ctu_config(args): + """ CTU configuration is created from the chosen phases and dir """ + + return ( + CtuConfig(collect=args.ctu_phases.collect, + analyze=args.ctu_phases.analyze, + dir=args.ctu_dir, + func_map_cmd=args.func_map_cmd) + if hasattr(args, 'ctu_phases') and hasattr(args.ctu_phases, 'dir') + else CtuConfig(collect=False, analyze=False, dir='', func_map_cmd='')) + + def analyze_parameters(args): # type: (argparse.Namespace) -> Dict[str, Any] """ Mapping between the command line parameters and the analyzer run @@ -110,15 +137,6 @@ def analyze_parameters(args): line parameters are in a named tuple. The keys are very similar, and some values are preprocessed. """ - def prefix_with(constant, pieces): - # type: (Any, List[Any]) -> List[Any] - """ From a sequence create another sequence where every second element - is from the original sequence and the odd elements are the prefix. - - eg.: prefix_with(0, [1,2,3]) creates [0, 1, 0, 2, 0, 3] """ - - return [elem for piece in pieces for elem in [constant, piece]] - def direct_args(args): # type: (argparse.Namespace) -> List[str] """ A group of command line arguments can mapped to command @@ -165,10 +183,73 @@ def direct_args(args): 'output_failures': args.output_failures, 'direct_args': direct_args(args), 'force_debug': args.force_debug, - 'excludes': args.excludes + 'excludes': args.excludes, + 'ctu': get_ctu_config(args) } +def create_global_ctu_function_map(func_map_lines): + """ Takes iterator of individual function maps and creates a global map + keeping only unique names. We leave conflicting names out of CTU. + A function map contains the id of a function (mangled name) and the + originating source (the corresponding AST file) name.""" + + mangled_to_asts = {} + + for line in func_map_lines: + mangled_name, ast_file = line.strip().split(' ', 1) + # We collect all occurences of a function name into a list + if mangled_name not in mangled_to_asts: + mangled_to_asts[mangled_name] = {ast_file} + else: + mangled_to_asts[mangled_name].add(ast_file) + + mangled_ast_pairs = [] + + for mangled_name, ast_files in mangled_to_asts.items(): + if len(ast_files) == 1: + mangled_ast_pairs.append((mangled_name, ast_files.pop())) + + return mangled_ast_pairs + + +def merge_ctu_func_maps(ctudir): + """ Merge individual function maps into a global one. + + As the collect phase runs parallel on multiple threads, all compilation + units are separately mapped into a temporary file in CTU_TEMP_FNMAP_FOLDER. + These function maps contain the mangled names of functions and the source + (AST generated from the source) which had them. + These files should be merged at the end into a global map file: + CTU_FUNCTION_MAP_FILENAME.""" + + def generate_func_map_lines(fnmap_dir): + """ Iterate over all lines of input files in random order. """ + + files = glob.glob(os.path.join(fnmap_dir, '*')) + for filename in files: + with open(filename, 'r') as in_file: + for line in in_file: + yield line + + def write_global_map(ctudir, mangled_ast_pairs): + """ Write (mangled function name, ast file) pairs into final file. """ + + extern_fns_map_file = os.path.join(ctudir, CTU_FUNCTION_MAP_FILENAME) + with open(extern_fns_map_file, 'w') as out_file: + for mangled_name, ast_file in mangled_ast_pairs: + out_file.write('%s %s\n' % (mangled_name, ast_file)) + + fnmap_dir = os.path.join(ctudir, CTU_TEMP_FNMAP_FOLDER) + + func_map_lines = generate_func_map_lines(fnmap_dir) + mangled_ast_pairs = create_global_ctu_function_map(func_map_lines) + write_global_map(ctudir, mangled_ast_pairs) + + # Remove all temporary files + shutil.rmtree(fnmap_dir, ignore_errors=True) + + def run_analyzer_parallel(compilations, args): # type: (Iterable[Compilation], argparse.Namespace) -> None """ Runs the analyzer against the given compilations. """ @@ -185,6 +266,32 @@ def run_analyzer_parallel(compilations, args): pool.join() +def run_analyzer_with_ctu(compilations, args): + """ Governs multiple runs in CTU mode or runs once in normal mode. """ + + ctu_config = get_ctu_config(args) + if ctu_config.collect: + shutil.rmtree(ctu_config.dir, ignore_errors=True) + os.makedirs(os.path.join(ctu_config.dir, CTU_TEMP_FNMAP_FOLDER)) + if ctu_config.collect and ctu_config.analyze: + # compilations is a generator but we want to do 2 CTU rounds + compilation_list = list(compilations) + # CTU strings are coming from args.ctu_dir and func_map_cmd, + # so we can leave it empty + args.ctu_phases = CtuConfig(collect=True, analyze=False, + dir='', func_map_cmd='') + run_analyzer_parallel(compilation_list, args) + merge_ctu_func_maps(ctu_config.dir) + args.ctu_phases = CtuConfig(collect=False, analyze=True, + dir='', func_map_cmd='') + run_analyzer_parallel(compilation_list, args) + shutil.rmtree(ctu_config.dir, ignore_errors=True) + else: + run_analyzer_parallel(compilations, args) + if ctu_config.collect: + merge_ctu_func_maps(ctu_config.dir) + + def setup_environment(args): # type: (argparse.Namespace) -> Dict[str, str] """ Set up environment for build command to interpose compiler wrapper. """ @@ -287,9 +394,9 @@ def wrapper(*args, **kwargs): 'excludes', # list of directories 'force_debug', # kill non debug macros 'output_dir', # where generated report files shall go - 'output_format', # it's 'plist', 'html', 'plist-html', - # 'text' or 'plist-multi-file' - 'output_failures']) # generate crash reports or not + 'output_format', # it's 'plist', 'html', both or plist-multi-file + 'output_failures', # generate crash reports or not + 'ctu']) # ctu control options def run(opts): # type: (Dict[str, Any]) -> Dict[str, Any] """ Entry point to run (or not) static analyzer against a single entry @@ -416,8 +523,104 @@ def target(): return result +def func_map_list_src_to_ast(func_src_list, triple_arch): + """ Turns textual function map list with source files into a + function map list with ast files. """ + + func_ast_list = [] + for fn_src_txt in func_src_list: + dpos = fn_src_txt.find(" ") + mangled_name = fn_src_txt[0:dpos] + path = fn_src_txt[dpos + 1:] + # Normalize path on windows as well + path = os.path.splitdrive(path)[1] + # Make relative path out of absolute + path = path[1:] if path[0] == os.sep else path + ast_path = os.path.join("ast", triple_arch, path + ".ast") + func_ast_list.append(mangled_name + "@" + triple_arch + " " + ast_path) + return func_ast_list + + +@require(['clang', 'directory', 'flags', 'direct_args', 'source', 'ctu']) +def ctu_collect_phase(opts): + """ Preprocess source by generating all data needed by CTU analysis. """ + + def generate_ast(triple_arch): + """ Generates ASTs for the current compilation command. """ + + args = opts['direct_args'] + opts['flags'] + ast_joined_path = os.path.join(opts['ctu'].dir, 'ast', triple_arch, + os.path.realpath(opts['source'])[1:] + + '.ast') + ast_path = os.path.abspath(ast_joined_path) + ast_dir = os.path.dirname(ast_path) + if not os.path.isdir(ast_dir): + os.makedirs(ast_dir) + ast_command = [opts['clang'], '-emit-ast'] + ast_command.extend(args) + ast_command.append('-w') + ast_command.append(opts['source']) + ast_command.append('-o') + ast_command.append(ast_path) + logging.debug("Generating AST using '%s'", ast_command) + run_command(ast_command, cwd=opts['directory']) + + def map_functions(triple_arch): + """ Generate function map file for the current source. """ + + args = opts['direct_args'] + opts['flags'] + funcmap_command = [opts['ctu'].func_map_cmd] + funcmap_command.append(opts['source']) + funcmap_command.append('--') + funcmap_command.extend(args) + logging.debug("Generating function map using '%s'", funcmap_command) + func_src_list = run_command(funcmap_command, cwd=opts['directory']) + func_ast_list = func_map_list_src_to_ast(func_src_list, triple_arch) + extern_fns_map_folder = os.path.join(opts['ctu'].dir, + CTU_TEMP_FNMAP_FOLDER) + if func_ast_list: + with tempfile.NamedTemporaryFile(mode='w', + dir=extern_fns_map_folder, + delete=False) as out_file: + out_file.write("\n".join(func_ast_list) + "\n") + + cwd = opts['directory'] + cmd = [opts['clang'], '--analyze'] + opts['direct_args'] + opts['flags'] \ + + [opts['source']] + triple_arch = get_triple_arch(cmd, cwd) + generate_ast(triple_arch) + map_functions(triple_arch) + + +@require(['ctu']) +def dispatch_ctu(opts, continuation=run_analyzer): + """ Execute only one phase of 2 phases of CTU if needed. """ + + ctu_config = opts['ctu'] + # Recover namedtuple from json when coming from analyze_cc + if not hasattr(ctu_config, 'collect'): + ctu_config = CtuConfig(collect=ctu_config[0], + analyze=ctu_config[1], + dir=ctu_config[2], + func_map_cmd=ctu_config[3]) + opts['ctu'] = ctu_config + + if ctu_config.collect or ctu_config.analyze: + assert ctu_config.collect != ctu_config.analyze + if ctu_config.collect: + return ctu_collect_phase(opts) + if ctu_config.analyze: + ctu_options = ['ctu-dir=' + ctu_config.dir, + 'reanalyze-ctu-visited=true'] + analyzer_options = prefix_with('-analyzer-config', ctu_options) + direct_options = prefix_with('-Xanalyzer', analyzer_options) + opts['direct_args'].extend(direct_options) + + return continuation(opts) + + @require(['flags', 'force_debug']) -def filter_debug_flags(opts, continuation=run_analyzer): +def filter_debug_flags(opts, continuation=dispatch_ctu): # type: (...) -> Dict[str, Any] """ Filter out nondebug macros when requested. """ diff --git a/libscanbuild/arguments.py b/libscanbuild/arguments.py index d7d00e9..2ee4779 100644 --- a/libscanbuild/arguments.py +++ b/libscanbuild/arguments.py @@ -20,8 +20,8 @@ import tempfile from typing import Tuple, Dict # noqa: ignore=F401 -from libscanbuild import reconfigure_logging -from libscanbuild.clang import get_checkers +from libscanbuild import reconfigure_logging, CtuConfig +from libscanbuild.clang import get_checkers, is_ctu_capable __all__ = ['parse_args_for_intercept_build', 'parse_args_for_analyze_build', @@ -105,6 +105,11 @@ def normalize_args_for_analyze(args, from_build_command): # add cdb parameter invisibly to make report module working. args.cdb = 'compile_commands.json' + # Make ctu_dir an abspath as it is needed inside clang + if not from_build_command and hasattr(args, 'ctu_phases') \ + and hasattr(args.ctu_phases, 'dir'): + args.ctu_dir = os.path.abspath(args.ctu_dir) + def validate_args_for_analyze(parser, args, from_build_command): # type: (argparse.ArgumentParser, argparse.Namespace, bool) -> None @@ -130,6 +135,18 @@ def validate_args_for_analyze(parser, args, from_build_command): elif not from_build_command and not os.path.exists(args.cdb): parser.error(message='compilation database is missing') + # If the user wants CTU mode + if not from_build_command and hasattr(args, 'ctu_phases') \ + and hasattr(args.ctu_phases, 'dir'): + # If CTU analyze_only, the input directory should exist + if args.ctu_phases.analyze and not args.ctu_phases.collect \ + and not os.path.exists(args.ctu_dir): + parser.error(message='missing CTU directory') + # Check CTU capability via checking clang-func-mapping + if not is_ctu_capable(args.clang, args.func_map_cmd): + parser.error(message="""This version of clang does not support CTU + functionality or clang-func-mapping command not found.""") + def create_intercept_parser(): # type: () -> argparse.ArgumentParser @@ -236,7 +253,6 @@ def create_analyze_parser(from_build_command): action='store_const', help="""Cause the results as a set of .plist files with extra information on related files.""") - # TODO: implement '-view ' advanced = parser.add_argument_group('advanced options') advanced.add_argument( @@ -351,6 +367,50 @@ def create_analyze_parser(from_build_command): if from_build_command: parser.add_argument( dest='build', nargs=argparse.REMAINDER, help="""Command to run.""") + else: + ctu = parser.add_argument_group('cross translation unit analysis') + ctu_mutex_group = ctu.add_mutually_exclusive_group() + ctu_mutex_group.add_argument( + '--ctu', + action='store_const', + const=CtuConfig(collect=True, analyze=True, + dir='', func_map_cmd=''), + dest='ctu_phases', + help="""Perform cross translation unit (ctu) analysis (both collect + and analyze phases) using default for temporary output. + At the end of the analysis, the temporary directory is removed.""") + ctu.add_argument( + '--ctu-dir', + metavar='', + default='ctu-dir', + help="""Defines the temporary directory used between ctu + phases.""") + ctu_mutex_group.add_argument( + '--ctu-collect-only', + action='store_const', + const=CtuConfig(collect=True, analyze=False, + dir='', func_map_cmd=''), + dest='ctu_phases', + help="""Perform only the collect phase of ctu. + Keep for further use.""") + ctu_mutex_group.add_argument( + '--ctu-analyze-only', + action='store_const', + const=CtuConfig(collect=False, analyze=True, + dir='', func_map_cmd=''), + dest='ctu_phases', + help="""Perform only the analyze phase of ctu. should be + present and will not be removed after analysis.""") + ctu.add_argument( + '--use-func-map-cmd', + metavar='', + dest='func_map_cmd', + default='clang-func-mapping', + help="""'%(prog)s' uses the 'clang-func-mapping' executable + relative to itself for generating function maps for static + analysis. One can override this behavior with this option by using + the 'clang-func-mapping' packaged with Xcode (on OS X) or from the + PATH.""") return parser diff --git a/libscanbuild/clang.py b/libscanbuild/clang.py index 2730562..7066596 100644 --- a/libscanbuild/clang.py +++ b/libscanbuild/clang.py @@ -8,13 +8,15 @@ Since Clang command line interface is so rich, but this project is using only a subset of that, it makes sense to create a function specific wrapper. """ +import subprocess import re from typing import List, Set, FrozenSet, Callable # noqa: ignore=F401 from typing import Iterable, Tuple, Dict # noqa: ignore=F401 from libscanbuild import shell_split, run_command -__all__ = ['get_version', 'get_arguments', 'get_checkers'] +__all__ = ['get_version', 'get_arguments', 'get_checkers', 'is_ctu_capable', + 'get_triple_arch'] # regex for activated checker ACTIVE_CHECKER_PATTERN = re.compile(r'^-analyzer-checker=(.*)$') @@ -162,3 +164,29 @@ def get_checkers(clang, plugins): raise Exception('Could not query Clang for available checkers.') return checkers + + +def is_ctu_capable(clang_cmd, func_map_cmd): + """ Detects if the current (or given) clang and function mapping + executables are CTU compatible. """ + + try: + run_command([func_map_cmd, '-version']) + run_command([clang_cmd, '--version']) + except (OSError, subprocess.CalledProcessError): + return False + return True + + +def get_triple_arch(command, cwd): + """Returns the architecture part of the target triple for the given + compilation command. """ + + cmd = get_arguments(command, cwd) + arch = "" + i = 0 + while i < len(cmd) and cmd[i] != "-triple": + i += 1 + if i < (len(cmd) - 1): + arch = cmd[i + 1].split("-")[0] + return arch diff --git a/tests/functional/cases/analyze/analyze_ctu.fts b/tests/functional/cases/analyze/analyze_ctu.fts new file mode 100644 index 0000000..debec13 --- /dev/null +++ b/tests/functional/cases/analyze/analyze_ctu.fts @@ -0,0 +1,101 @@ +#!/usr/bin/env bash + +# RUN: bash %s %T/ctu +# RUN: cd %T/ctu; ./go_with_ctu.sh %{analyze-build} -o . --cdb buildlog.json --ctu --plist-multi-file + +set -o errexit +set -o nounset +set -o xtrace + +# the test creates a subdirectory inside output dir. +# +# ${root_dir} +# ├── buildlog.json +# ├── check.sh +# └── src +# ├── lib.c +# └── main.c + +root_dir=$1 +mkdir -p "${root_dir}/src" + +go_with_ctu="${root_dir}/go_with_ctu.sh" +cat > ${go_with_ctu} << EOF +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o xtrace + +if command -v clang-func-mapping; then + \$* | ./check.sh; +fi +EOF +chmod +x ${go_with_ctu} + +cat > "${root_dir}/src/lib.c" << EOF +int bad_guy(int * i) +{ + *i = 9; + return *i; +} +EOF + +cat > "${root_dir}/src/main.c" << EOF +int bad_guy(int * i); + +void bad_guy_test() +{ + int * ptr = 0; + bad_guy(ptr); +} +EOF + +cat > "${root_dir}/buildlog.json" << EOF +[ + { + "directory": "${root_dir}", + "file": "${root_dir}/src/lib.c", + "command": "cc -c ./src/lib.c -o ./src/lib.o" + }, + { + "directory": "${root_dir}", + "file": "${root_dir}/src/main.c", + "command": "cc -c ./src/main.c -o ./src/main.o" + } +] +EOF + +checker_file="${root_dir}/check.sh" +cat > ${checker_file} << EOF +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o xtrace + +out=\$(sort | uniq) +runs=\$(echo "\$out" | grep "exec command" | sort | uniq) +analyze_out=\$(echo "\$out" | grep "logging_analyzer_output" | sort | uniq) + +assert_present() { + local pattern="\$1"; + local message="\$2"; + + if [ \$(echo "\$runs" | grep -- "\$pattern" | wc -l) -eq 0 ]; then + echo "\$message" && false; + fi +} + +assert_present "ctu-dir=" "using CTU mode" +assert_present "reanalyze-ctu-visited=true" "using CTU reanalyze mode" + +if [ \$(echo "\$analyze_out" | grep -- "Dereference of null pointer" | wc -l) -ne 1 ]; then + echo "cross translation unit problem found" && false; +fi + +if [ \$(find ${root_dir} -type d -name "scan-build-*" | wc -l) -ne 1 ]; then + echo "CTU report was generated" && false; +fi +EOF +chmod +x ${checker_file} diff --git a/tests/unit/test_analyze.py b/tests/unit/test_analyze.py index 4a562f7..232336a 100644 --- a/tests/unit/test_analyze.py +++ b/tests/unit/test_analyze.py @@ -4,13 +4,13 @@ # This file is distributed under the University of Illinois Open Source # License. See LICENSE.TXT for details. -import libear -import libscanbuild.analyze as sut import unittest import os import os.path import glob import platform +import libear +import libscanbuild.analyze as sut IS_WINDOWS = os.getenv('windows') @@ -337,3 +337,83 @@ def test_directory_name_comparison(self): sut.report_directory(tmp_dir, False) as report_dir3: self.assertLess(report_dir1, report_dir2) self.assertLess(report_dir2, report_dir3) + + +class PrefixWithTest(unittest.TestCase): + + def test_gives_empty_on_empty(self): + res = sut.prefix_with(0, []) + self.assertFalse(res) + + def test_interleaves_prefix(self): + res = sut.prefix_with(0, [1, 2, 3]) + self.assertListEqual([0, 1, 0, 2, 0, 3], res) + + +class MergeCtuMapTest(unittest.TestCase): + + def test_no_map_gives_empty(self): + pairs = sut.create_global_ctu_function_map([]) + self.assertFalse(pairs) + + def test_multiple_maps_merged(self): + concat_map = ['_Z1fun1i@x86_64 ast/x86_64/fun1.c.ast', + '_Z1fun2i@x86_64 ast/x86_64/fun2.c.ast', + '_Z1fun3i@x86_64 ast/x86_64/fun3.c.ast'] + pairs = sut.create_global_ctu_function_map(concat_map) + self.assertTrue(('_Z1fun1i@x86_64', 'ast/x86_64/fun1.c.ast') in pairs) + self.assertTrue(('_Z1fun2i@x86_64', 'ast/x86_64/fun2.c.ast') in pairs) + self.assertTrue(('_Z1fun3i@x86_64', 'ast/x86_64/fun3.c.ast') in pairs) + self.assertEqual(3, len(pairs)) + + def test_not_unique_func_left_out(self): + concat_map = ['_Z1fun1i@x86_64 ast/x86_64/fun1.c.ast', + '_Z1fun2i@x86_64 ast/x86_64/fun2.c.ast', + '_Z1fun1i@x86_64 ast/x86_64/fun7.c.ast'] + pairs = sut.create_global_ctu_function_map(concat_map) + self.assertFalse(('_Z1fun1i@x86_64', 'ast/x86_64/fun1.c.ast') in pairs) + self.assertFalse(('_Z1fun1i@x86_64', 'ast/x86_64/fun7.c.ast') in pairs) + self.assertTrue(('_Z1fun2i@x86_64', 'ast/x86_64/fun2.c.ast') in pairs) + self.assertEqual(1, len(pairs)) + + def test_duplicates_are_kept(self): + concat_map = ['_Z1fun1i@x86_64 ast/x86_64/fun1.c.ast', + '_Z1fun2i@x86_64 ast/x86_64/fun2.c.ast', + '_Z1fun1i@x86_64 ast/x86_64/fun1.c.ast'] + pairs = sut.create_global_ctu_function_map(concat_map) + self.assertTrue(('_Z1fun1i@x86_64', 'ast/x86_64/fun1.c.ast') in pairs) + self.assertTrue(('_Z1fun2i@x86_64', 'ast/x86_64/fun2.c.ast') in pairs) + self.assertEqual(2, len(pairs)) + + def test_space_handled_in_source(self): + concat_map = ['_Z1fun1i@x86_64 ast/x86_64/f un.c.ast'] + pairs = sut.create_global_ctu_function_map(concat_map) + self.assertTrue(('_Z1fun1i@x86_64', 'ast/x86_64/f un.c.ast') in pairs) + self.assertEqual(1, len(pairs)) + + +class FuncMapSrcToAstTest(unittest.TestCase): + + def test_empty_gives_empty(self): + fun_ast_lst = sut.func_map_list_src_to_ast([], 'armv7') + self.assertFalse(fun_ast_lst) + + def test_sources_to_asts(self): + fun_src_lst = ['_Z1f1i ' + os.path.join(os.sep + 'path', 'f1.c'), + '_Z1f2i ' + os.path.join(os.sep + 'path', 'f2.c')] + fun_ast_lst = sut.func_map_list_src_to_ast(fun_src_lst, 'armv7') + self.assertTrue('_Z1f1i@armv7 ' + + os.path.join('ast', 'armv7', 'path', 'f1.c.ast') + in fun_ast_lst) + self.assertTrue('_Z1f2i@armv7 ' + + os.path.join('ast', 'armv7', 'path', 'f2.c.ast') + in fun_ast_lst) + self.assertEqual(2, len(fun_ast_lst)) + + def test_spaces_handled(self): + fun_src_lst = ['_Z1f1i ' + os.path.join(os.sep + 'path', 'f 1.c')] + fun_ast_lst = sut.func_map_list_src_to_ast(fun_src_lst, 'armv7') + self.assertTrue('_Z1f1i@armv7 ' + + os.path.join('ast', 'armv7', 'path', 'f 1.c.ast') + in fun_ast_lst) + self.assertEqual(1, len(fun_ast_lst)) diff --git a/tests/unit/test_clang.py b/tests/unit/test_clang.py index d9ec7af..8ad8e99 100644 --- a/tests/unit/test_clang.py +++ b/tests/unit/test_clang.py @@ -90,3 +90,16 @@ def test_parse_checkers(self): self.assertEqual('Checker One description', result.get('checker.one')) self.assertTrue('checker.two' in result) self.assertEqual('Checker Two description', result.get('checker.two')) + + +class ClangIsCtuCapableTest(unittest.TestCase): + def test_ctu_not_found(self): + is_ctu = sut.is_ctu_capable('not-found-clang', + 'not-found-clang-func-mapping') + self.assertFalse(is_ctu) + + +class ClangGetTripleArchTest(unittest.TestCase): + def test_arch_is_not_empty(self): + arch = sut.get_triple_arch(['clang', '-E', '-'], '.') + self.assertTrue(len(arch) > 0)