260 changes: 260 additions & 0 deletions clang/tools/scan-build-py/libear/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
# -*- 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 compiles the intercept library. """

import sys
import os
import os.path
import re
import tempfile
import shutil
import contextlib
import logging

__all__ = ['build_libear']


def build_libear(compiler, dst_dir):
""" Returns the full path to the 'libear' library. """

try:
src_dir = os.path.dirname(os.path.realpath(__file__))
toolset = make_toolset(src_dir)
toolset.set_compiler(compiler)
toolset.set_language_standard('c99')
toolset.add_definitions(['-D_GNU_SOURCE'])

configure = do_configure(toolset)
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('exect', 'HAVE_EXECT')
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'))

target = create_shared_library('ear', toolset)
target.add_include(dst_dir)
target.add_sources('ear.c')
target.link_against(toolset.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)


@contextlib.contextmanager
def TemporaryDirectory(**kwargs):
name = tempfile.mkdtemp(**kwargs)
try:
yield name
finally:
shutil.rmtree(name)


class Toolset(object):
""" Abstract class to represent different toolset. """

def __init__(self, src_dir):
self.src_dir = src_dir
self.compiler = None
self.c_flags = []

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):
raise NotImplementedError()

def shared_library_name(self, name):
raise NotImplementedError()

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):
raise NotImplementedError()


class DarwinToolset(Toolset):
def __init__(self, src_dir):
Toolset.__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 UnixToolset(Toolset):
def __init__(self, src_dir):
Toolset.__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 LinuxToolset(UnixToolset):
def __init__(self, src_dir):
UnixToolset.__init__(self, src_dir)

def dl_libraries(self):
return ['dl']


def make_toolset(src_dir):
platform = sys.platform
if platform in {'win32', 'cygwin'}:
raise RuntimeError('not implemented on this platform')
elif platform == 'darwin':
return DarwinToolset(src_dir)
elif platform in {'linux', 'linux2'}:
return LinuxToolset(src_dir)
else:
return UnixToolset(src_dir)


class Configure(object):
def __init__(self, toolset):
self.ctx = toolset
self.results = {'APPLE': sys.platform == 'darwin'}

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 <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))


def do_configure(toolset):
return Configure(toolset)


class SharedLibrary(object):
def __init__(self, name, toolset):
self.name = toolset.shared_library_name(name)
self.ctx = toolset
self.inc = []
self.src = []
self.lib = []

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)


def create_shared_library(name, toolset):
return SharedLibrary(name, toolset)
23 changes: 23 additions & 0 deletions clang/tools/scan-build-py/libear/config.h.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* -*- coding: utf-8 -*-
// The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
*/

#pragma once

#cmakedefine HAVE_EXECVE
#cmakedefine HAVE_EXECV
#cmakedefine HAVE_EXECVPE
#cmakedefine HAVE_EXECVP
#cmakedefine HAVE_EXECVP2
#cmakedefine HAVE_EXECT
#cmakedefine HAVE_EXECL
#cmakedefine HAVE_EXECLP
#cmakedefine HAVE_EXECLE
#cmakedefine HAVE_POSIX_SPAWN
#cmakedefine HAVE_POSIX_SPAWNP
#cmakedefine HAVE_NSGETENVIRON

#cmakedefine APPLE
605 changes: 605 additions & 0 deletions clang/tools/scan-build-py/libear/ear.c

Large diffs are not rendered by default.

82 changes: 82 additions & 0 deletions clang/tools/scan-build-py/libscanbuild/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# -*- 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 responsible to run the Clang static analyzer against any build
and generate reports.
"""


def duplicate_check(method):
""" Predicate to detect duplicated entries.
Unique hash method can be use to detect duplicates. Entries are
represented as dictionaries, which has no default hash method.
This implementation uses a set datatype to store the unique hash values.
This method returns a method which can detect the duplicate values. """

def predicate(entry):
entry_hash = predicate.unique(entry)
if entry_hash not in predicate.state:
predicate.state.add(entry_hash)
return False
return True

predicate.unique = method
predicate.state = set()
return predicate


def tempdir():
""" Return the default temorary directory. """

from os import getenv
return getenv('TMPDIR', getenv('TEMP', getenv('TMP', '/tmp')))


def initialize_logging(verbose_level):
""" Output content controlled by the verbosity level. """

import sys
import os.path
import logging
level = logging.WARNING - min(logging.WARNING, (10 * verbose_level))

if verbose_level <= 3:
fmt_string = '{0}: %(levelname)s: %(message)s'
else:
fmt_string = '{0}: %(levelname)s: %(funcName)s: %(message)s'

program = os.path.basename(sys.argv[0])
logging.basicConfig(format=fmt_string.format(program), level=level)


def command_entry_point(function):
""" Decorator for command entry points. """

import functools
import logging

@functools.wraps(function)
def wrapper(*args, **kwargs):

exit_code = 127
try:
exit_code = function(*args, **kwargs)
except KeyboardInterrupt:
logging.warning('Keyboard interupt')
except Exception:
logging.exception('Internal error.')
if logging.getLogger().isEnabledFor(logging.DEBUG):
logging.error("Please report this bug and attach the output "
"to the bug report")
else:
logging.error("Please run this command again and turn on "
"verbose mode (add '-vvv' as argument).")
finally:
return exit_code

return wrapper
502 changes: 502 additions & 0 deletions clang/tools/scan-build-py/libscanbuild/analyze.py

Large diffs are not rendered by default.

156 changes: 156 additions & 0 deletions clang/tools/scan-build-py/libscanbuild/clang.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# -*- 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 is responsible for the Clang executable.
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 re
import subprocess
import logging
from libscanbuild.shell import decode

__all__ = ['get_version', 'get_arguments', 'get_checkers']


def get_version(cmd):
""" Returns the compiler version as string. """

lines = subprocess.check_output([cmd, '-v'], stderr=subprocess.STDOUT)
return lines.decode('ascii').splitlines()[0]


def get_arguments(command, cwd):
""" Capture Clang invocation.
This method returns the front-end invocation that would be executed as
a result of the given driver invocation. """

def lastline(stream):
last = None
for line in stream:
last = line
if last is None:
raise Exception("output not found")
return last

cmd = command[:]
cmd.insert(1, '-###')
logging.debug('exec command in %s: %s', cwd, ' '.join(cmd))
child = subprocess.Popen(cmd,
cwd=cwd,
universal_newlines=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
line = lastline(child.stdout)
child.stdout.close()
child.wait()
if child.returncode == 0:
if re.search(r'clang(.*): error:', line):
raise Exception(line)
return decode(line)
else:
raise Exception(line)


def get_active_checkers(clang, plugins):
""" To get the default plugins we execute Clang to print how this
compilation would be called.
For input file we specify stdin and pass only language information. """

def checkers(language):
""" Returns a list of active checkers for the given language. """

load = [elem
for plugin in plugins
for elem in ['-Xclang', '-load', '-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, '.') if pattern.match(arg)]

result = set()
for language in ['c', 'c++', 'objective-c', 'objective-c++']:
result.update(checkers(language))
return result


def get_checkers(clang, plugins):
""" Get all the available checkers from default and from the plugins.
clang -- the compiler we are using
plugins -- list of plugins which was requested by the user
This method returns a dictionary of all available checkers and status.
{<plugin name>: (<plugin description>, <is active by default>)} """

plugins = plugins if plugins else []

def parse_checkers(stream):
""" Parse clang -analyzer-checker-help output.
Below the line 'CHECKERS:' are there the name description pairs.
Many of them are in one line, but some long named plugins has the
name and the description in separate lines.
The plugin name is always prefixed with two space character. The
name contains no whitespaces. Then followed by newline (if it's
too long) or other space characters comes the description of the
plugin. The description ends with a newline character. """

# find checkers header
for line in stream:
if re.match(r'^CHECKERS:', line):
break
# find entries
state = None
for line in stream:
if state and not re.match(r'^\s\s\S', line):
yield (state, line.strip())
state = None
elif re.match(r'^\s\s\S+$', line.rstrip()):
state = line.strip()
else:
pattern = re.compile(r'^\s\s(?P<key>\S*)\s*(?P<value>.*)')
match = pattern.match(line.rstrip())
if match:
current = match.groupdict()
yield (current['key'], current['value'])

def is_active(actives, entry):
""" Returns true if plugin name is matching the active plugin names.
actives -- set of active plugin names (or prefixes).
entry -- the current plugin name to judge.
The active plugin names are specific plugin names or prefix of some
names. One example for prefix, when it say 'unix' and it shall match
on 'unix.API', 'unix.Malloc' and 'unix.MallocSizeof'. """

return any(re.match(r'^' + a + r'(\.|$)', entry) for a in actives)

actives = get_active_checkers(clang, plugins)

load = [elem for plugin in plugins for elem in ['-load', plugin]]
cmd = [clang, '-cc1'] + load + ['-analyzer-checker-help']

logging.debug('exec command: %s', ' '.join(cmd))
child = subprocess.Popen(cmd,
universal_newlines=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
checkers = {
k: (v, is_active(actives, k))
for k, v in parse_checkers(child.stdout)
}
child.stdout.close()
child.wait()
if child.returncode == 0 and len(checkers):
return checkers
else:
raise Exception('Could not query Clang for available checkers.')
133 changes: 133 additions & 0 deletions clang/tools/scan-build-py/libscanbuild/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# -*- 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 is responsible for to parse a compiler invocation. """

import re
import os

__all__ = ['Action', 'classify_parameters', 'classify_source']


