development-hub/tests/test_parser_edge_cases.py

527 lines
13 KiB
Python

"""Edge case tests for parsers."""
import pytest
from pathlib import Path
from development_hub.parsers.todos_parser import TodosParser
from development_hub.parsers.goals_parser import GoalsParser, MilestonesParser, GoalsSaver
from development_hub.models.todo import Todo, TodoList
from development_hub.models.goal import Goal, GoalList, Milestone, MilestoneStatus
class TestTodosParserEdgeCases:
"""Edge cases for TodosParser."""
def test_empty_file(self, tmp_path):
"""Handles empty file."""
todos_file = tmp_path / "todos.md"
todos_file.write_text("")
parser = TodosParser(todos_file)
result = parser.parse()
assert isinstance(result, TodoList)
assert len(result.all_todos) == 0
def test_frontmatter_only(self, tmp_path):
"""Handles file with only frontmatter."""
todos_file = tmp_path / "todos.md"
todos_file.write_text("""---
type: todos
project: test
---
""")
parser = TodosParser(todos_file)
result = parser.parse()
assert len(result.all_todos) == 0
assert parser.frontmatter is not None
def test_malformed_checkbox(self, tmp_path):
"""Handles malformed checkbox lines."""
todos_file = tmp_path / "todos.md"
todos_file.write_text("""---
type: todos
---
# TODOs
## Active Tasks
### High Priority
- [ ] Valid task #high
- [ ] Extra space #high
- [x] Completed #high
- [-] Invalid checkbox #high
- [ ] Another valid #high
""")
parser = TodosParser(todos_file)
result = parser.parse()
# Should parse valid ones, skip malformed
valid_tasks = [t for t in result.all_todos if "Valid" in t.text or "Another" in t.text or "Completed" in t.text]
assert len(valid_tasks) >= 2
def test_todo_with_special_characters(self, tmp_path):
"""Handles special characters in todo text."""
todos_file = tmp_path / "todos.md"
todos_file.write_text("""---
type: todos
---
# TODOs
## Active Tasks
### High Priority
- [ ] Task with "quotes" and 'apostrophes' #high
- [ ] Task with <html> tags #high
- [ ] Task with unicode: cafe #high
- [ ] Task with emoji: test #high
""")
parser = TodosParser(todos_file)
result = parser.parse()
# Save and reload
parser.save(result)
parser2 = TodosParser(todos_file)
result2 = parser2.parse()
assert len(result.high_priority) == len(result2.high_priority)
def test_priority_extraction(self, tmp_path):
"""Correctly extracts priority tags."""
todos_file = tmp_path / "todos.md"
todos_file.write_text("""---
type: todos
---
# TODOs
## Active Tasks
### High Priority
- [ ] Task #high
- [ ] Task with milestone @M1 #high
### Medium Priority
- [ ] Medium task #medium
### Low Priority
- [ ] Low task #low
""")
parser = TodosParser(todos_file)
result = parser.parse()
assert len(result.high_priority) == 2
assert len(result.medium_priority) == 1
assert len(result.low_priority) == 1
def test_milestone_extraction(self, tmp_path):
"""Correctly extracts milestone tags."""
todos_file = tmp_path / "todos.md"
todos_file.write_text("""---
type: todos
---
# TODOs
## Active Tasks
### High Priority
- [ ] Task for M1 @M1 #high
- [ ] Task for M2 @M2 #high
- [ ] Task for M10 @M10 #high
- [ ] Task no milestone #high
""")
parser = TodosParser(todos_file)
result = parser.parse()
milestones = [t.milestone for t in result.high_priority]
assert "M1" in milestones
assert "M2" in milestones
assert "M10" in milestones
assert None in milestones
def test_completed_date_preservation(self, tmp_path):
"""Preserves completion dates."""
todos_file = tmp_path / "todos.md"
todos_file.write_text("""---
type: todos
---
# TODOs
## Active Tasks
### High Priority
## Completed
- [x] Done task #high (2026-01-15)
- [x] Another done #medium (2025-12-31)
""")
parser = TodosParser(todos_file)
result = parser.parse()
assert len(result.completed) == 2
# Dates should be preserved in text or parsed
# Save and verify dates preserved
parser.save(result)
content = todos_file.read_text()
assert "2026-01-15" in content
assert "2025-12-31" in content
class TestGoalsParserEdgeCases:
"""Edge cases for GoalsParser."""
def test_empty_sections(self, tmp_path):
"""Handles empty goal sections."""
goals_file = tmp_path / "goals.md"
goals_file.write_text("""---
type: goals
---
# Goals
## Active
## Future
## Non-Goals
""")
parser = GoalsParser(goals_file)
result = parser.parse()
assert len(result.active) == 0
assert len(result.future) == 0
assert len(result.non_goals) == 0
def test_missing_sections(self, tmp_path):
"""Handles missing sections."""
goals_file = tmp_path / "goals.md"
goals_file.write_text("""---
type: goals
---
# Goals
## Active
- [ ] Only active goal #high
""")
parser = GoalsParser(goals_file)
result = parser.parse()
assert len(result.active) == 1
assert len(result.future) == 0
assert len(result.non_goals) == 0
def test_three_state_checkbox(self, tmp_path):
"""Correctly parses three-state checkboxes."""
goals_file = tmp_path / "goals.md"
goals_file.write_text("""---
type: goals
---
# Goals
## Active
- [ ] Not achieved #high
- [~] Partially achieved #medium
- [x] Fully achieved #low
""")
parser = GoalsParser(goals_file)
result = parser.parse()
assert result.active[0].completed == False
assert result.active[0].partial == False
assert result.active[1].completed == False
assert result.active[1].partial == True
assert result.active[2].completed == True
assert result.active[2].partial == False
def test_goal_priority_extraction(self, tmp_path):
"""Correctly extracts goal priorities."""
goals_file = tmp_path / "goals.md"
goals_file.write_text("""---
type: goals
---
# Goals
## Active
- [ ] High priority goal #high
- [ ] Medium priority goal #medium
- [ ] Low priority goal #low
- [ ] Default priority goal
""")
parser = GoalsParser(goals_file)
result = parser.parse()
priorities = [g.priority for g in result.active]
assert "high" in priorities
assert "medium" in priorities
assert "low" in priorities
class TestMilestonesParserEdgeCases:
"""Edge cases for MilestonesParser."""
def test_milestone_without_deliverables(self, tmp_path):
"""Handles milestone with no deliverables table."""
milestones_file = tmp_path / "milestones.md"
milestones_file.write_text("""---
type: milestones
---
# Milestones
#### M1: Simple Milestone
**Target**: January 2026
**Status**: In Progress (50%)
Just a description, no deliverables table.
---
""")
parser = MilestonesParser(milestones_file)
result = parser.parse()
assert len(result) == 1
assert result[0].id == "M1"
assert len(result[0].deliverables) == 0
def test_progress_parsing(self, tmp_path):
"""Correctly parses progress percentages."""
milestones_file = tmp_path / "milestones.md"
milestones_file.write_text("""---
type: milestones
---
# Milestones
#### M1: Zero Progress
**Target**: Jan 2026
**Status**: Not Started
---
#### M2: Half Done
**Target**: Jan 2026
**Status**: In Progress (50%)
---
#### M3: Complete
**Target**: Jan 2026
**Status**: Completed (100%)
---
""")
parser = MilestonesParser(milestones_file)
result = parser.parse()
by_id = {m.id: m for m in result}
assert by_id["M1"].progress == 0
assert by_id["M2"].progress == 50
assert by_id["M3"].progress == 100
def test_status_parsing(self, tmp_path):
"""Correctly parses all milestone statuses."""
milestones_file = tmp_path / "milestones.md"
milestones_file.write_text("""---
type: milestones
---
# Milestones
#### M1: Not Started
**Target**: Jan 2026
**Status**: Not Started
---
#### M2: Planning
**Target**: Jan 2026
**Status**: Planning (10%)
---
#### M3: In Progress
**Target**: Jan 2026
**Status**: In Progress (60%)
---
#### M4: Complete
**Target**: Jan 2026
**Status**: Completed (100%)
---
""")
parser = MilestonesParser(milestones_file)
result = parser.parse()
by_id = {m.id: m for m in result}
assert by_id["M1"].status == MilestoneStatus.NOT_STARTED
assert by_id["M2"].status == MilestoneStatus.PLANNING
assert by_id["M3"].status == MilestoneStatus.IN_PROGRESS
assert by_id["M4"].status == MilestoneStatus.COMPLETE
def test_deliverable_status_parsing(self, tmp_path):
"""Correctly parses deliverable statuses."""
milestones_file = tmp_path / "milestones.md"
milestones_file.write_text("""---
type: milestones
---
# Milestones
#### M1: With Deliverables
**Target**: Jan 2026
**Status**: In Progress (33%)
| Deliverable | Status |
|-------------|--------|
| Task A | Done |
| Task B | In Progress |
| Task C | Not Started |
---
""")
parser = MilestonesParser(milestones_file)
result = parser.parse()
deliverables = result[0].deliverables
assert len(deliverables) == 3
from development_hub.models.goal import DeliverableStatus
statuses = {d.name: d.status for d in deliverables}
assert statuses["Task A"] == DeliverableStatus.DONE
assert statuses["Task B"] == DeliverableStatus.IN_PROGRESS
assert statuses["Task C"] == DeliverableStatus.NOT_STARTED
class TestTodoListOperations:
"""Test TodoList model operations."""
def test_add_todo_to_correct_priority(self):
"""Adding todo goes to correct priority list."""
todo_list = TodoList(source_file="/tmp/test.md")
high_todo = Todo(text="High task", priority="high", completed=False)
med_todo = Todo(text="Medium task", priority="medium", completed=False)
low_todo = Todo(text="Low task", priority="low", completed=False)
todo_list.add_todo(high_todo)
todo_list.add_todo(med_todo)
todo_list.add_todo(low_todo)
assert high_todo in todo_list.high_priority
assert med_todo in todo_list.medium_priority
assert low_todo in todo_list.low_priority
def test_remove_todo(self):
"""Removing todo from list."""
todo_list = TodoList(source_file="/tmp/test.md")
todo = Todo(text="Test task", priority="high", completed=False)
todo_list.add_todo(todo)
assert todo in todo_list.high_priority
todo_list.remove_todo(todo)
assert todo not in todo_list.high_priority
def test_get_by_milestone(self):
"""Get todos by milestone."""
todo_list = TodoList(source_file="/tmp/test.md")
t1 = Todo(text="Task 1", priority="high", completed=False, milestone="M1")
t2 = Todo(text="Task 2", priority="high", completed=False, milestone="M1")
t3 = Todo(text="Task 3", priority="high", completed=False, milestone="M2")
t4 = Todo(text="Task 4", priority="high", completed=False, milestone=None)
todo_list.add_todo(t1)
todo_list.add_todo(t2)
todo_list.add_todo(t3)
todo_list.add_todo(t4)
m1_todos = todo_list.get_by_milestone("M1")
assert len(m1_todos) == 2
assert t1 in m1_todos
assert t2 in m1_todos
def test_active_todos_excludes_completed(self):
"""active_todos excludes completed items."""
todo_list = TodoList(source_file="/tmp/test.md")
active = Todo(text="Active", priority="high", completed=False)
completed = Todo(text="Done", priority="high", completed=True)
todo_list.add_todo(active)
todo_list.completed.append(completed)
assert active in todo_list.active_todos
assert completed not in todo_list.active_todos
class TestGoalListOperations:
"""Test GoalList model operations."""
def test_create_goal_list(self):
"""Create a GoalList with goals."""
goal_list = GoalList(
active=[Goal(text="Active 1", priority="high")],
future=[Goal(text="Future 1", priority="medium")],
non_goals=[Goal(text="Non-goal 1", priority="low")],
)
assert len(goal_list.active) == 1
assert len(goal_list.future) == 1
assert len(goal_list.non_goals) == 1
def test_goal_with_partial_state(self):
"""Goal with partial state."""
goal = Goal(
text="Partial goal",
priority="high",
completed=False,
partial=True,
)
assert goal.completed == False
assert goal.partial == True
if __name__ == "__main__":
pytest.main([__file__, "-v"])