packages feed

hurl-workbench-cli-0.1.0.0: test/fixtures/scenario-isolation/server.py

"""Disposable provider-owned HTTP fixture. Database mode requires real psql.

Only --memory selects the local test fixture; real lease acceptance never falls
back to it. Credentials remain in the provider's environment/protected passfile.
"""
import argparse
import http.server
import json
import os
import pathlib
import subprocess
import threading
import time

parser = argparse.ArgumentParser()
parser.add_argument("--directory", type=pathlib.Path, required=True)
parser.add_argument("--name", required=True, choices=("first", "second"))
parser.add_argument("--mode", default="success", choices=("success", "failure", "cancellation"))
parser.add_argument("--memory", action="store_true")
options = parser.parse_args()
markers = []
lock = threading.Lock()


def sql(statement):
    result = subprocess.run([os.environ["SCENARIO_PSQL"], "-X", "-w", "-Atq"],
                            input=statement.encode(), capture_output=True, timeout=10)
    if result.returncode:
        raise RuntimeError("private database operation failed")
    return result.stdout.decode().strip().splitlines()


if not options.memory:
    assert sql("SELECT current_database();") == [os.environ["PGDATABASE"]]
    assert sql("SELECT current_user;") == [os.environ["PGUSER"]]
    sql("CREATE TABLE scenario_markers (marker text NOT NULL);")


class Handler(http.server.BaseHTTPRequestHandler):
    def log_message(self, *_args):
        pass

    def reply(self, status, body):
        data = json.dumps(body).encode()
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)

    def do_GET(self):
        if self.path == "/health":
            self.reply(200, {})
            return
        with lock:
            values = list(markers) if options.memory else sql("SELECT marker FROM scenario_markers ORDER BY marker;")
        self.reply(200, {"count": len(values), "marker": values[0] if len(values) == 1 else None})

    def do_POST(self):
        data = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
        marker = data["marker"]
        assert marker in ("private-first-marker", "private-second-marker")
        with lock:
            if options.memory:
                markers.append(marker)
            else:
                sql("INSERT INTO scenario_markers VALUES ('" + marker + "');")
        (options.directory / (options.name + ".posted")).touch()
        if options.mode == "failure":
            self.reply(500, {})
            return
        deadline = time.monotonic() + 20
        while options.mode == "cancellation" or not all((options.directory / (name + ".posted")).exists() for name in ("first", "second")):
            if time.monotonic() >= deadline:
                self.reply(504, {})
                return
            time.sleep(0.01)
        self.reply(201, {})


server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
ready = options.directory / (options.name + ".ready.json")
temporary = ready.with_suffix(".tmp")
temporary.touch(mode=0o600)
temporary.write_text(json.dumps({"endpoint": f"http://127.0.0.1:{server.server_port}", "pid": os.getpid()}))
temporary.replace(ready)
server.serve_forever()