From 6c3405f5fee17b71afd5ffc8f03361628fb4f207 Mon Sep 17 00:00:00 2001 From: Gleb Chesnokov Date: Mon, 19 Oct 2026 21:19:30 +0300 Subject: [PATCH] scripts: Add Cppcheck with native build contexts Expose make cppcheck using freshly captured kernel CHECK commands and real userspace compiler invocations. Keep their include paths and definitions unchanged and analyze kernel and userspace contexts separately. Use the shared source scope without additional input exclusions, project models or diagnostic suppressions. Relevant diffs trigger full configured analysis; KUnit coverage follows the actual build configuration. Retain build logs, compilation databases, native XML and checker coverage. Separate source findings from parser failures, missing inputs and incomplete builds, with isolated report and capture fixtures. --- Makefile | 14 ++ scripts/README.md | 26 +++- scripts/analyzer_policy.py | 4 +- scripts/cppcheck/capture.py | 55 +++++++ scripts/cppcheck/cppcheck.py | 208 ++++++++++++++++++++++++++ scripts/lint_kbuild.py | 5 +- scripts/policy/cppcheck.json | 9 ++ scripts/tests/test_analyzer_policy.py | 15 +- scripts/tests/test_cppcheck.py | 99 ++++++++++++ 9 files changed, 428 insertions(+), 7 deletions(-) create mode 100644 scripts/cppcheck/capture.py create mode 100644 scripts/cppcheck/cppcheck.py create mode 100644 scripts/policy/cppcheck.json create mode 100644 scripts/tests/test_cppcheck.py diff --git a/Makefile b/Makefile index 05adda0e7..8bcd4b2f6 100644 --- a/Makefile +++ b/Makefile @@ -170,6 +170,7 @@ help: ' ANALYZER_FORMAT=text|json policy output format (default: text)' \ ' sparse, smatch analyze configured modules through Kbuild' \ ' coccicheck run kernel Coccinelle report rules' \ + ' cppcheck analyze fresh kernel and userspace build contexts' \ ' DIFFBR=commit compare with one commit; select relevant inputs' \ ' lint-test test shared analyzer tooling (no modules)' \ ' all make all' \ @@ -279,6 +280,19 @@ coccicheck: export SCST_LINT_KDIR := $(KDIR) coccicheck: +@$(PYTHON) -B scripts/coccinelle/coccicheck.py +.PHONY: cppcheck lint-userspace +cppcheck: export SCST_LINT_KDIR := $(KDIR) +cppcheck: export SCST_LINT_CC := $(CC) +cppcheck: + +@$(PYTHON) -B scripts/cppcheck/cppcheck.py + +lint-userspace: + +$(MAKE) -C scst/src ../include/scst_itf_ver.h + +$(MAKE) -C $(ISCSI_DIR) progs + +$(MAKE) -C usr/fileio + +$(MAKE) -C usr/stpgd + +$(MAKE) -C usr/events + # Preserve dependency order. The newer QLogic base is compiled for its target # module, but only sources selected by analyzer policy reach the checker. lint-kbuild: diff --git a/scripts/README.md b/scripts/README.md index 9fdac9e12..14f85ef4f 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -14,6 +14,7 @@ direct callers of a relocated script must use its new directory below. | `policy/` | Reviewed shared analyzer input policy; exclusions carry their reasons. | | `sparse_smatch/` | Native Kbuild analyzers (`make sparse`, `make smatch`) and CHECK records. | | `coccinelle/` | Kernel semantic-patch report rules (`make coccicheck`). | +| `cppcheck/` | Native analysis using captured compiler commands (`make cppcheck`). | | `tests/` | Isolated regression fixtures for the shared analyzer helpers (`make lint-test`). | The existing `checkpatch`, `checkpatch_commits`, `checkpatch_diff`, and @@ -79,7 +80,7 @@ python3 -B scripts/analyzer_policy.py validate ``` The Python commands also accept `--format=json` and a policy name: `common`, -`sparse`, `smatch` or `coccicheck`. Use `make analyzer-policy ANALYZER=smatch` to see the +`sparse`, `smatch`, `coccicheck` or `cppcheck`. Use `make analyzer-policy ANALYZER=smatch` to see the effective input scope and all diagnostic exceptions. A file inventory does not establish analysis or build coverage. @@ -208,6 +209,29 @@ incomplete analysis, even when native `spatch` returns zero. Findings return 1; incomplete analysis returns 2 (Make maps either failure to 2). Full output and per-rule coverage are in `.cache/lint/coccicheck.{log,json}`. +## Cppcheck + +```sh +make -j8 cppcheck KDIR=/path/to/kernel/build KUNIT_TEST=y +make -j8 cppcheck KDIR=/path/to/kernel/build KUNIT_TEST=y DIFFBR=master +``` + +The target requires Cppcheck with `--check-level` and `--checkers-report` +support, a prepared kernel and SCST build dependencies. It captures fresh +Kbuild `CHECK` commands and rebuilds userspace programs (including the event +example) to collect their compiler commands. It then runs kernel and userspace +analysis separately with all native checks enabled. Nothing is installed or +loaded. KUnit coverage follows `KUNIT_TEST` and the kernel configuration. + +A relevant diff triggers the full configured analysis, including header +consumers. Explicit or changed C files absent from the build cause a failure. +Real includes and definitions are preserved; no project models, header +substitutions, inline suppressions or diagnostic exceptions are enabled. +Native parser failures and missing includes remain visible as incomplete +analysis. Source findings return 1 and incomplete analysis returns 2; Make +maps both to 2. Full logs and the summary are in `.cache/lint/cppcheck.{log,json}`; +the summary links to captured commands, native XML and checker coverage. + ## Dependencies and tooling tests Shared analyzer tooling requires Linux, Git and Python 3.10+, using only the diff --git a/scripts/analyzer_policy.py b/scripts/analyzer_policy.py index 373bd86d0..159cb2ce1 100644 --- a/scripts/analyzer_policy.py +++ b/scripts/analyzer_policy.py @@ -24,8 +24,8 @@ KBUILD_SCHEMA = { "kernel_exceptions": [{"header": str, "message": str, "reason": str}], } BASIC_SCHEMA = {"version": int, "notes": [str]} -POLICIES = ("common", "sparse", "smatch", "coccicheck") -BASIC_POLICIES = ("coccicheck",) +POLICIES = ("common", "sparse", "smatch", "coccicheck", "cppcheck") +BASIC_POLICIES = ("coccicheck", "cppcheck") def relative_path(value: str, location: str): diff --git a/scripts/cppcheck/capture.py b/scripts/cppcheck/capture.py new file mode 100644 index 000000000..ab9c37fbe --- /dev/null +++ b/scripts/cppcheck/capture.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Record Kbuild CHECK arguments or successful userspace compiler invocations.""" + +import json +import os +from pathlib import Path +import shlex +import sys +import uuid + +sys.dont_write_bytecode = True +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / "scripts")) +from lint_common import entrypoint, run # noqa: E402 +from lint_scope import in_scope # noqa: E402 + + +def source_files(arguments, profile, root): + if profile == "kernel": + candidates = arguments[-1:] + elif "-c" in arguments: + candidates = [arg for arg in arguments if arg.endswith(".c") and not arg.startswith("-")] + else: + return [] + return [Path(arg).resolve() for arg in candidates + if Path(arg).is_file() and in_scope(Path(arg), root, "cppcheck")] + + +def main(): + profile, *arguments = sys.argv[1:] + if profile not in {"kernel", "userspace"}: + raise ValueError(f"unknown capture profile: {profile}") + sources = source_files(arguments, profile, ROOT) + compiler = shlex.split(os.environ.get("SCST_LINT_CC", "cc")) + if not compiler: + raise ValueError("empty userspace compiler command") + # Kernel CHECK receives the real c_flags. It only records; Kbuild still + # invokes its own compiler. Do not run Sparse or add __CHECKER__ definitions. + command = (["cc"] if profile == "kernel" else compiler) + arguments + records = [] + for source in sources: + path = Path(os.environ["SCST_LINT_RECORDS"]) / f"{uuid.uuid4()}.json" + record = {"file": str(source), "directory": str(Path.cwd()), "arguments": command, + "profile": profile, "returncode": 2} + path.write_text(json.dumps(record) + "\n") + records.append((path, record)) + status = run(command).returncode if profile == "userspace" else 0 + for path, record in records: + record["returncode"] = status + path.write_text(json.dumps(record) + "\n") + return status + + +if __name__ == "__main__": + sys.exit(entrypoint(main)) diff --git a/scripts/cppcheck/cppcheck.py b/scripts/cppcheck/cppcheck.py new file mode 100644 index 000000000..e9befa278 --- /dev/null +++ b/scripts/cppcheck/cppcheck.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +"""Run Cppcheck on fresh kernel and userspace commands without project models.""" + +import argparse +import json +import os +from pathlib import Path +import re +import shlex +import shutil +import subprocess +import sys +import tempfile +import xml.etree.ElementTree as ET + +sys.dont_write_bytecode = True +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[1] +sys.path.insert(0, str(ROOT / "scripts")) +from lint_common import copy_output, entrypoint, log_path, run, show_log # noqa: E402 +from lint_kbuild import build_lock, collect, unchecked_invocations # noqa: E402 +from lint_scope import select_sources # noqa: E402 + +INCOMPLETE = {"syntaxError", "unknownMacro", "internalAstError", "internalError", + "preprocessorErrorDirective", "noValidConfiguration", "cppcheckError", + "directiveAsMacroParameter", "missingInclude", "missingIncludeSystem", + "toomanyconfigs"} + + +def native_result(xml, progress, checkers, status, expected, root): + failures = [] + diagnostics = [] + try: + document = ET.fromstring(xml) + if document.tag != "results" or document.get("version") != "2" or ( + document.find("cppcheck") is None or document.find("errors") is None): + raise ValueError("unexpected Cppcheck XML structure") + for error in document.findall("errors/error"): + if not error.get("id") or not error.get("severity") or error.get("msg") is None: + raise ValueError("incomplete Cppcheck diagnostic") + diagnostics.append({**error.attrib, + "locations": [location.attrib for location in error.findall("location")]}) + if error.get("id") in INCOMPLETE: + failures.append(f"{error.get('id')}: {error.get('msg')}") + except (ET.ParseError, ValueError) as error: + failures.append(f"invalid or incomplete XML report: {error}") + if status not in {0, 3} or (status == 3 and not diagnostics): + failures.append(f"unexpected native status: {status}") + if ("No critical errors encountered." not in checkers or + "There were critical errors" in checkers or + not re.search(r"^Yes\s+\S+", checkers, re.MULTILINE)): + failures.append("missing checker coverage or critical native errors") + checked = set() + for line in progress.splitlines(): + match = re.match(r"^Checking (.+?\.c)(?: |:).*\.\.\.$", line) + if match: + checked.add((root / match[1]).resolve()) + if re.match(r"^(?:cppcheck: (?:error|warning):|Failed to|Internal error)", line): + failures.append(line) + missing = sorted(str(path) for path in set(expected) - checked) + failures.extend(f"no native checking record for {path}" for path in missing) + return {"native_status": status, "diagnostics": diagnostics, "failures": failures, + "checked_files": sorted(str(path) for path in checked & set(expected)), + "remaining": len(diagnostics), "complete": not failures} + + +def capture_userspace(directory, log): + env = dict(os.environ, SCST_LINT_RECORDS=str(directory)) + compiler = shlex.join([sys.executable, str(HERE / "capture.py"), "userspace"]) + command = ["make", "--no-silent", "-k", "-B", "-C", str(ROOT), "lint-userspace", + "CC=" + compiler] + with log.open("ab") as stream: + result = run(command, env=env, stdout=stream, stderr=subprocess.STDOUT, + progress="Cppcheck userspace build context") + records = [json.loads(path.read_text()) for path in sorted(directory.glob("*.json"))] + return result.returncode, records + + +def project_entry(record): + """Resolve paths from the compiler cwd, not the analyzer's cwd. + + Native project import leaves forced includes relative and does not use + -isystem paths in its own frontend. Pass the same real headers via -I. + Keep the original command untouched in the capture record. + """ + directory = Path(record["directory"]) + arguments = iter(record["arguments"]) + normalized = [] + system_includes = [] + for argument in arguments: + option = next((flag for flag in ("-isystem", "-include", "-I") + if argument.startswith(flag)), None) + if option is None: + normalized.append(argument) + continue + value = argument[len(option):] or next(arguments, "") + if not value: + raise ValueError(f"missing operand for compiler option {option}") + path = directory / value + if option != "-include" or path.is_file(): + value = str(path.resolve()) + if option == "-isystem": + system_includes.extend(["-I", value]) + else: + normalized.extend([option, value]) + # GCC searches explicit system directories after every ordinary -I path. + normalized.extend(system_includes) + return {"file": record["file"], "directory": str(directory), "arguments": normalized} + + +def analyze(profile, records, directory, log): + project = directory / f"{profile}.json" + # Keep each distinct compiler context, including repeated files with + # different definitions. Do not strip include paths or rewrite headers. + commands = {json.dumps(project_entry(row), sort_keys=True) for row in records} + project.write_text("[\n" + ",\n".join(sorted(commands)) + "\n]\n") + stdout = directory / f"{profile}.stdout" + xml = directory / f"{profile}.xml" + checkers = directory / f"{profile}.checkers" + checkers.write_text("") + command = ["cppcheck", "--project=" + str(project), "--enable=all", "--inconclusive", + "--check-level=exhaustive", "--force", "--xml", "--xml-version=2", + "--error-exitcode=3", "--checkers-report=" + str(checkers)] + try: + with stdout.open("wb") as output, xml.open("wb") as errors: + result = run(command, cwd=ROOT, stdout=output, stderr=errors, + env=dict(os.environ, NO_COLOR="1"), progress=f"Cppcheck {profile}") + finally: + with log.open("ab") as stream: + stream.write(f"\nCppcheck {profile}: {shlex.join(command)}\n".encode()) + for path in (stdout, xml, checkers): + if path.exists(): + copy_output(path, stream) + record = native_result(xml.read_text(errors="replace"), stdout.read_text(errors="replace"), + checkers.read_text(errors="replace"), result.returncode, + [Path(row["file"]) for row in records], ROOT) + return {"profile": profile, "translation_units": len(commands), **record} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--base", default=os.environ.get("DIFFBR") or None) + parser.add_argument("files", nargs="*", type=Path) + args = parser.parse_args() + with build_lock(ROOT): + log = log_path(ROOT, "cppcheck.log") + report = log.with_suffix(".json") + summary = {"tool": "cppcheck", "run_id": os.environ.get("SCST_LINT_RUN_ID"), + "outcome": "incomplete", "exit_code": 2, "profiles": [], + "remaining": 0, "checked_files": []} + report.write_text(json.dumps(summary, indent=2) + "\n") + show_log(log) + try: + selected = select_sources(ROOT, args.files, args.base, "cppcheck") + summary["selected_files"] = [p.relative_to(ROOT).as_posix() for p in selected] + if not selected: + print("No eligible Cppcheck inputs; analysis skipped.") + summary.update(outcome="skipped", exit_code=0) + return 0 + if not shutil.which("cppcheck"): + raise ValueError("cppcheck is not installed") + with log.open("ab") as stream: + result = run(["cppcheck", "--version"], stdout=stream, stderr=subprocess.STDOUT) + if result.returncode: + raise ValueError(f"cppcheck version probe failed: {result.returncode}") + # Fresh records prevent stale incremental-build state from implying + # coverage. Retain commands and native reports for diagnosis. + directory = Path(tempfile.mkdtemp(prefix="cppcheck-", dir=log.parent)) + summary["records_directory"] = str(directory) + kernel_records = directory / "kernel" + user_records = directory / "userspace" + kernel_records.mkdir() + user_records.mkdir() + checker = [sys.executable, str(HERE / "capture.py"), "kernel"] + kernel_status, kernel = collect(ROOT, "cppcheck", kernel_records, log, checker=checker) + missing_checks = unchecked_invocations(ROOT, "cppcheck", log, kernel) + user_status, userspace = capture_userspace(user_records, log) + records = kernel + userspace + captured = {Path(row["file"]) for row in records} + missing = ([str(path) for path in selected if path.suffix == ".c" and path not in captured] + if args.files or args.base else []) + summary.update(kernel_make_status=kernel_status, userspace_make_status=user_status, + unchecked_files=missing, unrecorded_checks=missing_checks) + if (kernel_status or user_status or not kernel or not userspace or missing or + missing_checks or any(row["returncode"] for row in records)): + raise ValueError("incomplete Cppcheck build context; inspect the build log and summary") + for profile, rows in (("kernel", kernel), ("userspace", userspace)): + result = analyze(profile, rows, directory, log) + summary["profiles"].append(result) + summary["remaining"] += result["remaining"] + report.write_text(json.dumps(summary, indent=2) + "\n") + if any(not row["complete"] for row in summary["profiles"]): + print("Cppcheck analysis is incomplete; inspect the full native reports.", file=sys.stderr) + return 2 + summary["checked_files"] = sorted(path.relative_to(ROOT).as_posix() for path in captured) + status = int(bool(summary["remaining"])) + summary.update(outcome="findings" if status else "clean", exit_code=status) + print(f"Cppcheck: {len(captured)} files, {summary['remaining']} diagnostic(s).") + return status + finally: + report.write_text(json.dumps(summary, indent=2) + "\n") + if os.environ.get("LINT_DEFER_REPORTS") != "1": + copy_output(log) + show_log(log) + + +if __name__ == "__main__": + sys.exit(entrypoint(main)) diff --git a/scripts/lint_kbuild.py b/scripts/lint_kbuild.py index 993b6f094..cea2cd573 100644 --- a/scripts/lint_kbuild.py +++ b/scripts/lint_kbuild.py @@ -54,10 +54,11 @@ def validate_checker(root, tool, log): raise ValueError(f"{tool} is incompatible with {probe}; module build was not started") -def collect(root, tool, directory, log): +def collect(root, tool, directory, log, *, checker=None): """Retain CHECK exits even when the enclosing Kbuild discards them.""" env = dict(os.environ, SCST_LINT_RECORDS=str(directory)) - checker = shlex.join([sys.executable, str(root / "scripts/sparse_smatch/check.py"), tool]) + checker = shlex.join(checker or + [sys.executable, str(root / "scripts/sparse_smatch/check.py"), tool]) command = ["make", "--no-silent", "-k", "-C", str(root), "lint-kbuild", "C=2", "V=0", "CHECK=" + checker] with log.open("ab") as output: diff --git a/scripts/policy/cppcheck.json b/scripts/policy/cppcheck.json new file mode 100644 index 000000000..c27d91c51 --- /dev/null +++ b/scripts/policy/cppcheck.json @@ -0,0 +1,9 @@ +{ + "version": 1, + "notes": [ + "Capture fresh kernel CHECK and userspace compiler commands.", + "Analyze configured translation units with native headers and definitions.", + "KUNIT_TEST=y includes KUnit sources through their actual Kbuild context.", + "No project models, inline suppressions or diagnostic exceptions." + ] +} diff --git a/scripts/tests/test_analyzer_policy.py b/scripts/tests/test_analyzer_policy.py index 1b1532da8..c5d76bc14 100644 --- a/scripts/tests/test_analyzer_policy.py +++ b/scripts/tests/test_analyzer_policy.py @@ -156,7 +156,8 @@ class PolicyCliTests(unittest.TestCase): (self.scripts / "policy").mkdir(parents=True) source = analyzer_policy.POLICY_PATH.parent.parent for name in ("analyzer_policy.py", "lint_scope.py", "policy/common.json", - "policy/sparse.json", "policy/smatch.json"): + "policy/sparse.json", "policy/smatch.json", "policy/coccicheck.json", + "policy/cppcheck.json"): shutil.copyfile(source / name, self.scripts / name) def run_cli(self, *args): @@ -211,7 +212,7 @@ class PolicyCliTests(unittest.TestCase): "usr", "iscsi-scst/usr"}) def test_configuration_and_inventory_errors_fail_visibly(self): - unknown = self.run_cli("show", "cppcheck") + unknown = self.run_cli("show", "unknown-tool") self.assertEqual(unknown.returncode, 2) no_git = self.run_cli("files", "--format=json") self.assertEqual(no_git.returncode, 2) @@ -230,6 +231,16 @@ class PolicyCliTests(unittest.TestCase): self.assertEqual(broken.stdout, "") self.assertIn("analyzer policy:", broken.stderr) + def test_basic_analyzers_inherit_common_scope_without_exceptions(self): + common = json.loads((self.scripts / "policy/common.json").read_text()) + for tool in ("coccicheck", "cppcheck"): + with self.subTest(tool=tool): + result = self.run_cli("show", tool, "--format=json") + self.assertEqual(result.returncode, 0, result.stderr) + report = json.loads(result.stdout) + self.assertEqual(report["effective_source_policy"], common) + self.assertEqual(set(report["configured_policy"]), {"version", "notes"}) + if __name__ == "__main__": unittest.main() diff --git a/scripts/tests/test_cppcheck.py b/scripts/tests/test_cppcheck.py new file mode 100644 index 000000000..25892520c --- /dev/null +++ b/scripts/tests/test_cppcheck.py @@ -0,0 +1,99 @@ +"""Cppcheck report/capture fixtures without native analysis or compilation.""" + +import json +from pathlib import Path +import sys +import tempfile +import unittest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from analyzer_policy import read_policy +from cppcheck.capture import source_files +from cppcheck.cppcheck import native_result, project_entry + + +class CppcheckTests(unittest.TestCase): + def setUp(self): + self.source = Path("/tmp/scst/scst/src/file.c") + self.progress = f"Checking {self.source} ...\n" + self.checkers = "No critical errors encountered.\nYes CheckOther::checkSomething\n" + + def report(self, diagnostics="", status=0, progress=None, checkers=None): + xml = ('' + f'{diagnostics}') + return native_result(xml, self.progress if progress is None else progress, + self.checkers if checkers is None else checkers, + status, [self.source], Path("/tmp/scst")) + + def test_valid_empty_report_requires_native_coverage(self): + self.assertTrue(self.report()["complete"]) + for kwargs in ({"progress": ""}, {"checkers": ""}, {"status": 1}, {"status": 3}, + {"checkers": "There were critical errors (syntaxError).\nYes Check\n"}): + with self.subTest(kwargs=kwargs): + self.assertFalse(self.report(**kwargs)["complete"]) + + def test_relative_progress_and_configuration_suffix(self): + self.assertTrue(self.report(progress="Checking scst/src/file.c: CONFIG_X=1...\n")["complete"]) + + def test_findings_are_preserved(self): + diagnostic = '' + result = self.report(diagnostic, 3) + self.assertTrue(result["complete"]) + self.assertEqual(result["remaining"], 1) + self.assertEqual(result["diagnostics"][0]["id"], "nullPointer") + + def test_native_zero_with_parser_failure_is_incomplete(self): + for identifier in ("syntaxError", "unknownMacro", "missingIncludeSystem", "internalError"): + with self.subTest(identifier=identifier): + diagnostic = f'' + self.assertFalse(self.report(diagnostic)["complete"]) + + def test_malformed_and_partial_xml_are_incomplete(self): + for xml in ("", "", '', + ''): + with self.subTest(xml=xml): + self.assertFalse(native_result(xml, self.progress, self.checkers, 0, + [self.source], Path("/tmp/scst"))["complete"]) + + def test_capture_requires_compile_and_shared_scope(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + source = root / "usr/fileio/file.c" + source.parent.mkdir(parents=True) + source.touch() + self.assertEqual(source_files(["-c", str(source)], "userspace", root), [source]) + self.assertEqual(source_files(["-M", str(source)], "userspace", root), []) + self.assertEqual(source_files(["-Iinclude", str(source)], "kernel", root), [source]) + outside = root / "qla2x00t/legacy.c" + outside.parent.mkdir() + outside.touch() + self.assertEqual(source_files([str(outside)], "kernel", root), []) + + def test_basic_policy_rejects_models_and_suppressions(self): + with tempfile.TemporaryDirectory() as temporary: + path = Path(temporary) / "policy.json" + policy = {"version": 1, "notes": ["Native commands."]} + path.write_text(json.dumps(policy)) + self.assertEqual(read_policy(path, "cppcheck"), policy) + for field in ("models", "suppressions", "source_exceptions"): + path.write_text(json.dumps({**policy, field: []})) + with self.subTest(field=field), self.assertRaises(ValueError): + read_policy(path, "cppcheck") + + def test_project_retains_real_headers_relative_to_compiler_directory(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + (root / "include").mkdir() + (root / "include/config.h").touch() + arguments = ["cc", "-Iinclude", "-isystem", "system", "-include", "include/config.h", + "-D__KERNEL__", "-c", "file.c"] + record = {"directory": str(root), "file": str(root / "file.c"), "arguments": arguments} + entry = project_entry(record) + self.assertEqual(entry["arguments"], ["cc", "-I", str(root / "include"), + "-include", str(root / "include/config.h"), + "-D__KERNEL__", "-c", "file.c", "-I", str(root / "system")]) + self.assertEqual(record["arguments"], arguments) + + +if __name__ == "__main__": + unittest.main()