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
+
+
+
+
+
+ |
+ |
+
+
+
+
+
+ No results found. Check the filters.
+ |
+
+
+
+
+
+
+
+
+
+
+
+
Summary
+
+
+
60 tests took 60 ms.
+
(Un)check the boxes to filter the results.
+
+
+
+
+
+
+
+
+
+
+
+
+ Result |
+ Test |
+ Duration |
+ Links |
+
+
+
+
+
+
\ 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