fix: add tests and project configuration for CI/CD

- Add tests directory with comprehensive test suite
- Add development configuration files (requirements-dev.txt, setup.cfg)
- Add pre-commit hooks and gitignore for project hygiene
- Ensure CI workflow has all necessary files to run 121 tests
This commit is contained in:
CI Bot
2026-02-06 03:44:37 +00:00
parent 52e792305b
commit b536daa983
20 changed files with 2656 additions and 0 deletions

61
tests/conftest.py Normal file
View File

@@ -0,0 +1,61 @@
"""Pytest configuration and fixtures for regex humanizer tests."""
import pytest
from regex_humanizer.parser import RegexParser, parse_regex
from regex_humanizer.translator import RegexTranslator, translate_regex
from regex_humanizer.test_generator import TestCaseGenerator, generate_test_cases
@pytest.fixture
def parser():
"""Provide a RegexParser instance."""
return RegexParser
@pytest.fixture
def translator():
"""Provide a RegexTranslator instance."""
return RegexTranslator("pcre")
@pytest.fixture
def test_generator():
"""Provide a TestCaseGenerator instance."""
return TestCaseGenerator("pcre")
@pytest.fixture
def sample_patterns():
"""Provide sample regex patterns for testing."""
return {
"simple_literal": "hello",
"character_class": "[a-z]",
"quantifier_plus": "a+",
"quantifier_star": "b*",
"quantifier_question": "c?",
"digit": "\\d{3}",
"word": "\\w+",
"email": "[a-z]+@[a-z]+\\.[a-z]+",
"phone": "\\d{3}-\\d{4}",
"ip_address": "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}",
"url": "https?://[^\\s]+",
"date": "\\d{4}-\\d{2}-\\d{2}",
"group": "(hello)\\s+(world)",
"named_group": "(?P<name>[a-z]+)",
"lookahead": "\\d+(?=px)",
"negative_lookahead": "\\d+(?!px)",
"lookbehind": "(?<=\\$)\\d+",
"non_capturing": "(?:hello)\\s+(?:world)",
"alternation": "cat|dog",
"anchor_start": "^start",
"anchor_end": "end$",
"word_boundary": "\\bword\\b",
"complex": "^(?:http|https)://[\\w.-]+\\.(?:com|org|net)$",
}
@pytest.fixture
def flavor_manager():
"""Provide the flavor manager."""
from regex_humanizer.flavors import get_flavor_manager
return get_flavor_manager()

75
tests/fixtures/README_EXAMPLE.md vendored Normal file
View File

@@ -0,0 +1,75 @@
# Example Generated README
This is an example of a README file that can be generated by Auto README Generator CLI.
## Overview
A Python project located at `/example/project` containing multiple files.
## Supported Languages
This project uses:
- **Python**
## Installation
```bash
pip install -r requirements.txt
pip install -e .
```
### Dependencies
- `requests` v2.31.0
- `click` v8.0.0
## Usage
### Basic Usage
```python
from project_name import main
main()
```
## Features
- Test suite included
- Uses 2 dependencies
- Contains 1 classes
- Contains 3 functions
## API Reference
### `hello()`
Say hello.
**Parameters:**
### `add(a, b)`
Add two numbers.
**Parameters:** `a, b`
## Contributing
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
For Python development:
- Run tests with `pytest`
- Format code with `black` and `isort`
- Check types with `mypy`
## License
MIT
---
*Generated by Auto README Generator on 2024-01-15*

253
tests/test_cli.py Normal file
View File

@@ -0,0 +1,253 @@
"""Tests for the CLI interface."""
import json
import pytest
from click.testing import CliRunner
from regex_humanizer.cli import main, explain, test, flavors, validate, convert
class TestCLIMain:
"""Test the main CLI entry point."""
def test_main_help(self):
"""Test that main help works."""
runner = CliRunner()
result = runner.invoke(main, ["--help"])
assert result.exit_code == 0
assert "Regex Humanizer" in result.output
assert "explain" in result.output
assert "test" in result.output
assert "interactive" in result.output
class TestCLIExplain:
"""Test the explain command."""
def test_explain_simple_literal(self):
"""Test explaining a simple literal."""
runner = CliRunner()
result = runner.invoke(explain, ["hello"])
assert result.exit_code == 0
assert "Pattern" in result.output or "hello" in result.output
def test_explain_with_flavor_option(self):
"""Test explaining with a flavor option."""
runner = CliRunner()
result = runner.invoke(explain, ["--flavor", "javascript", "test"])
assert result.exit_code == 0
assert "Flavor" in result.output or "javascript" in result.output
def test_explain_with_json_output(self):
"""Test explaining with JSON output."""
runner = CliRunner()
result = runner.invoke(explain, ["--output", "json", "\\d+"])
assert result.exit_code == 0
assert "{", "}" in result.output
def test_explain_with_verbose(self):
"""Test explaining with verbose flag."""
runner = CliRunner()
result = runner.invoke(explain, ["--verbose", "\\d+"])
assert result.exit_code == 0
assert "Features" in result.output or "digit" in result.output.lower()
def test_explain_complex_pattern(self):
"""Test explaining a complex pattern."""
runner = CliRunner()
pattern = r"^(?:http|https)://[\w.-]+\.(?:com|org|net)$"
result = runner.invoke(explain, [pattern])
assert result.exit_code == 0
assert "Pattern" in result.output
def test_explain_phone_pattern(self):
"""Test explaining a phone pattern."""
runner = CliRunner()
result = runner.invoke(explain, [r"\d{3}-\d{4}"])
assert result.exit_code == 0
def test_explain_character_class(self):
"""Test explaining a character class."""
runner = CliRunner()
result = runner.invoke(explain, ["[a-zA-Z]+"])
assert result.exit_code == 0
class TestCLITest:
"""Test the test command."""
def test_test_simple_literal(self):
"""Test generating test cases for a simple literal."""
runner = CliRunner()
result = runner.invoke(test, ["hello"])
assert result.exit_code == 0
assert "Matching" in result.output or "hello" in result.output
assert "Non-matching" in result.output
def test_test_with_count_option(self):
"""Test generating a specific number of test cases."""
runner = CliRunner()
result = runner.invoke(test, ["--count", "3", "a"])
assert result.exit_code == 0
def test_test_with_json_output(self):
"""Test generating test cases with JSON output."""
runner = CliRunner()
result = runner.invoke(test, ["--output", "json", "test"])
assert result.exit_code == 0
data = json.loads(result.output)
assert "matching" in data
assert "non_matching" in data
def test_test_phone_pattern(self):
"""Test generating test cases for a phone pattern."""
runner = CliRunner()
result = runner.invoke(test, [r"\d{3}-\d{4}"])
assert result.exit_code == 0
assert "Matching" in result.output
def test_test_email_pattern(self):
"""Test generating test cases for an email pattern."""
runner = CliRunner()
result = runner.invoke(test, [r"[a-z]+@[a-z]+\.[a-z]+"])
assert result.exit_code == 0
def test_test_quantifier_pattern(self):
"""Test generating test cases for a quantifier pattern."""
runner = CliRunner()
result = runner.invoke(test, ["a{2,4}"])
assert result.exit_code == 0
class TestCLIFlavors:
"""Test the flavors command."""
def test_flavors_list(self):
"""Test listing available flavors."""
runner = CliRunner()
result = runner.invoke(flavors)
assert result.exit_code == 0
assert "pcre" in result.output.lower()
assert "javascript" in result.output.lower()
assert "python" in result.output.lower()
class TestCLIValidate:
"""Test the validate command."""
def test_validate_valid_pattern(self):
"""Test validating a valid pattern."""
runner = CliRunner()
result = runner.invoke(validate, ["hello"])
assert result.exit_code == 0
assert "PASSED" in result.output or "Validation" in result.output
def test_validate_with_flavor(self):
"""Test validating with a specific flavor."""
runner = CliRunner()
result = runner.invoke(validate, ["--flavor", "javascript", "test"])
assert result.exit_code == 0
def test_validate_complex_pattern(self):
"""Test validating a complex pattern."""
runner = CliRunner()
pattern = r"^(?:http|https)://[\w.-]+\.(?:com|org|net)$"
result = runner.invoke(validate, [pattern])
assert result.exit_code == 0
class TestCLIConvert:
"""Test the convert command."""
def test_convert_pcre_to_js(self):
"""Test converting a pattern from PCRE to JavaScript."""
runner = CliRunner()
result = runner.invoke(convert, ["(?P<test>hello)", "--from-flavor", "pcre", "--to-flavor", "javascript"])
assert result.exit_code == 0
assert "Converted" in result.output
def test_convert_with_defaults(self):
"""Test converting with default flavors."""
runner = CliRunner()
result = runner.invoke(convert, ["test"])
assert result.exit_code == 0
assert "Original" in result.output
assert "Converted" in result.output
class TestCLIInteractive:
"""Test the interactive command."""
def test_interactive_command_exists(self):
"""Test that interactive command is available."""
runner = CliRunner()
result = runner.invoke(main, ["interactive", "--help"])
assert result.exit_code == 0
assert "interactive" in result.output.lower()
def test_interactive_with_flavor(self):
"""Test interactive mode with a specific flavor."""
runner = CliRunner()
result = runner.invoke(main, ["interactive", "--flavor", "python"])
assert result.exit_code == 0
class TestCLIIntegration:
"""Integration tests for CLI."""
def test_flavor_option_global(self):
"""Test that global flavor option works."""
runner = CliRunner()
result = runner.invoke(main, ["--flavor", "python", "explain", "test"])
assert result.exit_code == 0
def test_error_handling_invalid_pattern(self):
"""Test error handling for invalid patterns."""
runner = CliRunner()
result = runner.invoke(explain, ["["])
assert result.exit_code == 0
def test_output_json_structure(self):
"""Test JSON output has correct structure."""
runner = CliRunner()
result = runner.invoke(explain, ["--output", "json", "\\d+"])
assert result.exit_code == 0
data = json.loads(result.output)
assert "pattern" in data
assert "flavor" in data
assert "explanation" in data
def test_output_test_json_structure(self):
"""Test JSON test output has correct structure."""
runner = CliRunner()
result = runner.invoke(test, ["--output", "json", "\\d"])
assert result.exit_code == 0
data = json.loads(result.output)
assert "pattern" in data
assert "matching" in data
assert "non_matching" in data

