Skip to content

Commit 0600be5

Browse files
georgeglarsonclaude
andcommitted
Add 132 tests across all 6 exercises targeting audit gaps
Covers parser boundaries, overflow isolation, negative ranges, malformed delimiters, round-trip symmetry, movement order, CLI entry points, rate limiting, validation utilities, and FK constraints. Total: 968 tests. CashRegister (Rust): 79 → 94 (+15) MissingNumber (Zig): 53 → 70 (+17) MorseCode (Perl): 200 → 232 (+32) OnScreenKeyboard (Python): 74 → 94 (+20) GildedRose (Go): 82 → 107 (+25) RestaurantReviews (TS): 348 → 371 (+23) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ab86cb5 commit 0600be5

16 files changed

Lines changed: 1223 additions & 0 deletions

File tree

CashRegister/src/parse.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,4 +239,79 @@ mod tests {
239239
other => panic!("expected MalformedLine, got {:?}", other),
240240
}
241241
}
242+
243+
#[test]
244+
fn parse_rejects_negative_amount() {
245+
let err = parse_dollars_to_cents("-2.13").unwrap_err();
246+
assert!(
247+
err.contains("invalid") || err.contains("not a valid"),
248+
"expected rejection of negative amount, got: {err}"
249+
);
250+
}
251+
252+
#[test]
253+
fn parse_rejects_negative_penny() {
254+
let err = parse_dollars_to_cents("-0.01").unwrap_err();
255+
assert!(
256+
err.contains("invalid") || err.contains("not a valid"),
257+
"expected rejection of negative amount, got: {err}"
258+
);
259+
}
260+
261+
#[test]
262+
fn parse_zero_dollars_and_cents() {
263+
assert_eq!(parse_dollars_to_cents("0.00"), Ok(0));
264+
}
265+
266+
#[test]
267+
fn parse_near_overflow_boundary_ok() {
268+
// 42949672 * 100 + 95 = 4294967295 = u32::MAX
269+
assert_eq!(
270+
parse_dollars_to_cents("42949672.95"),
271+
Ok(u32::MAX),
272+
"42949672.95 should parse to u32::MAX (4294967295 cents)"
273+
);
274+
}
275+
276+
#[test]
277+
fn parse_line_multiple_commas_errors() {
278+
let result = parse_line("2.13,,3.00", 1);
279+
assert!(
280+
result.is_err(),
281+
"multiple commas should produce an error, got: {:?}",
282+
result
283+
);
284+
}
285+
286+
#[test]
287+
fn parse_line_zero_payment() {
288+
let tx = parse_line("0.00,0.00", 1).unwrap();
289+
assert_eq!(tx.owed_cents, 0);
290+
assert_eq!(tx.paid_cents, 0);
291+
assert_eq!(tx.change_cents, 0);
292+
}
293+
294+
#[test]
295+
fn parse_input_all_blank_lines() {
296+
let results = parse_input("\n\n\n");
297+
assert!(
298+
results.is_empty(),
299+
"all-blank input should produce empty vec, got {} results",
300+
results.len()
301+
);
302+
}
303+
304+
#[test]
305+
fn parse_input_line_numbering_after_multiple_blanks() {
306+
// Lines: 1="", 2="", 3="bad_line", 4=""
307+
let input = "\n\nbad_line\n";
308+
let results = parse_input(input);
309+
assert_eq!(results.len(), 1);
310+
match &results[0] {
311+
Err(CashRegisterError::MalformedLine { line, .. }) => {
312+
assert_eq!(*line, 3, "expected line 3, got {line}");
313+
}
314+
other => panic!("expected MalformedLine for line 3, got {:?}", other),
315+
}
316+
}
242317
}

CashRegister/tests/integration.rs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,170 @@ fn verbose_eur() {
392392
assert!(lines[1].starts_with("Owed €3.33, Paid €5.00 -> "));
393393
}
394394

