334 lines
9.7 KiB
Python
334 lines
9.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Fixture loader utility for test data management.
|
|
|
|
Provides functions to load, copy, and create test fixtures for TeamManager testing.
|
|
"""
|
|
|
|
import json
|
|
import shutil
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Dict, Any, Optional, List
|
|
import sys
|
|
import os
|
|
|
|
# Add parent directory to path
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
FIXTURES_DIR = Path(".teams/fixtures")
|
|
|
|
|
|
class FixtureLoader:
|
|
"""Utility class for loading and managing test fixtures."""
|
|
|
|
AVAILABLE_FIXTURES = [
|
|
"minimal-project",
|
|
"full-project",
|
|
"phase1-complete",
|
|
"partial-assignments",
|
|
"empty-project",
|
|
]
|
|
|
|
def __init__(self, fixtures_dir: Path = None):
|
|
"""
|
|
Initialize the fixture loader.
|
|
|
|
Args:
|
|
fixtures_dir: Directory containing fixture files (default: .teams/fixtures)
|
|
"""
|
|
self.fixtures_dir = fixtures_dir or FIXTURES_DIR
|
|
|
|
def list_fixtures(self) -> List[str]:
|
|
"""List all available fixtures."""
|
|
return self.AVAILABLE_FIXTURES.copy()
|
|
|
|
def load_fixture(self, name: str) -> Dict[str, Any]:
|
|
"""
|
|
Load a fixture by name.
|
|
|
|
Args:
|
|
name: Fixture name (e.g., "minimal-project")
|
|
|
|
Returns:
|
|
Dict containing the fixture data
|
|
|
|
Raises:
|
|
FileNotFoundError: If fixture doesn't exist
|
|
ValueError: If name is invalid
|
|
"""
|
|
if not name or not isinstance(name, str):
|
|
raise ValueError("Fixture name must be a non-empty string")
|
|
|
|
# Sanitize name to prevent path traversal
|
|
safe_name = name.replace("..", "").replace("/", "").replace("\\", "")
|
|
fixture_path = self.fixtures_dir / f"{safe_name}.json"
|
|
|
|
if not fixture_path.exists():
|
|
available = ", ".join(self.AVAILABLE_FIXTURES)
|
|
raise FileNotFoundError(
|
|
f"Fixture '{name}' not found at {fixture_path}. "
|
|
f"Available fixtures: {available}"
|
|
)
|
|
|
|
with open(fixture_path, 'r', encoding='utf-8') as f:
|
|
return json.load(f)
|
|
|
|
def load_fixture_to_temp(self, name: str, project_name: Optional[str] = None) -> Path:
|
|
"""
|
|
Load a fixture to a temporary directory for test isolation.
|
|
|
|
Args:
|
|
name: Fixture name
|
|
project_name: Optional new project name (default: use fixture's project_name)
|
|
|
|
Returns:
|
|
Path to the temporary directory containing the fixture
|
|
"""
|
|
data = self.load_fixture(name)
|
|
|
|
# Use provided project name or fixture's name
|
|
if project_name:
|
|
data["project_name"] = project_name
|
|
|
|
# Create temp directory
|
|
temp_dir = Path(tempfile.mkdtemp(prefix=f"fixture_{name}_"))
|
|
config_path = temp_dir / f"{data['project_name']}.json"
|
|
|
|
# Write fixture to temp location
|
|
with open(config_path, 'w', encoding='utf-8') as f:
|
|
json.dump(data, f, indent=2)
|
|
|
|
return temp_dir
|
|
|
|
def copy_fixture(self, name: str, dest_dir: Path, new_project_name: Optional[str] = None) -> Path:
|
|
"""
|
|
Copy a fixture to a destination directory.
|
|
|
|
Args:
|
|
name: Fixture name
|
|
dest_dir: Destination directory
|
|
new_project_name: Optional new project name
|
|
|
|
Returns:
|
|
Path to the copied fixture file
|
|
"""
|
|
data = self.load_fixture(name)
|
|
|
|
# Use new project name if provided
|
|
project_name = new_project_name or data["project_name"]
|
|
dest_path = dest_dir / f"{project_name}.json"
|
|
|
|
# Update project name in data
|
|
if new_project_name:
|
|
data["project_name"] = new_project_name
|
|
data["updated_at"] = self._get_timestamp()
|
|
|
|
# Ensure destination exists
|
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Write fixture
|
|
with open(dest_path, 'w', encoding='utf-8') as f:
|
|
json.dump(data, f, indent=2)
|
|
|
|
return dest_path
|
|
|
|
def create_custom_fixture(self, project_name: str, teams_data: List[Dict],
|
|
dest_dir: Optional[Path] = None) -> Path:
|
|
"""
|
|
Create a custom fixture programmatically.
|
|
|
|
Args:
|
|
project_name: Name for the project
|
|
teams_data: List of team data dicts
|
|
dest_dir: Optional destination directory (default: fixtures_dir)
|
|
|
|
Returns:
|
|
Path to the created fixture file
|
|
"""
|
|
fixture_data = {
|
|
"project_name": project_name,
|
|
"updated_at": self._get_timestamp(),
|
|
"teams": teams_data
|
|
}
|
|
|
|
dest_dir = dest_dir or self.fixtures_dir
|
|
dest_path = dest_dir / f"{project_name}.json"
|
|
|
|
# Ensure destination exists
|
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
with open(dest_path, 'w', encoding='utf-8') as f:
|
|
json.dump(fixture_data, f, indent=2)
|
|
|
|
return dest_path
|
|
|
|
def create_minimal_team(self, team_id: int, name: str, phase: str) -> Dict[str, Any]:
|
|
"""
|
|
Create a minimal team data structure for custom fixtures.
|
|
|
|
Args:
|
|
team_id: Team ID
|
|
name: Team name
|
|
phase: Phase name
|
|
|
|
Returns:
|
|
Team data dict
|
|
"""
|
|
return {
|
|
"id": team_id,
|
|
"name": name,
|
|
"phase": phase,
|
|
"description": f"Team {team_id} description",
|
|
"status": "not_started",
|
|
"started_at": None,
|
|
"completed_at": None,
|
|
"roles": [],
|
|
"exit_criteria": ["Exit criterion 1"]
|
|
}
|
|
|
|
def create_minimal_role(self, name: str, assigned_to: Optional[str] = None) -> Dict[str, Any]:
|
|
"""
|
|
Create a minimal role data structure.
|
|
|
|
Args:
|
|
name: Role name
|
|
assigned_to: Optional assignee name
|
|
|
|
Returns:
|
|
Role data dict
|
|
"""
|
|
return {
|
|
"name": name,
|
|
"responsibility": f"{name} responsibilities",
|
|
"deliverables": ["Deliverable 1"],
|
|
"assigned_to": assigned_to
|
|
}
|
|
|
|
def _get_timestamp(self) -> str:
|
|
"""Get current ISO timestamp."""
|
|
from datetime import datetime
|
|
return datetime.now().isoformat()
|
|
|
|
def validate_fixture(self, data: Dict[str, Any]) -> List[str]:
|
|
"""
|
|
Validate fixture data structure.
|
|
|
|
Args:
|
|
data: Fixture data dict
|
|
|
|
Returns:
|
|
List of validation errors (empty if valid)
|
|
"""
|
|
errors = []
|
|
|
|
if "project_name" not in data:
|
|
errors.append("Missing 'project_name' field")
|
|
|
|
if "teams" not in data:
|
|
errors.append("Missing 'teams' field")
|
|
elif not isinstance(data["teams"], list):
|
|
errors.append("'teams' must be a list")
|
|
else:
|
|
team_ids = set()
|
|
for i, team in enumerate(data["teams"]):
|
|
if "id" not in team:
|
|
errors.append(f"Team {i}: Missing 'id' field")
|
|
elif team["id"] in team_ids:
|
|
errors.append(f"Team {i}: Duplicate team ID {team['id']}")
|
|
else:
|
|
team_ids.add(team["id"])
|
|
|
|
if "name" not in team:
|
|
errors.append(f"Team {team.get('id', i)}: Missing 'name' field")
|
|
|
|
if "roles" not in team:
|
|
errors.append(f"Team {team.get('id', i)}: Missing 'roles' field")
|
|
|
|
return errors
|
|
|
|
|
|
def load_fixture(name: str) -> Dict[str, Any]:
|
|
"""
|
|
Convenience function to load a fixture.
|
|
|
|
Args:
|
|
name: Fixture name
|
|
|
|
Returns:
|
|
Fixture data dict
|
|
"""
|
|
loader = FixtureLoader()
|
|
return loader.load_fixture(name)
|
|
|
|
|
|
def load_fixture_to_temp(name: str, project_name: Optional[str] = None) -> Path:
|
|
"""
|
|
Convenience function to load a fixture to a temp directory.
|
|
|
|
Args:
|
|
name: Fixture name
|
|
project_name: Optional new project name
|
|
|
|
Returns:
|
|
Path to temp directory
|
|
"""
|
|
loader = FixtureLoader()
|
|
return loader.load_fixture_to_temp(name, project_name)
|
|
|
|
|
|
def copy_fixture(name: str, dest_dir: Path, new_project_name: Optional[str] = None) -> Path:
|
|
"""
|
|
Convenience function to copy a fixture.
|
|
|
|
Args:
|
|
name: Fixture name
|
|
dest_dir: Destination directory
|
|
new_project_name: Optional new project name
|
|
|
|
Returns:
|
|
Path to copied fixture
|
|
"""
|
|
loader = FixtureLoader()
|
|
return loader.copy_fixture(name, dest_dir, new_project_name)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(description="Fixture loader utility")
|
|
parser.add_argument("command", choices=["list", "load", "validate"],
|
|
help="Command to execute")
|
|
parser.add_argument("--fixture", help="Fixture name")
|
|
parser.add_argument("--project-name", help="New project name (for copy)")
|
|
|
|
args = parser.parse_args()
|
|
|
|
loader = FixtureLoader()
|
|
|
|
if args.command == "list":
|
|
print("Available fixtures:")
|
|
for name in loader.list_fixtures():
|
|
print(f" - {name}")
|
|
|
|
elif args.command == "load":
|
|
if not args.fixture:
|
|
print("Error: --fixture is required")
|
|
sys.exit(1)
|
|
data = loader.load_fixture(args.fixture)
|
|
print(json.dumps(data, indent=2))
|
|
|
|
elif args.command == "validate":
|
|
if not args.fixture:
|
|
print("Error: --fixture is required")
|
|
sys.exit(1)
|
|
data = loader.load_fixture(args.fixture)
|
|
errors = loader.validate_fixture(data)
|
|
if errors:
|
|
print(f"Validation errors for '{args.fixture}':")
|
|
for error in errors:
|
|
print(f" - {error}")
|
|
sys.exit(1)
|
|
else:
|
|
print(f"Fixture '{args.fixture}' is valid")
|