0%

Capstone: A Live Scoreboard · capstone

Capstone project: Run the Whole Quiz Night

The server and the client have never yet run together. This lesson runs them through a whole quiz night, adds the test that pins how a quiz ends, and checks the entire project at once. One scenario of pl_ws.py puts your two programs on stage:

python pl_ws.py quiz-night

It calls your server’s main in one thread and your client’s main for viewer A and for every scorekeeper, plays two viewers of its own, B and S, and slows S down on purpose. A finished version, run on the site, printed the transcript below; read it block by block. Lines with | are a program’s standard output, lines with ! its standard error, and PORT stands for the the system chose.

scenario quiz-night: your server and your client through a whole quiz night
viewer A is your watch; B is this program's; S is this program's and is slowed down on purpose
part 1: a quiz of three updates; first a scorekeeper with the wrong token, then three scores
your server's main() is called with --port 0 --max-updates 3 --deadline 6
viewer B joined; its first message was a snapshot with version 0
viewer S joined; its first message was a snapshot with version 0
scorekeeper (wrong token) ! the scoreboard refused the connection: HTTP 401
scorekeeper (wrong token) exited with 1
scorekeeper | version 1: Owls 3, Foxes 0, Herons 0
scorekeeper exited with 0
scorekeeper | version 2: Owls 3, Foxes 2, Herons 0
scorekeeper exited with 0
scorekeeper | version 3: Owls 3, Foxes 2, Herons 5
scorekeeper exited with 0

Your server refused the wrong token before the upgrade, and your score turned the 401 into one standard-error line and exit status 1. Then three scores, each answered with its snapshot.

viewer A (your client) | version 0: Owls 0, Foxes 0, Herons 0
viewer A (your client) | version 1: Owls 3, Foxes 0, Herons 0
viewer A (your client) | version 2: Owls 3, Foxes 2, Herons 0
viewer A (your client) | version 3: Owls 3, Foxes 2, Herons 5
viewer A (your client) exited with 0
viewer B received versions [0, 1, 2, 3], then the server closed it with 1000 'the quiz is over'
viewer S received versions [0, 1, 3], then the server closed it with 1000 'the quiz is over'
your server | listening on 127.0.0.1:PORT
your server | the quiz is over at version 3; every viewer was closed with 1000
your main() returned 0

Your watch printed every version from 0 to 3 and left with 0 on the server’s close. Viewer S never saw version 2: while its thread was still busy sending version 1, version 3 replaced version 2 in its outbox. It still received the last version before its close with 1000, which is exactly Lesson 1’s rule. Which versions S receives depends on timing, so your run may show a different ; what never changes is that its last version is the quiz’s last version, followed by 1000.

part 2: a quiz of ten updates with a deadline of 1.5 seconds; one score
your server's main() is called with --port 0 --max-updates 10 --deadline 1.5
viewer B joined; its first message was a snapshot with version 0
viewer S joined; its first message was a snapshot with version 0
scorekeeper | version 1: Owls 2, Foxes 0, Herons 0
scorekeeper exited with 0
viewer A (your client) | version 0: Owls 0, Foxes 0, Herons 0
viewer A (your client) | version 1: Owls 2, Foxes 0, Herons 0
viewer A (your client) exited with 0
viewer B received versions [0, 1], then the server closed it with 1001 'the scoreboard is closing'
viewer S received versions [0, 1], then the server closed it with 1001 'the scoreboard is closing'
your server | listening on 127.0.0.1:PORT
your server | the deadline passed at version 1; every connection was closed with 1001
your main() returned 0
the token from token.txt appears in no line of this transcript

The second quiz could not finish: ten updates were due and only one came. At the deadline your server closed everyone with 1001, your watch took that as a normal ending, and main still returned 0. The last line is a check pl_ws.py makes on its own output; one more line, not shown, counts threads and changes from run to run.

In a quiz that finishes at version 3, the slowed viewer S received versions 0, 1 and 3, then a close with 1000. Is that a fault?

The last test

The ending is the rule this chapter added, and nothing in your suite pins it yet. Replace the last stub in tests/test_scoreboard.py, test_the_last_update_is_sent_before_the_close. Make a board with max_updates=1 and apply one score, so the quiz is over at version 1. Build a stand-in viewer with types.SimpleNamespace: an id, a send that records each message, parsed, and a close(code, reason) that records the code. Call scoreboard_server.viewer_handler(viewer, board). It returns at once, because the last snapshot is already the join snapshot: nothing has to wait and no recv is needed. Then assert that the last message sent has version 1, that the closes recorded are exactly [1000], and that board["finished"].is_set().

What Check my work checks

