From 76ee403bfbcb484185527c01f807907dad99db27 Mon Sep 17 00:00:00 2001
From: David Malcolm <dmalcolm@redhat.com>
Date: Thu, 25 May 2017 10:57:50 -0400
Subject: [PATCH 06/28] FIXME: rename checker scripts

---
 checkers/checker.py                | 551 +++++++++++++++++++++++++++++++++++++
 checkers/clang_analyzer.py         | 155 +++++++++++
 checkers/cppcheck.py               | 146 ++++++++++
 checkers/fakegcc.py                | 551 -------------------------------------
 checkers/fakegcc_clang_analyzer.py | 155 -----------
 checkers/fakegcc_cppcheck.py       | 146 ----------
 gcc/checkers.c                     |   4 +-
 7 files changed, 854 insertions(+), 854 deletions(-)
 create mode 100755 checkers/checker.py
 create mode 100755 checkers/clang_analyzer.py
 create mode 100755 checkers/cppcheck.py
 delete mode 100755 checkers/fakegcc.py
 delete mode 100755 checkers/fakegcc_clang_analyzer.py
 delete mode 100755 checkers/fakegcc_cppcheck.py

diff --git a/checkers/checker.py b/checkers/checker.py
new file mode 100755
index 0000000..2b6d65f
--- /dev/null
+++ b/checkers/checker.py
@@ -0,0 +1,551 @@
+#!/usr/bin/env python
+#   Copyright 2012, 2013, 2015, 2017 David Malcolm <dmalcolm@redhat.com>
+#   Copyright 2012, 2013, 2015, 2017 Red Hat, Inc.
+#
+#   This is free software: you can redistribute it and/or modify it
+#   under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 3 of the License, or
+#   (at your option) any later version.
+#
+#   This program is distributed in the hope that it will be useful, but
+#   WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#   General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program.  If not, see
+#   <http://www.gnu.org/licenses/>.
+
+import json
+import logging
+import os
+import re
+import StringIO
+import sys
+import tempfile
+import time
+import traceback
+import unittest
+
+# http://pypi.python.org/pypi/subprocess32
+# so that we can use timeouts
+from subprocess32 import Popen, PIPE, STDOUT, TimeoutExpired
+
+from firehose.model import Analysis, Generator, Metadata, Failure, \
+    Location, File, Message, Issue, Trace
+
+from gccinvocation import GccInvocation
+
+def in_chroot():
+    return os.path.exists('/builddir')
+
+def make_file(givenpath):
+    from firehose.model import File
+    return File(givenpath=givenpath,
+                abspath=None,
+                hash_=None)
+
+def make_stats(timer):
+    from firehose.model import Stats
+    return Stats(wallclocktime=timer.get_elapsed_time())
+
+class Timer:
+    """
+    Simple measurement of wallclock time taken
+    """
+    def __init__(self):
+        self.starttime = time.time()
+
+    def get_elapsed_time(self):
+        """Get elapsed time in seconds as a float"""
+        curtime = time.time()
+        return curtime - self.starttime
+
+    def elapsed_time_as_str(self):
+        """Get elapsed time as a string (with units)"""
+        elapsed = self.get_elapsed_time()
+        result = '%0.3f seconds' % elapsed
+        if elapsed > 120:
+            result += ' (%i minutes)' % int(elapsed / 60)
+        return result
+
+class Context:
+    def __init__(self, enable_logging=False, capture_exceptions=True):
+        self.enable_logging = enable_logging
+        self.capture_exceptions = capture_exceptions
+        if self.enable_logging:
+            if in_chroot():
+                logging.basicConfig(format='%(asctime)s %(message)s',
+                                    datefmt='%H:%M:%S',
+                                    level=logging.INFO,
+                                    filename='/builddir/fakegcc.log')
+            else:
+                logging.basicConfig(format='%(asctime)s %(message)s',
+                                    #datefmt='%H:%M:%S',
+                                    level=logging.INFO,
+                                    stream=sys.stdout)
+            self.log('logging initialized')
+
+        self.stdout = sys.stdout
+        self.stderr = sys.stderr
+        self.returncode = None
+
+    def log(self, msg):
+        if self.enable_logging:
+            logging.info(msg)
+
+    def write_streams(self, toolname, out, err):
+        for line in out.splitlines():
+            self.log('stdout from %r: %s\n' % (toolname, line))
+        for line in err.splitlines():
+            self.log('stderr from %r: %s\n' % (toolname, line))
+
+class SubprocessResult:
+    """
+    A bundle of information relating to a subprocess invocation.
+    """
+    def __init__(self, sourcefile, argv, returncode, out, err, timer):
+        self.sourcefile = sourcefile
+        self.argv = argv
+        self.returncode = returncode
+        self.out = out
+        self.err = err
+        self.timer = timer
+
+    def set_custom_fields(self, analysis):
+        analysis.set_custom_field('returncode', self.returncode)
+        analysis.set_custom_field('stdout', self.out.decode('utf-8'))
+        analysis.set_custom_field('stderr', self.err.decode('utf-8'))
+
+class Tool:
+    def __init__(self, name, ctxt):
+        self.name = name
+        self.timeout = 60
+        self.ctxt = ctxt
+
+    def log(self, msg):
+        self.ctxt.log(msg)
+
+    def raw_invoke(self, gccinv, sourcefile):
+        """
+        Run the tool, with a timeout, returning an Analysis instance.
+        May well raise an exception if something major went wrong.
+        """
+        raise NotImplementedError
+
+    def checked_invoke(self, gccinv, sourcefile):
+        """
+        Call "invoke", handling exceptions.  Return an Analysis instance.
+        """
+        try:
+            self.log('about to invoke: %s with %r' % (self.name, gccinv))
+            analysis = self.raw_invoke(gccinv, sourcefile)
+        except TimeoutExpired:
+            analysis = self._make_failed_analysis(
+                sourcefile, t, msgtext='Timeout running %s' % self.name,
+                failureid='timeout')
+            analysis.set_custom_field('timeout', TIMEOUT)
+        except Exception, exc:
+            # Capture the exception as a Failure instance.
+            # Alternatively when debugging such failures, it can
+            # be easier to re-raise the exception:
+            if not self.ctxt.capture_exceptions:
+                raise
+            analysis = \
+                self._make_failed_analysis(
+                    sourcefile, None,
+                    msgtext=('Exception running %s: %s'
+                             % (self.name, exc)),
+                    failureid='exception')
+            tb_str = traceback.format_exc()
+            analysis.set_custom_field('traceback', tb_str)
+        if sourcefile:
+            analysis.metadata.file_.givenpath = sourcefile
+            analysis.metadata.file_.abspath = os.path.join(os.getcwd(),
+                                                           sourcefile)
+        return analysis
+
+    def handle_output(self, result):
+        """
+        Given a SubprocessResult, return an Analysis instance.
+        """
+        raise NotImplementedError
+
+    def _make_failed_analysis(self, sourcefile, t, msgtext, failureid):
+        """
+        Something went wrong; build a failure report.
+        """
+        generator = Generator(name=self.name,
+                              version=None)
+        if t:
+            stats = make_stats(t)
+        else:
+            stats = None
+
+        metadata = Metadata(generator=generator,
+                            sut=None,
+                            file_ = make_file(sourcefile),
+                            stats=stats)
+        file_ = File(givenpath=sourcefile,
+                     abspath=None,
+                     hash_=None)
+        location = Location(file=file_,
+                            function=None,
+                            point=None,
+                            range_=None)
+        message = Message(msgtext)
+        results = [Failure(failureid=failureid,
+                           location=location,
+                           message=message,
+                           customfields=None)]
+        analysis = Analysis(metadata, results)
+        return analysis
+
+    def _run_subprocess(self, sourcefile, argv, env=None):
+        """
+        Support for running the bulk of the side effect in a subprocess,
+        with timeout support.
+        """
+        self.log('%s: _run_subprocess(%r, %r)' % (self.name, sourcefile, argv))
+        if 0:
+            self.log('env: %s' % env)
+        p = Popen(argv,
+                  stdout=PIPE, stderr=PIPE, env=env)
+        try:
+            t = Timer()
+            out, err = p.communicate(timeout=self.timeout)
+            self.ctxt.write_streams(argv[0], out, err)
+            result = SubprocessResult(sourcefile, argv, p.returncode, out, err, t)
+            analysis = self.handle_output(result)
+            return analysis
+        except TimeoutExpired:
+            analysis = self._make_failed_analysis(sourcefile, t,
+                                                  msgtext='Timeout running %s' % self.name,
+                                                  failureid='timeout')
+            analysis.set_custom_field('timeout', self.timeout)
+            analysis.set_custom_field('command-line', ' '.join(argv))
+            return analysis
+
+############################################################################
+# Tool subclasses
+############################################################################
+
+class InvokeRealGcc(Tool):
+    """
+    Tool subclass that invokes a real gcc compiler binary
+    """
+    def __init__(self, executable, ctxt, extra_args=None, extra_env=None):
+        Tool.__init__(self, 'gcc', ctxt)
+        self.executable = executable
+        self.extra_args = extra_args
+        self.extra_env = extra_env
+        # We are only ever invoked with individual input files.
+        # Override any -o, to ensure we don't interfere with the output
+        # from the real compiler:
+        self.output_file = tempfile.NamedTemporaryFile()
+
+    def raw_invoke(self, gccinv, sourcefile):
+        args = [self.executable] + gccinv.argv[1:]
+        if self.extra_args:
+            args += self.extra_args
+        args += ['-o', self.output_file.name]
+
+        # The result parser requires the C locale
+        env = os.environ.copy()
+        env['LANG'] = 'C'
+        if self.extra_env:
+            for key, value in self.extra_env.iteritems():
+                env[key] = value
+        return self._run_subprocess(sourcefile, args, env=env)
+
+    def handle_output(self, result):
+        from firehose.parsers.gcc import parse_file
+
+        f = StringIO.StringIO(result.err)
+        analysis = parse_file(f, stats=make_stats(result.timer))
+        if result.sourcefile:
+            analysis.metadata.file_ = File(givenpath=result.sourcefile,
+                                           abspath=None)
+        self.set_custom_fields(result, analysis)
+
+        self.result = result
+
+        return analysis
+
+    def set_custom_fields(self, result, analysis):
+        analysis.set_custom_field('gcc-invocation',
+                                  ' '.join(result.argv))
+        result.set_custom_fields(analysis)
+
+
+class InvokeCustomGcc(InvokeRealGcc):
+    """
+    Tool subclass that invokes a custom build of gcc 6 I have.
+    This is a patched version of gcc that writes out a file
+    to DUMPBASE.custom-dump.txt, containing a series of
+    lines of the form
+       KEY: VALUE
+    """
+    def __init__(self, executable, ctxt, extra_args=None, extra_env=None):
+        InvokeRealGcc.__init__(self, executable, ctxt, extra_args, extra_env)
+        self.name = 'custom-gcc'
+
+    def handle_output(self, result):
+        analysis = InvokeRealGcc.handle_output(self, result)
+        analysis.metadata.generator.name = 'custom-gcc'
+        dumpbase = os.path.join(os.path.dirname(self.output_file.name),
+                                os.path.basename(result.sourcefile))
+        dumpfile_path = dumpbase + '.custom-dump.txt'
+        if os.path.exists(dumpfile_path):
+            self.log('found custom dumpfile: %s' % dumpfile_path)
+            with open(dumpfile_path) as f:
+                for line in f:
+                    self.log(line)
+                    # Expect lines of the form "KEY: VALUE"
+                    m = re.match('^(.+): (.+)$', line)
+                    self.log(str(m.groups()))
+                    key, value = m.groups()
+                    analysis.set_custom_field(key, value)
+        else:
+            self.log('could not find custom dumpfile: %s' % dumpfile_path)
+        return analysis
+
+class InvokeCpychecker(Tool):
+    """
+    Tool subclass that invoke the gcc-python-plugin's "cpychecker" code.
+
+    This currently requires the "firehose" branch of gcc-python-plugin
+    """
+
+    def __init__(self, ctxt):
+        Tool.__init__(self, 'cpychecker', ctxt)
+
+    def raw_invoke(self, gccinv, sourcefile):
+        # Invoke the plugin, but for robustness, do it in an entirely
+        # separate gcc invocation
+        argv = gccinv.argv[:]
+
+        self.outputxmlpath = '%s.firehose.xml' % sourcefile
+
+        # The plugin needs to be able to find its own modules, or we get:
+        #   ImportError: No module named libcpychecker
+        # We can either set PYTHONPATH in the environment,
+        # or provide a full path to the plugin in the invocation line:
+        # in the latter case, gcc-python.c:setup_sys sets up sys.path
+        # inside the plugin to include the directory containing the plugin
+        # if we provide a full path to the plugin here.
+
+        # That said, the plugin's Makefile installs the plugin
+        # as "python.so" to $(GCCPLUGINS_DIR)
+        # and the support modules to $(GCCPLUGINS_DIR)/$(PLUGIN_DIR)
+        # So let's do it via PYTHONPATH
+
+        # FIXME: hacked in path:
+        plugin_path = '/home/david/coding/gcc-python/gcc-python/cpychecker-firehose-output/'
+        plugin_gcc_c_api_path = '/home/david/coding/gcc-python/gcc-python/cpychecker-firehose-output/gcc-c-api'
+        plugin_sys_path = '/home/david/coding/gcc-python/gcc-python/cpychecker-firehose-output/'
+        plugin_full_name = os.path.join(plugin_path, 'python.so')
+        env = os.environ.copy()
+        env['PYTHONPATH'] = plugin_sys_path
+        env['LD_LIBRARY_PATH'] = plugin_gcc_c_api_path
+
+        # We would use the regular keyword argument syntax:
+        #   outputxmlpath='foo'
+        # but unfortunately gcc's option parser seems to not be able to
+        # cope with '='  within an option's value.  So we do it using
+        # dictionary syntax instead:
+        pycmd = ('from libcpychecker import main, Options; '
+                 'main(Options(**{"outputxmlpath":"%s", '
+                 '"verify_refcounting": True, '
+                 '"maxtrans": 1024, '
+                 '}))' % self.outputxmlpath)
+        # Note that some RPMs also rename the plugin from
+        # "python.so" to "python2.so", which would require further work.
+        argv += ['-fplugin=%s' % plugin_full_name,
+                 '-fplugin-arg-python-command=%s' % pycmd]
+
+        args = [get_real_executable(argv)] + argv[1:]
+
+        return self._run_subprocess(sourcefile, args, env)
+
+    def handle_output(self, result):
+        if os.path.exists(self.outputxmlpath):
+            with open(self.outputxmlpath) as f:
+                analysis = Analysis.from_xml(f)
+                analysis.metadata.file_ = make_file(result.sourcefile)
+                analysis.metadata.stats = make_stats(result.timer)
+        else:
+            analysis = \
+                self._make_failed_analysis(
+                    result.sourcefile, result.timer,
+                    msgtext=('Unable to locate XML output from %s'
+                             % self.name),
+                    failureid='no-output-found')
+        analysis.set_custom_field('cpychecker-invocation',
+                                  ' '.join(result.argv))
+        result.set_custom_fields(analysis)
+        return analysis
+
+############################################################################
+# Test suite
+############################################################################
+
+class ToolTests(unittest.TestCase):
+    def make_ctxt(self, capture_exceptions=True):
+        return Context(enable_logging=0, capture_exceptions=capture_exceptions)
+
+    def make_tool(self):
+        """Hook for self.make_compiler()"""
+        raise NotImplementedError
+
+    def verify_basic_metadata(self, analysis, sourcefile):
+        """Hook for self.invoke()"""
+        raise NotImplementedError
+
+    def invoke(self, sourcefile, extraargs = None):
+        """Invoke a tool and sanity-check the result"""
+        tool = self.make_tool()
+        argv = ['gcc', '-c', sourcefile]
+        if extraargs:
+            argv += extraargs
+        gccinv = GccInvocation(argv)
+        analysis = tool.checked_invoke(gccinv, sourcefile)
+
+        if 0:
+            print(analysis)
+
+        # Call a subclass hook to check basic metadata:
+        self.verify_basic_metadata(analysis, sourcefile)
+
+        # Verify that we can serialize to XML:
+        xml_bytes = analysis.to_xml_bytes()
+        self.assert_(xml_bytes.startswith(b'<analysis>'))
+
+        # Verify it can roundtrip through JSON:
+        js_bytes = json.dumps(analysis.to_json(), indent=1)
+        other = Analysis.from_json(json.loads(js_bytes))
+        #self.assertEqual(analysis, other)
+
+        return analysis
+
+    def assert_metadata(self, analysis,
+                        expected_generator_name, expected_given_path):
+        self.assertEqual(analysis.metadata.generator.name,
+                         expected_generator_name)
+        self.assertEqual(analysis.metadata.file_.givenpath, expected_given_path)
+        self.assertIn(expected_given_path, analysis.metadata.file_.abspath)
+
+    def assert_has_custom_field(self, analysis, name):
+        self.assert_(analysis.customfields)
+        self.assert_(name in analysis.customfields)
+
+class BuggyToolTests(ToolTests):
+    def make_tool(self):
+        """
+        Override base class impl, so that we can enable
+        exception-capture (and provide a custom tool)
+        """
+        class BuggyTool(Tool):
+            def raw_invoke(self, gccinv, sourcefile):
+                raise ValueError('test of raising an exception')
+
+        ctxt = self.make_ctxt(capture_exceptions=True)
+        tool = BuggyTool('buggy', ctxt)
+        return tool
+
+    def verify_basic_metadata(self, analysis, sourcefile):
+         self.assert_metadata(analysis, 'buggy', sourcefile)
+
+    def test_exception_handling(self):
+        analysis = self.invoke('test-sources/harmless.c')
+        #print(analysis)
+        self.assertEqual(len(analysis.results), 1)
+        r0 = analysis.results[0]
+        self.assertIsInstance(r0, Failure)
+        self.assertEqual(r0.failureid, 'exception')
+        self.assertEqual(r0.message.text,
+                         ('Exception running buggy:'
+                          ' test of raising an exception'))
+        self.assert_(analysis.customfields['traceback'].startswith(
+            'Traceback (most recent call last):\n'))
+
+class RealGccTests(ToolTests):
+    def make_tool(self):
+        ctxt = self.make_ctxt()
+        return InvokeRealGcc('gcc', ctxt)
+
+    def verify_basic_metadata(self, analysis, sourcefile):
+        # Verify basic metadata:
+        self.assert_metadata(analysis, 'gcc', sourcefile)
+        self.assert_has_custom_field(analysis, 'gcc-invocation')
+        self.assert_has_custom_field(analysis, 'stdout')
+        self.assert_has_custom_field(analysis, 'stderr')
+
+    def test_file_not_found(self):
+        analysis = self.invoke('does-not-exist.c')
+        #print(analysis)
+        # Currently this gives no output:
+        self.assertEqual(len(analysis.results), 0)
+
+    def test_timeout(self):
+        sourcefile = 'test-sources/harmless.c'
+        tool = self.make_tool()
+        tool.timeout = 0
+        gccinv = GccInvocation(['gcc', sourcefile])
+        analysis = tool.checked_invoke(gccinv, sourcefile)
+        self.assert_metadata(analysis, tool.name, sourcefile)
+        self.assertEqual(len(analysis.results), 1)
+        r0 = analysis.results[0]
+        self.assertIsInstance(r0, Failure)
+        self.assertEqual(r0.failureid, 'timeout')
+        self.assert_has_custom_field(analysis, 'timeout')
+        self.assert_has_custom_field(analysis, 'command-line')
+
+    def test_harmless_file(self):
+        analysis = self.invoke('test-sources/harmless.c')
+        #print(analysis)
+        self.assertEqual(len(analysis.results), 0)
+
+    def test_divide_by_zero(self):
+        analysis = self.invoke('test-sources/divide-by-zero.c', ['-Wall'])
+        self.assertEqual(len(analysis.results), 1)
+        r0 = analysis.results[0]
+        self.assertIsInstance(r0, Issue)
+        self.assertEqual(r0.testid, 'div-by-zero')
+        self.assertEqual(r0.location.file.givenpath,
+                         'test-sources/divide-by-zero.c')
+        self.assertEqual(r0.location.function.name, 'divide_by_zero')
+        self.assertEqual(r0.location.point.line, 3)
+        self.assertEqual(r0.message.text, 'division by zero')
+        self.assertEqual(r0.severity, None)
+
+############################################################################
+# Entrypoint
+############################################################################
+
+def tool_main(argv, tool_class):
+    """
+    Entrypoint for use by the various per-tool scripts
+    """
+    # If we're invoked with "unittest" as the first param,
+    # run the unit test suite:
+    if len(argv) >= 2:
+        if argv[1] == 'unittest':
+            sys.argv = [argv[0]] + argv[2:]
+            return unittest.main()
+
+    ctxt = Context()
+    tool = tool_class(ctxt)
+
+    gccinv = GccInvocation(argv)
+    ctxt.log('  gccinv.sources: %r' % gccinv.sources)
+    assert len(gccinv.sources) == 1
+    sourcefile = gccinv.sources[0]
+    ctxt.log('    sourcefile: %r' % sourcefile)
+    analysis = tool.checked_invoke(gccinv, sourcefile)
+    json.dump(analysis.to_json(), sys.stdout, indent=1)
+
+# Run the unit tests
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())
diff --git a/checkers/clang_analyzer.py b/checkers/clang_analyzer.py
new file mode 100755
index 0000000..bbe8050
--- /dev/null
+++ b/checkers/clang_analyzer.py
@@ -0,0 +1,155 @@
+#!/usr/bin/env python
+#   Copyright 2012, 2013, 2015, 2017 David Malcolm <dmalcolm@redhat.com>
+#   Copyright 2012, 2013, 2015, 2017 Red Hat, Inc.
+#
+#   This is free software: you can redistribute it and/or modify it
+#   under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 3 of the License, or
+#   (at your option) any later version.
+#
+#   This program is distributed in the hope that it will be useful, but
+#   WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#   General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program.  If not, see
+#   <http://www.gnu.org/licenses/>.
+
+import glob
+import os
+import sys
+import tempfile
+import traceback
+import unittest
+
+from gccinvocation import GccInvocation
+
+from checker import Tool, Context, ToolTests, make_file, make_stats, \
+    tool_main
+
+from firehose.model import Analysis, Generator, Metadata, Failure, \
+    Location, File, Message, Issue, Trace
+from firehose.parsers.clanganalyzer import parse_plist
+
+class InvokeClangAnalyzer(Tool):
+    """
+    Tool subclass that invokes the clang analyzer
+    """
+    def __init__(self, ctxt):
+        Tool.__init__(self, 'clang-analyzer', ctxt)
+
+    def raw_invoke(self, gccinv, sourcefile):
+        self.resultdir = tempfile.mkdtemp()
+        args = ['scan-build', '-v', '-plist',
+                '--use-analyzer', '/usr/bin/clang', # rhbz 923834
+                '-o', self.resultdir,
+                'gcc'] + gccinv.argv[1:]
+        return self._run_subprocess(sourcefile, args)
+
+    def handle_output(self, result):
+        if result.returncode:
+            analysis = self._make_failed_analysis(result.sourcefile, result.timer,
+                                                  msgtext='Bad exit code running %s' % self.name,
+                                                  failureid='bad-exit-code')
+            self.set_custom_fields(result, analysis)
+            return analysis
+
+        # Given e.g. resultdir='/tmp/tmpQW2l2B', the plist files
+        # are an extra level deep e.g.:
+        #  '/tmp/tmpQW2l2B/2013-01-22-1/report-MlwJri.plist'
+        self.log(self.resultdir)
+        for plistpath in glob.glob(os.path.join(self.resultdir,
+                                                '*/*.plist')):
+            analysis = parse_plist(plistpath,
+                                   file_=make_file(result.sourcefile),
+                                   stats=make_stats(result.timer))
+            self.set_custom_fields(result, analysis)
+            analysis.set_custom_field('plistpath', plistpath)
+            return analysis # could there be more than one?
+
+        # Not found?
+        analysis = self._make_failed_analysis(
+            result.sourcefile, result.timer,
+            msgtext='Unable to locate plist file',
+            failureid='plist-not-found')
+        self.set_custom_fields(result, analysis)
+        return analysis
+
+    def set_custom_fields(self, result, analysis):
+        analysis.set_custom_field('scan-build-invocation',
+                                  ' '.join(result.argv))
+        result.set_custom_fields(analysis)
+
+class ClangAnalyzerTests(ToolTests):
+    def make_tool(self):
+        ctxt = self.make_ctxt()
+        return InvokeClangAnalyzer(ctxt)
+
+    def verify_basic_metadata(self, analysis, sourcefile):
+        # Verify basic metadata:
+        self.assert_metadata(analysis, 'clang-analyzer', sourcefile)
+        self.assert_has_custom_field(analysis, 'scan-build-invocation')
+        self.assert_has_custom_field(analysis, 'stdout')
+        self.assert_has_custom_field(analysis, 'stderr')
+
+    def test_file_not_found(self):
+        analysis = self.invoke('does-not-exist.c')
+        #print(analysis)
+        self.assertEqual(len(analysis.results), 1)
+        self.assertIsInstance(analysis.results[0], Failure)
+        self.assertEqual(analysis.results[0].failureid, 'bad-exit-code')
+
+    def test_timeout(self):
+        sourcefile = 'test-sources/harmless.c'
+        tool = self.make_tool()
+        tool.timeout = 0
+        gccinv = GccInvocation(['gcc', sourcefile])
+        analysis = tool.checked_invoke(gccinv, sourcefile)
+        self.assert_metadata(analysis, 'clang-analyzer', sourcefile)
+        self.assertEqual(len(analysis.results), 1)
+        r0 = analysis.results[0]
+        self.assertIsInstance(r0, Failure)
+        self.assertEqual(r0.failureid, 'timeout')
+        self.assert_has_custom_field(analysis, 'timeout')
+        self.assert_has_custom_field(analysis, 'command-line')
+
+    def test_harmless_file(self):
+        analysis = self.invoke('test-sources/harmless.c')
+        #print(analysis)
+        self.assertEqual(len(analysis.results), 0)
+
+    def test_read_through_null(self):
+        analysis = self.invoke('test-sources/read-through-null.c')
+        #print(analysis)
+        #print(analysis.to_json())
+        self.assertEqual(len(analysis.results), 1)
+        r0 = analysis.results[0]
+        self.assertIsInstance(r0, Issue)
+        self.assertEqual(r0.testid, None)
+        self.assertEqual(r0.location.file.givenpath,
+                         'test-sources/read-through-null.c')
+        self.assertEqual(r0.location.point.line, 3)
+        self.assertEqual(r0.message.text,
+                         "Dereference of null pointer")
+        self.assertEqual(r0.severity, None)
+        self.assertIsInstance(r0.trace, Trace)
+
+    def test_out_of_bounds(self):
+        analysis = self.invoke('test-sources/out-of-bounds.c')
+        #print(analysis)
+        self.assertEqual(len(analysis.results), 1)
+
+        r0 = analysis.results[0]
+        self.assertIsInstance(r0, Issue)
+        self.assertEqual(r0.testid, None)
+        self.assertEqual(r0.location.file.givenpath,
+                         'test-sources/out-of-bounds.c')
+        self.assertEqual(r0.location.point.line, 5)
+        self.assertEqual(r0.message.text,
+                         "Undefined or garbage value returned to caller")
+        self.assertEqual(r0.severity, None)
+        self.assertIsInstance(r0.trace, Trace)
+
+if __name__ == '__main__':
+    sys.exit(tool_main(sys.argv, InvokeClangAnalyzer))
diff --git a/checkers/cppcheck.py b/checkers/cppcheck.py
new file mode 100755
index 0000000..15c3f36
--- /dev/null
+++ b/checkers/cppcheck.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python
+#   Copyright 2012, 2013, 2015, 2017 David Malcolm <dmalcolm@redhat.com>
+#   Copyright 2012, 2013, 2015, 2017 Red Hat, Inc.
+#
+#   This is free software: you can redistribute it and/or modify it
+#   under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 3 of the License, or
+#   (at your option) any later version.
+#
+#   This program is distributed in the hope that it will be useful, but
+#   WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#   General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program.  If not, see
+#   <http://www.gnu.org/licenses/>.
+
+import sys
+import tempfile
+import unittest
+
+from gccinvocation import GccInvocation
+
+from checker import Tool, Context, ToolTests, make_file, make_stats, \
+    tool_main
+
+from firehose.model import Analysis, Generator, Metadata, Failure, \
+    Location, File, Message, Issue, Trace
+from firehose.parsers.cppcheck import parse_file
+
+class InvokeCppcheck(Tool):
+    """
+    Tool subclass that invokes "cppcheck"
+    """
+    def __init__(self, ctxt):
+        Tool.__init__(self, 'cppcheck', ctxt)
+
+    def raw_invoke(self, gccinv, sourcefile):
+        args = ['cppcheck',
+                '--xml', '--xml-version=2',
+                sourcefile]
+        return self._run_subprocess(sourcefile, args)
+
+    def handle_output(self, result):
+        if result.returncode:
+            analysis = self._make_failed_analysis(result.sourcefile, result.timer,
+                                                  msgtext='Bad exit code running %s' % self.name,
+                                                  failureid='bad-exit-code')
+            self.set_custom_fields(result, analysis)
+            return analysis
+
+        # (there doesn't seem to be a way to have cppcheck directly
+        # save its XML output to a given location)
+
+        with tempfile.NamedTemporaryFile() as outfile:
+            outfile.write(result.err)
+            outfile.flush()
+
+            with open(outfile.name) as infile:
+                # Parse stderr into firehose XML format and save:
+                analysis = parse_file(infile,
+                                      file_=make_file(result.sourcefile),
+                                      stats=make_stats(result.timer))
+                self.set_custom_fields(result, analysis)
+                return analysis
+
+    def set_custom_fields(self, result, analysis):
+        analysis.set_custom_field('cppcheck-invocation',
+                                  ' '.join(result.argv))
+        result.set_custom_fields(analysis)
+
+class CppcheckTests(ToolTests):
+    def make_tool(self):
+        ctxt = self.make_ctxt()
+        return InvokeCppcheck(ctxt)
+
+    def verify_basic_metadata(self, analysis, sourcefile):
+        # Verify basic metadata:
+        self.assert_metadata(analysis, 'cppcheck', sourcefile)
+        self.assert_has_custom_field(analysis, 'cppcheck-invocation')
+        self.assert_has_custom_field(analysis, 'stdout')
+        self.assert_has_custom_field(analysis, 'stderr')
+
+    def test_file_not_found(self):
+        analysis = self.invoke('does-not-exist.c')
+        #print(analysis)
+        self.assertEqual(len(analysis.results), 1)
+        self.assertIsInstance(analysis.results[0], Failure)
+        self.assertEqual(analysis.results[0].failureid, 'bad-exit-code')
+
+    def test_timeout(self):
+        sourcefile = 'test-sources/harmless.c'
+        tool = self.make_tool()
+        tool.timeout = 0
+        gccinv = GccInvocation(['gcc', sourcefile])
+        analysis = tool.checked_invoke(gccinv, sourcefile)
+        self.assert_metadata(analysis, 'cppcheck', sourcefile)
+        self.assertEqual(len(analysis.results), 1)
+        r0 = analysis.results[0]
+        self.assertIsInstance(r0, Failure)
+        self.assertEqual(r0.failureid, 'timeout')
+        self.assert_has_custom_field(analysis, 'timeout')
+        self.assert_has_custom_field(analysis, 'command-line')
+
+    def test_harmless_file(self):
+        analysis = self.invoke('test-sources/harmless.c')
+        #print(analysis)
+        self.assertEqual(len(analysis.results), 0)
+
+    def test_read_through_null(self):
+        analysis = self.invoke('test-sources/read-through-null.c')
+        self.assertEqual(len(analysis.results), 1)
+        r0 = analysis.results[0]
+        self.assertIsInstance(r0, Issue)
+        self.assertEqual(r0.testid, 'nullPointer')
+        self.assertEqual(r0.location.file.givenpath,
+                         'test-sources/read-through-null.c')
+        self.assertEqual(r0.location.point.line, 3)
+        self.assertEqual(r0.message.text,
+                         "Null pointer dereference")
+        self.assertEqual(r0.severity, 'error')
+
+    def test_out_of_bounds(self):
+        analysis = self.invoke('test-sources/out-of-bounds.c')
+        #print(analysis)
+        self.assertEqual(len(analysis.results), 2)
+
+        r0 = analysis.results[0]
+        self.assertIsInstance(r0, Issue)
+        self.assertEqual(r0.testid, 'arrayIndexOutOfBounds')
+        self.assertEqual(r0.location.file.givenpath,
+                         'test-sources/out-of-bounds.c')
+        self.assertEqual(r0.location.point.line, 5)
+        self.assertEqual(
+            r0.message.text,
+            "Array 'arr[10]' accessed at index 15, which is out of bounds.")
+        self.assertEqual(r0.severity, 'error')
+
+        r1 = analysis.results[1]
+        self.assertIsInstance(r1, Issue)
+        self.assertEqual(r1.testid, 'uninitvar')
+        # etc
+
+if __name__ == '__main__':
+    sys.exit(tool_main(sys.argv, InvokeCppcheck))
diff --git a/checkers/fakegcc.py b/checkers/fakegcc.py
deleted file mode 100755
index 2b6d65f..0000000
--- a/checkers/fakegcc.py
+++ /dev/null
@@ -1,551 +0,0 @@
-#!/usr/bin/env python
-#   Copyright 2012, 2013, 2015, 2017 David Malcolm <dmalcolm@redhat.com>
-#   Copyright 2012, 2013, 2015, 2017 Red Hat, Inc.
-#
-#   This is free software: you can redistribute it and/or modify it
-#   under the terms of the GNU General Public License as published by
-#   the Free Software Foundation, either version 3 of the License, or
-#   (at your option) any later version.
-#
-#   This program is distributed in the hope that it will be useful, but
-#   WITHOUT ANY WARRANTY; without even the implied warranty of
-#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-#   General Public License for more details.
-#
-#   You should have received a copy of the GNU General Public License
-#   along with this program.  If not, see
-#   <http://www.gnu.org/licenses/>.
-
-import json
-import logging
-import os
-import re
-import StringIO
-import sys
-import tempfile
-import time
-import traceback
-import unittest
-
-# http://pypi.python.org/pypi/subprocess32
-# so that we can use timeouts
-from subprocess32 import Popen, PIPE, STDOUT, TimeoutExpired
-
-from firehose.model import Analysis, Generator, Metadata, Failure, \
-    Location, File, Message, Issue, Trace
-
-from gccinvocation import GccInvocation
-
-def in_chroot():
-    return os.path.exists('/builddir')
-
-def make_file(givenpath):
-    from firehose.model import File
-    return File(givenpath=givenpath,
-                abspath=None,
-                hash_=None)
-
-def make_stats(timer):
-    from firehose.model import Stats
-    return Stats(wallclocktime=timer.get_elapsed_time())
-
-class Timer:
-    """
-    Simple measurement of wallclock time taken
-    """
-    def __init__(self):
-        self.starttime = time.time()
-
-    def get_elapsed_time(self):
-        """Get elapsed time in seconds as a float"""
-        curtime = time.time()
-        return curtime - self.starttime
-
-    def elapsed_time_as_str(self):
-        """Get elapsed time as a string (with units)"""
-        elapsed = self.get_elapsed_time()
-        result = '%0.3f seconds' % elapsed
-        if elapsed > 120:
-            result += ' (%i minutes)' % int(elapsed / 60)
-        return result
-
-class Context:
-    def __init__(self, enable_logging=False, capture_exceptions=True):
-        self.enable_logging = enable_logging
-        self.capture_exceptions = capture_exceptions
-        if self.enable_logging:
-            if in_chroot():
-                logging.basicConfig(format='%(asctime)s %(message)s',
-                                    datefmt='%H:%M:%S',
-                                    level=logging.INFO,
-                                    filename='/builddir/fakegcc.log')
-            else:
-                logging.basicConfig(format='%(asctime)s %(message)s',
-                                    #datefmt='%H:%M:%S',
-                                    level=logging.INFO,
-                                    stream=sys.stdout)
-            self.log('logging initialized')
-
-        self.stdout = sys.stdout
-        self.stderr = sys.stderr
-        self.returncode = None
-
-    def log(self, msg):
-        if self.enable_logging:
-            logging.info(msg)
-
-    def write_streams(self, toolname, out, err):
-        for line in out.splitlines():
-            self.log('stdout from %r: %s\n' % (toolname, line))
-        for line in err.splitlines():
-            self.log('stderr from %r: %s\n' % (toolname, line))
-
-class SubprocessResult:
-    """
-    A bundle of information relating to a subprocess invocation.
-    """
-    def __init__(self, sourcefile, argv, returncode, out, err, timer):
-        self.sourcefile = sourcefile
-        self.argv = argv
-        self.returncode = returncode
-        self.out = out
-        self.err = err
-        self.timer = timer
-
-    def set_custom_fields(self, analysis):
-        analysis.set_custom_field('returncode', self.returncode)
-        analysis.set_custom_field('stdout', self.out.decode('utf-8'))
-        analysis.set_custom_field('stderr', self.err.decode('utf-8'))
-
-class Tool:
-    def __init__(self, name, ctxt):
-        self.name = name
-        self.timeout = 60
-        self.ctxt = ctxt
-
-    def log(self, msg):
-        self.ctxt.log(msg)
-
-    def raw_invoke(self, gccinv, sourcefile):
-        """
-        Run the tool, with a timeout, returning an Analysis instance.
-        May well raise an exception if something major went wrong.
-        """
-        raise NotImplementedError
-
-    def checked_invoke(self, gccinv, sourcefile):
-        """
-        Call "invoke", handling exceptions.  Return an Analysis instance.
-        """
-        try:
-            self.log('about to invoke: %s with %r' % (self.name, gccinv))
-            analysis = self.raw_invoke(gccinv, sourcefile)
-        except TimeoutExpired:
-            analysis = self._make_failed_analysis(
-                sourcefile, t, msgtext='Timeout running %s' % self.name,
-                failureid='timeout')
-            analysis.set_custom_field('timeout', TIMEOUT)
-        except Exception, exc:
-            # Capture the exception as a Failure instance.
-            # Alternatively when debugging such failures, it can
-            # be easier to re-raise the exception:
-            if not self.ctxt.capture_exceptions:
-                raise
-            analysis = \
-                self._make_failed_analysis(
-                    sourcefile, None,
-                    msgtext=('Exception running %s: %s'
-                             % (self.name, exc)),
-                    failureid='exception')
-            tb_str = traceback.format_exc()
-            analysis.set_custom_field('traceback', tb_str)
-        if sourcefile:
-            analysis.metadata.file_.givenpath = sourcefile
-            analysis.metadata.file_.abspath = os.path.join(os.getcwd(),
-                                                           sourcefile)
-        return analysis
-
-    def handle_output(self, result):
-        """
-        Given a SubprocessResult, return an Analysis instance.
-        """
-        raise NotImplementedError
-
-    def _make_failed_analysis(self, sourcefile, t, msgtext, failureid):
-        """
-        Something went wrong; build a failure report.
-        """
-        generator = Generator(name=self.name,
-                              version=None)
-        if t:
-            stats = make_stats(t)
-        else:
-            stats = None
-
-        metadata = Metadata(generator=generator,
-                            sut=None,
-                            file_ = make_file(sourcefile),
-                            stats=stats)
-        file_ = File(givenpath=sourcefile,
-                     abspath=None,
-                     hash_=None)
-        location = Location(file=file_,
-                            function=None,
-                            point=None,
-                            range_=None)
-        message = Message(msgtext)
-        results = [Failure(failureid=failureid,
-                           location=location,
-                           message=message,
-                           customfields=None)]
-        analysis = Analysis(metadata, results)
-        return analysis
-
-    def _run_subprocess(self, sourcefile, argv, env=None):
-        """
-        Support for running the bulk of the side effect in a subprocess,
-        with timeout support.
-        """
-        self.log('%s: _run_subprocess(%r, %r)' % (self.name, sourcefile, argv))
-        if 0:
-            self.log('env: %s' % env)
-        p = Popen(argv,
-                  stdout=PIPE, stderr=PIPE, env=env)
-        try:
-            t = Timer()
-            out, err = p.communicate(timeout=self.timeout)
-            self.ctxt.write_streams(argv[0], out, err)
-            result = SubprocessResult(sourcefile, argv, p.returncode, out, err, t)
-            analysis = self.handle_output(result)
-            return analysis
-        except TimeoutExpired:
-            analysis = self._make_failed_analysis(sourcefile, t,
-                                                  msgtext='Timeout running %s' % self.name,
-                                                  failureid='timeout')
-            analysis.set_custom_field('timeout', self.timeout)
-            analysis.set_custom_field('command-line', ' '.join(argv))
-            return analysis
-
-############################################################################
-# Tool subclasses
-############################################################################
-
-class InvokeRealGcc(Tool):
-    """
-    Tool subclass that invokes a real gcc compiler binary
-    """
-    def __init__(self, executable, ctxt, extra_args=None, extra_env=None):
-        Tool.__init__(self, 'gcc', ctxt)
-        self.executable = executable
-        self.extra_args = extra_args
-        self.extra_env = extra_env
-        # We are only ever invoked with individual input files.
-        # Override any -o, to ensure we don't interfere with the output
-        # from the real compiler:
-        self.output_file = tempfile.NamedTemporaryFile()
-
-    def raw_invoke(self, gccinv, sourcefile):
-        args = [self.executable] + gccinv.argv[1:]
-        if self.extra_args:
-            args += self.extra_args
-        args += ['-o', self.output_file.name]
-
-        # The result parser requires the C locale
-        env = os.environ.copy()
-        env['LANG'] = 'C'
-        if self.extra_env:
-            for key, value in self.extra_env.iteritems():
-                env[key] = value
-        return self._run_subprocess(sourcefile, args, env=env)
-
-    def handle_output(self, result):
-        from firehose.parsers.gcc import parse_file
-
-        f = StringIO.StringIO(result.err)
-        analysis = parse_file(f, stats=make_stats(result.timer))
-        if result.sourcefile:
-            analysis.metadata.file_ = File(givenpath=result.sourcefile,
-                                           abspath=None)
-        self.set_custom_fields(result, analysis)
-
-        self.result = result
-
-        return analysis
-
-    def set_custom_fields(self, result, analysis):
-        analysis.set_custom_field('gcc-invocation',
-                                  ' '.join(result.argv))
-        result.set_custom_fields(analysis)
-
-
-class InvokeCustomGcc(InvokeRealGcc):
-    """
-    Tool subclass that invokes a custom build of gcc 6 I have.
-    This is a patched version of gcc that writes out a file
-    to DUMPBASE.custom-dump.txt, containing a series of
-    lines of the form
-       KEY: VALUE
-    """
-    def __init__(self, executable, ctxt, extra_args=None, extra_env=None):
-        InvokeRealGcc.__init__(self, executable, ctxt, extra_args, extra_env)
-        self.name = 'custom-gcc'
-
-    def handle_output(self, result):
-        analysis = InvokeRealGcc.handle_output(self, result)
-        analysis.metadata.generator.name = 'custom-gcc'
-        dumpbase = os.path.join(os.path.dirname(self.output_file.name),
-                                os.path.basename(result.sourcefile))
-        dumpfile_path = dumpbase + '.custom-dump.txt'
-        if os.path.exists(dumpfile_path):
-            self.log('found custom dumpfile: %s' % dumpfile_path)
-            with open(dumpfile_path) as f:
-                for line in f:
-                    self.log(line)
-                    # Expect lines of the form "KEY: VALUE"
-                    m = re.match('^(.+): (.+)$', line)
-                    self.log(str(m.groups()))
-                    key, value = m.groups()
-                    analysis.set_custom_field(key, value)
-        else:
-            self.log('could not find custom dumpfile: %s' % dumpfile_path)
-        return analysis
-
-class InvokeCpychecker(Tool):
-    """
-    Tool subclass that invoke the gcc-python-plugin's "cpychecker" code.
-
-    This currently requires the "firehose" branch of gcc-python-plugin
-    """
-
-    def __init__(self, ctxt):
-        Tool.__init__(self, 'cpychecker', ctxt)
-
-    def raw_invoke(self, gccinv, sourcefile):
-        # Invoke the plugin, but for robustness, do it in an entirely
-        # separate gcc invocation
-        argv = gccinv.argv[:]
-
-        self.outputxmlpath = '%s.firehose.xml' % sourcefile
-
-        # The plugin needs to be able to find its own modules, or we get:
-        #   ImportError: No module named libcpychecker
-        # We can either set PYTHONPATH in the environment,
-        # or provide a full path to the plugin in the invocation line:
-        # in the latter case, gcc-python.c:setup_sys sets up sys.path
-        # inside the plugin to include the directory containing the plugin
-        # if we provide a full path to the plugin here.
-
-        # That said, the plugin's Makefile installs the plugin
-        # as "python.so" to $(GCCPLUGINS_DIR)
-        # and the support modules to $(GCCPLUGINS_DIR)/$(PLUGIN_DIR)
-        # So let's do it via PYTHONPATH
-
-        # FIXME: hacked in path:
-        plugin_path = '/home/david/coding/gcc-python/gcc-python/cpychecker-firehose-output/'
-        plugin_gcc_c_api_path = '/home/david/coding/gcc-python/gcc-python/cpychecker-firehose-output/gcc-c-api'
-        plugin_sys_path = '/home/david/coding/gcc-python/gcc-python/cpychecker-firehose-output/'
-        plugin_full_name = os.path.join(plugin_path, 'python.so')
-        env = os.environ.copy()
-        env['PYTHONPATH'] = plugin_sys_path
-        env['LD_LIBRARY_PATH'] = plugin_gcc_c_api_path
-
-        # We would use the regular keyword argument syntax:
-        #   outputxmlpath='foo'
-        # but unfortunately gcc's option parser seems to not be able to
-        # cope with '='  within an option's value.  So we do it using
-        # dictionary syntax instead:
-        pycmd = ('from libcpychecker import main, Options; '
-                 'main(Options(**{"outputxmlpath":"%s", '
-                 '"verify_refcounting": True, '
-                 '"maxtrans": 1024, '
-                 '}))' % self.outputxmlpath)
-        # Note that some RPMs also rename the plugin from
-        # "python.so" to "python2.so", which would require further work.
-        argv += ['-fplugin=%s' % plugin_full_name,
-                 '-fplugin-arg-python-command=%s' % pycmd]
-
-        args = [get_real_executable(argv)] + argv[1:]
-
-        return self._run_subprocess(sourcefile, args, env)
-
-    def handle_output(self, result):
-        if os.path.exists(self.outputxmlpath):
-            with open(self.outputxmlpath) as f:
-                analysis = Analysis.from_xml(f)
-                analysis.metadata.file_ = make_file(result.sourcefile)
-                analysis.metadata.stats = make_stats(result.timer)
-        else:
-            analysis = \
-                self._make_failed_analysis(
-                    result.sourcefile, result.timer,
-                    msgtext=('Unable to locate XML output from %s'
-                             % self.name),
-                    failureid='no-output-found')
-        analysis.set_custom_field('cpychecker-invocation',
-                                  ' '.join(result.argv))
-        result.set_custom_fields(analysis)
-        return analysis
-
-############################################################################
-# Test suite
-############################################################################
-
-class ToolTests(unittest.TestCase):
-    def make_ctxt(self, capture_exceptions=True):
-        return Context(enable_logging=0, capture_exceptions=capture_exceptions)
-
-    def make_tool(self):
-        """Hook for self.make_compiler()"""
-        raise NotImplementedError
-
-    def verify_basic_metadata(self, analysis, sourcefile):
-        """Hook for self.invoke()"""
-        raise NotImplementedError
-
-    def invoke(self, sourcefile, extraargs = None):
-        """Invoke a tool and sanity-check the result"""
-        tool = self.make_tool()
-        argv = ['gcc', '-c', sourcefile]
-        if extraargs:
-            argv += extraargs
-        gccinv = GccInvocation(argv)
-        analysis = tool.checked_invoke(gccinv, sourcefile)
-
-        if 0:
-            print(analysis)
-
-        # Call a subclass hook to check basic metadata:
-        self.verify_basic_metadata(analysis, sourcefile)
-
-        # Verify that we can serialize to XML:
-        xml_bytes = analysis.to_xml_bytes()
-        self.assert_(xml_bytes.startswith(b'<analysis>'))
-
-        # Verify it can roundtrip through JSON:
-        js_bytes = json.dumps(analysis.to_json(), indent=1)
-        other = Analysis.from_json(json.loads(js_bytes))
-        #self.assertEqual(analysis, other)
-
-        return analysis
-
-    def assert_metadata(self, analysis,
-                        expected_generator_name, expected_given_path):
-        self.assertEqual(analysis.metadata.generator.name,
-                         expected_generator_name)
-        self.assertEqual(analysis.metadata.file_.givenpath, expected_given_path)
-        self.assertIn(expected_given_path, analysis.metadata.file_.abspath)
-
-    def assert_has_custom_field(self, analysis, name):
-        self.assert_(analysis.customfields)
-        self.assert_(name in analysis.customfields)
-
-class BuggyToolTests(ToolTests):
-    def make_tool(self):
-        """
-        Override base class impl, so that we can enable
-        exception-capture (and provide a custom tool)
-        """
-        class BuggyTool(Tool):
-            def raw_invoke(self, gccinv, sourcefile):
-                raise ValueError('test of raising an exception')
-
-        ctxt = self.make_ctxt(capture_exceptions=True)
-        tool = BuggyTool('buggy', ctxt)
-        return tool
-
-    def verify_basic_metadata(self, analysis, sourcefile):
-         self.assert_metadata(analysis, 'buggy', sourcefile)
-
-    def test_exception_handling(self):
-        analysis = self.invoke('test-sources/harmless.c')
-        #print(analysis)
-        self.assertEqual(len(analysis.results), 1)
-        r0 = analysis.results[0]
-        self.assertIsInstance(r0, Failure)
-        self.assertEqual(r0.failureid, 'exception')
-        self.assertEqual(r0.message.text,
-                         ('Exception running buggy:'
-                          ' test of raising an exception'))
-        self.assert_(analysis.customfields['traceback'].startswith(
-            'Traceback (most recent call last):\n'))
-
-class RealGccTests(ToolTests):
-    def make_tool(self):
-        ctxt = self.make_ctxt()
-        return InvokeRealGcc('gcc', ctxt)
-
-    def verify_basic_metadata(self, analysis, sourcefile):
-        # Verify basic metadata:
-        self.assert_metadata(analysis, 'gcc', sourcefile)
-        self.assert_has_custom_field(analysis, 'gcc-invocation')
-        self.assert_has_custom_field(analysis, 'stdout')
-        self.assert_has_custom_field(analysis, 'stderr')
-
-    def test_file_not_found(self):
-        analysis = self.invoke('does-not-exist.c')
-        #print(analysis)
-        # Currently this gives no output:
-        self.assertEqual(len(analysis.results), 0)
-
-    def test_timeout(self):
-        sourcefile = 'test-sources/harmless.c'
-        tool = self.make_tool()
-        tool.timeout = 0
-        gccinv = GccInvocation(['gcc', sourcefile])
-        analysis = tool.checked_invoke(gccinv, sourcefile)
-        self.assert_metadata(analysis, tool.name, sourcefile)
-        self.assertEqual(len(analysis.results), 1)
-        r0 = analysis.results[0]
-        self.assertIsInstance(r0, Failure)
-        self.assertEqual(r0.failureid, 'timeout')
-        self.assert_has_custom_field(analysis, 'timeout')
-        self.assert_has_custom_field(analysis, 'command-line')
-
-    def test_harmless_file(self):
-        analysis = self.invoke('test-sources/harmless.c')
-        #print(analysis)
-        self.assertEqual(len(analysis.results), 0)
-
-    def test_divide_by_zero(self):
-        analysis = self.invoke('test-sources/divide-by-zero.c', ['-Wall'])
-        self.assertEqual(len(analysis.results), 1)
-        r0 = analysis.results[0]
-        self.assertIsInstance(r0, Issue)
-        self.assertEqual(r0.testid, 'div-by-zero')
-        self.assertEqual(r0.location.file.givenpath,
-                         'test-sources/divide-by-zero.c')
-        self.assertEqual(r0.location.function.name, 'divide_by_zero')
-        self.assertEqual(r0.location.point.line, 3)
-        self.assertEqual(r0.message.text, 'division by zero')
-        self.assertEqual(r0.severity, None)
-
-############################################################################
-# Entrypoint
-############################################################################
-
-def tool_main(argv, tool_class):
-    """
-    Entrypoint for use by the various per-tool scripts
-    """
-    # If we're invoked with "unittest" as the first param,
-    # run the unit test suite:
-    if len(argv) >= 2:
-        if argv[1] == 'unittest':
-            sys.argv = [argv[0]] + argv[2:]
-            return unittest.main()
-
-    ctxt = Context()
-    tool = tool_class(ctxt)
-
-    gccinv = GccInvocation(argv)
-    ctxt.log('  gccinv.sources: %r' % gccinv.sources)
-    assert len(gccinv.sources) == 1
-    sourcefile = gccinv.sources[0]
-    ctxt.log('    sourcefile: %r' % sourcefile)
-    analysis = tool.checked_invoke(gccinv, sourcefile)
-    json.dump(analysis.to_json(), sys.stdout, indent=1)
-
-# Run the unit tests
-
-if __name__ == '__main__':
-    sys.exit(unittest.main())
diff --git a/checkers/fakegcc_clang_analyzer.py b/checkers/fakegcc_clang_analyzer.py
deleted file mode 100755
index 29f7b87..0000000
--- a/checkers/fakegcc_clang_analyzer.py
+++ /dev/null
@@ -1,155 +0,0 @@
-#!/usr/bin/env python
-#   Copyright 2012, 2013, 2015, 2017 David Malcolm <dmalcolm@redhat.com>
-#   Copyright 2012, 2013, 2015, 2017 Red Hat, Inc.
-#
-#   This is free software: you can redistribute it and/or modify it
-#   under the terms of the GNU General Public License as published by
-#   the Free Software Foundation, either version 3 of the License, or
-#   (at your option) any later version.
-#
-#   This program is distributed in the hope that it will be useful, but
-#   WITHOUT ANY WARRANTY; without even the implied warranty of
-#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-#   General Public License for more details.
-#
-#   You should have received a copy of the GNU General Public License
-#   along with this program.  If not, see
-#   <http://www.gnu.org/licenses/>.
-
-import glob
-import os
-import sys
-import tempfile
-import traceback
-import unittest
-
-from gccinvocation import GccInvocation
-
-from fakegcc import Tool, Context, ToolTests, make_file, make_stats, \
-    tool_main
-
-from firehose.model import Analysis, Generator, Metadata, Failure, \
-    Location, File, Message, Issue, Trace
-from firehose.parsers.clanganalyzer import parse_plist
-
-class InvokeClangAnalyzer(Tool):
-    """
-    Tool subclass that invokes the clang analyzer
-    """
-    def __init__(self, ctxt):
-        Tool.__init__(self, 'clang-analyzer', ctxt)
-
-    def raw_invoke(self, gccinv, sourcefile):
-        self.resultdir = tempfile.mkdtemp()
-        args = ['scan-build', '-v', '-plist',
-                '--use-analyzer', '/usr/bin/clang', # rhbz 923834
-                '-o', self.resultdir,
-                'gcc'] + gccinv.argv[1:]
-        return self._run_subprocess(sourcefile, args)
-
-    def handle_output(self, result):
-        if result.returncode:
-            analysis = self._make_failed_analysis(result.sourcefile, result.timer,
-                                                  msgtext='Bad exit code running %s' % self.name,
-                                                  failureid='bad-exit-code')
-            self.set_custom_fields(result, analysis)
-            return analysis
-
-        # Given e.g. resultdir='/tmp/tmpQW2l2B', the plist files
-        # are an extra level deep e.g.:
-        #  '/tmp/tmpQW2l2B/2013-01-22-1/report-MlwJri.plist'
-        self.log(self.resultdir)
-        for plistpath in glob.glob(os.path.join(self.resultdir,
-                                                '*/*.plist')):
-            analysis = parse_plist(plistpath,
-                                   file_=make_file(result.sourcefile),
-                                   stats=make_stats(result.timer))
-            self.set_custom_fields(result, analysis)
-            analysis.set_custom_field('plistpath', plistpath)
-            return analysis # could there be more than one?
-
-        # Not found?
-        analysis = self._make_failed_analysis(
-            result.sourcefile, result.timer,
-            msgtext='Unable to locate plist file',
-            failureid='plist-not-found')
-        self.set_custom_fields(result, analysis)
-        return analysis
-
-    def set_custom_fields(self, result, analysis):
-        analysis.set_custom_field('scan-build-invocation',
-                                  ' '.join(result.argv))
-        result.set_custom_fields(analysis)
-
-class ClangAnalyzerTests(ToolTests):
-    def make_tool(self):
-        ctxt = self.make_ctxt()
-        return InvokeClangAnalyzer(ctxt)
-
-    def verify_basic_metadata(self, analysis, sourcefile):
-        # Verify basic metadata:
-        self.assert_metadata(analysis, 'clang-analyzer', sourcefile)
-        self.assert_has_custom_field(analysis, 'scan-build-invocation')
-        self.assert_has_custom_field(analysis, 'stdout')
-        self.assert_has_custom_field(analysis, 'stderr')
-
-    def test_file_not_found(self):
-        analysis = self.invoke('does-not-exist.c')
-        #print(analysis)
-        self.assertEqual(len(analysis.results), 1)
-        self.assertIsInstance(analysis.results[0], Failure)
-        self.assertEqual(analysis.results[0].failureid, 'bad-exit-code')
-
-    def test_timeout(self):
-        sourcefile = 'test-sources/harmless.c'
-        tool = self.make_tool()
-        tool.timeout = 0
-        gccinv = GccInvocation(['gcc', sourcefile])
-        analysis = tool.checked_invoke(gccinv, sourcefile)
-        self.assert_metadata(analysis, 'clang-analyzer', sourcefile)
-        self.assertEqual(len(analysis.results), 1)
-        r0 = analysis.results[0]
-        self.assertIsInstance(r0, Failure)
-        self.assertEqual(r0.failureid, 'timeout')
-        self.assert_has_custom_field(analysis, 'timeout')
-        self.assert_has_custom_field(analysis, 'command-line')
-
-    def test_harmless_file(self):
-        analysis = self.invoke('test-sources/harmless.c')
-        #print(analysis)
-        self.assertEqual(len(analysis.results), 0)
-
-    def test_read_through_null(self):
-        analysis = self.invoke('test-sources/read-through-null.c')
-        #print(analysis)
-        #print(analysis.to_json())
-        self.assertEqual(len(analysis.results), 1)
-        r0 = analysis.results[0]
-        self.assertIsInstance(r0, Issue)
-        self.assertEqual(r0.testid, None)
-        self.assertEqual(r0.location.file.givenpath,
-                         'test-sources/read-through-null.c')
-        self.assertEqual(r0.location.point.line, 3)
-        self.assertEqual(r0.message.text,
-                         "Dereference of null pointer")
-        self.assertEqual(r0.severity, None)
-        self.assertIsInstance(r0.trace, Trace)
-
-    def test_out_of_bounds(self):
-        analysis = self.invoke('test-sources/out-of-bounds.c')
-        #print(analysis)
-        self.assertEqual(len(analysis.results), 1)
-
-        r0 = analysis.results[0]
-        self.assertIsInstance(r0, Issue)
-        self.assertEqual(r0.testid, None)
-        self.assertEqual(r0.location.file.givenpath,
-                         'test-sources/out-of-bounds.c')
-        self.assertEqual(r0.location.point.line, 5)
-        self.assertEqual(r0.message.text,
-                         "Undefined or garbage value returned to caller")
-        self.assertEqual(r0.severity, None)
-        self.assertIsInstance(r0.trace, Trace)
-
-if __name__ == '__main__':
-    sys.exit(tool_main(sys.argv, InvokeClangAnalyzer))
diff --git a/checkers/fakegcc_cppcheck.py b/checkers/fakegcc_cppcheck.py
deleted file mode 100755
index 74d86ed..0000000
--- a/checkers/fakegcc_cppcheck.py
+++ /dev/null
@@ -1,146 +0,0 @@
-#!/usr/bin/env python
-#   Copyright 2012, 2013, 2015, 2017 David Malcolm <dmalcolm@redhat.com>
-#   Copyright 2012, 2013, 2015, 2017 Red Hat, Inc.
-#
-#   This is free software: you can redistribute it and/or modify it
-#   under the terms of the GNU General Public License as published by
-#   the Free Software Foundation, either version 3 of the License, or
-#   (at your option) any later version.
-#
-#   This program is distributed in the hope that it will be useful, but
-#   WITHOUT ANY WARRANTY; without even the implied warranty of
-#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-#   General Public License for more details.
-#
-#   You should have received a copy of the GNU General Public License
-#   along with this program.  If not, see
-#   <http://www.gnu.org/licenses/>.
-
-import sys
-import tempfile
-import unittest
-
-from gccinvocation import GccInvocation
-
-from fakegcc import Tool, Context, ToolTests, make_file, make_stats, \
-    tool_main
-
-from firehose.model import Analysis, Generator, Metadata, Failure, \
-    Location, File, Message, Issue, Trace
-from firehose.parsers.cppcheck import parse_file
-
-class InvokeCppcheck(Tool):
-    """
-    Tool subclass that invokes "cppcheck"
-    """
-    def __init__(self, ctxt):
-        Tool.__init__(self, 'cppcheck', ctxt)
-
-    def raw_invoke(self, gccinv, sourcefile):
-        args = ['cppcheck',
-                '--xml', '--xml-version=2',
-                sourcefile]
-        return self._run_subprocess(sourcefile, args)
-
-    def handle_output(self, result):
-        if result.returncode:
-            analysis = self._make_failed_analysis(result.sourcefile, result.timer,
-                                                  msgtext='Bad exit code running %s' % self.name,
-                                                  failureid='bad-exit-code')
-            self.set_custom_fields(result, analysis)
-            return analysis
-
-        # (there doesn't seem to be a way to have cppcheck directly
-        # save its XML output to a given location)
-
-        with tempfile.NamedTemporaryFile() as outfile:
-            outfile.write(result.err)
-            outfile.flush()
-
-            with open(outfile.name) as infile:
-                # Parse stderr into firehose XML format and save:
-                analysis = parse_file(infile,
-                                      file_=make_file(result.sourcefile),
-                                      stats=make_stats(result.timer))
-                self.set_custom_fields(result, analysis)
-                return analysis
-
-    def set_custom_fields(self, result, analysis):
-        analysis.set_custom_field('cppcheck-invocation',
-                                  ' '.join(result.argv))
-        result.set_custom_fields(analysis)
-
-class CppcheckTests(ToolTests):
-    def make_tool(self):
-        ctxt = self.make_ctxt()
-        return InvokeCppcheck(ctxt)
-
-    def verify_basic_metadata(self, analysis, sourcefile):
-        # Verify basic metadata:
-        self.assert_metadata(analysis, 'cppcheck', sourcefile)
-        self.assert_has_custom_field(analysis, 'cppcheck-invocation')
-        self.assert_has_custom_field(analysis, 'stdout')
-        self.assert_has_custom_field(analysis, 'stderr')
-
-    def test_file_not_found(self):
-        analysis = self.invoke('does-not-exist.c')
-        #print(analysis)
-        self.assertEqual(len(analysis.results), 1)
-        self.assertIsInstance(analysis.results[0], Failure)
-        self.assertEqual(analysis.results[0].failureid, 'bad-exit-code')
-
-    def test_timeout(self):
-        sourcefile = 'test-sources/harmless.c'
-        tool = self.make_tool()
-        tool.timeout = 0
-        gccinv = GccInvocation(['gcc', sourcefile])
-        analysis = tool.checked_invoke(gccinv, sourcefile)
-        self.assert_metadata(analysis, 'cppcheck', sourcefile)
-        self.assertEqual(len(analysis.results), 1)
-        r0 = analysis.results[0]
-        self.assertIsInstance(r0, Failure)
-        self.assertEqual(r0.failureid, 'timeout')
-        self.assert_has_custom_field(analysis, 'timeout')
-        self.assert_has_custom_field(analysis, 'command-line')
-
-    def test_harmless_file(self):
-        analysis = self.invoke('test-sources/harmless.c')
-        #print(analysis)
-        self.assertEqual(len(analysis.results), 0)
-
-    def test_read_through_null(self):
-        analysis = self.invoke('test-sources/read-through-null.c')
-        self.assertEqual(len(analysis.results), 1)
-        r0 = analysis.results[0]
-        self.assertIsInstance(r0, Issue)
-        self.assertEqual(r0.testid, 'nullPointer')
-        self.assertEqual(r0.location.file.givenpath,
-                         'test-sources/read-through-null.c')
-        self.assertEqual(r0.location.point.line, 3)
-        self.assertEqual(r0.message.text,
-                         "Null pointer dereference")
-        self.assertEqual(r0.severity, 'error')
-
-    def test_out_of_bounds(self):
-        analysis = self.invoke('test-sources/out-of-bounds.c')
-        #print(analysis)
-        self.assertEqual(len(analysis.results), 2)
-
-        r0 = analysis.results[0]
-        self.assertIsInstance(r0, Issue)
-        self.assertEqual(r0.testid, 'arrayIndexOutOfBounds')
-        self.assertEqual(r0.location.file.givenpath,
-                         'test-sources/out-of-bounds.c')
-        self.assertEqual(r0.location.point.line, 5)
-        self.assertEqual(
-            r0.message.text,
-            "Array 'arr[10]' accessed at index 15, which is out of bounds.")
-        self.assertEqual(r0.severity, 'error')
-
-        r1 = analysis.results[1]
-        self.assertIsInstance(r1, Issue)
-        self.assertEqual(r1.testid, 'uninitvar')
-        # etc
-
-if __name__ == '__main__':
-    sys.exit(tool_main(sys.argv, InvokeCppcheck))
diff --git a/gcc/checkers.c b/gcc/checkers.c
index 4cc715f..d92c141 100644
--- a/gcc/checkers.c
+++ b/gcc/checkers.c
@@ -252,6 +252,6 @@ run_analyzers (const char *path)
   if (0)
     inform (UNKNOWN_LOCATION, "run_analyzers (%qs)", path);
 
-  run_one_analyzer ("../../src/checkers/fakegcc_clang_analyzer.py");
-  run_one_analyzer ("../../src/checkers/fakegcc_cppcheck.py");
+  run_one_analyzer ("../../src/checkers/clang_analyzer.py");
+  run_one_analyzer ("../../src/checkers/cppcheck.py");
 }
-- 
1.8.5.3