class Action(object):
""" Enumeration class for compiler action. """

Link, Compile, Ignored = range(3)


def classify_parameters(command):
""" Parses the command line arguments of the given invocation. """

# result value of this method.
# some value are preset, some will be set only when found.
result = {
'action': Action.Link,
'files': [],
'output': None,
'compile_options': [],
'c++': is_cplusplus_compiler(command[0])
# archs_seen
# language
}

# data structure to ignore compiler parameters.
# key: parameter name, value: number of parameters to ignore afterwards.
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
}

args = iter(command[1:])
for arg in args:
# compiler action parameters are the most important ones...
if arg in {'-E', '-S', '-cc1', '-M', '-MM', '-###'}:
result.update({'action': Action.Ignored})
elif arg == '-c':
result.update({'action': max(result['action'], Action.Compile)})
# arch flags are taken...
elif arg == '-arch':
archs = result.get('archs_seen', [])
result.update({'archs_seen': archs + [next(args)]})
# explicit language option taken...
elif arg == '-x':
result.update({'language': next(args)})
# output flag taken...
elif arg == '-o':
result.update({'output': next(args)})
# warning disable options are taken...
elif re.match(r'^-Wno-', arg):
result['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):
result['files'].append(arg)
# and consider everything else as compile option.
else:
result['compile_options'].append(arg)

return result


def classify_source(filename, cplusplus=False):
""" Return the language from file 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 is_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)
return False if match is None else True
359 changes: 359 additions & 0 deletions clang/tools/scan-build-py/libscanbuild/intercept.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,359 @@
# -*- 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 is responsible to capture the compiler invocation of any
build process. The result of that should be a compilation database.
This implementation is using the LD_PRELOAD or DYLD_INSERT_LIBRARIES
mechanisms provided by the dynamic linker. The related library is implemented
in C language and can be found under 'libear' directory.
The 'libear' library is capturing all child process creation and logging the
relevant information about it into separate files in a specified directory.
The parameter of this process is the output directory name, where the report
files shall be placed. This parameter is passed as an environment variable.
The module also implements compiler wrappers to intercept the compiler calls.
The module implements the build command execution and the post-processing of
the output files, which will condensates into a compilation database. """

import sys
import os
import os.path
import re
import itertools
import json
import glob
import argparse
import logging
import subprocess
from libear import build_libear, TemporaryDirectory
from libscanbuild import duplicate_check, tempdir, initialize_logging
from libscanbuild import command_entry_point
from libscanbuild.command import Action, classify_parameters
from libscanbuild.shell import encode, decode

__all__ = ['capture', 'intercept_build_main', 'intercept_build_wrapper']

GS = chr(0x1d)
RS = chr(0x1e)
US = chr(0x1f)

COMPILER_WRAPPER_CC = 'intercept-cc'
COMPILER_WRAPPER_CXX = 'intercept-c++'


@command_entry_point
def intercept_build_main(bin_dir):
""" Entry point for 'intercept-build' command. """

parser = create_parser()
args = parser.parse_args()

initialize_logging(args.verbose)
logging.debug('Parsed arguments: %s', args)

if not args.build:
parser.print_help()
return 0

return capture(args, bin_dir)


def capture(args, bin_dir):
""" The entry point of build command interception. """

def post_processing(commands):
""" To make a compilation database, it needs to filter out commands
which are not compiler calls. Needs to find the source file name
from the arguments. And do shell escaping on the command.
To support incremental builds, it is desired to read elements from
an existing compilation database from a previous run. These elemets
shall be merged with the new elements. """

# create entries from the current run
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)))
# read entries from previous run
if 'append' in args and args.append and os.path.exists(args.cdb):
with open(args.cdb) as handle:
previous = iter(json.load(handle))
else:
previous = iter([])
# filter out duplicate entries from both
duplicate = duplicate_check(entry_hash)
return (entry for entry in itertools.chain(previous, current)
if os.path.exists(entry['file']) and not duplicate(entry))

with TemporaryDirectory(prefix='intercept-', dir=tempdir()) as tmp_dir:
# run the build command
environment = setup_environment(args, tmp_dir, bin_dir)
logging.debug('run build in environment: %s', environment)
exit_code = subprocess.call(args.build, env=environment)
logging.info('build finished with exit code: %d', exit_code)
# read the intercepted exec calls
commands = itertools.chain.from_iterable(
parse_exec_trace(os.path.join(tmp_dir, filename))
for filename in sorted(glob.iglob(os.path.join(tmp_dir, '*.cmd'))))
# do post processing only if that was requested
if 'raw_entries' not in args or not args.raw_entries:
entries = post_processing(commands)
else:
entries = commands
# dump the compilation database
with open(args.cdb, 'w+') as handle:
json.dump(list(entries), handle, sort_keys=True, indent=4)
return exit_code


def setup_environment(args, destination, bin_dir):
""" Sets up the environment for the build command.
It sets the required environment variables and execute the given command.
The exec calls will be logged by the 'libear' preloaded library or by the
'wrapper' programs. """

c_compiler = args.cc if 'cc' in args else 'cc'
cxx_compiler = args.cxx if 'cxx' in args else 'c++'

libear_path = None if args.override_compiler or is_preload_disabled(
sys.platform) else build_libear(c_compiler, destination)

environment = dict(os.environ)
environment.update({'INTERCEPT_BUILD_TARGET_DIR': destination})

if not libear_path:
logging.debug('intercept gonna use compiler wrappers')
environment.update({
'CC': os.path.join(bin_dir, COMPILER_WRAPPER_CC),
'CXX': os.path.join(bin_dir, COMPILER_WRAPPER_CXX),
'INTERCEPT_BUILD_CC': c_compiler,
'INTERCEPT_BUILD_CXX': cxx_compiler,
'INTERCEPT_BUILD_VERBOSE': 'DEBUG' if args.verbose > 2 else 'INFO'
})
elif sys.platform == 'darwin':
logging.debug('intercept gonna preload libear on OSX')
environment.update({
'DYLD_INSERT_LIBRARIES': libear_path,
'DYLD_FORCE_FLAT_NAMESPACE': '1'
})
else:
logging.debug('intercept gonna preload libear on UNIX')
environment.update({'LD_PRELOAD': libear_path})

return environment


def intercept_build_wrapper(cplusplus):
""" Entry point for `intercept-cc` and `intercept-c++` compiler wrappers.
It does generate execution report into target directory. And execute
the wrapped compilation with the real compiler. The parameters for
report and execution are from environment variables.
Those parameters which for 'libear' library can't have meaningful
values are faked. """

# initialize wrapper logging
logging.basicConfig(format='intercept: %(levelname)s: %(message)s',
level=os.getenv('INTERCEPT_BUILD_VERBOSE', 'INFO'))
# write report
try:
target_dir = os.getenv('INTERCEPT_BUILD_TARGET_DIR')
if not target_dir:
raise UserWarning('exec report target directory not found')
pid = str(os.getpid())
target_file = os.path.join(target_dir, pid + '.cmd')
logging.debug('writing exec report to: %s', target_file)
with open(target_file, 'ab') as handler:
working_dir = os.getcwd()
command = US.join(sys.argv) + US
content = RS.join([pid, pid, 'wrapper', working_dir, command]) + GS
handler.write(content.encode('utf-8'))
except IOError:
logging.exception('writing exec report failed')
except UserWarning as warning:
logging.warning(warning)
# execute with real compiler
compiler = os.getenv('INTERCEPT_BUILD_CXX', 'c++') if cplusplus \
else os.getenv('INTERCEPT_BUILD_CC', 'cc')
compilation = [compiler] + sys.argv[1:]
logging.debug('execute compiler: %s', compilation)
return subprocess.call(compilation)


def parse_exec_trace(filename):
""" Parse the file generated by the 'libear' preloaded library.
Given filename points to a file which contains the basic report
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)):
records = group.split(RS)
yield {
'pid': records[0],
'ppid': records[1],
'function': records[2],
'directory': records[3],
'command': records[4].split(US)[:-1]
}


def format_entry(entry):
""" Generate the desired fields for compilation database entries. """

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'])
if atoms['action'] <= Action.Compile:
for source in atoms['files']:
compiler = 'c++' if atoms['c++'] else 'cc'
flags = atoms['compile_options']
flags += ['-o', atoms['output']] if atoms['output'] else []
flags += ['-x', atoms['language']] if 'language' in atoms else []
flags += [elem
for arch in atoms.get('archs_seen', [])
for elem in ['-arch', arch]]
command = [compiler, '-c'] + flags + [source]
logging.debug('formated as: %s', command)
yield {
'directory': entry['directory'],
'command': encode(command),
'file': abspath(entry['directory'], source)
}


def is_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'^([^/]*/)*([^-]*-)*[mg](cc|\+\+)(-\d+(\.\d+){0,2})?$'),
re.compile(r'^([^/]*/)*([^-]*-)*clang(\+\+)?(-\d+(\.\d+){0,2})?$'),
re.compile(r'^([^/]*/)*llvm-g(cc|\+\+)$'),
]
executable = entry['command'][0]
return any((pattern.match(executable) for pattern in patterns))


def is_preload_disabled(platform):
""" Library-based interposition will fail silently if SIP is enabled,
so this should be detected. You can detect whether SIP is enabled on
Darwin by checking whether (1) there is a binary called 'csrutil' in
the path and, if so, (2) whether the output of executing 'csrutil status'
contains 'System Integrity Protection status: enabled'.
Same problem on linux when SELinux is enabled. The status query program
'sestatus' and the output when it's enabled 'SELinux status: enabled'. """

