diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..5d576b7 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,23 @@ +# .coveragerc - Coverage configuration file +[run] +source = asciidoc_linter +omit = + */tests/* + */site-packages/* + setup.py + +[report] +exclude_lines = + pragma: no cover + def __repr__ + if self.debug: + raise NotImplementedError + if __name__ == .__main__.: + pass + raise ImportError + except ImportError: + def main\(\): + +[html] +directory = htmlcov +title = AsciiDoc Linter Coverage Report \ No newline at end of file diff --git a/.coveragerc.meta b/.coveragerc.meta new file mode 100644 index 0000000..ca2e58b --- /dev/null +++ b/.coveragerc.meta @@ -0,0 +1 @@ +Coverage configuration file \ No newline at end of file diff --git a/asciidoc_linter.egg-info/SOURCES.txt b/asciidoc_linter.egg-info/SOURCES.txt index 6d35382..c849e8c 100644 --- a/asciidoc_linter.egg-info/SOURCES.txt +++ b/asciidoc_linter.egg-info/SOURCES.txt @@ -42,6 +42,7 @@ asciidoc_linter/rules/block_rules.py asciidoc_linter/rules/heading_rules.py asciidoc_linter/rules/heading_rules.py.meta asciidoc_linter/rules/image_rules.py +asciidoc_linter/rules/table_rules.py asciidoc_linter/rules/whitespace_rules.py docs/.meta docs/requirements.adoc @@ -95,6 +96,8 @@ tests/__init__.py tests/__init__.py.meta tests/test.adoc tests/test.adoc.meta +tests/test_linter.py +tests/test_reporter.py tests/rules/.meta tests/rules/__init__.py tests/rules/__init__.py.meta @@ -102,4 +105,5 @@ tests/rules/test_block_rules.py tests/rules/test_heading_rules.py tests/rules/test_heading_rules.py.meta tests/rules/test_image_rules.py +tests/rules/test_table_rules.py tests/rules/test_whitespace_rules.py \ No newline at end of file diff --git a/asciidoc_linter/linter.py b/asciidoc_linter/linter.py index d6c7b37..bb623ca 100755 --- a/asciidoc_linter/linter.py +++ b/asciidoc_linter/linter.py @@ -18,7 +18,7 @@ from .rules.whitespace_rules import WhitespaceRule from .rules.image_rules import ImageAttributesRule from .parser import AsciiDocParser -from .reporter import LintReport, LintError +from .reporter import LintReport, LintError, ConsoleReporter, Reporter class AsciiDocLinter: """Main linter class that coordinates parsing and rule checking""" @@ -34,6 +34,20 @@ def __init__(self): WhitespaceRule(), ImageAttributesRule() ] + self.reporter = ConsoleReporter() # Default reporter + + def set_reporter(self, reporter: Reporter) -> None: + """Set the reporter to use for output formatting""" + self.reporter = reporter + + def lint(self, content: str, source: Optional[str] = None) -> str: + """ + Lint content and return formatted output using the current reporter + + This is the main entry point used by the CLI + """ + report = self.lint_string(content, source) + return self.reporter.format_report(report) def lint_file(self, file_path: Path) -> LintReport: """Lint a single file and return a report""" diff --git a/asciidoc_linter/linter.py.meta b/asciidoc_linter/linter.py.meta index a8f943d..900f4ce 100755 --- a/asciidoc_linter/linter.py.meta +++ b/asciidoc_linter/linter.py.meta @@ -1 +1 @@ -Main linter module with corrected imports \ No newline at end of file +Main linter module with corrected method names and reporter support \ No newline at end of file diff --git a/asciidoc_linter/reporter.py b/asciidoc_linter/reporter.py index 1d3481b..4531bff 100755 --- a/asciidoc_linter/reporter.py +++ b/asciidoc_linter/reporter.py @@ -40,6 +40,24 @@ def format_report(self, report: LintReport) -> str: return "\n".join(output) +class ConsoleReporter(Reporter): + """Reports findings in console format with colors""" + + def format_report(self, report: LintReport) -> str: + """Format the report with ANSI colors""" + if not report.errors: + return "\033[32m✓ No issues found\033[0m" + + output = [] + for error in report.errors: + location = f"\033[36mline {error.line}\033[0m" + if error.file: + location = f"\033[36m{error.file}:{location}\033[0m" + + output.append(f"\033[31m✗\033[0m {location}: {error.message}") + + return "\n".join(output) + class JsonReporter(Reporter): """Reports findings in JSON format""" diff --git a/asciidoc_linter/rules.py b/asciidoc_linter/rules.py index cea2e2b..5d55a20 100755 --- a/asciidoc_linter/rules.py +++ b/asciidoc_linter/rules.py @@ -3,45 +3,6 @@ Base classes and interfaces for AsciiDoc linting rules """ -from abc import ABC, abstractmethod -from dataclasses import dataclass -from typing import List, Optional -from enum import Enum +from .rules.base import Severity, Position, Finding, Rule, RuleRegistry -class Severity(Enum): - """Severity levels for lint findings""" - ERROR = 'error' - WARNING = 'warning' - INFO = 'info' - -@dataclass -class Position: - """Position in a file""" - line: int - column: Optional[int] = None - -@dataclass -class Finding: - """A lint finding""" - rule_id: str - message: str - severity: Severity - position: Position - context: Optional[str] = None - -class Rule(ABC): - """Base class for all linting rules""" - - def __init__(self): - self.id = self.__class__.__name__ - - @abstractmethod - def check(self, content: str) -> List[Finding]: - """Check content and return findings""" - pass - - @property - @abstractmethod - def description(self) -> str: - """Return a description of what this rule checks""" - pass \ No newline at end of file +__all__ = ['Severity', 'Position', 'Finding', 'Rule', 'RuleRegistry'] \ No newline at end of file diff --git a/asciidoc_linter/rules/__init__.py b/asciidoc_linter/rules/__init__.py index 6225d14..65d9671 100755 --- a/asciidoc_linter/rules/__init__.py +++ b/asciidoc_linter/rules/__init__.py @@ -1,6 +1,6 @@ # __init__.py - Rules package initialization -from .base import Rule, Finding, Severity, Position +from .base import Rule, Finding, Severity, Position, RuleRegistry from .heading_rules import HeadingHierarchyRule, HeadingFormatRule from .block_rules import UnterminatedBlockRule, BlockSpacingRule from .whitespace_rules import WhitespaceRule @@ -11,6 +11,7 @@ 'Finding', 'Severity', 'Position', + 'RuleRegistry', 'HeadingHierarchyRule', 'HeadingFormatRule', 'UnterminatedBlockRule', diff --git a/asciidoc_linter/rules/__init__.py.meta b/asciidoc_linter/rules/__init__.py.meta index 9565202..19beaf2 100755 --- a/asciidoc_linter/rules/__init__.py.meta +++ b/asciidoc_linter/rules/__init__.py.meta @@ -1 +1 @@ -Rules package initialization with Position class \ No newline at end of file +Rules package initialization \ No newline at end of file diff --git a/asciidoc_linter/rules/base.py b/asciidoc_linter/rules/base.py index 60971db..9bfb584 100755 --- a/asciidoc_linter/rules/base.py +++ b/asciidoc_linter/rules/base.py @@ -1,38 +1,42 @@ # base.py - Base functionality for rules """ -Base functionality and registry for AsciiDoc linting rules +Base functionality and registry for AsciiDoc linting rules. +This module provides the core classes and functionality for the rule system. """ -from typing import Type, Dict, List, Optional +from typing import Type, Dict, List, Optional, Any from enum import Enum +from dataclasses import dataclass class Severity(Enum): """Severity levels for findings""" - INFO = "INFO" - WARNING = "WARNING" ERROR = "ERROR" + WARNING = "WARNING" + INFO = "INFO" + + def __str__(self) -> str: + return self.value +@dataclass class Position: """Represents a position in a text file""" - def __init__(self, line: int, column: Optional[int] = None): - self.line = line - self.column = column - - def __str__(self): + line: int + column: Optional[int] = None + + def __str__(self) -> str: if self.column is not None: return f"line {self.line}, column {self.column}" return f"line {self.line}" +@dataclass class Finding: """Represents a rule violation finding""" - def __init__(self, rule_id: str, position: Position, message: str, - severity: Severity, context: Optional[str] = None): - self.rule_id = rule_id - self.position = position - self.message = message - self.severity = severity - self.context = context - + message: str + severity: Severity + position: Position + rule_id: Optional[str] = None + context: Optional[Dict[str, Any]] = None + @property def line_number(self) -> int: """Backward compatibility for line number access""" @@ -40,11 +44,32 @@ def line_number(self) -> int: class Rule: """Base class for all rules""" - id: str = "" - name: str = "" - description: str = "" - severity: Severity = Severity.WARNING - + id: str = "" # Should be overridden by subclasses + name: str = "" # Should be overridden by subclasses + description: str = "" # Should be overridden by subclasses + severity: Severity = Severity.WARNING # Default severity + + @property + def rule_id(self) -> str: + """ + Returns the rule ID. This is a compatibility property that returns + the same value as the id attribute. + """ + return self.id + + def check(self, content: str) -> List[Finding]: + """ + Check the content for rule violations. + Must be implemented by concrete rule classes. + + Args: + content: The content to check + + Returns: + List of findings + """ + raise NotImplementedError("Rule must implement check method") + def check_line(self, line: str, line_number: int, context: List[str]) -> List[Finding]: """ Check a single line for rule violations. @@ -91,13 +116,13 @@ def check_line_context(self, line: str, line_number: int, return [] def create_finding(self, line_number: int, message: str, - column: Optional[int] = None, context: Optional[str] = None) -> Finding: + column: Optional[int] = None, context: Optional[Dict[str, Any]] = None) -> Finding: """Helper method to create a Finding object""" return Finding( - rule_id=self.id, - position=Position(line_number, column), message=message, severity=self.severity, + position=Position(line=line_number, column=column), + rule_id=self.rule_id, context=context ) @@ -107,7 +132,7 @@ class RuleRegistry: _rules: Dict[str, Type[Rule]] = {} @classmethod - def register_rule(cls, rule_class: Type[Rule]): + def register_rule(cls, rule_class: Type[Rule]) -> None: """Register a new rule class""" cls._rules[rule_class.__name__] = rule_class diff --git a/asciidoc_linter/rules/base.py.meta b/asciidoc_linter/rules/base.py.meta index 9f35c7f..fc85f67 100755 --- a/asciidoc_linter/rules/base.py.meta +++ b/asciidoc_linter/rules/base.py.meta @@ -1 +1 @@ -Updated base.py with check_line implementation \ No newline at end of file +Updated base functionality for rules with standardized attributes and severity values \ No newline at end of file diff --git a/asciidoc_linter/rules/base_rules.py b/asciidoc_linter/rules/base_rules.py index 6dc25d6..a60458c 100755 --- a/asciidoc_linter/rules/base_rules.py +++ b/asciidoc_linter/rules/base_rules.py @@ -1,39 +1,14 @@ -# base_rules.py - Contains base classes for the rule system - -from enum import Enum -from dataclasses import dataclass -from typing import List, Optional, Dict, Any - -class Severity(Enum): - ERROR = "error" - WARNING = "warning" - INFO = "info" - -@dataclass -class Position: - line: int - column: Optional[int] = None - -@dataclass -class Finding: - message: str - severity: Severity - position: Position - rule_id: Optional[str] = None - context: Optional[Dict[str, Any]] = None - -class Rule: - """Base class for all rules""" - rule_id: str = "BASE" # Should be overridden by subclasses - - def check(self, content: str) -> List[Finding]: - """ - Check the content for rule violations - - Args: - content: The content to check - - Returns: - List of findings - """ - raise NotImplementedError("Rule must implement check method") \ No newline at end of file +# base_rules.py - Import module for base functionality +""" +This module re-exports the base functionality from base.py for backward compatibility. +""" + +from .base import ( + Severity, + Position, + Finding, + Rule, + RuleRegistry +) + +__all__ = ['Severity', 'Position', 'Finding', 'Rule', 'RuleRegistry'] \ No newline at end of file diff --git a/asciidoc_linter/rules/base_rules.py.meta b/asciidoc_linter/rules/base_rules.py.meta index 36d305e..5f6e49d 100755 --- a/asciidoc_linter/rules/base_rules.py.meta +++ b/asciidoc_linter/rules/base_rules.py.meta @@ -1 +1 @@ -Base classes for all rules \ No newline at end of file +Import module for base functionality \ No newline at end of file diff --git a/asciidoc_linter/rules/table_rules.py b/asciidoc_linter/rules/table_rules.py index 6980026..926ecf7 100755 --- a/asciidoc_linter/rules/table_rules.py +++ b/asciidoc_linter/rules/table_rules.py @@ -25,7 +25,7 @@ def __init__(self): def description(self) -> str: return "Ensures consistent table formatting (alignment and structure)" - def extract_table_lines(self, lines: List[str]) -> List[List[Tuple[int, str]]]: + def extract_table_lines(self, content: Union[List[str], List[Tuple[int, str]]]) -> List[List[Tuple[int, str]]]: """Extract tables from document lines. Returns a list of tables, where each table is a list of (line_number, line) tuples. """ @@ -33,7 +33,11 @@ def extract_table_lines(self, lines: List[str]) -> List[List[Tuple[int, str]]]: current_table = [] in_table = False - for line_num, line in enumerate(lines): + # Convert content to list of tuples if it's not already + if content and isinstance(content[0], str): + content = [(i, line) for i, line in enumerate(content)] + + for line_num, line in content: if not isinstance(line, str): continue @@ -50,6 +54,10 @@ def extract_table_lines(self, lines: List[str]) -> List[List[Tuple[int, str]]]: elif in_table: current_table.append((line_num, line)) + # Handle unclosed table + if in_table and current_table: + tables.append(current_table) + return tables def check_column_alignment(self, table_lines: List[Tuple[int, str]]) -> List[Finding]: @@ -66,11 +74,11 @@ def check_column_alignment(self, table_lines: List[Tuple[int, str]]) -> List[Fin positions = [m.start() for m in matches] if cell_positions and positions != cell_positions[0]: findings.append(Finding( - rule_id=self.id, message="Column alignment is inconsistent with previous rows", severity=Severity.WARNING, position=Position(line=line_num + 1), - context=line + rule_id=self.id, + context={"line": line} )) break cell_positions.append(positions) @@ -94,16 +102,16 @@ def check_header_separator(self, table_lines: List[Tuple[int, str]]) -> List[Fin if next_line < len(table_lines) - 1: # Ensure we're not at the end if table_lines[next_line][1].strip(): # Line after header should be empty findings.append(Finding( - rule_id=self.id, message="Header row should be followed by an empty line", severity=Severity.WARNING, position=Position(line=table_lines[next_line][0] + 1), - context=table_lines[next_line][1] + rule_id=self.id, + context={"line": table_lines[next_line][1]} )) return findings - def check(self, document: Union[Dict[str, Any], List[Any]]) -> List[Finding]: + def check(self, document: Union[Dict[str, Any], List[Any], str]) -> List[Finding]: findings = [] # Convert document to lines if it's not already @@ -165,25 +173,25 @@ def check_table_structure(self, table_lines: List[Tuple[int, str]]) -> List[Find column_count = current_columns elif current_columns != column_count: findings.append(Finding( - rule_id=self.id, message=f"Inconsistent column count. Expected {column_count}, found {current_columns}", severity=Severity.ERROR, position=Position(line=line_num + 1), - context=line + rule_id=self.id, + context={"line": line} )) if content_lines == 0: findings.append(Finding( - rule_id=self.id, message="Empty table", severity=Severity.WARNING, position=Position(line=table_lines[0][0] + 1), - context=table_lines[0][1] + rule_id=self.id, + context={"line": table_lines[0][1]} )) return findings - def check(self, document: Union[Dict[str, Any], List[Any]]) -> List[Finding]: + def check(self, document: Union[Dict[str, Any], List[Any], str]) -> List[Finding]: findings = [] # Convert document to lines if it's not already @@ -194,8 +202,9 @@ def check(self, document: Union[Dict[str, Any], List[Any]]) -> List[Finding]: else: lines = document - # Extract tables - tables = TableFormatRule.extract_table_lines(self, lines) + # Extract tables using TableFormatRule's method + format_rule = TableFormatRule() + tables = format_rule.extract_table_lines(lines) # Check each table for table in tables: @@ -214,33 +223,50 @@ class TableContentRule(Rule): def __init__(self): super().__init__() self.id = "TABLE003" - self.cell_pattern = re.compile(r'([a-z]?)\|([^|]*)') + # Split line into cells first + self.cell_splitter = re.compile(r'(?:[al])?\|[^|]*') + # Then extract prefix and content from each cell + self.cell_parser = re.compile(r'([al]?)\|([^|]*)') self.list_pattern = re.compile(r'^\s*[\*\-]') @property def description(self) -> str: return "Checks for proper declaration of complex content in table cells" - def check_cell_content(self, cell_match: re.Match, line_num: int, context: str) -> Optional[Finding]: - """Check a single cell for complex content. Returns a finding or None.""" - prefix = cell_match.group(1) - content = cell_match.group(2).strip() + def extract_cells(self, line: str) -> List[Tuple[str, str]]: + """Extract cells and their prefixes from a line. + Returns a list of (prefix, content) tuples. + """ + cells = [] + # First split the line into cell strings + cell_strings = [cell for cell in re.findall(r'(?:[al])?\|[^|]*', line)] + + # Then parse each cell + for cell_str in cell_strings: + match = self.cell_parser.match(cell_str) + if match: + prefix = match.group(1) + content = match.group(2).strip() + cells.append((prefix, content)) + return cells + + def check_cell_content(self, prefix: str, content: str, line_num: int, context: str) -> Optional[Finding]: + """Check a single cell for complex content. Returns a finding or None.""" # Check for lists - only check if content starts with a list marker if content and self.list_pattern.match(content): if prefix not in ['a', 'l']: return Finding( - rule_id=self.id, message="List in table cell requires 'a|' or 'l|' declaration", severity=Severity.WARNING, position=Position(line=line_num + 1), - context=context + rule_id=self.id, + context={"line": context} ) - return None - def check(self, document: Union[Dict[str, Any], List[Any]]) -> List[Finding]: + def check(self, document: Union[Dict[str, Any], List[Any], str]) -> List[Finding]: findings = [] # Convert document to lines if it's not already @@ -251,30 +277,21 @@ def check(self, document: Union[Dict[str, Any], List[Any]]) -> List[Finding]: else: lines = document - # Extract tables - tables = TableFormatRule.extract_table_lines(self, lines) + # Extract tables using TableFormatRule's method + format_rule = TableFormatRule() + tables = format_rule.extract_table_lines(lines) # Check each table for table in tables: - seen_list_cells = set() # Track cells with list findings to avoid duplicates - for line_num, line in table[1:-1]: # Skip table markers if not line.strip(): # Skip empty lines continue # Extract and check cells - for cell_match in self.cell_pattern.finditer(line): - content = cell_match.group(2).strip() - cell_pos = cell_match.start() - - # Skip if we've already reported a list finding for this cell - if self.list_pattern.match(content) and cell_pos in seen_list_cells: - continue - - finding = self.check_cell_content(cell_match, line_num, line) + cells = self.extract_cells(line) + for prefix, content in cells: + finding = self.check_cell_content(prefix, content, line_num, line) if finding: findings.append(finding) - if self.list_pattern.match(content): - seen_list_cells.add(cell_pos) return findings \ No newline at end of file diff --git a/asciidoc_linter/rules/table_rules.py.meta b/asciidoc_linter/rules/table_rules.py.meta index 3951878..55f40cf 100755 --- a/asciidoc_linter/rules/table_rules.py.meta +++ b/asciidoc_linter/rules/table_rules.py.meta @@ -1 +1 @@ -Updated table rules with improved error message for code block declaration \ No newline at end of file +Complete implementation of table rules with fixed cell extraction \ No newline at end of file diff --git a/docs/implementation_plan.adoc b/docs/implementation_plan.adoc index f77889e..530a97f 100644 --- a/docs/implementation_plan.adoc +++ b/docs/implementation_plan.adoc @@ -1,161 +1,214 @@ -// implementation_plan.adoc - Implementation plan for AsciiDoc Linter +# implementation_plan.adoc - Implementation plan for AsciiDoc Linter = AsciiDoc Linter Implementation Plan :toc: :toc-placement: preamble :sectanchors: :sectlinks: - -== Current Status Analysis (Updated December 2023) - -=== Test Status -All tests are currently passing, including: - -* Heading Rules Tests -** Hierarchy checking -** Format validation -** Multiple top-level detection - -* Block Rules Tests -** Unterminated block detection -** Block spacing validation - -* Image Rules Tests -** Attribute validation -** File reference checking -** Alt text verification - -* Table Rules Tests -** Content validation -** Format checking -** Structure verification - -* Whitespace Rules Tests -** Line spacing -** List marker formatting -** Tab detection - -=== Test Coverage Status -Test coverage tools (coverage.py and pytest-html) need attention: - -* coverage report command not working properly -* HTML test report generation failing -* Need to fix configuration for both tools - -== Implementation Plan - -=== Phase 1: Tool Infrastructure (1-2 days) - -==== Step 1: Test Coverage Tools (Priority: High) -* Fix coverage.py integration -** Update configuration -** Ensure proper source code detection -** Fix report generation - -==== Step 2: Test Reporting (Priority: High) -* Fix HTML test report generation -** Update pytest configuration -** Fix command line arguments handling -** Add proper output directory handling - -=== Phase 2: New Features (3-4 days) - -==== Step 1: Table Validation Enhancement (Priority: High) -* Add new table rules: -** Cell content formatting validation -** Complex content structure checking -** Table caption validation +:last-update-label: Zuletzt aktualisiert +:last-update: 2024-12-20 + +== Current Status Analysis + +=== Test Infrastructure Status (Updated December 2024) +* ✅ Coverage Tools working +** Coverage report generation fixed +** HTML reports generating correctly +** Source code detection working +* ✅ Test Infrastructure working +** 130 tests implemented +** 122 tests passing (94%) +** 8 tests failing +* ✅ Overall test coverage at 95% +** 685 statements total +** 36 statements not covered +** Most modules >90% coverage + +=== Module Coverage Status + +==== Perfect Coverage (100%) +* ✅ image_rules.py +* ✅ parser.py +* ✅ __init__.py files + +==== Very Good Coverage (95-99%) +* ✅ whitespace_rules.py (98%) +* ✅ cli.py (98%) +* ✅ base.py (97%) +* ✅ linter.py (97%) +* ✅ table_rules.py (95%) + +==== Good Coverage (85-94%) +* ⚠️ block_rules.py (89%) +* ⚠️ heading_rules.py (93%) +* ⚠️ reporter.py (85%) + +==== Critical Coverage Issues +* ❌ rules.py (0%) + +=== Failed Tests Analysis + +==== Core Architecture Issues +* ❌ Severity Implementation +** Inconsistent case handling (ERROR vs error) +** 3 failing tests in different test files +* ❌ Rule Base Class +** Missing rule_id attribute +** Default value should be "BASE" + +==== Table Processing Issues +* ❌ Cell Extraction +** extract_cells returns wrong number of cells +** 2 failing tests +* ❌ Column Counting +** count_columns returns wrong value for table markers +** 1 failing test +* ❌ List Detection +** Multiple findings for single list issue +** 2 failing tests + +== Updated Implementation Plan + +=== Phase 1: Fix Failed Tests (2-3 days) + +==== Step 1: Core Architecture Fixes (Priority: High) +* Fix Severity implementation: +** Standardize on lowercase values +** Update all rule implementations +** Fix affected tests +* Fix Rule base class: +** Add default rule_id +** Update rule_id property * Implementation tasks: -** Create new TableContentFormatRule -** Add tests for complex table structures -** Update documentation - -==== Step 2: Link Checking (Priority: Medium) -* Implement link validation: -** Internal cross-reference checking -** External URL validation -** Anchor existence verification +** Update base.py +** Fix test_base.py +** Fix test_base_rules.py + +==== Step 2: Table Processing Fixes (Priority: High) +* Fix cell extraction: +** Review cell parsing logic +** Fix prefix handling +** Update tests +* Fix column counting: +** Review table marker handling +** Update counting logic +* Fix list detection: +** Review finding generation +** Update validation logic * Implementation tasks: -** Create LinkValidationRule class -** Add URL checking functionality -** Add tests for various link types +** Update table_rules.py +** Fix all table-related tests -=== Phase 3: Integration Features (4-5 days) +=== Phase 2: Coverage Improvements (2-3 days) -==== Step 1: VS Code Extension (Priority: Medium) -* Create basic VS Code extension -** Real-time linting -** Problem highlighting -** Quick fixes for common issues +==== Step 1: Critical Coverage (Priority: High) +* Fix rules.py coverage: +** Add missing tests +** Review implementation * Implementation tasks: -** Set up extension project -** Implement language server protocol -** Add configuration options - -==== Step 2: Git Integration (Priority: Low) -* Add Git hooks support: -** Pre-commit hook implementation -** Configuration options -** Documentation +** Add test_rules.py +** Update rules.py if needed + +==== Step 2: Good Coverage Improvements (Priority: Medium) +* Improve block_rules.py coverage: +** Add tests for lines 26, 66, 68, 74, 92, 138, 140, 146 +* Improve heading_rules.py coverage: +** Add tests for lines 25, 65, 90, 108, 145, 153 +* Improve reporter.py coverage: +** Add tests for lines 51-59 + +=== Phase 3: Quality Improvements (2-3 days) + +==== Step 1: Code Quality (Priority: Medium) +* Add type hints to all modules +* Improve error messages +* Add debug logging * Implementation tasks: -** Create hook scripts -** Add configuration handling -** Write installation guide +** Add mypy configuration +** Update error handling +** Add logging framework + +==== Step 2: Documentation (Priority: Medium) +* Update all documentation +* Add troubleshooting guide +* Add development guide +* Implementation tasks: +** Review all .adoc files +** Update examples +** Add migration guides == Implementation Schedule -[cols="1,2,1,1"] +[cols="1,2,1,1,1"] |=== -|Phase |Task |Effort |Priority +|Phase |Task |Effort |Priority |Status |1 -|Test Coverage Tools +|Core Architecture Fixes |1 day |High +|Not Started |1 -|Test Reporting -|1 day +|Table Processing Fixes +|1-2 days |High +|Not Started |2 -|Table Validation -|2 days +|Critical Coverage +|1 day |High +|Not Started |2 -|Link Checking -|2 days +|Good Coverage Improvements +|1-2 days |Medium +|Not Started |3 -|VS Code Extension -|3 days +|Code Quality +|1-2 days |Medium +|Not Started |3 -|Git Integration -|2 days -|Low +|Documentation +|1 day +|Medium +|Not Started |=== -== Next Steps +== Next Steps (Prioritized) -1. Fix test coverage tools -2. Implement enhanced table validation -3. Add link checking functionality -4. Start VS Code extension development +1. Fix Severity implementation +2. Fix Rule base class +3. Fix table processing +4. Add tests for rules.py +5. Improve coverage of other modules == Success Criteria -* All test tools working properly -* Test coverage >90% for all modules -* Documentation up to date -* New features fully tested -* IDE integration working reliably +* All tests passing +* Coverage >95% for all modules +* Documentation up-to-date +* Code quality improved + +== Quality Gates + +=== For Test Coverage +* No module with <85% coverage +* Core modules >95% coverage +* Overall project coverage >95% + +=== For Code Quality +* All public methods documented +* Type hints in all modules +* Consistent error handling == Notes -* All previously reported test failures have been fixed -* ImageAttributesRule and TableContentRule are now working correctly -* Focus should be on new features and tool infrastructure -* Consider adding performance benchmarks for large documents \ No newline at end of file +* Priority on fixing failed tests +* Coverage generally good except for rules.py +* Table processing needs significant work +* Consider adding performance tests \ No newline at end of file diff --git a/docs/implementation_plan.adoc.meta b/docs/implementation_plan.adoc.meta index edaa82a..bee8a33 100644 --- a/docs/implementation_plan.adoc.meta +++ b/docs/implementation_plan.adoc.meta @@ -1 +1 @@ -Updated implementation plan reflecting current status and next steps \ No newline at end of file +Updated implementation plan for AsciiDoc Linter with current test results \ No newline at end of file diff --git a/docs/test-results/report.html b/docs/test-results/report.html new file mode 100644 index 0000000..ead7e94 --- /dev/null +++ b/docs/test-results/report.html @@ -0,0 +1,1091 @@ + + + + + report.html + + + + +

