Files
eigen/scripts/test_check_style.py

324 lines
17 KiB
Python

#!/usr/bin/env python3
# SPDX-FileCopyrightText: The Eigen Authors
# SPDX-License-Identifier: MPL-2.0
"""Unit tests for scripts/check_style.py.
Runs the checks on synthetic post-images, the diff parser on a crafted diff,
and the diff mode against a temporary git repository, so the expectations do
not depend on the checked-out tree.
Usage: python3 scripts/test_check_style.py
"""
import os
import subprocess
import sys
import tempfile
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from check_style import (
added_from_structured_patch,
added_lines_from_diff,
find_findings,
hook_added_line_numbers,
run_diff_mode,
)
def messages(rel_path, text, added=None):
lines = text.splitlines()
if added is None:
added = set(range(1, len(lines) + 1))
return [m for _, m in find_findings(rel_path, lines, added)]
def assert_flags(rel_path, text, *fragments, **kwargs):
got = messages(rel_path, text, kwargs.get("added"))
for fragment in fragments:
assert any(fragment in m for m in got), "expected %r in findings for %s, got %r" % (fragment, rel_path, got)
def assert_clean(rel_path, text, added=None):
got = messages(rel_path, text, added)
assert not got, "expected no findings for %s, got %r" % (rel_path, got)
def test_conventions_flagged():
assert_flags("Eigen/src/Core/Foo.h", "std::integral_constant<bool, true> b;\n", "bool_constant")
assert_flags("Eigen/src/Core/Foo.h", "enum { Flags = 0 };\n", "static constexpr")
assert_flags("Eigen/src/Core/Foo.h", "enum : unsigned int { Flags = 0 };\n", "static constexpr")
assert_flags("Eigen/src/Core/Foo.h", "enum : std::uint32_t { Flags = 0 };\n", "static constexpr")
assert_flags("Eigen/src/Core/Foo.h", "S s = {.size = 3};\n", "designated initializer")
assert_flags("Eigen/src/Core/Foo.h", "if constexpr (kSize > 4) {}\n", "EIGEN_IF_CONSTEXPR")
assert_flags("test/foo.cpp", "std::optional<int> x;\n", "C++17/20 library facility")
assert_flags("test/foo.cpp", "std::any x;\n", "C++17/20 library facility")
assert_flags("test/foo.cpp", "std::filesystem::path p;\n", "C++17/20 library facility")
assert_flags("Eigen/src/Core/Foo.h", "double x = std::sqrt(2.0);\n", "numext::")
assert_flags("Eigen/src/Core/Foo.h", "double x = (std::sqrt)(2.0);\n", "numext::")
assert_flags("Eigen/src/Core/Foo.h", "double x = std::exp2(2.0);\n", "numext::")
assert_flags("Eigen/src/Core/Foo.h", "double x = std::cbrt(2.0);\n", "numext::")
assert_flags("Eigen/src/Core/Foo.h", "double x = std::acosh(2.0);\n", "numext::")
assert_flags("Eigen/src/Core/Foo.h", "auto x = std::conj(value);\n", "numext::")
assert_flags("Eigen/src/Core/Foo.h", "double x = std::frexp(value, &exponent);\n", "EIGEN_USING_STD")
def test_scoping():
# Eigen's public module headers are C++ despite having no extension.
assert_flags("Eigen/Core", "enum { Flags = 0 };\n", "static constexpr")
assert_flags("unsupported/Eigen/FFT", "if constexpr (kSize > 4) {}\n", "EIGEN_IF_CONSTEXPR")
# std:: math is only flagged in library implementation headers.
assert_clean("test/foo.cpp", "double x = std::sqrt(2.0);\n")
# C++14 checks do not apply outside the C++14 trees.
assert_clean("benchmarks/Core/foo.cpp", "if constexpr (kSize > 4) {}\n")
# Non-C++ files are ignored entirely.
assert_clean("AGENTS.md", "enum { Flags = 0 };\nif constexpr (x) {}\n")
# Only ADDED lines are reported: the same violations on unchanged lines stay silent.
assert_clean("Eigen/src/Core/Foo.h", "enum { Flags = 0 };\nint x = 1;\n", added={2})
# Sources with a documented C++17 requirement are exempt from the C++14 checks...
assert_clean("test/sycl_basic.cpp", "if constexpr (kSize > 4) {}\n")
assert_clean("Eigen/src/Core/arch/SYCL/PacketMath.h", "if constexpr (kSize > 4) {}\n")
assert_clean("unsupported/Eigen/src/Tensor/TensorDeviceSycl.h", "if constexpr (kSize > 4) {}\n")
assert_clean("unsupported/test/tensor_sycl.cpp", "if constexpr (kSize > 4) {}\n")
assert_clean("unsupported/Eigen/src/FFT/duccfft_impl.h", "if constexpr (kSize > 4) {}\n")
assert_clean("unsupported/test/duccfft.cpp", "if constexpr (kSize > 4) {}\n")
assert_clean("failtest/structured_bindings_dynamic_matrix.cpp", "if constexpr (kSize > 4) {}\n")
assert_clean("failtest/structured_bindings_dynamic_array.cpp", "if constexpr (kSize > 4) {}\n")
assert_clean("failtest/structured_bindings_rowmajor.cpp", "if constexpr (kSize > 4) {}\n")
# A coincidental substring is not a documented C++17 requirement.
assert_flags("test/not_sycl_related.cpp", "if constexpr (kSize > 4) {}\n", "EIGEN_IF_CONSTEXPR")
assert_flags("unsupported/Eigen/src/FFT/kissfft_impl.h", "if constexpr (kSize > 4) {}\n", "EIGEN_IF_CONSTEXPR")
# ...but not from the other conventions.
assert_flags("test/sycl_basic.cpp", "std::integral_constant<bool, true> b;\n", "bool_constant")
def test_false_positive_probes():
assert_clean("Eigen/src/Core/Foo.h", "opts.size = 4;\nfoo(a.b, c.d);\n") # member access, not init
assert_clean("Eigen/src/Core/Foo.h", "double v[] = {.5, 1.5};\n") # float literal, not init
assert_clean("Eigen/src/Core/Foo.h", "enum class Kind { A, B };\n") # scoped enums are fine
assert_clean("Eigen/src/Core/Foo.h", "enum Kind : unsigned int { A, B };\n") # named enums are fine
assert_clean("Eigen/src/Core/Foo.h", 'auto s = "if constexpr integral_constant<bool,";\n') # in a string
assert_clean("Eigen/src/Core/Foo.h", "const char c = 'N';\n") # character literal
assert_clean("Eigen/src/Core/Foo.h", "const wchar_t c = L'N';\n") # prefixed character literal
assert_clean("Eigen/src/Core/Foo.h", "// if constexpr discussed in a comment\nint x = 1;\n")
# A C++14 digit separator is not the start of a character literal; code
# later on the same line must remain visible to convention checks.
assert_flags("Eigen/src/Core/Foo.h", "auto n = 1'000; std::integral_constant<bool, true> b;\n",
"bool_constant")
assert_flags("Eigen/src/Core/Foo.h", "auto n = 0xFF'00; std::integral_constant<bool, true> b;\n",
"bool_constant")
assert_clean("Eigen/src/Core/Foo.h", "using MyInt = int;\nstatic constexpr unsigned int Flags = 0;\n"
"const char* p = nullptr;\nEIGEN_IF_CONSTEXPR (kSize > 4) {}\n"
"double x = numext::sqrt(2.0);\n")
def test_multiline_designated_initializer():
# The designator on its own line after `{` or `,` is still a C++20 designated initializer.
assert_flags("Eigen/src/Core/Foo.h", "S s = {\n .size = 3,\n};\n", "designated initializer")
assert_flags("Eigen/src/Core/Foo.h", "S s = {\n .a = 1,\n .b = 2,\n};\n", "designated initializer")
# A wrapped member assignment is not: the previous code line does not end with `{` or `,`.
assert_clean("Eigen/src/Core/Foo.h", "obj\n .member = value;\n")
assert_clean("Eigen/src/Core/Foo.h", "foo(bar)\n .field = 1;\n")
def test_context_across_diff_gaps():
# An added continuation line inside an existing block comment is a comment,
# not code, even though the surrounding lines were not added.
text = "/* existing block\n * Do not use if constexpr here\n */\nint x = 1;\n"
assert_clean("Eigen/src/Core/Foo.h", text, added={2})
# Same for an existing Doxygen block: additions inherit its exemption.
doxy = "/** \\brief Existing docs.\n" + "\n".join(" * added line %d" % i for i in range(8)) + "\n */\nint x;\n"
assert_clean("Eigen/src/Core/Foo.h", doxy, added=set(range(2, 10)))
# A string on an unchanged line does not leak its content into added lines.
text = 'const char* s = "no /* here";\nstd::integral_constant<bool, true> b;\n'
assert_flags("Eigen/src/Core/Foo.h", text, "bool_constant", added={2})
def test_comment_verbosity():
narration = "\n".join("// narration line %d" % i for i in range(6)) + "\nint x = 1;\n"
assert_flags("Eigen/src/Core/Foo.h", narration, "non-Doxygen comment")
# License headers and Doxygen blocks are exempt however long they are.
license_header = "\n".join("// SPDX-License-Identifier: MPL-2.0" if i == 0 else "// Copyright notice %d" % i
for i in range(8)) + "\nint x = 1;\n"
assert_clean("Eigen/src/Core/Foo.h", license_header)
# Five added lines stay under the threshold.
assert_clean("Eigen/src/Core/Foo.h", "\n".join("// l%d" % i for i in range(5)) + "\nint x = 1;\n")
# Extending an existing narration block by two lines is not reported: only
# ADDED lines count toward the threshold.
block = "\n".join("// old line %d" % i for i in range(10)) + "\n// new a\n// new b\nint x = 1;\n"
assert_clean("Eigen/src/Core/Foo.h", block, added={11, 12})
# Six added lines inside an existing non-Doxygen block are reported.
block = "// old line\n" + "\n".join("// new %d" % i for i in range(6)) + "\nint x = 1;\n"
assert_flags("Eigen/src/Core/Foo.h", block, "non-Doxygen comment", added=set(range(2, 8)))
# Blank physical lines and a closing line without a leading `*` remain
# inside their lexical block, but the blank line does not count as prose.
block = "/* first\nsecond\nthird\n\nfourth\nfifth\nsixth */\nint x = 1;\n"
assert_flags("Eigen/src/Core/Foo.h", block, "non-Doxygen comment")
# A blank line inside Doxygen must not split off the remaining lines and
# lose the exemption inherited from the opening delimiter.
doxy = "/** docs\n\nline one\nline two\nline three\nline four\nline five\nline six */\nint x = 1;\n"
assert_clean("Eigen/src/Core/Foo.h", doxy)
# An adjacent ordinary heading is a separate comment run and must not
# strip the exemption from the Doxygen block that follows it.
doxy = "// API documentation follows\n/** docs\nline one\nline two\nline three\nline four\nline five\nline six */\n"
assert_clean("Eigen/src/Core/Foo.h", doxy)
# The Doxygen kind is carried even when the opener follows code and is not
# itself a comment-only line.
trailing_doxy = ("int value; /**< docs\n * line one\n * line two\n * line three\n"
" * line four\n * line five\n * line six */\n")
assert_clean("Eigen/src/Core/Foo.h", trailing_doxy)
# Conversely, ordinary narration immediately after Doxygen does not
# inherit its exemption.
narration = "/** short docs */\n" + "\n".join("// narration %d" % i for i in range(6)) + "\n"
assert_flags("Eigen/src/Core/Foo.h", narration, "non-Doxygen comment")
def test_multiline_literals():
# Contents of a raw string spanning lines are literal, not code.
raw = 'const char* prog = R"cl(\nif constexpr (true) { std::optional<int> x; }\n)cl";\nint y = 1;\n'
assert_clean("Eigen/src/Core/Foo.h", raw)
# Code after the raw string closes is lexed again.
raw_then_code = 'auto s = R"(\ntext\n)"; std::integral_constant<bool, true> b;\n'
assert_flags("Eigen/src/Core/Foo.h", raw_then_code, "bool_constant")
# A backslash-spliced ordinary string stays a literal on its continuation lines.
spliced = 'const char* s = "first \\\nif constexpr (x) \\\nlast";\nint z = 1;\n'
assert_clean("Eigen/src/Core/Foo.h", spliced)
# A raw-string-looking suffix of an identifier is an ordinary string.
assert_clean("Eigen/src/Core/Foo.h", 'auto v = myR"(not raw)";\n')
# Single-line raw strings close on the same line.
assert_clean("Eigen/src/Core/Foo.h", 'auto s = R"(if constexpr NULL typedef)";\n')
def test_hook_line_mapping():
content = "int x;\nint changed;\nconst char* p = NULL;\n"
# A snippet with a trailing newline covers only its own line, not the next.
assert hook_added_line_numbers(content, ["int changed;\n"]) == {2}
assert hook_added_line_numbers(content, ["int changed;"]) == {2}
# Multi-line snippets cover their span.
assert hook_added_line_numbers(content, ["int x;\nint changed;\n"]) == {1, 2}
# An ambiguous snippet cannot be located; the caller must fall back.
assert hook_added_line_numbers("true\nif constexpr (true)\n", ["true"]) is None
# An absent snippet likewise.
assert hook_added_line_numbers(content, ["not present"]) is None
def test_structured_patch():
response = {"structuredPatch": [
{"oldStart": 4, "oldLines": 2, "newStart": 5, "newLines": 3,
"lines": [" context", "-old line", "+new one", "+new two", " context"]},
]}
assert added_from_structured_patch(response) == {6, 7}
deletion_only = {"structuredPatch": [
{"oldStart": 1, "oldLines": 3, "newStart": 1, "newLines": 2,
"lines": [" context", "-removed", " context"]},
]}
# A valid empty result must not fall back and mark the retained context.
deletion_added = added_from_structured_patch(deletion_only)
assert deletion_added == set()
assert_clean("Eigen/src/Core/Foo.h", "int keep;\nstd::integral_constant<bool, true> b;\n",
added=deletion_added)
assert added_from_structured_patch({}) is None
assert added_from_structured_patch({"structuredPatch": "bogus"}) is None
def test_diff_parser():
diff = (
"diff --git a/Eigen/src/Core/Foo.h b/Eigen/src/Core/Foo.h\n"
"--- a/Eigen/src/Core/Foo.h\n"
"+++ b/Eigen/src/Core/Foo.h\n"
"@@ -10,0 +11,2 @@ context\n"
"+const char* p = NULL;\n"
"+int y = 2;\n"
"@@ -20,1 +23,1 @@ context\n"
"-old line\n"
"+typedef int T;\n"
"diff --git a/gone.cpp b/gone.cpp\n"
"--- a/gone.cpp\n"
"+++ /dev/null\n"
)
files = added_lines_from_diff(diff)
assert set(files) == {"Eigen/src/Core/Foo.h"}, files
assert files["Eigen/src/Core/Foo.h"] == {11, 12, 23}, files
def test_no_newline_marker():
# Replacing an unterminated last line: the "\ No newline" markers must not
# advance the line counter, or the addition maps past the post-image.
diff = (
"--- a/Eigen/src/Core/Foo.h\n"
"+++ b/Eigen/src/Core/Foo.h\n"
"@@ -1 +1 @@\n"
"-int old;\n"
"\\ No newline at end of file\n"
"+const char* p = NULL;\n"
"\\ No newline at end of file\n"
)
files = added_lines_from_diff(diff)
assert files["Eigen/src/Core/Foo.h"] == {1}, files
assert messages("Eigen/src/Core/Foo.h", "std::integral_constant<bool, true> b;", added={1})
response = {"structuredPatch": [
{"oldStart": 1, "oldLines": 1, "newStart": 1, "newLines": 1,
"lines": ["-int old;", "\\ No newline at end of file",
"+const char* p = NULL;", "\\ No newline at end of file"]},
]}
assert added_from_structured_patch(response) == {1}
def test_diff_mode_merge_base_and_untracked():
with tempfile.TemporaryDirectory(prefix="check_style_test_") as tmp:
def sh(*args):
subprocess.run(args, cwd=tmp, check=True, capture_output=True)
def write(rel, text):
path = os.path.join(tmp, rel)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as handle:
handle.write(text)
sh("git", "init", "-q", "-b", "main")
# The base carries a one-line file without a trailing newline, so the
# feature diff emits "\ No newline at end of file" markers.
write("Eigen/src/Core/NoEol.h", "int old;")
sh("git", "add", "Eigen/src/Core/NoEol.h")
sh("git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-q", "-m", "base")
sh("git", "branch", "target")
# Feature branch adds a violating file and replaces the unterminated line.
write("Eigen/src/Core/Added.h", "std::integral_constant<bool, true> b;\n")
write("Eigen/src/Core/NoEol.h", "enum { Flags = 0 };")
sh("git", "add", "Eigen/src/Core/Added.h", "Eigen/src/Core/NoEol.h")
sh("git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-q", "-m", "feature")
# Target advances independently with its own violating file: a
# two-tree diff against `target` would report it in reverse.
sh("git", "checkout", "-q", "target")
write("Eigen/src/Core/TargetOnly.h", "std::integral_constant<bool, false> legacy;\n")
sh("git", "add", "Eigen/src/Core/TargetOnly.h")
sh("git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-q", "-m", "target-only")
sh("git", "checkout", "-q", "main")
# An untracked new file must be scanned even though git diff omits it.
write("Eigen/src/Core/Untracked.h", "std::integral_constant<bool, true> b;\n")
write("Eigen/NewModule", "enum { Flags = 0 };\n")
results = run_diff_mode("target", root=tmp)
paths = {rel_path for rel_path, _, _ in results}
assert "Eigen/src/Core/Added.h" in paths, results
assert "Eigen/src/Core/Untracked.h" in paths, results
assert "Eigen/NewModule" in paths, results
assert "Eigen/src/Core/TargetOnly.h" not in paths, results
# The unterminated replacement maps to line 1 despite the markers.
noeol = [(l, m) for p, l, m in results if p == "Eigen/src/Core/NoEol.h"]
assert noeol and noeol[0][0] == 1 and "static constexpr" in noeol[0][1], results
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())