"""Testing file 'cli/pre_commit_scripts/check_doxygen.py'."""
import io
import sys
import unittest
from contextlib import redirect_stderr
from pathlib import Path
from unittest.mock import MagicMock, patch
try:
from cli.foxbms_version import get_version
from cli.helpers.misc import PROJECT_ROOT
from cli.pre_commit_scripts import check_doxygen
except ModuleNotFoundError:
sys.path.insert(0, str(Path(__file__).parents[3]))
from cli.foxbms_version import get_version
from cli.helpers.misc import PROJECT_ROOT
from cli.pre_commit_scripts import check_doxygen
def expected_error_msg(file_path: str, label: str):
"""Creates the expected error message"""
return f"{file_path}: Doxygen @{label} field is wrong/missing."
class TestCheckDoxygenComment(unittest.TestCase):
"""Test of the main function"""
@classmethod
def setUpClass(cls):
cls.tests_dir = Path(__file__).parent / Path(__file__).stem
cls.version = get_version()
@patch("cli.pre_commit_scripts.check_doxygen.Popen")
def test_invalid_version(self, mock_popen: MagicMock):
"""A invalid number"""
process = mock_popen.return_value.__enter__.return_value
process.returncode = 0
process.communicate.return_value = (b"foxBMS 2: 0.0.0", b"")
test_file = "invalid-version.c"
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.main([str(self.tests_dir / test_file)])
self.assertEqual(result, 1)
self.assertIn(expected_error_msg(test_file, "version"), err.getvalue())
@patch("cli.pre_commit_scripts.check_doxygen.Popen")
def test_version_not_determinable(self, mock_popen: MagicMock):
"""A invalid number"""
process = mock_popen.return_value.__enter__.return_value
process.returncode = 0
process.communicate.return_value = (b"", b"")
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.main([str(self.tests_dir / "invalid-version.c")])
self.assertIn("Could not determine foxBMS version.", err.getvalue())
self.assertEqual(result, 1)
def test_ignore_version_comment(self):
"""@version shall be ignored"""
test_file = (
(self.tests_dir / "ignore-version-comment.c")
.relative_to(PROJECT_ROOT)
.as_posix()
)
files = [Path(test_file)]
check_doxygen.IGNORE_ERROR["version"].append(test_file)
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.check_doxygen(files, self.version)
self.assertEqual(result, 0)
@patch("cli.pre_commit_scripts.check_doxygen.Popen")
def test_valid_doxygen(self, mock_popen: MagicMock):
"""A valid doxygen comment shall not raise an error"""
files = [str(self.tests_dir / "valid-doxygen.c")]
process = mock_popen.return_value.__enter__.return_value
process.returncode = 0
ver_str = f"foxBMS 2: {self.version}"
process.communicate.return_value = (ver_str.encode("utf-8"), b"")
result = check_doxygen.main(files)
self.assertEqual(result, 0)
def test_invalid_encoding(self):
"""Invalid file encoding"""
test_file = "invalid-encoding_utf-16.c"
files = [self.tests_dir / test_file]
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.check_doxygen(files, self.version)
self.assertEqual(result, 1)
self.assertIn(f"{test_file}: Could not ASCII-decode this file.", err.getvalue())
def test_no_doxygen_comments(self):
"""All doxygen comments are missing"""
test_file = "no-doxygen-comments.c"
files = [self.tests_dir / test_file]
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.check_doxygen(files, self.version)
self.assertEqual(result, 10)
self.assertIn(
f"{test_file}: Doxygen comment start marker is missing.", err.getvalue()
)
self.assertIn(expected_error_msg(test_file, "file"), err.getvalue())
self.assertIn(expected_error_msg(test_file, "author"), err.getvalue())
self.assertIn(expected_error_msg(test_file, "date"), err.getvalue())
self.assertIn(expected_error_msg(test_file, "updated"), err.getvalue())
self.assertIn(expected_error_msg(test_file, "version"), err.getvalue())
self.assertIn(expected_error_msg(test_file, "ingroup"), err.getvalue())
self.assertIn(expected_error_msg(test_file, "prefix"), err.getvalue())
self.assertIn(expected_error_msg(test_file, "brief"), err.getvalue())
self.assertIn(expected_error_msg(test_file, "details"), err.getvalue())
def test_no_start_comment(self):
"""Doxygen block comment start label is missing"""
test_file = "no-start-comment.c"
files = [self.tests_dir / test_file]
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.check_doxygen(files, self.version)
self.assertEqual(result, 1)
self.assertIn(
f"{test_file}: Doxygen comment start marker is missing.", err.getvalue()
)
def test_no_file_comment(self):
"""@file comment is missing"""
test_file = "no-file-comment.c"
files = [self.tests_dir / test_file]
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.check_doxygen(files, self.version)
self.assertEqual(result, 1)
self.assertIn(expected_error_msg(test_file, "file"), err.getvalue())
def test_no_author_comment(self):
"""@author comment is missing"""
test_file = "no-author-comment.c"
files = [self.tests_dir / test_file]
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.check_doxygen(files, self.version)
self.assertEqual(result, 1)
self.assertIn(expected_error_msg(test_file, "author"), err.getvalue())
def test_ignore_author_comment(self):
"""@author shall be ignored"""
test_file = (
(self.tests_dir / "ignore-author-comment.c")
.relative_to(PROJECT_ROOT)
.as_posix()
)
files = [Path(test_file)]
check_doxygen.IGNORE_ERROR["author"].append(test_file)
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.check_doxygen(files, self.version)
self.assertEqual(result, 0)
def test_ignore_author_comment_entirely(self):
"""@author shall be ignored"""
test_file = (
(self.tests_dir / "ignore-author-comment-entirely.c")
.relative_to(PROJECT_ROOT)
.as_posix()
)
files = [Path(test_file)]
check_doxygen.IGNORE_ERROR["author"].append(test_file)
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.check_doxygen(files, self.version)
self.assertEqual(result, 0)
def test_no_date_comment(self):
"""@date comment is missing"""
test_file = "no-date-comment.c"
files = [self.tests_dir / test_file]
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.check_doxygen(files, self.version)
self.assertEqual(result, 1)
self.assertIn(expected_error_msg(test_file, "date"), err.getvalue())
def test_ignore_date_comment(self):
"""@date shall be ignored"""
test_file = (
(self.tests_dir / "ignore-date-comment.c")
.relative_to(PROJECT_ROOT)
.as_posix()
)
files = [Path(test_file)]
check_doxygen.IGNORE_ERROR["date"].append(test_file)
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.check_doxygen(files, self.version)
self.assertEqual(result, 0)
def test_no_updated_comment(self):
"""@updated comment is missing"""
test_file = "no-updated-comment.c"
files = [self.tests_dir / test_file]
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.check_doxygen(files, self.version)
self.assertEqual(result, 1)
self.assertIn(expected_error_msg(test_file, "updated"), err.getvalue())
def test_no_ingroup_comment(self):
"""@ingroup comment is missing"""
test_file = "no-ingroup-comment.c"
files = [self.tests_dir / test_file]
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.check_doxygen(files, self.version)
self.assertEqual(result, 1)
self.assertIn(expected_error_msg(test_file, "ingroup"), err.getvalue())
def test_no_prefix_comment(self):
"""@prefix comment is missing"""
test_file = "no-prefix-comment.c"
files = [self.tests_dir / test_file]
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.check_doxygen(files, self.version)
self.assertEqual(result, 1)
self.assertIn(expected_error_msg(test_file, "prefix"), err.getvalue())
def test_no_brief_comment(self):
"""@brief comment is missing"""
test_file = "no-brief-comment.c"
files = [self.tests_dir / test_file]
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.check_doxygen(files, self.version)
self.assertEqual(result, 1)
self.assertIn(expected_error_msg(test_file, "brief"), err.getvalue())
def test_multiline_brief_comment(self):
"""@brief shall be supported"""
test_file = "multiline-brief.c"
files = [self.tests_dir / test_file]
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.check_doxygen(files, self.version)
self.assertEqual(result, 0)
def test_no_details_comment(self):
"""@details comment is missing"""
test_file = "no-details-comment.c"
files = [self.tests_dir / test_file]
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.check_doxygen(files, self.version)
self.assertEqual(result, 1)
self.assertIn(expected_error_msg(test_file, "details"), err.getvalue())
def test_empty_line_between_multiline_brief_and_details(self):
"""@details comment is missing"""
test_file = "empty-line-between-brief-and-details.c"
files = [self.tests_dir / test_file]
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.check_doxygen(files, self.version)
self.assertEqual(result, 1)
self.assertIn(expected_error_msg(test_file, "details"), err.getvalue())
def test_files_with_special_start_lines(self):
"""Special file is tested"""
test_file = Path(
"tests/cli/pre_commit_scripts/test_check_license_info/no-license.c"
)
files = [test_file]
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.check_doxygen(files, self.version)
self.assertEqual(result, 0)
def test_default_confidential_offset_is_used_without_bsd_header(self):
"""Use confidential default offset when file has no SPDX marker and no index map entry."""
content = "\n".join(
[
"header 0",
"header 1",
"header 2",
"header 3",
"header 4",
"header 5",
"header 6",
"header 7",
"header 8",
"/**",
" * @file default-offset.c",
" * @author foxBMS Team",
" * @date 2026-03-27 (date of creation)",
" * @updated 2026-03-27 (date of last update)",
" * @version v1.2.3",
" * @ingroup APP",
" * @prefix BMS",
" *",
" * @brief BRIEF",
" * More brief text.",
" * @details DETAILS",
]
)
file_mock = MagicMock(spec=Path)
file_mock.as_posix.return_value = "mock/default-offset.c"
file_mock.name = "default-offset.c"
file_mock.read_text.return_value = content
err = io.StringIO()
with redirect_stderr(err):
result = check_doxygen.run_check(file_mock, "1.2.3")
self.assertEqual(result, 0)
self.assertEqual(err.getvalue(), "")
if __name__ == "__main__":
unittest.main()