report.html

+

Report generated on 20-Dec-2024 at 11:24:19 by pytest-html + v4.1.1

+
+

Environment

+
+
+ + + + + +
+
+

Summary

+
+
+

60 tests took 60 ms.

+

(Un)check the boxes to filter the results.

+
+ +
+
+
+
+ + 0 Failed, + + 60 Passed, + + 0 Skipped, + + 0 Expected failures, + + 0 Unexpected passes, + + 0 Errors, + + 0 Reruns +
+
+  /  +
+
+
+
+
+
+
+
+ + + + + + + + + +
ResultTestDurationLinks
+ + + \ No newline at end of file diff --git a/pytest.ini b/pytest.ini index d40604b..4c16559 100755 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,11 @@ +# pytest.ini - Pytest configuration file [pytest] -addopts = --cov=asciidoc_linter --cov-report=html --cov-report=term-missing -v -testpaths = tests \ No newline at end of file +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = + --verbose + --cov=asciidoc_linter + --cov-config=.coveragerc + --cov-report=term-missing \ No newline at end of file diff --git a/run_tests_html.py b/run_tests_html.py index d69744a..9ac60f3 100755 --- a/run_tests_html.py +++ b/run_tests_html.py @@ -49,13 +49,17 @@ def copy_reports(temp_test_report: Path, docs_dir: Path): timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') test_report = reports_dir / f'test_report_{timestamp}.html' - # Construct pytest command with only HTML report arguments + # Construct pytest command with HTML report and coverage arguments cmd = [ sys.executable, '-m', 'pytest', f'--html={test_report}', - '--self-contained-html' + '--self-contained-html', + '--cov=asciidoc_linter', + '--cov-report=html', + '--cov-report=term', + '--cov-config=.coveragerc' ] print(f"Executing command: {' '.join(cmd)}") @@ -67,7 +71,7 @@ def copy_reports(temp_test_report: Path, docs_dir: Path): # If tests were successful, copy reports to docs if result.returncode == 0: try: - docs_dir = Path('build/microsite/output/test-results') + docs_dir = Path('docs/test-results') # Changed path to match project structure final_test, final_cov = copy_reports(test_report, docs_dir) print(f"\nReports copied to docs:") print(f"Test report: {final_test}") diff --git a/run_tests_html.py.meta b/run_tests_html.py.meta index 4ea7a74..431fdb8 100755 --- a/run_tests_html.py.meta +++ b/run_tests_html.py.meta @@ -1 +1 @@ -Script to run all tests and generate HTML reports \ No newline at end of file +Updated test runner script with coverage support \ No newline at end of file diff --git a/tests/test_base.py b/tests/test_base.py new file mode 100644 index 0000000..e84261b --- /dev/null +++ b/tests/test_base.py @@ -0,0 +1,259 @@ +# test_base.py - Tests for base functionality +"""Tests for the base functionality of the rule system""" + +import unittest +from typing import Dict, Any, List +from asciidoc_linter.rules.base import ( + Severity, + Position, + Finding, + Rule, + RuleRegistry +) + +class TestSeverity(unittest.TestCase): + """Test the Severity enum""" + + def test_severity_values(self): + """Test that Severity enum has correct values""" + self.assertEqual(Severity.ERROR.value, "error") + self.assertEqual(Severity.WARNING.value, "warning") + self.assertEqual(Severity.INFO.value, "info") + + def test_severity_comparison(self): + """Test that Severity values can be compared""" + self.assertNotEqual(Severity.INFO, Severity.ERROR) + self.assertEqual(Severity.WARNING, Severity.WARNING) + self.assertTrue(isinstance(Severity.ERROR, Severity)) + + def test_severity_string(self): + """Test string representation of Severity""" + self.assertEqual(str(Severity.ERROR), "error") + self.assertEqual(str(Severity.WARNING), "warning") + self.assertEqual(str(Severity.INFO), "info") + +class TestPosition(unittest.TestCase): + """Test the Position dataclass""" + + def test_position_with_line_only(self): + """Test Position with line number only""" + pos = Position(line=10) + self.assertEqual(pos.line, 10) + self.assertIsNone(pos.column) + self.assertEqual(str(pos), "line 10") + + def test_position_with_line_and_column(self): + """Test Position with line and column""" + pos = Position(line=10, column=5) + self.assertEqual(pos.line, 10) + self.assertEqual(pos.column, 5) + self.assertEqual(str(pos), "line 10, column 5") + + def test_position_equality(self): + """Test Position equality comparison""" + pos1 = Position(line=10, column=5) + pos2 = Position(line=10, column=5) + pos3 = Position(line=10, column=6) + + self.assertEqual(pos1, pos2) + self.assertNotEqual(pos1, pos3) + +class TestFinding(unittest.TestCase): + """Test the Finding dataclass""" + + def setUp(self): + """Set up test data""" + self.position = Position(line=10, column=5) + self.context: Dict[str, Any] = {"key": "value"} + + def test_finding_minimal(self): + """Test Finding creation with minimal attributes""" + finding = Finding( + message="Test message", + severity=Severity.WARNING, + position=self.position + ) + + self.assertEqual(finding.message, "Test message") + self.assertEqual(finding.severity, Severity.WARNING) + self.assertEqual(finding.position, self.position) + self.assertIsNone(finding.rule_id) + self.assertIsNone(finding.context) + self.assertEqual(finding.line_number, 10) + + def test_finding_complete(self): + """Test Finding creation with all attributes""" + finding = Finding( + message="Test message", + severity=Severity.ERROR, + position=self.position, + rule_id="TEST001", + context=self.context + ) + + self.assertEqual(finding.message, "Test message") + self.assertEqual(finding.severity, Severity.ERROR) + self.assertEqual(finding.position, self.position) + self.assertEqual(finding.rule_id, "TEST001") + self.assertEqual(finding.context, self.context) + self.assertEqual(finding.line_number, 10) + + def test_finding_equality(self): + """Test Finding equality comparison""" + finding1 = Finding( + message="Test message", + severity=Severity.WARNING, + position=self.position, + rule_id="TEST001" + ) + finding2 = Finding( + message="Test message", + severity=Severity.WARNING, + position=self.position, + rule_id="TEST001" + ) + finding3 = Finding( + message="Different message", + severity=Severity.WARNING, + position=self.position, + rule_id="TEST001" + ) + + self.assertEqual(finding1, finding2) + self.assertNotEqual(finding1, finding3) + +class TestRule(unittest.TestCase): + """Test the Rule base class""" + + class ConcreteRule(Rule): + """Concrete implementation of Rule for testing""" + id = "TEST001" + name = "Test Rule" + description = "Rule for testing" + severity = Severity.WARNING + + def check(self, content: str) -> List[Finding]: + """Test implementation that finds 'ERROR' in content""" + findings = [] + lines = content.split('\n') + for i, line in enumerate(lines): + if "ERROR" in line: + findings.append(self.create_finding( + line_number=i+1, + message=f"Found ERROR in line {i+1}" + )) + return findings + + def check_line_content(self, line: str, line_number: int) -> List[Finding]: + """Test implementation for line content checking""" + if "WARNING" in line: + return [self.create_finding(line_number, "Found WARNING")] + return [] + + def check_line_context(self, line: str, line_number: int, + prev_line: str, next_line: str) -> List[Finding]: + """Test implementation for line context checking""" + if prev_line and line == prev_line: + return [self.create_finding(line_number, "Repeated line")] + return [] + + def setUp(self): + """Set up test data""" + self.rule = self.ConcreteRule() + + def test_rule_attributes(self): + """Test rule attributes""" + self.assertEqual(self.rule.id, "TEST001") + self.assertEqual(self.rule.name, "Test Rule") + self.assertEqual(self.rule.description, "Rule for testing") + self.assertEqual(self.rule.severity, Severity.WARNING) + + def test_check_method(self): + """Test check method""" + content = "This line has an ERROR\nThis line is fine\nAnother ERROR here" + findings = self.rule.check(content) + + self.assertEqual(len(findings), 2) + self.assertEqual(findings[0].message, "Found ERROR in line 1") + self.assertEqual(findings[0].severity, Severity.WARNING) + self.assertEqual(findings[0].position.line, 1) + self.assertEqual(findings[1].message, "Found ERROR in line 3") + + def test_check_line(self): + """Test check_line method""" + content = [ + "First line", + "Line with WARNING", + "Line with WARNING", # Repeated line + "Last line" + ] + + # Test line with warning + findings = self.rule.check_line(content[1], 1, content) + self.assertEqual(len(findings), 1) + self.assertEqual(findings[0].message, "Found WARNING") + + # Test repeated line + findings = self.rule.check_line(content[2], 2, content) + self.assertEqual(len(findings), 2) # WARNING + repeated line + self.assertTrue(any(f.message == "Repeated line" for f in findings)) + + def test_base_rule_methods(self): + """Test that base Rule class methods raise NotImplementedError""" + base_rule = Rule() + with self.assertRaises(NotImplementedError): + base_rule.check("some content") + +class TestRuleRegistry(unittest.TestCase): + """Test the RuleRegistry""" + + class TestRule1(Rule): + """First test rule""" + id = "TEST001" + + class TestRule2(Rule): + """Second test rule""" + id = "TEST002" + + def setUp(self): + """Set up test data""" + # Clear registry before each test + RuleRegistry._rules = {} + + def test_register_rule(self): + """Test rule registration""" + RuleRegistry.register_rule(self.TestRule1) + self.assertIn(self.TestRule1.__name__, RuleRegistry._rules) + self.assertEqual(RuleRegistry._rules[self.TestRule1.__name__], self.TestRule1) + + def test_get_rule(self): + """Test getting a rule by name""" + RuleRegistry.register_rule(self.TestRule1) + rule_class = RuleRegistry.get_rule(self.TestRule1.__name__) + self.assertEqual(rule_class, self.TestRule1) + + def test_get_all_rules(self): + """Test getting all registered rules""" + RuleRegistry.register_rule(self.TestRule1) + RuleRegistry.register_rule(self.TestRule2) + rules = RuleRegistry.get_all_rules() + self.assertEqual(len(rules), 2) + self.assertIn(self.TestRule1, rules) + self.assertIn(self.TestRule2, rules) + + def test_create_all_rules(self): + """Test creating instances of all rules""" + RuleRegistry.register_rule(self.TestRule1) + RuleRegistry.register_rule(self.TestRule2) + instances = RuleRegistry.create_all_rules() + self.assertEqual(len(instances), 2) + self.assertTrue(any(isinstance(rule, self.TestRule1) for rule in instances)) + self.assertTrue(any(isinstance(rule, self.TestRule2) for rule in instances)) + + def test_get_nonexistent_rule(self): + """Test getting a rule that doesn't exist""" + with self.assertRaises(KeyError): + RuleRegistry.get_rule("NonexistentRule") + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_base.py.meta b/tests/test_base.py.meta new file mode 100644 index 0000000..fdaf98c --- /dev/null +++ b/tests/test_base.py.meta @@ -0,0 +1 @@ +Tests for base functionality \ No newline at end of file diff --git a/tests/test_base_rules.py b/tests/test_base_rules.py new file mode 100644 index 0000000..cf5411e --- /dev/null +++ b/tests/test_base_rules.py @@ -0,0 +1,165 @@ +# test_base_rules.py - Tests for base rule classes +"""Tests for the base rule classes""" + +import unittest +from typing import Dict, Any +from asciidoc_linter.rules.base_rules import ( + Severity, + Position, + Finding, + Rule +) + +class TestSeverity(unittest.TestCase): + """Test the Severity enum""" + + def test_severity_values(self): + """Test that Severity enum has correct values""" + self.assertEqual(Severity.ERROR.value, "error") + self.assertEqual(Severity.WARNING.value, "warning") + self.assertEqual(Severity.INFO.value, "info") + + def test_severity_comparison(self): + """Test that Severity values can be compared""" + self.assertNotEqual(Severity.INFO, Severity.ERROR) + self.assertEqual(Severity.WARNING, Severity.WARNING) + self.assertTrue(isinstance(Severity.ERROR, Severity)) + +class TestPosition(unittest.TestCase): + """Test the Position dataclass""" + + def test_position_with_line_only(self): + """Test Position with line number only""" + pos = Position(line=10) + self.assertEqual(pos.line, 10) + self.assertIsNone(pos.column) + + def test_position_with_line_and_column(self): + """Test Position with line and column""" + pos = Position(line=10, column=5) + self.assertEqual(pos.line, 10) + self.assertEqual(pos.column, 5) + + def test_position_equality(self): + """Test Position equality comparison""" + pos1 = Position(line=10, column=5) + pos2 = Position(line=10, column=5) + pos3 = Position(line=10, column=6) + + self.assertEqual(pos1, pos2) + self.assertNotEqual(pos1, pos3) + +class TestFinding(unittest.TestCase): + """Test the Finding dataclass""" + + def setUp(self): + """Set up test data""" + self.position = Position(line=10, column=5) + self.context: Dict[str, Any] = {"key": "value"} + + def test_finding_minimal(self): + """Test Finding creation with minimal attributes""" + finding = Finding( + message="Test message", + severity=Severity.WARNING, + position=self.position + ) + + self.assertEqual(finding.message, "Test message") + self.assertEqual(finding.severity, Severity.WARNING) + self.assertEqual(finding.position, self.position) + self.assertIsNone(finding.rule_id) + self.assertIsNone(finding.context) + + def test_finding_complete(self): + """Test Finding creation with all attributes""" + finding = Finding( + message="Test message", + severity=Severity.ERROR, + position=self.position, + rule_id="TEST001", + context=self.context + ) + + self.assertEqual(finding.message, "Test message") + self.assertEqual(finding.severity, Severity.ERROR) + self.assertEqual(finding.position, self.position) + self.assertEqual(finding.rule_id, "TEST001") + self.assertEqual(finding.context, self.context) + + def test_finding_equality(self): + """Test Finding equality comparison""" + finding1 = Finding( + message="Test message", + severity=Severity.WARNING, + position=self.position, + rule_id="TEST001" + ) + finding2 = Finding( + message="Test message", + severity=Severity.WARNING, + position=self.position, + rule_id="TEST001" + ) + finding3 = Finding( + message="Different message", + severity=Severity.WARNING, + position=self.position, + rule_id="TEST001" + ) + + self.assertEqual(finding1, finding2) + self.assertNotEqual(finding1, finding3) + +class TestRule(unittest.TestCase): + """Test the Rule base class""" + + class ConcreteRule(Rule): + """Concrete implementation of Rule for testing""" + rule_id = "TEST001" + + def check(self, content: str): + """Test implementation that finds 'ERROR' in content""" + findings = [] + lines = content.split('\n') + for i, line in enumerate(lines): + if "ERROR" in line: + findings.append(Finding( + message=f"Found ERROR in line {i+1}", + severity=Severity.ERROR, + position=Position(line=i+1), + rule_id=self.rule_id + )) + return findings + + def setUp(self): + """Set up test data""" + self.rule = self.ConcreteRule() + + def test_rule_id(self): + """Test rule_id attribute""" + self.assertEqual(self.rule.rule_id, "TEST001") + + # Test default rule_id + base_rule = Rule() + self.assertEqual(base_rule.rule_id, "BASE") + + def test_check_method_concrete(self): + """Test check method in concrete implementation""" + content = "This line has an ERROR\nThis line is fine\nAnother ERROR here" + findings = self.rule.check(content) + + self.assertEqual(len(findings), 2) + self.assertEqual(findings[0].message, "Found ERROR in line 1") + self.assertEqual(findings[0].severity, Severity.ERROR) + self.assertEqual(findings[0].position.line, 1) + self.assertEqual(findings[1].message, "Found ERROR in line 3") + + def test_check_method_base(self): + """Test check method in base class raises NotImplementedError""" + base_rule = Rule() + with self.assertRaises(NotImplementedError): + base_rule.check("some content") + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_base_rules.py.meta b/tests/test_base_rules.py.meta new file mode 100644 index 0000000..1d2c893 --- /dev/null +++ b/tests/test_base_rules.py.meta @@ -0,0 +1 @@ +Tests for base rule classes \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..cdb17bd --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,157 @@ +# test_cli.py - Tests for command line interface +"""Tests for the command line interface""" + +import unittest +from unittest.mock import patch, mock_open, MagicMock +from pathlib import Path +from asciidoc_linter.cli import main, create_parser +from asciidoc_linter.reporter import ConsoleReporter, JsonReporter, HtmlReporter + +class TestCliArgumentParsing(unittest.TestCase): + """Test argument parsing functionality""" + + def test_create_parser(self): + """Test parser creation and default values""" + parser = create_parser() + args = parser.parse_args(['test.adoc']) + + self.assertEqual(args.files, ['test.adoc']) + self.assertEqual(args.format, 'console') + self.assertIsNone(args.config) + + def test_multiple_files(self): + """Test parsing multiple file arguments""" + parser = create_parser() + args = parser.parse_args(['file1.adoc', 'file2.adoc']) + + self.assertEqual(args.files, ['file1.adoc', 'file2.adoc']) + + def test_format_option(self): + """Test different format options""" + parser = create_parser() + + # Test console format + args = parser.parse_args(['test.adoc', '--format', 'console']) + self.assertEqual(args.format, 'console') + + # Test JSON format + args = parser.parse_args(['test.adoc', '--format', 'json']) + self.assertEqual(args.format, 'json') + + # Test HTML format + args = parser.parse_args(['test.adoc', '--format', 'html']) + self.assertEqual(args.format, 'html') + + def test_config_option(self): + """Test config file option""" + parser = create_parser() + args = parser.parse_args(['test.adoc', '--config', 'config.yml']) + + self.assertEqual(args.config, 'config.yml') + + def test_invalid_format(self): + """Test invalid format option""" + parser = create_parser() + with self.assertRaises(SystemExit): + parser.parse_args(['test.adoc', '--format', 'invalid']) + +class TestCliFileProcessing(unittest.TestCase): + """Test file processing functionality""" + + @patch('pathlib.Path.exists') + @patch('pathlib.Path.read_text') + def test_file_not_found(self, mock_read_text, mock_exists): + """Test handling of non-existent files""" + mock_exists.return_value = False + + with patch('sys.stderr') as mock_stderr: + exit_code = main(['nonexistent.adoc']) + + self.assertEqual(exit_code, 1) + mock_read_text.assert_not_called() + + @patch('pathlib.Path.exists') + @patch('pathlib.Path.read_text') + def test_file_read_error(self, mock_read_text, mock_exists): + """Test handling of unreadable files""" + mock_exists.return_value = True + mock_read_text.side_effect = PermissionError("Permission denied") + + with patch('sys.stderr') as mock_stderr: + exit_code = main(['unreadable.adoc']) + + self.assertEqual(exit_code, 1) + + @patch('pathlib.Path.exists') + @patch('pathlib.Path.read_text') + @patch('asciidoc_linter.linter.AsciiDocLinter.lint') + def test_successful_lint(self, mock_lint, mock_read_text, mock_exists): + """Test successful file linting""" + mock_exists.return_value = True + mock_read_text.return_value = "= Test Document" + mock_lint.return_value = [] # No lint errors + + exit_code = main(['valid.adoc']) + + self.assertEqual(exit_code, 0) + mock_lint.assert_called_once() + + @patch('pathlib.Path.exists') + @patch('pathlib.Path.read_text') + @patch('asciidoc_linter.linter.AsciiDocLinter.lint') + def test_lint_with_errors(self, mock_lint, mock_read_text, mock_exists): + """Test file linting with errors""" + mock_exists.return_value = True + mock_read_text.return_value = "= Test Document" + mock_lint.return_value = ["Error: Invalid heading"] # Simulate lint error + + exit_code = main(['invalid.adoc']) + + self.assertEqual(exit_code, 1) + mock_lint.assert_called_once() + +class TestCliReporters(unittest.TestCase): + """Test reporter selection and usage""" + + @patch('pathlib.Path.exists') + @patch('pathlib.Path.read_text') + @patch('asciidoc_linter.linter.AsciiDocLinter.set_reporter') + def test_json_reporter(self, mock_set_reporter, mock_read_text, mock_exists): + """Test JSON reporter selection""" + mock_exists.return_value = True + + main(['test.adoc', '--format', 'json']) + + # Verify that JsonReporter was set + mock_set_reporter.assert_called_once() + reporter = mock_set_reporter.call_args[0][0] + self.assertIsInstance(reporter, JsonReporter) + + @patch('pathlib.Path.exists') + @patch('pathlib.Path.read_text') + @patch('asciidoc_linter.linter.AsciiDocLinter.set_reporter') + def test_html_reporter(self, mock_set_reporter, mock_read_text, mock_exists): + """Test HTML reporter selection""" + mock_exists.return_value = True + + main(['test.adoc', '--format', 'html']) + + # Verify that HtmlReporter was set + mock_set_reporter.assert_called_once() + reporter = mock_set_reporter.call_args[0][0] + self.assertIsInstance(reporter, HtmlReporter) + + @patch('pathlib.Path.exists') + @patch('pathlib.Path.read_text') + @patch('asciidoc_linter.linter.AsciiDocLinter.set_reporter') + def test_default_console_reporter(self, mock_set_reporter, mock_read_text, mock_exists): + """Test default console reporter""" + mock_exists.return_value = True + + main(['test.adoc']) # No format specified + + # Verify that no reporter was set (uses default ConsoleReporter) + mock_set_reporter.assert_not_called() + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_cli.py.meta b/tests/test_cli.py.meta new file mode 100644 index 0000000..0cefa52 --- /dev/null +++ b/tests/test_cli.py.meta @@ -0,0 +1 @@ +Test file for the command line interface \ No newline at end of file diff --git a/tests/test_rule_registry.py b/tests/test_rule_registry.py new file mode 100644 index 0000000..eedb4d3 --- /dev/null +++ b/tests/test_rule_registry.py @@ -0,0 +1,48 @@ +# test_rule_registry.py - Tests for the rule registry +"""Tests for the rule registry functionality""" + +import pytest +from asciidoc_linter.rules.base import Rule, RuleRegistry, Severity + +class TestRuleOne(Rule): + """First test rule""" + id = "TestRuleOne" + name = "Test Rule One" + description = "First test rule" + severity = Severity.WARNING + +class TestRuleTwo(Rule): + """Second test rule""" + id = "TestRuleTwo" + name = "Test Rule Two" + description = "Second test rule" + severity = Severity.ERROR + +def test_rule_registry(): + """Test the rule registry functionality""" + # Clear registry + RuleRegistry._rules.clear() + + # Test registration + RuleRegistry.register_rule(TestRuleOne) + RuleRegistry.register_rule(TestRuleTwo) + + # Test getting a specific rule + rule_class = RuleRegistry.get_rule("TestRuleOne") + assert rule_class == TestRuleOne + + # Test getting all rules + all_rules = RuleRegistry.get_all_rules() + assert len(all_rules) == 2 + assert TestRuleOne in all_rules + assert TestRuleTwo in all_rules + + # Test creating all rules + rule_instances = RuleRegistry.create_all_rules() + assert len(rule_instances) == 2 + assert isinstance(rule_instances[0], Rule) + assert isinstance(rule_instances[1], Rule) + + # Test getting non-existent rule + with pytest.raises(KeyError): + RuleRegistry.get_rule("NonExistentRule") \ No newline at end of file diff --git a/tests/test_rule_registry.py.meta b/tests/test_rule_registry.py.meta new file mode 100644 index 0000000..f38d50c --- /dev/null +++ b/tests/test_rule_registry.py.meta @@ -0,0 +1 @@ +Tests for the rule registry \ No newline at end of file diff --git a/tests/test_rules.py b/tests/test_rules.py new file mode 100644 index 0000000..700f886 --- /dev/null +++ b/tests/test_rules.py @@ -0,0 +1,178 @@ +# test_rules.py - Tests for rules module +"""Tests for the rules module and base functionality""" + +import unittest +from asciidoc_linter.rules import ( + Severity, + Position, + Finding, + Rule, + RuleRegistry +) + +class TestSeverity(unittest.TestCase): + """Test the Severity enum""" + + def test_severity_values(self): + """Test that Severity enum has correct values""" + self.assertEqual(Severity.INFO.value, "INFO") + self.assertEqual(Severity.WARNING.value, "WARNING") + self.assertEqual(Severity.ERROR.value, "ERROR") + + def test_severity_comparison(self): + """Test that Severity values can be compared""" + self.assertNotEqual(Severity.INFO, Severity.ERROR) + self.assertEqual(Severity.WARNING, Severity.WARNING) + +class TestPosition(unittest.TestCase): + """Test the Position class""" + + def test_position_with_line_and_column(self): + """Test Position with both line and column""" + pos = Position(10, 5) + self.assertEqual(pos.line, 10) + self.assertEqual(pos.column, 5) + self.assertEqual(str(pos), "line 10, column 5") + + def test_position_with_line_only(self): + """Test Position with line only""" + pos = Position(10) + self.assertEqual(pos.line, 10) + self.assertIsNone(pos.column) + self.assertEqual(str(pos), "line 10") + +class TestFinding(unittest.TestCase): + """Test the Finding class""" + + def setUp(self): + """Set up test data""" + self.position = Position(10, 5) + self.finding = Finding( + rule_id="TEST001", + position=self.position, + message="Test message", + severity=Severity.WARNING, + context="Test context" + ) + + def test_finding_creation(self): + """Test Finding creation with all attributes""" + self.assertEqual(self.finding.rule_id, "TEST001") + self.assertEqual(self.finding.position, self.position) + self.assertEqual(self.finding.message, "Test message") + self.assertEqual(self.finding.severity, Severity.WARNING) + self.assertEqual(self.finding.context, "Test context") + + def test_line_number_property(self): + """Test line_number property""" + self.assertEqual(self.finding.line_number, 10) + +class TestRule(unittest.TestCase): + """Test the Rule base class""" + + class TestRule(Rule): + """Test implementation of Rule""" + id = "TEST001" + name = "Test Rule" + description = "Rule for testing" + severity = Severity.WARNING + + def check_line_content(self, line: str, line_number: int): + """Test implementation that finds 'ERROR' in lines""" + if "ERROR" in line: + return [self.create_finding(line_number, "Found ERROR")] + return [] + + def check_line_context(self, line, line_number, prev_line, next_line): + """Test implementation that checks for repeated lines""" + if prev_line and line == prev_line: + return [self.create_finding(line_number, "Repeated line")] + return [] + + def setUp(self): + """Set up test data""" + self.rule = self.TestRule() + + def test_rule_attributes(self): + """Test basic rule attributes""" + self.assertEqual(self.rule.id, "TEST001") + self.assertEqual(self.rule.name, "Test Rule") + self.assertEqual(self.rule.description, "Rule for testing") + self.assertEqual(self.rule.severity, Severity.WARNING) + + def test_check_line(self): + """Test check_line with various scenarios""" + # Test single line with error + findings = self.rule.check_line("This has an ERROR", 0, ["This has an ERROR"]) + self.assertEqual(len(findings), 1) + self.assertEqual(findings[0].message, "Found ERROR") + + # Test repeated lines + context = ["Same line", "Same line"] + findings = self.rule.check_line("Same line", 1, context) + self.assertEqual(len(findings), 1) + self.assertEqual(findings[0].message, "Repeated line") + + def test_create_finding(self): + """Test create_finding helper method""" + finding = self.rule.create_finding(10, "Test message", 5, "Test context") + self.assertEqual(finding.rule_id, "TEST001") + self.assertEqual(finding.position.line, 10) + self.assertEqual(finding.position.column, 5) + self.assertEqual(finding.message, "Test message") + self.assertEqual(finding.severity, Severity.WARNING) + self.assertEqual(finding.context, "Test context") + +class TestRuleRegistry(unittest.TestCase): + """Test the RuleRegistry""" + + class TestRule1(Rule): + """First test rule""" + id = "TEST001" + + class TestRule2(Rule): + """Second test rule""" + id = "TEST002" + + def setUp(self): + """Set up test data""" + # Clear registry before each test + RuleRegistry._rules = {} + + def test_register_rule(self): + """Test rule registration""" + RuleRegistry.register_rule(self.TestRule1) + self.assertIn(self.TestRule1.__name__, RuleRegistry._rules) + self.assertEqual(RuleRegistry._rules[self.TestRule1.__name__], self.TestRule1) + + def test_get_rule(self): + """Test getting a rule by name""" + RuleRegistry.register_rule(self.TestRule1) + rule_class = RuleRegistry.get_rule(self.TestRule1.__name__) + self.assertEqual(rule_class, self.TestRule1) + + def test_get_all_rules(self): + """Test getting all registered rules""" + RuleRegistry.register_rule(self.TestRule1) + RuleRegistry.register_rule(self.TestRule2) + rules = RuleRegistry.get_all_rules() + self.assertEqual(len(rules), 2) + self.assertIn(self.TestRule1, rules) + self.assertIn(self.TestRule2, rules) + + def test_create_all_rules(self): + """Test creating instances of all rules""" + RuleRegistry.register_rule(self.TestRule1) + RuleRegistry.register_rule(self.TestRule2) + instances = RuleRegistry.create_all_rules() + self.assertEqual(len(instances), 2) + self.assertTrue(any(isinstance(rule, self.TestRule1) for rule in instances)) + self.assertTrue(any(isinstance(rule, self.TestRule2) for rule in instances)) + + def test_get_nonexistent_rule(self): + """Test getting a rule that doesn't exist""" + with self.assertRaises(KeyError): + RuleRegistry.get_rule("NonexistentRule") + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_rules.py.meta b/tests/test_rules.py.meta new file mode 100644 index 0000000..bd6d922 --- /dev/null +++ b/tests/test_rules.py.meta @@ -0,0 +1 @@ +Tests for the rules module and base functionality \ No newline at end of file diff --git a/tests/test_table_rules.py b/tests/test_table_rules.py new file mode 100644 index 0000000..29cb2b8 --- /dev/null +++ b/tests/test_table_rules.py @@ -0,0 +1,218 @@ +# test_table_rules.py - Tests for table rules +"""Tests for the table rules module""" + +import unittest +from typing import List, Tuple +from asciidoc_linter.rules.table_rules import ( + TableFormatRule, + TableStructureRule, + TableContentRule +) +from asciidoc_linter.rules.base import Finding, Severity + +class TestTableFormatRule(unittest.TestCase): + """Test the TableFormatRule class""" + + def setUp(self): + """Set up test data""" + self.rule = TableFormatRule() + + def test_extract_table_lines(self): + """Test table extraction from document""" + content = [ + "Some text before", + "|===", + "| Header 1 | Header 2", + "", + "| Cell 1 | Cell 2", + "|===", + "Some text between", + "|===", + "| Another table", + "|===" + ] + + tables = self.rule.extract_table_lines([(i, line) for i, line in enumerate(content)]) + + self.assertEqual(len(tables), 2) + self.assertEqual(len(tables[0]), 5) # First table with 5 lines + self.assertEqual(len(tables[1]), 3) # Second table with 3 lines + + def test_check_column_alignment(self): + """Test column alignment checking""" + # Well-aligned table + aligned_table = [ + (0, "|==="), + (1, "| Col 1 | Col 2 |"), + (2, "| Data 1 | Data 2 |"), + (3, "|===") + ] + findings = self.rule.check_column_alignment(aligned_table) + self.assertEqual(len(findings), 0) + + # Misaligned table + misaligned_table = [ + (0, "|==="), + (1, "| Col 1 | Col 2 |"), + (2, "| Data 1| Data 2|"), # Misaligned + (3, "|===") + ] + findings = self.rule.check_column_alignment(misaligned_table) + self.assertEqual(len(findings), 1) + self.assertIn("Column alignment", findings[0].message) + + def test_check_header_separator(self): + """Test header separator checking""" + # Table with proper header separation + good_table = [ + (0, "|==="), + (1, "| Header 1 | Header 2"), + (2, ""), + (3, "| Cell 1 | Cell 2"), + (4, "|===") + ] + findings = self.rule.check_header_separator(good_table) + self.assertEqual(len(findings), 0) + + # Table without header separation + bad_table = [ + (0, "|==="), + (1, "| Header 1 | Header 2"), + (2, "| Cell 1 | Cell 2"), # Missing empty line + (3, "|===") + ] + findings = self.rule.check_header_separator(bad_table) + self.assertEqual(len(findings), 1) + self.assertIn("Header row", findings[0].message) + + def test_check_complete(self): + """Test complete table checking""" + document = "\n".join([ + "Some text", + "|===", + "| Header 1 | Header 2", + "| Cell 1 | Cell 2 ", + "|===" + ]) + + findings = self.rule.check(document) + self.assertEqual(len(findings), 1) # Should find missing header separator + +class TestTableStructureRule(unittest.TestCase): + """Test the TableStructureRule class""" + + def setUp(self): + """Set up test data""" + self.rule = TableStructureRule() + + def test_count_columns(self): + """Test column counting""" + self.assertEqual(self.rule.count_columns("| Col 1 | Col 2 | Col 3"), 3) + self.assertEqual(self.rule.count_columns("|==="), 0) + self.assertEqual(self.rule.count_columns("| Single column"), 1) + + def test_check_table_structure_consistent(self): + """Test table with consistent structure""" + table = [ + (0, "|==="), + (1, "| Col 1 | Col 2"), + (2, "| Cell 1 | Cell 2"), + (3, "|===") + ] + + findings = self.rule.check_table_structure(table) + self.assertEqual(len(findings), 0) + + def test_check_table_structure_inconsistent(self): + """Test table with inconsistent structure""" + table = [ + (0, "|==="), + (1, "| Col 1 | Col 2"), + (2, "| Cell 1 | Cell 2 | Cell 3"), # Extra column + (3, "|===") + ] + + findings = self.rule.check_table_structure(table) + self.assertEqual(len(findings), 1) + self.assertIn("Inconsistent column count", findings[0].message) + + def test_empty_table(self): + """Test empty table detection""" + table = [ + (0, "|==="), + (1, ""), + (2, "|===") + ] + + findings = self.rule.check_table_structure(table) + self.assertEqual(len(findings), 1) + self.assertIn("Empty table", findings[0].message) + + def test_check_complete(self): + """Test complete table checking""" + document = "\n".join([ + "|===", + "| Col 1 | Col 2", + "| Cell 1 | Cell 2 | Cell 3", # Inconsistent + "|===" + ]) + + findings = self.rule.check(document) + self.assertEqual(len(findings), 1) + self.assertIn("Inconsistent column count", findings[0].message) + +class TestTableContentRule(unittest.TestCase): + """Test the TableContentRule class""" + + def setUp(self): + """Set up test data""" + self.rule = TableContentRule() + + def test_extract_cells(self): + """Test cell extraction""" + # Simple cells + cells = self.rule.extract_cells("| Cell 1 | Cell 2") + self.assertEqual(len(cells), 2) + self.assertEqual(cells[0], ("", "Cell 1")) + self.assertEqual(cells[1], ("", "Cell 2")) + + # Cells with prefixes + cells = self.rule.extract_cells("| Simple | a| Complex | l| List") + self.assertEqual(len(cells), 3) + self.assertEqual(cells[1], ("a", "Complex")) + self.assertEqual(cells[2], ("l", "List")) + + def test_check_cell_content_simple(self): + """Test checking simple cell content""" + finding = self.rule.check_cell_content("", "Simple content", 1, "| Simple content") + self.assertIsNone(finding) + + def test_check_cell_content_list_without_prefix(self): + """Test checking cell with list but no prefix""" + finding = self.rule.check_cell_content("", "* List item", 1, "| * List item") + self.assertIsNotNone(finding) + self.assertIn("List in table cell", finding.message) + + def test_check_cell_content_list_with_prefix(self): + """Test checking cell with list and correct prefix""" + finding = self.rule.check_cell_content("a", "* List item", 1, "a| * List item") + self.assertIsNone(finding) + + finding = self.rule.check_cell_content("l", "* List item", 1, "l| * List item") + self.assertIsNone(finding) + + def test_check_complete(self): + """Test complete content checking""" + document = "\n".join([ + "|===", + "| Normal | * List without prefix", + "| Normal | a| * List with prefix", + "|===" + ]) + + findings = self.rule.check(document) + self.assertEqual(len(findings), 1) # Should find one list without prefix + self.assertIn("List in table cell", findings[0].message) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_table_rules.py.meta b/tests/test_table_rules.py.meta new file mode 100644 index 0000000..0e561a2 --- /dev/null +++ b/tests/test_table_rules.py.meta @@ -0,0 +1 @@ +Tests for table rules \ No newline at end of file