diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..7f10b63 --- /dev/null +++ b/.clang-format @@ -0,0 +1,3 @@ +BasedOnStyle: Google +IndentWidth: 2 +ColumnLimit: 80 diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..5c20edd --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,25 @@ +Checks: '-*,bugprone-*,clang-analyzer-*,performance-*,readability-identifier-naming' +HeaderFilterRegex: '^(include|src)/fesa/.*' +FormatStyle: file +ExtraArgs: ['-std=c++17'] +CheckOptions: + readability-identifier-naming.ClassCase: CamelCase + readability-identifier-naming.StructCase: CamelCase + readability-identifier-naming.EnumCase: CamelCase + readability-identifier-naming.TypeAliasCase: CamelCase + readability-identifier-naming.FunctionCase: CamelCase + readability-identifier-naming.MethodCase: CamelCase + readability-identifier-naming.VariableCase: lower_case + readability-identifier-naming.ParameterCase: lower_case + readability-identifier-naming.LocalVariableCase: lower_case + readability-identifier-naming.PrivateMemberCase: lower_case + readability-identifier-naming.PrivateMemberSuffix: _ + readability-identifier-naming.ProtectedMemberCase: lower_case + readability-identifier-naming.ProtectedMemberSuffix: _ + readability-identifier-naming.PublicMemberCase: lower_case + readability-identifier-naming.ConstantCase: CamelCase + readability-identifier-naming.ConstantPrefix: k + readability-identifier-naming.EnumConstantCase: CamelCase + readability-identifier-naming.EnumConstantPrefix: k + readability-identifier-naming.NamespaceCase: lower_case + readability-identifier-naming.MacroDefinitionCase: UPPER_CASE diff --git a/.gitignore b/.gitignore index bc05d64..03c4805 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ __pycache__/ # local Harness configuration and build outputs .harness/config.json .harness/build/ +.harness/doxygen/ # phase execution outputs phases/**/phase*-output.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 67cae40..3f23b0c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,16 @@ set(CMAKE_CXX_EXTENSIONS OFF) include(cmake/FesaDependencies.cmake) +find_package(Doxygen QUIET) +if(Doxygen_FOUND) + add_custom_target(fesa_docs + COMMAND "${DOXYGEN_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + COMMENT "Generating FESA API documentation" + VERBATIM + ) +endif() + enable_testing() add_subdirectory(src/fesa) diff --git a/Doxyfile b/Doxyfile new file mode 100644 index 0000000..b24754a --- /dev/null +++ b/Doxyfile @@ -0,0 +1,16 @@ +PROJECT_NAME = FESA +PROJECT_NUMBER = 0.1.0 +OUTPUT_DIRECTORY = .harness/doxygen +INPUT = include src +EXCLUDE = tests +RECURSIVE = YES +FILE_PATTERNS = *.h *.cpp +EXTRACT_ALL = NO +EXTRACT_PRIVATE = YES +EXTRACT_STATIC = YES +WARN_IF_UNDOCUMENTED = YES +WARN_IF_DOC_ERROR = YES +WARN_AS_ERROR = YES +GENERATE_HTML = YES +HTML_OUTPUT = html +GENERATE_LATEX = NO diff --git a/tests/test_cpp_policy_contract.py b/tests/test_cpp_policy_contract.py new file mode 100644 index 0000000..5240dbd --- /dev/null +++ b/tests/test_cpp_policy_contract.py @@ -0,0 +1,83 @@ +from pathlib import Path +import re + + +ROOT = Path(__file__).resolve().parents[1] + + +def read(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def doxygen_assignments(text: str) -> dict[str, str]: + assignments = {} + for line in text.splitlines(): + match = re.fullmatch(r"\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*?)\s*", line) + if match is not None: + assignments[match.group(1)] = match.group(2) + return assignments + + +def test_p_style_001_cpp_style_and_doxygen_tooling_contract(): + required_paths = ( + ROOT / ".clang-format", + ROOT / ".clang-tidy", + ROOT / "Doxyfile", + ) + missing_paths = [path.name for path in required_paths if not path.is_file()] + assert not missing_paths, f"missing C++ policy files: {missing_paths}" + + clang_format = read(ROOT / ".clang-format") + for setting in ( + "BasedOnStyle: Google", + "IndentWidth: 2", + "ColumnLimit: 80", + ): + assert setting in clang_format + + clang_tidy = read(ROOT / ".clang-tidy") + for setting in ( + "-std=c++17", + "readability-identifier-naming", + "readability-identifier-naming.ClassCase: CamelCase", + "readability-identifier-naming.StructCase: CamelCase", + "readability-identifier-naming.EnumCase: CamelCase", + "readability-identifier-naming.TypeAliasCase: CamelCase", + "readability-identifier-naming.FunctionCase: CamelCase", + "readability-identifier-naming.MethodCase: CamelCase", + "readability-identifier-naming.VariableCase: lower_case", + "readability-identifier-naming.ParameterCase: lower_case", + "readability-identifier-naming.LocalVariableCase: lower_case", + "readability-identifier-naming.PrivateMemberCase: lower_case", + "readability-identifier-naming.PrivateMemberSuffix: _", + "readability-identifier-naming.ProtectedMemberCase: lower_case", + "readability-identifier-naming.ProtectedMemberSuffix: _", + "readability-identifier-naming.PublicMemberCase: lower_case", + "readability-identifier-naming.ConstantCase: CamelCase", + "readability-identifier-naming.ConstantPrefix: k", + "readability-identifier-naming.EnumConstantCase: CamelCase", + "readability-identifier-naming.EnumConstantPrefix: k", + "readability-identifier-naming.NamespaceCase: lower_case", + "readability-identifier-naming.MacroDefinitionCase: UPPER_CASE", + ): + assert setting in clang_tidy + + doxyfile = doxygen_assignments(read(ROOT / "Doxyfile")) + assert doxyfile["INPUT"].split() == ["include", "src"] + assert doxyfile["EXCLUDE"].split() == ["tests"] + assert doxyfile["WARN_AS_ERROR"] == "YES" + assert doxyfile["OUTPUT_DIRECTORY"] == ".harness/doxygen" + assert doxyfile["HTML_OUTPUT"] == "html" + + gitignore_lines = read(ROOT / ".gitignore").splitlines() + assert ".harness/doxygen/" in gitignore_lines + + cmake = read(ROOT / "CMakeLists.txt") + assert "find_package(Doxygen QUIET)" in cmake + doxygen_block = re.search( + r"if\(Doxygen_FOUND\)(?P.*?)endif\(\)", cmake, re.DOTALL + ) + assert doxygen_block is not None + assert "add_custom_target(fesa_docs" in doxygen_block.group("body") + assert "Doxyfile" in doxygen_block.group("body") + assert re.search(r"add_custom_target\(fesa_docs\s+ALL\b", cmake) is None