if platform == 'darwin':
pattern = re.compile(r'System Integrity Protection status:\s+enabled')
command = ['csrutil', 'status']
elif platform in {'linux', 'linux2'}:
pattern = re.compile(r'SELinux status:\s+enabled')
command = ['sestatus']
else:
return False

try:
lines = subprocess.check_output(command).decode('utf-8')
return any((pattern.match(line) for line in lines.splitlines()))
except:
return False


def entry_hash(entry):
""" Implement unique hash method for compilation database entries. """

# For faster lookup in set filename is reverted
filename = entry['file'][::-1]
# For faster lookup in set directory is reverted
directory = entry['directory'][::-1]
# On OS X the 'cc' and 'c++' compilers are wrappers for
# 'clang' therefore both call would be logged. To avoid
# this the hash does not contain the first word of the
# command.
command = ' '.join(decode(entry['command'])[1:])

return '<>'.join([filename, directory, command])


def create_parser():
""" Command line argument parser factory method. """

parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter)

parser.add_argument(
'--verbose', '-v',
action='count',
default=0,
help="""Enable verbose output from '%(prog)s'. A second and third
flag increases verbosity.""")
parser.add_argument(
'--cdb',
metavar='<file>',
default="compile_commands.json",
help="""The JSON compilation database.""")
group = parser.add_mutually_exclusive_group()
group.add_argument(
'--append',
action='store_true',
help="""Append new entries to existing compilation database.""")
group.add_argument(
'--disable-filter', '-n',
dest='raw_entries',
action='store_true',
help="""Intercepted child process creation calls (exec calls) are all
logged to the output. The output is not a compilation database.
This flag is for debug purposes.""")

advanced = parser.add_argument_group('advanced options')
advanced.add_argument(
'--override-compiler',
action='store_true',
help="""Always resort to the compiler wrapper even when better
intercept methods are available.""")
advanced.add_argument(
'--use-cc',
metavar='<path>',
dest='cc',
default='cc',
help="""When '%(prog)s' analyzes a project by interposing a compiler
wrapper, which executes a real compiler for compilation and
do other tasks (record the compiler invocation). Because of
this interposing, '%(prog)s' does not know what compiler your
project normally uses. Instead, it simply overrides the CC
environment variable, and guesses your default compiler.
If you need '%(prog)s' to use a specific compiler for
*compilation* then you can use this option to specify a path
to that compiler.""")
advanced.add_argument(
'--use-c++',
metavar='<path>',
dest='cxx',
default='c++',
help="""This is the same as "--use-cc" but for C++ code.""")

parser.add_argument(
dest='build',
nargs=argparse.REMAINDER,
help="""Command to run.""")

return parser
530 changes: 530 additions & 0 deletions clang/tools/scan-build-py/libscanbuild/report.py

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions clang/tools/scan-build-py/libscanbuild/resources/scanview.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
body { color:#000000; background-color:#ffffff }
body { font-family: Helvetica, sans-serif; font-size:9pt }
h1 { font-size: 14pt; }
h2 { font-size: 12pt; }
table { font-size:9pt }
table { border-spacing: 0px; border: 1px solid black }
th, table thead {
background-color:#eee; color:#666666;
font-weight: bold; cursor: default;
text-align:center;
font-weight: bold; font-family: Verdana;
white-space:nowrap;
}
.W { font-size:0px }
th, td { padding:5px; padding-left:8px; text-align:left }
td.SUMM_DESC { padding-left:12px }
td.DESC { white-space:pre }
td.Q { text-align:right }
td { text-align:left }
tbody.scrollContent { overflow:auto }

table.form_group {
background-color: #ccc;
border: 1px solid #333;
padding: 2px;
}

table.form_inner_group {
background-color: #ccc;
border: 1px solid #333;
padding: 0px;
}

table.form {
background-color: #999;
border: 1px solid #333;
padding: 2px;
}

td.form_label {
text-align: right;
vertical-align: top;
}
/* For one line entires */
td.form_clabel {
text-align: right;
vertical-align: center;
}
td.form_value {
text-align: left;
vertical-align: top;
}
td.form_submit {
text-align: right;
vertical-align: top;
}

h1.SubmitFail {
color: #f00;
}
h1.SubmitOk {
}
47 changes: 47 additions & 0 deletions clang/tools/scan-build-py/libscanbuild/resources/selectable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
function SetDisplay(RowClass, DisplayVal)
{
var Rows = document.getElementsByTagName("tr");
for ( var i = 0 ; i < Rows.length; ++i ) {
if (Rows[i].className == RowClass) {
Rows[i].style.display = DisplayVal;
}
}
}

function CopyCheckedStateToCheckButtons(SummaryCheckButton) {
var Inputs = document.getElementsByTagName("input");
for ( var i = 0 ; i < Inputs.length; ++i ) {
if (Inputs[i].type == "checkbox") {
if(Inputs[i] != SummaryCheckButton) {
Inputs[i].checked = SummaryCheckButton.checked;
Inputs[i].onclick();
}
}
}
}

function returnObjById( id ) {
if (document.getElementById)
var returnVar = document.getElementById(id);
else if (document.all)
var returnVar = document.all[id];
else if (document.layers)
var returnVar = document.layers[id];
return returnVar;
}

var NumUnchecked = 0;

function ToggleDisplay(CheckButton, ClassName) {
if (CheckButton.checked) {
SetDisplay(ClassName, "");
if (--NumUnchecked == 0) {
returnObjById("AllBugsCheck").checked = true;
}
}
else {
SetDisplay(ClassName, "none");
NumUnchecked++;
returnObjById("AllBugsCheck").checked = false;
}
}
492 changes: 492 additions & 0 deletions clang/tools/scan-build-py/libscanbuild/resources/sorttable.js

Large diffs are not rendered by default.

256 changes: 256 additions & 0 deletions clang/tools/scan-build-py/libscanbuild/runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
# -*- 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 is responsible to run the analyzer commands. """

import os
import os.path
import tempfile
import functools
import subprocess
import logging
from libscanbuild.command import classify_parameters, Action, classify_source
from libscanbuild.clang import get_arguments, get_version
from libscanbuild.shell import decode

__all__ = ['run']


def require(required):
""" Decorator for checking the required values in state.
It checks the required attributes in the passed state and stop when
any of those is missing. """

def decorator(function):
@functools.wraps(function)
def wrapper(*args, **kwargs):
for key in required:
if key not in args[0]:
raise KeyError(
'{0} not passed to {1}'.format(key, function.__name__))

return function(*args, **kwargs)

return wrapper

return decorator


@require(['command', 'directory', 'file', # an entry from compilation database
'clang', 'direct_args', # compiler name, and arguments from command
'output_dir', 'output_format', 'output_failures'])
def run(opts):
""" Entry point to run (or not) static analyzer against a single entry
of the compilation database.
This complex task is decomposed into smaller methods which are calling
each other in chain. If the analyzis is not possibe the given method
just return and break the chain.
The passed parameter is a python dictionary. Each method first check
that the needed parameters received. (This is done by the 'require'
decorator. It's like an 'assert' to check the contract between the
caller and the called method.) """

try:
command = opts.pop('command')
logging.debug("Run analyzer against '%s'", command)
opts.update(classify_parameters(decode(command)))

return action_check(opts)
except Exception:
logging.error("Problem occured during analyzis.", exc_info=1)
return None


@require(['report', 'directory', 'clang', 'output_dir', 'language', 'file',
'error_type', 'error_output', 'exit_code'])
def report_failure(opts):
""" Create report when analyzer failed.
The major report is the preprocessor output. The output filename generated
randomly. The compiler output also captured into '.stderr.txt' file.
And some more execution context also saved into '.info.txt' file. """

def extension(opts):
""" Generate preprocessor file extension. """

mapping = {'objective-c++': '.mii', 'objective-c': '.mi', 'c++': '.ii'}
return mapping.get(opts['language'], '.i')

def destination(opts):
""" Creates failures directory if not exits yet. """

name = os.path.join(opts['output_dir'], 'failures')
if not os.path.isdir(name):
os.makedirs(name)
return name

error = opts['error_type']
(handle, name) = tempfile.mkstemp(suffix=extension(opts),
prefix='clang_' + error + '_',
dir=destination(opts))
os.close(handle)
cwd = opts['directory']
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)

with open(name + '.info.txt', 'w') as handle:
handle.write(opts['file'] + os.linesep)
handle.write(error.title().replace('_', ' ') + os.linesep)
handle.write(' '.join(cmd) + os.linesep)
handle.write(' '.join(os.uname()) + os.linesep)
handle.write(get_version(cmd[0]))
handle.close()

with open(name + '.stderr.txt', 'w') as handle:
handle.writelines(opts['error_output'])
handle.close()

return {
'error_output': opts['error_output'],
'exit_code': opts['exit_code']
}


@require(['clang', 'analyze', 'directory', 'output'])
def run_analyzer(opts, continuation=report_failure):
""" It assembles the analysis command line and executes it. Capture the
output of the analysis and returns with it. If failure reports are
requested, it calls the continuation to generate it. """

cwd = opts['directory']
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,
universal_newlines=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
output = child.stdout.readlines()
child.stdout.close()
# do report details if it were asked
child.wait()
if opts.get('output_failures', False) and child.returncode:
error_type = 'crash' if child.returncode & 127 else 'other_error'
opts.update({
'error_type': error_type,
'error_output': output,
'exit_code': child.returncode
})
return continuation(opts)
return {'error_output': output, 'exit_code': child.returncode}


