Skip to content

Commit 0efb7b0

Browse files
authored
Run conformance test with pyvoy (#50)
Signed-off-by: Anuraag Agrawal <[email protected]>
1 parent 6be07d9 commit 0efb7b0

File tree

6 files changed

+252
-116
lines changed

6 files changed

+252
-116
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ jobs:
8484
- name: run conformance tests
8585
# TODO: Debug stdin/stdout issues on Windows
8686
if: ${{ !startsWith(matrix.os, 'windows-') }}
87-
run: uv run pytest -rfEP ${{ matrix.coverage == 'cov' && '--cov=connectrpc --cov-report=xml' || '' }}
87+
run: uv run pytest ${{ matrix.coverage == 'cov' && '--cov=connectrpc --cov-report=xml' || '' }}
8888
working-directory: conformance
8989

9090
- name: run tests with minimal dependencies

conformance/test/_util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import asyncio
22
import sys
33

4-
VERSION_CONFORMANCE = "v1.0.4"
4+
VERSION_CONFORMANCE = "0bed9ca203aeda4e344edc442a4b2ede91726db5"
55

66

77
async def create_standard_streams():

conformance/test/server.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,50 @@ async def serve_hypercorn(
608608
await proc.wait()
609609

610610

611+
async def serve_pyvoy(
612+
request: ServerCompatRequest,
613+
mode: Literal["sync", "async"],
614+
certfile: str | None,
615+
keyfile: str | None,
616+
cafile: str | None,
617+
port_future: asyncio.Future[int],
618+
):
619+
args = ["--port=0"]
620+
if certfile:
621+
args.append(f"--tls-cert={certfile}")
622+
if keyfile:
623+
args.append(f"--tls-key={keyfile}")
624+
if cafile:
625+
args.append(f"--tls-ca-cert={cafile}")
626+
627+
if mode == "sync":
628+
args.append("--interface=wsgi")
629+
args.append("server:wsgi_app")
630+
else:
631+
args.append("server:asgi_app")
632+
633+
proc = await asyncio.create_subprocess_exec(
634+
"pyvoy",
635+
*args,
636+
stderr=asyncio.subprocess.STDOUT,
637+
stdout=asyncio.subprocess.PIPE,
638+
env=_server_env(request),
639+
)
640+
stdout = proc.stdout
641+
assert stdout is not None
642+
stdout = _tee_to_stderr(stdout)
643+
try:
644+
async for line in stdout:
645+
if b"listening on" in line:
646+
port = int(line.strip().split(b"127.0.0.1:")[1])
647+
port_future.set_result(port)
648+
break
649+
await _consume_log(stdout)
650+
except asyncio.CancelledError:
651+
proc.terminate()
652+
await proc.wait()
653+
654+
611655
async def serve_uvicorn(
612656
request: ServerCompatRequest,
613657
certfile: str | None,
@@ -655,7 +699,7 @@ def _find_free_port():
655699
return s.getsockname()[1]
656700

657701

658-
Server = Literal["daphne", "granian", "gunicorn", "hypercorn", "uvicorn"]
702+
Server = Literal["daphne", "granian", "gunicorn", "hypercorn", "pyvoy", "uvicorn"]
659703

660704

661705
class Args(argparse.Namespace):
@@ -728,14 +772,22 @@ async def main() -> None:
728772
request, args.mode, certfile, keyfile, cafile, port_future
729773
)
730774
)
775+
case "pyvoy":
776+
serve_task = asyncio.create_task(
777+
serve_pyvoy(
778+
request, args.mode, certfile, keyfile, cafile, port_future
779+
)
780+
)
731781
case "uvicorn":
732782
if args.mode == "sync":
733783
msg = "uvicorn does not support sync mode"
734784
raise ValueError(msg)
735785
serve_task = asyncio.create_task(
736786
serve_uvicorn(request, certfile, keyfile, cafile, port_future)
737787
)
788+
738789
asyncio.get_event_loop().add_signal_handler(signal.SIGTERM, serve_task.cancel)
790+
739791
port = await port_future
740792
response = ServerCompatResponse()
741793
response.host = "127.0.0.1"

conformance/test/test_server.py

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def macos_raise_ulimit():
2727

2828
# There is a relatively low time limit for the server to respond with a resource error
2929
# for this test. In resource limited environments such as CI, it doesn't seem to be enough,
30-
# notably it is the first request that will take the longest to process as it also sets up
30+
# notably it is the first message that will take the longest to process as it also sets up
3131
# the request. We can consider raising this delay in the runner to see if it helps.
3232
#
3333
# https://github.com/connectrpc/conformance/blob/main/internal/app/connectconformance/testsuites/data/server_message_size.yaml#L46
@@ -37,24 +37,20 @@ def macos_raise_ulimit():
3737
]
3838

3939

