datasette/tests/test_cli_serve_server.py
alvinttang 300fe125c5 fix: resolve actual port before printing --root URL when -p 0
Closes #873.

When `datasette -p 0 --root` was used, the printed auth-token URL
contained the literal placeholder port 0 instead of the OS-assigned
port that uvicorn would later bind to. Same applied to `--open`.

Fix: when `port == 0` and we need to print/open a URL before the
server starts (because of --root or --open), pre-bind a TCP socket
on (host, 0), read the assigned port via getsockname(), and hand the
bound socket to uvicorn via Server.run(sockets=[...]). uvicorn.run()'s
own `fd=` parameter assumes AF_UNIX so we use the Config/Server API
in this branch only; the existing uvicorn.run() path is unchanged.

Adds a regression test that launches `datasette --memory -p 0 --root`,
parses the printed URL, asserts the port is non-zero, and confirms a
server is actually listening on that port.
2026-04-25 22:30:14 +08:00

90 lines
2.9 KiB
Python

import httpx
import pytest
import re
import socket
import subprocess
import sys
import tempfile
import time
@pytest.mark.serial
def test_serve_localhost_http(ds_localhost_http_server):
response = httpx.get("http://localhost:8041/_memory.json")
assert {
"database": "_memory",
"path": "/_memory",
"tables": [],
}.items() <= response.json().items()
@pytest.mark.serial
@pytest.mark.skipif(
not hasattr(socket, "AF_UNIX"), reason="Requires socket.AF_UNIX support"
)
def test_serve_unix_domain_socket(ds_unix_domain_socket_server):
_, uds = ds_unix_domain_socket_server
transport = httpx.HTTPTransport(uds=uds)
client = httpx.Client(transport=transport)
response = client.get("http://localhost/_memory.json")
assert {
"database": "_memory",
"path": "/_memory",
"tables": [],
}.items() <= response.json().items()
@pytest.mark.serial
def test_serve_root_url_uses_actual_port_when_port_is_zero():
# Regression test for https://github.com/simonw/datasette/issues/873
# `datasette -p 0 --root` printed http://127.0.0.1:0/... instead of
# the OS-assigned port.
proc = subprocess.Popen(
[sys.executable, "-m", "datasette", "--memory", "-p", "0", "--root"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
cwd=tempfile.gettempdir(),
text=True,
)
try:
# Read lines until we see the auth-token URL or time out
url_line = None
deadline = time.time() + 10.0
while time.time() < deadline:
line = proc.stdout.readline()
if not line:
if proc.poll() is not None:
break
continue
if "/-/auth-token?token=" in line:
url_line = line.strip()
break
assert url_line, "Did not see auth-token URL in datasette output"
match = re.match(r"http://127\.0\.0\.1:(\d+)/-/auth-token\?token=", url_line)
assert match, f"Unexpected auth-token URL format: {url_line!r}"
printed_port = int(match.group(1))
assert printed_port != 0, (
"datasette -p 0 --root should print the OS-assigned port, "
"not the placeholder 0"
)
# Confirm a server is actually listening on that printed port
deadline2 = time.time() + 5.0
last_err = None
while time.time() < deadline2:
try:
response = httpx.get(f"http://127.0.0.1:{printed_port}/_memory.json")
assert response.status_code == 200
break
except httpx.ConnectError as exc:
last_err = exc
time.sleep(0.1)
else:
raise AssertionError(
f"Could not connect to printed port {printed_port}: {last_err}"
)
finally:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()