adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
319 lines
8.0 KiB
JavaScript
319 lines
8.0 KiB
JavaScript
/**
|
|
* Unit tests for proxy routing logic.
|
|
*
|
|
* Tests the classifyRoute function which determines where each request goes:
|
|
* "worker", "python", or "sveltekit".
|
|
*
|
|
* Run: node --test js/app/proxy_routes.test.js
|
|
*/
|
|
|
|
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
classifyRoute,
|
|
hashString,
|
|
matchesPrefix,
|
|
PYTHON_ROUTE_PREFIXES,
|
|
STATIC_ROUTE_PREFIXES
|
|
} from "./proxy_routes.js";
|
|
|
|
describe("classifyRoute", () => {
|
|
describe("python routes", () => {
|
|
const pythonPaths = [
|
|
"/config",
|
|
"/config/",
|
|
"/gradio_api/info",
|
|
"/gradio_api/queue/join",
|
|
"/gradio_api/call/predict",
|
|
"/login",
|
|
"/login/",
|
|
"/logout",
|
|
"/theme.css",
|
|
"/robots.txt",
|
|
"/pwa_icon",
|
|
"/manifest.json",
|
|
"/monitoring",
|
|
"/monitoring/some/path"
|
|
];
|
|
|
|
for (const path of pythonPaths) {
|
|
it(`${path} -> python`, () => {
|
|
assert.equal(classifyRoute(path, { hasWorkers: true }).route, "python");
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("worker routes (workers configured)", () => {
|
|
const workerPaths = [
|
|
"/gradio_api/upload",
|
|
"/gradio_api/file=/tmp/test.txt",
|
|
"/gradio_api/file/some/path",
|
|
"/upload",
|
|
"/file=/tmp/test.txt",
|
|
"/file/some/path",
|
|
"/static/img/logo.svg",
|
|
"/static/css/style.css",
|
|
"/assets/some.js",
|
|
"/svelte/some/file.js",
|
|
"/favicon.ico",
|
|
"/custom_component/abc"
|
|
];
|
|
|
|
for (const path of workerPaths) {
|
|
it(`${path} -> worker`, () => {
|
|
assert.equal(classifyRoute(path, { hasWorkers: true }).route, "worker");
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("worker routes fall back to python (no workers)", () => {
|
|
const paths = [
|
|
"/gradio_api/upload",
|
|
"/upload",
|
|
"/file=/tmp/test.txt",
|
|
"/static/img/logo.svg",
|
|
"/favicon.ico"
|
|
];
|
|
|
|
for (const path of paths) {
|
|
it(`${path} -> python (no workers)`, () => {
|
|
assert.equal(
|
|
classifyRoute(path, { hasWorkers: false }).route,
|
|
"python"
|
|
);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("sveltekit routes", () => {
|
|
const sveltekitPaths = [
|
|
"/",
|
|
"/_app/immutable/nodes/1.CPQUYxzy.js",
|
|
"/_app/immutable/chunks/dgSRqNc-.js",
|
|
"/some/random/page"
|
|
];
|
|
|
|
for (const path of sveltekitPaths) {
|
|
it(`${path} -> sveltekit`, () => {
|
|
assert.equal(
|
|
classifyRoute(path, { hasWorkers: true }).route,
|
|
"sveltekit"
|
|
);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("priority", () => {
|
|
it("/gradio_api/upload goes to worker, not python", () => {
|
|
assert.equal(
|
|
classifyRoute("/gradio_api/upload", { hasWorkers: true }).route,
|
|
"worker"
|
|
);
|
|
});
|
|
|
|
it("/gradio_api/upload_progress goes to worker, not python", () => {
|
|
assert.equal(
|
|
classifyRoute("/gradio_api/upload_progress", {
|
|
hasWorkers: true
|
|
}).route,
|
|
"worker"
|
|
);
|
|
});
|
|
|
|
it("/gradio_api/file=/path goes to worker, not python", () => {
|
|
assert.equal(
|
|
classifyRoute("/gradio_api/file=/path", { hasWorkers: true }).route,
|
|
"worker"
|
|
);
|
|
});
|
|
|
|
it("/gradio_api/file/path goes to worker, not python", () => {
|
|
assert.equal(
|
|
classifyRoute("/gradio_api/file/path", { hasWorkers: true }).route,
|
|
"worker"
|
|
);
|
|
});
|
|
|
|
it("/gradio_api/info still goes to python", () => {
|
|
assert.equal(
|
|
classifyRoute("/gradio_api/info", { hasWorkers: true }).route,
|
|
"python"
|
|
);
|
|
});
|
|
|
|
it("/gradio_api/queue/join still goes to python", () => {
|
|
assert.equal(
|
|
classifyRoute("/gradio_api/queue/join", { hasWorkers: true }).route,
|
|
"python"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("server mode", () => {
|
|
it("/ -> python when serverModeEnabled", () => {
|
|
assert.equal(
|
|
classifyRoute("/", { serverModeEnabled: true }).route,
|
|
"python"
|
|
);
|
|
});
|
|
|
|
it("/any/path -> python when serverModeEnabled", () => {
|
|
assert.equal(
|
|
classifyRoute("/any/path", { serverModeEnabled: true }).route,
|
|
"python"
|
|
);
|
|
});
|
|
|
|
it("worker routes still go to workers even in server mode", () => {
|
|
assert.equal(
|
|
classifyRoute("/gradio_api/upload", {
|
|
hasWorkers: true,
|
|
serverModeEnabled: true
|
|
}).route,
|
|
"worker"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("upload affinity routing", () => {
|
|
const opts = { hasWorkers: true, numWorkers: 3 };
|
|
|
|
it("upload with upload_id gets a deterministic workerIndex", () => {
|
|
const result = classifyRoute("/gradio_api/upload", {
|
|
...opts,
|
|
queryString: "upload_id=abc123"
|
|
});
|
|
assert.equal(result.route, "worker");
|
|
assert.equal(typeof result.workerIndex, "number");
|
|
assert.ok(result.workerIndex >= 0 && result.workerIndex < 3);
|
|
});
|
|
|
|
it("upload_progress with same upload_id gets same workerIndex", () => {
|
|
const uploadResult = classifyRoute("/gradio_api/upload", {
|
|
...opts,
|
|
queryString: "upload_id=abc123"
|
|
});
|
|
const progressResult = classifyRoute("/gradio_api/upload_progress", {
|
|
...opts,
|
|
queryString: "upload_id=abc123"
|
|
});
|
|
assert.equal(uploadResult.workerIndex, progressResult.workerIndex);
|
|
});
|
|
|
|
it("/upload (no gradio_api prefix) also gets affinity", () => {
|
|
const result = classifyRoute("/upload", {
|
|
...opts,
|
|
queryString: "upload_id=test456"
|
|
});
|
|
assert.equal(result.route, "worker");
|
|
assert.equal(typeof result.workerIndex, "number");
|
|
});
|
|
|
|
it("/upload_progress (no gradio_api prefix) also gets affinity", () => {
|
|
const uploadResult = classifyRoute("/upload", {
|
|
...opts,
|
|
queryString: "upload_id=test456"
|
|
});
|
|
const progressResult = classifyRoute("/upload_progress", {
|
|
...opts,
|
|
queryString: "upload_id=test456"
|
|
});
|
|
assert.equal(uploadResult.workerIndex, progressResult.workerIndex);
|
|
});
|
|
|
|
it("upload without upload_id gets no workerIndex (round-robin)", () => {
|
|
const result = classifyRoute("/gradio_api/upload", {
|
|
...opts,
|
|
queryString: ""
|
|
});
|
|
assert.equal(result.route, "worker");
|
|
assert.equal(result.workerIndex, undefined);
|
|
});
|
|
|
|
it("different upload_ids can map to different workers", () => {
|
|
// Generate several IDs and check we get at least 2 distinct indices
|
|
const indices = new Set();
|
|
for (let i = 0; i < 20; i++) {
|
|
const result = classifyRoute("/gradio_api/upload", {
|
|
...opts,
|
|
queryString: `upload_id=id-${i}`
|
|
});
|
|
indices.add(result.workerIndex);
|
|
}
|
|
assert.ok(
|
|
indices.size > 1,
|
|
"expected different upload_ids to map to different workers"
|
|
);
|
|
});
|
|
|
|
it("non-upload worker routes get no workerIndex", () => {
|
|
const result = classifyRoute("/static/img/logo.svg", {
|
|
...opts,
|
|
queryString: "upload_id=abc123"
|
|
});
|
|
assert.equal(result.route, "worker");
|
|
assert.equal(result.workerIndex, undefined);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("hashString", () => {
|
|
it("returns a non-negative integer", () => {
|
|
const h = hashString("test");
|
|
assert.equal(typeof h, "number");
|
|
assert.ok(h >= 0);
|
|
assert.ok(Number.isInteger(h));
|
|
});
|
|
|
|
it("is deterministic", () => {
|
|
assert.equal(hashString("abc123"), hashString("abc123"));
|
|
});
|
|
|
|
it("different strings produce different hashes", () => {
|
|
assert.notEqual(hashString("abc"), hashString("xyz"));
|
|
});
|
|
});
|
|
|
|
describe("matchesPrefix", () => {
|
|
it("exact match", () => {
|
|
assert.ok(matchesPrefix("/config", ["/config"]));
|
|
});
|
|
|
|
it("prefix match", () => {
|
|
assert.ok(matchesPrefix("/config/foo", ["/config"]));
|
|
});
|
|
|
|
it("no match", () => {
|
|
assert.ok(!matchesPrefix("/other", ["/config"]));
|
|
});
|
|
|
|
it("does not match partial prefix", () => {
|
|
// /configurable should NOT match /config as a prefix?
|
|
// Actually it does because /configurable.startsWith("/config") is true.
|
|
// This is the current behavior — documenting it.
|
|
assert.ok(matchesPrefix("/configurable", ["/config"]));
|
|
});
|
|
});
|
|
|
|
describe("route prefix lists are consistent", () => {
|
|
it("STATIC_ROUTE_PREFIXES checked before PYTHON_ROUTE_PREFIXES catches /gradio_api/*", () => {
|
|
// The critical routes that must be in STATIC before PYTHON catches them
|
|
const critical = [
|
|
"/gradio_api/upload",
|
|
"/gradio_api/upload_progress",
|
|
"/gradio_api/file=",
|
|
"/gradio_api/file/"
|
|
];
|
|
for (const route of critical) {
|
|
assert.ok(
|
|
STATIC_ROUTE_PREFIXES.some((p) => route.startsWith(p)),
|
|
`${route} must be in STATIC_ROUTE_PREFIXES`
|
|
);
|
|
}
|
|
});
|
|
|
|
it("/gradio_api is in PYTHON_ROUTE_PREFIXES", () => {
|
|
assert.ok(PYTHON_ROUTE_PREFIXES.includes("/gradio_api"));
|
|
});
|
|
});
|