Introduction to Software Engineering · Chapter 10

Lab-10: Software Maintenance and Evolution

Sprint 2 ends this week, and your team works as a maintenance team. Three change requests arrive for ITC Club Hub: you classify them, analyse the impact of the largest one, clean up three code smells behind your tests, fix a bug test-first, deliver the waiting list, and publish release v0.2.0 with a changelog, a tag-triggered release and a maintenance plan. The challenge hands you a 160-line legacy report module to reverse-engineer, pin down with characterization tests, and plan its modernisation.

ISO/IEC/IEEE 14764:2022 · SemVer 2.0.0 · Keep a Changelog 1.1.0 C++20 · CMake 3.28 · GoogleTest 1.15 · GitHub Actions · lizard ≈ 7 hours per team 5 tasks + 1 challenge
How to work through this lab. Do the tasks in order; each builds on the previous. Read the Goal, follow the Steps, compare with the Expected output, and tick the Acceptance checklist (saved in your browser). Open a hint only when stuck for more than 10 minutes. This is team work: the lead role is named on each task, and every student's contribution must be visible in the Git history.
0

Setup

20 min
  1. Tools. The toolchain from Labs 06 to 09, plus lizard (a small Python tool that measures cyclomatic complexity of C++ functions) and, optionally, the GitHub CLI gh. Use a Markdown editor with Mermaid preview for the diagrams (VS Code with Markdown Preview Mermaid Support, or mermaid.live).
    cmake --version                 # 3.28 or later
    g++ --version                   # GCC 13+ (or clang++ 17+, or MSVC 2022+)
    git --version
    python -m pip install lizard    # cyclomatic complexity, used in Task 3
    lizard --version
    gh --version                    # optional, used in Task 5
  2. Start from a green baseline. Everything in this lab is measured against the state of main today, so make sure it builds and all tests pass before you change anything.
    git switch main && git pull
    git switch -c lab-10
    cmake -S . -B build && cmake --build build && ctest --test-dir build
    git describe --tags --abbrev=0  # last release tag, e.g. v0.1.0 (no tag? see the table)
  3. Case-study brief. The box below is everything this lab needs to know about the product; there is no other product document to read.
    Case-study brief: ITC Club Hub, week 12
    Product
    ITC Club Hub, an application for student clubs at the Institute of Technology of Cambodia. Club leaders register a club, an administrator approves it; clubs publish events (title, date and time, location, capacity, description); students browse, register, cancel and receive a reminder; organisers check students in at the door and see attendance; administrators see a monthly activity report. Your team implements the core in C++20: domain model, business rules, a command-line front end, CSV or JSON file storage.
    Business rules
    Capacity cannot be exceeded (a waiting list is a Should feature) · a student cannot register twice for the same event · cancellation is possible up to 24 hours before the start, so at exactly 24 h it is still allowed (start - now >= 24h) · events in the past cannot be registered for · only approved clubs can publish events.
    Domain names
    Namespace clubhub: Club, Event, Student, Registration, ClubStatus (Pending, Approved, Rejected), RegistrationStatus (Registered, Cancelled, CheckedIn, Waitlisted), RegistrationService with registerStudent, cancel, checkIn, and the interfaces EventRepository and RegistrationRepository with CSV-backed implementations.
    Repository
    CMakeLists.txt, src/, include/clubhub/, tests/ (GoogleTest via FetchContent), .github/workflows/ci.yml, and one folder per lab (lab01/ … lab09/).
    Where we are
    Week 12, the last week of Sprint 2 (weeks 11 and 12). Sprint 1 (weeks 9 and 10) delivered the first increment. Project submission and demo are in week 14. Roles: one Product Owner, a Scrum Master who rotates each sprint, the rest Developers. The instructor or teaching assistant plays the client and has sent the three change requests of Task 1.
    Public API
    For versioning, Club Hub's public API is: the CLI commands and their output, the CSV file formats, and the headers in include/clubhub/. Versions follow SemVer; before the week-14 submission the project is in 0.y.z (initial development).
    Personal data
    Student ID, name, email, phone (optional), club membership and attendance records. Any export of attendance contains personal data and must be limited to the organisers of that event (Chapter 03).
  4. Inputs from earlier labs. This lab changes the code, tests and CI from Labs 06 to 09 and the backlog from Lab-05. If one of them is incomplete, use the fallback and carry on; do not stop to repair the earlier lab.
    InputWhere it usually isWhat you need from itFallback if missing
    Product backlog and user stories (Lab-05)GitHub Projects board, lab05/Story ids for registration and cancellation, MoSCoW, acceptance criteriaUse the business rules in the brief; Task 2 writes the waiting-list story from scratch.
    Class and state diagrams (Lab-06)lab06/Classes to change (Task 2), diagrams to update (Task 4)Start from the Mermaid skeletons in the Task 4 starter.
    Sprint 1 increment (Lab-07)main, tag v0.1.0 if you made oneWorking registerStudent, cancel, checkIn; the last releaseNo tag yet? Tag the Sprint 1 merge commit now: git tag -a v0.1.0 <commit> -m "Sprint 1".
    CI pipeline (Lab-08).github/workflows/ci.ymlBuild and tests on every push and pull requestCopy the checkout, configure, build and ctest steps of the Task 5 release.yml into ci.yml with on: [push, pull_request].
    Tests, coverage, static analysis (Lab-09)tests/, .clang-tidy, gcovr reportThe safety net for Task 3; the boundary cases of the 24 h rule for Task 4If a class has no tests, write characterization tests before refactoring it (Task 3, step 3).
  5. Deliverable folders. Create lab10/; code, tests and release files change in their usual places.
    club-hub/
    ├── CMakeLists.txt                       # Task 5: project(clubhub VERSION 0.2.0)
    ├── CHANGELOG.md                         # Task 5 (repository root)
    ├── .github/workflows/release.yml        # Task 5
    ├── include/clubhub/Version.hpp.in       # Task 5
    ├── src/  include/  tests/               # Tasks 3 and 4
    ├── legacy/                              # Challenge: report_legacy.cpp, data/, expected/
    └── lab10/
        ├── change-requests/CR-13.md  CR-14.md  CR-15.md   # Task 1
        ├── triage.md                                     # Task 1
        ├── impact-analysis-CR-14.md                      # Task 2
        ├── refactoring-log.md                            # Task 3
        ├── complexity.md                                 # Task 3
        ├── diagrams/class-diagram.mmd                    # Task 4
        ├── diagrams/registration-state.mmd               # Task 4
        ├── maintenance-plan.md                           # Task 5
        ├── README.md                                     # Task 5: record what changed
        └── legacy/as-is.mmd  to-be.mmd  findings.md  modernisation.md   # Challenge
  6. Roles and conventions. The Product Owner leads Tasks 1 and 2 with one developer; Developers work in pairs on Tasks 3 and 4 (swap driver every 25 minutes); the Scrum Master acts as release manager in Task 5 and closes Sprint 2. Every student authors at least one commit in Tasks 3 or 4 (git shortlog -sn main..lab-10 must list everyone). Commit prefixes: fix:, feat:, refactor:, test:, docs:, chore(release):; each message names its CR ((CR-14)) where there is one. Every Markdown file starts with a title, a version and a date.