@require(['output_dir'])
def set_analyzer_output(opts, continuation=run_analyzer):
""" Create output file if was requested.
This plays a role only if .plist files are requested. """

if opts.get('output_format') in {'plist', 'plist-html'}:
with tempfile.NamedTemporaryFile(prefix='report-',
suffix='.plist',
delete=False,
dir=opts['output_dir']) as output:
opts.update({'output': ['-o', output.name]})
return continuation(opts)
else:
opts.update({'output': ['-o', opts['output_dir']]})
return continuation(opts)


@require(['file', 'directory', 'clang', 'direct_args', 'language',
'output_dir', 'output_format', 'output_failures'])
def create_commands(opts, continuation=set_analyzer_output):
""" Create command to run analyzer or failure report generation.
It generates commands (from compilation database entries) which contains
enough information to run the analyzer (and the crash report generation
if that was requested). """

common = []
if 'arch' in opts:
common.extend(['-arch', opts.pop('arch')])
common.extend(opts.pop('compile_options', []))
common.extend(['-x', opts['language']])
common.append(os.path.relpath(opts['file'], opts['directory']))

opts.update({
'analyze': ['--analyze'] + opts['direct_args'] + common,
'report': ['-fsyntax-only', '-E'] + common
})

return continuation(opts)


@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. """

accepteds = {
'c', 'c++', 'objective-c', 'objective-c++', 'c-cpp-output',
'c++-cpp-output', 'objective-c-cpp-output'
}

key = 'language'
language = opts[key] if key in opts else \
classify_source(opts['file'], opts['c++'])

if language is None:
logging.debug('skip analysis, language not known')
return None
elif language not in accepteds:
logging.debug('skip analysis, language not supported')
return None
else:
logging.debug('analysis, language: %s', language)
opts.update({key: language})
return continuation(opts)


@require([])
def arch_check(opts, continuation=language_check):
""" Do run analyzer through one of the given architectures. """

disableds = {'ppc', 'ppc64'}

key = 'archs_seen'
if key in opts:
# filter out disabled architectures and -arch switches
archs = [a for a in opts[key] if a not in disableds]

if not archs:
logging.debug('skip analysis, found not supported arch')
return None
else:
# There should be only one arch given (or the same multiple
# times). If there are multiple arch are given and are not
# the same, those should not change the pre-processing step.
# But that's the only pass we have before run the analyzer.
arch = archs.pop()
logging.debug('analysis, on arch: %s', arch)

opts.update({'arch': arch})
del opts[key]
return continuation(opts)
else:
logging.debug('analysis, on default arch')
return continuation(opts)


@require(['action'])
def action_check(opts, continuation=arch_check):
""" Continue analysis only if it compilation or link. """

if opts.pop('action') <= Action.Compile:
return continuation(opts)
else:
logging.debug('skip analysis, not compilation nor link')
return None
66 changes: 66 additions & 0 deletions clang/tools/scan-build-py/libscanbuild/shell.py
Original file line number Diff line number Diff line change
@@ -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(word):
""" 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 current in word:
if state == 0 and current in reserved:
return True
elif state == 0 and current == '\\':
state = 1
elif state == 1 and current in reserved | {'\\'}:
state = 0
elif state == 0 and current == '"':
state = 2
elif state == 2 and current == '"':
state = 0
elif state == 0 and current == "'":
state = 3
elif state == 3 and current == "'":
state = 0
return state != 0

def escape(word):
""" Do protect argument if that's needed. """

table = {'\\': '\\\\', '"': '\\"'}
escaped = ''.join([table.get(c, c) for c in word])

return '"' + escaped + '"' if needs_quote(word) 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)]
18 changes: 18 additions & 0 deletions clang/tools/scan-build-py/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# -*- 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 unittest

import tests.unit
import tests.functional.cases


def suite():
loader = unittest.TestLoader()
suite = unittest.TestSuite()
suite.addTests(loader.loadTestsFromModule(tests.unit))
suite.addTests(loader.loadTestsFromModule(tests.functional.cases))
return suite
Empty file.
71 changes: 71 additions & 0 deletions clang/tools/scan-build-py/tests/functional/cases/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# -*- 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 re
import os.path
import subprocess


def load_tests(loader, suite, pattern):
from . import test_from_cdb
suite.addTests(loader.loadTestsFromModule(test_from_cdb))
from . import test_from_cmd
suite.addTests(loader.loadTestsFromModule(test_from_cmd))
from . import test_create_cdb
suite.addTests(loader.loadTestsFromModule(test_create_cdb))
from . import test_exec_anatomy
suite.addTests(loader.loadTestsFromModule(test_exec_anatomy))
return suite


def make_args(target):
this_dir, _ = os.path.split(__file__)
path = os.path.normpath(os.path.join(this_dir, '..', 'src'))
return ['make', 'SRCDIR={}'.format(path), 'OBJDIR={}'.format(target), '-f',
os.path.join(path, 'build', 'Makefile')]


def silent_call(cmd, *args, **kwargs):
kwargs.update({'stdout': subprocess.PIPE, 'stderr': subprocess.STDOUT})
return subprocess.call(cmd, *args, **kwargs)


def silent_check_call(cmd, *args, **kwargs):
kwargs.update({'stdout': subprocess.PIPE, 'stderr': subprocess.STDOUT})
return subprocess.check_call(cmd, *args, **kwargs)


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(
exit_code, analyzer_cmd + build_cmd, None)
else:
return result


def create_empty_file(filename):
with open(filename, 'a') as handle:
pass
191 changes: 191 additions & 0 deletions clang/tools/scan-build-py/tests/functional/cases/test_create_cdb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
# The LLVM Compiler Infrastructure
#
# This file is distributed under the University of Illinois Open Source
# License. See LICENSE.TXT for details.

from ...unit import fixtures
from . import make_args, silent_check_call, silent_call, create_empty_file
import unittest

import os.path
import json


class CompilationDatabaseTest(unittest.TestCase):
@staticmethod
def run_intercept(tmpdir, args):
result = os.path.join(tmpdir, 'cdb.json')
make = make_args(tmpdir) + args
silent_check_call(
['intercept-build', '--cdb', result] + make)
return result

@staticmethod
def count_entries(filename):
with open(filename, 'r') as handler:
content = json.load(handler)
return len(content)

def test_successful_build(self):
with fixtures.TempDir() as tmpdir:
result = self.run_intercept(tmpdir, ['build_regular'])
self.assertTrue(os.path.isfile(result))
self.assertEqual(5, self.count_entries(result))

def test_successful_build_with_wrapper(self):
with fixtures.TempDir() as tmpdir:
result = os.path.join(tmpdir, 'cdb.json')
make = make_args(tmpdir) + ['build_regular']
silent_check_call(['intercept-build', '--cdb', result,
'--override-compiler'] + make)
self.assertTrue(os.path.isfile(result))
self.assertEqual(5, self.count_entries(result))

@unittest.skipIf(os.getenv('TRAVIS'), 'ubuntu make return -11')
def test_successful_build_parallel(self):
with fixtures.TempDir() as tmpdir:
result = self.run_intercept(tmpdir, ['-j', '4', 'build_regular'])
self.assertTrue(os.path.isfile(result))
self.assertEqual(5, self.count_entries(result))

@unittest.skipIf(os.getenv('TRAVIS'), 'ubuntu env remove clang from path')
def test_successful_build_on_empty_env(self):
with fixtures.TempDir() as tmpdir:
result = os.path.join(tmpdir, 'cdb.json')
make = make_args(tmpdir) + ['CC=clang', 'build_regular']
silent_check_call(['intercept-build', '--cdb', result,
'env', '-'] + make)
self.assertTrue(os.path.isfile(result))
self.assertEqual(5, self.count_entries(result))

def test_successful_build_all_in_one(self):
with fixtures.TempDir() as tmpdir:
result = self.run_intercept(tmpdir, ['build_all_in_one'])
self.assertTrue(os.path.isfile(result))
self.assertEqual(5, self.count_entries(result))

def test_not_successful_build(self):
with fixtures.TempDir() as tmpdir:
result = os.path.join(tmpdir, 'cdb.json')
make = make_args(tmpdir) + ['build_broken']
silent_call(
['intercept-build', '--cdb', result] + make)
self.assertTrue(os.path.isfile(result))
self.assertEqual(2, self.count_entries(result))


class ExitCodeTest(unittest.TestCase):
@staticmethod
def run_intercept(tmpdir, target):
result = os.path.join(tmpdir, 'cdb.json')
make = make_args(tmpdir) + [target]
return silent_call(
['intercept-build', '--cdb', result] + make)

def test_successful_build(self):
with fixtures.TempDir() as tmpdir:
exitcode = self.run_intercept(tmpdir, 'build_clean')
self.assertFalse(exitcode)

def test_not_successful_build(self):
with fixtures.TempDir() as tmpdir:
exitcode = self.run_intercept(tmpdir, 'build_broken')
self.assertTrue(exitcode)


class ResumeFeatureTest(unittest.TestCase):
@staticmethod
def run_intercept(tmpdir, target, args):
result = os.path.join(tmpdir, 'cdb.json')
make = make_args(tmpdir) + [target]
silent_check_call(
['intercept-build', '--cdb', result] + args + make)
return result

@staticmethod
def count_entries(filename):
with open(filename, 'r') as handler:
content = json.load(handler)
return len(content)

