Add comprehensive tests and filtering options to populate-kanban script

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-27 04:03:22 +00:00
parent 1211d714a1
commit e79ea8564a
2 changed files with 384 additions and 0 deletions

View File

@@ -412,6 +412,25 @@ def main():
help='Limit number of issues to create (for testing)'
)
parser.add_argument(
'--filter-priority',
type=str,
choices=['critical', 'high', 'medium', 'low'],
help='Filter by priority level (e.g., --filter-priority critical)'
)
parser.add_argument(
'--filter-label',
type=str,
help='Filter by label (e.g., --filter-label security)'
)
parser.add_argument(
'--exclude-checklist',
action='store_true',
help='Exclude checklist items (items from "Done Criteria" or similar sections)'
)
args = parser.parse_args()
# Auto-detect todo directory if not specified
@@ -430,6 +449,38 @@ def main():
parser_obj = TodoParser(args.todo_dir)
items = parser_obj.parse_all()
# Apply filters
if args.exclude_checklist:
# Exclude items from checklist-like sections
checklist_sections = ['Done Criteria', 'Quick Wins', 'Sanity Check', 'Checklist']
original_count = len(items)
items = [
item for item in items
if not any(section.lower() in item.section.lower() for section in checklist_sections)
]
excluded = original_count - len(items)
if excluded > 0:
print(f"\nExcluded {excluded} checklist items")
if args.filter_priority:
# Filter by priority
priority_emoji = {
'critical': '🔴 Critical',
'high': '🟠 High',
'medium': '🟡 Medium',
'low': '🟢 Low'
}
target_priority = priority_emoji[args.filter_priority]
original_count = len(items)
items = [item for item in items if item.priority == target_priority]
print(f"\nFiltered to {len(items)} items with priority: {target_priority}")
if args.filter_label:
# Filter by label
original_count = len(items)
items = [item for item in items if args.filter_label in item.labels]
print(f"\nFiltered to {len(items)} items with label: {args.filter_label}")
if args.limit:
items = items[:args.limit]
print(f"\nLimited to first {args.limit} items")

View File