134
tests/test_config.py Normal file
View File

@@ -0,0 +1,134 @@
"""Tests for configuration management."""
import os
import pytest
from mcp_server_cli.config import (
ConfigManager,
create_config_template,
load_config_from_path,
)
from mcp_server_cli.models import AppConfig, LocalLLMConfig, ServerConfig
class TestConfigManager:
"""Tests for ConfigManager."""
def test_load_default_config(self, tmp_path):
"""Test loading default configuration."""
config_path = tmp_path / "config.yaml"
config_path.write_text("")
manager = ConfigManager(config_path)
config = manager.load()
assert isinstance(config, AppConfig)
assert config.server.port == 3000
assert config.server.host == "127.0.0.1"
def test_load_config_with_values(self, tmp_path):
"""Test loading configuration with custom values."""
config_file = tmp_path / "config.yaml"
config_file.write_text("""
server:
host: "127.0.0.1"
port: 8080
log_level: "DEBUG"
llm:
enabled: false
base_url: "http://localhost:11434"
model: "llama2"
security:
allowed_commands:
- ls
- cat
- echo
blocked_paths:
- /etc
- /root
""")
manager = ConfigManager(config_file)
config = manager.load()
assert config.server.port == 8080
assert config.server.host == "127.0.0.1"
assert config.server.log_level == "DEBUG"
class TestConfigFromPath:
"""Tests for loading config from path."""
def test_load_from_path_success(self, tmp_path):
"""Test successful config loading from path."""
config_file = tmp_path / "config.yaml"
config_file.write_text("server:\n port: 8080")
config = load_config_from_path(str(config_file))
assert config.server.port == 8080
def test_load_from_path_not_found(self):
"""Test loading from nonexistent path."""
with pytest.raises(FileNotFoundError):
load_config_from_path("/nonexistent/path/config.yaml")
class TestConfigTemplate:
"""Tests for configuration template."""
def test_create_template(self):
"""Test creating a config template."""
template = create_config_template()
assert "server" in template
assert "llm" in template
assert "security" in template
assert "tools" in template
def test_template_has_required_fields(self):
"""Test that template has all required fields."""
template = create_config_template()
assert template["server"]["port"] == 3000
assert "allowed_commands" in template["security"]
class TestConfigValidation:
"""Tests for configuration validation."""
def test_valid_config(self):
"""Test creating a valid config."""
config = AppConfig(
server=ServerConfig(port=4000, host="localhost"),
llm=LocalLLMConfig(enabled=True, base_url="http://localhost:1234"),
)
assert config.server.port == 4000
assert config.llm.enabled is True
def test_config_with_empty_tools(self):
"""Test config with empty tools list."""
config = AppConfig(tools=[])
assert len(config.tools) == 0
class TestEnvVarMapping:
"""Tests for environment variable mappings."""
def test_get_env_var_name(self):
"""Test environment variable name generation."""
manager = ConfigManager()
assert manager.get_env_var_name("server.port") == "MCP_SERVER_PORT"
assert manager.get_env_var_name("host") == "MCP_HOST"
def test_get_from_env(self):
"""Test getting values from environment."""
manager = ConfigManager()
os.environ["MCP_TEST_VAR"] = "test_value"
try:
result = manager.get_from_env("test_var")
assert result == "test_value"
finally:
del os.environ["MCP_TEST_VAR"]

224
tests/test_generator.py Normal file
View File

