adf0d17497
publish / version_or_publish (push) Waiting to run
storybook-build / changes (push) Waiting to run
storybook-build / :storybook-build (push) Blocked by required conditions
Sync Gradio Skills to Hugging Face / sync-skills (push) Waiting to run
functional / changes (push) Waiting to run
functional / build-frontend (push) Blocked by required conditions
functional / functional-test-SSR=false (push) Blocked by required conditions
functional / functional-reload (push) Blocked by required conditions
functional / functional-test-SSR=true (push) Blocked by required conditions
hygiene / hygiene-test (push) Waiting to run
python / changes (push) Waiting to run
python / build (push) Blocked by required conditions
python / test-ubuntu-latest-flaky (push) Blocked by required conditions
python / test-ubuntu-latest-not-flaky (push) Blocked by required conditions
python / test-windows-latest-flaky (push) Blocked by required conditions
python / test-windows-latest-not-flaky (push) Blocked by required conditions
js / changes (push) Waiting to run
js / js-test (push) Blocked by required conditions
docs-build / changes (push) Waiting to run
docs-build / docs-build (push) Blocked by required conditions
docs-build / website-build (push) Blocked by required conditions
348 lines
11 KiB
Python
348 lines
11 KiB
Python
import asyncio
|
|
import json
|
|
import sys
|
|
import time
|
|
from unittest.mock import patch
|
|
|
|
import gradio_client as grc
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
import gradio as gr
|
|
from gradio.route_utils import API_PREFIX
|
|
|
|
|
|
class TestQueueing:
|
|
def test_single_request(self, connect):
|
|
with gr.Blocks() as demo:
|
|
name = gr.Textbox()
|
|
output = gr.Textbox()
|
|
|
|
def greet(x):
|
|
return f"Hello, {x}!"
|
|
|
|
name.submit(greet, name, output)
|
|
|
|
with connect(demo) as client:
|
|
job = client.submit("x", fn_index=0)
|
|
assert job.result() == "Hello, x!"
|
|
|
|
def test_all_status_messages(self, connect):
|
|
with gr.Blocks() as demo:
|
|
name = gr.Textbox()
|
|
output = gr.Textbox()
|
|
|
|
def greet(x):
|
|
time.sleep(2)
|
|
return f"Hello, {x}!"
|
|
|
|
name.submit(greet, name, output, concurrency_limit=2)
|
|
|
|
app, local_url, _ = demo.launch(prevent_thread_lock=True)
|
|
test_client = TestClient(app)
|
|
client = grc.Client(local_url)
|
|
|
|
client.submit("a", fn_index=0)
|
|
job2 = client.submit("b", fn_index=0)
|
|
client.submit("c", fn_index=0)
|
|
job4 = client.submit("d", fn_index=0)
|
|
|
|
sizes = []
|
|
while job4.status().code.value != "FINISHED":
|
|
queue_status = test_client.get(f"{API_PREFIX}/queue/status").json()
|
|
queue_size = queue_status["queue_size"]
|
|
if len(sizes) == 0 or queue_size != sizes[-1]:
|
|
sizes.append(queue_size)
|
|
time.sleep(0.01)
|
|
|
|
time.sleep(0.1)
|
|
queue_status = test_client.get(f"{API_PREFIX}/queue/status").json()
|
|
queue_size = queue_status["queue_size"]
|
|
if queue_size != sizes[-1]:
|
|
sizes.append(queue_size)
|
|
|
|
assert (
|
|
max(sizes)
|
|
in [
|
|
2,
|
|
3,
|
|
4,
|
|
]
|
|
) # Can be 2 - 4, depending on if the workers have picked up jobs before the queue status is checked
|
|
|
|
assert min(sizes) == 0
|
|
assert sizes[-1] == 0
|
|
|
|
assert job2.result() == "Hello, b!"
|
|
assert job4.result() == "Hello, d!"
|
|
|
|
def test_cached_generator_finishes_on_queue_cache_hit(self, connect):
|
|
call_count = 0
|
|
|
|
@gr.cache
|
|
def stream_text(text):
|
|
nonlocal call_count
|
|
call_count += 1
|
|
for i in range(len(text)):
|
|
yield text[: i + 1]
|
|
|
|
with gr.Blocks() as demo:
|
|
name = gr.Textbox()
|
|
output = gr.Textbox()
|
|
name.submit(stream_text, name, output)
|
|
|
|
demo.queue()
|
|
|
|
with connect(demo) as client:
|
|
first = client.submit("hello", fn_index=0)
|
|
assert first.result(timeout=5) == "hello"
|
|
assert first.outputs() == ["h", "he", "hel", "hell", "hello"]
|
|
|
|
second = client.submit("hello", fn_index=0)
|
|
assert second.result(timeout=5) == "hello"
|
|
assert second.outputs() == ["h", "he", "hel", "hell", "hello"]
|
|
|
|
assert call_count == 1
|
|
|
|
def test_queue_average_excludes_manual_cache_hits(self, connect):
|
|
def greet(x, c=gr.Cache()):
|
|
hit = c.get(x)
|
|
if hit is not None:
|
|
return hit["value"]
|
|
time.sleep(0.02)
|
|
value = f"Hello, {x}!"
|
|
c.set(x, value=value)
|
|
return value
|
|
|
|
with gr.Blocks() as demo:
|
|
name = gr.Textbox()
|
|
output = gr.Textbox()
|
|
name.submit(greet, name, output)
|
|
|
|
demo.queue()
|
|
|
|
with connect(demo) as client:
|
|
first = client.submit("x", fn_index=0)
|
|
assert first.result(timeout=5) == "Hello, x!"
|
|
|
|
second = client.submit("x", fn_index=0)
|
|
assert second.result(timeout=5) == "Hello, x!"
|
|
|
|
process_time = demo._queue.process_time_per_fn[demo.fns[0]]
|
|
assert process_time.count == 1
|
|
assert process_time.avg_time >= 0.02
|
|
|
|
@pytest.mark.flaky
|
|
@pytest.mark.parametrize(
|
|
"default_concurrency_limit, statuses",
|
|
[
|
|
("not_set", ["IN_QUEUE", "IN_QUEUE", "PROCESSING"]),
|
|
(None, ["PROCESSING", "PROCESSING", "PROCESSING"]),
|
|
(1, ["IN_QUEUE", "IN_QUEUE", "PROCESSING"]),
|
|
(2, ["IN_QUEUE", "PROCESSING", "PROCESSING"]),
|
|
],
|
|
)
|
|
def test_default_concurrency_limits(self, default_concurrency_limit, statuses):
|
|
with gr.Blocks() as demo:
|
|
a = gr.Number()
|
|
b = gr.Number()
|
|
output = gr.Number()
|
|
|
|
add_btn = gr.Button("Add")
|
|
|
|
@add_btn.click(inputs=[a, b], outputs=output)
|
|
def add(x, y):
|
|
time.sleep(4)
|
|
return x + y
|
|
|
|
demo.queue(default_concurrency_limit=default_concurrency_limit)
|
|
_, local_url, _ = demo.launch(
|
|
prevent_thread_lock=True,
|
|
)
|
|
client = grc.Client(local_url)
|
|
|
|
add_job_1 = client.submit(1, 1, fn_index=0)
|
|
add_job_2 = client.submit(1, 1, fn_index=0)
|
|
add_job_3 = client.submit(1, 1, fn_index=0)
|
|
|
|
time.sleep(2)
|
|
|
|
add_job_statuses = [add_job_1.status(), add_job_2.status(), add_job_3.status()]
|
|
assert sorted([s.code.value for s in add_job_statuses]) == statuses
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
sys.platform == "win32",
|
|
reason="Heartbeat task is not reliably cancelled by the time the SSE stream "
|
|
"loop returns on Windows CI (cancellation does not propagate within a "
|
|
"reasonable wait). Passes on Linux/macOS.",
|
|
)
|
|
def test_heartbeat_task_cancelled_after_stream_completes():
|
|
"""Verify the heartbeat task is cancelled when the SSE stream ends normally."""
|
|
with gr.Blocks() as demo:
|
|
name = gr.Textbox()
|
|
output = gr.Textbox()
|
|
|
|
def greet(x):
|
|
return f"Hello, {x}!"
|
|
|
|
name.submit(greet, name, output)
|
|
|
|
app, local_url, _ = demo.launch(prevent_thread_lock=True)
|
|
|
|
heartbeat_tasks = []
|
|
original_create_task = asyncio.create_task
|
|
|
|
def tracking_create_task(coro, **kwargs):
|
|
task = original_create_task(coro, **kwargs)
|
|
heartbeat_tasks.append(task)
|
|
return task
|
|
|
|
with patch("gradio.routes.asyncio.create_task", side_effect=tracking_create_task):
|
|
test_client = TestClient(app)
|
|
r = test_client.post(
|
|
f"{API_PREFIX}/queue/join",
|
|
json={
|
|
"data": ["hello"],
|
|
"fn_index": 0,
|
|
"event_data": None,
|
|
"session_hash": "test_heartbeat",
|
|
"trigger_id": None,
|
|
},
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
r = test_client.get(f"{API_PREFIX}/queue/data?session_hash=test_heartbeat")
|
|
|
|
# Verify we got a process_completed message
|
|
got_completed = False
|
|
for line in r.iter_lines():
|
|
if "data" in line:
|
|
data = json.loads(line[5:])
|
|
if data["msg"] == "process_completed":
|
|
got_completed = True
|
|
assert got_completed
|
|
|
|
assert len(heartbeat_tasks) > 0, "No heartbeat tasks were created"
|
|
for task in heartbeat_tasks:
|
|
assert task.cancelled() or task.done(), (
|
|
"Heartbeat task was not cancelled after stream completed"
|
|
)
|
|
demo.close()
|
|
|
|
|
|
def test_cancel_removes_pending_event_from_queue():
|
|
"""Cancelling a queued (not yet running) event should remove it from the queue."""
|
|
with gr.Blocks() as demo:
|
|
start = gr.Button()
|
|
output = gr.Textbox()
|
|
|
|
def slow():
|
|
time.sleep(2)
|
|
return "done"
|
|
|
|
start.click(slow, None, output)
|
|
|
|
demo.queue(default_concurrency_limit=1)
|
|
app, _, _ = demo.launch(prevent_thread_lock=True)
|
|
test_client = TestClient(app)
|
|
|
|
join_payload = {
|
|
"data": [],
|
|
"fn_index": 0,
|
|
"event_data": None,
|
|
"session_hash": "sess1",
|
|
"trigger_id": None,
|
|
}
|
|
|
|
try:
|
|
first = test_client.post(f"{API_PREFIX}/queue/join", json=join_payload)
|
|
second = test_client.post(f"{API_PREFIX}/queue/join", json=join_payload)
|
|
third = test_client.post(f"{API_PREFIX}/queue/join", json=join_payload)
|
|
assert first.status_code == 200
|
|
assert second.status_code == 200
|
|
assert third.status_code == 200
|
|
|
|
second_event_id = second.json()["event_id"]
|
|
third_event_id = third.json()["event_id"]
|
|
|
|
# First event gets picked up by the worker; second and third are queued.
|
|
# The worker dequeues asynchronously, so wait for it to settle before
|
|
# asserting (avoids a race on slower CI runners where all three are
|
|
# momentarily still in the queue).
|
|
for _ in range(50):
|
|
if len(demo._queue) == 2:
|
|
break
|
|
time.sleep(0.1)
|
|
assert len(demo._queue) == 2
|
|
assert second_event_id in demo._queue.event_ids_to_events
|
|
assert second_event_id in demo._queue.pending_event_ids_session["sess1"]
|
|
|
|
# Cancel the second (pending/queued) event
|
|
resp = test_client.post(
|
|
f"{API_PREFIX}/cancel",
|
|
json={
|
|
"session_hash": "sess1",
|
|
"fn_index": 0,
|
|
"event_id": second_event_id,
|
|
},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert third_event_id in demo._queue.event_ids_to_events
|
|
|
|
assert len(demo._queue) == 1
|
|
r = test_client.get(f"{API_PREFIX}/queue/data?session_hash=sess1")
|
|
|
|
# Verify we got a process_completed message
|
|
got_completed = False
|
|
for line in r.iter_lines():
|
|
if "data" in line:
|
|
data = json.loads(line[5:])
|
|
if data["msg"] == "process_completed":
|
|
got_completed = True
|
|
assert got_completed
|
|
assert second_event_id not in demo._queue.pending_event_ids_session["sess1"]
|
|
assert second_event_id not in demo._queue.event_ids_to_events
|
|
finally:
|
|
demo.close()
|
|
|
|
|
|
def test_analytics_summary(monkeypatch):
|
|
"""Test that the analytics summary endpoint is correctly being computed every N requests,
|
|
where N is set by the GRADIO_ANALYTICS_CACHE_FREQUENCY environment variable."""
|
|
monkeypatch.setenv("GRADIO_ANALYTICS_CACHE_FREQUENCY", 2)
|
|
with gr.Blocks() as demo:
|
|
name = gr.Textbox()
|
|
output = gr.Textbox()
|
|
|
|
def greet(x):
|
|
return f"Hello, {x}!"
|
|
|
|
name.submit(greet, name, output, api_name="predict")
|
|
|
|
_, local_url, _ = demo.launch(prevent_thread_lock=True)
|
|
test_client = TestClient(demo.app)
|
|
client = grc.Client(local_url)
|
|
with test_client as tc:
|
|
event_analytics = tc.get("/monitoring/summary").json()
|
|
assert event_analytics == {"functions": {}}
|
|
client.predict(
|
|
"a",
|
|
api_name="/predict",
|
|
)
|
|
client.predict(
|
|
"a",
|
|
api_name="/predict",
|
|
)
|
|
event_analytics = tc.get("/monitoring/summary").json()
|
|
assert "predict" in event_analytics["functions"]
|
|
assert event_analytics["functions"]["predict"]["total_requests"] == 2
|
|
client.predict("a", api_name="/predict")
|
|
event_analytics = tc.get("/monitoring/summary").json()
|
|
assert "predict" in event_analytics["functions"]
|
|
assert event_analytics["functions"]["predict"]["total_requests"] == 2
|
|
client.predict("a", api_name="/predict")
|
|
event_analytics = tc.get("/monitoring/summary").json()
|
|
assert "predict" in event_analytics["functions"]
|
|
assert event_analytics["functions"]["predict"]["total_requests"] == 4
|