mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-23 23:20:33 +00:00
407 lines
8.7 KiB
Python
Executable File
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())
|