Files
Laszlo NagyandClaude Fable 5 52fcd971d6 fix: write proper lines in the report_failure output files
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>
2026-08-10 22:14:01 +10:00

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)