libeigen/eigen!2870 Co-authored-by: Rasmus Munk Larsen <rlarsen@nvidia.com> Co-authored-by: Rasmus Munk Larsen <rmlarsen@gmail.com>
253 lines
11 KiB
Python
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())
|