|
|
|
|
|
""" |
|
|
Simple API test script with detailed logging. |
|
|
Tests YLFF API endpoints without complex dependencies. |
|
|
""" |
|
|
|
|
|
import argparse |
|
|
import json |
|
|
import logging |
|
|
import sys |
|
|
import time |
|
|
from datetime import datetime |
|
|
from pathlib import Path |
|
|
from typing import Any, Dict |
|
|
import requests |
|
|
|
|
|
|
|
|
logging.basicConfig( |
|
|
level=logging.INFO, |
|
|
format="%(asctime)s - %(levelname)s - %(message)s", |
|
|
datefmt="%H:%M:%S", |
|
|
stream=sys.stdout, |
|
|
force=True, |
|
|
) |
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
def test_endpoint(base_url: str, method: str, endpoint: str, **kwargs) -> Dict[str, Any]: |
|
|
"""Test a single endpoint.""" |
|
|
url = f"{base_url.rstrip('/')}{endpoint}" |
|
|
|
|
|
timeout = kwargs.pop("timeout", 300) |
|
|
logger.info(f"→ {method} {url}") |
|
|
|
|
|
try: |
|
|
start_time = time.time() |
|
|
response = requests.request(method, url, timeout=timeout, **kwargs) |
|
|
duration = time.time() - start_time |
|
|
|
|
|
logger.info(f"← {response.status_code} ({duration:.3f}s)") |
|
|
|
|
|
try: |
|
|
data = response.json() if response.content else None |
|
|
except json.JSONDecodeError: |
|
|
data = response.text |
|
|
|
|
|
return { |
|
|
"status_code": response.status_code, |
|
|
"data": data, |
|
|
"duration": duration, |
|
|
"success": 200 <= response.status_code < 300, |
|
|
} |
|
|
except requests.exceptions.RequestException as e: |
|
|
logger.error(f"✗ Request failed: {e}") |
|
|
return {"status_code": None, "error": str(e), "success": False} |
|
|
|
|
|
|
|
|
def main(): |
|
|
parser = argparse.ArgumentParser(description="Test YLFF API endpoints") |
|
|
parser.add_argument("--base-url", default="http://localhost:8000", help="Base URL") |
|
|
parser.add_argument("--arkit-dir", type=str, help="ARKit directory") |
|
|
parser.add_argument("--sequence-dir", type=str, help="Sequence directory") |
|
|
|
|
|
args = parser.parse_args() |
|
|
|
|
|
logger.info("=" * 80) |
|
|
logger.info("YLFF API Test") |
|
|
logger.info("=" * 80) |
|
|
logger.info(f"Base URL: {args.base_url}") |
|
|
logger.info("") |
|
|
|
|
|
results = [] |
|
|
|
|
|
|
|
|
logger.info("[1/11] Testing /health") |
|
|
result = test_endpoint(args.base_url, "GET", "/health") |
|
|
results.append(("GET /health", result)) |
|
|
logger.info("") |
|
|
|
|
|
|
|
|
logger.info("[2/11] Testing /") |
|
|
result = test_endpoint(args.base_url, "GET", "/") |
|
|
results.append(("GET /", result)) |
|
|
logger.info("") |
|
|
|
|
|
|
|
|
logger.info("[3/11] Testing /models") |
|
|
result = test_endpoint(args.base_url, "GET", "/models") |
|
|
results.append(("GET /models", result)) |
|
|
logger.info("") |
|
|
|
|
|
|
|
|
logger.info("[4/11] Testing /api/v1/jobs") |
|
|
result = test_endpoint(args.base_url, "GET", "/api/v1/jobs") |
|
|
results.append(("GET /api/v1/jobs", result)) |
|
|
logger.info("") |
|
|
|
|
|
|
|
|
logger.info("[5/11] Testing /api/v1/profiling/metrics") |
|
|
result = test_endpoint(args.base_url, "GET", "/api/v1/profiling/metrics") |
|
|
results.append(("GET /api/v1/profiling/metrics", result)) |
|
|
logger.info("") |
|
|
|
|
|
|
|
|
logger.info("[6/11] Testing /api/v1/profiling/hot-paths") |
|
|
result = test_endpoint(args.base_url, "GET", "/api/v1/profiling/hot-paths") |
|
|
results.append(("GET /api/v1/profiling/hot-paths", result)) |
|
|
logger.info("") |
|
|
|
|
|
|
|
|
logger.info("[7/11] Testing /api/v1/profiling/latency") |
|
|
result = test_endpoint(args.base_url, "GET", "/api/v1/profiling/latency") |
|
|
results.append(("GET /api/v1/profiling/latency", result)) |
|
|
logger.info("") |
|
|
|
|
|
|
|
|
logger.info("[8/11] Testing /api/v1/profiling/system") |
|
|
result = test_endpoint(args.base_url, "GET", "/api/v1/profiling/system") |
|
|
results.append(("GET /api/v1/profiling/system", result)) |
|
|
logger.info("") |
|
|
|
|
|
|
|
|
if args.sequence_dir: |
|
|
logger.info(f"[9/11] Testing /api/v1/validate/sequence (dir: {args.sequence_dir})") |
|
|
payload = { |
|
|
"sequence_dir": args.sequence_dir, |
|
|
"use_case": "ba_validation", |
|
|
"accept_threshold": 2.0, |
|
|
"reject_threshold": 30.0, |
|
|
} |
|
|
result = test_endpoint(args.base_url, "POST", "/api/v1/validate/sequence", json=payload) |
|
|
results.append(("POST /api/v1/validate/sequence", result)) |
|
|
if result.get("success") and result.get("data"): |
|
|
job_id = result["data"].get("job_id") |
|
|
logger.info(f" Job ID: {job_id}") |
|
|
logger.info("") |
|
|
else: |
|
|
logger.info("[9/11] Skipping /api/v1/validate/sequence (no sequence_dir)") |
|
|
logger.info("") |
|
|
|
|
|
|
|
|
if args.arkit_dir: |
|
|
logger.info(f"[10/11] Testing /api/v1/validate/arkit (dir: {args.arkit_dir})") |
|
|
payload = { |
|
|
"arkit_dir": args.arkit_dir, |
|
|
"output_dir": "data/test_arkit_output", |
|
|
"max_frames": 10, |
|
|
"frame_interval": 1, |
|
|
"device": "cuda", |
|
|
"gui": False, |
|
|
} |
|
|
result = test_endpoint(args.base_url, "POST", "/api/v1/validate/arkit", json=payload) |
|
|
results.append(("POST /api/v1/validate/arkit", result)) |
|
|
if result.get("success") and result.get("data"): |
|
|
job_id = result["data"].get("job_id") |
|
|
logger.info(f" Job ID: {job_id}") |
|
|
logger.info("") |
|
|
else: |
|
|
logger.info("[10/11] Skipping /api/v1/validate/arkit (no arkit_dir)") |
|
|
logger.info("") |
|
|
|
|
|
|
|
|
logger.info("[11/11] Polling job status until completion") |
|
|
job_ids = [] |
|
|
for endpoint, result in results: |
|
|
if result.get("success") and result.get("data"): |
|
|
job_id = result["data"].get("job_id") |
|
|
if job_id: |
|
|
job_ids.append(job_id) |
|
|
|
|
|
if job_ids: |
|
|
logger.info(f" Found {len(job_ids)} job(s) to monitor") |
|
|
for job_id in job_ids: |
|
|
logger.info(f" Monitoring job: {job_id}") |
|
|
max_polls = 60 |
|
|
poll_interval = 5 |
|
|
|
|
|
for poll_num in range(max_polls): |
|
|
result = test_endpoint(args.base_url, "GET", f"/api/v1/jobs/{job_id}") |
|
|
if result.get("success") and result.get("data"): |
|
|
status = result["data"].get("status", "unknown") |
|
|
message = result["data"].get("message", "") |
|
|
logger.info( |
|
|
f" Poll {poll_num + 1}/{max_polls}: Status={status}, " |
|
|
f"Message={message[:60]}" |
|
|
) |
|
|
|
|
|
if status in ["completed", "failed"]: |
|
|
logger.info(f" Job {status}!") |
|
|
if status == "completed": |
|
|
job_result = result["data"].get("result", {}) |
|
|
if job_result: |
|
|
logger.info(f" Result keys: {list(job_result.keys())}") |
|
|
|
|
|
|
|
|
|
|
|
validation_stats = job_result.get("validation_stats", {}) |
|
|
|
|
|
|
|
|
if not validation_stats: |
|
|
logger.info( |
|
|
" Fetching validation statistics from " |
|
|
"results endpoint..." |
|
|
) |
|
|
stats_result = test_endpoint( |
|
|
args.base_url, |
|
|
"GET", |
|
|
f"/api/v1/validation/results/{job_id}", |
|
|
) |
|
|
if stats_result.get("success") and stats_result.get("data"): |
|
|
validation_stats = stats_result["data"].get( |
|
|
"validation_stats", {} |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
if ( |
|
|
not validation_stats |
|
|
and stats_result.get("status_code") == 404 |
|
|
): |
|
|
logger.info( |
|
|
" Results endpoint not available, " |
|
|
"checking local validation results..." |
|
|
) |
|
|
|
|
|
|
|
|
common_dirs = [ |
|
|
"data/test_arkit_output", |
|
|
"data/arkit_ba_validation", |
|
|
"data/arkit_validation", |
|
|
] |
|
|
for output_dir in common_dirs: |
|
|
results_file = ( |
|
|
Path(output_dir) / "validation_results.json" |
|
|
) |
|
|
if results_file.exists(): |
|
|
try: |
|
|
with open(results_file) as f: |
|
|
val_data = json.load(f) |
|
|
|
|
|
if "da3_vs_arkit" in val_data: |
|
|
rot_errors = val_data["da3_vs_arkit"].get( |
|
|
"rotation_errors_deg", [] |
|
|
) |
|
|
if rot_errors: |
|
|
accepted = sum( |
|
|
1 for e in rot_errors if e < 2.0 |
|
|
) |
|
|
learnable = sum( |
|
|
1 |
|
|
for e in rot_errors |
|
|
if 2.0 <= e < 30.0 |
|
|
) |
|
|
outlier = sum( |
|
|
1 for e in rot_errors if e >= 30.0 |
|
|
) |
|
|
total = len(rot_errors) |
|
|
validation_stats = { |
|
|
"total_frames": total, |
|
|
"accepted": accepted, |
|
|
"rejected_learnable": learnable, |
|
|
"rejected_outlier": outlier, |
|
|
"accepted_percentage": 100.0 |
|
|
* accepted |
|
|
/ total, |
|
|
"rejected_learnable_percentage": ( |
|
|
100.0 * learnable / total |
|
|
), |
|
|
"rejected_outlier_percentage": 100.0 |
|
|
* outlier |
|
|
/ total, |
|
|
} |
|
|
if "ba_result" in val_data: |
|
|
validation_stats["ba_status"] = ( |
|
|
val_data["ba_result"].get( |
|
|
"status" |
|
|
) |
|
|
) |
|
|
validation_stats[ |
|
|
"max_error_deg" |
|
|
] = val_data["ba_result"].get( |
|
|
"error" |
|
|
) |
|
|
logger.info( |
|
|
f" Found validation results at: " |
|
|
f"{results_file}" |
|
|
) |
|
|
break |
|
|
except Exception as e: |
|
|
logger.warning( |
|
|
f" Could not read {results_file}: {e}" |
|
|
) |
|
|
|
|
|
if validation_stats: |
|
|
logger.info("") |
|
|
logger.info(" === BA Validation Statistics ===") |
|
|
total = validation_stats.get("total_frames", 0) |
|
|
accepted = validation_stats.get("accepted", 0) |
|
|
rejected_learnable = validation_stats.get( |
|
|
"rejected_learnable", 0 |
|
|
) |
|
|
rejected_outlier = validation_stats.get("rejected_outlier", 0) |
|
|
|
|
|
logger.info(f" Total Frames Processed: {total}") |
|
|
logger.info("") |
|
|
logger.info(" Frame Categorization:") |
|
|
accepted_pct = validation_stats.get("accepted_percentage", 0) |
|
|
learnable_pct = validation_stats.get( |
|
|
"rejected_learnable_percentage", 0 |
|
|
) |
|
|
outlier_pct = validation_stats.get( |
|
|
"rejected_outlier_percentage", 0 |
|
|
) |
|
|
logger.info( |
|
|
f" ✓ Accepted (< 2°): " |
|
|
f"{accepted:3d} frames ({accepted_pct:5.1f}%)" |
|
|
) |
|
|
logger.info( |
|
|
f" ⚠ Rejected-Learnable (2-30°): " |
|
|
f"{rejected_learnable:3d} frames " |
|
|
f"({learnable_pct:5.1f}%)" |
|
|
) |
|
|
logger.info( |
|
|
f" ✗ Rejected-Outlier (> 30°): " |
|
|
f"{rejected_outlier:3d} frames " |
|
|
f"({outlier_pct:5.1f}%)" |
|
|
) |
|
|
logger.info("") |
|
|
total_rejected = rejected_learnable + rejected_outlier |
|
|
rejected_pct = ( |
|
|
100.0 * total_rejected / total if total > 0 else 0 |
|
|
) |
|
|
logger.info( |
|
|
f" Total Rejected: {total_rejected} frames " |
|
|
f"({rejected_pct:.1f}%)" |
|
|
) |
|
|
logger.info("") |
|
|
|
|
|
if validation_stats.get("ba_status"): |
|
|
logger.info( |
|
|
f" BA Validation Status: " |
|
|
f"{validation_stats['ba_status']}" |
|
|
) |
|
|
if validation_stats.get("max_error_deg"): |
|
|
max_error = validation_stats["max_error_deg"] |
|
|
logger.info(f" Max Rotation Error: {max_error:.2f}°") |
|
|
|
|
|
|
|
|
if "diagnostics" in validation_stats: |
|
|
diag = validation_stats["diagnostics"] |
|
|
logger.info("") |
|
|
logger.info(" === Detailed Diagnostics ===") |
|
|
|
|
|
if "error_distribution" in diag: |
|
|
err_dist = diag["error_distribution"] |
|
|
if "rotation_errors_deg" in err_dist: |
|
|
rot_dist = err_dist["rotation_errors_deg"] |
|
|
logger.info(" Rotation Error Distribution:") |
|
|
logger.info( |
|
|
f" Q1 (25th): {rot_dist.get('q1', 0):.2f}°" |
|
|
) |
|
|
median = rot_dist.get("median", 0) |
|
|
logger.info(f" Median: {median:.2f}°") |
|
|
logger.info( |
|
|
f" Q3 (75th): {rot_dist.get('q3', 0):.2f}°" |
|
|
) |
|
|
logger.info( |
|
|
f" 90th: {rot_dist.get('p90', 0):.2f}°" |
|
|
) |
|
|
logger.info( |
|
|
f" 95th: {rot_dist.get('p95', 0):.2f}°" |
|
|
) |
|
|
|
|
|
if "alignment_info" in diag.get("da3_vs_arkit", {}): |
|
|
align = diag["da3_vs_arkit"]["alignment_info"] |
|
|
logger.info("") |
|
|
logger.info(" Alignment Diagnostics:") |
|
|
scale_factor = align.get("scale_factor", 0) |
|
|
rotation_det = align.get("rotation_det", 0) |
|
|
logger.info( |
|
|
f" Scale factor: {scale_factor:.6f} " |
|
|
f"(should be ~1.0)" |
|
|
) |
|
|
logger.info( |
|
|
f" Rotation det: {rotation_det:.6f} " |
|
|
f"(should be ~1.0)" |
|
|
) |
|
|
|
|
|
if ( |
|
|
"per_frame_errors" in diag |
|
|
and len(diag["per_frame_errors"]) > 0 |
|
|
): |
|
|
logger.info("") |
|
|
logger.info(" Sample Frame Errors (first 5):") |
|
|
for frame_err in diag["per_frame_errors"][:5]: |
|
|
frame_idx = frame_err["frame_idx"] |
|
|
rot_err = frame_err["rotation_error_deg"] |
|
|
trans_err = frame_err["translation_error_m"] |
|
|
category = frame_err["category"] |
|
|
logger.info( |
|
|
f" Frame {frame_idx}: " |
|
|
f"{rot_err:.2f}° rot, " |
|
|
f"{trans_err:.3f}m trans - " |
|
|
f"{category}" |
|
|
) |
|
|
|
|
|
logger.info("") |
|
|
break |
|
|
|
|
|
if poll_num < max_polls - 1: |
|
|
time.sleep(poll_interval) |
|
|
else: |
|
|
logger.warning(f" Failed to get job status: {result}") |
|
|
break |
|
|
|
|
|
results.append((f"GET /api/v1/jobs/{job_id} (final)", result)) |
|
|
logger.info("") |
|
|
else: |
|
|
logger.info(" No job IDs available to check") |
|
|
logger.info("") |
|
|
|
|
|
|
|
|
logger.info("[12/12] Getting profiling metrics after job execution") |
|
|
result = test_endpoint(args.base_url, "GET", "/api/v1/profiling/metrics") |
|
|
results.append(("GET /api/v1/profiling/metrics (post-exec)", result)) |
|
|
if result.get("success") and result.get("data"): |
|
|
metrics = result["data"] |
|
|
logger.info(f" Total entries: {metrics.get('total_entries', 0)}") |
|
|
logger.info(f" Stages tracked: {len(metrics.get('stage_stats', {}))}") |
|
|
if metrics.get("hot_paths"): |
|
|
logger.info(" Top 5 hot paths:") |
|
|
for i, path in enumerate(metrics["hot_paths"][:5], 1): |
|
|
logger.info( |
|
|
f" {i}. {path.get('function')}: {path.get('total_time', 0):.3f}s " |
|
|
f"({path.get('call_count', 0)} calls)" |
|
|
) |
|
|
logger.info("") |
|
|
|
|
|
|
|
|
logger.info("[13/13] Getting latency breakdown") |
|
|
result = test_endpoint(args.base_url, "GET", "/api/v1/profiling/latency") |
|
|
results.append(("GET /api/v1/profiling/latency (post-exec)", result)) |
|
|
if result.get("success") and result.get("data"): |
|
|
latency = result["data"] |
|
|
total = latency.get("total_time", 0) |
|
|
breakdown = latency.get("breakdown", {}) |
|
|
logger.info(f" Total time: {total:.3f}s") |
|
|
logger.info(" Breakdown by stage:") |
|
|
for stage, stats in sorted( |
|
|
breakdown.items(), key=lambda x: x[1].get("total_time", 0), reverse=True |
|
|
)[:10]: |
|
|
pct = stats.get("percentage", 0) |
|
|
avg = stats.get("avg_time", 0) |
|
|
calls = stats.get("call_count", 0) |
|
|
logger.info( |
|
|
f" {stage:30s} {stats.get('total_time', 0):8.3f}s ({pct:5.1f}%) " |
|
|
f"avg: {avg:.3f}s, calls: {calls}" |
|
|
) |
|
|
logger.info("") |
|
|
|
|
|
|
|
|
logger.info("=" * 80) |
|
|
logger.info("Test Summary") |
|
|
logger.info("=" * 80) |
|
|
success_count = sum(1 for _, r in results if r.get("success")) |
|
|
total_count = len(results) |
|
|
logger.info(f"Success: {success_count}/{total_count}") |
|
|
logger.info("") |
|
|
|
|
|
logger.info("Endpoint Results:") |
|
|
for endpoint, result in results: |
|
|
status = "✓" if result.get("success") else "✗" |
|
|
status_code = result.get("status_code", "N/A") |
|
|
duration = result.get("duration", 0) |
|
|
status_code_str = str(status_code) if status_code is not None else "N/A" |
|
|
logger.info(f"{status} {endpoint:50s} {status_code_str:>3} ({duration:.3f}s)") |
|
|
|
|
|
|
|
|
output_file = Path("data/api_test_results.json") |
|
|
output_file.parent.mkdir(parents=True, exist_ok=True) |
|
|
|
|
|
output_data = { |
|
|
"timestamp": datetime.now().isoformat(), |
|
|
"base_url": args.base_url, |
|
|
"summary": { |
|
|
"total_tests": total_count, |
|
|
"successful": success_count, |
|
|
"failed": total_count - success_count, |
|
|
}, |
|
|
"results": [ |
|
|
{ |
|
|
"endpoint": endpoint, |
|
|
"status_code": r.get("status_code"), |
|
|
"success": r.get("success"), |
|
|
"duration": r.get("duration"), |
|
|
"data": r.get("data") if r.get("success") else None, |
|
|
"error": r.get("error") if not r.get("success") else None, |
|
|
} |
|
|
for endpoint, r in results |
|
|
], |
|
|
} |
|
|
|
|
|
with open(output_file, "w") as f: |
|
|
json.dump(output_data, f, indent=2, default=str) |
|
|
|
|
|
logger.info("") |
|
|
logger.info(f"Results saved to: {output_file}") |
|
|
|
|
|
return 0 if success_count == total_count else 1 |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
sys.exit(main()) |
|
|
|