Level 5 — MasterySoftware Engineering

Software Engineering

90 minutes60 marksprintable — key stays hidden on paper

Chapter: 4.5 Software Engineering Level: 5 — Mastery (cross-domain: build/prove, quantitative reasoning) Time limit: 90 minutes Total marks: 60

Instructions: Answer all three questions. Justify all quantitative claims. Where you make an engineering trade-off, state the assumption explicitly. Partial credit is awarded for correct reasoning even where the final figure differs.


Question 1 — CI/CD Pipeline Reliability & Test Economics (20 marks)

A team runs a CI pipeline with four independent sequential stages, each with a probability of passing (not raising a false failure due to flakiness) of p1=0.99p_1=0.99, p2=0.98p_2=0.98, p3=0.95p_3=0.95, p4=0.90p_4=0.90. The pipeline reports "green" only if all stages pass.

(a) Compute the probability that a single pipeline run is green when the underlying commit is actually correct (i.e. any failure is a false failure from flakiness). Give the numeric probability of at least one flaky (false) failure. (4)

(b) The stage-3 test suite is flaky. If a run fails, the team retries the whole pipeline up to a maximum of NN total attempts, stopping at the first green. Assuming the per-run green probability PP from part (a) is constant and runs are independent, derive a closed-form expression for the probability that the commit is eventually reported green within NN attempts, and evaluate it for N=3N=3. (5)

(c) Each pipeline run costs 6 CPU-minutes and $0.02/CPU-minute. Using the retry policy of (b) with N=3N=3, derive the expected number of runs per commit (as a function of PP and NN) and hence the expected cost per commit. (5)

(d) The team can invest engineering effort to make stage 4 non-flaky (p4=1.0p_4=1.0). Recompute PP, and argue quantitatively (using your cost model) whether reducing flakiness or simply raising NN is the better lever. Discuss this in terms of technical debt (4.5.20). (6)


Question 2 — REST API Design + State Machine Proof (20 marks)

You are designing a REST API for an order resource. Orders move through states: CREATED → PAID → SHIPPED → DELIVERED, with CREATED → CANCELLED and PAID → REFUNDED also allowed. No other transitions exist.

(a) Draw (as a transition table or list) the UML state machine for Order. Then prove that the state graph is a DAG (contains no cycle), and state the implication for idempotency of a PATCH that advances state. (5)

(b) Design the REST endpoints for: create order, fetch one order, list orders with pagination, advance state, cancel. For each, give the HTTP method, URI template, and the expected success and one relevant error status code. Explain why using PUT vs PATCH for the state-advance operation matters. (7)

(c) A client requests page 3 of a listing with ?page=3&limit=20. Give the offset used in the query and the range of item indices (0-based) returned. Then explain, with a concrete failure scenario, why offset-based pagination breaks under concurrent inserts, and how cursor/keyset pagination fixes it. (5)

(d) Write an acceptance criterion in Given/When/Then form for the "cancel an already-shipped order" case, and map it to the correct HTTP status code. Classify this test as unit / integration / system and justify. (3)


Question 3 — Code Coverage, Mutation Testing & TDD (20 marks)

Consider the function:

def classify(x, y):
    if x > 0 and y > 0:
        return "A"
    if x > 0 or y > 0:
        return "B"
    return "C"

(a) Draw the control-flow structure and compute the cyclomatic complexity. State the minimum number of test cases needed for full branch coverage and give a concrete test set achieving it, with expected outputs. (6)

(b) Distinguish branch coverage from path coverage here: how many distinct execution paths exist, and give one path that a branch-complete suite might not independently exercise. (4)

(c) Your suite from (a) reaches 100% branch coverage. Explain mutation testing: define the mutation score, then apply the mutant that changes x > 0 and y > 0 to x > 0 or y > 0. Give a test input that kills this mutant (with original vs mutant output) and one that does not, and compute the mutation score if your suite has 5 mutants of which this one is the only survivor. (6)

(d) Rewrite classify using the Red-Green-Refactor TDD cycle: state what the first failing test is, what minimal code makes it green, and one refactoring you would apply once all tests pass. Name the code smell your refactoring addresses. (4)


End of paper.

Answer keyMark scheme & solutions

Question 1

(a) Green means all independent stages pass: P=0.99×0.98×0.95×0.90=0.829521.P = 0.99 \times 0.98 \times 0.95 \times 0.90 = 0.829521. Probability of at least one flaky false failure =1P=0.1704790.1705= 1 - P = 0.170479 \approx 0.1705. Why: independence ⇒ multiply pass probabilities; complement gives failure. (2 for product, 2 for complement)

