179 lines
5.4 KiB
Python
179 lines
5.4 KiB
Python
"""
|
|
Artifact storage and retrieval for test results.
|
|
"""
|
|
|
|
import shutil
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass
|
|
class TestArtifact:
|
|
"""Test artifact (video, screenshot, trace, etc.)."""
|
|
type: str # "video", "screenshot", "trace", "log"
|
|
filename: str
|
|
path: str
|
|
size: int
|
|
mimetype: str
|
|
url: str # Streaming endpoint
|
|
|
|
|
|
class ArtifactStore:
|
|
"""Manage test artifacts."""
|
|
|
|
def __init__(self, artifacts_dir: Path):
|
|
self.artifacts_dir = artifacts_dir
|
|
self.videos_dir = artifacts_dir / "videos"
|
|
self.screenshots_dir = artifacts_dir / "screenshots"
|
|
self.traces_dir = artifacts_dir / "traces"
|
|
|
|
# Ensure directories exist
|
|
self.videos_dir.mkdir(parents=True, exist_ok=True)
|
|
self.screenshots_dir.mkdir(parents=True, exist_ok=True)
|
|
self.traces_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
def store_artifact(
|
|
self,
|
|
source_path: Path,
|
|
run_id: str,
|
|
artifact_type: str
|
|
) -> Optional[TestArtifact]:
|
|
"""
|
|
Store an artifact and return its metadata.
|
|
|
|
Args:
|
|
source_path: Path to the source file
|
|
run_id: Test run ID
|
|
artifact_type: Type of artifact (video, screenshot, trace)
|
|
|
|
Returns:
|
|
TestArtifact metadata or None if storage fails
|
|
"""
|
|
if not source_path.exists():
|
|
return None
|
|
|
|
# Determine destination directory
|
|
if artifact_type == "video":
|
|
dest_dir = self.videos_dir
|
|
mimetype = "video/webm"
|
|
elif artifact_type == "screenshot":
|
|
dest_dir = self.screenshots_dir
|
|
mimetype = "image/png"
|
|
elif artifact_type == "trace":
|
|
dest_dir = self.traces_dir
|
|
mimetype = "application/zip"
|
|
else:
|
|
# Unknown type, store in root artifacts dir
|
|
dest_dir = self.artifacts_dir
|
|
mimetype = "application/octet-stream"
|
|
|
|
# Create run-specific subdirectory
|
|
run_dir = dest_dir / run_id
|
|
run_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Copy file
|
|
dest_path = run_dir / source_path.name
|
|
try:
|
|
shutil.copy2(source_path, dest_path)
|
|
except Exception:
|
|
return None
|
|
|
|
# Build streaming URL
|
|
url = f"/tools/tester/api/artifact/{run_id}/{source_path.name}"
|
|
|
|
return TestArtifact(
|
|
type=artifact_type,
|
|
filename=source_path.name,
|
|
path=str(dest_path),
|
|
size=dest_path.stat().st_size,
|
|
mimetype=mimetype,
|
|
url=url,
|
|
)
|
|
|
|
def get_artifact(self, run_id: str, filename: str) -> Optional[Path]:
|
|
"""
|
|
Retrieve an artifact file.
|
|
|
|
Args:
|
|
run_id: Test run ID
|
|
filename: Artifact filename
|
|
|
|
Returns:
|
|
Path to artifact file or None if not found
|
|
"""
|
|
# Search in all artifact directories
|
|
for artifact_dir in [self.videos_dir, self.screenshots_dir, self.traces_dir]:
|
|
artifact_path = artifact_dir / run_id / filename
|
|
if artifact_path.exists():
|
|
return artifact_path
|
|
|
|
# Check root artifacts dir
|
|
artifact_path = self.artifacts_dir / run_id / filename
|
|
if artifact_path.exists():
|
|
return artifact_path
|
|
|
|
return None
|
|
|
|
def list_artifacts(self, run_id: str) -> list[TestArtifact]:
|
|
"""
|
|
List all artifacts for a test run.
|
|
|
|
Args:
|
|
run_id: Test run ID
|
|
|
|
Returns:
|
|
List of TestArtifact metadata
|
|
"""
|
|
artifacts = []
|
|
|
|
# Search in all artifact directories
|
|
type_mapping = {
|
|
self.videos_dir: ("video", "video/webm"),
|
|
self.screenshots_dir: ("screenshot", "image/png"),
|
|
self.traces_dir: ("trace", "application/zip"),
|
|
}
|
|
|
|
for artifact_dir, (artifact_type, mimetype) in type_mapping.items():
|
|
run_dir = artifact_dir / run_id
|
|
if not run_dir.exists():
|
|
continue
|
|
|
|
for artifact_file in run_dir.iterdir():
|
|
if artifact_file.is_file():
|
|
artifacts.append(TestArtifact(
|
|
type=artifact_type,
|
|
filename=artifact_file.name,
|
|
path=str(artifact_file),
|
|
size=artifact_file.stat().st_size,
|
|
mimetype=mimetype,
|
|
url=f"/tools/tester/api/artifact/{run_id}/{artifact_file.name}",
|
|
))
|
|
|
|
return artifacts
|
|
|
|
def cleanup_old_artifacts(self, keep_recent: int = 10):
|
|
"""
|
|
Clean up old artifact directories, keeping only the most recent runs.
|
|
|
|
Args:
|
|
keep_recent: Number of recent runs to keep
|
|
"""
|
|
# Get all run directories sorted by modification time
|
|
all_runs = []
|
|
|
|
for artifact_dir in [self.videos_dir, self.screenshots_dir, self.traces_dir]:
|
|
for run_dir in artifact_dir.iterdir():
|
|
if run_dir.is_dir():
|
|
all_runs.append(run_dir)
|
|
|
|
# Sort by modification time (newest first)
|
|
all_runs.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
|
|
|
# Keep only the most recent
|
|
for old_run in all_runs[keep_recent:]:
|
|
try:
|
|
shutil.rmtree(old_run)
|
|
except Exception:
|
|
pass # Ignore errors during cleanup
|