395+
// ─── Negative amount tests ──────────────────────────────────────────
396+
397+
#[test]
398+
fn negative_amount_errors() {
399+
let dir = env!("CARGO_MANIFEST_DIR");
400+
let path = format!("{dir}/test_negative.txt");
401+
std::fs::write(&path, "-2.13,3.00\n").unwrap();
402+
403+
let output = cargo_bin()
404+
.arg(&path)
405+
.output()
406+
.expect("failed to run binary");
407+
408+
std::fs::remove_file(&path).ok();
409+
410+
assert!(!output.status.success());
411+
let stderr = String::from_utf8_lossy(&output.stderr);
412+
assert!(
413+
stderr.contains("line 1"),
414+
"expected line number in error, got: {stderr}"
415+
);
416+
// stdout should be empty — no valid lines
417+
let stdout = String::from_utf8_lossy(&output.stdout);
418+
assert!(stdout.is_empty(), "expected no stdout, got: {stdout}");
419+
}
420+
421+
#[test]
422+
fn exit_code_is_2_on_line_error() {
423+
let dir = env!("CARGO_MANIFEST_DIR");
424+
let path = format!("{dir}/test_exit2.txt");
425+
std::fs::write(&path, "bad_line\n").unwrap();
426+
427+
let output = cargo_bin()
428+
.arg(&path)
429+
.output()
430+
.expect("failed to run binary");
431+
432+
std::fs::remove_file(&path).ok();
433+
434+
assert_eq!(
435+
output.status.code(),
436+
Some(2),
437+
"expected exit code 2, got: {:?}",
438+
output.status.code()
439+
);
440+
}
441+
442+
#[test]
443+
fn exit_code_is_0_on_success() {
444+
let output = cargo_bin()
445+
.args(["sample_input.txt", "--seed", "42"])
446+
.output()
447+
.expect("failed to run binary");
448+
449+
assert_eq!(
450+
output.status.code(),
451+
Some(0),
452+
"expected exit code 0, got: {:?}",
453+
output.status.code()
454+
);
455+
}
456+
457+
#[test]
458+
fn empty_input_produces_no_output_and_exits_0() {
459+
let dir = env!("CARGO_MANIFEST_DIR");
460+
let path = format!("{dir}/test_empty.txt");
461+
std::fs::write(&path, "\n\n").unwrap();
462+
463+
let output = cargo_bin()
464+
.arg(&path)
465+
.output()
466+
.expect("failed to run binary");
467+
468+
std::fs::remove_file(&path).ok();
469+
470+
let stdout = String::from_utf8_lossy(&output.stdout);
471+
let stderr = String::from_utf8_lossy(&output.stderr);
472+
assert!(stdout.is_empty(), "expected no stdout, got: {stdout}");
473+
assert!(stderr.is_empty(), "expected no stderr, got: {stderr}");
474+
assert_eq!(
475+
output.status.code(),
476+
Some(0),
477+
"expected exit code 0, got: {:?}",
478+
output.status.code()
479+
);
480+
}
481+
482+
#[test]
483+
fn invalid_seed_flag_is_silently_ignored() {
484+
// --seed abc cannot be parsed as u64, so parse_flag returns None
485+
// and the program runs with a random (non-seeded) RNG — no error.
486+
let output = cargo_bin()
487+
.args(["sample_input.txt", "--seed", "abc"])
488+
.output()
489+
.expect("failed to run binary");
490+
491+
assert!(
492+
output.status.success(),
493+
"bad --seed should not crash; stderr: {}",
494+
String::from_utf8_lossy(&output.stderr)
495+
);
496+
let stdout = String::from_utf8_lossy(&output.stdout);
497+
assert_eq!(
498+
stdout.lines().count(),
499+
3,
500+
"should still produce 3 output lines"
501+
);
502+
}
503+
504+
#[test]
505+
fn missing_divisor_value_uses_default() {
506+
// --divisor is the last arg with no value following it.
507+
// parse_flag returns None, so divisor defaults to 3.
508+
let output = cargo_bin()
509+
.args(["sample_input.txt", "--divisor"])
510+
.output()
511+
.expect("failed to run binary");
512+
513+
assert!(
514+
output.status.success(),
515+
"missing --divisor value should fall back to default; stderr: {}",
516+
String::from_utf8_lossy(&output.stderr)
517+
);
518+
let stdout = String::from_utf8_lossy(&output.stdout);
519+
assert_eq!(
520+
stdout.lines().count(),
521+
3,
522+
"should still produce 3 output lines"
523+
);
524+
}
525+
526+
#[test]
527+
fn all_lines_invalid_gives_empty_stdout_and_stderr_errors() {
528+
let dir = env!("CARGO_MANIFEST_DIR");
529+
let path = format!("{dir}/test_all_bad.txt");
530+
std::fs::write(&path, "bad\nalso_bad\nstill_bad\n").unwrap();
531+
532+
let output = cargo_bin()
533+
.arg(&path)
534+
.output()
535+
.expect("failed to run binary");
536+
537+
std::fs::remove_file(&path).ok();
538+
539+
let stdout = String::from_utf8_lossy(&output.stdout);
540+
let stderr = String::from_utf8_lossy(&output.stderr);
541+
542+
assert!(stdout.is_empty(), "expected no stdout, got: {stdout}");
543+
assert!(
544+
stderr.contains("line 1"),
545+
"expected line 1 error, got: {stderr}"
546+
);
547+
assert!(
548+
stderr.contains("line 2"),
549+
"expected line 2 error, got: {stderr}"
550+
);
551+
assert!(
552+
stderr.contains("line 3"),
553+
"expected line 3 error, got: {stderr}"
554+
);
555+
assert!(!output.status.success());
556+
assert_eq!(output.status.code(), Some(2));
557+
}
558+
395559
// ─── Helpers ────────────────────────────────────────────────────────
396560

