Files
comfyui_o1key/tests/test_http2_client.py
Jony ba920f2b66 Publish current ComfyUI O1Key code baseline
Replace the prior release tree with the current plugin, frontend, tests, and documentation. Document retired node IDs and the public Gitea update source.
2026-09-24 19:56:48 +08:00

198 lines
6.8 KiB
Python

import importlib.util
import sys
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
ROOT = Path(__file__).resolve().parents[1]
def _load_module():
spec = importlib.util.spec_from_file_location(
"o1key_http2_client_test_module",
ROOT / "utils" / "http2_client.py",
)
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def _load_module_without_httpx():
module_name = "o1key_http2_client_without_httpx_test_module"
spec = importlib.util.spec_from_file_location(
module_name,
ROOT / "utils" / "http2_client.py",
)
module = importlib.util.module_from_spec(spec)
with patch.dict(sys.modules, {"httpx": None}):
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module
HTTP2_CLIENT = _load_module()
class _ChunkContent:
def __init__(self, chunks, error=None):
self.chunks = chunks
self.error = error
async def iter_chunked(self, _chunk_size):
for chunk in self.chunks:
yield chunk
if self.error is not None:
raise self.error
class _TraceResponse:
def __init__(self, chunks, headers=None, error=None):
self.status = 200
self.http_version = "HTTP/2"
self.headers = headers or {}
self.content = _ChunkContent(chunks, error=error)
class Http2ClientTests(unittest.TestCase):
def test_enables_http2_when_runtime_support_is_present(self):
fake_client = MagicMock()
with (
patch.object(HTTP2_CLIENT, "http2_runtime_available", return_value=True),
patch.object(HTTP2_CLIENT.httpx, "AsyncClient", return_value=fake_client) as constructor,
):
client = HTTP2_CLIENT.O1keyAsyncHttpClient(http2=True)
self.assertTrue(client.http2_enabled)
self.assertTrue(constructor.call_args.kwargs["http2"])
self.assertTrue(constructor.call_args.kwargs["verify"])
def test_falls_back_to_http11_when_h2_runtime_is_missing(self):
fake_client = MagicMock()
with (
patch.object(HTTP2_CLIENT, "http2_runtime_available", return_value=False),
patch.object(HTTP2_CLIENT.httpx, "AsyncClient", return_value=fake_client) as constructor,
):
client = HTTP2_CLIENT.O1keyAsyncHttpClient(http2=True)
self.assertFalse(client.http2_enabled)
self.assertFalse(constructor.call_args.kwargs["http2"])
def test_task_id_validation_uses_only_explicit_task_fields(self):
payload = {"id": "result-image-id", "data": {"taskId": "task-7"}}
self.assertEqual(HTTP2_CLIENT.response_task_id(payload), "task-7")
self.assertEqual(
HTTP2_CLIENT.validate_response_task_id(payload, "task-7"),
"task-7",
)
with self.assertRaisesRegex(
HTTP2_CLIENT.ResponseTaskIdMismatchError,
"requested_task_id=task-8.*response_task_id=task-7",
):
HTTP2_CLIENT.validate_response_task_id(payload, "task-8")
class ResponseBodyDiagnosticsTests(unittest.IsolatedAsyncioTestCase):
async def test_exact_content_length_is_reported_as_match(self):
response = _TraceResponse(
[b'{"ok":', b'true}'],
headers={"Content-Length": "11"},
)
body, diagnostics = await HTTP2_CLIENT.read_response_body_with_diagnostics(response)
self.assertEqual(body, b'{"ok":true}')
self.assertEqual(diagnostics["declared_bytes"], 11)
self.assertEqual(diagnostics["received_bytes"], 11)
self.assertEqual(diagnostics["length_check"], "match")
async def test_short_content_length_raises_with_received_byte_count(self):
response = _TraceResponse(
[b"1234"],
headers={"Content-Length": "10"},
)
with self.assertRaisesRegex(
HTTP2_CLIENT.ResponseBodyIntegrityError,
r"Content-Length=10.*received=4B.*length_check=mismatch",
):
await HTTP2_CLIENT.read_response_body_with_diagnostics(response)
async def test_stream_failure_keeps_partial_received_byte_count(self):
response = _TraceResponse(
[b"1234"],
headers={"Content-Length": "10"},
error=OSError("connection closed"),
)
with self.assertRaisesRegex(
HTTP2_CLIENT.ResponseBodyIntegrityError,
r"读取提前中断.*Content-Length=10.*received=4B.*connection closed",
):
await HTTP2_CLIENT.read_response_body_with_diagnostics(response)
async def test_compressed_response_does_not_compare_decoded_size(self):
response = _TraceResponse(
[b"decoded body"],
headers={"Content-Length": "5", "Content-Encoding": "gzip"},
)
_body, diagnostics = await HTTP2_CLIENT.read_response_body_with_diagnostics(response)
self.assertEqual(diagnostics["length_check"], "skipped-compressed")
class AiohttpFallbackTests(unittest.IsolatedAsyncioTestCase):
async def test_missing_httpx_uses_working_aiohttp_session(self):
module = _load_module_without_httpx()
self.assertFalse(module.HTTPX_AVAILABLE)
self.assertFalse(module.http2_runtime_available())
self.assertIsInstance(
module.create_timeout(
120.0,
connect=30.0,
read=60.0,
write=30.0,
pool=30.0,
),
module.aiohttp.ClientTimeout,
)
client = module.O1keyAsyncHttpClient(http2=True)
self.assertEqual(client.backend, "aiohttp")
self.assertFalse(client.http2_enabled)
async with client as active_client:
self.assertIsInstance(active_client._client, module.aiohttp.ClientSession)
async def test_missing_httpx_converts_files_to_aiohttp_multipart(self):
module = _load_module_without_httpx()
client = module.O1keyAsyncHttpClient(http2=True)
fake_session = MagicMock()
sentinel_context = object()
fake_session.post.return_value = sentinel_context
client._client = fake_session
result = client.post(
"https://example.invalid/upload",
headers={"Authorization": "Bearer test"},
files={"file": ("reference.jpg", b"jpeg", "image/jpeg")},
timeout=module.create_timeout(
120.0,
connect=30.0,
read=60.0,
write=30.0,
pool=30.0,
),
)
self.assertIs(result, sentinel_context)
payload = fake_session.post.call_args.kwargs["data"]
self.assertIsInstance(payload, module.aiohttp.FormData)
self.assertEqual(len(payload._fields), 1)
if __name__ == "__main__":
unittest.main(verbosity=2)