def test_overwrite_existing_cdb(self):
with fixtures.TempDir() as tmpdir:
result = self.run_intercept(tmpdir, 'build_clean', [])
self.assertTrue(os.path.isfile(result))
result = self.run_intercept(tmpdir, 'build_regular', [])
self.assertTrue(os.path.isfile(result))
self.assertEqual(2, self.count_entries(result))

def test_append_to_existing_cdb(self):
with fixtures.TempDir() as tmpdir:
result = self.run_intercept(tmpdir, 'build_clean', [])
self.assertTrue(os.path.isfile(result))
result = self.run_intercept(tmpdir, 'build_regular', ['--append'])
self.assertTrue(os.path.isfile(result))
self.assertEqual(5, self.count_entries(result))


class ResultFormatingTest(unittest.TestCase):
@staticmethod
def run_intercept(tmpdir, command):
result = os.path.join(tmpdir, 'cdb.json')
silent_check_call(
['intercept-build', '--cdb', result] + command,
cwd=tmpdir)
with open(result, 'r') as handler:
content = json.load(handler)
return content

def assert_creates_number_of_entries(self, command, count):
with fixtures.TempDir() as tmpdir:
filename = os.path.join(tmpdir, 'test.c')
create_empty_file(filename)
command.append(filename)
cmd = ['sh', '-c', ' '.join(command)]
cdb = self.run_intercept(tmpdir, cmd)
self.assertEqual(count, len(cdb))

def test_filter_preprocessor_only_calls(self):
self.assert_creates_number_of_entries(['cc', '-c'], 1)
self.assert_creates_number_of_entries(['cc', '-c', '-E'], 0)
self.assert_creates_number_of_entries(['cc', '-c', '-M'], 0)
self.assert_creates_number_of_entries(['cc', '-c', '-MM'], 0)

def assert_command_creates_entry(self, command, expected):
with fixtures.TempDir() as tmpdir:
filename = os.path.join(tmpdir, command[-1])
create_empty_file(filename)
cmd = ['sh', '-c', ' '.join(command)]
cdb = self.run_intercept(tmpdir, cmd)
self.assertEqual(' '.join(expected), cdb[0]['command'])

def test_filter_preprocessor_flags(self):
self.assert_command_creates_entry(
['cc', '-c', '-MD', 'test.c'],
['cc', '-c', 'test.c'])
self.assert_command_creates_entry(
['cc', '-c', '-MMD', 'test.c'],
['cc', '-c', 'test.c'])
self.assert_command_creates_entry(
['cc', '-c', '-MD', '-MF', 'test.d', 'test.c'],
['cc', '-c', 'test.c'])

def test_pass_language_flag(self):
self.assert_command_creates_entry(
['cc', '-c', '-x', 'c', 'test.c'],
['cc', '-c', '-x', 'c', 'test.c'])
self.assert_command_creates_entry(
['cc', '-c', 'test.c'],
['cc', '-c', 'test.c'])

def test_pass_arch_flags(self):
self.assert_command_creates_entry(
['clang', '-c', 'test.c'],
['cc', '-c', 'test.c'])
self.assert_command_creates_entry(
['clang', '-c', '-arch', 'i386', 'test.c'],
['cc', '-c', '-arch', 'i386', 'test.c'])
self.assert_command_creates_entry(
['clang', '-c', '-arch', 'i386', '-arch', 'armv7l', 'test.c'],
['cc', '-c', '-arch', 'i386', '-arch', 'armv7l', 'test.c'])
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
# The LLVM Compiler Infrastructure
#
# This file is distributed under the University of Illinois Open Source
# License. See LICENSE.TXT for details.

from ...unit import fixtures
import unittest

import os.path
import subprocess
import json


def run(source_dir, target_dir):
def execute(cmd):
return subprocess.check_call(cmd,
cwd=target_dir,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)

execute(['cmake', source_dir])
execute(['make'])

result_file = os.path.join(target_dir, 'result.json')
expected_file = os.path.join(target_dir, 'expected.json')
execute(['intercept-build', '--cdb', result_file, './exec',
expected_file])
return (expected_file, result_file)


class ExecAnatomyTest(unittest.TestCase):
def assertEqualJson(self, expected, result):
def read_json(filename):
with open(filename) as handler:
return json.load(handler)

lhs = read_json(expected)
rhs = read_json(result)
for item in lhs:
self.assertTrue(rhs.count(item))
for item in rhs:
self.assertTrue(lhs.count(item))

def test_all_exec_calls(self):
this_dir, _ = os.path.split(__file__)
source_dir = os.path.normpath(os.path.join(this_dir, '..', 'exec'))
with fixtures.TempDir() as tmp_dir:
expected, result = run(source_dir, tmp_dir)
self.assertEqualJson(expected, result)
183 changes: 183 additions & 0 deletions clang/tools/scan-build-py/tests/functional/cases/test_from_cdb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
# The LLVM Compiler Infrastructure
#
# This file is distributed under the University of Illinois Open Source
# License. See LICENSE.TXT for details.

from ...unit import fixtures
from . import call_and_report
import unittest

import os.path
import string
import subprocess
import glob


def prepare_cdb(name, target_dir):
target_file = 'build_{0}.json'.format(name)
this_dir, _ = os.path.split(__file__)
path = os.path.normpath(os.path.join(this_dir, '..', 'src'))
source_dir = os.path.join(path, 'compilation_database')
source_file = os.path.join(source_dir, target_file + '.in')
target_file = os.path.join(target_dir, 'compile_commands.json')
with open(source_file, 'r') as in_handle:
with open(target_file, 'w') as out_handle:
for line in in_handle:
temp = string.Template(line)
out_handle.write(temp.substitute(path=path))
return target_file


def run_analyzer(directory, cdb, args):
cmd = ['analyze-build', '--cdb', cdb, '--output', directory] \
+ args
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)
exit_code, reportdir = run_analyzer(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)
exit_code, reportdir = run_analyzer(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)
exit_code, reportdir = run_analyzer(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)
exit_code, __ = run_analyzer(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)
exit_code, __ = run_analyzer(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)
exit_code, __ = run_analyzer(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)
exit_code, __ = run_analyzer(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)
exit_code, __ = run_analyzer(
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)
exit_code, __ = run_analyzer(
tmpdir, cdb, ['--status-bugs', '--plist'])
self.assertFalse(exit_code)


class OutputFormatTest(unittest.TestCase):
@staticmethod
def get_html_count(directory):
return len(glob.glob(os.path.join(directory, 'report-*.html')))

@staticmethod
def get_plist_count(directory):
return len(glob.glob(os.path.join(directory, 'report-*.plist')))

def test_default_creates_html_report(self):
with fixtures.TempDir() as tmpdir:
cdb = prepare_cdb('regular', tmpdir)
exit_code, reportdir = run_analyzer(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)
exit_code, reportdir = run_analyzer(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)
exit_code, reportdir = run_analyzer(tmpdir, cdb, ['--plist'])
self.assertFalse(
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)
exit_code, reportdir = run_analyzer(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)
exit_code, reportdir = run_analyzer(
tmpdir, cdb, ['--no-failure-reports'])
self.assertFalse(
os.path.isdir(os.path.join(reportdir, 'failures')))


class TitleTest(unittest.TestCase):
def assertTitleEqual(self, directory, expected):
import re
patterns = [
re.compile(r'<title>(?P<page>.*)</title>'),
re.compile(r'<h1>(?P<head>.*)</h1>')
]
result = dict()

index = os.path.join(directory, 'index.html')
with open(index, 'r') as handler:
for line in handler.readlines():
for regex in patterns:
match = regex.match(line.strip())
if match:
result.update(match.groupdict())
break
self.assertEqual(result['page'], result['head'])
self.assertEqual(result['page'], expected)

def test_default_title_in_report(self):
with fixtures.TempDir() as tmpdir:
cdb = prepare_cdb('broken', tmpdir)
exit_code, reportdir = run_analyzer(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)
exit_code, reportdir = run_analyzer(
tmpdir, cdb, ['--html-title', 'this is the title'])
self.assertTitleEqual(reportdir, 'this is the title')
118 changes: 118 additions & 0 deletions clang/tools/scan-build-py/tests/functional/cases/test_from_cmd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
# The LLVM Compiler Infrastructure
#
# This file is distributed under the University of Illinois Open Source
# License. See LICENSE.TXT for details.

from ...unit import fixtures
from . import make_args, check_call_and_report, create_empty_file
import unittest

import os
import os.path
import glob


class OutputDirectoryTest(unittest.TestCase):

@staticmethod
def run_analyzer(outdir, args, cmd):
return check_call_and_report(
['scan-build', '--intercept-first', '-o', outdir] + args,
cmd)

def test_regular_keeps_report_dir(self):
with fixtures.TempDir() as tmpdir:
make = make_args(tmpdir) + ['build_regular']
outdir = self.run_analyzer(tmpdir, [], make)
self.assertTrue(os.path.isdir(outdir))

def test_clear_deletes_report_dir(self):
with fixtures.TempDir() as tmpdir:
make = make_args(tmpdir) + ['build_clean']
outdir = self.run_analyzer(tmpdir, [], make)
self.assertFalse(os.path.isdir(outdir))

def test_clear_keeps_report_dir_when_asked(self):
with fixtures.TempDir() as tmpdir:
make = make_args(tmpdir) + ['build_clean']
outdir = self.run_analyzer(tmpdir, ['--keep-empty'], make)
self.assertTrue(os.path.isdir(outdir))


