diff --git a/CHANGELOG.md b/CHANGELOG.md index 946bdcc..68ff601 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. ## Next Release +### Features + +#### CPP Rules + +- Add CPP rule to check that in a header file there is always a header guard present +- Adding option to enforce header guard format via file path + ## [0.0.3](https://github.com/repo/owner/releases/tag/0.0.3) - 2025-12-20 diff --git a/src/devops/config/config_cpp.py b/src/devops/config/config_cpp.py index d978ebb..c58f51b 100644 --- a/src/devops/config/config_cpp.py +++ b/src/devops/config/config_cpp.py @@ -13,16 +13,23 @@ class CppConfig: # Enable or disable running C++ style checks (e.g., clang-format, clang-tidy). style_checks: bool = True + # Enable or disable verification that source files contain # the expected license header. license_header_check: bool = True + # Path to the license header file whose contents should be enforced, or None to use # the tool's default behavior (for example, no custom license header content). license_header: str | None = None + # If True, limit checks to files that are currently staged # (e.g., in a pre-commit hook). check_only_staged_files: bool = False + # If True, enforce that header guards match the file path. + # This helps ensure consistency and prevents duplicate header guards. + header_guards_according_to_filepath: bool = False + def to_toml_lines(self) -> list[str]: """Convert the CppConfig to TOML lines. @@ -50,6 +57,11 @@ def to_toml_lines(self) -> list[str]: f"#check_only_staged_files = {str(self.check_only_staged_files).lower()}\n" ) + lines.append( + "#header_guards_according_to_filepath = " + f"{str(self.header_guards_according_to_filepath).lower()}\n" + ) + return lines @@ -80,11 +92,18 @@ def parse_cpp_config(raw_config: dict) -> CppConfig: table, "check_only_staged_files", default=CppConfig.check_only_staged_files ) + header_guards_according_to_filepath = get_bool( + table, + "header_guards_according_to_filepath", + default=CppConfig.header_guards_according_to_filepath, + ) + config = CppConfig( style_checks=style_checks, license_header_check=license_header_check, license_header=license_header, check_only_staged_files=check_only_staged_files, + header_guards_according_to_filepath=header_guards_according_to_filepath, ) config_logger.debug(f"Parsed C++ configuration: {config}") diff --git a/src/devops/cpp/checks.py b/src/devops/cpp/checks.py index e191261..7105191 100644 --- a/src/devops/cpp/checks.py +++ b/src/devops/cpp/checks.py @@ -14,6 +14,7 @@ ) from devops.logger import cpp_check_logger from devops.rules import ( + FileRuleInput, ResultType, Rule, filter_file_rules, @@ -89,6 +90,7 @@ def run_file_rules(rules: list[Rule], file: Path) -> list[ResultType]: """ results = [] + file = Path(file) # to be 100% sure file_type = determine_file_type(file) if any(not is_file_rule(rule) for rule in rules): @@ -101,7 +103,7 @@ def run_file_rules(rules: list[Rule], file: Path) -> list[ResultType]: if file_type not in rule.file_types: continue - results.append(rule.apply((content,))) + results.append(rule.apply(FileRuleInput(file_content=content, path=file))) return results diff --git a/src/devops/cpp/license_header.py b/src/devops/cpp/license_header.py index d3932f3..e0eed00 100644 --- a/src/devops/cpp/license_header.py +++ b/src/devops/cpp/license_header.py @@ -2,21 +2,25 @@ from __future__ import annotations +import typing from pathlib import Path from devops.files import file_exist, open_file from devops.rules import ResultType, ResultTypeEnum, Rule, RuleInputType, RuleType +if typing.TYPE_CHECKING: + from devops.rules import FileRuleInput + def check_license_header( - file_content: str, required_header_file: str | Path + file_rule_input: FileRuleInput, required_header_file: str | Path ) -> ResultType: """Check if the file content starts with the required license header. Parameters ---------- - file_content: str - The content of the file to check. + file_rule_input: FileRuleInput + The input containing the file content and path to check. required_header_file: str | Path The path to the file containing the required license header. @@ -30,6 +34,8 @@ def check_license_header( DevOpsFileNotFoundError If the required header file does not exist. """ + file_content = file_rule_input.file_content + required_header_file = Path(required_header_file) # return value can be ignored as exception will be raised if file does not exist diff --git a/src/devops/cpp/style_rules.py b/src/devops/cpp/style_rules.py index a355247..75fd7f6 100644 --- a/src/devops/cpp/style_rules.py +++ b/src/devops/cpp/style_rules.py @@ -1,8 +1,17 @@ """C++ rules for mstd devops.""" -from devops.rules import Rule, RuleInputType, RuleType +from __future__ import annotations + +import typing + +from devops import __GLOBAL_CONFIG__ +from devops.files import FileType +from devops.rules import ResultType, ResultTypeEnum, Rule, RuleInputType, RuleType from devops.utils import check_key_sequence_ordered +if typing.TYPE_CHECKING: + from devops.rules import FileRuleInput + class CheckKeySeqOrder(Rule): """Rule to check that a specific key sequence appears only in a given order.""" @@ -25,6 +34,148 @@ def __init__(self, key_sequence: str) -> None: ) +class HeaderGuardError(Exception): + """Custom exception for header guard errors.""" + + +def find_define_macro(lines: list[str], macro: str) -> bool: + """Check if the #define for the given macro exists in the lines. + + Parameters + ---------- + lines: list[str] + The lines of the header file. + macro: str + The header guard macro to look for. + + Returns + ------- + bool + True if the #define for the macro is found, False otherwise. + + """ + for _line in lines: + line = _line.strip() + if line.startswith("#define"): + parts = line.split() + if len(parts) <= 1: + continue + if parts[1] == macro: + return True + return False + + +def find_header_guard(lines: list[str]) -> str: + """Find the header guard macro in the given lines. + + Parameters + ---------- + lines: list[str] + The lines of the header file. + + Returns + ------- + str + The header guard macro if found. + + Raises + ------ + HeaderGuardError + If the header guard is not properly defined. + + """ + macro = None + # Search for the header guard macro near the beginning of the file. + # Limit the search to the first N lines to avoid picking up feature-detection + # or other conditional macros that appear later in the file. + max_header_guard_search_lines = 50 + for idx, _line in enumerate(lines): + if idx > max_header_guard_search_lines: + break + line = _line.strip() + if line.startswith("#ifndef"): + parts = line.split() + if len(parts) <= 1: + continue + macro = parts[1] + # Assume the first valid #ifndef within the search window is the + # header guard and stop searching to avoid later #ifndef directives. + break + + if macro is None: + msg = "Header guard macro not found with #ifndef." + raise HeaderGuardError(msg) + + is_defined = find_define_macro(lines, macro) + + if not is_defined: + msg = "Header guard macro not defined with #define." + raise HeaderGuardError(msg) + + if not any(line.lstrip().startswith("#endif") for line in lines): + msg = "Header guard missing closing #endif." + raise HeaderGuardError(msg) + + return macro + + +def check_header_guards(file_rule_input: FileRuleInput) -> ResultType: + """Check if the C++ header file has proper header guards. + + Parameters + ---------- + file_rule_input: FileRuleInput + The input containing the file content and path to check. + + Returns + ------- + ResultType + The result of the header guard check. + """ + file_content = file_rule_input.file_content + path = file_rule_input.path + + lines = file_content.splitlines() + try: + guard_macro = find_header_guard(lines) + except HeaderGuardError as e: + return ResultType(ResultTypeEnum.Error, str(e)) + + if __GLOBAL_CONFIG__.cpp.header_guards_according_to_filepath and path is not None: + expected_macro = str(path).upper() + expected_macro = expected_macro.removeprefix("INCLUDE/") + expected_macro = expected_macro.removeprefix("TEST/") + expected_macro = expected_macro.replace("/", "__") + expected_macro = expected_macro.removesuffix(".HPP") + expected_macro = expected_macro.removesuffix(".H") + expected_macro = "__" + expected_macro + "_HPP__" + + if guard_macro != expected_macro: + msg = ( + f"Header guard macro '{guard_macro}' does not match expected " + f"macro '{expected_macro}' according to file path." + ) + return ResultType(ResultTypeEnum.Error, msg) + + return ResultType(ResultTypeEnum.Ok) + + +class CheckHeaderGuards(Rule): + """Rule to check for proper header guards in C++ header files.""" + + def __init__(self) -> None: + """Initialize CheckHeaderGuards rule.""" + super().__init__( + name="CheckHeaderGuards", + description="Ensure that all C++ header files have proper header guards.", + rule_type=RuleType.CPP_STYLE, + rule_input_type=RuleInputType.FILE, + file_types=[FileType.CPPHeader], + func=check_header_guards, + ) + + rule01 = CheckKeySeqOrder("static inline constexpr") +rule02 = CheckHeaderGuards() -cpp_style_rules = [rule01] +cpp_style_rules = [rule01, rule02] diff --git a/src/devops/rules/__init__.py b/src/devops/rules/__init__.py index b9f3702..f34fb1c 100644 --- a/src/devops/rules/__init__.py +++ b/src/devops/rules/__init__.py @@ -1,7 +1,8 @@ -"""Top level package for rules in mstd checks.""" +"""Top level package for rules in devops.""" from .result_type import ResultType, ResultTypeEnum from .rules import ( + FileRuleInput, Rule, RuleInputType, RuleType, @@ -14,6 +15,7 @@ __all__ = ["ResultType", "ResultTypeEnum"] __all__ += [ + "FileRuleInput", "Rule", "RuleInputType", "RuleType", diff --git a/src/devops/rules/rules.py b/src/devops/rules/rules.py index b3b8276..79edc63 100644 --- a/src/devops/rules/rules.py +++ b/src/devops/rules/rules.py @@ -1,20 +1,23 @@ -"""Module defining rules for mstd checks.""" +"""Module defining rules for devops.""" from __future__ import annotations import typing +from dataclasses import dataclass from devops.enums import StrEnum from devops.files import FileType if typing.TYPE_CHECKING: from collections.abc import Callable + from pathlib import Path + from typing import Any from .result_type import ResultType class RuleType(StrEnum): - """Enumeration of rule types for mstd checks.""" + """Enumeration of rule types for devops.""" GENERAL = "GENERAL" CPP_STYLE = "CPP_STYLE" @@ -33,13 +36,21 @@ def cpp_rules(cls) -> set[RuleType]: class RuleInputType(StrEnum): - """Enumeration of rule input types for mstd checks.""" + """Enumeration of rule input types for devops.""" NONE = "NONE" LINE = "LINE" FILE = "FILE" +@dataclass(frozen=True) +class FileRuleInput: + """Dataclass for file rule input.""" + + file_content: str + path: Path | None = None + + class Rule: """Base class for defining a rule.""" @@ -127,13 +138,13 @@ def __init__( self.rule_identifier = Rule.increment_rule_counter(rule_type) - def apply(self, args: tuple) -> ResultType: + def apply(self, rule_input: Any) -> ResultType: """Apply the rule on a specific line. Parameters ---------- - args: tuple - The arguments to pass to the rule function. + rule_input: Any + The rule input to apply the rule on. Returns ------- @@ -141,10 +152,7 @@ def apply(self, args: tuple) -> ResultType: The result of applying the rule. """ - if isinstance(args, str): - args = (args,) - - return self.func(*args) if args else self.func() + return self.func(rule_input) def filter_cpp_rules(rules: list[Rule]) -> list[Rule]: diff --git a/tests/cpp/test_license_header.py b/tests/cpp/test_license_header.py index 1876445..500e06a 100644 --- a/tests/cpp/test_license_header.py +++ b/tests/cpp/test_license_header.py @@ -5,7 +5,7 @@ import typing from devops.cpp.license_header import CheckLicenseHeader, check_license_header -from devops.rules import ResultTypeEnum, RuleInputType, RuleType +from devops.rules import FileRuleInput, ResultTypeEnum, RuleInputType, RuleType if typing.TYPE_CHECKING: from pathlib import Path @@ -30,7 +30,9 @@ def test_check_license_header_present(self, tmp_path: Path) -> None: # Create file content that starts with the header file_content = "// Copyright 2024\n// All rights reserved\n\nint main() {}\n" - result = check_license_header(file_content, header_file) + result = check_license_header( + FileRuleInput(file_content=file_content), header_file + ) assert result.value == ResultTypeEnum.Ok def test_check_license_header_missing(self, tmp_path: Path) -> None: @@ -49,7 +51,9 @@ def test_check_license_header_missing(self, tmp_path: Path) -> None: # Create file content without the header file_content = "int main() {}\n" - result = check_license_header(file_content, header_file) + result = check_license_header( + FileRuleInput(file_content=file_content), header_file + ) assert result.value == ResultTypeEnum.Error assert result.description == "Missing or incorrect license header." @@ -69,7 +73,9 @@ def test_check_license_header_incorrect(self, tmp_path: Path) -> None: # Create file content with different header file_content = "// Copyright 2023\n// Some rights reserved\n\nint main() {}\n" - result = check_license_header(file_content, header_file) + result = check_license_header( + FileRuleInput(file_content=file_content), header_file + ) assert result.value == ResultTypeEnum.Error assert result.description == "Missing or incorrect license header." @@ -89,7 +95,9 @@ def test_check_license_header_partial_match(self, tmp_path: Path) -> None: # Create file content with only part of the header file_content = "// Copyright 2024\n\nint main() {}\n" - result = check_license_header(file_content, header_file) + result = check_license_header( + FileRuleInput(file_content=file_content), header_file + ) assert result.value == ResultTypeEnum.Error def test_check_license_header_empty_file(self, tmp_path: Path) -> None: @@ -108,7 +116,9 @@ def test_check_license_header_empty_file(self, tmp_path: Path) -> None: # Empty file content file_content = "" - result = check_license_header(file_content, header_file) + result = check_license_header( + FileRuleInput(file_content=file_content), header_file + ) assert result.value == ResultTypeEnum.Error def test_check_license_header_empty_header(self, tmp_path: Path) -> None: @@ -127,7 +137,9 @@ def test_check_license_header_empty_header(self, tmp_path: Path) -> None: # Any file content should pass with empty header file_content = "int main() {}\n" - result = check_license_header(file_content, header_file) + result = check_license_header( + FileRuleInput(file_content=file_content), header_file + ) assert result.value == ResultTypeEnum.Ok def test_check_license_header_with_str_path(self, tmp_path: Path) -> None: @@ -146,7 +158,9 @@ def test_check_license_header_with_str_path(self, tmp_path: Path) -> None: # Create file content with the header file_content = "// Header\nint main() {}\n" - result = check_license_header(file_content, str(header_file)) + result = check_license_header( + FileRuleInput(file_content=file_content), str(header_file) + ) assert result.value == ResultTypeEnum.Ok def test_check_license_header_multiline(self, tmp_path: Path) -> None: @@ -171,7 +185,9 @@ def test_check_license_header_multiline(self, tmp_path: Path) -> None: # Create file content with the header file_content = header_text + "\n#include \n\nint main() {}\n" - result = check_license_header(file_content, header_file) + result = check_license_header( + FileRuleInput(file_content=file_content), header_file + ) assert result.value == ResultTypeEnum.Ok def test_check_license_header_with_leading_whitespace(self, tmp_path: Path) -> None: @@ -193,7 +209,9 @@ def test_check_license_header_with_leading_whitespace(self, tmp_path: Path) -> N " // Copyright 2024\n // All rights reserved\n\nint main() {}\n" ) - result = check_license_header(file_content, header_file) + result = check_license_header( + FileRuleInput(file_content=file_content), header_file + ) assert result.value == ResultTypeEnum.Ok @@ -238,7 +256,7 @@ def test_check_license_header_class_apply_with_valid_content( rule = CheckLicenseHeader(str(header_file)) file_content = "// Header\nint main() {}\n" - result = rule.apply((file_content,)) + result = rule.apply(FileRuleInput(file_content=file_content)) assert result.value == ResultTypeEnum.Ok def test_check_license_header_class_apply_with_invalid_content( @@ -258,7 +276,7 @@ def test_check_license_header_class_apply_with_invalid_content( rule = CheckLicenseHeader(str(header_file)) file_content = "int main() {}\n" - result = rule.apply((file_content,)) + result = rule.apply(FileRuleInput(file_content=file_content, path=header_file)) assert result.value == ResultTypeEnum.Error assert result.description == "Missing or incorrect license header." @@ -277,5 +295,5 @@ def test_check_license_header_class_with_path_object(self, tmp_path: Path) -> No rule = CheckLicenseHeader(header_file) file_content = "// Header\nint main() {}\n" - result = rule.apply((file_content,)) + result = rule.apply(FileRuleInput(file_content=file_content, path=header_file)) assert result.value == ResultTypeEnum.Ok diff --git a/tests/cpp/test_style_rules.py b/tests/cpp/test_style_rules.py index effac7e..8e4f8cd 100644 --- a/tests/cpp/test_style_rules.py +++ b/tests/cpp/test_style_rules.py @@ -1,8 +1,25 @@ """Unit tests for C++ style rules.""" -from devops.cpp.style_rules import CheckKeySeqOrder, cpp_style_rules, rule01 +from __future__ import annotations + +from pathlib import Path +from unittest.mock import patch + +import pytest + +from devops.cpp.style_rules import ( + CheckHeaderGuards, + CheckKeySeqOrder, + HeaderGuardError, + check_header_guards, + cpp_style_rules, + find_define_macro, + find_header_guard, + rule01, + rule02, +) from devops.files import FileType -from devops.rules import ResultTypeEnum, Rule, RuleInputType, RuleType +from devops.rules import FileRuleInput, ResultTypeEnum, Rule, RuleInputType, RuleType class TestCheckKeySeqOrder: @@ -259,3 +276,441 @@ def test_wrong_order_in_function(self) -> None: "inline static constexpr auto compute() -> int { return 42; }" ) assert result.value == ResultTypeEnum.Error + + +class TestFindDefineMacro: + """Tests for find_define_macro function.""" + + def test_find_define_macro_present(self) -> None: + """Test finding a #define macro that exists.""" + lines = [ + "#ifndef MY_HEADER_HPP", + "#define MY_HEADER_HPP", + "", + "// code here", + "#endif", + ] + result = find_define_macro(lines, "MY_HEADER_HPP") + assert result is True + + def test_find_define_macro_not_present(self) -> None: + """Test when #define macro does not exist.""" + lines = [ + "#ifndef MY_HEADER_HPP", + "#define OTHER_MACRO", + "", + "// code here", + "#endif", + ] + result = find_define_macro(lines, "MY_HEADER_HPP") + assert result is False + + def test_find_define_macro_with_whitespace(self) -> None: + """Test finding #define with leading/trailing whitespace.""" + lines = [ + " #ifndef MY_HEADER_HPP ", + " #define MY_HEADER_HPP ", + "", + "// code here", + "#endif", + ] + result = find_define_macro(lines, "MY_HEADER_HPP") + assert result is True + + def test_find_define_macro_multiple_defines(self) -> None: + """Test finding specific macro among multiple #defines.""" + lines = [ + "#ifndef MY_HEADER_HPP", + "#define FIRST_MACRO", + "#define MY_HEADER_HPP", + "#define LAST_MACRO", + "#endif", + ] + result = find_define_macro(lines, "MY_HEADER_HPP") + assert result is True + + def test_find_define_macro_malformed_define(self) -> None: + """Test with malformed #define (no macro name).""" + lines = [ + "#ifndef MY_HEADER_HPP", + "#define", + "#endif", + ] + result = find_define_macro(lines, "MY_HEADER_HPP") + assert result is False + + def test_find_define_macro_empty_lines(self) -> None: + """Test with empty list of lines.""" + lines: list[str] = [] + result = find_define_macro(lines, "MY_HEADER_HPP") + assert result is False + + +class TestFindHeaderGuard: + """Tests for find_header_guard function.""" + + def test_find_header_guard_valid(self) -> None: + """Test finding a valid header guard.""" + lines = [ + "#ifndef MY_HEADER_HPP", + "#define MY_HEADER_HPP", + "", + "// code here", + "#endif", + ] + result = find_header_guard(lines) + assert result == "MY_HEADER_HPP" + + def test_find_header_guard_missing_ifndef(self) -> None: + """Test when #ifndef is missing.""" + lines = [ + "#define MY_HEADER_HPP", + "", + "// code here", + "#endif", + ] + with pytest.raises( + HeaderGuardError, match="Header guard macro not found with #ifndef" + ): + find_header_guard(lines) + + def test_find_header_guard_missing_define(self) -> None: + """Test when #define is missing.""" + lines = [ + "#ifndef MY_HEADER_HPP", + "", + "// code here", + "#endif", + ] + with pytest.raises( + HeaderGuardError, match="Header guard macro not defined with #define" + ): + find_header_guard(lines) + + def test_find_header_guard_missing_endif(self) -> None: + """Test when #endif is missing.""" + lines = [ + "#ifndef MY_HEADER_HPP", + "#define MY_HEADER_HPP", + "", + "// code here", + ] + with pytest.raises( + HeaderGuardError, match="Header guard missing closing #endif" + ): + find_header_guard(lines) + + def test_find_header_guard_malformed_ifndef(self) -> None: + """Test with malformed #ifndef (no macro name).""" + lines = [ + "#ifndef", + "#define MY_HEADER_HPP", + "#endif", + ] + with pytest.raises( + HeaderGuardError, match="Header guard macro not found with #ifndef" + ): + find_header_guard(lines) + + def test_find_header_guard_with_whitespace(self) -> None: + """Test header guard with leading/trailing whitespace.""" + lines = [ + " #ifndef MY_HEADER_HPP ", + " #define MY_HEADER_HPP ", + "", + "// code here", + " #endif ", + ] + result = find_header_guard(lines) + assert result == "MY_HEADER_HPP" + + def test_find_header_guard_first_valid_ifndef(self) -> None: + """Test that it finds the first valid #ifndef within search window.""" + lines = [ + "// Some comment", + "#ifndef MY_HEADER_HPP", + "#define MY_HEADER_HPP", + "", + "#ifndef FEATURE_DETECTION", + "#define FEATURE_DETECTION", + "#endif", + "", + "#endif", + ] + result = find_header_guard(lines) + assert result == "MY_HEADER_HPP" + + def test_find_header_guard_beyond_search_limit(self) -> None: + """Test that #ifndef beyond search limit is not found.""" + # Create a file with #ifndef after 50 lines + lines = ["// comment line"] * 51 + lines.append("#ifndef MY_HEADER_HPP") + lines.append("#define MY_HEADER_HPP") + lines.append("#endif") + + with pytest.raises( + HeaderGuardError, match="Header guard macro not found with #ifndef" + ): + find_header_guard(lines) + + +class TestCheckHeaderGuards: + """Tests for check_header_guards function.""" + + def test_check_header_guards_valid(self) -> None: + """Test with valid header guards.""" + file_content = """#ifndef MY_HEADER_HPP +#define MY_HEADER_HPP + +class MyClass { +public: + void myMethod(); +}; + +#endif +""" + result = check_header_guards(FileRuleInput(file_content=file_content)) + assert result.value == ResultTypeEnum.Ok + + def test_check_header_guards_missing_ifndef(self) -> None: + """Test with missing #ifndef.""" + file_content = """#define MY_HEADER_HPP + +class MyClass { +public: + void myMethod(); +}; + +#endif +""" + result = check_header_guards(FileRuleInput(file_content=file_content)) + assert result.value == ResultTypeEnum.Error + assert "Header guard macro not found with #ifndef" in result.description + + def test_check_header_guards_missing_define(self) -> None: + """Test with missing #define.""" + file_content = """#ifndef MY_HEADER_HPP + +class MyClass { +public: + void myMethod(); +}; + +#endif +""" + result = check_header_guards(FileRuleInput(file_content=file_content)) + assert result.value == ResultTypeEnum.Error + assert "Header guard macro not defined with #define" in result.description + + def test_check_header_guards_missing_endif(self) -> None: + """Test with missing #endif.""" + file_content = """#ifndef MY_HEADER_HPP +#define MY_HEADER_HPP + +class MyClass { +public: + void myMethod(); +}; +""" + result = check_header_guards(FileRuleInput(file_content=file_content)) + assert result.value == ResultTypeEnum.Error + assert "Header guard missing closing #endif" in result.description + + def test_check_header_guards_with_path_match_disabled(self) -> None: + """Test header guards when path matching is disabled.""" + file_content = """#ifndef ANY_NAME_WORKS +#define ANY_NAME_WORKS + +class MyClass {}; + +#endif +""" + with patch("devops.cpp.style_rules.__GLOBAL_CONFIG__") as mock_config: + mock_config.cpp.header_guards_according_to_filepath = False + result = check_header_guards( + FileRuleInput( + file_content=file_content, path=Path("include/test/myfile.hpp") + ) + ) + assert result.value == ResultTypeEnum.Ok + + def test_check_header_guards_with_path_match_enabled_correct(self) -> None: + """Test header guards with path matching enabled and correct macro.""" + file_content = """#ifndef __MYFILE_HPP__ +#define __MYFILE_HPP__ + +class MyClass {}; + +#endif +""" + with patch("devops.cpp.style_rules.__GLOBAL_CONFIG__") as mock_config: + mock_config.cpp.header_guards_according_to_filepath = True + result = check_header_guards( + FileRuleInput( + file_content=file_content, path=Path("include/myfile.hpp") + ) + ) + assert result.value == ResultTypeEnum.Ok + + def test_check_header_guards_with_path_match_enabled_incorrect(self) -> None: + """Test header guards with path matching enabled but incorrect macro.""" + file_content = """#ifndef WRONG_MACRO_NAME +#define WRONG_MACRO_NAME + +class MyClass {}; + +#endif +""" + with patch("devops.cpp.style_rules.__GLOBAL_CONFIG__") as mock_config: + mock_config.cpp.header_guards_according_to_filepath = True + result = check_header_guards( + FileRuleInput( + file_content=file_content, path=Path("include/myfile.hpp") + ) + ) + assert result.value == ResultTypeEnum.Error + assert "does not match expected macro" in result.description + assert "__MYFILE_HPP__" in result.description + + def test_check_header_guards_with_include_prefix(self) -> None: + """Test header guards with INCLUDE/ prefix in path.""" + file_content = """#ifndef __UTILS__HELPER_HPP__ +#define __UTILS__HELPER_HPP__ + +class Helper {}; + +#endif +""" + with patch("devops.cpp.style_rules.__GLOBAL_CONFIG__") as mock_config: + mock_config.cpp.header_guards_according_to_filepath = True + result = check_header_guards( + FileRuleInput( + file_content=file_content, path=Path("INCLUDE/utils/helper.hpp") + ) + ) + assert result.value == ResultTypeEnum.Ok + + def test_check_header_guards_with_test_prefix(self) -> None: + """Test header guards with TEST/ prefix in path.""" + file_content = """#ifndef __MOCKS__MOCK_CLASS_HPP__ +#define __MOCKS__MOCK_CLASS_HPP__ + +class MockClass {}; + +#endif +""" + with patch("devops.cpp.style_rules.__GLOBAL_CONFIG__") as mock_config: + mock_config.cpp.header_guards_according_to_filepath = True + result = check_header_guards( + FileRuleInput( + file_content=file_content, path=Path("TEST/mocks/mock_class.hpp") + ) + ) + assert result.value == ResultTypeEnum.Ok + + def test_check_header_guards_with_h_extension(self) -> None: + """Test header guards with .h extension.""" + file_content = """#ifndef __MYHEADER_HPP__ +#define __MYHEADER_HPP__ + +void myFunction(); + +#endif +""" + with patch("devops.cpp.style_rules.__GLOBAL_CONFIG__") as mock_config: + mock_config.cpp.header_guards_according_to_filepath = True + result = check_header_guards( + FileRuleInput( + file_content=file_content, path=Path("include/myheader.h") + ) + ) + assert result.value == ResultTypeEnum.Ok + + def test_check_header_guards_no_path_provided(self) -> None: + """Test header guards when no path is provided.""" + file_content = """#ifndef MY_HEADER_HPP +#define MY_HEADER_HPP + +class MyClass {}; + +#endif +""" + with patch("devops.cpp.style_rules.__GLOBAL_CONFIG__") as mock_config: + mock_config.cpp.header_guards_according_to_filepath = True + result = check_header_guards(FileRuleInput(file_content=file_content)) + assert result.value == ResultTypeEnum.Ok + + def test_check_header_guards_nested_path(self) -> None: + """Test header guards with deeply nested path.""" + file_content = """#ifndef __A__B__C__D__FILE_HPP__ +#define __A__B__C__D__FILE_HPP__ + +class File {}; + +#endif +""" + with patch("devops.cpp.style_rules.__GLOBAL_CONFIG__") as mock_config: + mock_config.cpp.header_guards_according_to_filepath = True + result = check_header_guards( + FileRuleInput( + file_content=file_content, path=Path("include/a/b/c/d/file.hpp") + ) + ) + assert result.value == ResultTypeEnum.Ok + + +class TestCheckHeaderGuardsRule: + """Tests for CheckHeaderGuards rule class.""" + + def setup_method(self) -> None: + """Reset rule counters before each test.""" + Rule.cpp_style_rule_counter = 0 + Rule.general_rule_counter = 0 + + def test_rule_initialization(self) -> None: + """Test CheckHeaderGuards rule initialization.""" + rule = CheckHeaderGuards() + assert rule.name == "CheckHeaderGuards" + assert rule.rule_type == RuleType.CPP_STYLE + assert rule.rule_input_type == RuleInputType.FILE + assert rule.description == ( + "Ensure that all C++ header files have proper header guards." + ) + assert rule.file_types == [FileType.CPPHeader] + + def test_rule_apply_valid(self) -> None: + """Test applying rule to valid header file.""" + rule = CheckHeaderGuards() + file_content = """#ifndef MY_HEADER_HPP +#define MY_HEADER_HPP + +class MyClass {}; + +#endif +""" + result = rule.apply(FileRuleInput(file_content=file_content)) + assert result.value == ResultTypeEnum.Ok + + def test_rule_apply_invalid(self) -> None: + """Test applying rule to invalid header file.""" + rule = CheckHeaderGuards() + file_content = """class MyClass {}; +""" + result = rule.apply(FileRuleInput(file_content=file_content)) + assert result.value == ResultTypeEnum.Error + + def test_rule02_is_check_header_guards(self) -> None: + """Test that rule02 is a CheckHeaderGuards instance.""" + assert isinstance(rule02, CheckHeaderGuards) + + def test_rule02_in_cpp_style_rules(self) -> None: + """Test that rule02 is in the cpp_style_rules list.""" + assert rule02 in cpp_style_rules + + def test_cpp_style_rules_contains_header_guard_rule(self) -> None: + """Test that cpp_style_rules contains CheckHeaderGuards rule.""" + header_guard_rules = [ + rule for rule in cpp_style_rules if isinstance(rule, CheckHeaderGuards) + ] + assert len(header_guard_rules) == 1 + assert header_guard_rules[0] == rule02 diff --git a/tests/rules/test_rules.py b/tests/rules/test_rules.py index 378d027..b058d3c 100644 --- a/tests/rules/test_rules.py +++ b/tests/rules/test_rules.py @@ -175,21 +175,6 @@ def check_func(line: str) -> ResultType: result = rule.apply("goodbye world") assert result.value == ResultTypeEnum.Error - def test_apply_with_tuple_arg(self) -> None: - """Test Rule apply with tuple argument.""" - - def check_func(a: str, b: str) -> ResultType: - if a == b: - return ResultType(ResultTypeEnum.Ok) - return ResultType(ResultTypeEnum.Error, "Values don't match") - - rule = Rule(name="match_check", func=check_func) - result = rule.apply(("test", "test")) - assert result.value == ResultTypeEnum.Ok - - result = rule.apply(("test1", "test2")) - assert result.value == ResultTypeEnum.Error - class TestRuleFiltering: """Tests for rule filtering functions.""" diff --git a/tests/scripts/test_cpp_checks.py b/tests/scripts/test_cpp_checks.py index 67151a6..a420bd3 100644 --- a/tests/scripts/test_cpp_checks.py +++ b/tests/scripts/test_cpp_checks.py @@ -14,6 +14,8 @@ if typing.TYPE_CHECKING: from pathlib import Path + from devops.rules import FileRuleInput + cpp_rules = build_cpp_rules() @@ -378,8 +380,8 @@ def test_run_file_rules_receives_full_content(self, tmp_path: Path) -> None: received_content = [] - def capture_content(content: str) -> ResultType: - received_content.append(content) + def capture_content(file_rule_input: FileRuleInput) -> ResultType: + received_content.append(file_rule_input.file_content) return ResultType(ResultTypeEnum.Ok) rule = Rule(