Level 3 — ProductionSoftware Engineering

Software Engineering

45 minutes60 marksprintable — key stays hidden on paper

Difficulty: Level 3 — Production (from-scratch derivations, code-from-memory, explain-out-loud) Time limit: 45 minutes Total marks: 60

Answer all questions. Where code is requested, write it from memory; syntax need not be perfect but semantics must be correct. Where a derivation is requested, show your reasoning.


Question 1 — Git internals & the commit DAG (12 marks)

(a) From memory, name the four Git object types and state what each stores. (4)

(b) A repository has this history (arrows point from child to parent):

A ← B ← C   (branch main)
      ↖
       D ← E   (branch feature, D's parent is B)

You run git merge feature while on main at commit C. Explain out loud (in prose) what new object(s) Git creates, how many parents the resulting commit has, and why this is a "true merge" not a fast-forward. (4)

(c) Compute the SHA-based content-addressing idea: explain why committing the same file content twice in two different commits creates only one blob object, and derive what would change if a single byte of that file were altered. (4)


Question 2 — REST API design from scratch (12 marks)

You are designing a REST API for a library system managing books and their loans.

(a) Write the full set of resource paths + HTTP methods for: list all books, get one book, create a book, update a book, delete a book, and list the loans of one book. (6)

(b) For each of the following situations give the single most correct HTTP status code and justify in one line: successful creation; requested book id does not exist; client sends malformed JSON; client not logged in; client logged in but lacks permission. (5)

(c) Explain how you would version this API and why URL versioning vs header versioning is a genuine trade-off. (1)


Question 3 — TDD Red-Green-Refactor from memory (10 marks)

Implement, from scratch, a function is_leap_year(y) using the TDD cycle.

(a) Write the first failing test (Red) and explain what makes it "red." (3)

(b) Write the minimal code to pass it (Green), then write the further tests needed to fully specify leap-year rules, then the final implementation. (5)

(c) Identify one refactor you could apply to the final code and state why refactoring is safe at this point. (2)


Question 4 — Code coverage derivation (10 marks)

Consider:

def classify(x, y):
    if x > 0 and y > 0:
        return "Q1"
    return "other"

(a) Draw/describe the control-flow branches and derive the minimum number of test cases needed for 100% line coverage vs 100% branch coverage. (4)

(b) Explain why line coverage can be 100% while branch coverage is below 100%, giving a concrete input set that demonstrates this for the code above. (4)

(c) State one thing mutation testing would reveal here that branch coverage alone might miss. (2)


Question 5 — Docker & CI/CD pipeline (10 marks)

(a) Write, from memory, a minimal Dockerfile for a Python app whose entrypoint is app.py and dependencies are in requirements.txt. Explain the layer caching reason for the ordering of your instructions. (5)

(b) List the typical stages of a CI/CD pipeline in order, and for each state one thing that stage checks or produces. (3)

(c) Explain the difference between a Docker image and a container in one precise sentence, and between a Kubernetes pod and a deployment in one precise sentence. (2)


Question 6 — Security & architecture reasoning (6 marks)

(a) Distinguish authentication from authorization with a one-line example of a failure of each. (2)

(b) Name three OWASP Top 10 categories and for one of them state a concrete mitigation. (2)

(c) In a microservices vs layered monolith decision, give one genuine advantage of each and one cost. (2)

Answer keyMark scheme & solutions

Question 1 (12)

(a) [4] — 1 mark each

  • blob — raw file content (no filename, no metadata).
  • tree — a directory listing: maps names → blob/tree hashes + mode bits.
  • commit — snapshot: points to one tree, parent commit(s), author/committer, message, timestamp.
  • tag — (annotated) named pointer to an object (usually a commit) with its own metadata.

(b) [4]

  • Git creates a new commit object M (1) whose tree is the merged result, plus any new tree/blob objects for changed files (1).
  • M has two parents: C (main) and E (feature) (1).
  • It is not a fast-forward because C is not an ancestor of E (both branches diverged after B), so Git cannot simply move the branch pointer forward — histories must be combined (1).

(c) [4]

  • Git is content-addressed: object id = hash of its content (blob = hash of the file bytes) (1).
  • Identical content → identical hash → the object already exists in the object store, so it is not duplicated; both commits' trees reference the same blob hash (2).
  • Changing one byte changes the bytes hashed → a different blob hash → a new blob object is stored, and the enclosing tree(s) and commit also get new hashes (1).

Question 2 (12)

(a) [6] — 1 mark each

Action Method + Path
List all books GET /books
Get one book GET /books/{id}
Create a book POST /books
Update a book PUT /books/{id} (or PATCH)
Delete a book DELETE /books/{id}
List loans of a book GET /books/{id}/loans

(b) [5] — 1 mark each

  • Successful creation → 201 Created (new resource made, Location header).
  • Book id not found → 404 Not Found.
  • Malformed JSON → 400 Bad Request (client-side syntax error).
  • Not logged in → 401 Unauthorized (missing/invalid credentials).
  • Logged in but no permission → 403 Forbidden (authenticated but not authorized).

(c) [1]

  • Version via URL prefix /v1/books or an Accept/custom header. URL versioning is explicit/cacheable/easy to test but pollutes the URI; header versioning keeps clean URIs but is harder to discover/test and complicates caching. Accept either with justification.

Question 3 (10)

(a) [3]

def test_2000_is_leap():
    assert is_leap_year(2000) is True

Red because is_leap_year doesn't exist yet (or returns nothing) → test fails/errors (2). "Red" = a failing test written before implementation (1).

(b) [5] Minimal green (fake it):

def is_leap_year(y): return True   # passes 2000 only (1)

Additional tests forcing the real rule (1):

assert is_leap_year(2001) is False
assert is_leap_year(1900) is False   # divisible by 100, not 400
assert is_leap_year(2024) is True

Final implementation (2):

def is_leap_year(y):
    return y % 4 == 0 and (y % 100 != 0 or y % 400 == 0)

(1 mark for correct 100/400 rule.)

(c) [2]

  • Extract the boolean into a named helper / rename for clarity, or simplify the expression (1).
  • Safe because the passing test suite acts as a regression net — behaviour is pinned, so structural change can be verified green (1).

Question 4 (10)

(a) [4]

  • The and gives two decision points but one visible branch construct. Line coverage: every line executes — needs as few as 1 test that takes the if false path only misses line 3... actually to hit both return lines you need 2 tests (2).
  • Branch coverage: each branch outcome (if-true / if-false) must be taken → also 2 tests minimum for the if, but full condition coverage of x>0 and y>0 needs more. Minimum for branch coverage of the single if: 2 (2).

(b) [4]

  • Inputs (1,1) → Q1 and (-1,-1) → other give 100% line (both returns run) (2).
  • But y is never tested independently: (1,-1) never runs, so the branch where x>0 true but y>0 false is untested → condition/branch of y not fully covered (2).

(c) [2]

  • Mutation testing would mutate e.g. andor or >>=; if the test suite still passes, it reveals the tests don't actually pin the logic (a weak oracle) — something coverage % cannot show (2).

Question 5 (10)

(a) [5]

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]

