Index: tools/scan-build-py/README.md =================================================================== --- tools/scan-build-py/README.md +++ tools/scan-build-py/README.md @@ -1,5 +1,4 @@ [![Build Status](https://travis-ci.org/rizsotto/Beye.svg?branch=master)](https://travis-ci.org/rizsotto/Beye) -[![Coverage Status](https://coveralls.io/repos/rizsotto/Beye/badge.svg?branch=master)](https://coveralls.io/r/rizsotto/Beye?branch=master) Build EYE ========= Index: tools/scan-build-py/libear/CMakeLists.txt =================================================================== --- tools/scan-build-py/libear/CMakeLists.txt +++ tools/scan-build-py/libear/CMakeLists.txt @@ -29,10 +29,21 @@ check_function_exists(posix_spawnp HAVE_POSIX_SPAWNP) check_symbol_exists(_NSGetEnviron crt_externs.h HAVE_NSGETENVIRON) +find_package(Threads REQUIRED) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h) include_directories(${CMAKE_CURRENT_BINARY_DIR}) add_library(ear SHARED ear.c) target_link_libraries(ear ${CMAKE_DL_LIBS}) +if(THREADS_HAVE_PTHREAD_ARG) + set_property(TARGET ear PROPERTY COMPILE_OPTIONS "-pthread") + set_property(TARGET ear PROPERTY INTERFACE_COMPILE_OPTIONS "-pthread") +endif() +if(CMAKE_THREAD_LIBS_INIT) + target_link_libraries(ear "${CMAKE_THREAD_LIBS_INIT}") +endif() + +set_target_properties(ear PROPERTIES INSTALL_RPATH "@loader_path/${CMAKE_INSTALL_LIBDIR}") install(TARGETS ear LIBRARY DESTINATION libscanbuild) Index: tools/scan-build-py/libear/__init__.py =================================================================== --- /dev/null +++ tools/scan-build-py/libear/__init__.py @@ -0,0 +1,284 @@ +# -*- coding: utf-8 -*- +# The LLVM Compiler Infrastructure +# +# This file is distributed under the University of Illinois Open Source +# License. See LICENSE.TXT for details. +""" +. """ + +import sys +import os +import os.path +import re +import logging + +__all__ = ['ear_library'] + + +def ear_library(compiler, dst_dir): + """ Returns the full path to the 'libear' library. """ + + try: + src_dir = os.path.dirname(os.path.realpath(__file__)) + with make_context(src_dir) as context: + context.set_compiler(compiler) + context.set_language_standard('c99') + context.add_definitions(['-D_GNU_SOURCE']) + + with Configure(context) as configure: + configure.check_function_exists('execve', 'HAVE_EXECVE') + configure.check_function_exists('execv', 'HAVE_EXECV') + configure.check_function_exists('execvpe', 'HAVE_EXECVPE') + configure.check_function_exists('execvp', 'HAVE_EXECVP') + configure.check_function_exists('execvP', 'HAVE_EXECVP2') + configure.check_function_exists('execl', 'HAVE_EXECL') + configure.check_function_exists('execlp', 'HAVE_EXECLP') + configure.check_function_exists('execle', 'HAVE_EXECLE') + configure.check_function_exists('posix_spawn', + 'HAVE_POSIX_SPAWN') + configure.check_function_exists('posix_spawnp', + 'HAVE_POSIX_SPAWNP') + configure.check_symbol_exists('_NSGetEnviron', 'crt_externs.h', + 'HAVE_NSGETENVIRON') + configure.write_by_template( + os.path.join(src_dir, 'config.h.in'), + os.path.join(dst_dir, 'config.h')) + with SharedLibrary('ear', context) as target: + target.add_include(dst_dir) + target.add_sources('ear.c') + target.link_against(context.dl_libraries()) + target.link_against(['pthread']) + target.build_release(dst_dir) + return os.path.join(dst_dir, target.name) + + except Exception: + logging.info("Could not build interception library.", exc_info=True) + return None + + +def execute(cmd, *args, **kwargs): + """ Make subprocess execution silent. """ + + import subprocess + kwargs.update({'stdout': subprocess.PIPE, 'stderr': subprocess.STDOUT}) + return subprocess.check_call(cmd, *args, **kwargs) + + +class TemporaryDirectory(object): + """ This function creates a temporary directory using mkdtemp() (the + supplied arguments are passed directly to the underlying function). + The resulting object can be used as a context manager. On completion + of the context or destruction of the temporary directory object the + newly created temporary directory and all its contents are removed + from the filesystem. """ + + def __init__(self, **kwargs): + from tempfile import mkdtemp + self.name = mkdtemp(**kwargs) + + def __enter__(self): + return self.name + + def __exit__(self, _type, _value, _traceback): + self.cleanup() + + def cleanup(self): + from shutil import rmtree + if self.name is not None: + rmtree(self.name) + + +class Context: + """ Abstract class to represent different toolset. """ + + def __init__(self, src_dir): + self.src_dir = src_dir + self.compiler = None + self.c_flags = [] + + def __enter__(self): + """ declared to work 'with'. """ + return self + + def __exit__(self, _type, _value, _traceback): + """ declared to work 'with'. """ + pass + + def set_compiler(self, compiler): + """ part of public interface """ + self.compiler = compiler + + def set_language_standard(self, standard): + """ part of public interface """ + self.c_flags.append('-std=' + standard) + + def add_definitions(self, defines): + """ part of public interface """ + self.c_flags.extend(defines) + + def dl_libraries(self): + pass + + def shared_library_name(self, name): + pass + + def shared_library_c_flags(self, release): + extra = ['-DNDEBUG', '-O3'] if release else [] + return extra + ['-fPIC'] + self.c_flags + + def shared_library_ld_flags(self, release, name): + pass + + +class DarwinContext(Context): + def __init__(self, src_dir): + Context.__init__(self, src_dir) + + def dl_libraries(self): + return [] + + def shared_library_name(self, name): + return 'lib' + name + '.dylib' + + def shared_library_ld_flags(self, release, name): + extra = ['-dead_strip'] if release else [] + return extra + ['-dynamiclib', '-install_name', '@rpath/' + name] + + +class UnixContext(Context): + def __init__(self, src_dir): + Context.__init__(self, src_dir) + + def dl_libraries(self): + return [] + + def shared_library_name(self, name): + return 'lib' + name + '.so' + + def shared_library_ld_flags(self, release, name): + extra = [] if release else [] + return extra + ['-shared', '-Wl,-soname,' + name] + + +class LinuxContext(UnixContext): + def __init__(self, src_dir): + UnixContext.__init__(self, src_dir) + + def dl_libraries(self): + return ['dl'] + + +def make_context(src_dir): + platform = sys.platform + if platform in {'win32', 'cygwin'}: + raise RuntimeError('not implemented on this platform') + elif platform == 'darwin': + return DarwinContext(src_dir) + elif platform in {'linux', 'linux2'}: + return LinuxContext(src_dir) + else: + return UnixContext(src_dir) + + +class Configure: + def __init__(self, context): + self.ctx = context + self.results = {'APPLE': sys.platform == 'darwin'} + + def __enter__(self): + return self + + def __exit__(self, _type, _value, _traceback): + pass + + def _try_to_compile_and_link(self, source): + try: + with TemporaryDirectory() as work_dir: + src_file = 'check.c' + with open(os.path.join(work_dir, src_file), 'w') as handle: + handle.write(source) + + execute([self.ctx.compiler, src_file] + self.ctx.c_flags, + cwd=work_dir) + return True + except Exception: + return False + + def check_function_exists(self, function, name): + template = "int FUNCTION(); int main() { return FUNCTION(); }" + source = template.replace("FUNCTION", function) + + logging.debug('Checking function %s', function) + found = self._try_to_compile_and_link(source) + logging.debug('Checking function %s -- %s', function, + 'found' if found else 'not found') + self.results.update({name: found}) + + def check_symbol_exists(self, symbol, include, name): + template = """#include + int main() { return ((int*)(&SYMBOL))[0]; }""" + source = template.replace('INCLUDE', include).replace("SYMBOL", symbol) + + logging.debug('Checking symbol %s', symbol) + found = self._try_to_compile_and_link(source) + logging.debug('Checking symbol %s -- %s', symbol, + 'found' if found else 'not found') + self.results.update({name: found}) + + def write_by_template(self, template, output): + def transform(line, definitions): + + pattern = re.compile(r'^#cmakedefine\s+(\S+)') + m = pattern.match(line) + if m: + key = m.group(1) + if key not in definitions or not definitions[key]: + return '/* #undef {} */\n'.format(key) + else: + return '#define {}\n'.format(key) + return line + + with open(template, 'r') as src_handle: + logging.debug('Writing config to %s', output) + with open(output, 'w') as dst_handle: + for line in src_handle: + dst_handle.write(transform(line, self.results)) + + +class SharedLibrary: + def __init__(self, name, context): + self.name = context.shared_library_name(name) + self.ctx = context + self.inc = [] + self.src = [] + self.lib = [] + + def __enter__(self): + return self + + def __exit__(self, _type, _value, _traceback): + pass + + def add_include(self, directory): + self.inc.extend(['-I', directory]) + + def add_sources(self, source): + self.src.append(source) + + def link_against(self, libraries): + self.lib.extend(['-l' + lib for lib in libraries]) + + def build_release(self, directory): + for src in self.src: + logging.debug('Compiling %s', src) + execute( + [self.ctx.compiler, '-c', os.path.join(self.ctx.src_dir, src), + '-o', src + '.o'] + self.inc + + self.ctx.shared_library_c_flags(True), + cwd=directory) + logging.debug('Linking %s', self.name) + execute( + [self.ctx.compiler] + [src + '.o' for src in self.src] + + ['-o', self.name] + self.lib + + self.ctx.shared_library_ld_flags(True, self.name), + cwd=directory) Index: tools/scan-build-py/libear/config.h.in =================================================================== --- tools/scan-build-py/libear/config.h.in +++ tools/scan-build-py/libear/config.h.in @@ -20,14 +20,3 @@ #cmakedefine HAVE_NSGETENVIRON #cmakedefine APPLE - -#define ENV_OUTPUT "BUILD_INTERCEPT_TARGET_DIR" - -#ifdef APPLE -# define ENV_FLAT "DYLD_FORCE_FLAT_NAMESPACE" -# define ENV_PRELOAD "DYLD_INSERT_LIBRARIES" -# define ENV_SIZE 3 -#else -# define ENV_PRELOAD "LD_PRELOAD" -# define ENV_SIZE 2 -#endif Index: tools/scan-build-py/libear/ear.c =================================================================== --- tools/scan-build-py/libear/ear.c +++ tools/scan-build-py/libear/ear.c @@ -28,6 +28,7 @@ #include #include #include +#include #if defined HAVE_POSIX_SPAWN || defined HAVE_POSIX_SPAWNP #include @@ -40,6 +41,27 @@ extern char **environ; #endif +#define ENV_OUTPUT "BUILD_INTERCEPT_TARGET_DIR" +#ifdef APPLE +# define ENV_FLAT "DYLD_FORCE_FLAT_NAMESPACE" +# define ENV_PRELOAD "DYLD_INSERT_LIBRARIES" +# define ENV_SIZE 3 +#else +# define ENV_PRELOAD "LD_PRELOAD" +# define ENV_SIZE 2 +#endif + +#define DLSYM(TYPE_, VAR_, SYMBOL_) \ + union { \ + void *from; \ + TYPE_ to; \ + } cast; \ + if (0 == (cast.from = dlsym(RTLD_NEXT, SYMBOL_))) { \ + perror("bear: dlsym"); \ + exit(EXIT_FAILURE); \ + } \ + TYPE_ const VAR_ = cast.to; + typedef char const * bear_env_t[ENV_SIZE]; @@ -72,23 +94,12 @@ }; static int initialized = 0; +static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; static void on_load(void) __attribute__((constructor)); static void on_unload(void) __attribute__((destructor)); -#define DLSYM(TYPE_, VAR_, SYMBOL_) \ - union { \ - void *from; \ - TYPE_ to; \ - } cast; \ - if (0 == (cast.from = dlsym(RTLD_NEXT, SYMBOL_))) { \ - perror("bear: dlsym"); \ - exit(EXIT_FAILURE); \ - } \ - TYPE_ const VAR_ = cast.to; - - #ifdef HAVE_EXECVE static int call_execve(const char *path, char *const argv[], char *const envp[]); @@ -124,16 +135,20 @@ */ static void on_load(void) { + pthread_mutex_lock(&mutex); #ifdef HAVE_NSGETENVIRON environ = *_NSGetEnviron(); #endif if (!initialized) initialized = bear_capture_env_t(&initial_env); + pthread_mutex_unlock(&mutex); } static void on_unload(void) { + pthread_mutex_lock(&mutex); bear_release_env_t(&initial_env); initialized = 0; + pthread_mutex_unlock(&mutex); } @@ -373,6 +388,7 @@ if (!initialized) return; + pthread_mutex_lock(&mutex); const char *cwd = getcwd(NULL, 0); if (0 == cwd) { perror("bear: getcwd"); @@ -404,6 +420,7 @@ exit(EXIT_FAILURE); } free((void *)cwd); + pthread_mutex_unlock(&mutex); } /* update environment assure that chilren processes will copy the desired Index: tools/scan-build-py/libscanbuild/clang.py =================================================================== --- tools/scan-build-py/libscanbuild/clang.py +++ tools/scan-build-py/libscanbuild/clang.py @@ -11,7 +11,7 @@ import subprocess import logging import re -import shlex +from libscanbuild.shell import decode __all__ = ['get_version', 'get_arguments', 'get_checkers'] @@ -23,7 +23,7 @@ return lines.decode('ascii').splitlines()[0] -def get_arguments(cwd, command): +def get_arguments(command, cwd): """ Capture Clang invocation. Clang can be executed directly (when you just ask specific action to @@ -41,10 +41,6 @@ raise Exception("output not found") return last - def strip_quotes(quoted): - match = re.match(r'^\"([^\"]*)\"$', quoted) - return match.group(1) if match else quoted - cmd = command[:] cmd.insert(1, '-###') logging.debug('exec command in %s: %s', cwd, ' '.join(cmd)) @@ -57,9 +53,9 @@ child.stdout.close() child.wait() if 0 == child.returncode: - if re.match(r'^clang: error:', line): + if re.search(r'clang(.*): error:', line): raise Exception(line) - return [strip_quotes(x) for x in shlex.split(line)] + return decode(line) else: raise Exception(line) @@ -77,7 +73,7 @@ '-Xclang', plugin]] cmd = [clang, '--analyze'] + load + ['-x', language, '-'] pattern = re.compile(r'^-analyzer-checker=(.*)$') - return [pattern.match(arg).group(1) for arg in get_arguments('.', cmd) + return [pattern.match(arg).group(1) for arg in get_arguments(cmd, '.') if pattern.match(arg)] result = set() Index: tools/scan-build-py/libscanbuild/command.py =================================================================== --- tools/scan-build-py/libscanbuild/command.py +++ tools/scan-build-py/libscanbuild/command.py @@ -6,264 +6,121 @@ """ This module is responsible for to parse a compiler invocation. """ import re +import os -__all__ = ['Action', 'classify_parameters'] +__all__ = ['Action', 'classify_parameters', 'classify_source'] class Action(object): """ Enumeration class for compiler action. """ - Link, Compile, Preprocess, Info, Internal = range(5) + Link, Compile, Ignored = range(3) def classify_parameters(command): - """ Parses the command line arguments of the given invocation. - - To run analysis from a compilation command, first it disassembles the - compilation command. Classifies the parameters into groups and throws - away those which are not relevant. """ - - def match(state, iterator): - """ This method contains a list of pattern and action tuples. - The matching start from the top if the list, when the first match - happens the action is executed. """ - - def regex(pattern, action): - """ Matching expression for regex. """ - - def evaluate(iterator): - match = evaluate.regexp.match(iterator.current()) - if match: - action(state, iterator, match) - return True - - evaluate.regexp = re.compile(pattern) - return evaluate - - def anyof(opts, action): - """ Matching expression for string literals. """ - - def evaluate(iterator): - if iterator.current() in opts: - action(state, iterator, None) - return True - - return evaluate - - tasks = [ - # actions - regex(r'^-(E|MM?)$', take_action(Action.Preprocess)), - anyof({'-c'}, take_action(Action.Compile)), - anyof({'-print-prog-name'}, take_action(Action.Info)), - anyof({'-cc1'}, take_action(Action.Internal)), - # architectures - anyof({'-arch'}, take_two('archs_seen')), - # module names - anyof({'-filelist'}, take_from_file('files')), - regex(r'^[^-].+', take_one('files')), - # language - anyof({'-x'}, take_second('language')), - # output - anyof({'-o'}, take_second('output')), - # relevant compiler flags - anyof({'-write-strings', '-v'}, take_one('compile_options')), - anyof({'-ftrapv-handler', '--sysroot', '-target'}, - take_two('compile_options')), - regex(r'^-isysroot', take_two('compile_options')), - regex(r'^-m(32|64)$', take_one('compile_options')), - regex(r'^-mios-simulator-version-min(.*)', - take_joined('compile_options')), - regex(r'^-stdlib(.*)', take_joined('compile_options')), - regex(r'^-mmacosx-version-min(.*)', - take_joined('compile_options')), - regex(r'^-miphoneos-version-min(.*)', - take_joined('compile_options')), - regex(r'^-O[1-3]$', take_one('compile_options')), - anyof({'-O'}, take_as('-O1', 'compile_options')), - anyof({'-Os'}, take_as('-O2', 'compile_options')), - regex(r'^-[DIU](.*)$', take_joined('compile_options')), - regex(r'^-isystem(.*)$', take_joined('compile_options')), - anyof({'-nostdinc'}, take_one('compile_options')), - regex(r'^-std=', take_one('compile_options')), - regex(r'^-include', take_two('compile_options')), - anyof({ - '-idirafter', '-imacros', '-iprefix', '-iwithprefix', - '-iwithprefixbefore' - }, take_two('compile_options')), - regex(r'^-m.*', take_one('compile_options')), - regex(r'^-iquote(.*)', take_joined('compile_options')), - regex(r'^-Wno-', take_one('compile_options')), - # ignored flags - regex(r'^-framework$', take_two()), - regex(r'^-fobjc-link-runtime(.*)', take_joined()), - regex(r'^-[lL]', take_one()), - regex(r'^-M[TF]$', take_two()), - regex(r'^-[eu]$', take_two()), - anyof({'-fsyntax-only', '-save-temps'}, take_one()), - anyof({ - '-install_name', '-exported_symbols_list', '-current_version', - '-compatibility_version', '-init', '-seg1addr', - '-bundle_loader', '-multiply_defined', '--param', - '--serialize-diagnostics' - }, take_two()), - anyof({'-sectorder'}, take_four()), - # relevant compiler flags - regex(r'^-[fF](.+)$', take_one('compile_options')) - ] - for task in tasks: - if task(iterator): - return - - state = {'action': Action.Link, 'cxx': is_cplusplus_compiler(command[0])} - - arguments = Arguments(command) - for _ in arguments: - match(state, arguments) - return state - - -class Arguments(object): - """ An iterator wraper around compiler arguments. - - Python iterators are only implement the 'next' method, but this one - implements the 'current' query method as well. """ - - def __init__(self, args): - """ Takes full command line, but iterates on the parameters only. """ - - self.__sequence = [arg for arg in args[1:] if arg != ''] - self.__size = len(self.__sequence) - self.__current = -1 - - def __iter__(self): - """ Needed for python iterator. """ - - return self - - def __next__(self): - """ Needed for python iterator. (version 3.x) """ - - return self.next() - - def next(self): - """ Needed for python iterator. (version 2.x) """ - - self.__current += 1 - return self.current() - - def current(self): - """ Extra method to query the current element. """ - - if self.__current >= self.__size: - raise StopIteration + """ Parses the command line arguments of the given invocation. """ + + ignored = { + '-g': 0, + '-fsyntax-only': 0, + '-save-temps': 0, + '-install_name': 1, + '-exported_symbols_list': 1, + '-current_version': 1, + '-compatibility_version': 1, + '-init': 1, + '-e': 1, + '-seg1addr': 1, + '-bundle_loader': 1, + '-multiply_defined': 1, + '-sectorder': 3, + '--param': 1, + '--serialize-diagnostics': 1 + } + + state = { + 'action': Action.Link, + 'files': [], + 'output': None, + 'compile_options': [], + 'c++': cplusplus_compiler(command[0]) + } + + args = iter(command[1:]) + for arg in args: + # compiler action parameters are the most important ones... + if arg in {'-E', '-S', '-cc1', '-M', '-MM', '-###'}: + state.update({'action': Action.Ignored}) + elif arg == '-c': + state.update({'action': max(state['action'], Action.Compile)}) + # arch flags are taken... + elif arg == '-arch': + archs = state.get('archs_seen', []) + state.update({'archs_seen': archs + [next(args)]}) + # explicit language option taken... + elif arg == '-x': + state.update({'language': next(args)}) + # output flag taken... + elif arg == '-o': + state.update({'output': next(args)}) + # warning disable options are taken... + elif re.match(r'^-Wno-', arg): + state['compile_options'].append(arg) + # warning options are ignored... + elif re.match(r'^-[mW].+', arg): + pass + # some preprocessor parameters are ignored... + elif arg in {'-MD', '-MMD', '-MG', '-MP'}: + pass + elif arg in {'-MF', '-MT', '-MQ'}: + next(args) + # linker options are ignored... + elif arg in {'-static', '-shared', '-s', '-rdynamic'} or \ + re.match(r'^-[lL].+', arg): + pass + elif arg in {'-l', '-L', '-u', '-z', '-T', '-Xlinker'}: + next(args) + # some other options are ignored... + elif arg in ignored.keys(): + for _ in range(ignored[arg]): + next(args) + # parameters which looks source file are taken... + elif re.match(r'^[^-].+', arg) and classify_source(arg): + state['files'].append(arg) + # and consider everything else as compile option. else: - return self.__sequence[self.__current] - - -def take_n(count=1, *keys): - """ Take N number of arguments and append it to the refered values. """ - - def take(values, iterator, _match): - updates = [] - updates.append(iterator.current()) - for _ in range(count - 1): - updates.append(iterator.next()) - for key in keys: - current = values.get(key, []) - values.update({key: current + updates}) - - return take - - -def take_one(*keys): - """ Take one argument and append to the 'key' values. """ + state['compile_options'].append(arg) - return take_n(1, *keys) - - -def take_two(*keys): - """ Take two arguments and append to the 'key' values. """ - - return take_n(2, *keys) - - -def take_four(*keys): - """ Take four arguments and append to the 'key' values. """ - - return take_n(4, *keys) - - -def take_joined(*keys): - """ Take one or two arguments and append to the 'key' values. - - eg.: '-Isomething' shall take only one. - '-I something' shall take two. - - This action should go with regex matcher only. """ - - def take(values, iterator, match): - updates = [] - updates.append(iterator.current()) - if not match.group(1): - updates.append(iterator.next()) - for key in keys: - current = values.get(key, []) - values.update({key: current + updates}) - - return take - - -def take_from_file(*keys): - """ Take values from the refered file and append to the 'key' values. - - The refered file is the second argument. (So it consume two args.) """ - - def take(values, iterator, _match): - with open(iterator.next()) as handle: - current = [line.strip() for line in handle.readlines()] - for key in keys: - values[key] = current - - return take - - -def take_as(value, *keys): - """ Take one argument and append to the 'key' values. - - But instead of taking the argument, it takes the value as it was given. """ - - def take(values, _iterator, _match): - updates = [value] - for key in keys: - current = values.get(key, []) - values.update({key: current + updates}) - - return take - - -def take_second(*keys): - """ Take the second argument and append to the 'key' values. """ - - def take(values, iterator, _match): - current = iterator.next() - for key in keys: - values[key] = current - - return take - - -def take_action(action): - """ Take the action value and overwrite current value if that's bigger. """ - - def take(values, _iterator, _match): - key = 'action' - current = values[key] - values[key] = max(current, action) - - return take + return state -def is_cplusplus_compiler(name): +def classify_source(filename, cplusplus=False): + """ Return the language from fille name extension. """ + + mapping = { + '.c': 'c++' if cplusplus else 'c', + '.i': 'c++-cpp-output' if cplusplus else 'c-cpp-output', + '.ii': 'c++-cpp-output', + '.m': 'objective-c', + '.mi': 'objective-c-cpp-output', + '.mm': 'objective-c++', + '.mii': 'objective-c++-cpp-output', + '.C': 'c++', + '.cc': 'c++', + '.CC': 'c++', + '.cp': 'c++', + '.cpp': 'c++', + '.cxx': 'c++', + '.c++': 'c++', + '.C++': 'c++', + '.txx': 'c++' + } + + __, extension = os.path.splitext(os.path.basename(filename)) + return mapping.get(extension) + + +def cplusplus_compiler(name): """ Returns true when the compiler name refer to a C++ compiler. """ match = re.match(r'^([^/]*/)*(\w*-)*(\w+\+\+)(-(\d+(\.\d+){0,3}))?$', name) Index: tools/scan-build-py/libscanbuild/driver.py =================================================================== --- tools/scan-build-py/libscanbuild/driver.py +++ tools/scan-build-py/libscanbuild/driver.py @@ -20,7 +20,6 @@ import json import tempfile import multiprocessing -from libscanbuild import tempdir from libscanbuild.runner import run from libscanbuild.intercept import capture from libscanbuild.options import create_parser @@ -50,14 +49,17 @@ # next step to run the analyzer against the captured commands with ReportDirectory(args.output, args.keep_empty) as target_dir: - run_analyzer(args, target_dir.name) - # cover report generation and bug counting - number_of_bugs = document(args, target_dir.name, True) - # remove the compilation database when it was not requested - if args.action == 'all' and os.path.exists(args.cdb): - os.unlink(args.cdb) - # set exit status as it was requested - return number_of_bugs if args.status_bugs else exit_code + if args.action == 'analyze' or need_analyzer(args.build): + run_analyzer(args, target_dir.name) + # cover report generation and bug counting + number_of_bugs = document(args, target_dir.name, True) + # remove the compilation database when it was not requested + if args.action == 'all' and os.path.exists(args.cdb): + os.unlink(args.cdb) + # set exit status as it was requested + return number_of_bugs if args.status_bugs else exit_code + else: + return exit_code except KeyboardInterrupt: return 1 except Exception: @@ -103,6 +105,12 @@ parser.error('missing build command') +def need_analyzer(args): + """ Check the internt of the build command. """ + + return len(args) and not re.search('configure|autogen', args[0]) + + def run_analyzer(args, output_dir): """ Runs the analyzer against the given compilation database. """ @@ -164,15 +172,16 @@ result.append('-analyzer-output={0}'.format(args.output_format)) if args.analyzer_config: result.append(args.analyzer_config) - if 2 <= args.verbose: + if 4 <= args.verbose: result.append('-analyzer-display-progress') if args.plugins: result.extend(prefix_with('-load', args.plugins)) if args.enable_checker: - result.extend(prefix_with('-analyzer-checker', args.enable_checker)) + checkers = ','.join(args.enable_checker) + result.extend(['-analyzer-checker', checkers]) if args.disable_checker: - result.extend( - prefix_with('-analyzer-disable-checker', args.disable_checker)) + checkers = ','.join(args.disable_checker) + result.extend(['-analyzer-disable-checker', checkers]) if os.getenv('UBIVIZ'): result.append('-analyzer-viz-egraph-ubigraph') @@ -215,6 +224,7 @@ def __init__(self, hint, keep): self.name = ReportDirectory._create(hint) self.keep = keep + logging.info('Report directory created: %s', self.name) def __enter__(self): return self @@ -235,12 +245,5 @@ @staticmethod def _create(hint): - if tempdir() != hint: - try: - os.mkdir(hint) - return hint - except OSError: - raise - else: - stamp = time.strftime('%Y-%m-%d-%H%M%S', time.localtime()) - return tempfile.mkdtemp(prefix='scan-build-{0}-'.format(stamp)) + stamp = time.strftime('scan-build-%Y-%m-%d-%H%M%S-', time.localtime()) + return tempfile.mkdtemp(prefix=stamp, dir=hint) Index: tools/scan-build-py/libscanbuild/intercept.py =================================================================== --- tools/scan-build-py/libscanbuild/intercept.py +++ tools/scan-build-py/libscanbuild/intercept.py @@ -26,10 +26,12 @@ import os import os.path import re -import shlex +import glob import itertools +from libear import ear_library from libscanbuild import duplicate_check, tempdir from libscanbuild.command import Action, classify_parameters +from libscanbuild.shell import encode, decode __all__ = ['capture', 'wrapper'] @@ -48,7 +50,7 @@ current = itertools.chain.from_iterable( # creates a sequence of entry generators from an exec, # but filter out non compiler calls before. - (format_entry(x) for x in commands if is_compiler_call(x))) + (format_entry(x) for x in commands if compiler_call(x))) # read entries from previous run if 'append' in args and args.append and os.path.exists(args.cdb): with open(args.cdb) as handle: @@ -57,7 +59,8 @@ previous = iter([]) # filter out duplicate entries from both duplicate = duplicate_check(entry_hash) - return (entry for entry in itertools.chain(previous, current) + return (entry + for entry in itertools.chain(previous, current) if os.path.exists(entry['file']) and not duplicate(entry)) return commands @@ -66,10 +69,11 @@ environment = setup_environment(args, tmpdir, wrappers_dir) logging.debug('run build in environment: %s', environment) exit_code = subprocess.call(args.build, env=environment) - logging.debug('build finished with exit code: %d', exit_code) + logging.info('build finished with exit code: %d', exit_code) # read the intercepted exec calls - commands = (parse_exec_trace(os.path.join(tmpdir, filename)) - for filename in sorted(os.listdir(tmpdir))) + commands = ( + parse_exec_trace(os.path.join(tmpdir, filename)) + for filename in sorted(glob.iglob(os.path.join(tmpdir, '*.cmd')))) # do post processing entries = post_processing(itertools.chain.from_iterable(commands)) # dump the compilation database @@ -85,10 +89,13 @@ The exec calls will be logged by the 'libear' preloaded library or by the 'wrapper' programs. """ + compiler = args.cc if 'cc' in args.__dict__ else 'cc' + ear_library_path = ear_library(compiler, destination) + environment = dict(os.environ) environment.update({'BUILD_INTERCEPT_TARGET_DIR': destination}) - if sys.platform in {'win32', 'cygwin'} or not ear_library_path(False): + if args.override_compiler or not ear_library_path: environment.update({ 'CC': os.path.join(wrappers_dir, 'intercept-cc'), 'CXX': os.path.join(wrappers_dir, 'intercept-cxx'), @@ -98,11 +105,11 @@ }) elif 'darwin' == sys.platform: environment.update({ - 'DYLD_INSERT_LIBRARIES': ear_library_path(True), + 'DYLD_INSERT_LIBRARIES': ear_library_path, 'DYLD_FORCE_FLAT_NAMESPACE': '1' }) else: - environment.update({'LD_PRELOAD': ear_library_path(False)}) + environment.update({'LD_PRELOAD': ear_library_path}) return environment @@ -152,6 +159,7 @@ generated by the interception library or wrapper command. A single report file _might_ contain multiple process creation info. """ + logging.debug('parse exec trace file: %s', filename) with open(filename, 'r') as handler: content = handler.read() for group in filter(bool, content.split(GS)): @@ -168,54 +176,34 @@ def format_entry(entry): """ Generate the desired fields for compilation database entries. """ - def join_command(args): - return ' '.join([shell_escape(arg) for arg in args]) - def abspath(cwd, name): """ Create normalized absolute path from input filename. """ fullname = name if os.path.isabs(name) else os.path.join(cwd, name) return os.path.normpath(fullname) + logging.debug('format this command: %s', entry['command']) atoms = classify_parameters(entry['command']) - return ({ - 'directory': entry['directory'], - 'command': join_command(entry['command']), - 'file': abspath(entry['directory'], filename) - } for filename in atoms.get('files', []) - if is_source_file(filename) and atoms['action'] <= Action.Compile) - - -def shell_escape(arg): - """ Create a single string from list. - - The major challenge, to deal with white spaces. Which are used by - the shell as separator. (Eg.: -D_KEY="Value with spaces") """ - - def quote(arg): - table = {'\\': '\\\\', '"': '\\"', "'": "\\'"} - return '"' + ''.join([table.get(c, c) for c in arg]) + '"' - - return quote(arg) if len(shlex.split(arg)) > 1 else arg - - -def is_source_file(filename): - """ A predicate to decide the filename is a source file or not. """ - - accepted = { - '.c', '.C', '.cc', '.CC', '.cxx', '.cp', '.cpp', '.c++', '.m', '.mm', - '.i', '.ii', '.mii' - } - _, ext = os.path.splitext(filename) - return ext in accepted + if atoms['action'] <= Action.Compile: + for source in atoms['files']: + compiler = 'c++' if atoms['c++'] else 'cc' + output = ['-o', atoms['output']] if atoms.get('output') else [] + command = [compiler, '-c' + ] + atoms['compile_options'] + output + [source] + logging.debug('formated as: %s', command) + yield { + 'directory': entry['directory'], + 'command': encode(command), + 'file': abspath(entry['directory'], source) + } -def is_compiler_call(entry): +def compiler_call(entry): """ A predicate to decide the entry is a compiler call or not. """ patterns = [ re.compile(r'^([^/]*/)*intercept-c(c|\+\+)$'), re.compile(r'^([^/]*/)*c(c|\+\+)$'), - re.compile(r'^([^/]*/)*([^-]*-)*g(cc|\+\+)(-\d+(\.\d+){0,2})?$'), + re.compile(r'^([^/]*/)*([^-]*-)*[mg](cc|\+\+)(-\d+(\.\d+){0,2})?$'), re.compile(r'^([^/]*/)*([^-]*-)*clang(\+\+)?(-\d+(\.\d+){0,2})?$'), re.compile(r'^([^/]*/)*llvm-g(cc|\+\+)$'), ] @@ -234,23 +222,11 @@ # 'clang' therefore both call would be logged. To avoid # this the hash does not contain the first word of the # command. - command = ' '.join(shlex.split(entry['command'])[1:]) + command = ' '.join(decode(entry['command'])[1:]) return '<>'.join([filename, directory, command]) -def ear_library_path(darwin): - """ Returns the full path to the 'libear' library. """ - - try: - import pkg_resources - lib_name = 'libear.dylib' if darwin else 'libear.so' - lib_file = pkg_resources.resource_filename('libscanbuild', lib_name) - return lib_file if os.path.exists(lib_file) else None - except ImportError: - return None - - if sys.version_info.major >= 3 and sys.version_info.minor >= 2: from tempfile import TemporaryDirectory else: Index: tools/scan-build-py/libscanbuild/interposition.py =================================================================== --- tools/scan-build-py/libscanbuild/interposition.py +++ tools/scan-build-py/libscanbuild/interposition.py @@ -13,12 +13,11 @@ build_command) from libscanbuild.driver import (initialize_logging, ReportDirectory, analyzer_params, print_checkers, - print_active_checkers) + print_active_checkers, need_analyzer) from libscanbuild.report import document from libscanbuild.clang import get_checkers from libscanbuild.runner import action_check -from libscanbuild.intercept import is_source_file -from libscanbuild.command import classify_parameters +from libscanbuild.command import classify_parameters, classify_source __all__ = ['main', 'wrapper'] @@ -82,7 +81,7 @@ 'CXX': os.path.join(wrapper_dir, 'analyze-cxx'), 'BUILD_ANALYZE_CC': args.cc, 'BUILD_ANALYZE_CXX': args.cxx, - 'BUILD_ANALYZE_CLANG': args.clang, + 'BUILD_ANALYZE_CLANG': args.clang if need_analyzer(args.build) else '', 'BUILD_ANALYZE_VERBOSE': 'DEBUG' if args.verbose > 2 else 'WARNING', 'BUILD_ANALYZE_REPORT_DIR': destination, 'BUILD_ANALYZE_REPORT_FORMAT': args.output_format, @@ -104,6 +103,10 @@ compilation = [compiler] + sys.argv[1:] logging.info('execute compiler: %s', compilation) result = subprocess.call(compilation) + # exit when it fails, ... + if result or not os.getenv('BUILD_ANALYZE_CLANG'): + return result + # ... and run the analyzer if all went well. try: # collect the needed parameters from environment, crash when missing consts = { @@ -118,7 +121,7 @@ # get relevant parameters from command line arguments args = classify_parameters(sys.argv) filenames = args.pop('files', []) - for filename in (name for name in filenames if is_source_file(name)): + for filename in (name for name in filenames if classify_source(name)): parameters = dict(args, file=filename, **consts) logging.debug('analyzer parameters %s', parameters) current = action_check(parameters) @@ -128,5 +131,5 @@ logging.info(line.rstrip()) except Exception: logging.exception("run analyzer inside compiler wrapper failed.") - # return compiler exit code - return result + finally: + return 0 Index: tools/scan-build-py/libscanbuild/options.py =================================================================== --- tools/scan-build-py/libscanbuild/options.py +++ tools/scan-build-py/libscanbuild/options.py @@ -61,6 +61,11 @@ default=0, help="""Enable verbose output from '%(prog)s'. A second and third '-v' increases verbosity.""") + parser.add_argument( + '--override-compiler', + action='store_true', + help="""Always resort to the compiler wrapper even when better + interposition methods are available.""") if add_cdb: parser.add_argument('--cdb', metavar='', @@ -153,14 +158,12 @@ '--maxloop', '-maxloop', metavar='', type=int, - default=4, help="""Specifiy the number of times a block can be visited before giving up. Increase for more comprehensive coverage at a cost of speed.""") advanced.add_argument('--store', '-store', metavar='', dest='store_model', - default='region', choices=['region', 'basic'], help="""Specify the store model used by the analyzer. 'region' specifies a field- sensitive store model. @@ -171,7 +174,6 @@ '--constraints', '-constraints', metavar='', dest='constraints_model', - default='range', choices=['range', 'basic'], help="""Specify the contraint engine used by the analyzer. Specifying 'basic' uses a simpler, less powerful constraint model used by @@ -241,11 +243,11 @@ help="""Loading external checkers using the clang plugin interface.""") plugins.add_argument('--enable-checker', '-enable-checker', metavar='', - action='append', + action=AppendCommaSeparated, help="""Enable specific checker.""") plugins.add_argument('--disable-checker', '-disable-checker', metavar='', - action='append', + action=AppendCommaSeparated, help="""Disable specific checker.""") plugins.add_argument( '--help-checkers', @@ -258,3 +260,16 @@ '--help-checkers-verbose', action='store_true', help="""Print all available checkers and mark the enabled ones.""") + + +class AppendCommaSeparated(argparse.Action): + """ argparse Action class to support multiple comma separated lists. """ + + def __call__(self, __parser, namespace, values, __option_string): + # getattr(obj, attr, default) does not really returns default but none + if getattr(namespace, self.dest, None) is None: + setattr(namespace, self.dest, []) + # once it's fixed we can use as expected + actual = getattr(namespace, self.dest) + actual.extend(values.split(',')) + setattr(namespace, self.dest, actual) Index: tools/scan-build-py/libscanbuild/report.py =================================================================== --- tools/scan-build-py/libscanbuild/report.py +++ tools/scan-build-py/libscanbuild/report.py @@ -420,17 +420,9 @@ def copy_resource_files(output_dir): """ Copy the javascript and css files to the report directory. """ - try: - import pkg_resources - package = 'libscanbuild' - resources_dir = pkg_resources.resource_filename(package, 'resources') - for resource in pkg_resources.resource_listdir(package, 'resources'): - shutil.copy(os.path.join(resources_dir, resource), output_dir) - except ImportError: - resources_dir = os.path.join( - os.path.dirname(os.path.realpath(__file__)), 'resources') - for resource in os.listdir(resources_dir): - shutil.copy(os.path.join(resources_dir, resource), output_dir) + this_dir = os.path.dirname(os.path.realpath(__file__)) + for resource in os.listdir(os.path.join(this_dir, 'resources')): + shutil.copy(os.path.join(this_dir, 'resources', resource), output_dir) def encode_value(container, key, encode): Index: tools/scan-build-py/libscanbuild/runner.py =================================================================== --- tools/scan-build-py/libscanbuild/runner.py +++ tools/scan-build-py/libscanbuild/runner.py @@ -9,11 +9,11 @@ import logging import os import os.path -import shlex import tempfile import functools -from libscanbuild.command import classify_parameters, Action +from libscanbuild.command import classify_parameters, Action, classify_source from libscanbuild.clang import get_arguments, get_version +from libscanbuild.shell import decode __all__ = ['run'] @@ -29,7 +29,7 @@ try: command = opts.pop('command') logging.debug("Run analyzer against '%s'", command) - opts.update(classify_parameters(shlex.split(command))) + opts.update(classify_parameters(decode(command))) return action_check(opts) except Exception: @@ -87,7 +87,7 @@ dir=destination(opts)) os.close(handle) cwd = opts['directory'] - cmd = get_arguments(cwd, [opts['clang']] + opts['report'] + ['-o', name]) + cmd = get_arguments([opts['clang']] + opts['report'] + ['-o', name], cwd) logging.debug('exec command in %s: %s', cwd, ' '.join(cmd)) subprocess.call(cmd, cwd=cwd) @@ -116,7 +116,8 @@ requested, it calls the continuation to generate it. """ cwd = opts['directory'] - cmd = [opts['clang']] + opts['analyze'] + opts['output'] + cmd = get_arguments([opts['clang']] + opts['analyze'] + opts['output'], + cwd) logging.debug('exec command in %s: %s', cwd, ' '.join(cmd)) child = subprocess.Popen(cmd, cwd=cwd, @@ -170,7 +171,7 @@ common.extend(['-arch', opts.pop('arch')]) common.extend(opts.pop('compile_options', [])) common.extend(['-x', opts['language']]) - common.append(opts['file']) + common.append(os.path.relpath(opts['file'], opts['directory'])) opts.update({ 'analyze': ['--analyze'] + opts['direct_args'] + common, @@ -180,32 +181,11 @@ return continuation(opts) -@require(['file']) +@require(['file', 'c++']) def language_check(opts, continuation=create_commands): """ Find out the language from command line parameters or file name extension. The decision also influenced by the compiler invocation. """ - def from_filename(name, cplusplus_compiler): - """ Return the language from fille name extension. """ - - mapping = { - '.c': 'c++' if cplusplus_compiler else 'c', - '.cp': 'c++', - '.cpp': 'c++', - '.cxx': 'c++', - '.txx': 'c++', - '.cc': 'c++', - '.C': 'c++', - '.ii': 'c++-cpp-output', - '.i': 'c++-cpp-output' if cplusplus_compiler else 'c-cpp-output', - '.m': 'objective-c', - '.mi': 'objective-c-cpp-output', - '.mm': 'objective-c++', - '.mii': 'objective-c++-cpp-output' - } - (_, extension) = os.path.splitext(os.path.basename(name)) - return mapping.get(extension) - accepteds = { 'c', 'c++', 'objective-c', 'objective-c++', 'c-cpp-output', 'c++-cpp-output', 'objective-c-cpp-output' @@ -213,7 +193,7 @@ key = 'language' language = opts[key] if key in opts else \ - from_filename(opts['file'], opts.get('cxx', False)) + classify_source(opts['file'], opts['c++']) if language is None: logging.debug('skip analysis, language not known') @@ -236,7 +216,7 @@ key = 'archs_seen' if key in opts: # filter out disabled architectures and -arch switches - archs = [a for a in opts[key] if '-arch' != a and a not in disableds] + archs = [a for a in opts[key] if a not in disableds] if not archs: logging.debug('skip analysis, found not supported arch') Index: tools/scan-build-py/libscanbuild/shell.py =================================================================== --- /dev/null +++ tools/scan-build-py/libscanbuild/shell.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# The LLVM Compiler Infrastructure +# +# This file is distributed under the University of Illinois Open Source +# License. See LICENSE.TXT for details. +""" This module implements basic shell escaping/unescaping methods. """ + +import re +import shlex + +__all__ = ['encode', 'decode'] + + +def encode(command): + """ Takes a command as list and returns a string. """ + + def needs_quote(arg): + """ Returns true if arguments needs to be protected by quotes. + + Previous implementation was shlex.split method, but that's not good + for this job. Currently is running through the string with a basic + state checking. """ + + reserved = {' ', '$', '%', '&', '(', ')', '[', ']', '{', '}', '*', '|', + '<', '>', '@', '?', '!'} + state = 0 + for c in arg: + if state == 0 and c in reserved: + return True + elif state == 0 and c == '\\': + state = 1 + elif state == 1 and c in reserved | {'\\'}: + state = 0 + elif state == 0 and c == '"': + state = 2 + elif state == 2 and c == '"': + state = 0 + elif state == 0 and c == "'": + state = 3 + elif state == 3 and c == "'": + state = 0 + return state != 0 + + def escape(arg): + """ Do protect argument if that's needed. """ + + table = {'\\': '\\\\', '"': '\\"'} + escaped = ''.join([table.get(c, c) for c in arg]) + + return '"' + escaped + '"' if needs_quote(arg) else escaped + + return " ".join([escape(arg) for arg in command]) + + +def decode(string): + """ Takes a command string and returns as a list. """ + + def unescape(arg): + """ Gets rid of the escaping characters. """ + + if len(arg) >= 2 and arg[0] == arg[-1] and arg[0] == '"': + arg = arg[1:-1] + return re.sub(r'\\(["\\])', r'\1', arg) + return re.sub(r'\\([\\ $%&\(\)\[\]\{\}\*|<>@?!])', r'\1', arg) + + return [unescape(arg) for arg in shlex.split(string)] Index: tools/scan-build-py/setup.py =================================================================== --- tools/scan-build-py/setup.py +++ tools/scan-build-py/setup.py @@ -2,44 +2,6 @@ # -*- coding: utf-8 -*- from setuptools import setup -from subprocess import check_call -from distutils.dir_util import mkpath -from distutils.command.build import build -from distutils.command.install import install - - -class BuildEAR(build): - - def run(self): - import os - import os.path - - mkpath(self.build_temp) - - source_dir = os.path.join(os.getcwd(), 'libear') - dest_dir = os.path.abspath(self.build_lib) - - cmd = ['cmake', '-DCMAKE_INSTALL_PREFIX=' + dest_dir, source_dir] - check_call(cmd, cwd=self.build_temp) - - cmd = ['make', 'install'] - check_call(cmd, cwd=self.build_temp) - - -class Build(build): - - def run(self): - self.run_command('buildear') - build.run(self) - - -class Install(install): - - def run(self): - self.run_command('build') - self.run_command('install_scripts') - install.run(self) - setup( name='beye', @@ -55,9 +17,8 @@ scripts=['bin/scan-build', 'bin/intercept-build', 'bin/intercept-cc', 'bin/intercept-c++', 'bin/analyze-build', 'bin/analyze-cc', 'bin/analyze-c++'], - packages=['libscanbuild'], - package_data={'libscanbuild': ['resources/*']}, - cmdclass={'buildear': BuildEAR, 'install': Install, 'build': Build}, + packages=['libscanbuild', 'libear'], + package_data={'libscanbuild': ['resources/*'], 'libear': ['config.h.in', 'ear.c']}, classifiers=[ "Development Status :: 4 - Beta", "License :: OSI Approved :: University of Illinois/NCSA Open Source License", Index: tools/scan-build-py/tests/functional/cases/__init__.py =================================================================== --- tools/scan-build-py/tests/functional/cases/__init__.py +++ tools/scan-build-py/tests/functional/cases/__init__.py @@ -4,6 +4,7 @@ # This file is distributed under the University of Illinois Open Source # License. See LICENSE.TXT for details. +import re import os.path import subprocess @@ -37,3 +38,32 @@ return subprocess.check_call(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + + +def call_and_report(analyzer_cmd, build_cmd): + child = subprocess.Popen(analyzer_cmd + ['-v'] + build_cmd, + universal_newlines=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + pattern = re.compile('Report directory created: (.+)') + directory = None + for line in child.stdout.readlines(): + match = pattern.search(line) + if match and match.lastindex == 1: + directory = match.group(1) + break + child.stdout.close() + child.wait() + + return (child.returncode, directory) + + +def check_call_and_report(analyzer_cmd, build_cmd): + exit_code, result = call_and_report(analyzer_cmd, build_cmd) + if exit_code != 0: + raise subprocess.CalledProcessError( + "Command '{0}' returned non-zero exit status {1}".format( + cmd, exit_code)) + else: + return result Index: tools/scan-build-py/tests/functional/cases/test_from_cdb.py =================================================================== --- tools/scan-build-py/tests/functional/cases/test_from_cdb.py +++ tools/scan-build-py/tests/functional/cases/test_from_cdb.py @@ -5,6 +5,7 @@ # License. See LICENSE.TXT for details. from ...unit import fixtures +from . import call_and_report import unittest import os.path @@ -31,82 +32,66 @@ def run_driver(directory, cdb, args): cmd = ['intercept-build', 'analyze', '--cdb', cdb, '--output', directory] \ + args - child = subprocess.Popen(cmd, - universal_newlines=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - output = child.stdout.readlines() - child.stdout.close() - child.wait() - return (child.returncode, output) + return call_and_report(cmd, []) class OutputDirectoryTest(unittest.TestCase): def test_regular_keeps_report_dir(self): with fixtures.TempDir() as tmpdir: cdb = prepare_cdb('regular', tmpdir) - outdir = os.path.join(tmpdir, 'result') - exit_code, output = run_driver(outdir, cdb, []) - self.assertTrue(os.path.isdir(outdir)) + exit_code, reportdir = run_driver(tmpdir, cdb, []) + self.assertTrue(os.path.isdir(reportdir)) def test_clear_deletes_report_dir(self): with fixtures.TempDir() as tmpdir: cdb = prepare_cdb('clean', tmpdir) - outdir = os.path.join(tmpdir, 'result') - exit_code, output = run_driver(outdir, cdb, []) - self.assertFalse(os.path.isdir(outdir)) + exit_code, reportdir = run_driver(tmpdir, cdb, []) + self.assertFalse(os.path.isdir(reportdir)) def test_clear_keeps_report_dir_when_asked(self): with fixtures.TempDir() as tmpdir: cdb = prepare_cdb('clean', tmpdir) - outdir = os.path.join(tmpdir, 'result') - exit_code, output = run_driver(outdir, cdb, ['--keep-empty']) - self.assertTrue(os.path.isdir(outdir)) + exit_code, reportdir = run_driver(tmpdir, cdb, ['--keep-empty']) + self.assertTrue(os.path.isdir(reportdir)) class ExitCodeTest(unittest.TestCase): def test_regular_does_not_set_exit_code(self): with fixtures.TempDir() as tmpdir: cdb = prepare_cdb('regular', tmpdir) - outdir = os.path.join(tmpdir, 'result') - exit_code, output = run_driver(outdir, cdb, []) + exit_code, __ = run_driver(tmpdir, cdb, []) self.assertFalse(exit_code) def test_clear_does_not_set_exit_code(self): with fixtures.TempDir() as tmpdir: cdb = prepare_cdb('clean', tmpdir) - outdir = os.path.join(tmpdir, 'result') - exit_code, output = run_driver(outdir, cdb, []) + exit_code, __ = run_driver(tmpdir, cdb, []) self.assertFalse(exit_code) def test_regular_sets_exit_code_if_asked(self): with fixtures.TempDir() as tmpdir: cdb = prepare_cdb('regular', tmpdir) - outdir = os.path.join(tmpdir, 'result') - exit_code, output = run_driver(outdir, cdb, ['--status-bugs']) + exit_code, __ = run_driver(tmpdir, cdb, ['--status-bugs']) self.assertTrue(exit_code) def test_clear_does_not_set_exit_code_if_asked(self): with fixtures.TempDir() as tmpdir: cdb = prepare_cdb('clean', tmpdir) - outdir = os.path.join(tmpdir, 'result') - exit_code, output = run_driver(outdir, cdb, ['--status-bugs']) + exit_code, __ = run_driver(tmpdir, cdb, ['--status-bugs']) self.assertFalse(exit_code) def test_regular_sets_exit_code_if_asked_from_plist(self): with fixtures.TempDir() as tmpdir: cdb = prepare_cdb('regular', tmpdir) - outdir = os.path.join(tmpdir, 'result') - exit_code, output = run_driver( - outdir, cdb, ['--status-bugs', '--plist']) + exit_code, __ = run_driver( + tmpdir, cdb, ['--status-bugs', '--plist']) self.assertTrue(exit_code) def test_clear_does_not_set_exit_code_if_asked_from_plist(self): with fixtures.TempDir() as tmpdir: cdb = prepare_cdb('clean', tmpdir) - outdir = os.path.join(tmpdir, 'result') - exit_code, output = run_driver( - outdir, cdb, ['--status-bugs', '--plist']) + exit_code, __ = run_driver( + tmpdir, cdb, ['--status-bugs', '--plist']) self.assertFalse(exit_code) @@ -122,47 +107,46 @@ def test_default_creates_html_report(self): with fixtures.TempDir() as tmpdir: cdb = prepare_cdb('regular', tmpdir) - outdir = os.path.join(tmpdir, 'result') - exit_code, output = run_driver(outdir, cdb, []) - self.assertTrue(os.path.exists(os.path.join(outdir, 'index.html'))) - self.assertEqual(self.get_html_count(outdir), 2) - self.assertEqual(self.get_plist_count(outdir), 0) + exit_code, reportdir = run_driver(tmpdir, cdb, []) + self.assertTrue( + os.path.exists(os.path.join(reportdir, 'index.html'))) + self.assertEqual(self.get_html_count(reportdir), 2) + self.assertEqual(self.get_plist_count(reportdir), 0) def test_plist_and_html_creates_html_report(self): with fixtures.TempDir() as tmpdir: cdb = prepare_cdb('regular', tmpdir) - outdir = os.path.join(tmpdir, 'result') - exit_code, output = run_driver(outdir, cdb, ['--plist-html']) - self.assertTrue(os.path.exists(os.path.join(outdir, 'index.html'))) - self.assertEqual(self.get_html_count(outdir), 2) - self.assertEqual(self.get_plist_count(outdir), 5) + exit_code, reportdir = run_driver(tmpdir, cdb, ['--plist-html']) + self.assertTrue( + os.path.exists(os.path.join(reportdir, 'index.html'))) + self.assertEqual(self.get_html_count(reportdir), 2) + self.assertEqual(self.get_plist_count(reportdir), 5) def test_plist_does_not_creates_html_report(self): with fixtures.TempDir() as tmpdir: cdb = prepare_cdb('regular', tmpdir) - outdir = os.path.join(tmpdir, 'result') - exit_code, output = run_driver(outdir, cdb, ['--plist']) + exit_code, reportdir = run_driver(tmpdir, cdb, ['--plist']) self.assertFalse( - os.path.exists(os.path.join(outdir, 'index.html'))) - self.assertEqual(self.get_html_count(outdir), 0) - self.assertEqual(self.get_plist_count(outdir), 5) + os.path.exists(os.path.join(reportdir, 'index.html'))) + self.assertEqual(self.get_html_count(reportdir), 0) + self.assertEqual(self.get_plist_count(reportdir), 5) class FailureReportTest(unittest.TestCase): def test_broken_creates_failure_reports(self): with fixtures.TempDir() as tmpdir: cdb = prepare_cdb('broken', tmpdir) - outdir = os.path.join(tmpdir, 'result') - exit_code, output = run_driver(outdir, cdb, []) - self.assertTrue(os.path.isdir(os.path.join(outdir, 'failures'))) + exit_code, reportdir = run_driver(tmpdir, cdb, []) + self.assertTrue( + os.path.isdir(os.path.join(reportdir, 'failures'))) def test_broken_does_not_creates_failure_reports(self): with fixtures.TempDir() as tmpdir: cdb = prepare_cdb('broken', tmpdir) - outdir = os.path.join(tmpdir, 'result') - exit_code, output = run_driver( - outdir, cdb, ['--no-failure-reports']) - self.assertFalse(os.path.isdir(os.path.join(outdir, 'failures'))) + exit_code, reportdir = run_driver( + tmpdir, cdb, ['--no-failure-reports']) + self.assertFalse( + os.path.isdir(os.path.join(reportdir, 'failures'))) class TitleTest(unittest.TestCase): @@ -174,7 +158,7 @@ ] result = dict() - index = os.path.join(directory, 'result', 'index.html') + index = os.path.join(directory, 'index.html') with open(index, 'r') as handler: for line in handler.readlines(): for regex in patterns: @@ -188,14 +172,12 @@ def test_default_title_in_report(self): with fixtures.TempDir() as tmpdir: cdb = prepare_cdb('broken', tmpdir) - outdir = os.path.join(tmpdir, 'result') - exit_code, output = run_driver(outdir, cdb, []) - self.assertTitleEqual(tmpdir, 'src - analyzer results') + exit_code, reportdir = run_driver(tmpdir, cdb, []) + self.assertTitleEqual(reportdir, 'src - analyzer results') def test_given_title_in_report(self): with fixtures.TempDir() as tmpdir: cdb = prepare_cdb('broken', tmpdir) - outdir = os.path.join(tmpdir, 'result') - exit_code, output = run_driver( - outdir, cdb, ['--html-title', 'this is the title']) - self.assertTitleEqual(tmpdir, 'this is the title') + exit_code, reportdir = run_driver( + tmpdir, cdb, ['--html-title', 'this is the title']) + self.assertTitleEqual(reportdir, 'this is the title') Index: tools/scan-build-py/tests/functional/cases/test_from_cmd.py =================================================================== --- tools/scan-build-py/tests/functional/cases/test_from_cmd.py +++ tools/scan-build-py/tests/functional/cases/test_from_cmd.py @@ -5,7 +5,7 @@ # License. See LICENSE.TXT for details. from ...unit import fixtures -from . import make_args, silent_check_call +from . import make_args, check_call_and_report import unittest import os.path @@ -13,27 +13,25 @@ class OutputDirectoryTest(unittest.TestCase): @staticmethod - def run_sb(outdir, args): - return silent_check_call( - ['intercept-build', 'all', '-o', outdir] + args) + def run_sb(outdir, args, cmd): + return check_call_and_report( + ['intercept-build', 'all', '-o', outdir] + args, + cmd) def test_regular_keeps_report_dir(self): with fixtures.TempDir() as tmpdir: - outdir = os.path.join(tmpdir, 'result') make = make_args(tmpdir) + ['build_regular'] - self.run_sb(outdir, make) + outdir = self.run_sb(tmpdir, [], make) self.assertTrue(os.path.isdir(outdir)) def test_clear_deletes_report_dir(self): with fixtures.TempDir() as tmpdir: - outdir = os.path.join(tmpdir, 'result') make = make_args(tmpdir) + ['build_clean'] - self.run_sb(outdir, make) + outdir = self.run_sb(tmpdir, [], make) self.assertFalse(os.path.isdir(outdir)) def test_clear_keeps_report_dir_when_asked(self): with fixtures.TempDir() as tmpdir: - outdir = os.path.join(tmpdir, 'result') make = make_args(tmpdir) + ['build_clean'] - self.run_sb(outdir, ['--keep-empty'] + make) + outdir = self.run_sb(tmpdir, ['--keep-empty'], make) self.assertTrue(os.path.isdir(outdir)) Index: tools/scan-build-py/tests/functional/exec/main.c =================================================================== --- tools/scan-build-py/tests/functional/exec/main.c +++ tools/scan-build-py/tests/functional/exec/main.c @@ -58,7 +58,7 @@ cwd = NULL; } -void expected_out(const char *cmd, const char *file) { +void expected_out(const char *file) { if (need_comma) fprintf(fd, ",\n"); else @@ -66,7 +66,7 @@ fprintf(fd, "{\n"); fprintf(fd, " \"directory\": \"%s\",\n", cwd); - fprintf(fd, " \"command\": \"%s -c %s\",\n", cmd, file); + fprintf(fd, " \"command\": \"cc -c %s\",\n", file); fprintf(fd, " \"file\": \"%s/%s\"\n", cwd, file); fprintf(fd, "}\n"); } @@ -116,7 +116,7 @@ char *const compiler = "/usr/bin/cc"; char *const argv[] = {"cc", "-c", file, 0}; - expected_out("cc", file); + expected_out(file); create_source(file); FORK(execv(compiler, argv);) @@ -130,7 +130,7 @@ char *const argv[] = {compiler, "-c", file, 0}; char *const envp[] = {"THIS=THAT", 0}; - expected_out("/usr/bin/cc", file); + expected_out(file); create_source(file); FORK(execve(compiler, argv, envp);) @@ -143,7 +143,7 @@ char *const compiler = "cc"; char *const argv[] = {compiler, "-c", file, 0}; - expected_out(compiler, file); + expected_out(file); create_source(file); FORK(execvp(compiler, argv);) @@ -156,7 +156,7 @@ char *const compiler = "cc"; char *const argv[] = {compiler, "-c", file, 0}; - expected_out(compiler, file); + expected_out(file); create_source(file); FORK(execvP(compiler, _PATH_DEFPATH, argv);) @@ -170,7 +170,7 @@ char *const argv[] = {"/usr/bin/cc", "-c", file, 0}; char *const envp[] = {"THIS=THAT", 0}; - expected_out("/usr/bin/cc", file); + expected_out(file); create_source(file); FORK(execvpe(compiler, argv, envp);) @@ -182,7 +182,7 @@ char *const file = "execl.c"; char *const compiler = "/usr/bin/cc"; - expected_out("cc", file); + expected_out(file); create_source(file); FORK(execl(compiler, "cc", "-c", file, (char *)0);) @@ -194,7 +194,7 @@ char *const file = "execlp.c"; char *const compiler = "cc"; - expected_out(compiler, file); + expected_out(file); create_source(file); FORK(execlp(compiler, compiler, "-c", file, (char *)0);) @@ -207,7 +207,7 @@ char *const compiler = "/usr/bin/cc"; char *const envp[] = {"THIS=THAT", 0}; - expected_out(compiler, file); + expected_out(file); create_source(file); FORK(execle(compiler, compiler, "-c", file, (char *)0, envp);) @@ -220,7 +220,7 @@ char *const compiler = "cc"; char *const argv[] = {compiler, "-c", file, 0}; - expected_out(compiler, file); + expected_out(file); create_source(file); pid_t child; @@ -238,7 +238,7 @@ char *const compiler = "cc"; char *const argv[] = {compiler, "-c", file, 0}; - expected_out(compiler, file); + expected_out(file); create_source(file); pid_t child; Index: tools/scan-build-py/tests/functional/src/build/Makefile =================================================================== --- tools/scan-build-py/tests/functional/src/build/Makefile +++ tools/scan-build-py/tests/functional/src/build/Makefile @@ -31,3 +31,6 @@ build_all_in_one: $(CC) -o $(PROGRAM) $(CFLAGS) $(LDFLAGS) $(CLEAN_SRCS:%.c=$(SRCDIR)/%.c) + +clean: + rm -f $(PROGRAM) $(OBJDIR)/*.o Index: tools/scan-build-py/tests/unit/__init__.py =================================================================== --- tools/scan-build-py/tests/unit/__init__.py +++ tools/scan-build-py/tests/unit/__init__.py @@ -10,6 +10,7 @@ from . import test_report from . import test_driver from . import test_intercept +from . import test_shell def load_tests(loader, suite, pattern): @@ -19,4 +20,5 @@ suite.addTests(loader.loadTestsFromModule(test_report)) suite.addTests(loader.loadTestsFromModule(test_driver)) suite.addTests(loader.loadTestsFromModule(test_intercept)) + suite.addTests(loader.loadTestsFromModule(test_shell)) return suite Index: tools/scan-build-py/tests/unit/test_clang.py =================================================================== --- tools/scan-build-py/tests/unit/test_clang.py +++ tools/scan-build-py/tests/unit/test_clang.py @@ -18,8 +18,8 @@ handle.write('') result = sut.get_arguments( - tmpdir, - ['clang', '-c', filename, '-DNDEBUG', '-Dvar="this is it"']) + ['clang', '-c', filename, '-DNDEBUG', '-Dvar="this is it"'], + tmpdir) self.assertIn('NDEBUG', result) self.assertIn('var="this is it"', result) @@ -28,5 +28,5 @@ self.assertRaises( Exception, sut.get_arguments, - '.', - ['clang', '-###', '-fsyntax-only', '-x', 'c', 'notexist.c']) + ['clang', '-###', '-fsyntax-only', '-x', 'c', 'notexist.c'], + '.') Index: tools/scan-build-py/tests/unit/test_command.py =================================================================== --- tools/scan-build-py/tests/unit/test_command.py +++ tools/scan-build-py/tests/unit/test_command.py @@ -16,9 +16,6 @@ opts = sut.classify_parameters(cmd) self.assertEqual(expected, opts['action']) - Info = sut.Action.Info - test(Info, ['clang', 'source.c', '-print-prog-name']) - Link = sut.Action.Link test(Link, ['clang', 'source.c']) @@ -26,7 +23,7 @@ test(Compile, ['clang', '-c', 'source.c']) test(Compile, ['clang', '-c', 'source.c', '-MF', 'source.d']) - Preprocess = sut.Action.Preprocess + Preprocess = sut.Action.Ignored test(Preprocess, ['clang', '-E', 'source.c']) test(Preprocess, ['clang', '-c', '-E', 'source.c']) test(Preprocess, ['clang', '-c', '-M', 'source.c']) @@ -37,9 +34,9 @@ opts = sut.classify_parameters(cmd) return opts.get('compile_options', []) - self.assertEqual(['-O1'], test(['clang', '-c', 'source.c', '-O'])) + self.assertEqual(['-O'], test(['clang', '-c', 'source.c', '-O'])) self.assertEqual(['-O1'], test(['clang', '-c', 'source.c', '-O1'])) - self.assertEqual(['-O2'], test(['clang', '-c', 'source.c', '-Os'])) + self.assertEqual(['-Os'], test(['clang', '-c', 'source.c', '-Os'])) self.assertEqual(['-O2'], test(['clang', '-c', 'source.c', '-O2'])) self.assertEqual(['-O3'], test(['clang', '-c', 'source.c', '-O3'])) @@ -52,6 +49,15 @@ self.assertEqual('c', test(['clang', '-c', 'source.c', '-x', 'c'])) self.assertEqual('cpp', test(['clang', '-c', 'source.c', '-x', 'cpp'])) + def test_output(self): + def test(cmd): + opts = sut.classify_parameters(cmd) + return opts.get('output') + + self.assertEqual(None, test(['clang', '-c', 'source.c'])) + self.assertEqual('source.o', + test(['clang', '-c', '-o', 'source.o', 'source.c'])) + def test_arch(self): def test(cmd): opts = sut.classify_parameters(cmd) @@ -60,9 +66,9 @@ eq = self.assertEqual eq([], test(['clang', '-c', 'source.c'])) - eq(['-arch', 'mips'], + eq(['mips'], test(['clang', '-c', 'source.c', '-arch', 'mips'])) - eq(['-arch', 'mips', '-arch', 'i386'], + eq(['mips', 'i386'], test(['clang', '-c', 'source.c', '-arch', 'mips', '-arch', 'i386'])) def test_input_file(self): @@ -76,17 +82,6 @@ eq(['src.c'], test(['clang', '-c', 'src.c'])) eq(['s1.c', 's2.c'], test(['clang', '-c', 's1.c', 's2.c'])) - def test_output_file(self): - def test(cmd): - opts = sut.classify_parameters(cmd) - return opts.get('output', None) - - eq = self.assertEqual - - eq(None, test(['clang', 'src.c'])) - eq('src.o', test(['clang', '-c', 'src.c', '-o', 'src.o'])) - eq('src.o', test(['clang', '-c', '-o', 'src.o', 'src.c'])) - def test_include(self): def test(cmd): opts = sut.classify_parameters(cmd) @@ -130,20 +125,16 @@ test(['clang', '-c', 'src.c', '-Dvar="val ues"'])) def test_ignored_flags(self): - def test(cmd): - salt = ['-I.', '-D_THIS'] - opts = sut.classify_parameters(cmd + salt) - self.assertEqual(salt, opts.get('compile_options')) - return opts.get('link_options', []) + def test(flags): + cmd = ['clang', 'src.o'] + opts = sut.classify_parameters(cmd + flags) + self.assertEqual(['src.o'], opts.get('compile_options')) - eq = self.assertEqual - - eq([], - test(['clang', 'src.o'])) - eq([], - test(['clang', 'src.o', '-lrt', '-L/opt/company/lib'])) - eq([], - test(['clang', 'src.o', '-framework', 'foo'])) + test([]) + test(['-lrt', '-L/opt/company/lib']) + test(['-static']) + test(['-Wnoexcept', '-Wall']) + test(['-mtune=i386', '-mcpu=i386']) def test_compile_only_flags(self): def test(cmd): @@ -152,17 +143,8 @@ eq = self.assertEqual - eq([], test(['clang', '-c', 'src.c'])) - eq([], - test(['clang', '-c', 'src.c', '-Wnoexcept'])) - eq([], - test(['clang', '-c', 'src.c', '-Wall'])) - eq(['-Wno-cpp'], - test(['clang', '-c', 'src.c', '-Wno-cpp'])) eq(['-std=C99'], test(['clang', '-c', 'src.c', '-std=C99'])) - eq(['-mtune=i386', '-mcpu=i386'], - test(['clang', '-c', 'src.c', '-mtune=i386', '-mcpu=i386'])) eq(['-nostdinc'], test(['clang', '-c', 'src.c', '-nostdinc'])) eq(['-isystem', '/image/debian'], @@ -181,8 +163,6 @@ eq = self.assertEqual - eq([], - test(['clang', '-c', 'src.c', '-fsyntax-only'])) eq(['-fsinged-char'], test(['clang', '-c', 'src.c', '-fsinged-char'])) eq(['-fPIC'], @@ -194,12 +174,14 @@ eq(['-isysroot', '/'], test(['clang', '-c', 'src.c', '-isysroot', '/'])) eq([], + test(['clang', '-c', 'src.c', '-fsyntax-only'])) + eq([], test(['clang', '-c', 'src.c', '-sectorder', 'a', 'b', 'c'])) def test_detect_cxx_from_compiler_name(self): def test(cmd): opts = sut.classify_parameters(cmd) - return opts.get('cxx') + return opts.get('c++') eq = self.assertEqual Index: tools/scan-build-py/tests/unit/test_intercept.py =================================================================== --- tools/scan-build-py/tests/unit/test_intercept.py +++ tools/scan-build-py/tests/unit/test_intercept.py @@ -13,7 +13,7 @@ def test_compiler_call_filter(self): def test(command): - return sut.is_compiler_call({'command': [command]}) + return sut.compiler_call({'command': [command]}) self.assertTrue(test('clang')) self.assertTrue(test('clang-3.6')) Index: tools/scan-build-py/tests/unit/test_runner.py =================================================================== --- tools/scan-build-py/tests/unit/test_runner.py +++ tools/scan-build-py/tests/unit/test_runner.py @@ -141,18 +141,19 @@ def test(expected, input): spy = fixtures.Spy() self.assertEqual(spy.success, sut.language_check(input, spy.call)) - self.assertEqual(expected, spy.arg) + self.assertEqual(expected, spy.arg['language']) l = 'language' f = 'file' - i = 'cxx' - test({f: 'file.c', l: 'c'}, {f: 'file.c', l: 'c'}) - test({f: 'file.c', l: 'c++'}, {f: 'file.c', l: 'c++'}) - test({f: 'file.c', l: 'c++', i: True}, {f: 'file.c', i: True}) - test({f: 'file.c', l: 'c'}, {f: 'file.c'}) - test({f: 'file.cxx', l: 'c++'}, {f: 'file.cxx'}) - test({f: 'file.i', l: 'c-cpp-output'}, {f: 'file.i'}) - test({f: 'f.i', l: 'c-cpp-output'}, {f: 'f.i', l: 'c-cpp-output'}) + i = 'c++' + test('c', {f: 'file.c', l: 'c', i: False}) + test('c++', {f: 'file.c', l: 'c++', i: False}) + test('c++', {f: 'file.c', i: True}) + test('c', {f: 'file.c', i: False}) + test('c++', {f: 'file.cxx', i: False}) + test('c-cpp-output', {f: 'file.i', i: False}) + test('c++-cpp-output', {f: 'file.i', i: True}) + test('c-cpp-output', {f: 'f.i', l: 'c-cpp-output', i: True}) def test_arch_loop(self): def test(input): @@ -163,16 +164,16 @@ input = {'key': 'value'} self.assertEqual(input, test(input)) - input = {'archs_seen': ['-arch', 'i386']} + input = {'archs_seen': ['i386']} self.assertEqual({'arch': 'i386'}, test(input)) - input = {'archs_seen': ['-arch', 'ppc']} + input = {'archs_seen': ['ppc']} self.assertEqual(None, test(input)) - input = {'archs_seen': ['-arch', 'i386', '-arch', 'ppc']} + input = {'archs_seen': ['i386', 'ppc']} self.assertEqual({'arch': 'i386'}, test(input)) - input = {'archs_seen': ['-arch', 'i386', '-arch', 'sparc']} + input = {'archs_seen': ['i386', 'sparc']} result = test(input) self.assertTrue(result == {'arch': 'i386'} or result == {'arch': 'sparc'}) Index: tools/scan-build-py/tests/unit/test_shell.py =================================================================== --- /dev/null +++ tools/scan-build-py/tests/unit/test_shell.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# The LLVM Compiler Infrastructure +# +# This file is distributed under the University of Illinois Open Source +# License. See LICENSE.TXT for details. + +import libscanbuild.shell as sut +import unittest + + +class ShellTest(unittest.TestCase): + + def test_encode_decode_are_same(self): + def test(value): + self.assertEqual(sut.encode(sut.decode(value)), value) + + test("") + test("clang") + test("clang this and that") + + def test_decode_encode_are_same(self): + def test(value): + self.assertEqual(sut.decode(sut.encode(value)), value) + + test([]) + test(['clang']) + test(['clang', 'this', 'and', 'that']) + test(['clang', 'this and', 'that']) + test(['clang', "it's me", 'again']) + test(['clang', 'some "words" are', 'quoted']) + + def test_encode(self): + self.assertEqual(sut.encode(['clang', "it's me", 'again']), + 'clang "it\'s me" again') + self.assertEqual(sut.encode(['clang', "it(s me", 'again)']), + 'clang "it(s me" "again)"') + self.assertEqual(sut.encode(['clang', 'redirect > it']), + 'clang "redirect > it"') + self.assertEqual(sut.encode(['clang', '-DKEY="VALUE"']), + 'clang -DKEY=\\"VALUE\\"') + self.assertEqual(sut.encode(['clang', '-DKEY="value with spaces"']), + 'clang -DKEY=\\"value with spaces\\"')