| #!/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()) |