class RunAnalyzerTest(unittest.TestCase):

@staticmethod
def get_plist_count(directory):
return len(glob.glob(os.path.join(directory, 'report-*.plist')))

def test_interposition_works(self):
with fixtures.TempDir() as tmpdir:
make = make_args(tmpdir) + ['build_regular']
outdir = check_call_and_report(
['scan-build', '--plist', '-o', tmpdir, '--override-compiler'],
make)

self.assertTrue(os.path.isdir(outdir))
self.assertEqual(self.get_plist_count(outdir), 5)

def test_intercept_wrapper_works(self):
with fixtures.TempDir() as tmpdir:
make = make_args(tmpdir) + ['build_regular']
outdir = check_call_and_report(
['scan-build', '--plist', '-o', tmpdir, '--intercept-first',
'--override-compiler'],
make)

self.assertTrue(os.path.isdir(outdir))
self.assertEqual(self.get_plist_count(outdir), 5)

def test_intercept_library_works(self):
with fixtures.TempDir() as tmpdir:
make = make_args(tmpdir) + ['build_regular']
outdir = check_call_and_report(
['scan-build', '--plist', '-o', tmpdir, '--intercept-first'],
make)

self.assertTrue(os.path.isdir(outdir))
self.assertEqual(self.get_plist_count(outdir), 5)

@staticmethod
def compile_empty_source_file(target_dir, is_cxx):
compiler = '$CXX' if is_cxx else '$CC'
src_file_name = 'test.cxx' if is_cxx else 'test.c'
src_file = os.path.join(target_dir, src_file_name)
obj_file = os.path.join(target_dir, 'test.o')
create_empty_file(src_file)
command = ' '.join([compiler, '-c', src_file, '-o', obj_file])
return ['sh', '-c', command]

def test_interposition_cc_works(self):
with fixtures.TempDir() as tmpdir:
outdir = check_call_and_report(
['scan-build', '--plist', '-o', tmpdir, '--override-compiler'],
self.compile_empty_source_file(tmpdir, False))
self.assertEqual(self.get_plist_count(outdir), 1)

def test_interposition_cxx_works(self):
with fixtures.TempDir() as tmpdir:
outdir = check_call_and_report(
['scan-build', '--plist', '-o', tmpdir, '--override-compiler'],
self.compile_empty_source_file(tmpdir, True))
self.assertEqual(self.get_plist_count(outdir), 1)

def test_intercept_cc_works(self):
with fixtures.TempDir() as tmpdir:
outdir = check_call_and_report(
['scan-build', '--plist', '-o', tmpdir, '--override-compiler',
'--intercept-first'],
self.compile_empty_source_file(tmpdir, False))
self.assertEqual(self.get_plist_count(outdir), 1)

def test_intercept_cxx_works(self):
with fixtures.TempDir() as tmpdir:
outdir = check_call_and_report(
['scan-build', '--plist', '-o', tmpdir, '--override-compiler',
'--intercept-first'],
self.compile_empty_source_file(tmpdir, True))
self.assertEqual(self.get_plist_count(outdir), 1)
32 changes: 32 additions & 0 deletions clang/tools/scan-build-py/tests/functional/exec/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
project(exec C)

cmake_minimum_required(VERSION 2.8)

include(CheckCCompilerFlag)
check_c_compiler_flag("-std=c99" C99_SUPPORTED)
if (C99_SUPPORTED)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99")
endif()

include(CheckFunctionExists)
include(CheckSymbolExists)

add_definitions(-D_GNU_SOURCE)
list(APPEND CMAKE_REQUIRED_DEFINITIONS -D_GNU_SOURCE)

check_function_exists(execve HAVE_EXECVE)
check_function_exists(execv HAVE_EXECV)
check_function_exists(execvpe HAVE_EXECVPE)
check_function_exists(execvp HAVE_EXECVP)
check_function_exists(execvP HAVE_EXECVP2)
check_function_exists(exect HAVE_EXECT)
check_function_exists(execl HAVE_EXECL)
check_function_exists(execlp HAVE_EXECLP)
check_function_exists(execle HAVE_EXECLE)
check_function_exists(posix_spawn HAVE_POSIX_SPAWN)
check_function_exists(posix_spawnp HAVE_POSIX_SPAWNP)

configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h)
include_directories(${CMAKE_CURRENT_BINARY_DIR})

add_executable(exec main.c)
20 changes: 20 additions & 0 deletions clang/tools/scan-build-py/tests/functional/exec/config.h.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* -*- coding: utf-8 -*-
// The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
*/

#pragma once

#cmakedefine HAVE_EXECVE
#cmakedefine HAVE_EXECV
#cmakedefine HAVE_EXECVPE
#cmakedefine HAVE_EXECVP
#cmakedefine HAVE_EXECVP2
#cmakedefine HAVE_EXECT
#cmakedefine HAVE_EXECL
#cmakedefine HAVE_EXECLP
#cmakedefine HAVE_EXECLE
#cmakedefine HAVE_POSIX_SPAWN
#cmakedefine HAVE_POSIX_SPAWNP
307 changes: 307 additions & 0 deletions clang/tools/scan-build-py/tests/functional/exec/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
/* -*- coding: utf-8 -*-
// The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
*/

#include "config.h"

#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <paths.h>

#if defined HAVE_POSIX_SPAWN || defined HAVE_POSIX_SPAWNP
#include <spawn.h>
#endif

// ..:: environment access fixer - begin ::..
#ifdef HAVE_NSGETENVIRON
#include <crt_externs.h>
#else
extern char **environ;
#endif

char **get_environ() {
#ifdef HAVE_NSGETENVIRON
return *_NSGetEnviron();
#else
return environ;
#endif
}
// ..:: environment access fixer - end ::..

// ..:: test fixtures - begin ::..
static char const *cwd = NULL;
static FILE *fd = NULL;
static int need_comma = 0;

void expected_out_open(const char *expected) {
cwd = getcwd(NULL, 0);
fd = fopen(expected, "w");
if (!fd) {
perror("fopen");
exit(EXIT_FAILURE);
}
fprintf(fd, "[\n");
need_comma = 0;
}

void expected_out_close() {
fprintf(fd, "]\n");
fclose(fd);
fd = NULL;

free((void *)cwd);
cwd = NULL;
}

void expected_out(const char *file) {
if (need_comma)
fprintf(fd, ",\n");
else
need_comma = 1;

fprintf(fd, "{\n");
fprintf(fd, " \"directory\": \"%s\",\n", cwd);
fprintf(fd, " \"command\": \"cc -c %s\",\n", file);
fprintf(fd, " \"file\": \"%s/%s\"\n", cwd, file);
fprintf(fd, "}\n");
}

void create_source(char *file) {
FILE *fd = fopen(file, "w");
if (!fd) {
perror("fopen");
exit(EXIT_FAILURE);
}
fprintf(fd, "typedef int score;\n");
fclose(fd);
}

typedef void (*exec_fun)();

void wait_for(pid_t child) {
int status;
if (-1 == waitpid(child, &status, 0)) {
perror("wait");
exit(EXIT_FAILURE);
}
if (WIFEXITED(status) ? WEXITSTATUS(status) : EXIT_FAILURE) {
fprintf(stderr, "children process has non zero exit code\n");
exit(EXIT_FAILURE);
}
}

#define FORK(FUNC) \
{ \
pid_t child = fork(); \
if (-1 == child) { \
perror("fork"); \
exit(EXIT_FAILURE); \
} else if (0 == child) { \
FUNC fprintf(stderr, "children process failed to exec\n"); \
exit(EXIT_FAILURE); \
} else { \
wait_for(child); \
} \
}
// ..:: test fixtures - end ::..

#ifdef HAVE_EXECV
void call_execv() {
char *const file = "execv.c";
char *const compiler = "/usr/bin/cc";
char *const argv[] = {"cc", "-c", file, 0};

expected_out(file);
create_source(file);

FORK(execv(compiler, argv);)
}
#endif

#ifdef HAVE_EXECVE
void call_execve() {
char *const file = "execve.c";
char *const compiler = "/usr/bin/cc";
char *const argv[] = {compiler, "-c", file, 0};
char *const envp[] = {"THIS=THAT", 0};

expected_out(file);
create_source(file);

FORK(execve(compiler, argv, envp);)
}
#endif

#ifdef HAVE_EXECVP
void call_execvp() {
char *const file = "execvp.c";
char *const compiler = "cc";
char *const argv[] = {compiler, "-c", file, 0};

expected_out(file);
create_source(file);

FORK(execvp(compiler, argv);)
}
#endif

#ifdef HAVE_EXECVP2
void call_execvP() {
char *const file = "execv_p.c";
char *const compiler = "cc";
char *const argv[] = {compiler, "-c", file, 0};

expected_out(file);
create_source(file);

FORK(execvP(compiler, _PATH_DEFPATH, argv);)
}
#endif

#ifdef HAVE_EXECVPE
void call_execvpe() {
char *const file = "execvpe.c";
char *const compiler = "cc";
char *const argv[] = {"/usr/bin/cc", "-c", file, 0};
char *const envp[] = {"THIS=THAT", 0};

expected_out(file);
create_source(file);

FORK(execvpe(compiler, argv, envp);)
}
#endif

#ifdef HAVE_EXECT
void call_exect() {
char *const file = "exect.c";
char *const compiler = "/usr/bin/cc";
char *const argv[] = {compiler, "-c", file, 0};
char *const envp[] = {"THIS=THAT", 0};

expected_out(file);
create_source(file);

FORK(exect(compiler, argv, envp);)
}
#endif

