166 lines
4.7 KiB
Python
166 lines
4.7 KiB
Python
"""Integration tests for Flask web app."""
|
|
|
|
import io
|
|
|
|
import pytest
|
|
from app import app
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
app.config["TESTING"] = True
|
|
with app.test_client() as client:
|
|
yield client
|
|
|
|
|
|
def test_index_page(client):
|
|
"""GET / returns 200."""
|
|
resp = client.get("/")
|
|
assert resp.status_code == 200
|
|
|
|
|
|
def test_jobs_page(client):
|
|
"""GET /jobs returns 200."""
|
|
resp = client.get("/jobs")
|
|
assert resp.status_code == 200
|
|
|
|
|
|
def test_api_models(client):
|
|
"""GET /api/models returns JSON list."""
|
|
resp = client.get("/api/models")
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert isinstance(data, list)
|
|
|
|
|
|
def test_api_jobs(client):
|
|
"""GET /api/jobs returns JSON list."""
|
|
resp = client.get("/api/jobs")
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert isinstance(data, list)
|
|
|
|
|
|
def test_upload_no_video(client):
|
|
"""POST /upload without video returns 400."""
|
|
resp = client.post("/upload")
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_upload_invalid_extension_rejected(client):
|
|
"""POST /upload with a non-video file returns 400."""
|
|
resp = client.post(
|
|
"/upload",
|
|
data={"video": (io.BytesIO(b"not a video"), "notes.txt")},
|
|
content_type="multipart/form-data",
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_status_nonexistent(client):
|
|
"""GET /status/nonexistent returns 404."""
|
|
resp = client.get("/status/nonexistent")
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_api_job_detail_nonexistent(client):
|
|
"""GET /api/jobs/nonexistent returns 404."""
|
|
resp = client.get("/api/jobs/nonexistent")
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_cancel_nonexistent_job(client):
|
|
"""POST /api/jobs/nonexistent/cancel returns 404."""
|
|
resp = client.post("/api/jobs/nonexistent/cancel")
|
|
assert resp.status_code == 404
|
|
data = resp.get_json()
|
|
assert data["error"] == "not found"
|
|
|
|
|
|
def test_cancel_completed_job(client):
|
|
"""POST /api/jobs/<id>/cancel returns 400 for completed job."""
|
|
from src.job import JobQueue, Job, JobStatus
|
|
from app import job_queue
|
|
job = job_queue.add_job(video_path="/tmp/test.mp4", model_configs=[])
|
|
import time
|
|
time.sleep(0.3)
|
|
# Force status to COMPLETED
|
|
with job_queue._lock:
|
|
job.status = JobStatus.COMPLETED
|
|
resp = client.post(f"/api/jobs/{job.job_id}/cancel")
|
|
assert resp.status_code == 400
|
|
data = resp.get_json()
|
|
assert "cannot cancel" in data["error"]
|
|
|
|
|
|
def test_cancel_pending_or_running_job(client):
|
|
"""POST /api/jobs/<id>/cancel returns 200 for cancellable job."""
|
|
from src.job import JobQueue, JobStatus
|
|
from app import job_queue
|
|
job = job_queue.add_job(video_path="/tmp/test.mp4", model_configs=[])
|
|
import time
|
|
time.sleep(0.1)
|
|
# Force status to RUNNING so cancel succeeds
|
|
with job_queue._lock:
|
|
job.status = JobStatus.RUNNING
|
|
resp = client.post(f"/api/jobs/{job.job_id}/cancel")
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["status"] == "cancelled"
|
|
# Verify job is now CANCELLED
|
|
fetched = job_queue.get_job(job.job_id)
|
|
assert fetched.status == JobStatus.CANCELLED
|
|
|
|
|
|
# ── Box counts in API + status page (Task 2) ────────────────────────────
|
|
|
|
|
|
def _job_with_box_results():
|
|
import time
|
|
|
|
from app import job_queue
|
|
from src.job import JobResult, JobStatus
|
|
|
|
job = job_queue.add_job(video_path="/tmp/test.mp4", model_configs=[])
|
|
time.sleep(0.3)
|
|
with job_queue._lock:
|
|
job.status = JobStatus.COMPLETED
|
|
job.results.append(JobResult(
|
|
model_name="a.pt",
|
|
output_path="/tmp/out.mp4",
|
|
loading_count=5,
|
|
unloading_count=2,
|
|
net_count=3,
|
|
batch_count=1,
|
|
frame_count=10,
|
|
duration_seconds=1.0,
|
|
box_loading_count=4,
|
|
box_unloading_count=1,
|
|
box_net_count=3,
|
|
))
|
|
return job
|
|
|
|
|
|
def test_api_job_detail_includes_box_counts(client):
|
|
"""GET /api/jobs/<id> results JSON carries box counts."""
|
|
job = _job_with_box_results()
|
|
resp = client.get(f"/api/jobs/{job.job_id}")
|
|
assert resp.status_code == 200
|
|
r = resp.get_json()["results"][0]
|
|
assert r["box_loading_count"] == 4
|
|
assert r["box_unloading_count"] == 1
|
|
assert r["box_net_count"] == 3
|
|
assert r["loading_count"] == 5
|
|
|
|
|
|
def test_status_page_renders_box_and_sack_labels(client):
|
|
"""status.html shows Sack In/Out and Box In/Out stat labels."""
|
|
job = _job_with_box_results()
|
|
resp = client.get(f"/status/{job.job_id}")
|
|
assert resp.status_code == 200
|
|
html = resp.get_data(as_text=True)
|
|
assert "Sack In" in html
|
|
assert "Sack Out" in html
|
|
assert "Box In" in html
|
|
assert "Box Out" in html
|