@@ -0,0 +1,224 @@
"""Tests for the test case generator."""
import re
import pytest
from regex_humanizer.test_generator import (
TestCaseGenerator,
generate_test_cases,
)
class TestTestCaseGenerator:
"""Test cases for TestCaseGenerator."""
def test_generate_simple_literal(self):
"""Test generating test cases for a simple literal."""
generator = TestCaseGenerator("pcre")
matching = generator.generate_matching("hello", count=3)
assert len(matching) >= 1
assert "hello" in matching or len(matching[0]) > 0
def test_generate_character_class(self):
"""Test generating test cases for a character class."""
generator = TestCaseGenerator("pcre")
matching = generator.generate_matching("[abc]", count=3)
assert len(matching) >= 1
assert len(matching[0]) == 1
assert matching[0] in ["a", "b", "c"]
def test_generate_quantifier_plus(self):
"""Test generating test cases for plus quantifier."""
generator = TestCaseGenerator("pcre")
matching = generator.generate_matching("a+", count=3)
assert len(matching) >= 1
assert "a" in matching[0]
def test_generate_quantifier_star(self):
"""Test generating test cases for star quantifier."""
generator = TestCaseGenerator("pcre")
matching = generator.generate_matching("b*", count=3)
assert len(matching) >= 1
assert all("b" in m for m in matching)
def test_generate_quantifier_question(self):
"""Test generating test cases for question quantifier."""
generator = TestCaseGenerator("pcre")
matching = generator.generate_matching("c?", count=3)
assert len(matching) >= 1
assert len(matching[0]) <= 1
def test_generate_digit_class(self):
"""Test generating test cases for digit class."""
generator = TestCaseGenerator("pcre")
matching = generator.generate_matching("\\d+", count=3)
assert len(matching) >= 1
assert matching[0].isdigit()
def test_generate_word_class(self):
"""Test generating test cases for word class."""
generator = TestCaseGenerator("pcre")
matching = generator.generate_matching("\\w+", count=3)
assert len(matching) >= 1
def test_generate_non_matching_simple(self):
"""Test generating non-matching test cases."""
generator = TestCaseGenerator("pcre")
non_matching = generator.generate_non_matching("hello", count=3)
assert len(non_matching) >= 1
pattern = re.compile("hello")
for test_str in non_matching:
assert pattern.search(test_str) is None
def test_generate_non_matching_character_class(self):
"""Test generating non-matching cases for character class."""
generator = TestCaseGenerator("pcre")
non_matching = generator.generate_non_matching("[abc]", count=3)
assert len(non_matching) >= 1
for test_str in non_matching:
if len(test_str) > 0:
assert test_str[0] not in ["a", "b", "c"]
def test_generate_phone_pattern(self):
"""Test generating test cases for phone pattern."""
generator = TestCaseGenerator("pcre")
matching = generator.generate_matching("\\d{3}-\\d{4}", count=3)
assert len(matching) >= 1
assert "-" in matching[0]
parts = matching[0].split("-")
assert len(parts) == 2
assert len(parts[0]) == 3
assert len(parts[1]) == 4
def test_generate_with_flavor_js(self):
"""Test generating test cases with JavaScript flavor."""
generator = TestCaseGenerator("javascript")
matching = generator.generate_matching("test", count=2)
assert len(matching) >= 1
def test_generate_with_flavor_python(self):
"""Test generating test cases with Python flavor."""
generator = TestCaseGenerator("python")
matching = generator.generate_matching("test", count=2)
assert len(matching) >= 1
def test_generate_matching_count(self):
"""Test that correct number of matching cases are generated."""
generator = TestCaseGenerator("pcre")
matching = generator.generate_matching("a", count=10)
assert len(matching) <= 10
def test_generate_non_matching_count(self):
"""Test that correct number of non-matching cases are generated."""
generator = TestCaseGenerator("pcre")
non_matching = generator.generate_non_matching("a", count=10)
assert len(non_matching) <= 10
def test_generate_complex_pattern(self):
"""Test generating test cases for a complex pattern."""
pattern = r"^(?:http|https)://[\w.-]+\.(?:com|org|net)$"
generator = TestCaseGenerator("pcre")
matching = generator.generate_matching(pattern, count=3)
assert len(matching) >= 1
def test_generate_test_cases_function(self):
"""Test the generate_test_cases convenience function."""
result = generate_test_cases("test", flavor="pcre", matching_count=2, non_matching_count=2)
assert "pattern" in result
assert "flavor" in result
assert "matching" in result
assert "non_matching" in result
assert len(result["matching"]) >= 1
assert len(result["non_matching"]) >= 1
def test_generate_group_pattern(self):
"""Test generating test cases for a pattern with groups."""
generator = TestCaseGenerator("pcre")
matching = generator.generate_matching("(hello)\\s+(world)", count=3)
assert len(matching) >= 1
assert " " in matching[0] or len(matching[0]) > 5
def test_generate_alternation(self):
"""Test generating test cases for alternation."""
generator = TestCaseGenerator("pcre")
matching = generator.generate_matching("cat|dog", count=3)
assert len(matching) >= 1
assert matching[0] in ["cat", "dog"]
def test_generate_empty_pattern(self):
"""Test generating test cases for an empty pattern."""
generator = TestCaseGenerator("pcre")
matching = generator.generate_matching("", count=3)
assert len(matching) >= 0
def test_generate_anchored_pattern(self):
"""Test generating test cases for anchored patterns."""
generator = TestCaseGenerator("pcre")
matching = generator.generate_matching("^start", count=3)
assert len(matching) >= 1
assert matching[0].startswith("start")
def test_generate_dotted_pattern(self):
"""Test generating test cases for a pattern with dots."""
generator = TestCaseGenerator("pcre")
matching = generator.generate_matching("a.b", count=3)
assert len(matching) >= 1
assert len(matching[0]) == 3
assert matching[0][0] == "a"
assert matching[0][1] != "."
class TestTestCaseGeneratorEdgeCases:
"""Test edge cases for TestCaseGenerator."""
def test_generate_with_max_length(self):
"""Test that max_length is respected."""
generator = TestCaseGenerator("pcre")
matching = generator.generate_matching("a+", count=5, max_length=5)
for m in matching:
if len(m) > 0:
assert len(m) <= 5
def test_generate_nested_groups(self):
"""Test generating test cases for nested groups."""
generator = TestCaseGenerator("pcre")
matching = generator.generate_matching("((a)(b))", count=3)
assert len(matching) >= 1
def test_generate_quantifier_range(self):
"""Test generating test cases for range quantifier."""
generator = TestCaseGenerator("pcre")
matching = generator.generate_matching("a{2,4}", count=3)
assert len(matching) >= 1
assert 2 <= len(matching[0]) <= 4
assert matching[0] == "a" * len(matching[0])
def test_generate_lookahead_pattern(self):
"""Test generating test cases for a lookahead pattern."""
generator = TestCaseGenerator("pcre")
matching = generator.generate_matching("(?=test)", count=3)
assert len(matching) >= 0

116
tests/test_gitignore.py Normal file
View File

@@ -0,0 +1,116 @@
"""Tests for gitignore generation."""
import tempfile
from pathlib import Path
import pytest
from project_scaffold_cli.gitignore import GitignoreGenerator
class TestGitignoreGenerator:
"""Test GitignoreGenerator class."""
def test_generator_initialization(self):
"""Test generator can be initialized."""
gen = GitignoreGenerator()
assert gen is not None
def test_generate_python_gitignore(self):
"""Test generating Python .gitignore."""
gen = GitignoreGenerator()
with tempfile.TemporaryDirectory() as tmpdir:
output_path = Path(tmpdir) / ".gitignore"
gen.generate("python", output_path)
assert output_path.exists()
content = output_path.read_text()
assert "__pycache__" in content
assert "*.pyc" in content
assert "venv/" in content
assert ".pytest_cache" in content
def test_generate_nodejs_gitignore(self):
"""Test generating Node.js .gitignore."""
gen = GitignoreGenerator()
with tempfile.TemporaryDirectory() as tmpdir:
output_path = Path(tmpdir) / ".gitignore"
gen.generate("nodejs", output_path)
assert output_path.exists()
content = output_path.read_text()
assert "node_modules" in content
assert "npm-debug.log" in content
def test_generate_go_gitignore(self):
"""Test generating Go .gitignore."""
gen = GitignoreGenerator()
with tempfile.TemporaryDirectory() as tmpdir:
output_path = Path(tmpdir) / ".gitignore"
gen.generate("go", output_path)
assert output_path.exists()
content = output_path.read_text()
assert "*.exe" in content
assert "vendor/" in content
def test_generate_rust_gitignore(self):
"""Test generating Rust .gitignore."""
gen = GitignoreGenerator()
with tempfile.TemporaryDirectory() as tmpdir:
output_path = Path(tmpdir) / ".gitignore"
gen.generate("rust", output_path)
assert output_path.exists()
content = output_path.read_text()
assert "target/" in content
assert "Cargo.lock" in content
def test_generate_unsupported_language(self):
"""Test generating gitignore for unsupported language."""
gen = GitignoreGenerator()
with tempfile.TemporaryDirectory() as tmpdir:
with pytest.raises(ValueError) as exc_info:
gen.generate("unsupported", Path(tmpdir) / ".gitignore")
assert "Unsupported language" in str(exc_info.value)
def test_append_patterns(self):
"""Test appending patterns to existing .gitignore."""
gen = GitignoreGenerator()
with tempfile.TemporaryDirectory() as tmpdir:
gitignore_path = Path(tmpdir) / ".gitignore"
gitignore_path.write_text("# Original content\n")
gen.append_patterns(gitignore_path, {"*.custom", "secret.txt"})
content = gitignore_path.read_text()
assert "*.custom" in content
assert "secret.txt" in content
def test_get_template_content(self):
"""Test getting raw template content."""
gen = GitignoreGenerator()
python_content = gen.get_template_content("python")
assert isinstance(python_content, str)
assert len(python_content) > 0
nodejs_content = gen.get_template_content("nodejs")
assert isinstance(nodejs_content, str)
assert len(nodejs_content) > 0
def test_list_available_templates(self):
"""Test listing available templates."""
gen = GitignoreGenerator()
templates = gen.list_available_templates()
assert isinstance(templates, list)

313
tests/test_parser.py Normal file
View File