#ifdef HAVE_EXECL
void call_execl() {
char *const file = "execl.c";
char *const compiler = "/usr/bin/cc";

expected_out(file);
create_source(file);

FORK(execl(compiler, "cc", "-c", file, (char *)0);)
}
#endif

#ifdef HAVE_EXECLP
void call_execlp() {
char *const file = "execlp.c";
char *const compiler = "cc";

expected_out(file);
create_source(file);

FORK(execlp(compiler, compiler, "-c", file, (char *)0);)
}
#endif

#ifdef HAVE_EXECLE
void call_execle() {
char *const file = "execle.c";
char *const compiler = "/usr/bin/cc";
char *const envp[] = {"THIS=THAT", 0};

expected_out(file);
create_source(file);

FORK(execle(compiler, compiler, "-c", file, (char *)0, envp);)
}
#endif

#ifdef HAVE_POSIX_SPAWN
void call_posix_spawn() {
char *const file = "posix_spawn.c";
char *const compiler = "cc";
char *const argv[] = {compiler, "-c", file, 0};

expected_out(file);
create_source(file);

pid_t child;
if (0 != posix_spawn(&child, "/usr/bin/cc", 0, 0, argv, get_environ())) {
perror("posix_spawn");
exit(EXIT_FAILURE);
}
wait_for(child);
}
#endif

#ifdef HAVE_POSIX_SPAWNP
void call_posix_spawnp() {
char *const file = "posix_spawnp.c";
char *const compiler = "cc";
char *const argv[] = {compiler, "-c", file, 0};

expected_out(file);
create_source(file);

pid_t child;
if (0 != posix_spawnp(&child, "cc", 0, 0, argv, get_environ())) {
perror("posix_spawnp");
exit(EXIT_FAILURE);
}
wait_for(child);
}
#endif

int main(int argc, char *const argv[]) {
if (argc != 2)
exit(EXIT_FAILURE);

expected_out_open(argv[1]);
#ifdef HAVE_EXECV
call_execv();
#endif
#ifdef HAVE_EXECVE
call_execve();
#endif
#ifdef HAVE_EXECVP
call_execvp();
#endif
#ifdef HAVE_EXECVP2
call_execvP();
#endif
#ifdef HAVE_EXECVPE
call_execvpe();
#endif
#ifdef HAVE_EXECT
call_exect();
#endif
#ifdef HAVE_EXECL
call_execl();
#endif
#ifdef HAVE_EXECLP
call_execlp();
#endif
#ifdef HAVE_EXECLE
call_execle();
#endif
#ifdef HAVE_POSIX_SPAWN
call_posix_spawn();
#endif
#ifdef HAVE_POSIX_SPAWNP
call_posix_spawnp();
#endif
expected_out_close();
return 0;
}
6 changes: 6 additions & 0 deletions clang/tools/scan-build-py/tests/functional/src/broken-one.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#include <notexisting.hpp>

