Final review fixes for the per-worker test servers (#274): - A failed test gets the stderr its server wrote during that test attached as `server-stderr`, so an aiohttp traceback behind a 500 reaches the CI report. The launcher keeps the newest 256 KB with a stream position per test (stderrMark/stderrSince), and the fixture attaches it only when the test did not end as expected. - The port announcement counts only as a complete line; a chunk ending mid-number was read as a shorter port. - server.py --exit-on-stdin-eof shuts down gracefully (database closed, temp root removed) when stdin closes. The launcher holds that pipe, so a SIGKILLed worker leaves no server behind, on every OS. - settled() gives up after 600 frames with the observation stats instead of hanging to the test timeout. - The Classic text-sync test waits for the page's 50 ms timer to install serializeValue instead of sleeping 60 ms. With that timer run 100 ms late (a scratch copy), the old wait failed 20/20 and the poll passed 20/20; the spec passed 240/240 at 8 workers x10 and 240/240 at 1 CPU with 4 workers. - The workflow test checks each npm cache key names its job's pinned image tag. Every new unit test failed before its change (missing export, the old regex timing out on the split announcement, an unknown server flag, a hung settle, a mismatched cache key).
78 lines
2.8 KiB
Python
78 lines
2.8 KiB
Python
"""The page-workflow test server ends with its launcher (#274 review M2).
|
|
|
|
A Playwright worker that is SIGKILLed never runs its fixture teardown, so the
|
|
server it started must notice on its own: with ``--exit-on-stdin-eof`` it shuts
|
|
down when the launcher's end of its stdin pipe closes, and still removes its
|
|
temp root. This runs on every OS in CI, where Windows is the strict case.
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import unittest
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
SERVER_TIMEOUT_S = 60
|
|
|
|
|
|
class TestBrowserServerLifetime(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.tmp.cleanup)
|
|
|
|
def start(self, *extra):
|
|
env = {**os.environ, "TMPDIR": self.tmp.name}
|
|
env.update(TEMP=self.tmp.name, TMP=self.tmp.name)
|
|
process = subprocess.Popen(
|
|
[sys.executable, "-m", "tests.browser.server", "--port", "0", *extra],
|
|
cwd=REPO_ROOT,
|
|
env=env,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
|
|
def stop():
|
|
if process.poll() is None:
|
|
process.kill()
|
|
process.communicate(timeout=SERVER_TIMEOUT_S)
|
|
|
|
self.addCleanup(stop)
|
|
line = process.stdout.readline().decode()
|
|
self.assertTrue(line.startswith("PM_TEST_SERVER_PORT="), line)
|
|
url = f"http://127.0.0.1:{int(line.split('=', 1)[1])}/prompt_manager/test"
|
|
deadline = time.monotonic() + SERVER_TIMEOUT_S
|
|
while True:
|
|
try:
|
|
with urllib.request.urlopen(url, timeout=1) as response: # nosec B310
|
|
self.assertEqual(response.status, 200)
|
|
return process
|
|
except OSError:
|
|
self.assertIsNone(process.poll(), "the server exited early")
|
|
self.assertLess(time.monotonic(), deadline, "never ready")
|
|
time.sleep(0.1)
|
|
|
|
def roots(self):
|
|
return sorted(path.name for path in Path(self.tmp.name).glob("pm-browser-*"))
|
|
|
|
def test_server_exits_and_removes_its_temp_root_when_stdin_closes(self):
|
|
process = self.start("--exit-on-stdin-eof")
|
|
self.assertEqual(len(self.roots()), 1, "the server works in its own temp root")
|
|
process.stdin.close()
|
|
self.assertEqual(process.wait(timeout=SERVER_TIMEOUT_S), 0)
|
|
self.assertEqual(self.roots(), [], "the temp root and its database are removed")
|
|
|
|
def test_server_keeps_running_with_stdin_closed_without_the_flag(self):
|
|
process = self.start()
|
|
process.stdin.close()
|
|
with self.assertRaises(subprocess.TimeoutExpired):
|
|
process.wait(timeout=1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|