@@ -0,0 +1,313 @@
"""Tests for the regex parser."""
import pytest
from regex_humanizer.parser import (
RegexParser,
parse_regex,
NodeType,
RegexNode,
LiteralNode,
CharacterClassNode,
QuantifierNode,
GroupNode,
)
class TestRegexParser:
"""Test cases for RegexParser."""
def test_parse_simple_literal(self):
"""Test parsing a simple literal string."""
parser = RegexParser("hello")
ast = parser.parse()
assert ast.node_type == NodeType.SEQUENCE
assert len(ast.children) == 1
assert ast.children[0].node_type == NodeType.LITERAL
assert ast.children[0].value == "hello"
def test_parse_digit_shorthand(self):
"""Test parsing digit shorthand character class."""
parser = RegexParser("\\d+")
ast = parser.parse()
assert ast.node_type == NodeType.SEQUENCE
assert len(ast.children) == 1
quantifier = ast.children[0]
assert quantifier.node_type == NodeType.QUANTIFIER
assert quantifier.min_count == 1
def test_parse_character_class(self):
"""Test parsing a character class."""
parser = RegexParser("[a-z]")
ast = parser.parse()
assert ast.node_type == NodeType.SEQUENCE
assert len(ast.children) == 1
child = ast.children[0]
assert child.node_type == NodeType.POSITIVE_SET
assert "a" in child.characters or len(child.ranges) > 0
def test_parse_negated_character_class(self):
"""Test parsing a negated character class."""
parser = RegexParser("[^0-9]")
ast = parser.parse()
assert ast.node_type == NodeType.SEQUENCE
child = ast.children[0]
assert child.node_type == NodeType.NEGATIVE_SET
assert child.negated is True
def test_parse_plus_quantifier(self):
"""Test parsing a plus quantifier."""
parser = RegexParser("a+")
ast = parser.parse()
assert ast.node_type == NodeType.SEQUENCE
assert len(ast.children) == 1
quantifier = ast.children[0]
assert quantifier.node_type == NodeType.QUANTIFIER
assert quantifier.min_count == 1
assert quantifier.max_count == float('inf')
def test_parse_star_quantifier(self):
"""Test parsing a star quantifier."""
parser = RegexParser("b*")
ast = parser.parse()
assert ast.node_type == NodeType.SEQUENCE
assert len(ast.children) == 1
quantifier = ast.children[0]
assert quantifier.node_type == NodeType.QUANTIFIER
assert quantifier.min_count == 0
assert quantifier.max_count == float('inf')
def test_parse_question_quantifier(self):
"""Test parsing a question mark quantifier."""
parser = RegexParser("c?")
ast = parser.parse()
quantifier = ast.children[0]
assert quantifier.node_type == NodeType.QUANTIFIER
assert quantifier.min_count == 0
assert quantifier.max_count == 1
def test_parse_range_quantifier(self):
"""Test parsing a range quantifier like {2,5}."""
parser = RegexParser("a{2,5}")
ast = parser.parse()
quantifier = ast.children[0]
assert quantifier.node_type == NodeType.QUANTIFIER
assert quantifier.min_count == 2
assert quantifier.max_count == 5
def test_parse_lazy_quantifier(self):
"""Test parsing a lazy quantifier."""
parser = RegexParser("a+?")
ast = parser.parse()
quantifier = ast.children[0]
assert quantifier.node_type == NodeType.QUANTIFIER
assert quantifier.is_lazy is True
def test_parse_capturing_group(self):
"""Test parsing a capturing group."""
parser = RegexParser("(hello)")
ast = parser.parse()
assert ast.node_type == NodeType.SEQUENCE
assert len(ast.children) == 1
group = ast.children[0]
assert group.node_type == NodeType.CAPTURING_GROUP
assert not group.is_non_capturing
def test_parse_non_capturing_group(self):
"""Test parsing a non-capturing group."""
parser = RegexParser("(?:hello)")
ast = parser.parse()
group = ast.children[0]
assert group.node_type == NodeType.NON_CAPTURING_GROUP
assert group.is_non_capturing is True
def test_parse_named_group(self):
"""Test parsing a named group."""
parser = RegexParser("(?P<name>hello)")
ast = parser.parse()
group = ast.children[0]
assert group.node_type == NodeType.NAMED_GROUP
assert group.name == "name"
def test_parse_positive_lookahead(self):
"""Test parsing a positive lookahead."""
parser = RegexParser("(?=test)")
ast = parser.parse()
group = ast.children[0]
assert group.node_type == NodeType.LOOKAHEAD
def test_parse_negative_lookahead(self):
"""Test parsing a negative lookahead."""
parser = RegexParser("(?!test)")
ast = parser.parse()
group = ast.children[0]
assert group.node_type == NodeType.NEGATIVE_LOOKAHEAD
def test_parse_lookbehind(self):
"""Test parsing a lookbehind."""
parser = RegexParser("(?<=test)")
ast = parser.parse()
group = ast.children[0]
assert group.node_type == NodeType.LOOKBEHIND
def test_parse_anchor_start(self):
"""Test parsing a start anchor."""
parser = RegexParser("^start")
ast = parser.parse()
assert ast.children[0].node_type == NodeType.ANCHOR_START
def test_parse_anchor_end(self):
"""Test parsing an end anchor."""
parser = RegexParser("end$")
ast = parser.parse()
anchor = ast.children[-1]
assert anchor.node_type == NodeType.ANCHOR_END
def test_parse_word_boundary(self):
"""Test parsing a word boundary."""
parser = RegexParser("\\bword\\b")
ast = parser.parse()
assert any(child.node_type == NodeType.WORD_BOUNDARY for child in ast.children)
def test_parse_dot(self):
"""Test parsing a dot (any character)."""
parser = RegexParser(".")
ast = parser.parse()
assert ast.children[0].node_type == NodeType.DOT
def test_parse_complex_pattern(self):
"""Test parsing a complex regex pattern."""
pattern = r"^(?:http|https)://[\w.-]+\.(?:com|org|net)$"
parser = RegexParser(pattern)
ast = parser.parse()
assert ast.node_type == NodeType.SEQUENCE
assert len(ast.children) > 0
def test_parse_alternation(self):
"""Test parsing alternation with pipe."""
parser = RegexParser("cat|dog")
ast = parser.parse()
assert ast.node_type == NodeType.SEQUENCE
assert len(ast.children) >= 1
def test_parse_escaped_character(self):
"""Test parsing escaped characters."""
parser = RegexParser("\\.")
ast = parser.parse()
assert len(ast.children) > 0
def test_parse_whitespace_shorthand(self):
"""Test parsing whitespace shorthand."""
parser = RegexParser("\\s+")
ast = parser.parse()
assert ast.node_type == NodeType.SEQUENCE
quantifier = ast.children[0]
assert quantifier.node_type == NodeType.QUANTIFIER
def test_parse_word_char_shorthand(self):
"""Test parsing word character shorthand."""
parser = RegexParser("\\w*")
ast = parser.parse()
assert ast.node_type == NodeType.SEQUENCE
def test_parse_hex_escape(self):
"""Test parsing hex escape sequence."""
parser = RegexParser("\\x41")
ast = parser.parse()
assert len(ast.children) > 0
def test_parse_backreference(self):
"""Test parsing a backreference."""
parser = RegexParser("(a)\\1")
ast = parser.parse()
assert len(ast.children) > 0
def test_parse_empty_group(self):
"""Test parsing an empty group."""
parser = RegexParser("()")
ast = parser.parse()
group = ast.children[0]
assert group.node_type == NodeType.CAPTURING_GROUP
def test_parse_nested_groups(self):
"""Test parsing nested groups."""
parser = RegexParser("((a)(b))")
ast = parser.parse()
assert len(ast.children) == 1
def test_errors_empty(self):
"""Test that valid patterns have no errors."""
parser = RegexParser("hello")
parser.parse()
assert len(parser.get_errors()) == 0
def test_parse_email_pattern(self):
"""Test parsing a typical email regex pattern."""
pattern = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
parser = RegexParser(pattern)
ast = parser.parse()
assert ast.node_type == NodeType.SEQUENCE
assert len(ast.children) > 0
def test_parse_phone_pattern(self):
"""Test parsing a phone number regex."""
pattern = r"\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}"
parser = RegexParser(pattern)
ast = parser.parse()
assert ast.node_type == NodeType.SEQUENCE
def test_parse_date_pattern(self):
"""Test parsing a date regex."""
pattern = r"\d{4}-\d{2}-\d{2}"
parser = RegexParser(pattern)
ast = parser.parse()
assert ast.node_type == NodeType.SEQUENCE
class TestNodeTypes:
"""Test node type enumeration."""
def test_node_type_values(self):
"""Test that all expected node types exist."""
expected_types = [
"LITERAL", "CHARACTER_CLASS", "POSITIVE_SET", "NEGATIVE_SET",
"DOT", "GROUP", "CAPTURING_GROUP", "NON_CAPTURING_GROUP",
"NAMED_GROUP", "LOOKAHEAD", "LOOKBEHIND", "NEGATIVE_LOOKAHEAD",
"NEGATIVE_LOOKBEHIND", "QUANTIFIER", "ANCHOR_START", "ANCHOR_END",
"WORD_BOUNDARY", "START_OF_STRING", "END_OF_STRING", "DIGIT",
"NON_DIGIT", "WORD_CHAR", "WHITESPACE", "BACKREFERENCE",
]
for type_name in expected_types:
assert hasattr(NodeType, type_name), f"Missing node type: {type_name}"

