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:
94
.env.example
Normal file
94
.env.example
Normal file
@@ -0,0 +1,94 @@
|
||||
# 7000%AUTO Environment Variables
|
||||
# Copy this file to .env and fill in your values
|
||||
# ALL OpenCode settings are REQUIRED - the app will not start without them!
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Application Settings
|
||||
# -----------------------------------------------------------------------------
|
||||
APP_NAME=7000%AUTO
|
||||
DEBUG=true
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# OpenCode AI Settings (ALL REQUIRED - no defaults!)
|
||||
# -----------------------------------------------------------------------------
|
||||
# The application will NOT start if any of these are missing.
|
||||
#
|
||||
# Examples for different providers:
|
||||
#
|
||||
# MiniMax (Anthropic-compatible):
|
||||
# OPENCODE_API_KEY=your-minimax-key
|
||||
# OPENCODE_API_BASE=https://api.minimax.io/anthropic/v1
|
||||
# OPENCODE_SDK=@ai-sdk/anthropic
|
||||
# OPENCODE_MODEL=MiniMax-M2.1
|
||||
# OPENCODE_MAX_TOKENS=196608
|
||||
#
|
||||
# Claude (Anthropic):
|
||||
# OPENCODE_API_KEY=your-anthropic-key
|
||||
# OPENCODE_API_BASE=https://api.anthropic.com
|
||||
# OPENCODE_SDK=@ai-sdk/anthropic
|
||||
# OPENCODE_MODEL=claude-sonnet-4-5
|
||||
# OPENCODE_MAX_TOKENS=196608
|
||||
#
|
||||
# OpenAI:
|
||||
# OPENCODE_API_KEY=your-openai-key
|
||||
# OPENCODE_API_BASE=https://api.openai.com/v1
|
||||
# OPENCODE_SDK=@ai-sdk/openai
|
||||
# OPENCODE_MODEL=gpt-5.2
|
||||
# OPENCODE_MAX_TOKENS=196608
|
||||
#
|
||||
# Together (OpenAI-compatible):
|
||||
# OPENCODE_API_KEY=your-together-key
|
||||
# OPENCODE_API_BASE=https://api.together.xyz/v1
|
||||
# OPENCODE_SDK=@ai-sdk/openai
|
||||
# OPENCODE_MODEL=meta-llama/Llama-3.1-70B-Instruct-Turbo
|
||||
# OPENCODE_MAX_TOKENS=8192
|
||||
#
|
||||
# Groq (OpenAI-compatible):
|
||||
# OPENCODE_API_KEY=your-groq-key
|
||||
# OPENCODE_API_BASE=https://api.groq.com/openai/v1
|
||||
# OPENCODE_SDK=@ai-sdk/openai
|
||||
# OPENCODE_MODEL=llama-3.1-70b-versatile
|
||||
# OPENCODE_MAX_TOKENS=8000
|
||||
|
||||
# API Key (REQUIRED)
|
||||
OPENCODE_API_KEY=your-api-key-here
|
||||
|
||||
# API Base URL (REQUIRED)
|
||||
OPENCODE_API_BASE=https://api.minimax.io/anthropic/v1
|
||||
|
||||
# AI SDK npm package (REQUIRED)
|
||||
# Use @ai-sdk/anthropic for Anthropic-compatible APIs (Claude, MiniMax)
|
||||
# Use @ai-sdk/openai for OpenAI-compatible APIs (OpenAI, Together, Groq)
|
||||
OPENCODE_SDK=@ai-sdk/anthropic
|
||||
|
||||
# Model name (REQUIRED)
|
||||
OPENCODE_MODEL=MiniMax-M2.1
|
||||
|
||||
# Maximum output tokens (REQUIRED)
|
||||
OPENCODE_MAX_TOKENS=196608
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Gitea Settings (Required for uploading)
|
||||
# -----------------------------------------------------------------------------
|
||||
GITEA_TOKEN=your-gitea-token-here
|
||||
GITEA_USERNAME=your-gitea-username
|
||||
GITEA_URL=your-gitea-instance-url
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# X (Twitter) API Settings (Required for posting)
|
||||
# -----------------------------------------------------------------------------
|
||||
X_API_KEY=your-x-api-key
|
||||
X_API_SECRET=your-x-api-secret
|
||||
X_ACCESS_TOKEN=your-x-access-token
|
||||
X_ACCESS_TOKEN_SECRET=your-x-access-token-secret
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Optional Settings (have sensible defaults)
|
||||
# -----------------------------------------------------------------------------
|
||||
# DATABASE_URL=sqlite+aiosqlite:///./data/7000auto.db
|
||||
# HOST=0.0.0.0
|
||||
# PORT=8000
|
||||
# AUTO_START=true
|
||||
# MAX_CONCURRENT_PROJECTS=1
|
||||
# WORKSPACE_DIR=./workspace
|
||||
98
.gitignore
vendored
Normal file
98
.gitignore
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
# =============================================================================
|
||||
# 7000%AUTO .gitignore
|
||||
# =============================================================================
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
.venv/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.project
|
||||
.pydevproject
|
||||
.settings/
|
||||
|
||||
# Testing
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
htmlcov/
|
||||
.pytest_cache/
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Database
|
||||
data/
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Workspace (generated projects)
|
||||
workspace/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Docker
|
||||
.docker/
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Secrets
|
||||
*.pem
|
||||
*.key
|
||||
secrets/
|
||||
|
||||
# Project specific
|
||||
*.readme_generated
|
||||
.readme_cache/
|
||||
39
.pre-commit-config.yaml
Normal file
39
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,39 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- id: detect-private-key
|
||||
- id: fix-byte-order-marker
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 24.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3.9
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 7.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.8.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [types-requests, types-pyyaml]
|
||||
args: [--ignore-missing-imports, --disallow-untyped-defs]
|
||||
|
||||
ci:
|
||||
autofix_commit_msg: |
|
||||
style: pre-commit fixes
|
||||
autofix_pr_body: |
|
||||
{{$message}}
|
||||
38
CHANGELOG.md
Normal file
38
CHANGELOG.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Changelog
|
||||
|
||||
## [0.1.0] - 2024-02-05
|
||||
|
||||
### Added
|
||||
|
||||
- Initial MCP Server CLI implementation
|
||||
- FastAPI-based MCP protocol server
|
||||
- Click CLI interface
|
||||
- Built-in file operation tools (read, write, list, glob, search)
|
||||
- Git integration tools (status, log, diff)
|
||||
- Shell execution with security controls
|
||||
- Local LLM support (Ollama, LM Studio compatible)
|
||||
- YAML/JSON custom tool definitions
|
||||
- Configuration management with environment variable overrides
|
||||
- CORS support for AI assistant integration
|
||||
- Comprehensive test suite
|
||||
|
||||
### Features
|
||||
|
||||
- MCP protocol handshake (initialize/initialized)
|
||||
- Tools/list and tools/call endpoints
|
||||
- Async tool execution
|
||||
- Tool schema validation
|
||||
- Hot-reload support for custom tools
|
||||
|
||||
### Tools
|
||||
|
||||
- `file_tools`: File read, write, list, search, glob operations
|
||||
- `git_tools`: Git status, log, diff, commit operations
|
||||
- `shell_tools`: Safe shell command execution
|
||||
|
||||
### Configuration
|
||||
|
||||
- `config.yaml` support
|
||||
- Environment variable overrides (MCP_PORT, MCP_HOST, etc.)
|
||||
- Security settings (allowed commands, blocked paths)
|
||||
- Local LLM configuration
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 RepoHealth Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
5
requirements-dev.txt
Normal file
5
requirements-dev.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
pytest>=7.0
|
||||
pytest-cov>=4.0
|
||||
black>=23.0
|
||||
flake8>=6.0
|
||||
ruff>=0.1.0
|
||||
27
setup.cfg
Normal file
27
setup.cfg
Normal file
@@ -0,0 +1,27 @@
|
||||
[metadata]
|
||||
max-line-length = 100
|
||||
exclude = test.*?$, *.pyc, *.pyo, __pycache__, .mypy_cache, .tox, .nox, dist, build
|
||||
|
||||
[flake8]
|
||||
max-line-length = 100
|
||||
ignore = E203, E501, W503
|
||||
per-file-ignores = __init__.py:F401
|
||||
|
||||
[mypy]
|
||||
python_version = 3.9
|
||||
warn_return_any = True
|
||||
warn_unused_configs = True
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[tool:pytest]
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_functions = test_*
|
||||
|
||||
[tool:coverage:run]
|
||||
source = project_scaffold_cli
|
||||
omit = tests/*
|
||||
|
||||
[tool:black]
|
||||
line-length = 100
|
||||
target-version = ['py38', 'py39', 'py310', 'py311', 'py312']
|
||||
61
tests/conftest.py
Normal file
61
tests/conftest.py
Normal 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
75
tests/fixtures/README_EXAMPLE.md
vendored
Normal 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
253
tests/test_cli.py
Normal 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
134
tests/test_config.py
Normal 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
224
tests/test_generator.py
Normal 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
116
tests/test_gitignore.py
Normal 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
313
tests/test_parser.py
Normal 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
241
tests/test_parsers.py
Normal 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
99
tests/test_server.py
Normal 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
145
tests/test_templates.py
Normal 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
321
tests/test_tools.py
Normal 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
221
tests/test_translator.py
Normal 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
131
tests/test_utils.py
Normal 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
|
||||
Reference in New Issue
Block a user