The Robust Input Desk
You're building the front desk of an order kiosk — the layer that touches the outside world. Outside-world input is hostile: users mistype, fat-finger extra characters, paste junk, leave fields blank, and (in CS6340 terms) fuzzers feed your program malformed input on purpose. A program that merely compiled is not a program that survives that. This chapter is about making it survive.
You'll write the desk as a small library of pure, testable functions — a
quantity validator, a range clamp, a keystroke parser, a stock lookup, a
strict-digits check — and then the capstone: a readIntInRange routine that
recovers from bad std::cin input and keeps re-prompting until it gets a
valid number. Along the way you use the two error tools Chapter 9 introduces for
two different jobs: assert/static_assert to document states that are
impossible in correct code (a caller's bug), and runtime validation/recovery
for states that happen in normal operation (a user typing letters). The grader
hammers every function with good and malicious inputs — empty strings, letters
where numbers go, out-of-range values, trailing junk, EOF — and proves your desk
signals correctly and never crashes.
Your tasks
- Validate a quantity.
isValidQuantity(int)returnstrueiff the value is in[kMinQuantity, kMaxQuantity]inclusive. Use>=and<=— the boundaries1and99are valid (an off-by-one with>/<would wrongly reject them). - Clamp with a precondition.
clampToRange(value, lo, hi)pinsvalueinto[lo, hi](below→lo, above→hi, in-range→unchanged).lo > hiis a caller bug, not user error, so open withassert(lo <= hi && "clampToRange precondition: lo must be <= hi");. - Parse a keystroke (sentinel on junk).
parseMenuChoice(char)maps'1'..'4'to1..4and returnskInvalidChoicefor anything else — other digits, letters, punctuation, whitespace,'\0'. Never crash on junk; hand back the sentinel so the caller can react. - Stock lookup + compile-time invariant.
isInStock(int)reads a fixed table (item 3 is sold out). Add astatic_assertproving the table has exactlykMenuItemCountentries, andassertthatitemNumberis a valid menu number (the caller's responsibility). Remember the table is 0-based, the menu is 1-based. - Strict digits-only.
digitsOnly(std::string_view)returnstrueiff the text is non-empty and every character is'0'..'9'. Empty →false. This is the read-the-whole-token-then-validate approach — stricter thanoperator>>, which would stop at the first bad char. - The capstone — robust
readIntInRange(min, max). Read one integer in[min, max]fromstd::cin, retrying until valid. Implement the full 9.5 recovery loop: detect failed extraction,clear()the stream,ignore()the rest of the line, reject out-of-range numbers, and handle EOF so a closed stream can't loop forever. It must survive letters, blank lines, trailing junk (12x), and out-of-range input without crashing.
Success criteria
- The pure-function grader prints one
FAIL: … @line Nper broken check (the expression and the line intests/tests.cppit tripped on) and endsFAIL ❌ N check(s) failed. - The
std::cingrader expects your reader to accept exactly 7 then 50 out of the hostile script; anything else printsFAIL ❌ readIntInRange did not recover correctlywith the values it actually got. isValidQuantity(0)/(100)/(-5)— just outside the inclusive[1,99]bandclampToRange(7, 7, 7)— the degeneratelo == hirange (still legal)parseMenuChoice('0')/('5')/('a')/(' ')/('\0')— every junk → sentineldigitsOnly("")— empty string is not digits-only (the easy boundary to miss)digitsOnly("12a45")/(" 123")/("-5")/("3.14")— one bad char fails the whole token- the
cinscript:abc(not a number),200x(out of range and trailing junk), blank line (skipped, no crash), then a valid value — all recovered without a crash
Concepts practiced
- Detecting & handling errors: happy path vs sad path, and who recovers (9.4)
- Sentinel values — returning an out-of-band signal (
kInvalidChoice) for bad input (9.4) std::cinfailure recovery:.fail()state,.clear(),.ignore(...), and EOF (9.5)- Read-then-validate vs letting
operator>>stop early — strict token checking (9.5) assertfor preconditions / impossible states, with a descriptive&& "msg"(9.6)static_assertfor a compile-time invariant (table size) — no runtime cost (9.6)- Input validation & boundary/category testing: empty, negative, out-of-range, junk (9.1, 9.2)
- Reused from earlier chapters: named
constexprconstants &std::string_view(Ch 5),%/relational/logical operators (Ch 6), loops,if,while(true)(Ch 8)
Constraints
- Allowed:
assert/static_assert(<cassert>),std::cin/std::coutwith.clear()and.ignore(std::numeric_limits<std::streamsize>::max(), '\n')(<limits>),while/for/if, range-for,%/relational/logical operators, namedconstexprconstants,std::string_view, and the function machinery already in the files. - Forbidden (not taught yet, after Ch 9):
try/catch/throwexceptions (mentioned in 9.4 but covered later),std::optional,std::stoi/std::from_chars, containers (std::vector), classes, templates. The notes name some of these as future options — using them here is out of scope. - Idioms required by the notes:
assertis for programmer bugs / impossible states; runtime checks + re-prompting are for user input — don'tasserton user data (9.6).- In
readIntInRange, snapshot the success flag beforeclear(), then alwaysignore()the rest of the line (so12x'sxcan't poison the next read), and handle EOF before retrying (9.5). - Use a descriptive assertion — append
&& "message"— so a failure explains itself (9.6). static_assert(notassert) for the table-size check, because it's compile-time knowable (9.6 decision table).
- Keep Tasks 1–5 pure: read the parameters, return a value — no I/O, no globals. All interactive I/O lives in Task 6.
Build & run locally
make # compile-check your starter/desk.cpp (warning-clean)
make test # grade your code -> RED until the TASK blocks are filled in
make solution # run the grader against the reference solution
make clean # remove build artifacts(make run is an alias for make test here — for this lab, "running" your code
is running the graders against it, since the graders supply main.) make test
runs two graders: tests/tests.cpp unit-tests the pure
functions (Tasks 1–5), and tests/cin_driver.cpp feeds
hostile input from tests/cin_input.txt to your
readIntInRange (Task 6).
Hints
Task 1 — inclusive bounds
return (quantity >= kMinQuantity) && (quantity <= kMaxQuantity);. Both endpoints
must count, so it's >=/<=, not >/<. Referencing the named constants
(not literal 1/99) keeps the validator in sync with the rest of the desk.
Task 2 — assert first, then clamp
assert(lo <= hi && "clampToRange precondition: lo must be <= hi");
if (value < lo) return lo;
if (value > hi) return hi;
return value; // already in range — return it UNCHANGEDThe string literal is always true, so it doesn't change the logic; it just shows
up in the abort message if the precondition is ever violated. (Want to see it
fire? Call clampToRange(5, 10, 0) from a scratch main — the program aborts
with Assertion failed: (lo <= hi && "…").)
Task 3 — char arithmetic + the sentinel
The menu digits are consecutive characters, so once you know key is one of them,
key - '0' gives the number:
if (key >= '1' && key <= '0' + kMenuItemCount) // '1'..'4'
return key - '0';
return kInvalidChoice;'0' + kMenuItemCount is the char '4'. Everything that isn't '1'..'4' —
including other digits like '0' and '5' — falls through to the sentinel.
Task 4 — static_assert vs assert
constexpr bool inStock[] { true, true, false, true };
static_assert(sizeof(inStock) / sizeof(inStock[0]) == kMenuItemCount,
"stock table must have exactly kMenuItemCount entries");
assert(itemNumber >= 1 && itemNumber <= kMenuItemCount
&& "isInStock precondition: itemNumber must be a valid menu number");
return inStock[itemNumber - 1]; // 1-based menu -> 0-based indexstatic_assert checks the table size at compile time (it's a constant, costs
nothing at runtime, and can't be disabled). assert checks the argument at
runtime, because itemNumber isn't a constant. Note the - 1: item 1 lives at
index 0.
Task 5 — reject empty, then scan
if (text.empty())
return false; // no characters -> not digits-only
for (char c : text)
if (c < '0' || c > '9') // any non-digit kills the whole token
return false;
return true;Comparing the raw char against '0'..'9' keeps the rule explicit (no
std::isdigit locale/sign surprises). The empty-string guard is the boundary the
notes' category list puts first.
Task 6 — the recovery loop, step by step
assert(min <= max && "readIntInRange precondition: min must be <= max");
while (true)
{
std::cout << "Enter a number [" << min << ", " << max << "]: ";
int value {};
std::cin >> value;
if (std::cin.eof()) // stream closed: don't loop forever
{
std::cout << "Input closed; using " << min << ".\n";
return min;
}
const bool extracted { static_cast<bool>(std::cin) }; // SNAPSHOT before clear()
std::cin.clear(); // reset a failed stream
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // drop leftovers
if (!extracted) { std::cout << "That was not a whole number. Try again.\n"; continue; }
if (value < min || value > max) { std::cout << "That number is out of range. Try again.\n"; continue; }
return value; // happy path
}The three things people get wrong: (1) reading the success flag after clear()
(too late — clear() already wiped it); (2) forgetting ignore(), so 12x
leaves x to break the next prompt; (3) not handling EOF, so a closed stream spins
forever re-failing. Order: snapshot → clear → ignore → decide.
Stretch goals
- Replace the
kInvalidChoicesentinel withstd::optional<int>, so "no valid choice" can't be confused with a real value (notes 9.4 previews this; Ch 13+). - Rewrite
digitsOnly+ a parser as a realparseQuantity(std::string_view) -> std::optional<int>usingstd::from_chars, validating range during the parse. - Make
readIntInRangedistinguish EOF from a fail state more richly (e.g. return a status enum instead of theminfallback) and report viastd::cerr. - Add exceptions: have a low-level parser
throwon malformed input and let the deskcatchit, contrasting that channel with sentinels (Ch 27). - Wire the five validators into a full interactive ordering loop that reads a menu choice, a quantity, and confirms stock — a true integration test on top of your unit-tested parts (notes 9.1: unit vs integration).
// Chapter 9 — Error Handling · Project: The Robust Input Desk (STARTER)
// ─────────────────────────────────────────────────────────────────────────────
// Fill in the six TASK blocks below. Each maps 1:1 to a task in the README and to
// a declaration in ../desk.h. The bodies currently return PLACEHOLDERS so the file
// compiles immediately — that's why `make test` is RED right now. Your job is to
// turn it GREEN by making the desk survive bad and even malicious input.
//
// make build compile-check your code (should already work)
// make test grade it (RED until you fill these in)
// make solution run the reference if you get stuck
//
// Two tools, two jobs (notes 9.6):
// • assert(...) -> conditions that are IMPOSSIBLE in correct code
// (a caller broke a precondition = a programmer bug).
// • runtime handling -> conditions that DO happen (a user typed letters) —
// detect them and recover; never crash.
#include "../desk.h"
#include <cassert> // assert (Tasks 2, 4, 6)
#include <iostream> // std::cin / std::cout (Task 6)
#include <limits> // std::numeric_limits (Task 6: ignore the rest of a line)
// ─── TASK 1: range validation ────────────────────────────────────────────────
// Return true iff `quantity` is in [kMinQuantity, kMaxQuantity], INCLUSIVE.
// Use >= and <= so both endpoints count (an off-by-one with > or < would wrongly
// reject the boundary value itself).
//
// >>> YOUR CODE HERE <<<
//
bool isValidQuantity(int /*quantity*/)
{
return false; // placeholder — replace with the range check
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 2: clamp with a PRECONDITION assertion ─────────────────────────────
// Clamp `value` into [lo, hi]: below lo -> lo, above hi -> hi, otherwise return
// value UNCHANGED. lo > hi means the CALLER passed nonsense — a programmer bug,
// not user error — so document it with an assertion at the top:
// assert(lo <= hi && "clampToRange precondition: lo must be <= hi");
// The string literal is always true; it just rides into the failure message.
//
// >>> YOUR CODE HERE <<<
//
int clampToRange(int /*value*/, int /*lo*/, int /*hi*/)
{
return 0; // placeholder — assert the precondition, then clamp
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 3: parse a keystroke into a menu choice (sentinel on bad input) ────
// Map the digit chars '1'..'4' to the ints 1..4; return kInvalidChoice for
// ANYTHING else. The menu digits are consecutive characters, so once you've
// confirmed `key` is one of them, (key - '0') gives its numeric value. Don't
// crash on junk — hand back the sentinel so the caller can react (notes 9.4).
// (Upper bound as a char: '0' + kMenuItemCount is the char '4'.)
//
// >>> YOUR CODE HERE <<<
//
int parseMenuChoice(char /*key*/)
{
return kInvalidChoice; // placeholder — recognize '1'..'4', else sentinel
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 4: stock lookup guarded by a COMPILE-TIME invariant ────────────────
// The lookup table below must have exactly one entry per menu item. Add a
// static_assert that proves that at COMPILE time so the table and kMenuItemCount
// can never silently disagree:
// static_assert(sizeof(inStock) / sizeof(inStock[0]) == kMenuItemCount,
// "stock table must have exactly kMenuItemCount entries");
// itemNumber is the caller's responsibility to keep in 1..kMenuItemCount, so an
// out-of-range value is a programmer bug -> assert it. Then return the table
// entry (remember: itemNumber is 1-based, array indices are 0-based).
//
// >>> YOUR CODE HERE <<<
//
bool isInStock(int /*itemNumber*/)
{
// inStock[i] describes menu item (i + 1). Item 3 is sold out.
// (A fixed C-style array — formally Chapter 17 — provided here because it's
// the natural table for static_assert to guard. You only index it.)
constexpr bool inStock[] { true, true, false, true };
(void)inStock; // placeholder no-op so this compiles; DELETE when you use the table
return true; // placeholder — add the static_assert + assert, then look it up
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 5: strict text validation — digits only ───────────────────────────
// Return true iff `text` is NON-EMPTY and EVERY character is a digit '0'..'9'.
// Empty text must return false (it has no digits). Loop over the characters; the
// moment you see a char outside '0'..'9', return false. This is the
// read-the-whole-token-then-validate approach (notes 9.5) — stricter than letting
// operator>> stop at the first bad character.
//
// >>> YOUR CODE HERE <<<
//
bool digitsOnly(std::string_view /*text*/)
{
return false; // placeholder — reject empty, then check every char is a digit
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 6: the capstone — robust readIntInRange (cin.fail/clear/ignore) ────
// Read one integer in [min, max] from std::cin, RETRYING until it's valid.
// Implement the full Chapter 9.5 recovery loop. The pieces you need:
//
// assert(min <= max && "..."); // precondition (programmer's job)
// while (true) {
// std::cout << "Enter a number [" << min << ", " << max << "]: ";
// int value {};
// std::cin >> value;
//
// if (std::cin.eof()) { /* stop: return min, else you'd loop forever */ }
//
// const bool extracted { static_cast<bool>(std::cin) }; // snapshot BEFORE clearing
// std::cin.clear(); // reset a failed stream
// std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // drop leftovers
//
// if (!extracted) { /* "not a whole number"; continue */ }
// if (value < min || value > max) { /* "out of range"; continue */ }
// return value; // happy path
// }
//
// Why the order matters: snapshot success BEFORE clear() (clear() wipes the
// flags); always ignore() the rest of the line so junk like the "x" in "12x"
// can't poison the next prompt; handle EOF so a closed stream can't spin forever.
//
// >>> YOUR CODE HERE <<<
//
int readIntInRange(int min, int /*max*/)
{
return min; // placeholder — implement the retry/recovery loop above
}
// ─────────────────────────────────────────────────────────────────────────────
Try the lab first — the learning is in the attempt.
// Chapter 9 — Error Handling · Project: The Robust Input Desk (REFERENCE SOLUTION)
// ─────────────────────────────────────────────────────────────────────────────
// One complete, correct, warning-clean implementation. Peek only after you've
// taken a real swing at starter/desk.cpp — the learning is in handling the bad
// input yourself, then comparing.
//
// Notice the two distinct error tools, used for two distinct jobs (notes 9.6):
// • assert(...) — for conditions that are IMPOSSIBLE in correct code
// (a caller broke a precondition = a programmer bug).
// • runtime handling — for conditions that DO happen in normal operation
// (a user typed letters = expected reality, recover).
#include "../desk.h"
#include <cassert> // assert (Task 2, 4, 6) — notes 9.6
#include <iostream> // std::cin / std::cout (Task 6) — notes 9.5
#include <limits> // std::numeric_limits (Task 6 ignore) — notes 9.5
// TASK 1 — range validation.
// The simplest validator: a value is acceptable iff it sits inside the allowed
// closed interval. Both bounds are INCLUSIVE, so use <= / >= (an off-by-one here
// would wrongly reject kMaxQuantity itself — notes 9.3 boundary errors).
bool isValidQuantity(int quantity)
{
return (quantity >= kMinQuantity) && (quantity <= kMaxQuantity);
}
// TASK 2 — clamp with a precondition assertion.
// lo > hi is not a user mistake the desk should "handle" — it means whoever
// CALLED clampToRange passed nonsense, i.e. a bug in the program. assert documents
// that impossible state: in a debug build it aborts with a clear diagnostic; in a
// release build (NDEBUG) it compiles out. The string literal is always true, so it
// doesn't change the logic — it just rides along into the failure message (9.6).
int clampToRange(int value, int lo, int hi)
{
assert(lo <= hi && "clampToRange precondition: lo must be <= hi");
if (value < lo)
return lo;
if (value > hi)
return hi;
return value; // already in range — return UNCHANGED (the easy-to-miss case)
}
// TASK 3 — parse a keystroke into a menu choice, with a sentinel for bad input.
// '1'..'4' map to 1..4; everything else returns kInvalidChoice. Because the menu
// digits are CONSECUTIVE characters, (key - '0') converts a digit char to its int
// value — but ONLY after we've confirmed key is one of our menu digits. Returning
// a sentinel (not aborting, not throwing) lets the caller decide what to do, which
// is the right call for ordinary "the user typed something odd" input (9.4).
int parseMenuChoice(char key)
{
if (key >= '1' && key <= '0' + kMenuItemCount) // i.e. '1'..'4'
return key - '0';
return kInvalidChoice; // any other char: out-of-band "no valid choice"
}
// TASK 4 — stock lookup guarded by a compile-time invariant.
// The lookup table must have exactly one entry per menu item. static_assert
// proves that at COMPILE time (no runtime cost, can't be disabled by NDEBUG —
// notes 9.6): if someone later edits the table or changes kMenuItemCount, the
// build breaks loudly instead of reading past the end at runtime.
//
// itemNumber is the CALLER's responsibility to keep in 1..kMenuItemCount, so an
// out-of-range value is a programmer bug -> assert (a precondition), not handling.
bool isInStock(int itemNumber)
{
// inStock[i] describes menu item (i + 1). Item 3 is sold out.
// (A fixed C-style array — formally Chapter 17 — used here as the natural
// table for static_assert to guard.)
constexpr bool inStock[] { true, true, false, true };
static_assert(sizeof(inStock) / sizeof(inStock[0]) == kMenuItemCount,
"stock table must have exactly kMenuItemCount entries");
assert(itemNumber >= 1 && itemNumber <= kMenuItemCount
&& "isInStock precondition: itemNumber must be a valid menu number");
// Shift the 1-based menu number to a 0-based array index.
return inStock[itemNumber - 1];
}
// TASK 5 — strict text validation (read-then-validate, notes 9.5).
// "Digits only" means: at least one character, and EVERY character in '0'..'9'.
// We compare the raw char against the digit range rather than std::isdigit so the
// rule is explicit and has no locale/sign-extension footguns. An empty view has
// no digits, so it must return false (the easy-to-forget boundary — 9.2 category
// testing lists "empty string" first for a reason).
bool digitsOnly(std::string_view text)
{
if (text.empty())
return false;
for (char c : text) // range-for (a preview — formally Ch 16.8; an index loop works too)
{
if (c < '0' || c > '9') // any non-digit disqualifies the whole token
return false;
}
return true;
}
// TASK 6 — the capstone: robustly read an int in [min, max], retrying on bad input.
// This is the complete Chapter 9.5 recovery loop. Walk through what can go wrong
// and how each case is handled — none of them may crash or spin forever:
//
// "abc" -> extraction FAILS; std::cin goes into a fail state. We must clear()
// it and discard the buffered bad characters, else every future read
// fails instantly (an accidental infinite loop — notes 9.3/9.5).
// "12x" -> extraction SUCCEEDS reading 12 and leaves "x\n" in the buffer. We
// discard that leftover so it can't corrupt the next prompt.
// "999" -> extraction succeeds but the value is OUT OF RANGE: syntactically
// valid, semantically rejected. Re-prompt.
// ""/EOF -> if the input stream closes (end-of-file), clearing and retrying
// would loop forever, so we stop and return min as a safe fallback.
int readIntInRange(int min, int max)
{
assert(min <= max && "readIntInRange precondition: min must be <= max");
while (true) // retry loop — owned here because input collection owns the policy (9.4)
{
std::cout << "Enter a number [" << min << ", " << max << "]: ";
int value {};
std::cin >> value;
// If the stream has hit end-of-file, no more input is coming. Clearing and
// retrying here would spin forever (9.5 EOF warning). Bail out safely.
if (std::cin.eof())
{
std::cout << "Input closed; using " << min << ".\n";
return min;
}
// Snapshot whether extraction succeeded BEFORE we clear the state. Then
// ALWAYS clear() and discard the rest of the line — this resets a failed
// stream AND throws away leftover junk like the "x\n" after "12x".
const bool extracted { static_cast<bool>(std::cin) };
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
if (!extracted)
{
std::cout << "That was not a whole number. Try again.\n";
continue; // back to the prompt
}
if (value < min || value > max)
{
std::cout << "That number is out of range. Try again.\n";
continue;
}
return value; // happy path: a valid, in-range integer
}
}
// Chapter 9 — Error Handling · Project: The Robust Input Desk (GRADER — std::cin recovery)
// ─────────────────────────────────────────────────────────────────────────────
// readIntInRange (Task 6) reads std::cin, so it can't be checked by a normal
// CHECK(expr) call — we have to actually feed it input. This little driver does
// exactly that: it calls readIntInRange TWICE (proving the stream stays usable
// after the first recovery), then prints each accepted value on its own
// "ACCEPTED=<n>" line so the Makefile can verify the results, no matter how many
// retry/error messages the function printed in between.
//
// The Makefile (`cin-test` target) pipes a HOSTILE script into this program
// (tests/cin_input.txt). Traced against a correct readIntInRange(1, 99):
//
// FIRST call must return 7:
// abc not a number -> extraction fails -> clear+ignore, retry
// 200x 200 is out of range -> rejected; the trailing "x" is discarded
// 7 valid -> ACCEPTED=7
// SECOND call must return 50:
// !!! not a number -> extraction fails -> clear+ignore, retry
// 0 below the min of 1 -> rejected
// (blank) whitespace only -> >> skips it harmlessly, no crash
// 50 valid -> ACCEPTED=50
//
// Note the "200x" line does double duty: it proves an out-of-range value is
// rejected AND that leftover junk after the number is dropped (had we used a
// trailing-junk line whose number was IN range, like "12x", the reader would
// correctly accept 12 — so we deliberately make the prefix out of range here).
//
// If readIntInRange crashes, loops forever, or returns a wrong/out-of-range value,
// the two ACCEPTED lines won't read "7" then "50", and `make test` stays RED.
//
// The Makefile links this against starter/desk.cpp (your code) for `make test`,
// and against solution/desk.cpp for `make test-solution`.
#include <iostream>
#include "../desk.h"
int main()
{
// Read two integers in [1, 99] through the robust reader. All the hostile
// lines in between must be absorbed and recovered from without a crash.
const int first { readIntInRange(1, 99) };
const int second { readIntInRange(1, 99) };
// Machine-readable results on their own lines (normal output -> std::cout).
// The leading '\n' guarantees the first ACCEPTED= line starts cleanly, even
// though the last prompt readIntInRange printed had no trailing newline.
std::cout << '\n';
std::cout << "ACCEPTED=" << first << '\n';
std::cout << "ACCEPTED=" << second << '\n';
std::cout << "DONE\n";
return 0;
}
abc 200x 7 !!! 0 50
// Chapter 9 — Error Handling · Project: The Robust Input Desk (GRADER — pure functions)
// ─────────────────────────────────────────────────────────────────────────────
// A tiny no-framework unit-test harness (same style as the drills/CLAUDE.md spec).
// It includes ../desk.h and calls the PURE validators/parsers (Tasks 1–5) across
// MANY inputs — good ones AND deliberately malicious ones — checking each returns
// the right signal. Every CHECK that fails prints its expression and line number;
// any failure -> non-zero exit -> `make test` is RED.
//
// Task 6 (readIntInRange) reads std::cin, so it can't be unit-tested by calling it
// here — it's graded separately by FEEDING hostile input on stdin. See
// tests/cin_driver.cpp and the `cin-test` target in the Makefile.
//
// NOTE on assertions: this file is compiled with assertions LIVE (no -DNDEBUG), so
// clampToRange() and isInStock() are only ever called here with VALID preconditions
// (lo <= hi; itemNumber in 1..kMenuItemCount). Tripping a precondition on purpose
// would correctly abort the program — that's the assert doing its job, not a test
// failure. (Try it yourself from the README to watch one fire.)
#include <iostream>
#include "../desk.h"
static int fails = 0;
// CHECK: assert a boolean condition; on failure, report what and where.
#define CHECK(cond) \
do { if(!(cond)){ std::cerr << "FAIL: " #cond " @line " << __LINE__ << "\n"; ++fails; } } while(0)
int main()
{
// ── Task 1: isValidQuantity — the inclusive [1, 99] boundary ─────────────
CHECK(isValidQuantity(1) == true); // lower boundary is VALID
CHECK(isValidQuantity(50) == true); // ordinary middle value
CHECK(isValidQuantity(99) == true); // upper boundary is VALID
CHECK(isValidQuantity(0) == false); // malicious: just below the floor
CHECK(isValidQuantity(100) == false); // malicious: just above the cap
CHECK(isValidQuantity(-5) == false); // malicious: negative quantity
CHECK(isValidQuantity(kMinQuantity) == true); // stays in sync with the constants
CHECK(isValidQuantity(kMaxQuantity) == true);
// ── Task 2: clampToRange — pin values into [lo, hi] (valid preconditions) ─
CHECK(clampToRange(5, 0, 10) == 5); // already in range -> unchanged
CHECK(clampToRange(-3, 0, 10) == 0); // below low -> low
CHECK(clampToRange(42, 0, 10) == 10); // above high -> high
CHECK(clampToRange(0, 0, 10) == 0); // boundary: equals low
CHECK(clampToRange(10, 0, 10) == 10); // boundary: equals high
CHECK(clampToRange(7, 7, 7) == 7); // edge: degenerate range lo == hi (still legal)
CHECK(clampToRange(-100, -10, -1) == -10); // works with negative ranges too
// ── Task 3: parseMenuChoice — digits map; EVERYTHING else is the sentinel ─
CHECK(parseMenuChoice('1') == 1);
CHECK(parseMenuChoice('2') == 2);
CHECK(parseMenuChoice('3') == 3);
CHECK(parseMenuChoice('4') == 4); // top valid menu digit
CHECK(parseMenuChoice('0') == kInvalidChoice); // malicious: digit, but not on the menu
CHECK(parseMenuChoice('5') == kInvalidChoice); // malicious: digit just past the menu
CHECK(parseMenuChoice('9') == kInvalidChoice); // malicious: high digit
CHECK(parseMenuChoice('a') == kInvalidChoice); // malicious: a letter
CHECK(parseMenuChoice('!') == kInvalidChoice); // malicious: punctuation
CHECK(parseMenuChoice(' ') == kInvalidChoice); // malicious: whitespace
CHECK(parseMenuChoice('\0') == kInvalidChoice); // edge: null character
// ── Task 4: isInStock — the fixed table (item 3 sold out), valid numbers ─
CHECK(isInStock(1) == true);
CHECK(isInStock(2) == true);
CHECK(isInStock(3) == false); // the deliberately out-of-stock item
CHECK(isInStock(4) == true); // boundary: last valid menu number
// ── Task 5: digitsOnly — read-then-validate; reject anything non-digit ───
CHECK(digitsOnly("12345") == true); // normal: all digits
CHECK(digitsOnly("0") == true); // single digit
CHECK(digitsOnly("007") == true); // leading zeros are still digits
CHECK(digitsOnly("") == false); // EDGE: empty string is NOT digits-only
CHECK(digitsOnly("12a45") == false); // malicious: an embedded letter
CHECK(digitsOnly(" 123") == false); // malicious: a leading space
CHECK(digitsOnly("123 ") == false); // malicious: a trailing space
CHECK(digitsOnly("-5") == false); // malicious: a sign is not a digit
CHECK(digitsOnly("3.14") == false); // malicious: a decimal point
CHECK(digitsOnly("abc") == false); // malicious: all letters
if (!fails)
std::cout << "PASS \xE2\x9C\x85 all input-desk checks passed.\n";
else
std::cerr << "\nFAIL \xE2\x9D\x8C " << fails << " check(s) failed — fix the TASK blocks in desk.cpp.\n";
return fails ? 1 : 0;
}
// Chapter 9 — Error Handling · Project: The Robust Input Desk
// ─────────────────────────────────────────────────────────────────────────────
// This header is the CONTRACT between you and the grader. It declares every
// function you must implement plus the named constants the desk runs on. DO NOT
// EDIT THIS FILE — the grader (tests/tests.cpp) includes it, and so do BOTH
// starter/desk.cpp (yours) and solution/desk.cpp (the reference). Change a
// signature here and nothing links.
//
// THE BIG IDEA (Chapter 9): a program that compiled is not a program that is
// correct. Users — and fuzzers — type garbage: letters where you wanted a
// number, values out of range, blank lines, trailing junk, end-of-file. A
// ROBUST program decides what is acceptable and handles everything else
// gracefully: no crash, a clear signal, a chance to recover.
//
// You will build the "input desk" of an order kiosk as a small library of PURE,
// TESTABLE functions (Tasks 1–5) plus ONE interactive recovery routine that
// survives bad std::cin input (Task 6).
//
// • Pure validators/parsers -> unit-tested with good AND malicious inputs.
// • assert / static_assert -> document impossible states (programmer bugs).
// • readIntInRange -> the cin.fail()/clear()/ignore() recovery loop,
// tested by FEEDING it hostile input on stdin.
//
// Header guard (Chapter 2): stops this file being pasted in twice per build.
#ifndef DESK_H
#define DESK_H
#include <string_view> // std::string_view — cheap, non-owning text view (Chapter 5)
// ─── The desk's configuration (named constants — Chapter 5) ──────────────────
// Real programs scatter "magic numbers" everywhere; named constexpr constants
// keep the rules in ONE place so validators and tests can't drift apart.
inline constexpr int kMinQuantity { 1 }; // can't order zero or fewer items
inline constexpr int kMaxQuantity { 99 }; // per-line order cap
// The kiosk menu has exactly this many items, numbered 1..kMenuItemCount.
inline constexpr int kMenuItemCount { 4 };
// Sentinel value (notes 9.4): a result OUTSIDE the normal set that means "no
// valid value." parseMenuChoice returns this for any unrecognized keystroke.
// It is deliberately not a real menu number (1..kMenuItemCount), so a caller
// can always tell "invalid" apart from a genuine choice.
inline constexpr int kInvalidChoice { -1 };
// ─── TASK 1 ──────────────────────────────────────────────────────────────────
// Range validation. Return true iff `quantity` is an orderable amount, i.e. it
// lies in [kMinQuantity, kMaxQuantity] INCLUSIVE. This is the simplest shape of
// input validation: "is this value even allowed?"
bool isValidQuantity(int quantity);
// ─── TASK 2 ──────────────────────────────────────────────────────────────────
// Clamp `value` into [lo, hi] (values below lo become lo; above hi become hi;
// in-range values are returned unchanged). PRECONDITION: lo <= hi. Document that
// impossible-state with assert(lo <= hi) — a caller passing lo > hi is a
// PROGRAMMER BUG, not user error, so an assertion (not a runtime check) is right.
int clampToRange(int value, int lo, int hi);
// ─── TASK 3 ──────────────────────────────────────────────────────────────────
// Parse a single keystroke into a menu choice. Map the digit characters
// '1'..'4' to the ints 1..4; return the SENTINEL kInvalidChoice for ANYTHING
// else (other digits, letters, punctuation, whitespace). This is error handling
// by sentinel: the function never crashes and never lies — bad input gets the
// out-of-band signal so the caller can react.
int parseMenuChoice(char key);
// ─── TASK 4 ──────────────────────────────────────────────────────────────────
// Stock lookup guarded by a COMPILE-TIME invariant. `itemNumber` is expected to
// be a valid menu number (1..kMenuItemCount) — the caller guarantees that, so an
// out-of-range itemNumber is a programmer bug: assert it. Inside, decide
// in-stock from this fixed table (item 3 is sold out):
// item 1 -> in stock item 3 -> OUT of stock
// item 2 -> in stock item 4 -> in stock
// The table has kMenuItemCount entries; a static_assert must prove that at
// COMPILE time so the table and the menu size can never silently disagree.
bool isInStock(int itemNumber);
// ─── TASK 5 ──────────────────────────────────────────────────────────────────
// Text validation: return true iff `text` is NON-EMPTY and EVERY character is a
// decimal digit '0'..'9'. (Empty text is NOT digits-only.) This is the
// "read a whole token, then validate it yourself" approach from notes 9.5 —
// stricter than letting operator>> stop at the first bad char. Category testing
// territory: empty, normal digits, a leading space, an embedded letter, signs.
bool digitsOnly(std::string_view text);
// ─── TASK 6 ──────────────────────────────────────────────────────────────────
// THE CAPSTONE. Robustly read one integer in [min, max] from std::cin, RETRYING
// until the user supplies a valid one. This is the full Chapter 9.5 recovery
// pattern: detect failed extraction, std::cin.clear() the error state, then
// ignore the rest of the line, and ALSO reject syntactically-valid-but-
// out-of-range numbers. It must survive letters, blank lines, trailing junk
// (e.g. "12x"), and out-of-range values WITHOUT crashing or looping forever.
// PRECONDITION (programmer's responsibility): min <= max — assert it.
// Prompts/messages go to std::cout; the accepted value is returned.
int readIntInRange(int min, int max);
#endif // DESK_H
# Chapter 9 — Error Handling · The Robust Input Desk · unit-test grader (Style B).
# Targets follow the drills/CLAUDE.md Makefile contract. TABS, not spaces.
#
# The learner implements ../desk.h in starter/desk.cpp. There is no main() in
# desk.cpp on purpose — the graders supply main():
# • tests/tests.cpp unit-tests the PURE functions (Tasks 1–5) on good and
# malicious inputs.
# • tests/cin_driver.cpp drives the std::cin recovery routine (Task 6) by
# FEEDING it tests/cin_input.txt (letters, out-of-range,
# blank line, trailing junk) and checking it recovers.
# `make test` runs BOTH and is GREEN only when every part passes.
CXX := clang++
CXXFLAGS := -std=c++17 -Wall -Wextra
.PHONY: all build run test test-pure cin-test solution test-solution test-solution-pure cin-test-solution clean
all: build
# build — compile-check the learner's code (warning-clean object file, no link).
# There is no program to link yet because the tests/ files provide main(); the
# test targets do the full build+run. This just proves your desk.cpp compiles.
build:
$(CXX) $(CXXFLAGS) -c starter/desk.cpp -o starter/desk.o
@echo "OK \xE2\x9C\x85 starter/desk.cpp compiles. Now run: make test"
# run — for this lab, "running" the code IS running the graders against it.
run: test
# ── LEARNER'S GRADE (red -> green) ───────────────────────────────────────────
# test — run BOTH graders against starter/desk.cpp. RED until the TASK blocks are
# filled in; GREEN once they're all correct.
test: test-pure cin-test
@echo "PASS \xE2\x9C\x85 all parts passed — the input desk is robust."
# Part 1: unit-test the pure validators/parsers (Tasks 1–5).
test-pure:
$(CXX) $(CXXFLAGS) tests/tests.cpp starter/desk.cpp -o tests/run
@./tests/run
# Part 2: feed hostile input to the std::cin recovery routine (Task 6) and verify
# it accepts exactly 7 then 50. `grep ACCEPTED` ignores the retry/error messages
# printed in between; we compare only the accepted values.
cin-test:
$(CXX) $(CXXFLAGS) tests/cin_driver.cpp starter/desk.cpp -o tests/cin_run
@./tests/cin_run < tests/cin_input.txt > tests/.cin_out.txt 2>/dev/null; \
grep '^ACCEPTED=' tests/.cin_out.txt > tests/.cin_got.txt || true; \
printf 'ACCEPTED=7\nACCEPTED=50\n' > tests/.cin_exp.txt; \
if diff -q tests/.cin_exp.txt tests/.cin_got.txt >/dev/null 2>&1; then \
echo "PASS \xE2\x9C\x85 readIntInRange recovered from hostile input (got 7 then 50)."; \
else \
echo "FAIL \xE2\x9D\x8C readIntInRange did not recover correctly."; \
echo " expected accepted values:"; sed 's/^/ /' tests/.cin_exp.txt; \
echo " got:"; sed 's/^/ /' tests/.cin_got.txt; \
exit 1; \
fi
# ── SOLVABILITY PROOF (must be green) ────────────────────────────────────────
# solution — build+run the graders against the reference implementation.
solution: test-solution
# test-solution — proof the lab is solvable: the reference MUST pass every part.
test-solution: test-solution-pure cin-test-solution
@echo "PASS \xE2\x9C\x85 solution passes all parts."
test-solution-pure:
$(CXX) $(CXXFLAGS) tests/tests.cpp solution/desk.cpp -o tests/run
@./tests/run
cin-test-solution:
$(CXX) $(CXXFLAGS) tests/cin_driver.cpp solution/desk.cpp -o tests/cin_run
@./tests/cin_run < tests/cin_input.txt > tests/.cin_out.txt 2>/dev/null; \
grep '^ACCEPTED=' tests/.cin_out.txt > tests/.cin_got.txt || true; \
printf 'ACCEPTED=7\nACCEPTED=50\n' > tests/.cin_exp.txt; \
if diff -q tests/.cin_exp.txt tests/.cin_got.txt >/dev/null 2>&1; then \
echo "PASS \xE2\x9C\x85 solution's readIntInRange recovered from hostile input."; \
else \
echo "FAIL \xE2\x9D\x8C solution's readIntInRange did not recover correctly."; \
exit 1; \
fi
clean:
rm -f starter/desk.o tests/run tests/cin_run
rm -f tests/.cin_out.txt tests/.cin_got.txt tests/.cin_exp.txt
rm -rf tests/run.dSYM tests/cin_run.dSYM
make test locally
(see “Build & run locally” above).