241
tests/test_parsers.py Normal file
View File

@@ -0,0 +1,241 @@
"""Tests for dependency parsers."""
import tempfile
from pathlib import Path
from src.auto_readme.parsers import (
DependencyParserFactory,
GoDependencyParser,
JavaScriptDependencyParser,
PythonDependencyParser,
RustDependencyParser,
)
class TestPythonDependencyParser:
"""Tests for PythonDependencyParser."""
def test_can_parse_requirements_txt(self):
"""Test that parser recognizes requirements.txt."""
parser = PythonDependencyParser()
with tempfile.TemporaryDirectory() as tmp_dir:
req_file = Path(tmp_dir) / "requirements.txt"
req_file.write_text("requests>=2.31.0\n")
assert parser.can_parse(req_file)
def test_can_parse_pyproject_toml(self):
"""Test that parser recognizes pyproject.toml."""
parser = PythonDependencyParser()
with tempfile.TemporaryDirectory() as tmp_dir:
pyproject_file = Path(tmp_dir) / "pyproject.toml"
pyproject_file.write_text('[project]\ndependencies = ["requests>=2.0"]')
assert parser.can_parse(pyproject_file)
def test_parse_requirements_txt(self):
"""Test parsing requirements.txt file."""
with tempfile.TemporaryDirectory() as tmp_dir:
req_file = Path(tmp_dir) / "requirements.txt"
req_file.write_text("""
requests>=2.31.0
click>=8.0.0
pytest==7.0.0
-e git+https://github.com/user/repo.git#egg=package
# This is a comment
numpy~=1.24.0
""")
parser = PythonDependencyParser()
deps = parser.parse(req_file)
assert len(deps) == 5
names = {d.name for d in deps}
assert "requests" in names
assert "click" in names
assert "pytest" in names
assert "numpy" in names
def test_parse_pyproject_toml(self):
"""Test parsing pyproject.toml file."""
with tempfile.TemporaryDirectory() as tmp_dir:
pyproject_file = Path(tmp_dir) / "pyproject.toml"
pyproject_file.write_text("""
[project]
name = "test-project"
version = "0.1.0"
dependencies = [
"requests>=2.31.0",
"click>=8.0.0",
]
[project.optional-dependencies]
dev = ["pytest>=7.0.0", "black>=23.0.0"]
""")
parser = PythonDependencyParser()
deps = parser.parse(pyproject_file)
assert len(deps) == 4
class TestJavaScriptDependencyParser:
"""Tests for JavaScriptDependencyParser."""
def test_can_parse_package_json(self):
"""Test that parser recognizes package.json."""
parser = JavaScriptDependencyParser()
with tempfile.TemporaryDirectory() as tmp_dir:
package_file = Path(tmp_dir) / "package.json"
package_file.write_text('{"name": "test"}')
assert parser.can_parse(package_file)
def test_parse_package_json(self):
"""Test parsing package.json file."""
with tempfile.TemporaryDirectory() as tmp_dir:
package_file = Path(tmp_dir) / "package.json"
package_file.write_text("""
{
"name": "test-project",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.0",
"lodash": "~4.17.0"
},
"devDependencies": {
"jest": "^29.0.0"
},
"optionalDependencies": {
"bcrypt": "^5.0.0"
}
}
""")
parser = JavaScriptDependencyParser()
deps = parser.parse(package_file)
assert len(deps) == 4
express = next((d for d in deps if d.name == "express"), None)
assert express is not None
assert express.version == "4.18.0"
assert not express.is_dev
jest = next((d for d in deps if d.name == "jest"), None)
assert jest is not None
assert jest.is_dev
bcrypt = next((d for d in deps if d.name == "bcrypt"), None)
assert bcrypt is not None
assert bcrypt.is_optional
class TestGoDependencyParser:
"""Tests for GoDependencyParser."""
def test_can_parse_go_mod(self):
"""Test that parser recognizes go.mod."""
parser = GoDependencyParser()
with tempfile.TemporaryDirectory() as tmp_dir:
go_mod_file = Path(tmp_dir) / "go.mod"
go_mod_file.write_text("module test\n")
assert parser.can_parse(go_mod_file)
def test_parse_go_mod(self):
"""Test parsing go.mod file."""
with tempfile.TemporaryDirectory() as tmp_dir:
go_mod_file = Path(tmp_dir) / "go.mod"
go_mod_file.write_text("""
module test-go-project
go 1.21
require (
github.com/gin-gonic/gin v1.9.0
github.com/stretchr/testify v1.8.0
)
require github.com/user/repo v1.0.0
""")
parser = GoDependencyParser()
deps = parser.parse(go_mod_file)
assert len(deps) == 3
gin = next((d for d in deps if d.name == "github.com/gin-gonic/gin"), None)
assert gin is not None
assert gin.version is not None and "1.9.0" in gin.version
class TestRustDependencyParser:
"""Tests for RustDependencyParser."""
def test_can_parse_cargo_toml(self):
"""Test that parser recognizes Cargo.toml."""
parser = RustDependencyParser()
with tempfile.TemporaryDirectory() as tmp_dir:
cargo_file = Path(tmp_dir) / "Cargo.toml"
cargo_file.write_text('[package]\nname = "test"')
assert parser.can_parse(cargo_file)
def test_parse_cargo_toml(self):
"""Test parsing Cargo.toml file."""
with tempfile.TemporaryDirectory() as tmp_dir:
cargo_file = Path(tmp_dir) / "Cargo.toml"
cargo_file.write_text("""
[package]
name = "test-project"
version = "0.1.0"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
[dev-dependencies]
assertions = "0.1"
""")
parser = RustDependencyParser()
deps = parser.parse(cargo_file)
assert len(deps) == 3
serde = next((d for d in deps if d.name == "serde"), None)
assert serde is not None
assert serde.version is not None and "1.0" in serde.version
assert not serde.is_dev
assertions = next((d for d in deps if d.name == "assertions"), None)
assert assertions is not None
assert assertions.is_dev
class TestDependencyParserFactory:
"""Tests for DependencyParserFactory."""
def test_get_parser_python(self):
"""Test getting parser for Python file."""
parser = DependencyParserFactory.get_parser(Path("requirements.txt"))
assert isinstance(parser, PythonDependencyParser)
def test_get_parser_js(self):
"""Test getting parser for JavaScript file."""
parser = DependencyParserFactory.get_parser(Path("package.json"))
assert isinstance(parser, JavaScriptDependencyParser)
def test_get_parser_go(self):
"""Test getting parser for Go file."""
parser = DependencyParserFactory.get_parser(Path("go.mod"))
assert isinstance(parser, GoDependencyParser)
def test_get_parser_rust(self):
"""Test getting parser for Rust file."""
parser = DependencyParserFactory.get_parser(Path("Cargo.toml"))
assert isinstance(parser, RustDependencyParser)
def test_can_parse(self):
"""Test can_parse returns correct results."""
assert DependencyParserFactory.can_parse(Path("requirements.txt"))
assert DependencyParserFactory.can_parse(Path("package.json"))
assert DependencyParserFactory.can_parse(Path("go.mod"))
assert DependencyParserFactory.can_parse(Path("Cargo.toml"))
assert not DependencyParserFactory.can_parse(Path("random.txt"))

99
tests/test_server.py Normal file
View File

