Adding e2e testing
This commit is contained in:
@@ -85,3 +85,7 @@ coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Playwright artifacts
|
||||
playwright-report/
|
||||
test-results/
|
||||
|
||||
@@ -208,6 +208,20 @@ Or in Docker:
|
||||
docker compose exec api pytest --cov=app --cov-report=term-missing
|
||||
```
|
||||
|
||||
### End-to-End Testing
|
||||
Browser e2e tests use pytest plus Playwright. The recommended path is the containerized runner, which joins the same Compose network as the app and opens the web service at `http://web`.
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.e2e.yml up -d db api web
|
||||
docker compose -f docker-compose.yml -f docker-compose.e2e.yml run --rm e2e
|
||||
```
|
||||
|
||||
Authenticated browser tests are skipped unless `E2E_ADMIN_USERNAME` and `E2E_ADMIN_PASSWORD` are supplied. See [`tests/e2e/README.md`](./tests/e2e/README.md) for credential examples, host-run instructions, and guidance for adding specs.
|
||||
|
||||
The e2e Compose override uses a separate MySQL container and volume, so tests do not use the normal dev/prod database configured in `.env`. It is still a real MySQL database, but isolated for e2e.
|
||||
|
||||
E2e reports are written to `test-results/e2e-report.html` and `test-results/e2e-junit.xml`.
|
||||
|
||||
## Additional Features
|
||||
|
||||
### Email Notifications
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
services:
|
||||
db:
|
||||
container_name: ppr_e2e_db
|
||||
build:
|
||||
context: .
|
||||
dockerfile: tests/e2e/mysql.Dockerfile
|
||||
image: pprdev-e2e-db
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: e2e_root_password
|
||||
MYSQL_DATABASE: ppr_e2e
|
||||
MYSQL_USER: ppr_e2e
|
||||
MYSQL_PASSWORD: ppr_e2e_password
|
||||
volumes:
|
||||
- ppr_e2e_mysql_data:/var/lib/mysql
|
||||
|
||||
api:
|
||||
container_name: ppr_e2e_api
|
||||
ports:
|
||||
- "${E2E_API_PORT_EXTERNAL:-18002}:8000"
|
||||
environment:
|
||||
DB_HOST: db
|
||||
DB_PORT: 3306
|
||||
DB_NAME: ppr_e2e
|
||||
DB_USER: ppr_e2e
|
||||
DB_PASSWORD: ppr_e2e_password
|
||||
BASE_URL: http://web
|
||||
MAIL_HOST: 127.0.0.1
|
||||
MAIL_PORT: 1
|
||||
MAIL_USERNAME: e2e
|
||||
MAIL_PASSWORD: e2e
|
||||
MAIL_FROM: e2e@example.com
|
||||
MAIL_FROM_NAME: PPR E2E
|
||||
|
||||
web:
|
||||
container_name: ppr_e2e_web
|
||||
ports:
|
||||
- "${E2E_WEB_PORT_EXTERNAL:-18055}:80"
|
||||
environment:
|
||||
BASE_URL: ""
|
||||
command: >
|
||||
sh -c "if [ -z \"$${BASE_URL}\" ]; then API_BASE='/api/v1'; else API_BASE=\"$${BASE_URL}/api/v1\"; fi;
|
||||
printf 'window.PPR_CONFIG = { apiBase: \"%s\" };' \"$${API_BASE}\" > /usr/share/nginx/html/config.js;
|
||||
nginx -g 'daemon off;'"
|
||||
|
||||
e2e:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: tests/e2e/Dockerfile
|
||||
working_dir: /workspace
|
||||
volumes:
|
||||
- .:/workspace
|
||||
environment:
|
||||
E2E_BASE_URL: http://web
|
||||
E2E_ADMIN_USERNAME: ${E2E_ADMIN_USERNAME:-}
|
||||
E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-}
|
||||
E2E_HEALTH_URL: http://api:8000/health
|
||||
E2E_ARTIFACT_UID: ${E2E_ARTIFACT_UID:-1000}
|
||||
E2E_ARTIFACT_GID: ${E2E_ARTIFACT_GID:-1000}
|
||||
PYTHONDONTWRITEBYTECODE: "1"
|
||||
depends_on:
|
||||
- web
|
||||
networks:
|
||||
- public_network
|
||||
command: >
|
||||
bash -lc "mkdir -p test-results &&
|
||||
python tests/e2e/wait_for_web.py &&
|
||||
pytest tests/e2e
|
||||
--browser chromium
|
||||
--tracing=retain-on-failure
|
||||
--screenshot=only-on-failure
|
||||
--junitxml=test-results/e2e-junit.xml
|
||||
--html=test-results/e2e-report.html
|
||||
--self-contained-html;
|
||||
status=$$?;
|
||||
chown -R \"$${E2E_ARTIFACT_UID}:$${E2E_ARTIFACT_GID}\" test-results || true;
|
||||
exit $$status"
|
||||
|
||||
volumes:
|
||||
ppr_e2e_mysql_data:
|
||||
@@ -0,0 +1,6 @@
|
||||
FROM mcr.microsoft.com/playwright/python:v1.45.0-jammy
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
COPY tests/e2e/requirements.txt /tmp/e2e-requirements.txt
|
||||
RUN pip install --no-cache-dir -r /tmp/e2e-requirements.txt
|
||||
@@ -0,0 +1,90 @@
|
||||
# End-to-End Tests
|
||||
|
||||
The e2e suite uses pytest plus Playwright. The preferred path is the containerized runner, which joins the same Docker Compose network as the web app and opens `http://web`.
|
||||
|
||||
## Containerized Run
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.e2e.yml up -d db api web
|
||||
docker compose -f docker-compose.yml -f docker-compose.e2e.yml run --rm e2e
|
||||
```
|
||||
|
||||
The e2e override sets `E2E_BASE_URL=http://web`, makes the web container generate relative API config for browser-side requests, and forces the API container to use an e2e-only Compose database.
|
||||
|
||||
The e2e database is a real MySQL server, but it is isolated from the normal dev database:
|
||||
|
||||
- DB container: `ppr_e2e_db`
|
||||
- API container: `ppr_e2e_api`
|
||||
- Web container: `ppr_e2e_web`
|
||||
- Database name: `ppr_e2e`
|
||||
- Volume: `pprdev_ppr_e2e_mysql_data`
|
||||
|
||||
The e2e DB image is plain `mysql:8.0`, so the API should see a fresh empty database and create schema through Alembic migrations instead of stamping an older `db-init` schema.
|
||||
|
||||
Authenticated tests are skipped unless credentials are supplied:
|
||||
|
||||
```bash
|
||||
E2E_ADMIN_USERNAME=admin \
|
||||
E2E_ADMIN_PASSWORD=admin123 \
|
||||
docker compose -f docker-compose.yml -f docker-compose.e2e.yml run --rm e2e
|
||||
```
|
||||
|
||||
## Rebuild The Test Image
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.e2e.yml build e2e
|
||||
```
|
||||
|
||||
## Test Evidence
|
||||
|
||||
Containerized runs write evidence to `test-results/`:
|
||||
|
||||
- `test-results/e2e-report.html` for a human-readable report
|
||||
- `test-results/e2e-junit.xml` for CI systems
|
||||
- Playwright traces and screenshots on failures
|
||||
|
||||
If the report files are not owned by your host user, pass your UID/GID:
|
||||
|
||||
```bash
|
||||
E2E_ARTIFACT_UID=$(id -u) \
|
||||
E2E_ARTIFACT_GID=$(id -g) \
|
||||
docker compose -f docker-compose.yml -f docker-compose.e2e.yml run --rm e2e
|
||||
```
|
||||
|
||||
## Host Run
|
||||
|
||||
Running on the host is still supported for quick debugging if Python and Playwright are installed locally.
|
||||
|
||||
First-time host setup:
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r backend/requirements.txt
|
||||
pip install -r tests/e2e/requirements.txt
|
||||
python -m playwright install --with-deps chromium
|
||||
```
|
||||
|
||||
Run against a host-exposed web port:
|
||||
|
||||
```bash
|
||||
E2E_BASE_URL=http://localhost:8082 pytest tests/e2e
|
||||
```
|
||||
|
||||
Run authenticated host tests:
|
||||
|
||||
```bash
|
||||
E2E_BASE_URL=http://localhost:8082 \
|
||||
E2E_ADMIN_USERNAME=admin \
|
||||
E2E_ADMIN_PASSWORD=admin123 \
|
||||
pytest tests/e2e
|
||||
```
|
||||
|
||||
## Adding Tests
|
||||
|
||||
Put browser specs in `tests/e2e/test_*.py`. Start with user-visible behavior and stable selectors:
|
||||
|
||||
- Navigate through the same URLs users open.
|
||||
- Prefer roles and labels, such as `get_by_role()` and `get_by_label()`.
|
||||
- Use API setup only when a test needs specific records to exist.
|
||||
- Keep specs independent so they can run in any order.
|
||||
@@ -0,0 +1,27 @@
|
||||
import os
|
||||
|
||||
from playwright.sync_api import expect
|
||||
|
||||
|
||||
BASE_URL = os.getenv("E2E_BASE_URL", "http://localhost:8082").rstrip("/")
|
||||
ADMIN_USERNAME = os.getenv("E2E_ADMIN_USERNAME")
|
||||
ADMIN_PASSWORD = os.getenv("E2E_ADMIN_PASSWORD")
|
||||
|
||||
|
||||
def app_url(path):
|
||||
return f"{BASE_URL}/{path.lstrip('/')}"
|
||||
|
||||
|
||||
def login_as_admin(page):
|
||||
if page.locator("#login-username").is_visible():
|
||||
page.locator("#login-username").fill(ADMIN_USERNAME)
|
||||
page.locator("#login-password").fill(ADMIN_PASSWORD)
|
||||
page.locator("#login-btn").click()
|
||||
expect(page.locator("#current-user")).to_have_text(ADMIN_USERNAME)
|
||||
if page.get_by_role("heading", name="Login").count() > 0:
|
||||
expect(page.get_by_role("heading", name="Login")).to_be_hidden()
|
||||
|
||||
|
||||
def skip_without_admin_credentials(pytest):
|
||||
if not ADMIN_USERNAME or not ADMIN_PASSWORD:
|
||||
pytest.skip("Set E2E_ADMIN_USERNAME and E2E_ADMIN_PASSWORD to run authenticated e2e tests")
|
||||
@@ -0,0 +1 @@
|
||||
FROM mysql:8.0
|
||||
@@ -0,0 +1,3 @@
|
||||
playwright==1.45.0
|
||||
pytest-playwright>=0.5,<0.6
|
||||
pytest-html>=4,<5
|
||||
@@ -0,0 +1,73 @@
|
||||
import re
|
||||
|
||||
import pytest
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from helpers import app_url, login_as_admin, skip_without_admin_credentials
|
||||
|
||||
|
||||
def open_admin_dropdown(page):
|
||||
page.locator("#adminDropdownBtn").click()
|
||||
admin_menu = page.locator("#adminDropdownMenu")
|
||||
expect(admin_menu).to_have_class(re.compile("active"))
|
||||
return admin_menu
|
||||
|
||||
|
||||
def open_admin_page(page):
|
||||
skip_without_admin_credentials(pytest)
|
||||
page.goto(app_url("/admin"))
|
||||
expect(page).to_have_title(re.compile("PPR Admin Interface"))
|
||||
expect(page.get_by_role("heading", name=re.compile("Swansea Tower"))).to_be_visible()
|
||||
login_as_admin(page)
|
||||
|
||||
|
||||
def test_actions_and_admin_dropdowns_toggle_exclusively(page):
|
||||
open_admin_page(page)
|
||||
|
||||
actions_menu = page.locator("#actionsDropdownMenu")
|
||||
admin_menu = page.locator("#adminDropdownMenu")
|
||||
|
||||
expect(actions_menu).not_to_have_class(re.compile("active"))
|
||||
expect(admin_menu).not_to_have_class(re.compile("active"))
|
||||
|
||||
page.locator("#actionsDropdownBtn").click()
|
||||
expect(actions_menu).to_have_class(re.compile("active"))
|
||||
expect(admin_menu).not_to_have_class(re.compile("active"))
|
||||
expect(actions_menu.get_by_role("link", name=re.compile("New PPR"))).to_be_visible()
|
||||
expect(actions_menu.get_by_role("link", name=re.compile("Book Out"))).to_be_visible()
|
||||
expect(actions_menu.get_by_role("link", name=re.compile("Book In"))).to_be_visible()
|
||||
expect(actions_menu.get_by_role("link", name=re.compile("Overflight"))).to_be_visible()
|
||||
|
||||
admin_menu = open_admin_dropdown(page)
|
||||
expect(actions_menu).not_to_have_class(re.compile("active"))
|
||||
expect(admin_menu.get_by_role("link", name=re.compile("Admin View"))).to_be_visible()
|
||||
expect(admin_menu.get_by_role("link", name=re.compile("ATC View"))).to_be_visible()
|
||||
expect(admin_menu.get_by_role("link", name=re.compile("Reports"))).to_be_visible()
|
||||
expect(admin_menu.get_by_role("link", name=re.compile("Drone Requests"))).to_be_visible()
|
||||
expect(admin_menu.get_by_role("link", name=re.compile("Journal Log"))).to_be_visible()
|
||||
|
||||
page.locator(".container").click()
|
||||
expect(admin_menu).not_to_have_class(re.compile("active"))
|
||||
|
||||
|
||||
def test_admin_menu_links_navigate_to_expected_pages(page):
|
||||
open_admin_page(page)
|
||||
|
||||
menu_expectations = [
|
||||
("Admin View", re.compile(r"/admin$"), re.compile("PPR Admin Interface"), "title"),
|
||||
("ATC View", re.compile(r"/atc$"), re.compile("ATC Management Interface"), "title"),
|
||||
("Reports", re.compile(r"/reports$"), re.compile("PPR Reports"), "heading"),
|
||||
("Drone Requests", re.compile(r"/drone-requests$"), re.compile("Drone Flight Requests"), "heading"),
|
||||
("Journal Log", re.compile(r"/journal$"), re.compile("Journal Log"), "heading"),
|
||||
]
|
||||
|
||||
for label, url_pattern, expected_text, assertion_type in menu_expectations:
|
||||
page.goto(app_url("/admin"))
|
||||
login_as_admin(page)
|
||||
admin_menu = open_admin_dropdown(page)
|
||||
admin_menu.get_by_role("link", name=re.compile(label)).click()
|
||||
expect(page).to_have_url(url_pattern)
|
||||
if assertion_type == "title":
|
||||
expect(page).to_have_title(expected_text)
|
||||
else:
|
||||
expect(page.get_by_role("heading", name=expected_text)).to_be_visible()
|
||||
@@ -0,0 +1,64 @@
|
||||
import re
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from helpers import app_url, login_as_admin, skip_without_admin_credentials
|
||||
|
||||
|
||||
def drone_payload(operator_name):
|
||||
return {
|
||||
"operator_name": operator_name,
|
||||
"operator_id": "E2E-OP",
|
||||
"flyer_name": "E2E Remote Pilot",
|
||||
"flyer_id": "E2E-FLYER",
|
||||
"email": "drone-e2e@example.com",
|
||||
"phone": "0123456789",
|
||||
"flight_date": "2026-06-21",
|
||||
"estimated_takeoff_time": "10:00",
|
||||
"estimated_completion_time": "10:30",
|
||||
"estimated_takeoff_at": "2026-06-21T10:00:00",
|
||||
"estimated_completion_at": "2026-06-21T10:30:00",
|
||||
"maximum_elevation_ft_amsl": 200,
|
||||
"location_description": "E2E north apron survey",
|
||||
"location_latitude": 51.623389,
|
||||
"location_longitude": -4.069231,
|
||||
"location_inside_frz": "yes",
|
||||
"notes": "Created by Playwright e2e",
|
||||
}
|
||||
|
||||
|
||||
def test_drone_requests_requires_login_and_loads_queue(page):
|
||||
skip_without_admin_credentials(pytest)
|
||||
|
||||
page.goto(app_url("/drone-requests"))
|
||||
|
||||
expect(page).to_have_title(re.compile("Drone Flight Requests"))
|
||||
expect(page.get_by_role("heading", name="Drone Flight Requests")).to_be_visible()
|
||||
expect(page.get_by_role("heading", name="Login")).to_be_visible()
|
||||
|
||||
login_as_admin(page)
|
||||
|
||||
expect(page.locator("#request-list-body")).not_to_contain_text("Loading requests...")
|
||||
expect(page.locator("#request-count")).to_have_text(re.compile(r"\d+"))
|
||||
|
||||
|
||||
def test_public_drone_request_appears_in_tower_queue(page):
|
||||
skip_without_admin_credentials(pytest)
|
||||
|
||||
operator_name = f"E2E Rotor Ops {datetime.utcnow().strftime('%H%M%S%f')}"
|
||||
create_response = page.request.post(
|
||||
app_url("/api/v1/drone-requests/public"),
|
||||
data=json.dumps(drone_payload(operator_name)),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
assert create_response.ok, create_response.text()
|
||||
created = create_response.json()
|
||||
|
||||
page.goto(app_url("/drone-requests"))
|
||||
login_as_admin(page)
|
||||
|
||||
expect(page.locator("#request-list-body")).to_contain_text(created["reference_number"])
|
||||
expect(page.locator("#request-list-body")).to_contain_text(operator_name)
|
||||
@@ -0,0 +1,56 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from helpers import app_url, login_as_admin, skip_without_admin_credentials
|
||||
|
||||
|
||||
def local_flight_row(page, registration):
|
||||
return page.locator("#local-flights-table-body tr").filter(has_text=registration)
|
||||
|
||||
|
||||
def confirm_timestamp(page, button_text):
|
||||
expect(page.locator("#timestampModal")).to_be_visible()
|
||||
expect(page.locator("#timestamp-submit-btn")).to_contain_text(button_text)
|
||||
page.locator("#timestamp-submit-btn").click()
|
||||
expect(page.locator("#timestampModal")).to_be_hidden()
|
||||
|
||||
|
||||
def test_operator_books_out_local_flight_records_touch_and_go_and_lands(page):
|
||||
skip_without_admin_credentials(pytest)
|
||||
registration = f"GE2E{datetime.utcnow().strftime('%H%M%S')}"
|
||||
|
||||
page.goto(app_url("/admin"))
|
||||
login_as_admin(page)
|
||||
|
||||
page.locator("#actionsDropdownBtn").click()
|
||||
page.locator("#actionsDropdownMenu").get_by_role("link", name="🛫 Book Out (L)").click()
|
||||
|
||||
expect(page.locator("#localFlightModal")).to_be_visible()
|
||||
page.locator("#local_registration").fill(registration)
|
||||
page.locator("#local_type").fill("PA28")
|
||||
page.locator("#local_pob").fill("1")
|
||||
page.locator("#local_flight_type").select_option("LOCAL")
|
||||
page.locator("#local_duration").fill("45")
|
||||
page.locator("#local_notes").fill("E2E local flight lifecycle")
|
||||
page.locator("#local-flight-form").get_by_role("button", name="🛫 Book Out").click()
|
||||
|
||||
expect(page.locator("#localFlightModal")).to_be_hidden()
|
||||
row = local_flight_row(page, registration)
|
||||
expect(row).to_be_visible()
|
||||
expect(row).to_contain_text("GROUND")
|
||||
|
||||
row.get_by_role("button", name="TAKE OFF").click()
|
||||
confirm_timestamp(page, "Confirm Takeoff")
|
||||
expect(row).to_contain_text("LOCAL")
|
||||
|
||||
row.get_by_role("button", name="T&G").click()
|
||||
expect(page.locator("#circuitModal")).to_be_visible()
|
||||
page.locator("#circuit-form").get_by_role("button", name="Record Circuit").click()
|
||||
expect(page.locator("#circuitModal")).to_be_hidden()
|
||||
expect(row.locator("td").nth(7)).to_have_text("1")
|
||||
|
||||
row.get_by_role("button", name="LAND").click()
|
||||
confirm_timestamp(page, "Confirm Landing")
|
||||
expect(local_flight_row(page, registration)).to_have_count(0)
|
||||
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from playwright.sync_api import expect
|
||||
|
||||
|
||||
BASE_URL = os.getenv("E2E_BASE_URL", "http://localhost:8082").rstrip("/")
|
||||
|
||||
|
||||
def app_url(path):
|
||||
return f"{BASE_URL}/{path.lstrip('/')}"
|
||||
|
||||
|
||||
def test_flight_information_display_loads(page):
|
||||
page.goto(app_url("/"))
|
||||
|
||||
expect(page).to_have_title(re.compile("Swansea Airport - Arrivals & Departures"))
|
||||
expect(page.get_by_role("heading", name="Flight Information")).to_be_visible()
|
||||
|
||||
|
||||
def test_public_ppr_form_loads(page):
|
||||
page.goto(app_url("/ppr.html"))
|
||||
|
||||
expect(page).to_have_title(re.compile("Swansea PPR"))
|
||||
expect(page.get_by_role("heading", name=re.compile("PPR Request"))).to_be_visible()
|
||||
expect(page.get_by_role("button", name="Submit PPR Request")).to_be_visible()
|
||||
@@ -0,0 +1,38 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from playwright.sync_api import expect
|
||||
|
||||
|
||||
BASE_URL = os.getenv("E2E_BASE_URL", "http://localhost:8082").rstrip("/")
|
||||
|
||||
|
||||
def app_url(path):
|
||||
return f"{BASE_URL}/{path.lstrip('/')}"
|
||||
|
||||
|
||||
def test_public_ppr_form_submits_successfully(page):
|
||||
unique_registration = f"GE{datetime.utcnow().strftime('%H%M')}"
|
||||
|
||||
page.goto(app_url("/ppr.html"))
|
||||
|
||||
page.locator("#ac_reg").fill(unique_registration)
|
||||
page.locator("#ac_type").fill("PA28")
|
||||
page.locator("#ac_call").fill(unique_registration)
|
||||
page.locator("#captain").fill("E2E Test Pilot")
|
||||
page.locator("#in_from").fill("EGLL")
|
||||
page.locator("#eta-date").fill("2026-06-21")
|
||||
page.locator("#eta-time").select_option("10:00")
|
||||
page.locator("#pob_in").fill("2")
|
||||
page.locator("#fuel").select_option("100LL")
|
||||
page.locator("#out_to").fill("EGFF")
|
||||
page.locator("#etd-date").fill("2026-06-21")
|
||||
page.locator("#etd-time").select_option("12:00")
|
||||
page.locator("#pob_out").fill("2")
|
||||
page.locator("#phone").fill("0123456789")
|
||||
page.locator("#notes").fill("Submitted by Playwright e2e")
|
||||
|
||||
page.get_by_role("button", name="Submit PPR Request").click()
|
||||
|
||||
expect(page.locator("#success-message")).to_be_visible()
|
||||
expect(page.locator("#success-message")).to_contain_text("PPR Request Submitted")
|
||||
@@ -0,0 +1,24 @@
|
||||
import os
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
|
||||
base_url = os.getenv("E2E_BASE_URL", "http://web").rstrip("/")
|
||||
health_url = os.getenv("E2E_HEALTH_URL", f"{base_url}/")
|
||||
deadline = time.time() + int(os.getenv("E2E_WEB_TIMEOUT_SECONDS", "120"))
|
||||
last_error = None
|
||||
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
with urllib.request.urlopen(health_url, timeout=5) as response:
|
||||
if response.status < 500:
|
||||
break
|
||||
except Exception as exc:
|
||||
last_error = exc
|
||||
time.sleep(2)
|
||||
else:
|
||||
raise SystemExit(f"Timed out waiting for {health_url}: {last_error}")
|
||||
|
||||
with urllib.request.urlopen(f"{base_url}/", timeout=5) as response:
|
||||
if response.status >= 500:
|
||||
raise SystemExit(f"Web returned HTTP {response.status} at {base_url}/")
|
||||
Reference in New Issue
Block a user