This patch is an example of supporting a proprietary 3rd-party tool: a harness for invoking the Coverity checker.
It uses the '--json-output-v2' option to cov-format-errors, and then uses firehose.parsers.coverity.parse_json_v2 to parse the generated Coverity JSON format, turning it into firehose JSON. This isn't a great example of use of either the checker infrastructure, or of Coverity. As far as I can tell, Coverity is designed to be run on a number of source files at once, performing a relatively cheap data-gathering phase per-source-file, and then performing a more expensive LTO-style analysis that can follow dataflow between source files, thus obtaining much more accurate results that a purely one-file-at-a-time checker can. In contrast, the checker machinery in this patch kit is designed to run one file at a time. The harness code in this patch attempts to "square this circle", but it's not a good fit; it can detect problems that are within one source file, but prevents the checker from finding the more interesting problems that it's normally capable of. checkers/ChangeLog: * coverity.py: New file. --- checkers/coverity.py | 141 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 checkers/coverity.py diff --git a/checkers/coverity.py b/checkers/coverity.py new file mode 100644 index 0000000..533a6ae --- /dev/null +++ b/checkers/coverity.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python +# Copyright 2017 David Malcolm <dmalc...@redhat.com> +# Copyright 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/>. + +# Coverity is a trademark of Synopsys, Inc. in the U.S. and/or other +# countries. + +import os +import sys +import tempfile +import unittest + +from gccinvocation import GccInvocation + +from checker import Checker, Context, CheckerTests, make_file, make_stats, \ + tool_main + +from firehose.model import Analysis, Generator, Metadata, Failure, \ + Location, File, Message, Issue, Trace +from firehose.parsers.coverity import parse_json_v2 + +os.environ['PATH'] = '/opt/coverity/bin:' + os.environ['PATH'] + +class InvokeCoverity(Checker): + """ + Checker subclass that invokes Coverity + """ + name = 'coverity' + + def __init__(self, ctxt, verbose=False): + Checker.__init__(self, ctxt) + self.verbose = verbose + + def raw_invoke(self, gccinv, sourcefile): + # tempfile.TemporaryDirectory is only available from Python 3.2 onwards, + # so handle tempdir cleanup "by hand" + try: + tempdir_name = tempfile.mkdtemp() + + json_name = os.path.join(tempdir_name, 'output.json') + build_args = ['cov-build', '--dir', tempdir_name, 'gcc'] + gccinv.argv[1:] + if self.verbose: + print(build_args) + build_result = self.run_subprocess(sourcefile, build_args) + if self.verbose: + print(build_result) + + analyze_args = ['cov-analyze', '--dir', tempdir_name, + '--wait-for-license'] + analyze_result = self.run_subprocess(sourcefile, analyze_args) + if self.verbose: + print(analyze_result) + + format_args = ['cov-format-errors', '--dir', tempdir_name, + '--json-output-v2', json_name] + format_result = self.run_subprocess(sourcefile, format_args) + if self.verbose: + print(format_result) + + # Parse the output, returning an Analysis instance + analysis = parse_json_v2(json_name) + if self.verbose: + print(analysis) + return analysis + + # FIXME: timing metadata? + + finally: + pass # FIXME: cleanup tempdir + +class CoverityTests(CheckerTests): + def make_tool(self): + return self.make_tool_from_class(InvokeCoverity) + + def verify_basic_metadata(self, analysis, sourcefile): + # Verify basic metadata: + self.assert_metadata(analysis, 'coverity', sourcefile) + + def test_file_not_found(self): + analysis = self.invoke('does-not-exist.c') + 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, 'coverity', sourcefile) + self.assertEqual(len(analysis.results), 1) + r0 = analysis.results[0] + self.assertIsInstance(r0, Failure) + self.assertEqual(r0.failureid, 'timeout') + + def test_out_of_bounds(self): + analysis = self.invoke('test-sources/out-of-bounds.c') + if 0: + print(analysis) + self.assertEqual(len(analysis.results), 2) + + r0 = analysis.results[0] + self.assertIsInstance(r0, Issue) + self.assertEqual(r0.testid, 'OVERRUN') + self.assertEqual(r0.location.point.line, 5) + self.assertEqual(r0.message.text, + 'Overrunning array "arr" of 10 4-byte elements at' + ' element index 15 (byte offset 60) using index "15".') + self.assertIsInstance(r0.trace, Trace) + self.assertEqual(len(r0.trace.states), 1) + + r1 = analysis.results[1] + self.assertIsInstance(r1, Issue) + self.assertEqual(r1.testid, 'UNINIT') + self.assertEqual(r1.location.point.line, 5) + self.assertEqual(r1.message.text, + 'Using uninitialized value "arr[15]".') + self.assertIsInstance(r1.trace, Trace) + self.assertEqual(len(r1.trace.states), 2) + self.assertEqual(r1.trace.states[0].location.point.line, 3) + self.assertEqual(r1.trace.states[0].notes.text, + 'Declaring variable "arr" without initializer.') + self.assertEqual(r1.trace.states[1].location.point.line, 5) + self.assertEqual(r1.trace.states[1].notes.text, + 'Using uninitialized value "arr[15]".') + +if __name__ == '__main__': + sys.exit(tool_main(sys.argv, InvokeCoverity)) -- 1.8.5.3