@@ -0,0 +1,99 @@
"""Tests for MCP server."""
import pytest
from mcp_server_cli.models import MCPMethod, MCPRequest
from mcp_server_cli.server import MCPServer
from mcp_server_cli.tools import FileTools, GitTools, ShellTools
@pytest.fixture
def mcp_server():
"""Create an MCP server with registered tools."""
server = MCPServer()
server.register_tool(FileTools())
server.register_tool(GitTools())
server.register_tool(ShellTools())
return server
class TestMCPServer:
"""Tests for MCP server class."""
def test_server_creation(self):
"""Test server creation."""
server = MCPServer()
assert server.connection_state.value == "disconnected"
assert len(server.tool_registry) == 0
def test_register_tool(self, mcp_server):
"""Test tool registration."""
assert "file_tools" in mcp_server.tool_registry
assert len(mcp_server.list_tools()) == 3
def test_get_tool(self, mcp_server):
"""Test getting a registered tool."""
retrieved = mcp_server.get_tool("file_tools")
assert retrieved is not None
assert retrieved.name == "file_tools"
def test_list_tools(self, mcp_server):
"""Test listing all tools."""
tools = mcp_server.list_tools()
assert len(tools) >= 2
names = [t.name for t in tools]
assert "file_tools" in names
assert "git_tools" in names
class TestMCPProtocol:
"""Tests for MCP protocol implementation."""
def test_mcp_initialize(self, mcp_server):
"""Test MCP initialize request."""
request = MCPRequest(
id=1,
method=MCPMethod.INITIALIZE,
params={"protocol_version": "2024-11-05"},
)
import asyncio
response = asyncio.run(mcp_server.handle_request(request))
assert response.id == 1
assert response.result is not None
def test_mcp_tools_list(self, mcp_server):
"""Test MCP tools/list request."""
request = MCPRequest(
id=2,
method=MCPMethod.TOOLS_LIST,
)
import asyncio
response = asyncio.run(mcp_server.handle_request(request))
assert response.id == 2
assert response.result is not None
def test_mcp_invalid_method(self, mcp_server):
"""Test MCP request with invalid tool."""
request = MCPRequest(
id=3,
method=MCPMethod.TOOLS_CALL,
params={"name": "nonexistent"},
)
import asyncio
response = asyncio.run(mcp_server.handle_request(request))
assert response.error is not None or response.result.get("is_error") is True
class TestToolCall:
"""Tests for tool calling."""
def test_call_read_file_nonexistent(self, mcp_server):
"""Test calling read on nonexistent file."""
from mcp_server_cli.models import ToolCallParams
params = ToolCallParams(
name="read_file",
arguments={"path": "/nonexistent/file.txt"},
)
import asyncio
result = asyncio.run(mcp_server._handle_tool_call(params))
assert result.is_error is True

145
tests/test_templates.py Normal file
View File

@@ -0,0 +1,145 @@
"""Tests for template engine."""
import tempfile
from pathlib import Path
import pytest
from project_scaffold_cli.template_engine import TemplateEngine
class TestTemplateEngine:
"""Test TemplateEngine class."""
def test_engine_initialization(self):
"""Test engine can be initialized."""
engine = TemplateEngine()
assert engine is not None
assert engine.SUPPORTED_LANGUAGES == ["python", "nodejs", "go", "rust"]
def test_render_language_template_python(self):
"""Test rendering Python template."""
engine = TemplateEngine()
context = {
"project_name": "test-project",
"project_slug": "test-project",
"author": "Test Author",
"email": "test@example.com",
"description": "A test project",
"license": "MIT",
"year": "2024",
"language": "python",
}
with tempfile.TemporaryDirectory() as tmpdir:
output_dir = Path(tmpdir) / "test-project"
output_dir.mkdir()
engine.render_language_template("python", context, output_dir)
assert (output_dir / "setup.py").exists()
assert (output_dir / "README.md").exists()
def test_render_language_template_go(self):
"""Test rendering Go template."""
engine = TemplateEngine()
context = {
"project_name": "test-go-project",
"project_slug": "test-go-project",
"author": "Test Author",
"email": "test@example.com",
"description": "A test Go project",
"license": "MIT",
"year": "2024",
"language": "go",
}
with tempfile.TemporaryDirectory() as tmpdir:
output_dir = Path(tmpdir) / "test-go-project"
output_dir.mkdir()
engine.render_language_template("go", context, output_dir)
assert (output_dir / "go.mod").exists()
assert (output_dir / "main.go").exists()
assert (output_dir / "README.md").exists()
def test_render_language_template_rust(self):
"""Test rendering Rust template."""
engine = TemplateEngine()
context = {
"project_name": "test-rust-project",
"project_slug": "test-rust-project",
"author": "Test Author",
"email": "test@example.com",
"description": "A test Rust project",
"license": "MIT",
"year": "2024",
"language": "rust",
}
with tempfile.TemporaryDirectory() as tmpdir:
output_dir = Path(tmpdir) / "test-rust-project"
output_dir.mkdir()
engine.render_language_template("rust", context, output_dir)
assert (output_dir / "Cargo.toml").exists()
assert (output_dir / "src" / "main.rs").exists()
assert (output_dir / "README.md").exists()
def test_render_language_template_unsupported(self):
"""Test rendering unsupported language."""
engine = TemplateEngine()
context = {"project_name": "test"}
with tempfile.TemporaryDirectory() as tmpdir:
with pytest.raises(ValueError) as exc_info:
engine.render_language_template(
"unsupported", context, Path(tmpdir)
)
assert "Unsupported language" in str(exc_info.value)
def test_render_ci_template_github(self):
"""Test rendering GitHub Actions CI template."""
engine = TemplateEngine()
context = {
"project_name": "test-project",
"project_slug": "test-project",
}
with tempfile.TemporaryDirectory() as tmpdir:
output_dir = Path(tmpdir)
engine.render_ci_template("github", context, output_dir)
workflow_path = (
output_dir / ".github" / "workflows" / "ci.yml"
)
assert workflow_path.exists()
content = workflow_path.read_text()
assert "CI" in content
def test_render_ci_template_gitlab(self):
"""Test rendering GitLab CI template."""
engine = TemplateEngine()
context = {
"project_name": "test-project",
"project_slug": "test-project",
}
with tempfile.TemporaryDirectory() as tmpdir:
output_dir = Path(tmpdir)
engine.render_ci_template("gitlab", context, output_dir)
assert (output_dir / ".gitlab-ci.yml").exists()
def test_validate_context_missing_required(self):
"""Test context validation with missing required fields."""
engine = TemplateEngine()
missing = engine.validate_context({})
assert "project_name" in missing
assert "author" in missing
def test_validate_context_complete(self):
"""Test context validation with all required fields."""
engine = TemplateEngine()
context = {"project_name": "test", "author": "Test Author"}
missing = engine.validate_context(context)
assert len(missing) == 0

321
tests/test_tools.py Normal file
View File

