Frames, Byte by Byte · capstone
Capstone project: Project: Test the Codec Against the RFC
Your learning codec passed five lessons of checks, but none of those checks are yours. This project writes the tests that keep it honest: a pytest suite in tests/test_frames.py that pins ws_frames.py to the bytes RFC 6455 prints, so any later change that breaks a encode_frame and decode_frame again, and a suite like this one, the course’s version of this file, is what tells you nothing else broke.
What the suite must do
Open tests/test_frames.py. Two tests are supplied. test_bit_string_reads_0x81 checks one byte, and test_a_non_minimal_length_is_refused shows the pattern for a test that expects an error: it calls decode_frame inside try, returns when AssertionError if nothing was raised. Three more are placeholders that fail with assert False. Replace them:
test_encoding_matches_the_rfc: the RFC’s unmasked text frame for “Hello”, itsfor “Hello”, and the headers of its 256-byte and 65536-byte binary frames, each compared with what ws_frames.encode_framereturns.test_decoding_matches_the_rfc: the same RFC bytes fed tows_frames.decode_frame, asserting the, finand the payload that come back.test_unseen_payloads_round_trip: payloads of your own of 0, 125, 126, 65535 and 65536 bytes, the boundaries of the three length forms, each encoded and then decoded, with the result compared with what you started from.
Two rules come from how the suite is checked. First, keep import ws_frames and call the codec through the ws_frames.encode_frame(...), inside each test. Check my work runs your tests against other versions of the codec, swapping one fakes.reader_over in pieces of 7 bytes or more, such as fakes.pieces(frame, 7). reader_over copies its buffer for every piece, so a 65536-byte frame in one-byte pieces costs tens of thousands of copies and slows the run down. Check my work stops any run of your tests that takes longer than a second.
Bytes from the RFC
This chapter writes frames as hex pairs separated by spaces, the way hex(" ") prints them, and bytes.fromhex reads exactly that form, spaces allowed, so you can copy a frame from a lesson into a test without retyping it as numbers. bytes.fromhex("81 05 48 65 6c 6c 6f") is the RFC’s “Hello” frame as a bytes
One test, many cases
Python in Practice Chapter 10 used @pytest.mark.parametrize to run one test function once per case. Read the decorator as “run the test below once for each item in this
@pytest.mark.parametrize(
"opcode, payload",
[(ws_frames.OP_TEXT, b"Hi"), (ws_frames.OP_BINARY, bytes(range(3)))],
)
def test_a_round_trip(opcode, payload):
...
pytest reports each case as its own test, so one failing case names itself. Keep the payloads and expected bytes in the list, as data. The lists are built when the file is imported, so they must not call the codec; the calls belong inside the test.
Why the RFC’s bytes matter
A round trip, encoding a payload and decoding it again, proves that the encoder and decoder agree with each other. It does not prove they agree with anyone else. If both wrote and read the 16-bit length
A codec writes the 16-bit length little-endian in encode_frame and reads it little-endian in decode_frame, so the two agree with each other. Which test catches it?
Run it
This lesson has a
python -m pytest -q
With the file as supplied, the summary line reports 3 failed, 2 passed: the three placeholders fail, and the two supplied tests pass. When your three tests are written, every test should pass, and -q prints one dot per test. The demo program still runs as well:
python frames_demo.py
Then press Check my work. It first runs a few cases from each earlier lesson against your ws_frames.py, to be sure the codec is finished. Then it reads tests/test_frames.py, and runs your tests up to five times: once with a correct codec of its own, where every test must pass and at least five must be collected, and once each with four codecs that contain one realistic mistake, where at least one test must fail. Each failure message names the mistake your tests let through. The test names and payloads are your choice, and one test per case instead of parametrize is fine.
The gap that is left
Every frame in this suite is unmasked, and that is the codec’s limit. RFC 6455 requires a client to
Task
Replace the three Lesson 6 placeholders in tests/test_frames.py with tests, and keep the two supplied tests:
test_encoding_matches_the_rfc: the RFC’s unmasked “Hello” text, its “Hello” , and the headers of its 256-byte and 65536-byte binary frames, compared with what ws_frames.encode_framereturns;test_decoding_matches_the_rfc: the same RFC bytes decoded byws_frames.decode_frame, with the, finand the payload asserted;test_unseen_payloads_round_trip: your own payloads of 0, 125, 126, 65535 and 65536 bytes, encoded, decoded, and compared with the original.
Use @pytest.mark.parametrize for the cases if you like; one test per case is fine too. Keep import ws_frames and call the codec as ws_frames.encode_frame(...) and ws_frames.decode_frame(...) inside the tests, never at the top of the file. Feed frames over 1,000 bytes to fakes.reader_over in pieces of 7 bytes or more.
Run python -m pytest -q in the ws_frames.py, then runs your tests in this process: once with a correct codec of its own, where at least five tests must be collected and all must pass, and once each with four codecs that each contain one realistic mistake, where at least one test must fail. A run that takes longer than 1 second is stopped. The check does not read your terminal.