main ece7682

sandbox_acceptance.py RawHistoryFolder

1#!/usr/bin/env python3
2"""Bounded local + two-SSH-host acceptance using the real vev UI driver."""
3import json
4import select
5import subprocess
6import sys
7import time
8import uuid
9
10REQUEST_TIMEOUT = 20
11
12
13def run(*args, timeout=30, check=True):
14    result = subprocess.run(args, text=True, capture_output=True, timeout=timeout)
15    if check and result.returncode:
16        raise RuntimeError(f"command failed ({result.returncode}): {' '.join(args)}\n{result.stdout}{result.stderr}")
17    return result
18
19
20class Driver:
21    def __init__(self, container, args):
22        self.p = subprocess.Popen(
23            ["docker", "exec", "-i", "-e", "VEV_LOG=debug", container,
24             "vev", "--ui-driver", "--cols", "100", "--rows", "30", *args],
25            stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
26            text=True,
27        )
28        self.ident = 0
29        ready = self.read("ready")
30        self.attachment = ready["attachment"]
31        self.generation = ready.get("generation", 0)
32        deadline = time.monotonic() + REQUEST_TIMEOUT
33        while ready.get("status") != "attached":
34            if time.monotonic() >= deadline:
35                raise RuntimeError(f"UI driver did not attach: {ready}")
36            ready = self.call("capture").get("context", {})
37        self.generation = ready["generation"]
38
39    def read(self, label):
40        readable, _, _ = select.select([self.p.stdout], [], [], REQUEST_TIMEOUT)
41        if not readable:
42            self.close()
43            raise RuntimeError(f"UI driver timed out during {label}")
44        line = self.p.stdout.readline()
45        if not line:
46            err = self.p.stderr.read()
47            raise RuntimeError(f"UI driver EOF during {label}: {err}")
48        envelope = json.loads(line)
49        if "error" in envelope:
50            raise RuntimeError(f"UI driver error during {label}: {envelope['error']}")
51        return envelope["result"]
52
53    def call(self, op, **fields):
54        self.ident += 1
55        request = {"version": 1, "id": self.ident, "op": op,
56                   "attachment": self.attachment, **fields}
57        if op in ("text", "keys"):
58            request.setdefault("generation", self.generation)
59        self.p.stdin.write(json.dumps(request) + "\n")
60        self.p.stdin.flush()
61        result = self.read(op)
62        self.generation = result.get("context", {}).get("generation", self.generation)
63        return result
64
65    def close(self):
66        if self.p.poll() is None:
67            self.p.stdin.close()
68            try:
69                self.p.wait(timeout=5)
70            except subprocess.TimeoutExpired:
71                self.p.kill()
72                self.p.wait(timeout=5)
73
74
75def scenario(container, endpoint=None):
76    name = ("local" if endpoint is None else endpoint.replace("-", "")) + uuid.uuid4().hex[:7]
77    args = ["--session", name]
78    if endpoint:
79        args = ["--remote", endpoint, *args]
80    driver = Driver(container, args)
81    try:
82        context = driver.call("capture")["context"]
83        assert context["status"] == "attached", context
84        assert context["session"]["session_name"] == name, context
85        marker = "OK_" + name
86        accepted = driver.call("text", text=f"printf '{marker}'")
87        assert accepted["status"] == "processed", accepted
88        driver.call("keys", keys=["Enter"])
89        observed = driver.call("wait", expect={"text_contains": marker}, timeout_ms=15000)
90        assert observed["context"]["session"] == context["session"], observed
91        print(f"PASS ui endpoint={endpoint or 'local'} session={name} lifecycle={context['session']['lifecycle_id']}")
92    finally:
93        driver.close()
94    return name
95
96
97def await_remote_listing(container, endpoint, name, timeout=10.0):
98    """Poll `vev ls ENDPOINT` until the broker's observation lists NAME."""
99    deadline = time.monotonic() + timeout
100    listing = ""
101    while True:
102        listing = run("docker", "exec", container, "vev", "ls", endpoint, timeout=15, check=False).stdout
103        if any(line.split()[:1] == [f"{name}@{endpoint}"] for line in listing.splitlines()):
104            print(f"PASS ls endpoint={endpoint} session={name}")
105            return
106        if time.monotonic() >= deadline:
107            raise RuntimeError(f"`vev ls {endpoint}` never listed {name} within {timeout}s:\n{listing}")
108        time.sleep(0.5)
109
110
111def main():
112    if len(sys.argv) != 2:
113        raise SystemExit("usage: sandbox_acceptance.py CLIENT_CONTAINER")
114    client = sys.argv[1]
115    hosts = run("docker", "exec", client, "vev", "host", "list").stdout
116    for endpoint in ("remote-a", "remote-b"):
117        if endpoint not in hosts:
118            raise RuntimeError(f"CLI host list omitted {endpoint}: {hosts}")
119        catalog = run("docker", "exec", client, "vev", "ls", endpoint, timeout=30)
120        print(f"PASS cli endpoint={endpoint} catalog_lines={len(catalog.stdout.splitlines())}")
121    scenario(client)
122    created = {endpoint: scenario(client, endpoint) for endpoint in ("remote-a", "remote-b")}
123    for endpoint, name in created.items():
124        await_remote_listing(client, endpoint, name)
125
126
127if __name__ == "__main__":
128    main()

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close