@@ -0,0 +1,321 @@
"""Tests for tool execution engine and built-in tools."""
from pathlib import Path
import pytest
from mcp_server_cli.models import ToolParameter, ToolSchema
from mcp_server_cli.tools.base import ToolBase, ToolRegistry, ToolResult
from mcp_server_cli.tools.file_tools import (
GlobFilesTool,
ListDirectoryTool,
ReadFileTool,
WriteFileTool,
)
from mcp_server_cli.tools.git_tools import GitTools
from mcp_server_cli.tools.shell_tools import ExecuteCommandTool
class TestToolBase:
"""Tests for ToolBase abstract class."""
def test_tool_validation(self):
"""Test tool argument validation."""
class TestTool(ToolBase):
def __init__(self):
super().__init__(
name="test_tool",
description="A test tool",
)
def _create_input_schema(self) -> ToolSchema:
return ToolSchema(
properties={
"name": ToolParameter(
name="name",
type="string",
required=True,
),
"count": ToolParameter(
name="count",
type="integer",
enum=["1", "2", "3"],
),
},
required=["name"],
)
async def execute(self, arguments) -> ToolResult:
return ToolResult(success=True, output="OK")
tool = TestTool()
result = tool.validate_arguments({"name": "test"})
assert result["name"] == "test"
def test_missing_required_param(self):
"""Test that missing required parameters raise error."""
class TestTool(ToolBase):
def __init__(self):
super().__init__(name="test_tool", description="A test tool")
def _create_input_schema(self) -> ToolSchema:
return ToolSchema(
properties={
"required_param": ToolParameter(
name="required_param",
type="string",
required=True,
),
},
required=["required_param"],
)
async def execute(self, arguments) -> ToolResult:
return ToolResult(success=True, output="OK")
tool = TestTool()
with pytest.raises(ValueError, match="Missing required parameter"):
tool.validate_arguments({})
def test_invalid_enum_value(self):
"""Test that invalid enum values raise error."""
class TestTool(ToolBase):
def __init__(self):
super().__init__(name="test_tool", description="A test tool")
def _create_input_schema(self) -> ToolSchema:
return ToolSchema(
properties={
"color": ToolParameter(
name="color",
type="string",
enum=["red", "green", "blue"],
),
},
)
async def execute(self, arguments) -> ToolResult:
return ToolResult(success=True, output="OK")
tool = TestTool()
with pytest.raises(ValueError, match="Invalid value"):
tool.validate_arguments({"color": "yellow"})
class TestToolRegistry:
"""Tests for ToolRegistry."""
def test_register_and_get(self, tool_registry: ToolRegistry):
"""Test registering and retrieving a tool."""
class TestTool(ToolBase):
def __init__(self):
super().__init__(name="test_tool", description="A test tool")
def _create_input_schema(self) -> ToolSchema:
return ToolSchema(properties={}, required=[])
async def execute(self, arguments) -> ToolResult:
return ToolResult(success=True, output="OK")
tool = TestTool()
tool_registry.register(tool)
retrieved = tool_registry.get("test_tool")
assert retrieved is tool
assert retrieved.name == "test_tool"
def test_unregister(self, tool_registry: ToolRegistry):
"""Test unregistering a tool."""
class TestTool(ToolBase):
def __init__(self):
super().__init__(name="test_tool", description="A test tool")
def _create_input_schema(self) -> ToolSchema:
return ToolSchema(properties={}, required=[])
async def execute(self, arguments) -> ToolResult:
return ToolResult(success=True, output="OK")
tool = TestTool()
tool_registry.register(tool)
assert tool_registry.unregister("test_tool") is True
assert tool_registry.get("test_tool") is None
def test_list_tools(self, tool_registry: ToolRegistry):
"""Test listing all tools."""
class TestTool1(ToolBase):
def __init__(self):
super().__init__(name="tool1", description="Tool 1")
def _create_input_schema(self) -> ToolSchema:
return ToolSchema(properties={}, required=[])
async def execute(self, arguments) -> ToolResult:
return ToolResult(success=True, output="OK")
class TestTool2(ToolBase):
def __init__(self):
super().__init__(name="tool2", description="Tool 2")
def _create_input_schema(self) -> ToolSchema:
return ToolSchema(properties={}, required=[])
async def execute(self, arguments) -> ToolResult:
return ToolResult(success=True, output="OK")
tool_registry.register(TestTool1())
tool_registry.register(TestTool2())
tools = tool_registry.list()
assert len(tools) == 2
assert tool_registry.list_names() == ["tool1", "tool2"]
class TestFileTools:
"""Tests for file operation tools."""
@pytest.mark.asyncio
async def test_read_file(self, temp_file: Path):
"""Test reading a file."""
tool = ReadFileTool()
result = await tool.execute({"path": str(temp_file)})
assert result.success is True
assert "Hello, World!" in result.output
@pytest.mark.asyncio
async def test_read_nonexistent_file(self, temp_dir: Path):
"""Test reading a nonexistent file."""
tool = ReadFileTool()
result = await tool.execute({"path": str(temp_dir / "nonexistent.txt")})
assert result.success is False
assert "not found" in result.error.lower()
@pytest.mark.asyncio
async def test_write_file(self, temp_dir: Path):
"""Test writing a file."""
tool = WriteFileTool()
result = await tool.execute({
"path": str(temp_dir / "new_file.txt"),
"content": "New content",
})
assert result.success is True
assert (temp_dir / "new_file.txt").read_text() == "New content"
@pytest.mark.asyncio
async def test_list_directory(self, temp_dir: Path):
"""Test listing a directory."""
tool = ListDirectoryTool()
result = await tool.execute({"path": str(temp_dir)})
assert result.success is True
@pytest.mark.asyncio
async def test_glob_files(self, temp_dir: Path):
"""Test glob file search."""
(temp_dir / "test1.txt").touch()
(temp_dir / "test2.txt").touch()
(temp_dir / "subdir").mkdir()
(temp_dir / "subdir" / "test3.txt").touch()
tool = GlobFilesTool()
result = await tool.execute({
"path": str(temp_dir),
"pattern": "**/*.txt",
})
assert result.success is True
assert "test1.txt" in result.output
assert "test3.txt" in result.output
class TestShellTools:
"""Tests for shell execution tools."""
@pytest.mark.asyncio
async def test_execute_ls(self):
"""Test executing ls command."""
tool = ExecuteCommandTool()
result = await tool.execute({
"cmd": ["ls", "-1"],
"timeout": 10,
})
assert result.success is True
@pytest.mark.asyncio
async def test_execute_with_cwd(self, temp_dir: Path):
"""Test executing command with working directory."""
tool = ExecuteCommandTool()
result = await tool.execute({
"cmd": ["pwd"],
"cwd": str(temp_dir),
})
assert result.success is True
@pytest.mark.asyncio
async def test_execute_nonexistent_command(self):
"""Test executing nonexistent command."""
tool = ExecuteCommandTool()
result = await tool.execute({
"cmd": ["nonexistent_command_12345"],
})
assert result.success is False
assert "no such file" in result.error.lower()
@pytest.mark.asyncio
async def test_command_timeout(self):
"""Test command timeout."""
tool = ExecuteCommandTool()
result = await tool.execute({
"cmd": ["sleep", "10"],
"timeout": 1,
})
assert result.success is False
assert "timed out" in result.error.lower()
class TestGitTools:
"""Tests for git tools."""
@pytest.mark.asyncio
async def test_git_status_not_in_repo(self, temp_dir: Path):
"""Test git status in non-git directory."""
tool = GitTools()
result = await tool.execute({
"operation": "status",
"path": str(temp_dir),
})
assert result.success is False
assert "not in a git repository" in result.error.lower()
class TestToolResult:
"""Tests for ToolResult model."""
def test_success_result(self):
"""Test creating a success result."""
result = ToolResult(success=True, output="Test output")
assert result.success is True
assert result.output == "Test output"
assert result.error is None
def test_error_result(self):
"""Test creating an error result."""
result = ToolResult(
success=False,
output="",
error="Something went wrong",
)
assert result.success is False
assert result.error == "Something went wrong"

221
tests/test_translator.py Normal file
View File

