test: the lab prints from a signed-in lesson, and judges black and white on the job
A lesson site for the lab (session cookie, one-time addresses, a second origin, a request log) and real right-clicks on its words and on a JPEG, a PNG, a transparent PNG, a one-time picture, another site's, a blob:, a WebP, a canvas, a linked picture, a broken one, and Cancel. Each is judged on the job CUPS kept — where and how large, no other ink — and on the site log: nothing was fetched again. The clipboard is the teacher's afterwards. Black and white: a colourful lesson turned back and forth five times on page 2, a worksheet, a picture at 50% top left, a page full of pictures, back to Color, and Cancel. Every grey preview is grey pixel for pixel and the job is byte-for-byte the file shown, through pxlmono. The three 0.2.6.2 black-and-white scenarios now wait for the grey file before Print, as a teacher has to. 343 checks, none failing. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
594fbd810e
commit
cb12e59dea
@@ -0,0 +1,128 @@
|
||||
"""A signed-in lesson with pictures in it, for right-click printing.
|
||||
|
||||
wonderink-lesson-site.py PICTURES PORT OTHER_PORT LOG
|
||||
|
||||
Run inside the lab by tools/lab/wonderink-print-check. Stands in for
|
||||
Wonder Ink's own site, which the lab has no account for, with every kind
|
||||
of picture a lesson might put under a teacher's pointer:
|
||||
|
||||
/login sets a session cookie (HttpOnly, SameSite=Lax), as a
|
||||
sign-in does, and goes on to /lesson
|
||||
/lesson the page; its pictures are all behind that cookie
|
||||
/private/NAME a picture, 403 without the cookie
|
||||
/once/N/NAME a picture that can be fetched exactly once — so a
|
||||
print that fetches it again gets a 404, not a picture
|
||||
OTHER_PORT the same pictures from another origin, with no CORS
|
||||
|
||||
Every request is logged, one JSON line each, so a check can say what was
|
||||
fetched while it printed. What was measured with this, and why Print
|
||||
image fetches nothing, is in classroom-wonderink's _take_picture.
|
||||
"""
|
||||
|
||||
import http.server
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
PICTURES = Path(sys.argv[1])
|
||||
PORT, OTHER = int(sys.argv[2]), int(sys.argv[3])
|
||||
LOG = Path(sys.argv[4])
|
||||
TYPES = {".png": "image/png", ".jpg": "image/jpeg", ".webp": "image/webp"}
|
||||
fetched_once = set()
|
||||
lock = threading.Lock()
|
||||
|
||||
LESSON = """<html><head><title>Picture lesson</title><style>
|
||||
body { margin: 8px; font: 15px sans-serif; }
|
||||
img, canvas { width: 180px; height: 120px; object-fit: contain; border: 1px solid #ccc; margin: 4px; }
|
||||
</style></head><body>
|
||||
<h1 style="color:#c00">Noah's Ark</h1>
|
||||
<p id="words">Noah built an ark, and the animals came two by two. Colour the pictures below.</p>
|
||||
<img id="private-jpg" src="/private/ark-wide.jpg">
|
||||
<img id="private-png" src="/private/ark-tall.png">
|
||||
<img id="clear" src="/private/ark-clear.png">
|
||||
<img id="webp" src="/private/ark-wide.webp">
|
||||
<img id="once" src="/once/NONCE/ark-wide.png">
|
||||
<img id="cross" src="http://localhost:OTHER/img/ark-tall.png">
|
||||
<img id="blob">
|
||||
<canvas id="canvas" width="1600" height="1000"></canvas>
|
||||
<img id="broken" src="/private/not-there.png">
|
||||
<a href="/lesson#more"><img id="linked" src="/private/ark-tall.png"></a>
|
||||
<script>
|
||||
var waiting = 2;
|
||||
function one() { if (--waiting === 0) window.__ready = true; }
|
||||
fetch('/private/ark-tall.png', {credentials: 'include'})
|
||||
.then(function (r) { return r.blob(); })
|
||||
.then(function (b) { var i = document.getElementById('blob'); i.onload = one; i.src = URL.createObjectURL(b); });
|
||||
var art = new Image();
|
||||
art.onload = function () { document.getElementById('canvas').getContext('2d').drawImage(art, 0, 0); one(); };
|
||||
art.src = '/private/ark-wide.png';
|
||||
</script></body></html>"""
|
||||
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def log_message(self, *_):
|
||||
pass
|
||||
|
||||
def answer(self, code, body=b"", kind="text/plain", headers=()):
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", kind)
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
for name, value in headers:
|
||||
self.send_header(name, value)
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def picture(self, name):
|
||||
path = PICTURES / Path(name).name
|
||||
if not path.is_file() or path.suffix not in TYPES:
|
||||
return self.answer(404, b"no such picture")
|
||||
return self.answer(200, path.read_bytes(), TYPES[path.suffix],
|
||||
[("Cache-Control", "private, max-age=600")])
|
||||
|
||||
def do_GET(self):
|
||||
path = urlparse(self.path).path
|
||||
signed_in = "session=ok" in self.headers.get("Cookie", "")
|
||||
|
||||
with lock, open(LOG, "a") as log:
|
||||
log.write(json.dumps({"t": time.time(), "port": self.server.server_port,
|
||||
"path": path, "signed_in": signed_in}) + "\n")
|
||||
|
||||
if self.server.server_port == OTHER:
|
||||
if path.startswith("/img/"):
|
||||
return self.picture(path[5:])
|
||||
return self.answer(404)
|
||||
|
||||
if path == "/login":
|
||||
return self.answer(302, headers=[
|
||||
("Set-Cookie", "session=ok; Path=/; HttpOnly; SameSite=Lax"),
|
||||
("Location", "/lesson")])
|
||||
|
||||
if not signed_in:
|
||||
return self.answer(403, b"sign in first")
|
||||
|
||||
if path == "/lesson":
|
||||
page = LESSON.replace("NONCE", uuid.uuid4().hex).replace("OTHER", str(OTHER))
|
||||
return self.answer(200, page.encode(), "text/html")
|
||||
|
||||
if path.startswith("/private/"):
|
||||
return self.picture(path[9:])
|
||||
|
||||
if path.startswith("/once/"):
|
||||
with lock:
|
||||
if path in fetched_once:
|
||||
return self.answer(404, b"this address has been used")
|
||||
fetched_once.add(path)
|
||||
return self.picture(path.rsplit("/", 1)[1])
|
||||
|
||||
return self.answer(404)
|
||||
|
||||
|
||||
for port in (OTHER, PORT):
|
||||
server = http.server.ThreadingHTTPServer(("127.0.0.1", port), Handler)
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
|
||||
threading.Event().wait()
|
||||
Reference in New Issue
Block a user