@@ -0,0 +1,333 @@
#!/usr/bin/env python3
"""
Unit tests for populate-kanban.py
Run with: python3 -m pytest test_populate_kanban.py -v
Or: python3 test_populate_kanban.py
"""
import sys
import unittest
from pathlib import Path
from textwrap import dedent
import tempfile
import shutil
# Import the module we're testing
sys.path.insert(0, str(Path(__file__).parent))
import importlib.util
spec = importlib.util.spec_from_file_location("populate_kanban", Path(__file__).parent / "populate-kanban.py")
populate_kanban = importlib.util.module_from_spec(spec)
spec.loader.exec_module(populate_kanban)
TodoParser = populate_kanban.TodoParser
TodoItem = populate_kanban.TodoItem
GitHubIssueCreator = populate_kanban.GitHubIssueCreator
class TestTodoParser(unittest.TestCase):
"""Test TodoParser class"""
def setUp(self):
"""Create a temporary directory for test files"""
self.test_dir = Path(tempfile.mkdtemp())
self.todo_dir = self.test_dir / "docs" / "todo"
self.todo_dir.mkdir(parents=True, exist_ok=True)
# Create README with priority mappings
readme_content = dedent("""
# MetaBuilder TODO List
## Quick Reference
| File | Area | Priority |
|------|------|----------|
| [test-high.md](test-high.md) | Testing | High |
| [test-critical.md](test-critical.md) | Critical tasks | Critical |
| [test-medium.md](test-medium.md) | Medium priority | Medium |
""")
(self.todo_dir / "README.md").write_text(readme_content)
def tearDown(self):
"""Clean up temporary directory"""
shutil.rmtree(self.test_dir, ignore_errors=True)
def test_parse_simple_todo(self):
"""Test parsing a simple TODO item"""
test_file = self.todo_dir / "test.md"
content = dedent("""
# Test TODO File
## Section One
- [ ] This is a simple TODO item
- [x] This is done, should be skipped
- [ ] This is another TODO
""")
test_file.write_text(content)
parser = TodoParser(self.todo_dir)
items = parser._parse_file(test_file)
self.assertEqual(len(items), 2)
self.assertEqual(items[0].title, "This is a simple TODO item")
self.assertEqual(items[0].section, "Section One")
self.assertEqual(items[1].title, "This is another TODO")
def test_skip_empty_todos(self):
"""Test that empty TODO items are skipped"""
test_file = self.todo_dir / "test.md"
content = dedent("""
# Test
- [ ]
- [ ] a
- [ ] Valid TODO item
""")
test_file.write_text(content)
parser = TodoParser(self.todo_dir)
items = parser._parse_file(test_file)
# Should skip empty and very short items
self.assertEqual(len(items), 1)
self.assertEqual(items[0].title, "Valid TODO item")
def test_parse_with_context(self):
"""Test parsing TODO items with context"""
test_file = self.todo_dir / "test.md"
content = dedent("""
# Test
## Authentication
- [x] Implement login
- [x] Add password hashing
- [ ] Add two-factor authentication
""")
test_file.write_text(content)
parser = TodoParser(self.todo_dir)
items = parser._parse_file(test_file)
self.assertEqual(len(items), 1)
self.assertEqual(items[0].title, "Add two-factor authentication")
self.assertEqual(items[0].section, "Authentication")
# Check that context includes previous items
self.assertIn("[x]", items[0].body)
def test_categorize_file_by_name(self):
"""Test file categorization based on filename"""
parser = TodoParser(self.todo_dir)
# Test DBAL file
dbal_file = self.todo_dir / "4-DBAL-TODO.md"
dbal_file.write_text("# DBAL\n- [ ] Test\n")
labels = parser._categorize_file(dbal_file)
self.assertIn("dbal", labels)
# Test security file
security_file = self.todo_dir / "10-SECURITY-TODO.md"
security_file.write_text("# Security\n- [ ] Test\n")
labels = parser._categorize_file(security_file)
self.assertIn("security", labels)
# Test frontend file
frontend_file = self.todo_dir / "5-FRONTEND-TODO.md"
frontend_file.write_text("# Frontend\n- [ ] Test\n")
labels = parser._categorize_file(frontend_file)
self.assertIn("frontend", labels)
def test_categorize_file_by_directory(self):
"""Test file categorization based on directory"""
parser = TodoParser(self.todo_dir)
# Test core directory
core_dir = self.todo_dir / "core"
core_dir.mkdir(exist_ok=True)
core_file = core_dir / "test.md"
core_file.write_text("# Test\n- [ ] Item\n")
labels = parser._categorize_file(core_file)
self.assertIn("core", labels)
# Test infrastructure directory
infra_dir = self.todo_dir / "infrastructure"
infra_dir.mkdir(exist_ok=True)
infra_file = infra_dir / "test.md"
infra_file.write_text("# Test\n- [ ] Item\n")
labels = parser._categorize_file(infra_file)
self.assertIn("infrastructure", labels)
def test_get_priority_from_readme(self):
"""Test priority assignment from README"""
parser = TodoParser(self.todo_dir)
high_file = self.todo_dir / "test-high.md"
priority = parser._get_priority(high_file)
self.assertEqual(priority, "🟠 High")
critical_file = self.todo_dir / "test-critical.md"
priority = parser._get_priority(critical_file)
self.assertEqual(priority, "🔴 Critical")
medium_file = self.todo_dir / "test-medium.md"
priority = parser._get_priority(medium_file)
self.assertEqual(priority, "🟡 Medium")
def test_get_priority_default(self):
"""Test default priority assignment"""
parser = TodoParser(self.todo_dir)
# Security should be critical by default
security_file = self.todo_dir / "security-tasks.md"
priority = parser._get_priority(security_file)
self.assertEqual(priority, "🔴 Critical")
# Future features should be low
future_file = self.todo_dir / "future-features.md"
priority = parser._get_priority(future_file)
self.assertEqual(priority, "🟢 Low")
# Unknown should be medium
unknown_file = self.todo_dir / "random-tasks.md"
priority = parser._get_priority(unknown_file)
self.assertEqual(priority, "🟡 Medium")
def test_parse_all_excludes_special_files(self):
"""Test that special files are excluded from parsing"""
# Create some TODO files
(self.todo_dir / "1-TODO.md").write_text("# Test\n- [ ] Item 1\n")
(self.todo_dir / "README.md").write_text("# README\n- [ ] Should skip\n")
(self.todo_dir / "TODO_STATUS.md").write_text("# Status\n- [ ] Should skip\n")
(self.todo_dir / "TODO_SCAN_REPORT.md").write_text("# Scan\n- [ ] Should skip\n")
(self.todo_dir / "REFACTOR_PLAN.md").write_text("# Refactor\n- [ ] Should skip\n")
parser = TodoParser(self.todo_dir)
items = parser.parse_all()
# Should only parse 1-TODO.md
self.assertEqual(len(items), 1)
self.assertEqual(items[0].title, "Item 1")
def test_title_truncation(self):
"""Test that long titles are truncated"""
test_file = self.todo_dir / "test.md"
long_title = "A" * 150
content = f"# Test\n- [ ] {long_title}\n"
test_file.write_text(content)
parser = TodoParser(self.todo_dir)
items = parser._parse_file(test_file)
self.assertEqual(len(items), 1)
self.assertEqual(len(items[0].title), 103) # 100 + "..."
self.assertTrue(items[0].title.endswith("..."))
def test_section_tracking(self):
"""Test that section headers are tracked correctly"""
test_file = self.todo_dir / "test.md"
content = dedent("""
# Main Title
## Section A
- [ ] Item in A
## Section B
- [ ] Item in B
### Subsection B.1
- [ ] Item in B.1
""")
test_file.write_text(content)
parser = TodoParser(self.todo_dir)
items = parser._parse_file(test_file)
self.assertEqual(len(items), 3)
self.assertEqual(items[0].section, "Section A")
self.assertEqual(items[1].section, "Section B")
self.assertEqual(items[2].section, "Subsection B.1")
def test_line_number_tracking(self):
"""Test that line numbers are tracked correctly"""
test_file = self.todo_dir / "test.md"
# Note: dedent() adds a leading newline, so lines are 1-indexed from the first actual line
content = "# Test\n\n- [ ] Item on line 3\n\n- [ ] Item on line 5\n"
test_file.write_text(content)
parser = TodoParser(self.todo_dir)
items = parser._parse_file(test_file)
self.assertEqual(len(items), 2)
self.assertEqual(items[0].line_number, 3)
self.assertEqual(items[1].line_number, 5)
class TestTodoItem(unittest.TestCase):
"""Test TodoItem dataclass"""
def test_todo_item_creation(self):
"""Test creating a TodoItem"""
item = TodoItem(
title="Test Task",
body="This is a test body",
file="test.md",
section="Test Section",
labels=["test", "demo"],
priority="🟡 Medium",
line_number=42
)
self.assertEqual(item.title, "Test Task")
self.assertEqual(item.body, "This is a test body")
self.assertEqual(item.file, "test.md")
self.assertEqual(item.section, "Test Section")
self.assertEqual(item.labels, ["test", "demo"])
self.assertEqual(item.priority, "🟡 Medium")
self.assertEqual(item.line_number, 42)
class TestGitHubIssueCreator(unittest.TestCase):
"""Test GitHubIssueCreator class"""
def test_issue_creator_initialization(self):
"""Test creating a GitHubIssueCreator"""
creator = GitHubIssueCreator("owner/repo", project_id=5)
self.assertEqual(creator.repo, "owner/repo")
self.assertEqual(creator.project_id, 5)
def test_issue_creator_without_project(self):
"""Test creating a GitHubIssueCreator without project"""
creator = GitHubIssueCreator("owner/repo")
self.assertEqual(creator.repo, "owner/repo")
self.assertIsNone(creator.project_id)
def test_create_issue_dry_run(self):
"""Test dry run mode doesn't create actual issues"""
creator = GitHubIssueCreator("owner/repo")
item = TodoItem(
title="Test",
body="Body",
file="test.md",
section="Section",
labels=["test"],
priority="🟡 Medium",
line_number=1
)
result = creator.create_issue(item, dry_run=True)
self.assertIsNone(result)
def run_tests():
"""Run all tests"""
loader = unittest.TestLoader()
suite = loader.loadTestsFromModule(sys.modules[__name__])
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
return 0 if result.wasSuccessful() else 1
if __name__ == "__main__":
sys.exit(run_tests())