397561
/// Parse a USD output line like "1 dollar,2 quarters,1 nickel,2 pennies" into total cents.

GildedRose/inventory/inventory_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,127 @@ func TestMultiDay_100DaysNoNegativeQuality(t *testing.T) {
283283
}
284284
}
285285

286+
func TestMultiDay_backstageAt50SellIn6_cappedThenDrops(t *testing.T) {
287+
items := []item.Item{
288+
{Name: "VIP Pass", Category: "Backstage Passes", SellIn: 6, Quality: 50},
289+
}
290+
inv := New(items)
291+
292+
// Day 1: SellIn=5, +2 → capped at 50
293+
inv.NextDay()
294+
if inv.FindByName("VIP Pass").Quality != 50 {
295+
t.Errorf("day 1: expected 50, got %d", inv.FindByName("VIP Pass").Quality)
296+
}
297+
298+
// Day 2: SellIn=4, +3 → capped at 50
299+
inv.NextDay()
300+
if inv.FindByName("VIP Pass").Quality != 50 {
301+
t.Errorf("day 2: expected 50, got %d", inv.FindByName("VIP Pass").Quality)
302+
}
303+
304+
// Days 3–6: keep advancing through SellIn 3,2,1,0
305+
for i := 3; i <= 6; i++ {
306+
inv.NextDay()
307+
if inv.FindByName("VIP Pass").Quality != 50 {
308+
t.Errorf("day %d: expected 50, got %d", i, inv.FindByName("VIP Pass").Quality)
309+
}
310+
}
311+
312+
// Day 7: SellIn=-1, concert over → quality drops to 0
313+
inv.NextDay()
314+
if inv.FindByName("VIP Pass").Quality != 0 {
315+
t.Errorf("day 7 (post-concert): expected 0, got %d", inv.FindByName("VIP Pass").Quality)
316+
}
317+
}
318+
319+
func TestMultiDay_itemStartingPastSellBy(t *testing.T) {
320+
items := []item.Item{
321+
{Name: "Old Sword", Category: "Weapon", SellIn: -5, Quality: 10},
322+
}
323+
inv := New(items)
324+
325+
// Already past sell-by: degrades by 2 per day
326+
inv.NextDay() // SellIn=-6, Quality=8
327+
if inv.FindByName("Old Sword").Quality != 8 {
328+
t.Errorf("day 1: expected 8, got %d", inv.FindByName("Old Sword").Quality)
329+
}
330+
331+
inv.NextDay() // SellIn=-7, Quality=6
332+
if inv.FindByName("Old Sword").Quality != 6 {
333+
t.Errorf("day 2: expected 6, got %d", inv.FindByName("Old Sword").Quality)
334+
}
335+
336+
inv.NextDay() // SellIn=-8, Quality=4
337+
inv.NextDay() // SellIn=-9, Quality=2
338+
inv.NextDay() // SellIn=-10, Quality=0
339+
340+
if inv.FindByName("Old Sword").Quality != 0 {
341+
t.Errorf("day 5: expected 0, got %d", inv.FindByName("Old Sword").Quality)
342+
}
343+
if inv.FindByName("Old Sword").SellIn != -10 {
344+
t.Errorf("day 5: expected sellIn -10, got %d", inv.FindByName("Old Sword").SellIn)
345+
}
346+
}
347+
348+
// ============================================================
349+
// Empty inventory
350+
// ============================================================
351+
352+
func TestNew_emptyInventory_itemsReturnsEmptySlice(t *testing.T) {
353+
inv := New([]item.Item{})
354+
items := inv.Items()
355+
if len(items) != 0 {
356+
t.Errorf("expected 0 items, got %d", len(items))
357+
}
358+
}
359+
360+
func TestNew_emptyInventory_trashReturnsEmpty(t *testing.T) {
361+
inv := New([]item.Item{})
362+
trash := inv.Trash()
363+
if trash != nil && len(trash) != 0 {
364+
t.Errorf("expected nil or empty trash, got %d items", len(trash))
365+
}
366+
}
367+
368+
// ============================================================
369+
// FindByName edge cases
370+
// ============================================================
371+
372+
func TestFindByName_emptyString_returnsNil(t *testing.T) {
373+
inv := New(sampleItems())
374+
itm := inv.FindByName("")
375+
if itm != nil {
376+
t.Error("expected nil for empty string search")
377+
}
378+
}
379+
380+
// ============================================================
381+
// Multiple trash items
382+
// ============================================================
383+
384+
func TestTrash_multipleTrashItems(t *testing.T) {
385+
items := []item.Item{
386+
{Name: "Good Sword", Category: "Weapon", SellIn: 10, Quality: 10},
387+
{Name: "Broken Shield", Category: "Armor", SellIn: 0, Quality: 0},
388+
{Name: "Rotten Cheese", Category: "Food", SellIn: -5, Quality: 0},
389+
{Name: "Cursed Ring", Category: "Misc", SellIn: -3, Quality: -2},
390+
}
391+
inv := New(items)
392+
trash := inv.Trash()
393+
if len(trash) != 3 {
394+
t.Fatalf("expected 3 trash items, got %d", len(trash))
395+
}
396+
names := map[string]bool{}
397+
for _, itm := range trash {
398+
names[itm.Name] = true
399+
}
400+
for _, expected := range []string{"Broken Shield", "Rotten Cheese", "Cursed Ring"} {
401+
if !names[expected] {
402+
t.Errorf("expected %q in trash", expected)
403+
}
404+
}
405+
}
406+
286407
func TestMultiDay_100DaysNoQualityAbove50_exceptSulfuras(t *testing.T) {
287408
inv := New(sampleItems())
288409
for i := 0; i < 100; i++ {

0 commit comments

Comments
 (0)