527 lines
13 KiB
Python
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"])
|