Skip to content

← Testing with pytest and Hypothesis step 11 of 19

Hard Primitives

Check a Run-Length Codec with Four Properties

In the last article, Hypothesis generated the inputs and you wrote the properties. The grader cannot run Hypothesis, so here the driver supplies fixed sample strings and you write four property checks. Then you see which of five codecs each check catches.

The format

A run-length codec stores each run of a repeated letter as the letter followed by its count. Samples contain only the lowercase letters a to z, or are empty:

"aaab" -> "a3b1"
"aba"  -> "a1b1a1"
""     -> ""
  • A run is as long as it can be, so "aaab" has one run of three as.
  • Runs stay in their original order.
  • A count is a positive decimal number with no leading zeros, and it can have several digits. Ten as encode as "a10", not "a9" or two runs.
  • The codecs work on Python str values.

CODECS maps five names to an (encode, decode) pair: correct, cap_at_nine, bare_single, off_by_one and case_shifted. Only correct follows the format. Leave the codecs as they are. Your checks find each bug through the encode and decode functions they receive.

What to write

Fill in four checks. Each takes a codec's two functions and one sample, and returns True when its property holds and False otherwise:

def round_trip(encode: Encoder, decode: Decoder, sample: str) -> bool
def oracle(encode: Encoder, decode: Decoder, sample: str) -> bool
def length_preserved(encode: Encoder, decode: Decoder, sample: str) -> bool
def never_crashes(encode: Encoder, decode: Decoder, sample: str) -> bool

Keep all three parameters, even in a check that ignores decode.

Check Returns True when
round_trip decoding the encoded sample gives back the sample
oracle the encoder's output equals naive_encode(sample), the reference encoder for the format
length_preserved the counts in the encoder's output, read with counts_in, add up to len(sample)
never_crashes encoding the sample and then decoding the result both finish without raising

Let exceptions propagate out of your checks. The driver's _holds wrapper catches any Exception and counts it as a failed check, so a try block in your check would hide a crash.

What the driver reports

solve(codec=..., samples=...) runs all four checks on every sample. It returns one key per check, and each key lists the samples that failed that check. The lists keep the input order and any repeated samples. An empty sample list gives four empty lists.

For codec="case_shifted" and samples=["", "aaab", "aaab"], solve returns:

{
    "round_trip": [],
    "oracle": ["aaab", "aaab"],
    "length_preserved": [],
    "never_crashes": [],
}

This codec writes uppercase letters, as in "A3B1", and its decoder turns them back into lowercase. So the round trip passes. Only oracle compares the encoded text with the format, so only oracle fails.

What each check can miss

  • length_preserved reads only the counts. It passes "A3B1", whose counts are right and whose letters are wrong.
  • never_crashes never compares the decoded text with the sample.
  • A check catches a bug only when a sample triggers it. cap_at_nine encodes every sample correctly if no run is longer than nine. An empty report means these samples passed, and nothing more.