int value(int in)
{
return 2 * in;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
int test() { ;
42 changes: 42 additions & 0 deletions clang/tools/scan-build-py/tests/functional/src/build/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
SRCDIR := ..
OBJDIR := .

CFLAGS = -Wall -DDEBUG -Dvariable="value with space" -I $(SRCDIR)/include
LDFLAGS =
PROGRAM = $(OBJDIR)/prg

$(OBJDIR)/main.o: $(SRCDIR)/main.c
$(CC) $(CFLAGS) -c -o $@ $(SRCDIR)/main.c

$(OBJDIR)/clean-one.o: $(SRCDIR)/clean-one.c
$(CC) $(CFLAGS) -c -o $@ $(SRCDIR)/clean-one.c

$(OBJDIR)/clean-two.o: $(SRCDIR)/clean-two.c
$(CC) $(CFLAGS) -c -o $@ $(SRCDIR)/clean-two.c

$(OBJDIR)/emit-one.o: $(SRCDIR)/emit-one.c
$(CC) $(CFLAGS) -c -o $@ $(SRCDIR)/emit-one.c

$(OBJDIR)/emit-two.o: $(SRCDIR)/emit-two.c
$(CC) $(CFLAGS) -c -o $@ $(SRCDIR)/emit-two.c

$(OBJDIR)/broken-one.o: $(SRCDIR)/broken-one.c
$(CC) $(CFLAGS) -c -o $@ $(SRCDIR)/broken-one.c

$(OBJDIR)/broken-two.o: $(SRCDIR)/broken-two.c
$(CC) $(CFLAGS) -c -o $@ $(SRCDIR)/broken-two.c

$(PROGRAM): $(OBJDIR)/main.o $(OBJDIR)/clean-one.o $(OBJDIR)/clean-two.o $(OBJDIR)/emit-one.o $(OBJDIR)/emit-two.o
$(CC) $(LDFLAGS) -o $@ $(OBJDIR)/main.o $(OBJDIR)/clean-one.o $(OBJDIR)/clean-two.o $(OBJDIR)/emit-one.o $(OBJDIR)/emit-two.o

build_regular: $(PROGRAM)

build_clean: $(OBJDIR)/main.o $(OBJDIR)/clean-one.o $(OBJDIR)/clean-two.o

build_broken: $(OBJDIR)/main.o $(OBJDIR)/broken-one.o $(OBJDIR)/broken-two.o

build_all_in_one: $(SRCDIR)/main.c $(SRCDIR)/clean-one.c $(SRCDIR)/clean-two.c $(SRCDIR)/emit-one.c $(SRCDIR)/emit-two.c
$(CC) $(CFLAGS) $(LDFLAGS) -o $(PROGRAM) $(SRCDIR)/main.c $(SRCDIR)/clean-one.c $(SRCDIR)/clean-two.c $(SRCDIR)/emit-one.c $(SRCDIR)/emit-two.c

clean:
rm -f $(PROGRAM) $(OBJDIR)/*.o
13 changes: 13 additions & 0 deletions clang/tools/scan-build-py/tests/functional/src/clean-one.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#include <clean-one.h>

int do_nothing_loop()
{
int i = 32;
int idx = 0;

for (idx = i; idx > 0; --idx)
{
i += idx;
}
return i;
}
11 changes: 11 additions & 0 deletions clang/tools/scan-build-py/tests/functional/src/clean-two.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#include <clean-one.h>

#include <stdlib.h>

unsigned int another_method()
{
unsigned int const size = do_nothing_loop();
unsigned int const square = size * size;

return square;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
[
{
"directory": "${path}",
"command": "g++ -c -o main.o main.c -Wall -DDEBUG -Dvariable=value",
"file": "${path}/main.c"
}
,
{
"directory": "${path}",
"command": "cc -c -o broken-one.o broken-one.c -Wall -DDEBUG \"-Dvariable=value with space\"",
"file": "${path}/broken-one.c"
}
,
{
"directory": "${path}",
"command": "g++ -c -o broken-two.o broken-two.c -Wall -DDEBUG -Dvariable=value",
"file": "${path}/broken-two.c"
}
,
{
"directory": "${path}",
"command": "cc -c -o clean-one.o clean-one.c -Wall -DDEBUG \"-Dvariable=value with space\" -Iinclude",
"file": "${path}/clean-one.c"
}
,
{
"directory": "${path}",
"command": "g++ -c -o clean-two.o clean-two.c -Wall -DDEBUG -Dvariable=value -I ./include",
"file": "${path}/clean-two.c"
}
,
{
"directory": "${path}",
"command": "cc -c -o emit-one.o emit-one.c -Wall -DDEBUG \"-Dvariable=value with space\"",
"file": "${path}/emit-one.c"
}
,
{
"directory": "${path}",
"command": "g++ -c -o emit-two.o emit-two.c -Wall -DDEBUG -Dvariable=value",
"file": "${path}/emit-two.c"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[
{
"directory": "${path}",
"command": "g++ -c -o main.o main.c -Wall -DDEBUG -Dvariable=value",
"file": "${path}/main.c"
}
,
{
"directory": "${path}",
"command": "cc -c -o clean-one.o clean-one.c -Wall -DDEBUG \"-Dvariable=value with space\" -Iinclude",
"file": "${path}/clean-one.c"
}
,
{
"directory": "${path}",
"command": "g++ -c -o clean-two.o clean-two.c -Wall -DDEBUG -Dvariable=value -I ./include",
"file": "${path}/clean-two.c"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[
{
"directory": "${path}",
"command": "g++ -c -o main.o main.c -Wall -DDEBUG -Dvariable=value",
"file": "${path}/main.c"
}
,
{
"directory": "${path}",
"command": "cc -c -o clean-one.o clean-one.c -Wall -DDEBUG \"-Dvariable=value with space\" -Iinclude",
"file": "${path}/clean-one.c"
}
,
{
"directory": "${path}",
"command": "g++ -c -o clean-two.o clean-two.c -Wall -DDEBUG -Dvariable=value -I ./include",
"file": "${path}/clean-two.c"
}
,
{
"directory": "${path}",
"command": "cc -c -o emit-one.o emit-one.c -Wall -DDEBUG \"-Dvariable=value with space\"",
"file": "${path}/emit-one.c"
}
,
{
"directory": "${path}",
"command": "g++ -c -o emit-two.o emit-two.c -Wall -DDEBUG -Dvariable=value",
"file": "${path}/emit-two.c"
}
]
23 changes: 23 additions & 0 deletions clang/tools/scan-build-py/tests/functional/src/emit-one.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#include <assert.h>

int div(int numerator, int denominator)
{
return numerator / denominator;
}

void div_test()
{
int i = 0;
for (i = 0; i < 2; ++i)
assert(div(2 * i, i) == 2);
}

int do_nothing()
{
unsigned int i = 0;

int k = 100;
int j = k + 1;

return j;
}
13 changes: 13 additions & 0 deletions clang/tools/scan-build-py/tests/functional/src/emit-two.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

int bad_guy(int * i)
{
*i = 9;
return *i;
}

void bad_guy_test()
{
int * ptr = 0;

bad_guy(ptr);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#ifndef CLEAN_ONE_H
#define CLEAN_ONE_H

int do_nothing_loop();

#endif
4 changes: 4 additions & 0 deletions clang/tools/scan-build-py/tests/functional/src/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
int main()
{
return 0;
}
24 changes: 24 additions & 0 deletions clang/tools/scan-build-py/tests/unit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# The LLVM Compiler Infrastructure
#
# This file is distributed under the University of Illinois Open Source
# License. See LICENSE.TXT for details.

from . import test_command
from . import test_clang
from . import test_runner
from . import test_report
from . import test_analyze
from . import test_intercept
from . import test_shell


def load_tests(loader, suite, pattern):
suite.addTests(loader.loadTestsFromModule(test_command))
suite.addTests(loader.loadTestsFromModule(test_clang))
suite.addTests(loader.loadTestsFromModule(test_runner))
suite.addTests(loader.loadTestsFromModule(test_report))
suite.addTests(loader.loadTestsFromModule(test_analyze))
suite.addTests(loader.loadTestsFromModule(test_intercept))
suite.addTests(loader.loadTestsFromModule(test_shell))
return suite
40 changes: 40 additions & 0 deletions clang/tools/scan-build-py/tests/unit/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# -*- 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 contextlib
import tempfile
import shutil
import unittest


class Spy(object):
def __init__(self):
self.arg = None
self.success = 0

def call(self, params):
self.arg = params
return self.success


@contextlib.contextmanager
def TempDir():
name = tempfile.mkdtemp(prefix='scan-build-test-')
try:
yield name
finally:
shutil.rmtree(name)


class TestCase(unittest.TestCase):
def assertIn(self, element, collection):
found = False
for it in collection:
if element == it:
found = True

self.assertTrue(found, '{0} does not have {1}'.format(collection,
element))
8 changes: 8 additions & 0 deletions clang/tools/scan-build-py/tests/unit/test_analyze.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# -*- 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.analyze as sut
from . import fixtures
41 changes: 41 additions & 0 deletions clang/tools/scan-build-py/tests/unit/test_clang.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# -*- 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.clang as sut
from . import fixtures
import os.path


class GetClangArgumentsTest(fixtures.TestCase):
def test_get_clang_arguments(self):
with fixtures.TempDir() as tmpdir:
filename = os.path.join(tmpdir, 'test.c')
with open(filename, 'w') as handle:
handle.write('')

result = sut.get_arguments(
['clang', '-c', filename, '-DNDEBUG', '-Dvar="this is it"'],
tmpdir)

self.assertIn('NDEBUG', result)
self.assertIn('var="this is it"', result)

def test_get_clang_arguments_fails(self):
self.assertRaises(
Exception, sut.get_arguments,
['clang', '-###', '-fsyntax-only', '-x', 'c', 'notexist.c'], '.')


class GetCheckersTest(fixtures.TestCase):
def test_get_checkers(self):
# this test is only to see is not crashing
result = sut.get_checkers('clang', [])
self.assertTrue(len(result))

def test_get_active_checkers(self):
# this test is only to see is not crashing
result = sut.get_active_checkers('clang', [])
self.assertTrue(len(result))
193 changes: 193 additions & 0 deletions clang/tools/scan-build-py/tests/unit/test_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# -*- 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.command as sut
from . import fixtures
import unittest


class ParseTest(unittest.TestCase):

def test_action(self):
def test(expected, cmd):
opts = sut.classify_parameters(cmd)
self.assertEqual(expected, opts['action'])

Link = sut.Action.Link
test(Link, ['clang', 'source.c'])

Compile = sut.Action.Compile
test(Compile, ['clang', '-c', 'source.c'])
test(Compile, ['clang', '-c', 'source.c', '-MF', 'source.d'])

Preprocess = sut.Action.Ignored
test(Preprocess, ['clang', '-E', 'source.c'])
test(Preprocess, ['clang', '-c', '-E', 'source.c'])
test(Preprocess, ['clang', '-c', '-M', 'source.c'])
test(Preprocess, ['clang', '-c', '-MM', 'source.c'])

def test_optimalizations(self):
def test(cmd):
opts = sut.classify_parameters(cmd)
return opts.get('compile_options', [])

self.assertEqual(['-O'], test(['clang', '-c', 'source.c', '-O']))
self.assertEqual(['-O1'], test(['clang', '-c', 'source.c', '-O1']))
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']))

def test_language(self):
def test(cmd):
opts = sut.classify_parameters(cmd)
return opts.get('language')

self.assertEqual(None, test(['clang', '-c', 'source.c']))
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)
return opts.get('archs_seen', [])

eq = self.assertEqual

eq([], test(['clang', '-c', 'source.c']))
eq(['mips'],
test(['clang', '-c', 'source.c', '-arch', 'mips']))
eq(['mips', 'i386'],
test(['clang', '-c', 'source.c', '-arch', 'mips', '-arch', 'i386']))

def test_input_file(self):
def test(cmd):
opts = sut.classify_parameters(cmd)
return opts.get('files', [])

eq = self.assertEqual

eq(['src.c'], test(['clang', 'src.c']))
eq(['src.c'], test(['clang', '-c', 'src.c']))
eq(['s1.c', 's2.c'], test(['clang', '-c', 's1.c', 's2.c']))

def test_include(self):
def test(cmd):
opts = sut.classify_parameters(cmd)
return opts.get('compile_options', [])

eq = self.assertEqual

eq([], test(['clang', '-c', 'src.c']))
eq(['-include', '/usr/local/include'],
test(['clang', '-c', 'src.c', '-include', '/usr/local/include']))
eq(['-I.'],
test(['clang', '-c', 'src.c', '-I.']))
eq(['-I', '.'],
test(['clang', '-c', 'src.c', '-I', '.']))
eq(['-I/usr/local/include'],
test(['clang', '-c', 'src.c', '-I/usr/local/include']))
eq(['-I', '/usr/local/include'],
test(['clang', '-c', 'src.c', '-I', '/usr/local/include']))
eq(['-I/opt', '-I', '/opt/otp/include'],
test(['clang', '-c', 'src.c', '-I/opt', '-I', '/opt/otp/include']))
eq(['-isystem', '/path'],
test(['clang', '-c', 'src.c', '-isystem', '/path']))
eq(['-isystem=/path'],
test(['clang', '-c', 'src.c', '-isystem=/path']))

def test_define(self):
def test(cmd):
opts = sut.classify_parameters(cmd)
return opts.get('compile_options', [])

eq = self.assertEqual

eq([], test(['clang', '-c', 'src.c']))
eq(['-DNDEBUG'],
test(['clang', '-c', 'src.c', '-DNDEBUG']))
eq(['-UNDEBUG'],
test(['clang', '-c', 'src.c', '-UNDEBUG']))
eq(['-Dvar1=val1', '-Dvar2=val2'],
test(['clang', '-c', 'src.c', '-Dvar1=val1', '-Dvar2=val2']))
eq(['-Dvar="val ues"'],
test(['clang', '-c', 'src.c', '-Dvar="val ues"']))

def test_ignored_flags(self):
def test(flags):
cmd = ['clang', 'src.o']
opts = sut.classify_parameters(cmd + flags)
self.assertEqual(['src.o'], opts.get('compile_options'))

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):
opts = sut.classify_parameters(cmd)
return opts.get('compile_options', [])

eq = self.assertEqual

eq(['-std=C99'],
test(['clang', '-c', 'src.c', '-std=C99']))
eq(['-nostdinc'],
test(['clang', '-c', 'src.c', '-nostdinc']))
eq(['-isystem', '/image/debian'],
test(['clang', '-c', 'src.c', '-isystem', '/image/debian']))
eq(['-iprefix', '/usr/local'],
test(['clang', '-c', 'src.c', '-iprefix', '/usr/local']))
eq(['-iquote=me'],
test(['clang', '-c', 'src.c', '-iquote=me']))
eq(['-iquote', 'me'],
test(['clang', '-c', 'src.c', '-iquote', 'me']))

def test_compile_and_link_flags(self):
def test(cmd):
opts = sut.classify_parameters(cmd)
return opts.get('compile_options', [])

eq = self.assertEqual

eq(['-fsinged-char'],
test(['clang', '-c', 'src.c', '-fsinged-char']))
eq(['-fPIC'],
test(['clang', '-c', 'src.c', '-fPIC']))
eq(['-stdlib=libc++'],
test(['clang', '-c', 'src.c', '-stdlib=libc++']))
eq(['--sysroot', '/'],
test(['clang', '-c', 'src.c', '--sysroot', '/']))
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('c++')

eq = self.assertEqual

eq(False, test(['cc', '-c', 'src.c']))
eq(True, test(['c++', '-c', 'src.c']))
eq(False, test(['clang', '-c', 'src.c']))
eq(True, test(['clang++', '-c', 'src.c']))
eq(False, test(['gcc', '-c', 'src.c']))
eq(True, test(['g++', '-c', 'src.c']))
Loading