Unit test coverage: rings (game/rings.go has zero tests) #5
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
game/rings.go(182 lines, 6 functions) has no test coverage at all. Ofthe 32 tests in the suite, not one exercises ring wear/removal, hand
selection, or the ring food-drain contribution.
This is the first of three units splitting the
TODO.mdNext Step("broaden unit test coverage where playtesting finds thin spots — rings,
sticks, wizard commands") into commit-sized pieces.
Untested surface
ringOn()pickRingHand()ringOff()gethand()ringEat(hand)ringNum(obj)ringEatis the highest-value target: it feeds the hunger clock, so a wrongvalue is a slow, silent gameplay divergence from C that no current test would
catch.
Definition of done
game/rings_test.gocovering, at minimum:applied (e.g.
AddStrengthchanges strength,Dexteritychanges therelevant stat).
state change.
ringEatreturns the correct per-kind value for every ring kind that Cassigns a nonzero food cost, cross-checked against the C source on the
origin/c-masterbranch (rings.c), with the C values quoted in thetest or its comments.
ringNumoutput for a bonus and a non-bonus ring.comment as intentionally not exercised, with the reason.
make checkfully green.TODO.mdupdated in the same commit (this is one third of the currentNext Step — narrow the step rather than marking it complete).
(closes #N).Implementation requirements
the Go code currently returns. The C sources are on the
origin/c-masterbranch — read them with
git show origin/c-master:rings.c. Do NOT check outor modify that branch. A test that merely locks in current behavior is
worthless for a port; the whole point is catching divergence from C.
"fix" gameplay in a test-coverage commit, and do not write the test to
match the Go bug.
t.Parallel()(theparalleltestlinter is enforced andthe suite is at 0 issues — keep it there).
package gameneed the//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)file header, matching the existing test files.
maketargets only — never rungo testdirectly..golangci.yml.Implementation plan
Branch
test/rings-coverageoffmain@2f7a0d9, one commit, new filegame/rings_test.go(white-box,package game,//nolint:testpackageheader,
t.Parallel()everywhere). No game-code changes.C reference read via
git show origin/c-master:rings.c/origin/c-master:rogue.h/origin/c-master:things.c— no checkout ofthose branches.
C facts the tests will be written against
rogue.h275-289:R_PROTECT 0 … R_SUSTARM 13,MAXRINGS 14;LEFT 0,RIGHT 1(rogue.h122-123). Go'sRingKindiota order ingame/types.go303-316 matches index-for-index, souses[o_which]andringUses[RingKind]are the same lookup.rings.c ring_eatuses[], verbatim:1R_PROTECT,1R_ADDSTR,1R_SUSTSTR,-3R_SEARCH,-5R_SEEINVIS,0R_NOP,0R_AGGR,-3R_ADDHIT,-3R_ADDDAM,2R_REGEN,-2R_DIGEST,0R_TELEPORT,
1R_STEALTH,1R_SUSTARM.eat = (rnd(-eat) == 0)for negatives — a 1-in-n chance of 1, not aliteral cost — then
if (o_which == R_DIGEST) eat = -eat, so slowdigestion yields 0 or -1.
ring_numuses theotherwisemacro, whichrogue.h53 defines asbreak;default— so the four labels R_PROTECT / R_ADDSTR / R_ADDDAM /R_ADDHIT fall through to one
sprintfand every other kind returns""from the default arm. R_ADDHIT is the dexterity ring, i.e. Go'sRingDexterity; R_ADDDAM isRingIncreaseDamage.Tests
ringOnwith both hands free: promptsgethand, populates the chosenhand, applies the effect (add-strength changes
Stats.Str;see-invisible sets
CanSeeInvisible; aggravate wakes a sleepingmonster). One hand already full: the free hand is taken with no
prompt.
ringOffreverses it (dropRing:chg_str(-o_arm),unsee+extinguish) and clears the hand; both-hands case prompts; no-ringscase gives C's two wordings; a cursed ring refuses with
you can't. It appears to be cursedand stays on.you already have a ring on each hand/wearing two, with a full before/after state comparison assertingnothing moved.
it would be difficult to wrap that around a finger/not a ring; already-worn ring ->is_currentrejection.gethand:l/L/r/R/ESC, plus reprompt after a bad key.ringEatover all 14 kinds. Positive entries: exact value andzero RNG consumption (C's positive path never calls
rnd). Negativeentries: the game's
Rngis snapshotted,ringEatis called, and thesnapshot is replayed through
rnd(n)transcribed from C — pinningboth the denominator and that exactly one
rndcall happens — plus afrequency check over thousands of trials so a wrong denominator fails
loudly. Empty hand -> 0.
ringNum:[+2]/[-1]for each of the four C labels,""for anunknown ring,
""for a known kind outside the labels (thebreak;defaultarm).Every one of the 14 kinds is exercised by the
ringEattable; kinds withno
ring_on/dropcheckeffect get a comment saying so rather than amade-up assertion.
Verification
make check(fmt-check, lint, test with-race -cover), re-run wholeafter any fix since it short-circuits. Lint runs with a private empty
GOLANGCI_LINT_CACHE, retried on the parallel-run lock error, andaccepted only if it names no path outside the worktree. Each substantive
assertion is mutation-proved: the behaviour is broken in the game code,
the run must fail on that test and only that test, then reverted. Results
reported on the PR.
TODO.mdgets a Completed Steps entry in the same commit; the Next Stepis narrowed to sticks + wizard commands (#6, #7) rather than rotated.
If any expected value turns out to diverge from C, I stop and report
instead of writing the test to match the Go code.
Implemented in PR #34 (branch
test/rings-coverage, commitc61e282):#34
New
game/rings_test.go— 17 tests, 44 subtests, no game-code changes —covering
ringOn,pickRingHand,ringOff,gethand,ringEatandringNum, plusdropRing(thethings.c dropcheckarm that actually takesa worn ring off). Package coverage 53.7% -> 56.2%.
Every item in the definition of done is met: correct hand populated and
effect applied; removal reverses the effect and clears the hand; wearing
with both hands occupied rejected with a full before/after state check; a
non-ring rejected;
ringEatpinned for all fourteen kinds against C'suses[](not just the nonzero ones), both hands;ringNumfor bonus andnon-bonus rings, known and unknown.
make checkis fully green withgolangci-lint at 0 issues.
TODO.mdhas its Completed Steps entry in thesame commit, and
Next Stepis narrowed to sticks (#6) and wizard commands(#7) rather than rotated.
No divergence from C was found, so nothing was filed and no gameplay
was touched. The C was read only with
git show origin/c-master:...and isquoted in the test file and in the PR comment, including both flagged
quirks: a negative
uses[]entry is a one-in-n chance of 1 rather than aliteral cost, and
R_DIGEST's sign flip makes slow digestion return 0 or-1;
ring_num'sotherwisemacro isbreak;default(rogue.h53), so itsfour labels fall through to one
sprintfand everything else returns""from a default arm.
Verified with 23 mutations, each applied to the game code alone and
reverted, each failing its own test and only its own — including stripping
all three
ring_oneffect arms (3 of 3 failed) and altering threeringUsesentries (exactly those three subtests failed). The full table isin the PR comment. One mutation exposed a defect in the tests themselves: an
unaccepted hand key made
gethandloop on the headless terminal's fillerinput, so the test died of the 30s timeout instead of failing on its
assertion. Scripted hand answers now carry an abort tail, and that mutation
fails cleanly.
Every ring kind is exercised by the
ringEattable; the eleven that areinert at wear time in C are documented at the foot of the file as
deliberately not given a wear/remove test, with the files their powers
actually live in.