1

Classify three change requests

Easy Lead: Product Owner 30 min

Goal

Turn three incoming requests into logged change requests, classify each with ISO/IEC/IEEE 14764, size it, rate its urgency, and decide which ones enter the last days of Sprint 2.

Steps

  1. Read the three incoming requests in the starter. Create lab10/change-requests/CR-13.md, CR-14.md and CR-15.md from the template, and open one GitHub issue per request with the label change-request; write the issue number into the file.
  2. Classify each request as corrective, adaptive, perfective (additive, if it adds a feature) or preventive. Write one sentence of justification that names the trigger (reactive: a failure or an outside change; proactive: found by the team first).
  3. Size each request (S, M or L, plus story points on your Lab-07 scale) and rate its urgency (high, medium, low) with a reason: who is affected, how often, and is there a workaround?
  4. Give each request a MoSCoW priority and link it to the user story or requirement it touches (the Lab-05 id), or write new.
  5. Fill lab10/triage.md: one row per request, and a decision (this sprint, next sprint, rejected). Check the decision against the capacity left in Sprint 2 (hours left × developers) and write that calculation under the table.
  6. The Product Owner records the decision in each CR file (Status: Approved or Deferred) and on the issue. Commit: docs(lab10): triage CR-13 to CR-15.

Starter

INCOMING REQUESTS (week 12, forwarded by the teaching assistant)

[1] From: a student, e20230001
    "I tried to cancel my registration for the Arduino Workshop on Friday at
     14:00. The workshop starts on Saturday at 14:00. Club Hub answered
     'too late to cancel'. The rules say we can cancel up to 24 hours before."

[2] From: the Robotics Club leader
    "Our Line Follower Race filled up in ten minutes. Two students cancelled
     later and the seats stayed empty. Can students join a waiting list and get
     the seat automatically when someone cancels?"

[3] From: the clubs office
    "Every month we copy the attendance of each event into a spreadsheet by
     hand. Please let organisers export the attendance of an event as a CSV
     file: student ID, name, status, check-in time."
# CR-13 · <short title>
Version 1.0 · <date> · Issue #<n>

| Field       | Value |
|-------------|-------|
| Requester   | |
| Received    | week 12 |
| Description | what is wrong or wanted, in the requester's words |
| Category    | Corrective / Adaptive / Perfective (additive) / Preventive |
| Why         | one sentence: trigger (reactive or proactive) and reason |
| Size        | S / M / L · story points |
| Urgency     | high / medium / low · because ... |
| MoSCoW      | Must / Should / Could / Won't |
| Traces to   | US-.. / FR-.. / new |
| Status      | Submitted -> Triaged -> Approved / Deferred / Rejected |
| Decision    | who decided, when, and why |

Expected output

# Triage of change requests · Sprint 2 (v1.0, week 12)

| CR    | Title                                | Category (why)                                          | Size     | Urgency                                   | MoSCoW | Decision |
|-------|--------------------------------------|---------------------------------------------------------|----------|-------------------------------------------|--------|----------|
| CR-13 | Cancel rejected at exactly 24 h      | Corrective: a user reported a failure against the documented rule (reactive) | S · 1 pt | High: hits every event, no workaround | Must   | Sprint 2 |
| CR-14 | Waiting list for full events         | Perfective (additive): ...                              | M · ...  | ...                                       | Should | ...      |
| CR-15 | Attendance export as CSV             | ...                                                     | ...      | ...                                       | ...    | ...      |

Capacity left in Sprint 2: 3 days x 4 developers x 3 h = 36 h; CR-13 + CR-14 ≈ 3 + 14 h.

Show hints

  • Classify by the reason for the change, not by the size of the code change. A one-line fix and a two-day feature can both be perfective.
  • Urgency (how soon) and priority (how important) are different: a cosmetic typo on the help screen can be urgent before a demo and still be a Could.
  • Request [3] exports personal data. Record in CR-15 who may run the export and what it may contain; that is part of the request, not an afterthought.
  • If capacity is short, defer the least valuable request, never the bug that breaks a documented rule.

Acceptance checklist

2

Impact analysis of the waiting list (CR-14)

Medium Lead: Product Owner + one developer 45 min

Goal

Before anyone writes code for CR-14, find every requirement, diagram, class, file, test and document it touches, estimate the effort with its uncertainty, name the risks, and give the Product Owner a recommendation.

Steps

  1. Starting impact set: from your Lab-06 class diagram, list the classes and member functions CR-14 changes directly (for example RegistrationService::registerStudent and cancel).
  2. Dependency analysis: run the grep commands of the starter for every item of the starting set and add each file that includes, calls or stores it. Paste the command output into the file as evidence.
  3. Traceability: name the Lab-05 stories or requirements that change (the capacity rule), and write the new story "join the waiting list" with three Gherkin scenarios: join when full, promotion on cancel, no second place on the list.
  4. List by name the UML diagrams to update (at least the class diagram and the Registration state diagram), the tests to add (at least six test names) and the documents to update (README.md, help text, CHANGELOG.md).
  5. Estimate each row in hours, add an uncertainty margin with a reason, and compare the total with the capacity from Task 1.
  6. Record at least two risks with a mitigation (for example: files written by v0.1.0 must still load), the SemVer decision for the release, and a recommendation (approve, approve with reduced scope, defer). Copy the summary into CR-14 and move it to Analysed.

