From 24ac413fc5b87cfd0e056929d1f9fb6d97444c87 Mon Sep 17 00:00:00 2001 From: Vladimir Dubrovin <3proxy@3proxy.ru> Date: Sun, 11 Oct 2026 10:26:02 +0300 Subject: [PATCH] Tests: wait for the reload requested in the admin case The reload asked for through /R happens on the next tick of the main loop, and between the old services stopping and the new ones starting a request is refused: on Windows the new listener cannot bind the port until the old one has closed it, and retries with a growing delay. When the tick fell inside the case, every request after it failed with ConnectionRefusedError (MSVC run of 98e8dfb). The case now waits for both services to have restarted after each /R. Co-Authored-By: Claude Opus 5.5 --- tests/cases/admin.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/tests/cases/admin.py b/tests/cases/admin.py index e2455a9..078bd89 100644 --- a/tests/cases/admin.py +++ b/tests/cases/admin.py @@ -1,10 +1,12 @@ """The admin interface, now a set of handlers on the HTTP server.""" +import time + def run(t): adm = t.free_port() lim = t.free_port() - t.start("admin", f""" + server = t.start("admin", f""" log auth iponly allow * @@ -20,16 +22,34 @@ def run(t): url = f"http://127.0.0.1:{adm}" + def reload(path): + """Ask for a reload and wait for it to be done. It happens on the next + tick of the main loop, at most a second later; both old services stop + and the new ones start, and a request in between is refused (Windows + cannot bind a port still listening) or reset.""" + before = server.output() + accepting = before.count("Accepting connections") + exiting = before.count("Exiting thread") + reply = t.http(url + path) + deadline = time.time() + 10 + while time.time() < deadline: + now = server.output() + if (now.count("Accepting connections") >= accepting + 2 + and now.count("Exiting thread") >= exiting + 2): + break + time.sleep(0.05) + return reply + # --- the predefined pages ----------------------------------------- t.eq(200, t.http(url + "/").status, "the main page") t.eq(200, t.http(url + "/C").status, "the counters page") - t.eq(200, t.http(url + "/R").status, "the reload page") + t.eq(200, reload("/R").status, "the reload page") t.eq(200, t.http(url + "/S").status, "the services page") counters = t.http(url + "/C") t.contains(counters, "countin", "the counters page names the counter type") t.contains(counters, "", "the counters page renders a table") - t.contains(t.http(url + "/R"), "Reload", "the reload page confirms the request") + t.contains(reload("/R"), "Reload", "the reload page confirms the request") t.contains(t.http(url + "/S"), "<", "the services page returns markup") # --- the menu no longer offers the removed config editor -----------