mirror of
https://github.com/rizsotto/scan-build.git
synced 2026-11-03 17:11:10 +01:00
The captured analyzer output was written to '.stderr.txt' without newlines, concatenating everything into a single line (run_command returns splitlines() output). The '.info.txt' file used os.linesep through a text-mode handle, which Windows translates into "\r\r\n". Write "\n" and let text mode do the platform translation, and drop the redundant handle.close() calls inside the with blocks. Extend the unit test to use multi-line output, which now catches the missing-newline case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
357 lines
14 KiB
Python
357 lines
14 KiB
Python
# SPDX-License-Identifier: MIT
|
|
|
|
import glob
|
|
import os
|
|
import os.path
|
|
import platform
|
|
import tempfile
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
import clanganalyzer.analyze as sut
|
|
from clanganalyzer.clang import ClangCompilationError
|
|
|
|
IS_WINDOWS = os.getenv("windows")
|
|
|
|
|
|
class Spy:
|
|
def __init__(self):
|
|
self.arg = None
|
|
self.success = 0
|
|
|
|
def call(self, params):
|
|
self.arg = params
|
|
return self.success
|
|
|
|
|
|
class FilteringFlagsTest(unittest.TestCase):
|
|
@staticmethod
|
|
def classify_parameters(flags):
|
|
spy = Spy()
|
|
opts = {"flags": flags}
|
|
sut.classify_parameters(opts, spy.call)
|
|
return spy.arg
|
|
|
|
def assertLanguage(self, expected, flags):
|
|
self.assertEqual(expected, FilteringFlagsTest.classify_parameters(flags)["language"])
|
|
|
|
def test_language_captured(self):
|
|
self.assertLanguage(None, [])
|
|
self.assertLanguage("c", ["-x", "c"])
|
|
self.assertLanguage("cpp", ["-x", "cpp"])
|
|
|
|
def assertArch(self, expected, flags):
|
|
self.assertEqual(expected, FilteringFlagsTest.classify_parameters(flags)["arch_list"])
|
|
|
|
def test_arch(self):
|
|
self.assertArch([], [])
|
|
self.assertArch(["mips"], ["-arch", "mips"])
|
|
self.assertArch(["mips", "i386"], ["-arch", "mips", "-arch", "i386"])
|
|
|
|
def assertFlagsChanged(self, expected, flags):
|
|
self.assertEqual(expected, FilteringFlagsTest.classify_parameters(flags)["flags"])
|
|
|
|
def assertFlagsUnchanged(self, flags):
|
|
self.assertFlagsChanged(flags, flags)
|
|
|
|
def assertFlagsFiltered(self, flags):
|
|
self.assertFlagsChanged([], flags)
|
|
|
|
def test_optimalizations_pass(self):
|
|
self.assertFlagsUnchanged(["-O"])
|
|
self.assertFlagsUnchanged(["-O1"])
|
|
self.assertFlagsUnchanged(["-Os"])
|
|
self.assertFlagsUnchanged(["-O2"])
|
|
self.assertFlagsUnchanged(["-O3"])
|
|
|
|
def test_include_pass(self):
|
|
self.assertFlagsUnchanged([])
|
|
self.assertFlagsUnchanged(["-include", "/usr/local/include"])
|
|
self.assertFlagsUnchanged(["-I."])
|
|
self.assertFlagsUnchanged(["-I", "."])
|
|
self.assertFlagsUnchanged(["-I/usr/local/include"])
|
|
self.assertFlagsUnchanged(["-I", "/usr/local/include"])
|
|
self.assertFlagsUnchanged(["-I/opt", "-I", "/opt/otp/include"])
|
|
self.assertFlagsUnchanged(["-isystem", "/path"])
|
|
self.assertFlagsUnchanged(["-isystem=/path"])
|
|
|
|
def test_define_pass(self):
|
|
self.assertFlagsUnchanged(["-DNDEBUG"])
|
|
self.assertFlagsUnchanged(["-UNDEBUG"])
|
|
self.assertFlagsUnchanged(["-Dvar1=val1", "-Dvar2=val2"])
|
|
self.assertFlagsUnchanged(['-Dvar="val ues"'])
|
|
|
|
def test_output_filtered(self):
|
|
self.assertFlagsFiltered(["-o", "source.o"])
|
|
|
|
def test_some_warning_filtered(self):
|
|
self.assertFlagsFiltered(["-Wall"])
|
|
self.assertFlagsFiltered(["-Wnoexcept"])
|
|
self.assertFlagsFiltered(["-Wreorder", "-Wunused", "-Wundef"])
|
|
self.assertFlagsUnchanged(["-Wno-reorder", "-Wno-unused"])
|
|
|
|
def test_compile_only_flags_pass(self):
|
|
self.assertFlagsUnchanged(["-std=C99"])
|
|
self.assertFlagsUnchanged(["-nostdinc"])
|
|
self.assertFlagsUnchanged(["-isystem", "/image/debian"])
|
|
self.assertFlagsUnchanged(["-iprefix", "/usr/local"])
|
|
self.assertFlagsUnchanged(["-iquote=me"])
|
|
self.assertFlagsUnchanged(["-iquote", "me"])
|
|
|
|
def test_compile_and_link_flags_pass(self):
|
|
self.assertFlagsUnchanged(["-fsinged-char"])
|
|
self.assertFlagsUnchanged(["-fPIC"])
|
|
self.assertFlagsUnchanged(["-stdlib=libc++"])
|
|
self.assertFlagsUnchanged(["--sysroot", "/"])
|
|
self.assertFlagsUnchanged(["-isysroot", "/"])
|
|
|
|
def test_some_flags_filtered(self):
|
|
self.assertFlagsFiltered(["-g"])
|
|
self.assertFlagsFiltered(["-fsyntax-only"])
|
|
self.assertFlagsFiltered(["-save-temps"])
|
|
self.assertFlagsFiltered(["-init", "my_init"])
|
|
self.assertFlagsFiltered(["-sectorder", "a", "b", "c"])
|
|
|
|
def test_truncated_flag_list_does_not_raise(self):
|
|
# a flag expecting an argument at the end of the list shall not
|
|
# raise StopIteration (which imap_unordered would swallow silently)
|
|
self.assertFlagsFiltered(["-o"])
|
|
self.assertFlagsChanged(["-I."], ["-I.", "-o"])
|
|
self.assertArch([], ["-arch"])
|
|
self.assertLanguage(None, ["-x"])
|
|
self.assertFlagsFiltered(["-sectorder", "a"])
|
|
|
|
|
|
class RunAnalyzerTest(unittest.TestCase):
|
|
@staticmethod
|
|
def run_analyzer(content, failures_report):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
filename = os.path.join(tmpdir, "test.cpp")
|
|
with open(filename, "w") as handle:
|
|
handle.write(content)
|
|
|
|
opts = {
|
|
"clang": "clang",
|
|
"directory": os.getcwd(),
|
|
"flags": [],
|
|
"direct_args": [],
|
|
"source": filename,
|
|
"output_dir": tmpdir,
|
|
"output_format": "plist",
|
|
"output_failures": failures_report,
|
|
}
|
|
spy = Spy()
|
|
result = sut.run_analyzer(opts, spy.call)
|
|
return result, spy.arg
|
|
|
|
def test_run_analyzer(self):
|
|
content = "int div(int n, int d) { return n / d; }"
|
|
(result, fwds) = RunAnalyzerTest.run_analyzer(content, False)
|
|
self.assertEqual(None, fwds)
|
|
self.assertEqual(0, result["exit_code"])
|
|
|
|
def test_run_analyzer_crash(self):
|
|
content = "int div(int n, int d) { return n / d }"
|
|
(result, fwds) = RunAnalyzerTest.run_analyzer(content, False)
|
|
self.assertEqual(None, fwds)
|
|
self.assertEqual(1, result["exit_code"])
|
|
|
|
def test_run_analyzer_crash_and_forwarded(self):
|
|
content = "int div(int n, int d) { return n / d }"
|
|
(_, fwds) = RunAnalyzerTest.run_analyzer(content, True)
|
|
self.assertEqual(1, fwds["exit_code"])
|
|
self.assertTrue(len(fwds["error_output"]) > 0)
|
|
|
|
def test_run_analyzer_rejected_flags_reported_not_raised(self):
|
|
# a compilation database entry with flags clang rejects shall not
|
|
# blow up the run, but be reported as a failed analysis
|
|
opts = {
|
|
"clang": "clang",
|
|
"directory": os.getcwd(),
|
|
"flags": ["-fno-reorder-functions"],
|
|
"direct_args": [],
|
|
"source": "test.c",
|
|
"output_dir": tempfile.gettempdir(),
|
|
"output_format": "html",
|
|
"output_failures": False,
|
|
}
|
|
spy = Spy()
|
|
error = ClangCompilationError("clang: error: unknown argument: '-fno-reorder-functions'")
|
|
with mock.patch.object(sut, "get_arguments", side_effect=error):
|
|
result = sut.run_analyzer(opts, spy.call)
|
|
self.assertEqual(None, spy.arg)
|
|
self.assertEqual(1, result["exit_code"])
|
|
self.assertTrue(len(result["error_output"]) > 0)
|
|
|
|
|
|
class ReportFailureTest(unittest.TestCase):
|
|
def assertUnderFailures(self, path):
|
|
self.assertEqual("failures", os.path.basename(os.path.dirname(path)))
|
|
|
|
def test_report_failure_create_files(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
# create input file
|
|
filename = os.path.join(tmp_dir, "test.c")
|
|
with open(filename, "w") as handle:
|
|
handle.write("int main() { return 0")
|
|
uname_msg = " ".join(platform.uname()).strip()
|
|
error_lines = ["error: this is my error output", "note: with a second line"]
|
|
# execute test
|
|
opts = {
|
|
"clang": "clang",
|
|
"directory": os.getcwd(),
|
|
"flags": [],
|
|
"source": filename,
|
|
"output_dir": tmp_dir,
|
|
"language": "c",
|
|
"error_output": error_lines,
|
|
"exit_code": 13,
|
|
}
|
|
sut.report_failure(opts)
|
|
# find the info file
|
|
pp_files = glob.glob(os.path.join(tmp_dir, "failures", "*.i"))
|
|
self.assertIsNot(pp_files, [])
|
|
pp_file = pp_files[0]
|
|
# info file generated and content dumped
|
|
info_file = pp_file + ".info.txt"
|
|
self.assertTrue(os.path.exists(info_file))
|
|
with open(info_file) as info_handler:
|
|
lines = [line.strip() for line in info_handler.readlines() if line.strip()]
|
|
self.assertEqual("Other Error", lines[1])
|
|
self.assertEqual(uname_msg, lines[3])
|
|
# error file generated and content dumped
|
|
error_file = pp_file + ".stderr.txt"
|
|
self.assertTrue(os.path.exists(error_file))
|
|
with open(error_file) as error_handle:
|
|
# each captured output line shall be a line in the file
|
|
self.assertEqual([line + "\n" for line in error_lines], error_handle.readlines())
|
|
|
|
|
|
class AnalyzerTest(unittest.TestCase):
|
|
def test_nodebug_macros_appended(self):
|
|
def test(flags):
|
|
spy = Spy()
|
|
opts = {"flags": flags, "force_debug": True}
|
|
self.assertEqual(spy.success, sut.filter_debug_flags(opts, spy.call))
|
|
return spy.arg["flags"]
|
|
|
|
self.assertEqual(["-UNDEBUG"], test([]))
|
|
self.assertEqual(["-DNDEBUG", "-UNDEBUG"], test(["-DNDEBUG"]))
|
|
self.assertEqual(["-DSomething", "-UNDEBUG"], test(["-DSomething"]))
|
|
|
|
def test_set_language_fall_through(self):
|
|
def language(expected, input):
|
|
spy = Spy()
|
|
input.update({"compiler": "c", "source": "test.c"})
|
|
self.assertEqual(spy.success, sut.language_check(input, spy.call))
|
|
self.assertEqual(expected, spy.arg["language"])
|
|
|
|
language("c", {"language": "c", "flags": []})
|
|
language("c++", {"language": "c++", "flags": []})
|
|
|
|
def test_set_language_stops_on_not_supported(self):
|
|
spy = Spy()
|
|
input = {"compiler": "c", "flags": [], "source": "test.java", "language": "java"}
|
|
self.assertEqual({}, sut.language_check(input, spy.call))
|
|
self.assertIsNone(spy.arg)
|
|
|
|
def test_set_language_sets_flags(self):
|
|
def flags(expected, input):
|
|
spy = Spy()
|
|
input.update({"compiler": "c", "source": "test.c"})
|
|
self.assertEqual(spy.success, sut.language_check(input, spy.call))
|
|
self.assertEqual(expected, spy.arg["flags"])
|
|
|
|
flags(["-x", "c"], {"language": "c", "flags": []})
|
|
flags(["-x", "c++"], {"language": "c++", "flags": []})
|
|
|
|
def test_set_language_from_filename(self):
|
|
def language(expected, input):
|
|
spy = Spy()
|
|
input.update({"language": None, "flags": []})
|
|
self.assertEqual(spy.success, sut.language_check(input, spy.call))
|
|
self.assertEqual(expected, spy.arg["language"])
|
|
|
|
language("c", {"source": "file.c", "compiler": "c"})
|
|
language("c++", {"source": "file.c", "compiler": "c++"})
|
|
language("c++", {"source": "file.cxx", "compiler": "c"})
|
|
language("c++", {"source": "file.cxx", "compiler": "c++"})
|
|
language("c++", {"source": "file.cpp", "compiler": "c++"})
|
|
language("c-cpp-output", {"source": "file.i", "compiler": "c"})
|
|
language("c++-cpp-output", {"source": "file.i", "compiler": "c++"})
|
|
|
|
def test_arch_loop_sets_flags(self):
|
|
def flags(archs):
|
|
spy = Spy()
|
|
input = {"flags": [], "arch_list": archs}
|
|
sut.arch_check(input, spy.call)
|
|
return spy.arg["flags"]
|
|
|
|
self.assertEqual([], flags([]))
|
|
self.assertEqual(["-arch", "i386"], flags(["i386"]))
|
|
self.assertEqual(["-arch", "i386"], flags(["i386", "ppc"]))
|
|
self.assertEqual(["-arch", "sparc"], flags(["i386", "sparc"]))
|
|
|
|
def test_arch_loop_stops_on_not_supported(self):
|
|
def stop(archs):
|
|
spy = Spy()
|
|
input = {"flags": [], "arch_list": archs}
|
|
self.assertEqual({}, sut.arch_check(input, spy.call))
|
|
self.assertIsNone(spy.arg)
|
|
|
|
stop(["ppc"])
|
|
stop(["ppc64"])
|
|
|
|
|
|
@sut.require([])
|
|
def method_without_expecteds(opts):
|
|
return 0
|
|
|
|
|
|
@sut.require(["this", "that"])
|
|
def method_with_expecteds(opts):
|
|
return 0
|
|
|
|
|
|
class AnalyzerTestException(Exception):
|
|
"""Test-specific exception for testing exception handling."""
|
|
|
|
|
|
@sut.require([])
|
|
def method_exception_from_inside(opts):
|
|
raise AnalyzerTestException("here is one")
|
|
|
|
|
|
class RequireDecoratorTest(unittest.TestCase):
|
|
def test_method_without_expecteds(self):
|
|
self.assertEqual(method_without_expecteds({}), 0)
|
|
self.assertEqual(method_without_expecteds({}), 0)
|
|
self.assertEqual(method_without_expecteds({"this": 2}), 0)
|
|
self.assertEqual(method_without_expecteds({"that": 3}), 0)
|
|
|
|
def test_method_with_expecteds(self):
|
|
self.assertRaises(AssertionError, method_with_expecteds, {})
|
|
self.assertRaises(AssertionError, method_with_expecteds, {})
|
|
self.assertRaises(AssertionError, method_with_expecteds, {"this": 2})
|
|
self.assertRaises(AssertionError, method_with_expecteds, {"that": 3})
|
|
self.assertEqual(method_with_expecteds({"this": 0, "that": 3}), 0)
|
|
|
|
def test_method_exception_not_caught(self):
|
|
self.assertRaises(AnalyzerTestException, method_exception_from_inside, {})
|
|
|
|
|
|
class ReportDirectoryTest(unittest.TestCase):
|
|
# Test that successive report directory names ascend in lexicographic
|
|
# order. This is required so that report directories from two runs of
|
|
# clanganalyzer can be easily matched up to compare results.
|
|
@unittest.skipIf(IS_WINDOWS, "windows has low resolution timer")
|
|
def test_directory_name_comparison(self):
|
|
with (
|
|
tempfile.TemporaryDirectory() as tmp_dir,
|
|
sut.report_directory(tmp_dir, False) as report_dir1,
|
|
sut.report_directory(tmp_dir, False) as report_dir2,
|
|
sut.report_directory(tmp_dir, False) as report_dir3,
|
|
):
|
|
self.assertLess(report_dir1, report_dir2)
|
|
self.assertLess(report_dir2, report_dir3)
|