Starter

# ring 2: who includes, calls or stores what we change?
grep -rn "RegistrationStatus" include src tests
grep -rn "registerStudent(\|cancel(" src tests | wc -l
grep -rln "RegistrationRepository" include src tests
grep -rn "Registered\|Cancelled" src/*Csv*   # how is the status written to disk?
# Impact analysis IA-14 · Waiting list for full events
Version 1.0 · <date> · Analysts: <PO>, <developer>

## 1. Starting impact set (changed directly)
- ...

## 2. Dependency analysis (evidence: grep output below)
| File | Why it is affected | Hits |
|------|--------------------|------|

## 3. Impact table
| Artifact     | Affected items (by name) | Change      | Est. (h) |
|--------------|--------------------------|-------------|----------|
| Requirements |                          | amend / add |          |
| UML          |                          | update      |          |
| Code         |                          | modify      |          |
| Tests        |                          | add         |          |
| Docs         |                          | update      |          |
| Release      |                          | bump, tag   |          |
| **Subtotal** · uncertainty margin __ % because ... | | | |

## 4. Risks and mitigations
## 5. Version decision (SemVer) and recommendation
Feature: Waiting list for full events
  Scenario: A student joins the waiting list of a full event
    Given the event "Line Follower Race" has capacity 3 and 3 registrations
    When student "e20230010" registers for it
    Then the registration status is Waitlisted
    And the CLI prints "waitlisted (position 1)"

  Scenario: A cancellation promotes the first waitlisted student
    # TODO

  Scenario: A waitlisted student cannot take a second place on the list
    # TODO

Expected output

| Artifact | Affected items (by name)                                          | Change | Est. (h) |
|----------|-------------------------------------------------------------------|--------|----------|
| Code     | RegistrationService.hpp/.cpp (registerStudent returns the status;  | modify | 5        |
|          | cancel promotes the oldest Waitlisted), RegistrationRepository.hpp |        |          |
|          | (findWaitlisted(eventId), oldest first), CsvRegistrationRepository |        |          |
|          | .cpp (read/write "waitlisted"), src/Cli.cpp (messages, waitlist)   |        |          |
| Tests    | RegistrationServiceTest: FullEventPutsStudentOnWaitlist,           | add    | 3        |
|          | CancelPromotesFirstWaitlistedStudent, ... (6 in total); ...        |        |          |
| ...      |                                                                   |        |          |
| Subtotal · margin 30 % (first change to the promotion logic)       |        | 11 -> 14 |

Version decision: new, backward-compatible feature -> MINOR: 0.1.0 -> 0.2.0

Show hints

  • Think in three rings: code that changes, code and data that depend on it, then tests, models and documents. The chapter's ripple diagram (slide 22) is a good checklist.
  • RegistrationStatus::Waitlisted is already part of the domain names. If your enum lacks it, adding a value is a small change, but every switch over the enum is now in ring 2.
  • Data compatibility is the classic risk: if you add a column to registrations.csv, files written by v0.1.0 must still load (or the change is MAJOR). Storing the new status in the existing status column avoids the problem.
  • A margin is not padding: write why this change is uncertain (new logic, code nobody on the team wrote, missing tests).

Acceptance checklist

3

Code smells and refactoring, tests green at every step

Medium Lead: Developers in pairs 75 min

Goal

Find three code smells in your own code, remove them with small behaviour-preserving refactorings, prove that the tests were green after every commit, and measure the cyclomatic complexity of one function before and after.

Steps

  1. Find candidates: lizard src/ -s cyclomatic_complexity lists your functions from most to least complex; also run clang-tidy with the Lab-09 .clang-tidy. Read the top five functions.
  2. Record three different smells in lab10/refactoring-log.md: the smell name from the chapter's catalogue (Long Function, Duplicated Code, Repeated Switches, Mysterious Name, magic numbers, Feature Envy, ...), the file and line, the evidence, and the planned refactoring. One of them must be in your most complex function.
  3. Before touching a smell, check that tests execute that code (Lab-09 coverage report). If they do not, first add characterization tests that pin its current output and commit them as test: pin ....
  4. Refactor in small steps: one refactoring per commit with the refactor: prefix, build and ctest after each step, git restore . when a step turns red. At least two commits per smell. A bug you notice is written down as a new CR, not fixed in a refactoring commit.
  5. For your most complex function, count the cyclomatic complexity by hand before and after (list every decision point with its line number), confirm both numbers with lizard, and write lab10/complexity.md.
  6. Prove "green at every step": run the replay loop of the starter over the branch and paste its output into the log. Every line must say GREEN.
  7. Push, check that CI is green, and open a pull request Refactor three code smells (Lab-10 Task 3) into lab-10, reviewed by a team member who did not write it.

Starter

# Refactoring log · Lab-10 (v1.0, <date>)

| # | Smell (catalogue name) | Where (file:line) | Evidence | Refactoring(s) | Commits |
|---|------------------------|-------------------|----------|----------------|---------|
| 1 | Long Function          | src/Cli.cpp:16    | 41 lines, CC 11 | Extract Function x5, command map | a1b2c3d, ... |
| 2 |                        |                   |          |                |         |
| 3 |                        |                   |          |                |         |

## Bugs noticed while refactoring (not fixed here)
- ... -> logged as CR-16

## Replay: green at every step
(paste the output of the replay loop)
# replay every commit of lab-10: build and test each one (Git Bash, Linux, macOS)
for c in $(git rev-list --reverse main..lab-10); do
  git checkout -q "$c"
  if cmake --build build > /dev/null && ctest --test-dir build -Q; then
    echo "GREEN $(git log -1 --format='%h %s')"
  else
    echo "RED   $(git log -1 --format='%h %s')"
  fi
done
git checkout -q lab-10

Expected output

# Cyclomatic complexity · Cli::handleCommand (src/Cli.cpp)

| Line | Decision point              | +1 |
|------|-----------------------------|----|
| 21   | if (cmd == "list")          | 1  |
| 22   | for (const Event& e : ...)  | 1  |
| 25   | else if (cmd == "register") | 1  |
| ...  | ...                         |    |
| Total: 1 + 10 decision points = **11** (lizard: CCN 11)

After: handleCommand = 3 (one ?:, one if), largest new function = 3 (lizard agrees)
GREEN 3f1c2aa test: pin CLI output with CliTest
GREEN 8b20d41 refactor: extract Cli::cancel from handleCommand
GREEN 51e7a09 refactor: extract studentAndEvent() from three branches
GREEN c02d9be refactor: replace if-else chain with a command map
GREEN 77ab3f1 refactor: add kCancellationWindow constant
...

Show hints

  • Counting by hand: start at 1; add 1 for each if, else if, for, while, case, catch, &&, || and ?:. else and default add nothing.
  • Use the IDE's automated refactorings (Rename Symbol, Extract Function in CLion or VS Code with clangd); they are safer than copy and paste.
  • If a test turns red during a refactoring, the refactoring changed behaviour. Undo it; do not "fix" the test.
  • The replay loop leaves you in detached HEAD if you stop it half-way; git checkout lab-10 brings you back.

Acceptance checklist

4

Fix CR-13 test-first and deliver the waiting list (CR-14)

Hard Lead: Developers in pairs 120 min

Goal

Reproduce the 24-hour bug with a failing test before fixing it, then implement the waiting list (a Should feature) test-first, and bring the class and state diagrams up to date with the code.

Steps

  1. CR-13, red first: on branch fix/cr-13-cancel-24h, add the two CR-13 tests of the starter (adapt the fixture helpers and the error type to your code) and run them. Paste the failing output into CR-13.md. If both pass, your code does not have the bug: write "not reproducible, kept as regression tests" and continue.
  2. CR-13, green: fix with the smallest change (usually > versus >=, using kCancellationWindow), add the boundary case "24 h plus one second is allowed" if your Lab-09 tests lack it, and commit tests and fix together: fix: allow cancellation exactly 24 h before the start (CR-13).
  3. CR-14, tests first: on feature/cr-14-waitlist, write the six tests named in your impact analysis (the starter gives three). They fail to compile or fail: that is the red step.
  4. CR-14, implement: registerStudent returns Registered or Waitlisted; waitlisted registrations keep their order; cancelling a Registered place promotes the oldest Waitlisted one; a waitlisted student may cancel their place; the duplicate check includes waitlisted students. Commit in small feat: and test: steps.
  5. Storage and CLI: CsvRegistrationRepository reads and writes the status waitlisted in the existing status column (a v0.1.0 file must still load: add a test with such a file); the CLI prints waitlisted (position N) and gets a waitlist <event> command (one map entry if you refactored in Task 3).
  6. Models: copy your Lab-06 diagrams to lab10/diagrams/ (leave lab06/ unchanged as a record) and update class-diagram.mmd and registration-state.mmd: the state diagram must show Waitlisted → Registered (promotion) and Waitlisted → Cancelled.
  7. Run the Lab-09 coverage report: every new function in RegistrationService.cpp is executed by a test. Open one pull request per CR into lab-10, each reviewed by the other pair.

Starter

// include/clubhub/RegistrationService.hpp (CR-14 changes, excerpt)
class RegistrationService {
public:
    // Registered if a seat is free, Waitlisted if the event is full.
    RegistrationStatus registerStudent(const std::string& studentId,
                                       int eventId, TimePoint now);
    // Cancels; if a seat is freed, promotes the first waitlisted student.
    void cancel(const std::string& studentId, int eventId, TimePoint now);
    // Waitlisted registrations of an event, oldest first.
    std::vector<Registration> waitlist(int eventId) const;
};
// tests/RegistrationServiceTest.cpp (additions for CR-13 and CR-14)
// addEvent() and statusOf() are small fixture helpers: write them once.
// CancellationClosed stands for your error type from Lab-07 or Lab-09.
using namespace std::chrono;

const auto kStart = sys_days{2026y / November / 21} + 14h;

TEST_F(RegistrationServiceTest, CancelExactly24HoursBeforeStartIsAllowed) {
    const int eventId = addEvent("Arduino Workshop", kStart, 20);
    service.registerStudent("e20230001", eventId, kStart - 72h);

    EXPECT_NO_THROW(service.cancel("e20230001", eventId, kStart - 24h));
    EXPECT_EQ(statusOf("e20230001", eventId), RegistrationStatus::Cancelled);
}

TEST_F(RegistrationServiceTest, CancelOneSecondInsideWindowIsRejected) {
    const int eventId = addEvent("Arduino Workshop", kStart, 20);
    service.registerStudent("e20230001", eventId, kStart - 72h);

    EXPECT_THROW(service.cancel("e20230001", eventId, kStart - 24h + 1s),
                 CancellationClosed);
}

TEST_F(RegistrationServiceTest, FullEventPutsStudentOnWaitlist) {
    const int eventId = addEvent("Line Follower Race", kStart, 1);
    const auto now = kStart - 72h;
    EXPECT_EQ(service.registerStudent("e20230001", eventId, now),
              RegistrationStatus::Registered);
    EXPECT_EQ(service.registerStudent("e20230002", eventId, now),
              RegistrationStatus::Waitlisted);
    EXPECT_EQ(service.waitlist(eventId).size(), 1u);
}

TEST_F(RegistrationServiceTest, CancelPromotesFirstWaitlistedStudent) {
    // TODO capacity 1: register A, B, C; cancel A
    //      expect B Registered, C still Waitlisted
}
// TODO WaitlistIsFirstComeFirstServed, WaitlistedStudentCannotRegisterTwice,
//      CancellingAWaitlistedPlacePromotesNobody
%% lab10/diagrams/registration-state.mmd (skeleton)
stateDiagram-v2
  [*] --> Registered: registerStudent [seat free]
  Registered --> Cancelled: cancel [start - now >= 24 h]
  Registered --> CheckedIn: checkIn
  %% TODO: Waitlisted, its two ways out, and the guard on each transition

Expected output

$ ctest --test-dir build -R "RegistrationService|Csv|Cli" --output-on-failure
...
    Start 31: RegistrationServiceTest.CancelExactly24HoursBeforeStartIsAllowed
31/44 Test #31: RegistrationServiceTest.CancelExactly24HoursBeforeStartIsAllowed ...   Passed
    Start 33: RegistrationServiceTest.FullEventPutsStudentOnWaitlist
33/44 Test #33: RegistrationServiceTest.FullEventPutsStudentOnWaitlist .............   Passed
    Start 34: RegistrationServiceTest.CancelPromotesFirstWaitlistedStudent
34/44 Test #34: RegistrationServiceTest.CancelPromotesFirstWaitlistedStudent .......   Passed
    Start 40: CsvRegistrationRepositoryTest.LoadsVersion010FileWithoutWaitlist
40/44 Test #40: CsvRegistrationRepositoryTest.LoadsVersion010FileWithoutWaitlist ...   Passed
...
100% tests passed, 0 tests failed out of 44

The updated state diagram of Registration, rendered (yours may use different trigger names):

stateDiagram-v2
  [*] --> Registered: registerStudent [seat free]
  [*] --> Waitlisted: registerStudent [event full]
  Waitlisted --> Registered: promoted [a seat is freed]
  Waitlisted --> Cancelled: cancel
  Registered --> Cancelled: cancel [start - now >= 24 h]
  Registered --> CheckedIn: checkIn
  Cancelled --> [*]
  CheckedIn --> [*]

Show hints

  • If cancel reads the clock itself (system_clock::now()), you cannot test the 24 h boundary. Add a TimePoint now parameter (a seam) and let the CLI pass Clock::now().
  • "Oldest first" needs an order. Keep the registration time (or a sequence number) in Registration, or rely on the order of rows in the repository and test that it is preserved after a save and load.
  • Promotion belongs in RegistrationService::cancel, in one place. If the CLI or the repository also decides who gets the seat, that is Shotgun Surgery waiting to happen.
  • Mermaid state labels: write the guard in square brackets after the trigger, as in cancel [start - now >= 24 h].

Acceptance checklist

5

Release v0.2.0, changelog and maintenance plan

Medium Lead: Scrum Master as release manager 60 min

Goal

Close Sprint 2 with a proper release: one version number chosen with SemVer, a changelog, a tag that CI turns into a GitHub Release, and a short plan for who keeps Club Hub working after the course. Then record what changed in this lab.

Steps

  1. List the changes since the last release (git log --oneline v0.1.0..lab-10), group them into fixes, features and internal changes, and decide the version with SemVer. Write the reasoning in lab10/README.md (expected: 0.2.0).
  2. Keep the version in one place: project(clubhub VERSION 0.2.0 LANGUAGES CXX), generate Version.hpp from Version.hpp.in (starter), add a --version option to the CLI, and cover it with a test such as CliTest.VersionPrintsProjectVersion.
  3. Write CHANGELOG.md at the repository root in Keep a Changelog 1.1.0 format: an empty [Unreleased] section, [0.2.0] with Added, Changed and Fixed (and Deprecated if you deprecated anything), and [0.1.0] for Sprint 1. Every entry names its CR or issue.
  4. Add .github/workflows/release.yml (starter) and push. When CI is green on the head of lab-10, tag it: git tag -a v0.2.0 -m "Sprint 2 release" and git push origin v0.2.0. Check that the release run is green and that the GitHub Release shows the 0.2.0 notes. Merge the lab pull request later with a merge commit so the tagged commit stays in the history of main.
  5. Write lab10/maintenance-plan.md (starter headings): supported versions and compilers (GCC 13+, Clang 17+, MSVC 2022+), who handles bugs after the course, how bugs are reported and how fast they are answered, monthly dependency updates (the GoogleTest tag), the share of time for technical debt, an effort estimate with annual change traffic, and the handover.
  6. Close the CR-13 and CR-14 issues with a link to the release; CR-15 stays in the backlog with its triage decision. The Scrum Master closes Sprint 2 on the board.
  7. Record what changed: finish lab10/README.md with the table of this lab's files, the earlier-lab inputs you reused with their tag or commit, and a change-log row.

Starter

# CMakeLists.txt (excerpt): the single source of the version number
project(clubhub VERSION 0.2.0 LANGUAGES CXX)
configure_file(include/clubhub/Version.hpp.in
               ${PROJECT_BINARY_DIR}/generated/clubhub/Version.hpp @ONLY)
# add the generated folder to the target that contains your CLI
target_include_directories(clubhub_core PUBLIC ${PROJECT_BINARY_DIR}/generated)
// include/clubhub/Version.hpp.in
#pragma once
#include <string_view>

namespace clubhub {
inline constexpr std::string_view kVersion = "@PROJECT_VERSION@";
}
# .github/workflows/release.yml
name: release
on:
  push:
    tags: ['v*.*.*']
permissions:
  contents: write
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Tag must match the CMake project version
        run: grep -q "VERSION ${GITHUB_REF_NAME#v}" CMakeLists.txt
      - run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
      - run: cmake --build build
      - run: ctest --test-dir build --output-on-failure
      - name: Release notes = this version's CHANGELOG section
        run: |
          awk -v v="${GITHUB_REF_NAME#v}" \
            '$0 ~ "^## \\[" v "\\]" {p=1; next} /^## \[/ {p=0} p' \
            CHANGELOG.md > notes.md
      - run: gh release create "$GITHUB_REF_NAME" --notes-file notes.md
        env:
          GH_TOKEN: ${{ github.token }}
# ITC Club Hub · Maintenance plan (v1.0, <date>)
## 1. Scope and support (versions, compilers, platforms)
## 2. Roles after the course (maintainer, backup, who approves changes)
## 3. Bug intake and response times (template, triage day, blocker / major / minor)
## 4. Routine work (dependency updates, compiler updates, technical-debt share)
## 5. Effort estimate (ACT x development effort, show the numbers)
## 6. Handover and end of life
# Lab-10 · Software Maintenance and Evolution (v1.0, <date>)

## Files of this lab
| File | Task | Author(s) |
|------|------|-----------|

## Inputs reused from earlier labs
| Input | From | Version / commit |
|-------|------|------------------|
| Product backlog | Lab-05 | board snapshot <date> |
| Class and state diagrams | Lab-06 | lab06/ at <commit> |
| CI workflow | Lab-08 | ci.yml at <commit> |
| Tests and coverage | Lab-09 | tests/ at <commit> |

## Version decision
## Change log
| Date | Version | Change | By |
|------|---------|--------|----|

Expected output

$ ./build/clubhub --version
clubhub 0.2.0
$ git describe --tags
v0.2.0
$ gh release view v0.2.0          (abridged)
v0.2.0
### Added
- Waiting list for full events; a cancellation promotes the first waitlisted
  student (CR-14).
- `waitlist <event>` command.
### Changed
- `Cli::handleCommand` uses a command map (no behaviour change).
### Fixed
- Cancelling exactly 24 h before the start was rejected (CR-13).

Show hints

  • In 0.y.z a new feature bumps MINOR (0.1.0 → 0.2.0) and a fix bumps PATCH. Refactoring alone does not need a release.
  • The release job fails on purpose when the tag and CMakeLists.txt disagree. Fix the file, delete the local and remote tag (git tag -d v0.2.0, git push origin :refs/tags/v0.2.0) only if the release was never published, then tag again.
  • If gh release create fails with a permission error, check permissions: contents: write and the repository's Actions settings (workflow permissions).
  • A realistic ACT for a small tool used by one office is 10 to 25 % per year. Show the multiplication; the number matters less than the reasoning.

Acceptance checklist

Challenge: reverse-engineer and pin a legacy report module

Hard 90–120 min · optional, extra credit

Scenario

The clubs office still produces its monthly activity report with a program a student intern wrote in 2019. Nobody knows exactly what it computes, it has no tests, and the office wants Club Hub to take over the report. Before anyone changes a line, your team recovers its design, pins its current behaviour with characterization tests, and recommends a modernisation option.

Requirements

  1. Add the module below as legacy/report_legacy.cpp with the sample data in legacy/data/, build it as its own CMake target report_legacy, and run it for 2026-11 in both modes (report and --csv).
  2. Reverse-engineer lab10/legacy/as-is.mmd: a class diagram of the module as it is, with one «utility» class holding the globals and functions, plus the two record types hidden in the parallel vectors (g_ev* and g_reg*) with their attributes and multiplicities. Add a short flowchart of print_report.
  3. Golden master: save today's output as approved files in legacy/expected/ and register the provided golden.cmake check in CTest for the report mode, the CSV mode and a month with no events.
  4. Characterization tests in tests/LegacyReportTest.cpp: at least four tests that pin specific behaviours, including at least two odd ones, using the skeleton (it resets the globals the module never resets).
  5. lab10/legacy/findings.md: at least four suspicious behaviours you pinned, each written as a proposed change request with a category. Do not fix them: the characterization tests must stay green.
  6. lab10/legacy/modernisation.md: choose retain, wrap, refactor, replatform or replace, justify it (business value, technical quality, risk, effort, overlap with Club Hub's own report), draw the target design in to-be.mmd, and give a three-step plan in which the golden master stays green after every step.

Skeleton

// report_legacy.cpp - monthly activity report for the clubs office
// Written in 2019 by a student intern. No tests, no documentation.
// usage: report_legacy <events.csv> <registrations.csv> <YYYY-MM>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>

// ---- global state ----------------------------------------------------
std::vector<std::string> g_evId, g_evClub, g_evTitle, g_evDate;
std::vector<int> g_evCap;
std::vector<std::string> g_regEv, g_regStudent, g_regStatus;
int g_year = 0, g_month = 0;
int g_total = 0;      // grand total, updated by count_club()
char g_line[128];     // output buffer

void load_events(const char* path) {
    std::ifstream f(path);
    std::string line;
    std::getline(f, line); // skip header
    while (std::getline(f, line)) {
        if (line.size() < 3) continue;
        // id,club,title,start,capacity
        size_t p1 = line.find(',');
        size_t p2 = line.find(',', p1 + 1);
        size_t p3 = line.find(',', p2 + 1);
        size_t p4 = line.find(',', p3 + 1);
        g_evId.push_back(line.substr(0, p1));
        g_evClub.push_back(line.substr(p1 + 1, p2 - p1 - 1));
        g_evTitle.push_back(line.substr(p2 + 1, p3 - p2 - 1));
        g_evDate.push_back(line.substr(p3 + 1, p4 - p3 - 1));
        g_evCap.push_back(atoi(line.substr(p4 + 1).c_str()));
    }
}

void load_regs(const char* path) {
    std::ifstream f(path);
    std::string line;
    std::getline(f, line); // skip header
    while (std::getline(f, line)) {
        if (line.size() < 3) continue;
        // event_id,student_id,status
        size_t p1 = line.find(',');
        size_t p2 = line.find(',', p1 + 1);
        g_regEv.push_back(line.substr(0, p1));
        g_regStudent.push_back(line.substr(p1 + 1, p2 - p1 - 1));
        g_regStatus.push_back(line.substr(p2 + 1));
    }
}

int in_month(const std::string& date) {
    // date looks like 2026-11-20 18:00
    int y = atoi(date.substr(0, 4).c_str());
    int m = atoi(date.substr(5, 2).c_str());
    if (y == g_year && m == g_month) return 1;
    return 0;
}

// number of registrations of one event
int count_regs(const std::string& evId) {
    int n = 0;
    for (size_t i = 0; i < g_regEv.size(); i++) {
        if (g_regEv[i] == evId) n++;
    }
    return n;
}

// number of check-ins of one event
int count_checkins(const std::string& evId) {
    int n = 0;
    for (size_t i = 0; i < g_regEv.size(); i++) {
        if (g_regEv[i] == evId && g_regStatus[i] == "checked_in") n++;
    }
    return n;
}

// registrations of one club in the month (cancelled ones not counted)
int count_club(const std::string& club) {
    int n = 0;
    for (size_t e = 0; e < g_evId.size(); e++) {
        if (g_evClub[e] != club || !in_month(g_evDate[e])) continue;
        for (size_t i = 0; i < g_regEv.size(); i++) {
            if (g_regEv[i] == g_evId[e] && g_regStatus[i] != "cancelled") n++;
        }
    }
    g_total = g_total + n;
    return n;
}

void print_report() {
    // collect club names in order of first appearance
    std::vector<std::string> clubs;
    for (size_t e = 0; e < g_evId.size(); e++) {
        int found = 0;
        for (size_t c = 0; c < clubs.size(); c++)
            if (clubs[c] == g_evClub[e]) found = 1;
        if (!found) clubs.push_back(g_evClub[e]);
    }
    snprintf(g_line, sizeof g_line, "MONTHLY ACTIVITY REPORT %04d-%02d",
             g_year, g_month);
    std::cout << g_line << "\n";
    std::cout << "==========================================\n";
    for (size_t c = 0; c < clubs.size(); c++) {
        int events = 0;
        for (size_t e = 0; e < g_evId.size(); e++)
            if (g_evClub[e] == clubs[c] && in_month(g_evDate[e])) events++;
        if (events == 0) continue;
        std::cout << clubs[c] << " (" << events << " events, "
                  << count_club(clubs[c]) << " registrations)\n";
        for (size_t e = 0; e < g_evId.size(); e++) {
            if (g_evClub[e] != clubs[c] || !in_month(g_evDate[e])) continue;
            int r = count_regs(g_evId[e]);
            int ci = count_checkins(g_evId[e]);
            int pct = 0;
            if (r > 0) pct = ci * 100 / r;
            std::cout << "  " << g_evTitle[e] << ": " << r << "/"
                      << g_evCap[e] << " registered, " << ci
                      << " checked in (" << pct << "%)";
            if (r > g_evCap[e]) std::cout << " OVERBOOKED";
            std::cout << "\n";
        }
    }
    std::cout << "==========================================\n";
    std::cout << "TOTAL REGISTRATIONS: " << g_total << "\n";
}

// export the same numbers as CSV for the spreadsheet of the clubs office
void print_csv() {
    std::cout << "club,event,registered,checked_in\n";
    for (size_t e = 0; e < g_evId.size(); e++) {
        if (!in_month(g_evDate[e])) continue;
        int r = 0;
        for (size_t i = 0; i < g_regEv.size(); i++) {
            if (g_regEv[i] == g_evId[e]) r++;
        }
        int ci = 0;
        for (size_t i = 0; i < g_regEv.size(); i++) {
            if (g_regEv[i] == g_evId[e] && g_regStatus[i] == "checked_in") ci++;
        }
        std::cout << g_evClub[e] << "," << g_evTitle[e] << "," << r << ","
                  << ci << "\n";
    }
}

#ifndef REPORT_LEGACY_NO_MAIN
int main(int argc, char** argv) {
    if (argc < 4) {
        std::cout << "usage: report_legacy <events.csv> <registrations.csv>"
                     " <YYYY-MM> [--csv]\n";
        return 1;
    }
    load_events(argv[1]);
    load_regs(argv[2]);
    g_year = atoi(argv[3]);
    g_month = atoi(argv[3] + 5);
    if (argc > 4 && std::string(argv[4]) == "--csv") print_csv();
    else print_report();
    return 0;
}
#endif
# legacy/data/events.csv
id,club,title,start,capacity
1,Robotics Club,Arduino Workshop,2026-11-07 14:00,20
2,Robotics Club,Line Follower Race,2026-11-21 08:00,3
3,Debate Club,Open Debate: AI in Class,2026-11-12 17:30,40
4,Debate Club,Winter Tournament,2026-12-05 08:00,60
5,Photo Club,Campus Photo Walk,2026-10-30 16:00,15
# legacy/data/registrations.csv
event_id,student_id,status
1,e20230001,checked_in
1,e20230002,checked_in
1,e20230003,registered
2,e20230001,registered
2,e20230004,cancelled
2,e20230005,registered
2,e20230006,registered
3,e20230002,checked_in
3,e20230007,cancelled
3,e20230008,checked_in
4,e20230003,registered
5,e20230009,checked_in
# legacy/golden.cmake: golden-master check, run by CTest in script mode
execute_process(
  COMMAND "${EXE}" "${DATA}/events.csv" "${DATA}/registrations.csv" ${MONTH} ${MODE}
  OUTPUT_VARIABLE received RESULT_VARIABLE rc)
file(READ "${EXPECTED}" expected)
# compare the text, not the line endings (Windows writes \r\n)
string(REPLACE "\r" "" received "${received}")
string(REPLACE "\r" "" expected "${expected}")
if(NOT rc EQUAL 0 OR NOT received STREQUAL expected)
  file(WRITE "${EXPECTED}.received" "${received}")
  message(FATAL_ERROR "Output differs from ${EXPECTED}; see ${EXPECTED}.received")
endif()
# CMakeLists.txt (additions)
add_executable(report_legacy legacy/report_legacy.cpp)
add_test(NAME legacy.golden.report_2026_11
  COMMAND ${CMAKE_COMMAND} -DEXE=$<TARGET_FILE:report_legacy>
          -DDATA=${PROJECT_SOURCE_DIR}/legacy/data -DMONTH=2026-11
          -DEXPECTED=${PROJECT_SOURCE_DIR}/legacy/expected/report-2026-11.txt
          -P ${PROJECT_SOURCE_DIR}/legacy/golden.cmake)
# TODO: the same for --csv (-DMODE=--csv) and for a month without events

add_executable(legacy_report_test tests/LegacyReportTest.cpp)
target_compile_definitions(legacy_report_test PRIVATE
  LEGACY_DATA="${PROJECT_SOURCE_DIR}/legacy/data")
target_link_libraries(legacy_report_test PRIVATE GTest::gtest_main)
gtest_discover_tests(legacy_report_test)
// tests/LegacyReportTest.cpp: characterization tests pin TODAY's behaviour
#define REPORT_LEGACY_NO_MAIN
#include "../legacy/report_legacy.cpp"   // the legacy module has no header

#include <gtest/gtest.h>
#include <sstream>

namespace {
// The legacy code never resets its globals, so every test must.
void resetLegacyGlobals() {
    g_evId.clear(); g_evClub.clear(); g_evTitle.clear(); g_evDate.clear();
    g_evCap.clear(); g_regEv.clear(); g_regStudent.clear(); g_regStatus.clear();
    g_year = 0; g_month = 0; g_total = 0;
}

std::string runReport(int year, int month) {
    resetLegacyGlobals();
    load_events(LEGACY_DATA "/events.csv");
    load_regs(LEGACY_DATA "/registrations.csv");
    g_year = year;
    g_month = month;
    std::ostringstream out;
    std::streambuf* old = std::cout.rdbuf(out.rdbuf());
    print_report();
    std::cout.rdbuf(old);
    return out.str();
}
}  // namespace

TEST(LegacyReport, PerEventCountIncludesCancelledRegistrations) {
    // odd but current behaviour: 4 rows for event 2, one of them cancelled
    const std::string report = runReport(2026, 11);
    EXPECT_NE(report.find("Line Follower Race: 4/3 registered"),
              std::string::npos);
    EXPECT_NE(report.find("OVERBOOKED"), std::string::npos);
}

// TODO at least three more characterization tests, for example:
//  - the club line and the event lines of the same club (do they add up?)
//  - print_report() called twice in one run: what happens to the total?
//  - a month without events, and the --csv mode through print_csv()

Expected output

$ ./build/report_legacy legacy/data/events.csv legacy/data/registrations.csv 2026-11
MONTHLY ACTIVITY REPORT 2026-11
==========================================
Robotics Club (2 events, 6 registrations)
  Arduino Workshop: 3/20 registered, 2 checked in (66%)
  Line Follower Race: 4/3 registered, 0 checked in (0%) OVERBOOKED
Debate Club (1 events, 2 registrations)
  Open Debate: AI in Class: 3/40 registered, 2 checked in (66%)
==========================================
TOTAL REGISTRATIONS: 8
$ ctest --test-dir build -R legacy
    Start 20: legacy.golden.report_2026_11
1/7 Test #20: legacy.golden.report_2026_11 ...............   Passed
...
100% tests passed, 0 tests failed out of 7

The start of a good as-is.mmd (finish the attributes, the second record type and the functions):

classDiagram
  direction LR
  class report_legacy {
    <<utility>>
    +g_evId : vector~string~
    +g_evClub : vector~string~
    +g_total : int
    +load_events(path) void
    +count_club(club) int
    +print_report() void
  }
  class EventRow {
    <<recovered>>
    +id : string
    +club : string
    +capacity : int
  }
  report_legacy "1" o-- "0..*" EventRow : g_ev* vectors, same index

Show hints

  • Compare the club line with the event lines of the same club: do the numbers add up? Two copies of "count the registrations" have drifted apart.
  • Call print_report() twice in one test and look at the total. Globals that are never reset are a classic legacy trap, and the reason the skeleton resets them.
  • If every check-in shows 0 on Windows, look at the line endings of your CSV files: "checked_in\r" is not "checked_in". You have characterized a portability bug; record it, and add *.csv text eol=lf to .gitattributes instead of fixing the code now.
  • Club Hub already has (or will have) a ReportGenerator built on the repositories. A strangler-style replacement that runs both and compares their output is a strong option; argue for or against it with the evidence you collected.

Acceptance checklist

Grading rubric

ComponentPointsFull marks when…
Task 1 · Classify three change requests10Correct 14764 categories with trigger-based justification; size, urgency and MoSCoW with reasons; a triage decision that fits the calculated capacity.
Task 2 · Impact analysis of CR-1415All three rings covered with grep evidence; story with three Gherkin scenarios; tests, diagrams and documents named; justified estimate, risks, version decision and recommendation.
Task 3 · Code smells and refactoring20Three real smells with file and line; small refactor: commits, GREEN on replay; correct hand count of CC before and after, confirmed by lizard; reviewed pull request.
Task 4 · Bug fix and waiting list25Red-then-green evidence for CR-13; waiting list complete with at least six tests; old data files still load; diagrams match the code; both pull requests reviewed.
Task 5 · Release and maintenance plan20One consistent version everywhere; Keep a Changelog format with CR references; green release run and published notes; complete maintenance plan; lab10/README.md record.
Code, commit and diagram quality10Modern C++20 without raw new/delete; clear commit messages with prefixes and CR ids; every student visible in the history of Tasks 3 and 4; diagrams render.
★ Challenge (extra credit)+20As-is and to-be diagrams, golden master and characterization tests green, at least four findings as CRs, and a justified modernisation plan.
Total100 (+20)
Automatic deductions: −10 if CI is red on the tagged release commit · −5 for each refactor: commit that changes behaviour or is RED on replay · −5 if the CR-13 fix has no test that failed before it (or a recorded "not reproducible") · −5 if the version in CMakeLists.txt, the tag and CHANGELOG.md disagree · −5 for a change request without a category justification · −5 per student with no commit in Tasks 3 or 4 · −3 per test that asserts nothing.

Submission

  1. Repository layout at the end of this lab:
    club-hub/
    ├── CMakeLists.txt  CHANGELOG.md  README.md
    ├── .github/workflows/ci.yml  release.yml
    ├── include/clubhub/  (Version.hpp.in, RegistrationService.hpp, Cli.hpp, ...)
    ├── src/  tests/
    ├── legacy/                     # only if you did the challenge
    ├── lab01/ ... lab09/           # earlier labs, unchanged
    └── lab10/
        ├── change-requests/  triage.md  impact-analysis-CR-14.md
        ├── refactoring-log.md  complexity.md  diagrams/
        ├── maintenance-plan.md  README.md
        └── legacy/                 # challenge
  2. Self-check from a clean checkout of lab-10; everything must pass:
    cmake -S . -B build && cmake --build build && ctest --test-dir build
    ./build/clubhub --version        # clubhub 0.2.0
    git describe --tags              # v0.2.0 (or v0.2.0-N-g... after later commits)
    lizard src/ -C 10 -w             # no function above CC 10, or explained in complexity.md
    git shortlog -sn main..lab-10    # every team member appears
  3. Branch lab-10, pull request into main titled Lab-10 – <team name>, with links to the three CR issues and to the GitHub Release v0.2.0 in its description.