@@ -0,0 +1,221 @@
"""Tests for the regex translator."""
import pytest
from regex_humanizer.translator import RegexTranslator, translate_regex
class TestRegexTranslator:
"""Test cases for RegexTranslator."""
def test_translate_simple_literal(self):
"""Test translating a simple literal."""
result = translate_regex("hello")
assert "hello" in result.lower() or "hello" in result
def test_translate_character_class(self):
"""Test translating a character class."""
result = translate_regex("[a-z]")
assert "any" in result.lower() and ("a" in result.lower() or "z" in result.lower())
def test_translate_negated_class(self):
"""Test translating a negated character class."""
result = translate_regex("[^0-9]")
assert "except" in result.lower() or "not" in result.lower()
def test_translate_digit_class(self):
"""Test translating digit shorthand."""
result = translate_regex("\\d")
assert "digit" in result.lower()
def test_translate_non_digit_class(self):
"""Test translating non-digit shorthand."""
result = translate_regex("\\D")
assert "non-digit" in result.lower() or "non digit" in result.lower()
def test_translate_word_class(self):
"""Test translating word character shorthand."""
result = translate_regex("\\w")
assert "word" in result.lower()
def test_translate_whitespace_class(self):
"""Test translating whitespace shorthand."""
result = translate_regex("\\s")
assert "whitespace" in result.lower()
def test_translate_plus_quantifier(self):
"""Test translating plus quantifier."""
result = translate_regex("a+")
assert "one or more" in result.lower() or "1 or more" in result.lower()
def test_translate_star_quantifier(self):
"""Test translating star quantifier."""
result = translate_regex("b*")
assert "zero or more" in result.lower() or "0 or more" in result.lower()
def test_translate_question_quantifier(self):
"""Test translating question mark quantifier."""
result = translate_regex("c?")
assert "optional" in result.lower() or "0 or 1" in result.lower()
def test_translate_range_quantifier(self):
"""Test translating range quantifier."""
result = translate_regex("a{2,5}")
assert "between" in result.lower() and ("2" in result or "2" in result.lower())
def test_translate_exact_quantifier(self):
"""Test translating exact count quantifier."""
result = translate_regex("x{3}")
assert "exactly" in result.lower() and "3" in result
def test_translate_lazy_quantifier(self):
"""Test translating lazy quantifier."""
result = translate_regex(".+?")
assert "lazy" in result.lower()
def test_translate_capturing_group(self):
"""Test translating a capturing group."""
result = translate_regex("(test)")
assert "capturing" in result.lower() or "group" in result.lower()
def test_translate_non_capturing_group(self):
"""Test translating a non-capturing group."""
result = translate_regex("(?:test)")
assert "non-capturing" in result.lower()
def test_translate_named_group(self):
"""Test translating a named group."""
result = translate_regex("(?P<name>test)")
assert "name" in result.lower() or "named" in result.lower()
def test_translate_lookahead(self):
"""Test translating a positive lookahead."""
result = translate_regex("(?=test)")
assert "followed" in result.lower()
def test_translate_negative_lookahead(self):
"""Test translating a negative lookahead."""
result = translate_regex("(?!test)")
assert "not followed" in result.lower() or "not" in result.lower()
def test_translate_lookbehind(self):
"""Test translating a lookbehind."""
result = translate_regex("(?<=test)")
assert "preceded" in result.lower()
def test_translate_negative_lookbehind(self):
"""Test translating a negative lookbehind."""
result = translate_regex("(?<!test)")
assert "not preceded" in result.lower()
def test_translate_anchor_start(self):
"""Test translating a start anchor."""
result = translate_regex("^start")
assert "start" in result.lower()
def test_translate_anchor_end(self):
"""Test translating an end anchor."""
result = translate_regex("end$")
assert "end" in result.lower()
def test_translate_word_boundary(self):
"""Test translating a word boundary."""
result = translate_regex("\\bword\\b")
assert "word boundary" in result.lower()
def test_translate_dot(self):
"""Test translating a dot."""
result = translate_regex(".")
assert "any" in result.lower() and "character" in result.lower()
def test_translate_alternation(self):
"""Test translating alternation."""
result = translate_regex("cat|dog")
assert "cat" in result.lower() or "dog" in result.lower()
def test_translate_complex_pattern(self):
"""Test translating a complex pattern."""
pattern = r"^\d{3}-\d{4}$"
result = translate_regex(pattern)
assert len(result) > 0
assert "digit" in result.lower() or "3" in result
def test_translate_email_pattern(self):
"""Test translating an email pattern."""
pattern = r"[a-z]+@[a-z]+\.[a-z]+"
result = translate_regex(pattern)
assert "any" in result.lower() or "@" in result
def test_translate_url_pattern(self):
"""Test translating a URL pattern."""
pattern = r"https?://[^\s]+"
result = translate_regex(pattern)
assert len(result) > 0
def test_translate_escaped_char(self):
"""Test translating an escaped character."""
result = translate_regex("\\.")
assert "." in result or "period" in result.lower()
def test_translate_whitespace_literal(self):
"""Test translating a literal space."""
result = translate_regex(" ")
assert "space" in result.lower()
def test_translate_tab_literal(self):
"""Test translating a tab character."""
result = translate_regex("\\t")
assert "escape" in result.lower() or "\\t" in result or "tab" in result.lower()
def test_translate_backreference(self):
"""Test translating a backreference."""
result = translate_regex(r"(a)\1")
assert "same as" in result.lower() or "capture" in result.lower() or "\\1" in result
def test_translate_empty_pattern(self):
"""Test translating an empty pattern."""
result = translate_regex("")
assert len(result) >= 0
def test_translate_with_js_flavor(self):
"""Test translating with JavaScript flavor."""
result = translate_regex("test", flavor="javascript")
assert len(result) > 0
def test_translate_with_python_flavor(self):
"""Test translating with Python flavor."""
result = translate_regex("test", flavor="python")
assert len(result) > 0
class TestRegexTranslatorClass:
"""Test the RegexTranslator class directly."""
def test_translator_initialization(self):
"""Test translator initialization with default flavor."""
translator = RegexTranslator()
assert translator.flavor == "pcre"
def test_translator_custom_flavor(self):
"""Test translator initialization with custom flavor."""
translator = RegexTranslator("javascript")
assert translator.flavor == "javascript"
def test_translate_returns_string(self):
"""Test that translate returns a string."""
translator = RegexTranslator()
result = translator.translate("test")
assert isinstance(result, str)
def test_translate_empty_sequence(self):
"""Test translating an empty sequence node."""
from regex_humanizer.parser import RegexParser, NodeType, RegexNode
parser = RegexParser("")
ast = parser.parse()
translator = RegexTranslator()
result = translator._translate_node(ast)
assert isinstance(result, str)

131
tests/test_utils.py Normal file
View File

@@ -0,0 +1,131 @@
"""Tests for utility modules."""
import tempfile
from pathlib import Path
from src.auto_readme.utils.file_scanner import FileScanner
from src.auto_readme.utils.path_utils import PathUtils
class TestPathUtils:
"""Tests for PathUtils."""
def test_normalize_path(self):
"""Test path normalization."""
path = PathUtils.normalize_path("./foo/bar")
assert path.is_absolute()
def test_is_ignored_dir(self):
"""Test ignored directory detection."""
assert PathUtils.is_ignored_dir(Path("__pycache__"))
assert PathUtils.is_ignored_dir(Path(".git"))
assert PathUtils.is_ignored_dir(Path("node_modules"))
assert not PathUtils.is_ignored_dir(Path("src"))
def test_is_ignored_file(self):
"""Test ignored file detection."""
assert PathUtils.is_ignored_file(Path(".gitignore"))
assert not PathUtils.is_ignored_file(Path("main.py"))
def test_is_source_file(self):
"""Test source file detection."""
assert PathUtils.is_source_file(Path("main.py"))
assert PathUtils.is_source_file(Path("index.js"))
assert PathUtils.is_source_file(Path("main.go"))
assert PathUtils.is_source_file(Path("lib.rs"))
assert not PathUtils.is_source_file(Path("README.md"))
def test_is_config_file(self):
"""Test config file detection."""
assert PathUtils.is_config_file(Path("config.json"))
assert PathUtils.is_config_file(Path("settings.yaml"))
assert PathUtils.is_config_file(Path("pyproject.toml"))
def test_is_test_file(self):
"""Test test file detection."""
assert PathUtils.is_test_file(Path("test_main.py"))
assert PathUtils.is_test_file(Path("tests/utils.py"))
assert PathUtils.is_test_file(Path("example.test.js"))
assert not PathUtils.is_test_file(Path("main.py"))
def test_is_hidden(self):
"""Test hidden file detection."""
assert PathUtils.is_hidden(Path(".gitignore"))
assert not PathUtils.is_hidden(Path("main.py"))
def test_detect_project_root(self):
"""Test project root detection."""
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
subdir = root / "src" / "package"
subdir.mkdir(parents=True)
(root / "pyproject.toml").touch()
detected = PathUtils.detect_project_root(subdir)
assert detected == root
def test_get_relative_path(self):
"""Test relative path calculation."""
base = Path("/home/user/project")
target = Path("/home/user/project/src/main.py")
relative = PathUtils.get_relative_path(target, base)
assert relative == Path("src/main.py")
class TestFileScanner:
"""Tests for FileScanner."""
def test_scan_python_project(self, create_python_project):
"""Test scanning a Python project."""
scanner = FileScanner(create_python_project)
files = scanner.scan_and_create()
file_names = {f.path.name for f in files}
assert "main.py" in file_names
assert "__init__.py" in file_names
assert "requirements.txt" in file_names
assert "pyproject.toml" in file_names
def test_detect_project_type_python(self, create_python_project):
"""Test project type detection for Python."""
from src.auto_readme.models import ProjectType
scanner = FileScanner(create_python_project)
project_type = scanner.detect_project_type()
assert project_type == ProjectType.PYTHON
def test_detect_project_type_javascript(self, create_javascript_project):
"""Test project type detection for JavaScript."""
from src.auto_readme.models import ProjectType
scanner = FileScanner(create_javascript_project)
project_type = scanner.detect_project_type()
assert project_type == ProjectType.JAVASCRIPT
def test_detect_project_type_go(self, create_go_project):
"""Test project type detection for Go."""
from src.auto_readme.models import ProjectType
scanner = FileScanner(create_go_project)
project_type = scanner.detect_project_type()
assert project_type == ProjectType.GO
def test_detect_project_type_rust(self, create_rust_project):
"""Test project type detection for Rust."""
from src.auto_readme.models import ProjectType
scanner = FileScanner(create_rust_project)
project_type = scanner.detect_project_type()
assert project_type == ProjectType.RUST
def test_scan_mixed_project(self, create_mixed_project):
"""Test scanning a project with multiple languages."""
scanner = FileScanner(create_mixed_project)
files = scanner.scan_and_create()
assert len(files) > 0