This is the whole acceptance check of the capstone, in words. Importing the prints nothing, and the envelopes still round-trip and refuse. Under every interleaving it can arrange, a viewer gets its join snapshot and then only newer, whole boards, never from under a lock; three quick scores leave one snapshot waiting; and at the end of a quiz each viewer is sent the last snapshot, then closed once with 1000, and finished goes up only when nobody waits. A failed, a dead and a crashing viewer each leave the registry. The scorekeeper’s answers, the token check, the room limit, the server’s address, bounds, endings and exit statuses, the client’s output and exit statuses, and your contract’s facts are checked as before, and the token appears nowhere. Then your tests run against nine versions that each make one realistic mistake: Lesson 4’s eight and a viewer that closes with 1000 without sending the last snapshot.

Optional: run it on your own computer

This part is not assessed, and nothing on the site sees it: no lesson asks for a result from it, and nothing is sent back. If you want to watch your scoreboard outside the browser, press Download project, extract the ZIP into a new, empty folder, and open a in that folder. You need Python 3.11 or newer, because websockets 17.4 requires it; each tab starts by checking. If the number is lower, install a newer Python from python.org and use its name, such as python3.13 (or py -3.13 on Windows), wherever the commands say python3 (or py). The project’s README.md holds the same commands.

Check the version, set up once, then start the server in the first terminal:

python3 --version
python3 -m venv .venv
./.venv/bin/python -m pip install -r requirements.txt
./.venv/bin/python -m pytest -q
./.venv/bin/python scoreboard_server.py --max-updates 2 --deadline 60

In a second terminal in the same folder, watch; in a third, score twice:

./.venv/bin/python scoreboard_client.py watch --url ws://127.0.0.1:8765/watch --max-messages 3 --wait 60
./.venv/bin/python scoreboard_client.py score --url ws://127.0.0.1:8765/score --team Owls --points 3
./.venv/bin/python scoreboard_client.py score --url ws://127.0.0.1:8765/score --team Foxes --points 2

Every command names the Python inside .venv, so there is nothing to activate. Three terminals, because watch keeps its terminal busy until its last snapshot.

terminal 1   server: listening on 127.0.0.1:8765 ... closing line, exit 0
terminal 2   watch: version 0, version 1, version 2, exit 0
terminal 3   score Owls 3: version 1      score Foxes 2: version 2
all three    talk over 127.0.0.1, which never leaves your computer

In words: one terminal holds the server, one the viewer, and one the scorekeeper, and all three talk to each other on your own computer only. With a finished version on a Mac, these commands gave this run: the server printed listening on 127.0.0.1:8765; the two scores printed version 1: Owls 3, Foxes 0, Herons 0 and version 2: Owls 3, Foxes 2, Herons 0, each with exit status 0; watch printed versions 0 to 2 and exited with 0; and the server printed its closing line and exited with 0. A watch started after that printed could not connect to ws://127.0.0.1:8765/watch and exited with 1, because nothing listened any more.

Why does the run on your own computer need three terminals, not two?

After your run on your own computer, what does Python Land know about it?

The finished scoreboard

You have a server with a written contract, bounds and two clean endings, a client that turns every refusal and close into an exit status, and tests that notice realistic mistakes. Many servers you will meet look different from yours, though: they wait for many connections with one instead of a thread each. Chapter 11 gives you a taste of that.

Task

In tests/test_scoreboard.py, replace the last stub, test_the_last_update_is_sent_before_the_close, keeping its name: on a board made with scoreboard_server.make_board([...], max_updates=1) and given one score, call scoreboard_server.viewer_handler with a stand-in whose send records each message and whose close(code, reason) records the code, then assert that the last message sent has version 1, that the codes recorded are [1000], and that board["finished"].is_set(). Keep every other test, and keep every earlier lesson’s code correct.

Run python -m pytest -q until every test passes, and run python pl_ws.py quiz-night. Then press Check my work. It checks the whole capstone, with stand-ins and no thread or : the imports print nothing; the envelopes round-trip and refuse; the viewer under every interleaving it can arrange, in a quiz far from over and at its end; coalescing; a failed, a dead and a crashing viewer; the scorekeeper’s answers; the token check before 101 with hmac.compare_digest; the room limit; serve_scoreboard and main; watch, score and the supplied client main; the facts in SCOREBOARD_PROTOCOL.md; and the token on no stream, in no and in no message, all as in Lessons 1 to 3. Then it runs your tests in this process: once against your own code, where at least 11 tests must be collected and all must pass, then once each with one of nine replaced by a version that makes one realistic mistake, Lesson 4’s eight and a viewer_handler that closes with 1000 without sending the last snapshot. Against each, at least one test must fail. A run still going after 3 seconds is stopped, and against a mistaken version that counts as noticed; no new run starts once 8 seconds have passed. Extra tests, helpers and edits to README.md are fine. The optional run on your own computer is not checked.