Files
eigen/scripts/test_tidy_compile_db.py
2026-08-19 13:30:23 -07:00

253 lines
11 KiB
Python

#!/usr/bin/env python3
# SPDX-FileCopyrightText: The Eigen Authors
# SPDX-License-Identifier: MPL-2.0
"""Unit tests for scripts/tidy_compile_db.py.
The reduction decides what clang-tidy parses, so the cases that matter are the
ones where dropping an entry would drop an added line with it: a split part
selected by a guard rather than by the added text, several parts named at once,
and a second build configuration of the same source. Synthetic databases keep
the tests independent of a configured build directory; one case reads
`test/bdcsvd.cpp` from the tree so the guard shapes stay real.
Usage: python3 scripts/test_tidy_compile_db.py
"""
import json
import os
import subprocess
import sys
import tempfile
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from style_common import REPO_ROOT # noqa: E402
from tidy_compile_db import (MAX_PARTS, added_lines, configuration_of, part_of, # noqa: E402
reduce_entries, required_parts, summarize)
SCRIPT = os.path.join(REPO_ROOT, "scripts", "tidy_compile_db.py")
def entry(source, part=None, extra=(), output="obj"):
"""A compilation-database entry the way CMake's Ninja generator writes one."""
flags = " ".join(list(extra) + (["-DEIGEN_TEST_PART_%s=1" % part] if part else []))
return {"directory": "/build", "file": source,
"command": "/usr/bin/c++ %s -I/repo -std=c++14 -o %s -c %s" % (flags, output, source),
"output": output}
def parts_kept(matches, text, lines):
keep, _, _ = reduce_entries(matches, text, lines)
return [part_of(kept) for kept in keep]
def test_part_define_does_not_distinguish_configurations():
"""Entries differing only in the part share one configuration."""
first = entry("/repo/test/t.cpp", part="1", output="t_1.o")
second = entry("/repo/test/t.cpp", part="2", output="t_2.o")
assert configuration_of(first) == configuration_of(second)
assert part_of(first) == "1" and part_of(second) == "2"
# A macro that is not a part define does distinguish them, and so does no
# define at all.
shared = entry("/repo/blas/b.cpp", extra=["-DEIGEN_BLAS_BUILD_DLL", "-fPIC"])
assert configuration_of(shared) != configuration_of(entry("/repo/blas/b.cpp"))
def test_unguarded_lines_collapse_to_one_part():
matches = [entry("/repo/test/t.cpp", part=str(n), output="t_%d.o" % n) for n in (1, 2, 3)]
text = "void f() {\n int i = 0;\n}\n"
assert parts_kept(matches, text, [2]) == ["1"], parts_kept(matches, text, [2])
def test_guarded_line_selects_its_part():
"""The added text names no part; the guard around it does.
This is the case a scan of the diff alone gets wrong: with part 1 selected,
the preprocessor removes the whole changed region.
"""
matches = [entry("/repo/test/t.cpp", part=str(n), output="t_%d.o" % n) for n in (1, 2, 3)]
text = ("#if defined(EIGEN_TEST_PART_3) || defined(EIGEN_TEST_PART_ALL)\n"
"void guarded() { int* p = 0; }\n"
"#endif\n")
assert parts_kept(matches, text, [2]) == ["3"]
def test_every_named_part_is_retained():
"""Additions naming two parts keep both, not just the first."""
matches = [entry("/repo/test/t.cpp", part=str(n), output="t_%d.o" % n) for n in (1, 2, 3)]
text = ("void run() {\n"
" CALL_SUBTEST_1(f<int>());\n"
" CALL_SUBTEST_2(g<int>());\n"
"}\n")
assert parts_kept(matches, text, [2, 3]) == ["1", "2"]
def test_multi_line_call_subtest_covers_its_continuation():
matches = [entry("/repo/test/t.cpp", part=str(n), output="t_%d.o" % n) for n in (1, 2)]
text = ("void run() {\n"
" CALL_SUBTEST_2((f<Matrix<float, 1, 1>,\n"
" int>()));\n"
"}\n")
# Line 3 carries no part of its own; it is inside part 2's invocation.
assert parts_kept(matches, text, [3]) == ["2"]
def test_elif_chain_and_else_branch():
text = ("#if defined EIGEN_TEST_PART_1\n"
"int first;\n"
"#elif defined EIGEN_TEST_PART_2\n"
"int second;\n"
"#else\n"
"int rest;\n"
"#endif\n")
universe = frozenset(["1", "2", "3"])
assert required_parts(text, [2], universe) == [frozenset(["1"])]
assert required_parts(text, [4], universe) == [frozenset(["2"])]
# The `#else` is compiled by every part the branches above did not claim.
assert required_parts(text, [6], universe) == [frozenset(["3"])]
def test_guard_condition_spanning_continuation_lines():
text = ("#if defined(EIGEN_TEST_PART_2) || \\\n"
" defined(EIGEN_TEST_PART_3)\n"
"int guarded;\n"
"#endif\n")
universe = frozenset(["1", "2", "3"])
assert required_parts(text, [3], universe) == [frozenset(["2", "3"])]
def test_condition_that_only_restricts_other_macros():
"""A guard naming no part constrains no part, inside or after it."""
text = ("#ifdef __clang__\n"
"int a;\n"
"#else\n"
"int b;\n"
"#endif\n"
"int c;\n")
universe = frozenset(["1", "2"])
assert required_parts(text, [2, 4, 6], universe) == [universe, universe, universe]
def test_nested_guards_intersect():
text = ("#if defined(EIGEN_TEST_PART_1) || defined(EIGEN_TEST_PART_2)\n"
"#ifndef EIGEN_TEST_PART_2\n"
"int only_one;\n"
"#endif\n"
"#endif\n")
assert required_parts(text, [3], frozenset(["1", "2", "3"])) == [frozenset(["1"])]
def test_cap_reports_the_parts_it_leaves_out():
count = MAX_PARTS + 2
matches = [entry("/repo/test/t.cpp", part=str(n), output="t_%d.o" % n)
for n in range(1, count + 1)]
text = "void run() {\n" + "".join(" CALL_SUBTEST_%d(f());\n" % n
for n in range(1, count + 1)) + "}\n"
lines = list(range(2, count + 2))
keep, skipped, unreachable = reduce_entries(matches, text, lines)
assert len(keep) == MAX_PARTS, len(keep)
assert len(skipped) == count - MAX_PARTS, skipped
assert not unreachable
# A capped run must say so rather than report the file as clean.
report = summarize(matches, keep, skipped, unreachable)
assert "NOT CHECKED" in report and skipped[0] in report, report
def test_lines_no_configured_part_compiles_are_reported():
matches = [entry("/repo/test/t.cpp", part=str(n), output="t_%d.o" % n) for n in (1, 2)]
text = ("#if defined(EIGEN_TEST_PART_ALL)\n"
"int unbuilt;\n"
"#endif\n")
keep, skipped, unreachable = reduce_entries(matches, text, [2])
assert unreachable == 1 and not skipped
assert "no configured part" in summarize(matches, keep, skipped, unreachable)
def test_distinct_configurations_are_all_kept():
"""The static and shared builds of a BLAS source parse different code."""
static = entry("/repo/blas/b.cpp", output="static.o")
shared = entry("/repo/blas/b.cpp", extra=["-DEIGEN_BLAS_BUILD_DLL", "-fPIC"], output="shared.o")
matches = [static, shared]
text = ("#if defined(EIGEN_BLAS_BUILD_DLL)\n"
"typedef int OnlyInTheSharedBuild;\n"
"#endif\n")
keep, skipped, unreachable = reduce_entries(matches, text, [2])
assert keep == matches, keep
assert not skipped and not unreachable
assert summarize(matches, keep, skipped, unreachable) == ""
def test_configurations_and_parts_combine():
"""Each configuration keeps its own part selection."""
matches = ([entry("/repo/test/t.cpp", part=str(n), output="plain_%d.o" % n) for n in (1, 2)]
+ [entry("/repo/test/t.cpp", part=str(n), extra=["-DEIGEN_DONT_VECTORIZE"],
output="novec_%d.o" % n) for n in (1, 2)])
text = ("void run() {\n"
" CALL_SUBTEST_2(f());\n"
"}\n")
keep, _, _ = reduce_entries(matches, text, [2])
assert [(part_of(kept), kept["output"]) for kept in keep] == [("2", "plain_2.o"),
("2", "novec_2.o")]
def test_bdcsvd_guard_in_the_real_tree():
"""The tree's own `EIGEN_TEST_PART_53` block, as the review reported it."""
path = os.path.join(REPO_ROOT, "test", "bdcsvd.cpp")
with open(path, encoding="utf-8") as handle:
text = handle.read()
lines = text.splitlines()
guarded = next(number for number, line in enumerate(lines, 1)
if line.startswith("void bdcsvd_extreme_scale_regressions"))
universe = frozenset(str(n) for n in range(1, 60))
assert required_parts(text, [guarded], universe) == [frozenset(["53"])], guarded
def test_added_lines_expands_the_line_filter():
assert added_lines('[{"name": "test/t.cpp", "lines": [[3, 5], [9, 9]]}]') == [3, 4, 5, 9]
assert added_lines("[]") == []
def test_command_line_writes_the_reduced_database():
with tempfile.TemporaryDirectory(prefix="tidy_compile_db_test_") as tmp:
source = os.path.join(tmp, "t.cpp")
with open(source, "w") as handle:
handle.write("#if defined(EIGEN_TEST_PART_3)\nint guarded;\n#endif\n")
database = os.path.join(tmp, "compile_commands.json")
with open(database, "w") as handle:
json.dump([entry(source, part=str(n), output="t_%d.o" % n) for n in (1, 2, 3)], handle)
outdir = os.path.join(tmp, "reduced")
line_filter = json.dumps([{"name": "t.cpp", "lines": [[2, 2]]}])
done = subprocess.run([sys.executable, SCRIPT, database, source, outdir, line_filter],
capture_output=True, text=True)
assert done.returncode == 0, done
with open(os.path.join(outdir, "compile_commands.json")) as handle:
reduced = json.load(handle)
assert [part_of(kept) for kept in reduced] == ["3"], reduced
assert "checking part 3" in done.stdout, done.stdout
# A source outside the database is reported by exit status, not output.
done = subprocess.run([sys.executable, SCRIPT, database, os.path.join(tmp, "other.cpp"),
outdir, line_filter], capture_output=True, text=True)
assert done.returncode == 1, done
# An unreadable database is fatal, so the caller can stop rather than
# silently check nothing.
done = subprocess.run([sys.executable, SCRIPT, source, source, outdir, line_filter],
capture_output=True, text=True)
assert done.returncode == 2, done
def main():
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
for test in tests:
test()
print("PASS %s" % test.__name__)
print("%d tests passed" % len(tests))
return 0
if __name__ == "__main__":
sys.exit(main())