CS301 · Examination paper
2 hours 30 minutes · 100 marks · pass 40 · Merit 60 · Distinction 70 Closed book, one A4 sheet of handwritten notes. Section E runs on a laptop against a local copy of the repository; no internet, no editor autocompletion, no AI tooling.
Time budget. A 20 marks / 28 min · B 20 marks / 28 min · C 20 marks / 30 min · D 20 marks / 30 min · E 20 marks / 24 min · checking 10 min.
Sections A–D are written. Section E must run: an answer that does not execute scores nothing, however correct it looks. Marks are in bold on every question.
Section A — Write the specifications (20 marks)
Section titled “Section A — Write the specifications (20 marks)”Specifications in the house form: precondition, postcondition, exceptions, frame. No implementation. A clause that restates the signature earns nothing.
A1. (6) EnquiryBand.mark_unavailable in booking/models.py carries this
docstring and nothing else:
Pure: validate + set status. True if changed, False if already there.Raises TransitionRefused if the row may not make this move. Never saves.Write the full specification. The status field is an IntegerField whose values
are CANCELLED −100, CLOSED −10, BAND_UNAVAILABLE −1, OPEN 0, BOOKED 1000.
(a) (4) Pre, post (per state), exceptions, frame. (b) (2) Name the state the docstring leaves undefined, and write the clause that closes it.
A2. (6) booking/services.py holds mark_band_unavailable, which refuses when
an invoice exists with code has_invoice, calls the model method, saves with
update_fields=['status'], then queues an email on commit.
(a) (4) Specify it: pre, post, exceptions, frame. (b) (2) State the invariant that spans the service and the model method, and say which document can hold it.
A3. (4) Two candidate postconditions for extract_quote_fee in
payments/wise.py:
S1: returns the fee of the payment option whose payIn equals the requestS2: returns the fee of the option whose payIn equals the request, and raises when no enabled option matches(a) (2) Which is stronger, and by which clause. (b) (2) Which is the weakest specification that still serves the caller, and why the weaker one is the better choice here.
A4. (4) Against the specification you wrote in A1, write one test that falsifies the current body. Give the status it constructs, the call, and the single assert. State what the current code returns.
Examines. Ch6 A1, A2, A4 · Ch7 A3.
Section B — What the tests cannot catch (20 marks)
Section titled “Section B — What the tests cannot catch (20 marks)”B1. (5) A suite calls split_fee(10000) and asserts (1800, 8200). It is
green. Name three distinct defect classes that pass it, and for each the test
that would catch it.
B2. (5) A permission function’s suite reports 100% line and branch coverage. Name three ways the function can still be wrong, one sentence each.
B3. (5) ExtractQuoteFeeTests contains test_skips_disabled_options, which
asserts 2.00 when BALANCE is disabled. State what that expectation pins, then
rewrite the test so it fails on the current fallback.
B4. (5) A fee refactor loses eighteen pence per invoice. check.py prints the
failures and exits 0.
(a) (2) State what git bisect run python3 check.py reports, and why.
(b) (1) The two failures differ by exactly 0.18 from expectation. State what that
rules out and which function it accuses.
(c) (1) git bisect run over 1000 commits: state the step count.
(d) (1) State the one property the script must have for bisect to work.
Section C — Design an ADT with its rep invariant (20 marks)
Section titled “Section C — Design an ADT with its rep invariant (20 marks)”C1. (6) InstallmentPlan holds installments: tuple[Decimal, ...] and
total: Decimal.
(a) (3) Write the representation invariant and the abstraction function.
(b) (2) total is a stored field, not a property returning
sum(self.installments). State why, in terms of what an invariant is for.
(c) (1) Name the clause that catches a £250 invoice recorded with one £100
payment.
C2. (5) A colleague removes frozen=True from InstallmentPlan, changes the
rep to list[Decimal], and adds add_instalment(amount: Decimal) -> None, which
appends without touching total.
(a) (3) Give two distinct ways to obtain an instance that violates your RI, one through the new mutator and one that never calls it. Name which is which. (b) (2) Give the smallest change that closes each.
C3. (4) FeeBreakdown defines __eq__ and __hash__ over
(amount, booking_fee, planner_fee) and its fields are writable. A corrected
booking_fee is assigned after the object was used as a dictionary key.
(a) (2) State what k in payouts returns afterwards, and what len(payouts)
returns.
(b) (2) State the fix and the one-word reason hashing requires it.
C4. (5) Invoice enforces amount >= booking_fee + planner_fee in
Meta.constraints, computes band_fee as a GeneratedField, and checks
deposit_amount <= amount with an assert in clean().
(a) (3) For each of the three, state what forces the choice of enforcement point. (b) (2) State what none of the three catches.
Examines. Ch9 C2 · Ch10 C1, C2, C4 · Ch11 C1(a), C3.
Section D — Review a diff (20 marks)
Section titled “Section D — Review a diff (20 marks)”D1. (8) A PR claims to fix the case defect in extract_quote_fee.
@@ -712,6 +712,7 @@ def extract_quote_fee(quote: dict, pay_in: str = "BALANCE") -> float: payment_options = quote.get('paymentOptions', [])+ pay_in = pay_in.upper() for option in payment_options:def test_lowercase_pay_in(self): quote = {'paymentOptions': [ {'payIn': 'BANK_TRANSFER', 'disabled': False, 'fee': {'total': 1.90}}, {'payIn': 'BALANCE', 'disabled': False, 'fee': {'total': 4.30}}, ]} self.assertEqual(extract_quote_fee(quote, pay_in='balance'), 1.90)(a) (3) State why the new test is no evidence, naming the value that gives it away. (b) (3) Write the blocking comment: the input, the observed outcome, the intended outcome. Then give your verdict. (c) (2) State the question the fix raises, and whether it blocks.
D2. (6) A branch was cut before a colleague’s PR merged, then edited from that older checkout. The review page shows a small, mostly-additions diff.
(a) (2) State which pair of commits the review page diffs, and why the lost lines
cannot appear in it.
(b) (2) Give the two commands whose output differs, and what the extra - line is.
(c) (2) Give the gate command that would have caught it, and the exit status that
means failure.
D3. (6) A diff adds # type: ignore[arg-type] with no reason and no test.
(a) (2) State the three things you ask the author for.
(b) (2) State why warn_unused_ignores = False in mypy.ini makes the request
urgent rather than pedantic.
(c) (2) The ignore moves a defect from one stage to another. Name both stages.
Examines. Ch4 D1 · Ch5 D2 · Ch1 D3(c) · Ch2 D3(a)–(b).
Section E — Practical, on the laptop (20 marks)
Section titled “Section E — Practical, on the laptop (20 marks)”Work in a local clone of backend/. Paste real output for every part. No
internet, no AI tooling.
E1. (6) In payments/test_wise.py, add to ExtractQuoteFeeTests a test
asserting that pay_in="balance" returns the balance fee, 4.30, with
BANK_TRANSFER listed first in the fixture. Run:
pytest payments/test_wise.py -qPaste the summary and the failing assert.
E2. (5) Make the smallest fix in payments/wise.py. Rerun the same command.
Paste git diff and the new summary line.
E3. (5) Revert only the fix, keeping the test, and rerun. Paste both summaries side by side, then state in one sentence why the pair is the evidence and either run alone is not.
E4. (4) Run mypy-once with the fix reverted. Paste what it reports about
extract_quote_fee, and state in one sentence why its silence on the buggy
version is not evidence of correctness.
Examines. Ch3 E1–E3 · Ch1 E4 · Ch2 E4.
Mark scheme
Section titled “Mark scheme”Section A (20)
Section titled “Section A (20)”A1. (a) 4 = 1 pre (status in the five constants) · 2 post, one mark for
BOOKED → raises, status unchanged, one for BAND_UNAVAILABLE → False, status
unchanged, with the OPEN/CLOSED → True + −1 case required for full marks ·
1 frame (never saves). (b) 2 = 1 for naming CANCELLED · 1 for the clause
(raises TransitionRefused('cancelled')). A post that lists only BOOKED and
BAND_UNAVAILABLE scores 2 of 4: the specification is the defect, and an answer
that reproduces it is the answer A1 is testing.
A2. (a) 4 = 1 pre · 1 post on the return and the saved status · 1 exceptions
naming has_invoice and booked · 1 frame (update_fields=['status'], email
queued on commit, nothing else written). (b) 2 = 1 for the invariant (a band with
an outstanding invoice cannot go unavailable) · 1 for “the service’s docstring and
a test”, not the model method’s.
A3. (a) 2 = S2 stronger; the clause is the exception, which S1 leaves
open. 1 mark if the stronger one is named without the clause. (b) 2 = 1 for
S1 being weakest · 1 for the reason: the caller can supply the raise itself, and
the weaker spec leaves an implementation free to return the fallback fee without
breaking a client.
A4. 4 = 1 for Status.CANCELLED · 1 for the call · 1 for
assert raises TransitionRefused (or is False under the closed specification —
accept either only if it matches the A1(b) clause written) · 1 for “returns
True and sets −1”. An answer that asserts the current behaviour scores 0 on the
last mark.
Section B (20)
Section titled “Section B (20)”B1. 5 = 1 per class, 2 for the third only if its catching test is specific:
(a) each share rounded independently, so the pair can exceed the gross — property
test over 1..500; (b) the function ignores its argument and returns a constant —
second input, 999; (c) round() banker’s rounding instead of half up — an input
whose 18% ends in half a penny. A class named without a test scores 0 for that
class; three classes and no tests scores 0 overall.
B2. 5 = 1 each, 2 for the third: wrong branch selected by data no fixture holds · an authorisation check on an object type never in the fixture, enforced only in the UI · lines executed is not outcomes asserted (assertions of type and sign pass for every value the annotation permits). “Missing tests” is not an answer — coverage already claims there are none missing.
B3. 5 = 2 for what it pins (the fallback’s value, 2.00, i.e. current
behaviour not the contract) · 3 for the rewrite: BANK_TRANSFER first in the
fixture, pay_in="BALANCE" with BALANCE disabled, pytest.raises(ValueError).
A rewrite that only tightens the number to 4.30 scores 2.
B4. (a) 2 = it reports the last commit as good, or names the wrong commit,
because the script’s exit status is 0 and bisect reads only the status. 1 mark
for “it finds nothing” without the status mechanism. (b) 1 = a subtraction error
would differ by the planner fee or by a non-round amount; a constant whole-pound
difference accuses the quantiser in booking_fee. (c) 1 = 10 steps. (d) 1 =
non-zero on failure, no interactive input.
Section C (20)
Section titled “Section C (20)”C1. (a) 3 = 1 for len(installments) >= 1 · 1 for every amount > 0 · 1 for
sum(installments) == total; the AF is worth 0 on its own and 1 of the 3 must
come from it — an answer with an RI and no AF scores 2. (b) 2 = a property makes
the predicate true by construction, so it can never fail, and an invariant that
cannot fail is a comment. (c) 1 = sum(installments) == total.
C2. (a) 3 = 2 for the mutator path (add_instalment appends without checking
the sum or the sign) · 1 for the leak (add_instalment never called; a caller
mutating the list the accessor handed out, or a float-derived Decimal passed to
the constructor). Naming two paths that are both “the mutator” scores 2. (b) 2 =
1 per fix: re-check the RI in the mutator (or make it total-aware) · return
tuple(self.installments) so the rep is unreachable.
C3. (a) 2 = 1 for False · 1 for len(payouts) == 1 — the entry is still
there under the old hash. (b) 2 = 1 for @dataclass(frozen=True) (or a hash over
immutable fields plus no setter) · 1 for “immutability” as the reason, not
“consistency”.
C4. (a) 3 = 1 each: several writers bypass clean(), so the invariant must
sit in the database · band_fee is derived, so it is computed and cannot drift ·
one writer, so an assert is enough and an O(n) check in a hot loop is not.
(b) 2 = consistency, not intent: whether the fees are the agreed ones, or whether
booking_fee was computed at the right percentage.
Section D (20)
Section titled “Section D (20)”D1. (a) 3 = 1 for “it passes on the base commit” · 2 for naming 1.90 as the
value the bug produced, so the assertion was copied from output. (b) 3 = 2 for the
comment carrying input, observed, intended (pay_in='balance' with BANK_TRANSFER
first returns 1.90; the balance fee is 4.30) · 1 for “request changes, on the
test only”. A verdict of approve scores 0 on the second half however good the
comment. (c) 2 = 1 for “where is the legal set of payIn values defined, and
should an unknown value raise” · 1 for “ask, not block” with the reason (the
two-line fix is correct).
D2. (a) 2 = 1 for merge base → head · 1 for “a line that exists only on
master’s newer side was never in the base, so it appears in no hunk”. (b) 2 = 1
for git diff --stat origin/master...HEAD against
git diff --stat origin/master HEAD · 1 for identifying the extra - as the
colleague’s import. (c) 2 = 1 for
git merge-base --is-ancestor origin/master HEAD (or
git log --oneline HEAD..origin/master listing nothing) · 1 for exit status 1.
D3. (a) 2 = the specific error suppressed, the reason on the same line with a
ticket and date, and a test that fails without it — all three for 2, two for 1.
(b) 2 = nothing ever reports the ignore as stale, so it outlives its reason and
silently widens the Any hole. (c) 2 = 1 for static → never (not static →
dynamic); 1 for the consequence: no stage catches the defect the ignore hides.
Section E (20)
Section titled “Section E (20)”E1. 6 = 2 fixture lists BANK_TRANSFER first (otherwise the fallback satisfies
the assert) · 2 asserts 4.30 · 2 for a pasted run in which the new test fails.
A test that passes scores 0: on the current code it cannot pass unless the
expectation is 1.90.
E2. 5 = 3 for pay_in = pay_in.upper() before the loop (accept a
str.upper() bound once outside; an if per option scores 1) · 2 for the pasted
git diff and a green summary.
E3. 5 = 3 for two pasted summaries, one failing before the fix and one passing after · 2 for the sentence: a test that is green on both sides is decoration, because it cannot report the regression returning.
E4. 4 = 2 for real mypy-once output (an error scores 0 unless explained) ·
2 for the reason: pay_in is a str and "balance" is a str, and
quote: dict is dict[Any, Any], so the checker has nothing to disagree with.
Grade descriptors
Section titled “Grade descriptors”| Band | The script |
|---|---|
| Distinction (70+) | A gives four parts for both functions and names CANCELLED as the specification’s hole; A3 chooses the weaker spec and justifies it per caller; B names three classes with the test that catches each and explains the exit status; C writes an RI as a predicate, an AF that is total on it, and places each invariant by writer count; D blocks the test and names 1.90, and reads the two-dot/three-dot pair correctly; E shows red-before-green-after with real output and explains mypy’s silence by Any. |
| Merit (60–69) | A complete for one function, frame missing on the other; B1 two classes with tests; C1 RI right, AF vague (“the instalments”), C2 finds the mutator only; D1 blocks but the comment names the line rather than the input; E runs, but the fixture lists BALANCE first so the test would pass on the broken code. |
| Pass (40–59) | A has pre and post, no exceptions or frame; B lists “missing tests” or names classes without tests; C1 copies the RI from the chapter but cannot say why total is stored; D approves with questions, or requests changes without naming the blocking item; E produces a test that runs and passes on the buggy code. |
| Fail (<40) | A restates the signature; C has no predicate, only prose; D reviews the description rather than the diff; E has no pasted output, or the test asserts 1.90. |
Section floors. A ≥ 8/20, C ≥ 10/20, E ≥ 8/20. A candidate who cannot write a contract, state a rep invariant, or run the suite has not met the module’s outcome, whatever the aggregate.
Boundaries. 40 pass, 60 Merit, 70 Distinction, on the total out of 100, with the floors applied first.