(b) The commit is eventually green if at least one of NN independent runs is green. Probability all NN fail =(1P)N=(1-P)^N, so Peventual(N)=1(1P)N.P_{\text{eventual}}(N) = 1-(1-P)^N. For N=3N=3, P=0.829521P=0.829521: (1P)=0.170479(1-P)=0.170479, (0.170479)3=0.004954(0.170479)^3=0.004954, so Peventual=10.004954=0.9950460.9950.P_{\text{eventual}} = 1 - 0.004954 = 0.995046 \approx 0.9950. (2 derivation, 3 numeric)

(c) Let q=1Pq=1-P. Runs stop at first green; number of runs R{1,,N}R\in\{1,\dots,N\}.

  • R=kR=k (for k<Nk<N): first k1k-1 fail, kk-th green: prob qk1Pq^{k-1}P.
  • R=NR=N: either succeeds on NN-th (prev fail) or all fail — either way NN runs used: prob qN1q^{N-1}.

E[R]=k=1N1kqk1P+NqN1=1qN1qE[R] = \sum_{k=1}^{N-1} k\,q^{k-1}P + N q^{N-1} = \frac{1-q^N}{1-q} (the standard truncated-geometric mean, since 1q=P1-q=P). For N=3N=3, q=0.170479q=0.170479: E[R]=10.0049540.829521=0.9950460.829521=1.199561.20 runs.E[R]=\frac{1-0.004954}{0.829521}=\frac{0.995046}{0.829521}=1.19956\approx 1.20 \text{ runs}. Cost per run =6\times\0.02=$0.12.Expectedcost. Expected cost =1.19956\times0.12=$0.14395\approx$0.144$. (3 derivation, 2 numeric)

(d) With p4=1.0p_4=1.0: P=0.99×0.98×0.95=0.92169P'=0.99\times0.98\times0.95=0.92169, q=0.07831q'=0.07831.

  • Peventual(3)=10.078313=10.00048=0.99952P_{\text{eventual}}(3)=1-0.07831^3=1-0.00048=0.99952.
  • E[R]=10.0783130.92169=0.999520.92169=1.08444E[R]'=\frac{1-0.07831^3}{0.92169}=\frac{0.99952}{0.92169}=1.08444 runs → cost =\0.13013$.

Comparison: raising flakiness fix drops expected cost from $0.144 → $0.130 (~10%) and raises eventual-green from 0.9950 → 0.99952 (fewer masked real failures). Raising NN improves eventual-green but increases cost and, critically, retries mask genuine failures — a false economy. Flakiness is a form of technical debt (accidental/testing debt): retries pay recurring "interest" (compute + hidden bugs) whereas fixing stage 4 pays down principal. Preferred lever: reduce flakiness. (2 recompute, 2 quantitative comparison, 2 tech-debt argument)


Question 2

(a) Transition table:

From Event To
CREATED pay PAID
CREATED cancel CANCELLED
PAID ship SHIPPED
PAID refund REFUNDED
SHIPPED deliver DELIVERED

Terminal states: CANCELLED, DELIVERED, REFUNDED. DAG proof: assign a topological rank r(CREATED)=0, PAID=1, SHIPPED=2, DELIVERED=3, CANCELLED=1, REFUNDED=2. Every listed transition goes strictly from lower to higher rank, so no edge closes a cycle back to an earlier state ⇒ no cycle ⇒ DAG. Idempotency implication: because state only advances forward, a PATCH that says "advance to PAID" applied to an already-PAID order can be made idempotent (return current state / 409 rather than re-advancing), since no path returns to an earlier state. (2 table, 2 proof, 1 idempotency)

(b)

Action Method URI Success Error
Create order POST /orders 201 Created 400 Bad Request
Fetch one GET /orders/{id} 200 OK 404 Not Found
List (paged) GET /orders?page=&limit= 200 OK 400 (bad params)
Advance state PATCH /orders/{id} (body: target state) 200 OK 409 Conflict (illegal transition)
Cancel POST /orders/{id}/cancel (or PATCH) 200 OK 409 Conflict

PUT vs PATCH: PUT means replace the full resource and should be idempotent on the whole representation; using it for a state advance implies the client sends the entire order and can overwrite fields. PATCH expresses a partial modification (just the state), matching the intent of "advance state", and lets the server enforce transition rules and reject illegal jumps with 409. (1 per correct endpoint row ≈5, 2 PUT/PATCH)

(c) offset =(page1)×limit=(31)×20=40=(page-1)\times limit = (3-1)\times20 = 40. Item indices returned: 40–59 (0-based). Offset failure: between fetching page 2 and page 3, if a new item is inserted at the front, every existing item shifts by one index; the item previously at index 40 moves to 41, so page 3 re-shows an item already seen on page 2 (or skips one on deletion). Cursor/keyset pagination uses a stable sort key (e.g. WHERE id > last_seen_id ORDER BY id LIMIT 20); inserts/deletes elsewhere don't shift the cursor, so no duplicates/skips. (2 offset+range, 3 concurrency explanation)

