Add reusable validation and test helpers for tools to use
This commit is contained in:
241
tests/validators.py
Normal file
241
tests/validators.py
Normal file
@@ -0,0 +1,241 @@
|
||||
"""
|
||||
Reusable test validators and helpers for MCP tools.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
import ast
|
||||
|
||||
|
||||
class ValidationError(Exception):
|
||||
"""Raised when validation fails."""
|
||||
pass
|
||||
|
||||
|
||||
def validate_path(path_str: str) -> Path:
|
||||
"""
|
||||
Validate and resolve a filesystem path.
|
||||
|
||||
Args:
|
||||
path_str: Path string to validate
|
||||
|
||||
Returns:
|
||||
Resolved Path object
|
||||
|
||||
Raises:
|
||||
ValidationError: If path is invalid
|
||||
"""
|
||||
if not isinstance(path_str, str):
|
||||
raise ValidationError("path must be string")
|
||||
|
||||
path = Path(path_str).resolve()
|
||||
return path
|
||||
|
||||
|
||||
def validate_git_repo(path_str: str) -> bool:
|
||||
"""
|
||||
Validate that a path is a git repository.
|
||||
|
||||
Args:
|
||||
path_str: Path to check
|
||||
|
||||
Returns:
|
||||
True if valid git repo, raises ValidationError otherwise
|
||||
"""
|
||||
path = validate_path(path_str)
|
||||
if not (path / ".git").exists():
|
||||
raise ValidationError(f"Not a git repository: {path_str}")
|
||||
return True
|
||||
|
||||
|
||||
def validate_python_file(path_str: str) -> bool:
|
||||
"""
|
||||
Validate that a file is a Python file with valid syntax.
|
||||
|
||||
Args:
|
||||
path_str: Path to Python file
|
||||
|
||||
Returns:
|
||||
True if valid Python file
|
||||
|
||||
Raises:
|
||||
ValidationError: If not a Python file or invalid syntax
|
||||
"""
|
||||
path = validate_path(path_str)
|
||||
|
||||
if not path.suffix == ".py":
|
||||
raise ValidationError(f"Not a Python file: {path_str}")
|
||||
|
||||
try:
|
||||
content = path.read_text(encoding="utf-8")
|
||||
ast.parse(content)
|
||||
return True
|
||||
except SyntaxError as e:
|
||||
raise ValidationError(f"Syntax error in {path_str}: {e.msg} at line {e.lineno}")
|
||||
except Exception as e:
|
||||
raise ValidationError(f"Error reading file {path_str}: {e}")
|
||||
|
||||
|
||||
def validate_python_code(code: str) -> bool:
|
||||
"""
|
||||
Validate Python code syntax without executing it.
|
||||
|
||||
Args:
|
||||
code: Python code string
|
||||
|
||||
Returns:
|
||||
True if valid syntax
|
||||
|
||||
Raises:
|
||||
ValidationError: If syntax is invalid
|
||||
"""
|
||||
if not isinstance(code, str):
|
||||
raise ValidationError("code must be string")
|
||||
|
||||
try:
|
||||
ast.parse(code)
|
||||
return True
|
||||
except SyntaxError as e:
|
||||
raise ValidationError(f"Syntax error: {e.msg} at line {e.lineno}")
|
||||
|
||||
|
||||
def validate_regex_pattern(pattern: str) -> bool:
|
||||
"""
|
||||
Validate regex pattern.
|
||||
|
||||
Args:
|
||||
pattern: Regex pattern string
|
||||
|
||||
Returns:
|
||||
True if valid regex
|
||||
|
||||
Raises:
|
||||
ValidationError: If regex is invalid
|
||||
"""
|
||||
import re
|
||||
|
||||
if not isinstance(pattern, str):
|
||||
raise ValidationError("pattern must be string")
|
||||
|
||||
try:
|
||||
re.compile(pattern)
|
||||
return True
|
||||
except re.error as e:
|
||||
raise ValidationError(f"Invalid regex: {e}")
|
||||
|
||||
|
||||
def validate_git_message(message: str) -> bool:
|
||||
"""
|
||||
Validate git commit message.
|
||||
|
||||
Args:
|
||||
message: Commit message
|
||||
|
||||
Returns:
|
||||
True if valid message
|
||||
|
||||
Raises:
|
||||
ValidationError: If message is invalid
|
||||
"""
|
||||
if not isinstance(message, str):
|
||||
raise ValidationError("message must be string")
|
||||
|
||||
if not message.strip():
|
||||
raise ValidationError("message cannot be empty")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def validate_dict_payload(payload: dict[str, Any], required_keys: list[str]) -> bool:
|
||||
"""
|
||||
Validate that a payload dict has required keys.
|
||||
|
||||
Args:
|
||||
payload: Dictionary to validate
|
||||
required_keys: List of required key names
|
||||
|
||||
Returns:
|
||||
True if all required keys present
|
||||
|
||||
Raises:
|
||||
ValidationError: If any required keys missing
|
||||
"""
|
||||
if not isinstance(payload, dict):
|
||||
raise ValidationError("payload must be dict")
|
||||
|
||||
missing = [k for k in required_keys if k not in payload]
|
||||
if missing:
|
||||
raise ValidationError(f"Missing required keys: {missing}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def assert_file_exists(path_str: str) -> Path:
|
||||
"""
|
||||
Assert that a file exists.
|
||||
|
||||
Args:
|
||||
path_str: Path to file
|
||||
|
||||
Returns:
|
||||
Path object if file exists
|
||||
|
||||
Raises:
|
||||
ValidationError: If file doesn't exist
|
||||
"""
|
||||
path = validate_path(path_str)
|
||||
if not path.exists():
|
||||
raise ValidationError(f"File not found: {path_str}")
|
||||
return path
|
||||
|
||||
|
||||
def assert_dir_exists(path_str: str) -> Path:
|
||||
"""
|
||||
Assert that a directory exists.
|
||||
|
||||
Args:
|
||||
path_str: Path to directory
|
||||
|
||||
Returns:
|
||||
Path object if directory exists
|
||||
|
||||
Raises:
|
||||
ValidationError: If directory doesn't exist
|
||||
"""
|
||||
path = validate_path(path_str)
|
||||
if not path.is_dir():
|
||||
raise ValidationError(f"Directory not found: {path_str}")
|
||||
return path
|
||||
|
||||
|
||||
def compare_outputs(expected: str, actual: str, ignore_whitespace: bool = False) -> bool:
|
||||
"""
|
||||
Compare two outputs (useful for testing tool results).
|
||||
|
||||
Args:
|
||||
expected: Expected output
|
||||
actual: Actual output
|
||||
ignore_whitespace: Whether to ignore whitespace differences
|
||||
|
||||
Returns:
|
||||
True if outputs match
|
||||
"""
|
||||
if ignore_whitespace:
|
||||
expected = " ".join(expected.split())
|
||||
actual = " ".join(actual.split())
|
||||
|
||||
return expected == actual
|
||||
|
||||
|
||||
def extract_lines_with_pattern(text: str, pattern: str) -> list[str]:
|
||||
"""
|
||||
Extract lines matching a pattern (helper for validation).
|
||||
|
||||
Args:
|
||||
text: Text to search
|
||||
pattern: Literal string pattern
|
||||
|
||||
Returns:
|
||||
List of matching lines
|
||||
"""
|
||||
return [line for line in text.split("\n") if pattern in line]
|
||||
Reference in New Issue
Block a user
