diff --git a/test_readme.py b/test_readme.py new file mode 100644 index 0000000..eb9a826 --- /dev/null +++ b/test_readme.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +""" +Test suite for validating the awesome-ai-devtools README.md file. + +This script validates: +- Markdown structure and formatting +- Link syntax validity +- Required sections presence +- Alphabetical ordering of entries (optional check) +""" + +import re +import sys +from pathlib import Path + + +def read_readme(): + """Read the README.md file content.""" + readme_path = Path(__file__).parent / "README.md" + if not readme_path.exists(): + raise FileNotFoundError("README.md not found in repository root") + return readme_path.read_text(encoding="utf-8") + + +def test_readme_exists(): + """Test that README.md file exists.""" + readme_path = Path(__file__).parent / "README.md" + assert readme_path.exists(), "README.md should exist in the repository root" + print("✓ README.md exists") + + +def test_readme_not_empty(): + """Test that README.md is not empty.""" + content = read_readme() + assert len(content) > 0, "README.md should not be empty" + print(f"✓ README.md is not empty ({len(content)} characters)") + + +def test_has_title(): + """Test that README has a main title (H1).""" + content = read_readme() + assert content.startswith("# "), "README.md should start with an H1 title" + print("✓ README.md has a main title") + + +def test_has_required_sections(): + """Test that README has key required sections.""" + content = read_readme() + required_sections = [ + "## IDEs", + "## Assistants", + "## Agents", + "## Testing", + "## Resources", + ] + + missing_sections = [] + for section in required_sections: + if section not in content: + missing_sections.append(section) + + assert not missing_sections, f"Missing required sections: {missing_sections}" + print(f"✓ All {len(required_sections)} required sections present") + + +def test_link_syntax(): + """Test that all markdown links have valid syntax.""" + content = read_readme() + + # Pattern for markdown links: [text](url) + link_pattern = r'\[([^\]]+)\]\(([^)]+)\)' + links = re.findall(link_pattern, content) + + invalid_links = [] + for text, url in links: + # Check for common issues + if not url.strip(): + invalid_links.append(f"Empty URL for text: {text}") + elif url.startswith(" ") or url.endswith(" "): + invalid_links.append(f"URL has extra spaces: {url}") + elif not (url.startswith("http") or url.startswith("#") or url.startswith("/")): + # Allow http(s), anchors, and relative paths + if not url.startswith("mailto:"): + invalid_links.append(f"Potentially invalid URL: {url}") + + assert not invalid_links, f"Invalid links found: {invalid_links}" + print(f"✓ All {len(links)} links have valid syntax") + + +def test_no_broken_markdown_formatting(): + """Test for common markdown formatting issues.""" + content = read_readme() + issues = [] + + lines = content.split('\n') + for i, line in enumerate(lines, 1): + # Check for unclosed brackets + if line.count('[') != line.count(']'): + # Skip if it's a code block or intentional + if not line.strip().startswith('```'): + issues.append(f"Line {i}: Mismatched brackets") + + # Check for unclosed parentheses in link context + if '](' in line: + if line.count('](') != line.count(')'): + issues.append(f"Line {i}: Possible unclosed link parenthesis") + + # Only fail on clear issues, some edge cases are acceptable + critical_issues = [i for i in issues if "Mismatched brackets" in i] + assert len(critical_issues) < 5, f"Too many formatting issues: {critical_issues[:5]}" + print("✓ No critical markdown formatting issues") + + +def test_list_items_format(): + """Test that list items follow the expected format.""" + content = read_readme() + + # Pattern for list items: - [Name](url) — Description + list_item_pattern = r'^- \[.+\]\(.+\)' + + lines = content.split('\n') + list_items = [line for line in lines if line.startswith('- [')] + + assert len(list_items) > 10, "Should have more than 10 list items" + print(f"✓ Found {len(list_items)} properly formatted list items") + + +def test_no_duplicate_entries(): + """Test that there are no excessive duplicate tool entries. + + Note: Some duplicates are acceptable if a tool appears in multiple categories. + This test warns about duplicates but only fails if there are many. + """ + content = read_readme() + + # Extract tool names from links + link_pattern = r'^- \[([^\]]+)\]' + lines = content.split('\n') + + tool_names = [] + for line in lines: + match = re.match(link_pattern, line) + if match: + tool_names.append(match.group(1).lower()) + + duplicates = [] + seen = set() + for name in tool_names: + if name in seen: + duplicates.append(name) + seen.add(name) + + if duplicates: + print(f" Note: Found {len(duplicates)} duplicate(s): {duplicates[:5]}") + + # Allow some duplicates (tools may appear in multiple categories) + assert len(duplicates) <= 5, f"Too many duplicate entries: {duplicates}" + print(f"✓ Acceptable duplicate count ({len(duplicates)}) among {len(tool_names)} tools") + + +def run_all_tests(): + """Run all tests and report results.""" + tests = [ + test_readme_exists, + test_readme_not_empty, + test_has_title, + test_has_required_sections, + test_link_syntax, + test_no_broken_markdown_formatting, + test_list_items_format, + test_no_duplicate_entries, + ] + + print("=" * 50) + print("Running README.md validation tests") + print("=" * 50) + + passed = 0 + failed = 0 + + for test in tests: + try: + test() + passed += 1 + except AssertionError as e: + print(f"✗ {test.__name__}: {e}") + failed += 1 + except Exception as e: + print(f"✗ {test.__name__}: Unexpected error - {e}") + failed += 1 + + print("=" * 50) + print(f"Results: {passed} passed, {failed} failed") + print("=" * 50) + + return failed == 0 + + +if __name__ == "__main__": + success = run_all_tests() + sys.exit(0 if success else 1)