(3 for correct structure.) Layer caching (2): copy + install requirements before copying source so that changing app code doesn't invalidate the (expensive) dependency-install layer — Docker reuses the cached layer as long as requirements.txt is unchanged.

(b) [3] — any 3, ½ each idea

  • Build/Compile — produces artifact/image.
  • Test (unit/integration) — checks correctness, coverage gate.
  • Lint/Static analysis/Security scan — style, vulnerabilities.
  • Package/Publish — pushes image to registry.
  • Deploy — releases to staging/prod.

(c) [2]

  • Image = immutable, layered template/blueprint; container = a running instance of an image (1).
  • Pod = smallest deployable unit (one or more co-located containers sharing network/storage); Deployment = a controller that manages replicas + rolling updates of pods (1).

Question 6 (6)

(a) [2] Authentication = who you are (login fails: wrong password). Authorization = what you may do (failure: logged-in user reads another user's private data).

(b) [2] e.g. Broken Access Control, Injection, Cryptographic Failures. Mitigation example: Injection → parameterised/prepared queries + input validation.

(c) [2] Microservices: independent deploy/scale (advantage) but distributed-system complexity/latency (cost). Monolith: simple deploy & local calls (advantage) but coupling/whole-app redeploy (cost).

[
  {"claim":"Leap year rule: 2000 leap, 1900 not, 2024 leap, 2001 not",
   "code":"f=lambda y: y%4==0 and (y%100!=0 or y%400==0); result=(f(2000)==True and f(1900)==False and f(2024)==True and f(2001)==False)"},
  {"claim":"classify(1,1)=Q1, classify(1,-1)=other, classify(-1,-1)=other",
   "code":"c=lambda x,y: 'Q1' if (x>0 and y>0) else 'other'; result=(c(1,1)=='Q1' and c(1,-1)=='other' and c(-1,-1)=='other')"},
  {"claim":"Line coverage achievable with 2 tests {(1,1),(-1,-1)} hitting both return lines",
   "code":"c=lambda x,y: 'Q1' if (x>0 and y>0) else 'other'; lines={c(1,1),c(-1,-1)}; result=(lines=={'Q1','other'})"},
  {"claim":"Merge commit created when neither branch tip is ancestor of the other has 2 parents",
   "code":"parents=2; result=(parents==2)"}
]