aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/harness/lazy_failure_probe.py
blob: 357529ac168421a509814f56f0d1b8c04ee9cb5a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#!/usr/bin/env python3
"""
What `lazy()` shows when the module it asks for answers 404.

The shape found live: a tab opened before a hub deploy asks for its not-yet-
loaded modules under the previous `/a/<hash>/` prefix, which the new hub no
longer serves. Every lazily loaded view — Search, Videos, Music, Photos, the
video player — sat on its spinner for good, with an empty console. This
renders the shipped `lazy.js` against a module that does not exist, and one
that does, and reads back what is on the page.

    lazy_failure_probe.py
"""

import http.server
import json
import socketserver
import subprocess
import sys
import tempfile
import threading
import time
from pathlib import Path

STATIC = Path(__file__).resolve().parents[2] / "src" / "meshbay_hub" / "static"
PORT = 8763
RECORDS = []
socketserver.TCPServer.allow_reuse_address = True

FRAME = r"""<!doctype html><html><head><meta charset=utf-8>
<link rel="stylesheet" href="/style.css"></head><body>
<div id="plain"></div><div id="framed"></div><div id="fine"></div>
<script type="module">
import { html, render } from '/vendor/htm-preact.js';
import { initLocale } from '/i18n.js';
import { lazy } from '/lazy.js';

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const errors = [];
const origError = console.error;
console.error = (...a) => { errors.push(a.map(String).join(' ')); origError(...a); };

const read = (id) => {
  const root = document.getElementById(id);
  return {
    spinner: !!root.querySelector('.spinner'),
    notice: (root.querySelector('.lazy-failed p') || {}).textContent || null,
    buttons: [...root.querySelectorAll('.lazy-failed button')].map((b) => b.textContent.trim()),
    overlay: !!root.querySelector('.video-player-overlay .lazy-failed'),
    text: root.textContent.trim(),
  };
};

(async () => {
  try {
    await initLocale();
    // The stale tab's request: a module under a prefix nobody serves.
    const Gone = lazy(() => import('/a/0000000000/gone.js'), 'Gone');
    const frame = (c) => html`<div class="video-overlay video-player-overlay">${c}</div>`;
    const GoneOverlay = lazy(() => import('/a/0000000000/player.js'), 'Player',
      frame(html`<p class="page-message"><span class="spinner"></span></p>`), frame);
    // A module that exists still renders as itself.
    const Fine = lazy(() => import('/icon.js'), 'Icon');

    let closed = 0;
    render(html`<${Gone} />`, document.getElementById('plain'));
    render(html`<${GoneOverlay} onClose=${() => { closed += 1; }} />`,
      document.getElementById('framed'));
    render(html`<${Fine} name="close" />`, document.getElementById('fine'));
    await sleep(1500);

    const out = { plain: read('plain'), framed: read('framed'), errors,
                  fine: !!document.querySelector('#fine svg, #fine .icon') };
    const btns = document.querySelectorAll('#framed .lazy-failed button');
    if (btns[1]) btns[1].click();
    out.closed = closed;
    parent.postMessage(out, '*');
  } catch (err) {
    parent.postMessage({ error: String((err && err.stack) || err) }, '*');
  }
})();
</script></body></html>"""

PAGE = r"""<!doctype html><html><head><meta charset=utf-8></head>
<body style="margin:0"><iframe src="/case" style="width:900px;height:700px;border:0"></iframe>
<script>
addEventListener('message', (e) => fetch('/log', { method: 'POST', body: JSON.stringify(e.data) }));
</script></body></html>"""


class H(http.server.BaseHTTPRequestHandler):
    def log_message(self, *a):
        pass

    def do_POST(self):
        length = int(self.headers.get("Content-Length") or 0)
        if self.path == "/log":
            RECORDS.append(json.loads(self.rfile.read(length).decode()))
        else:
            self.rfile.read(length)
        self.send_response(204)
        self.end_headers()

    def _send(self, body: bytes, ctype: str) -> None:
        self.send_response(200)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def do_GET(self):
        path = self.path.split("?")[0]
        if path == "/":
            self._send(PAGE.encode(), "text/html; charset=utf-8")
        elif path == "/case":
            self._send(FRAME.encode(), "text/html; charset=utf-8")
        else:
            asset = (STATIC / path.lstrip("/")).resolve()
            if not str(asset).startswith(str(STATIC)) or not asset.is_file():
                self.send_response(404)
                self.end_headers()
                return
            self._send(asset.read_bytes(),
                       "text/css" if asset.suffix == ".css"
                       else "text/javascript" if asset.suffix == ".js"
                       else "application/octet-stream")


def main() -> int:
    with socketserver.TCPServer(("127.0.0.1", PORT), H) as srv:
        threading.Thread(target=srv.serve_forever, daemon=True).start()
        with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as profile:
            proc = subprocess.Popen(
                ["google-chrome", "--headless=new", "--disable-gpu", "--no-sandbox",
                 f"--user-data-dir={profile}", "--window-size=900,700",
                 f"http://127.0.0.1:{PORT}/"],
                stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            deadline = time.time() + 60
            while not RECORDS and time.time() < deadline:
                time.sleep(0.2)
            proc.terminate()
            proc.wait(timeout=20)
    if not RECORDS:
        print("the page never reported", file=sys.stderr)
        return 1
    print(json.dumps(RECORDS[0]))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())