Files
rippled/bin/pre-commit/test_check_doxygen_style.py
2026-07-13 14:38:33 +00:00

407 lines
8.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Tests for check_doxygen_style.py.
Run directly (no test framework needed):
./bin/pre-commit/test_check_doxygen_style.py
or under pytest:
pytest bin/pre-commit/test_check_doxygen_style.py
"""
import sys
import textwrap
from check_doxygen_style import Finding, check_source
def findings_for(text: str) -> list[Finding]:
"""Return the style violations for the given source text.
The text is dedented and its leading newline stripped, so fixtures can be
written as indented triple-quoted here-docs while keeping honest 1-based
line numbers.
"""
text = textwrap.dedent(text).lstrip("\n")
return check_source(text)
def labels_for(text: str) -> list[str]:
return [f.category.label for f in findings_for(text)]
def messages_for(text: str) -> list[str]:
return [f.message for f in findings_for(text)]
# --- well-formed input produces nothing -------------------------------------
def test_clean_block_ok() -> None:
code = """
/**
* Brief.
*
* @tparam T a type
* @param x the x
* @return the result
*/
"""
assert findings_for(code) == []
def test_blank_lines_inside_block_ok() -> None:
code = """
/**
* a
*
* b
*/
"""
assert findings_for(code) == []
def test_member_and_divider_allowed() -> None:
assert findings_for("int x; ///< ok member\n") == []
assert findings_for("//////////\n") == []
assert findings_for("//// text\n") == []
# --- line-comment forms ------------------------------------------------------
def test_triple_slash() -> None:
code = "/// doc\n"
assert labels_for(code) == ["triple-slash"]
def test_qt_line() -> None:
code = "//! doc\n"
assert labels_for(code) == ["qt-line"]
def test_qt_member() -> None:
code = "int x; //!< doc\n"
assert labels_for(code) == ["qt-member"]
def test_block_member() -> None:
code = "int x; /**< doc */\n"
assert labels_for(code) == ["block-member"]
def test_qt_block_member() -> None:
code = "int x; /*!< doc */\n"
assert labels_for(code) == ["qt-block-member"]
def test_doc_in_line_comment() -> None:
code = "// @param x\n"
assert labels_for(code) == ["doc-in-line-comment"]
# --- block forms -------------------------------------------------------------
def test_qt_comment() -> None:
code = """
/*!
* brief
*/
"""
assert labels_for(code) == ["qt-comment"]
def test_qt_comment_single_line() -> None:
# /*! ... */ on one line -> qt-comment (plus single-line-block)
code = "/*! brief */\n"
assert labels_for(code) == ["qt-comment", "single-line-block"]
def test_single_line_block() -> None:
code = "/** brief */\n"
assert labels_for(code) == ["single-line-block"]
def test_single_line_markers_allowed() -> None:
for marker in ("@{", "@}", "@cond LABEL", "@endcond", "@file foo.h"):
code = f"/** {marker} */\n"
assert findings_for(code) == [], marker
def test_text_on_opener() -> None:
code = """
/** text here
* more
*/
"""
assert labels_for(code) == ["text-on-opener"]
def test_bare_continuation() -> None:
code = """
/**
* a
bare line
*/
"""
assert labels_for(code) == ["bare-continuation"]
def test_over_indented_first_line() -> None:
code = """
/**
* over
*/
"""
assert labels_for(code) == ["over-indented"]
def test_over_indented_tag() -> None:
# a flush first line consumes "first content", isolating the tag check
code = """
/**
* brief
* @param x
*/
"""
assert labels_for(code) == ["over-indented-tag"]
def test_combined_marker() -> None:
code = """
/**
* @{
*/
"""
assert labels_for(code) == ["combined-marker"]
def test_prose_label() -> None:
for word in ("Returns", "Throws", "Exceptions"):
code = f"""
/**
* {word}:
*/
"""
assert labels_for(code) == ["prose-label"], word
def test_content_on_closer() -> None:
code = """
/**
* a
* b */
"""
assert labels_for(code) == ["content-on-closer"]
def test_plain_block_doc() -> None:
assert labels_for("/* @param x */\n") == ["plain-block-doc"]
assert findings_for("/* just an ordinary note */\n") == []
def test_tag_order() -> None:
out_of_order = """
/**
* @param x
* @tparam T
*/
"""
assert labels_for(out_of_order) == ["tag-order"]
correct = """
/**
* @tparam T
* @param x
* @return r
*/
"""
assert findings_for(correct) == []
single = """
/**
* @param x
*/
"""
assert findings_for(single) == [] # single tag: never out of order
# --- command spelling (must work on body/closer lines, not just the opener) --
def test_backslash_command_on_body_line() -> None:
code = r"""
/**
* \brief x
*/
"""
assert labels_for(code) == ["backslash-command"]
def test_backslash_command_suggests_canonical_spelling() -> None:
# a backslash + non-canonical spelling is fixed in one pass, not two:
# \sa -> @see (not @sa), \returns -> @return (not @returns)
sa = r"""
/**
* \sa other
*/
"""
assert messages_for(sa) == [r"use @see instead of \sa"]
returns = r"""
/**
* \returns x
*/
"""
assert messages_for(returns) == [r"use @return instead of \returns"]
def test_wrong_command_on_body_line() -> None:
code = """
/**
* @returns x
*/
"""
assert labels_for(code) == ["wrong-command"]
def test_body_line_commands_regression() -> None:
# regression: these live on body lines of a multi-line block
code = r"""
/**
* @returns bad
* @throw ex
* @sa other
* \param y
*/
"""
assert labels_for(code) == [
"wrong-command",
"wrong-command",
"wrong-command",
"backslash-command",
]
def test_command_on_closer_line() -> None:
code = """
/**
* a
* @sa b */
"""
assert labels_for(code) == ["wrong-command", "content-on-closer"]
def test_no_double_count_across_opener_body_closer() -> None:
code = """
/** @returns opener
* @throw body
* @sa closer */
"""
assert labels_for(code).count("wrong-command") == 3
def test_code_with_word_allowed() -> None:
# @code{.cpp} is valid Doxygen and must not be flagged
code = """
/**
* @code{.cpp}
* int x;
* @endcode
*/
"""
assert findings_for(code) == []
# --- rendered message text ---------------------------------------------------
def test_message_uses_category_description() -> None:
# a static category renders its default description
code = "/// doc\n"
assert messages_for(code) == ["use a /** ... */ block instead of ///"]
def test_message_detail_overrides() -> None:
# dynamic categories render the offending text via Finding.detail
backslash = r"""
/**
* \param y
*/
"""
assert messages_for(backslash) == [r"use @param instead of \param"]
wrong = """
/**
* @returns x
*/
"""
assert messages_for(wrong) == ["use @return instead of @returns"]
prose = """
/**
* Throws:
*/
"""
assert messages_for(prose) == ['use @throws instead of prose "Throws:"']
# --- robustness --------------------------------------------------------------
def test_empty_file_no_crash() -> None:
assert findings_for("") == []
def test_mid_line_plain_block_skipped() -> None:
# a /* opened mid-line (after code) and spanning lines is skipped, so its
# comment-like contents are not analyzed
code = """
int x = 0; /* note: @returns is not a real tag here
* @param also not real
*/
int y = 0;
"""
assert findings_for(code) == []
def test_unclosed_block_scanned_to_eof() -> None:
# an unterminated /** block is still scanned to EOF (no crash, body checked)
code = """
/**
* @returns x
"""
assert labels_for(code) == ["wrong-command"]
def test_banner_and_empty_comment_not_flagged() -> None:
code = """
/***
* banner
***/
"""
assert findings_for(code) == []
assert findings_for("/**/\n") == []
def main() -> int:
tests = sorted(
(name, fn)
for name, fn in globals().items()
if name.startswith("test_") and callable(fn)
)
failed = 0
for name, fn in tests:
try:
fn()
print(f"PASS {name}")
except AssertionError as exc:
failed += 1
print(f"FAIL {name}: {exc!r}")
print(f"\n{len(tests) - failed}/{len(tests)} passed")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())