(d)

Given an order in state SHIPPED When the client requests to cancel it Then the request is rejected with 409 Conflict and the order remains SHIPPED.

Classification: integration test — it exercises the API endpoint together with the state-machine enforcement and (likely) persistence layer, not a single isolated function. (2 GWT+status, 1 classification)


Question 3

(a) Two decision points (x>0 and y>0, then x>0 or y>0). Counting each boolean as we assess branch coverage of the two ifs: decisions = 2, so cyclomatic complexity V(G)=decisions+1=2+1=3.V(G) = \text{decisions} + 1 = 2 + 1 = 3. (Equivalently 3 linearly independent paths / 3 return outcomes.) Minimum test cases for branch coverage = 3 (each if needs a true and false outcome; three tests suffice since they chain):

x y Path Output
1 1 first if true "A"
1 −1 first false, second true "B"
−1 −1 both false "C"

This exercises both outcomes of both if decisions. (2 complexity, 2 min count, 2 test set)

(b) Branch coverage requires each decision to take true & false; path coverage requires every full route through the CFG. The compound conditions and two sequential ifs give more distinct condition-combinations. Reachable end-to-end paths (by output): "A", "B", "C" = 3 primary paths, but "B" is reachable two sub-ways: (x>0, y≤0) and (x≤0, y>0). So there are effectively 4 distinct input-condition paths, while our 3-test branch suite only exercised the (x>0,y≤0) variant of "B" — it never runs (x≤0, y>0). Path coverage would demand that fourth case too. (2 count, 2 uncovered path)

(c) Mutation testing seeds small faults (mutants) into the code and checks whether the test suite fails (kills) on them. Mutation score: score=killed mutantstotal non-equivalent mutants.\text{score} = \frac{\text{killed mutants}}{\text{total non-equivalent mutants}}. Mutant: x > 0 and y > 0x > 0 or y > 0.

  • Kills it: input (x=1,y=1)(x=1, y=-1). Original: first if false → "B". Mutant: first if true → "A". Outputs differ (B vs A) ⇒ a test asserting "B" fails ⇒ mutant killed.
  • Does not kill: input (x=1,y=1)(x=1, y=1): original "A", mutant also "A" (both true) ⇒ no difference ⇒ survives. With 5 mutants, only this one surviving means 4 killed: score=45=0.8=80%.\text{score} = \frac{4}{5} = 0.8 = 80\%. (2 definition, 2 kill/no-kill inputs, 2 score)

(d) Red-Green-Refactor:

  • Red: write first failing test, e.g. assert classify(1, 1) == "A" — fails because classify doesn't exist / returns nothing.
  • Green: minimal code to pass, e.g. def classify(x,y): return "A" (hard-coded), then add tests for "B" and "C" driving the real conditionals until all green.
  • Refactor: once green, extract the sign logic — e.g. both = x>0 and y>0; either = x>0 or y>0 or a lookup on (x>0, y>0) — improving readability without changing behaviour, re-running tests. Code smell addressed: duplicated/nested conditional logic (repeated x>0, y>0 comparisons) — often labelled duplicated condition / complex conditional. (1 red, 1 green, 1 refactor, 1 smell)
[
  {"claim":"Q1a green probability = 0.829521 and false-failure prob = 0.170479",
   "code":"P=Rational(99,100)*Rational(98,100)*Rational(95,100)*Rational(90,100); result = (abs(float(P)-0.829521)<1e-6) and (abs(float(1-P)-0.170479)<1e-6)"},
  {"claim":"Q1b eventual green within N=3 approx 0.995046",
   "code":"P=0.829521; q=1-P; result = abs((1-q**3)-0.995046)<1e-5"},
  {"claim":"Q1c expected runs for N=3 approx 1.19956 and cost approx 0.14395",
   "code":"P=0.829521; q=1-P; ER=(1-q**3)/(1-q); result = abs(ER-1.19956)<1e-4 and abs(ER*0.12-0.14395)<1e-4"},
  {"claim":"Q1d with p4=1, E[R]=1.08444 cost 0.13013",
   "code":"Pp=0.99*0.98*0.95; q=1-Pp; ER=(1-q**3)/(1-q); result = abs(ER-1.08444)<1e-4 and abs(ER*0.12-0.13013)<1e-4"},
  {"claim":"Q2c page 3 limit 20 offset = 40",
   "code":"page=3; limit=20; offset=(page-1)*limit; result = offset==40"},
  {"claim":"Q3c mutation score with 1 survivor of 5 = 0.8",
   "code":"result = Rational(4,5)==Rational(4,5) and abs(4/5-0.8)<1e-9"}
]