40-
@pytest.mark.parametrize("server", ["granian", "gunicorn", "hypercorn"])
40+
@pytest.mark.parametrize("server", ["gunicorn", "pyvoy"])
4141
def test_server_sync(server: str) -> None:
42+
if server == "pyvoy" and sys.platform == "win32":
43+
pytest.skip("pyvoy not supported on Windows")
44+
4245
args = maybe_patch_args_with_debug(
4346
[sys.executable, _server_py_path, "--mode", "sync", "--server", server]
4447
)
4548
opts = [
46-
# While Hypercorn and Granian supports HTTP/2 and WSGI, they both have simple wrappers
47-
# that reads the entire request body before running the application, which does not work for
48-
# full duplex. There are no other popular WSGI servers that support HTTP/2, so in practice
49-
# it cannot be supported. It is possible in theory following hyper-h2's example code in
50-
# https://python-hyper.org/projects/hyper-h2/en/stable/wsgi-example.html though.
49+
# TODO: Enable full-duplex in pyvoy
5150
"--skip",
5251
"**/bidi-stream/full-duplex/**",
5352
]
5453
match server:
55-
case "granian" | "hypercorn":
56-
# granian and hypercorn seem to have issues with concurrency
57-
opts += ["--parallel", "1"]
5854
case "gunicorn":
5955
# gunicorn doesn't support HTTP/2
6056
opts = ["--skip", "**/HTTPVersion:2/**"]
@@ -78,17 +74,14 @@ def test_server_sync(server: str) -> None:
7874
check=False,
7975
)
8076
if result.returncode != 0:
81-
if server == "granian":
82-
# Even with low parallelism, some tests are flaky. We'll need to investigate further.
83-
print( # noqa: T201
84-
f"Granian server tests failed, see output below, not treating as failure:\n{result.stdout}\n{result.stderr}"
85-
)
86-
return
8777
pytest.fail(f"\n{result.stdout}\n{result.stderr}")
8878

8979

90-
@pytest.mark.parametrize("server", ["daphne", "granian", "hypercorn", "uvicorn"])
80+
@pytest.mark.parametrize("server", ["daphne", "pyvoy", "uvicorn"])
9181
def test_server_async(server: str) -> None:
82+
if server == "pyvoy" and sys.platform == "win32":
83+
pytest.skip("pyvoy not supported on Windows")
84+
9285
args = maybe_patch_args_with_debug(
9386
[sys.executable, _server_py_path, "--mode", "async", "--server", server]
9487
)
@@ -104,9 +97,6 @@ def test_server_async(server: str) -> None:
10497
"--skip",
10598
"**/full-duplex/**",
10699
]
107-
case "granian" | "hypercorn":
108-
# granian and hypercorn seem to have issues with concurrency
109-
opts = ["--parallel", "1"]
110100
case "uvicorn":
111101
# uvicorn doesn't support HTTP/2
112102
opts = ["--skip", "**/HTTPVersion:2/**"]
@@ -115,6 +105,7 @@ def test_server_async(server: str) -> None:
115105
"go",
116106
"run",
117107
f"connectrpc.com/conformance/cmd/connectconformance@{VERSION_CONFORMANCE}",
108+
"-v",
118109
"--conf",
119110
_config_path,
120111
"--mode",
@@ -129,10 +120,4 @@ def test_server_async(server: str) -> None:
129120
check=False,
130121
)
131122
if result.returncode != 0:
132-
if server == "granian":
133-
# Even with low parallelism, some tests are flaky. We'll need to investigate further.
134-
print( # noqa: T201
135-
f"Granian server tests failed, see output below, not treating as failure:\n{result.stdout}\n{result.stderr}"
136-
)
137-
return
138123
pytest.fail(f"\n{result.stdout}\n{result.stderr}")

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ dev = [
4242
"daphne==4.2.1",
4343
"httpx[http2]==0.28.1",
4444
"hypercorn==0.17.3",
45-
"granian==2.5.5",
45+
"granian==2.5.7",
4646
"gunicorn==23.0.0",
4747
"just-bin==1.42.4; sys_platform != 'win32'",
4848
"mkdocs==1.6.1",
@@ -52,6 +52,8 @@ dev = [
5252
"pytest==8.4.2",
5353
"pytest-asyncio==1.2.0",
5454
"pytest-cov==7.0.0",
55+
"pytest-timeout==2.4.0",
56+
"pyvoy==0.1.1; sys_platform != 'win32'",
5557
"ruff~=0.13.2",
5658
"uvicorn==0.37.0",
5759
# Needed to enable HTTP/2 in daphne
@@ -72,6 +74,7 @@ module-name = "connectrpc"
7274

7375
[tool.pytest.ini_options]
7476
testpaths = ["test"]
77+
timeout = 1800 # 30 min
7578

7679
[tool.ruff.format]
7780
skip-magic-trailing-comma = true

0 commit comments

Comments
 (0)