Skip to content

test: pin drawImage failure modes, button-1 gating, and per-instance caches - #42

Merged
dmccoystephenson merged 2 commits into
mainfrom
test/pin-error-and-cache-isolation-behaviors
Sep 2, 2026
Merged

test: pin drawImage failure modes, button-1 gating, and per-instance caches#42
dmccoystephenson merged 2 commits into
mainfrom
test/pin-error-and-cache-isolation-behaviors

Conversation

@dmccoystephenson

Copy link
Copy Markdown
Member

Summary

Four behaviors that the Graphik docstrings promise, or that the cache design
depends on, were not exercised by any test. They are pinned here as
characterization tests. No production code is changed — the source diff is
empty and only test_graphik.py is touched.

  • drawImage distinguishes its two failure modes. The docstring's Raises:
    section separates FileNotFoundError (no file at the path) from
    pygame.error (a file that exists but cannot be decoded). Only the first was
    reachable by a test, and that test accepts either type, so nothing pinned the
    second. A text file named .bmp is now asserted to raise pygame.error
    specifically.
  • A failed load is not cached. The docstring states that a path which fails
    to load "caches nothing and raises on every call". The existing test only
    observed the first raise. The load is now asserted to be re-attempted on each
    of three consecutive failures, and an asset that appears on disk afterwards is
    asserted to draw on the very next call — which is the user-visible consequence
    of not negative-caching.
  • drawButton gates on button 1 specifically. The parametrized hit-test case
    list covered "pressed" and "not pressed" but no press of a different button.
    Three cases are added: middle-only and right-only must not fire the callback,
    while button 1 held together with the others must.
  • Both caches belong to the instance, not the class. _fonts and _images
    are built in __init__, so two Graphik objects cannot serve each other
    stale entries — and for the font cache that isolation is what keeps _getFont's
    display-session check meaningful. The isolation is pinned on observable work
    (one font construction and one image load per instance) rather than on the
    private dictionaries.

Evidence each test bites

Every addition was confirmed empirically by temporarily mutating the source,
running the suite, and restoring it:

Mutation applied to graphik.py Test(s) that failed
Decode failure swallowed, substituting a blank surface test_draw_image_undecodable_file_raises_pygame_error
Failed load cached as None and re-raised from the cache test_draw_image_does_not_cache_a_failed_load
if click[0] == 1 widened to if any(click) ..._invokes_callback_only_on_inside_click[middle_button_only], [right_button_only]
_images moved to a class attribute test_caches_belong_to_the_instance_not_the_class (image half)
_fonts and _fontDisplay moved to class attributes test_caches_belong_to_the_instance_not_the_class (font half)

Both halves of the isolation assertion were mutated separately, so neither is
carried by the other. The working tree was verified clean after each revert.

Deferred issues

The three open issues were all skipped this cycle. Each reason is recorded here
rather than as a comment on the issue itself:

With the backlog entirely maintainer-gated, this cycle was spent on test
expansion instead.

Test plan

  • python3 -m py_compile src/main/python/preponderous/graphik/graphik.py — succeeds
  • python3 -m pytest — 47 passed (41 before this change)
  • Headless: no new test requires a real display; the existing conftest.py
    dummy SDL drivers cover them
  • Each new assertion confirmed to fail against a mutated source and pass
    against the restored one (table above)
  • No production code touched, so no consumer-facing API surface changes

No tracking issue — this is a test-expansion cycle, not an issue fix.

This PR description was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).


drafted by Claude on behalf of Daniel Stephenson

dmccoystephenson and others added 2 commits September 2, 2026 01:04
…caches

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dmccoystephenson

Copy link
Copy Markdown
Member Author

Self-review rubric

Scored against the diff and against command output, not judgement. The external
anchor is green: the Test workflow passed on the head commit across Python
3.9–3.13, with 47 passed (collected 47 items) reported by each job — up from
41 on main.

  • Scope: PASSgit diff --name-only origin/main...HEAD lists exactly one
    file, src/test/python/preponderous/graphik/test_graphik.py. git diff origin/main...HEAD -- src/main is empty, so no production code is touched. 88
    insertions, 0 deletions.
  • Tests-fix (empirical): PASS — every new assertion was confirmed to bite by
    temporarily mutating graphik.py, running the suite, and reverting. Five
    mutations, each failing exactly the intended test and nothing unrelated:
    decode failure swallowed → ..._undecodable_file_raises_pygame_error; failed
    load negative-cached → ..._does_not_cache_a_failed_load; click[0] == 1
    widened to any(click) → the middle_button_only and right_button_only
    cases; _images moved to the class, and separately _fonts moved to the
    class → both halves of test_caches_belong_to_the_instance_not_the_class. The
    working tree was verified clean after each revert.
  • Sibling structure: PASS — the new functions are module-level test_*
    functions carrying a leading comment that states why the behavior is worth
    pinning, matching every neighbour. The existing _make_graphik,
    _write_solid_image, _count_image_loads, _count_font_constructions and
    _rgb helpers are reused rather than duplicated, and the three new mouse
    cases were folded into the existing parametrize list instead of being added as
    a parallel test.
  • Docs: PASS — no documentation row needs updating, because no behavior
    changed. The new tests assert what README.md and the drawImage /
    drawButton docstrings already state; pyproject.toml, RELEASING.md and
    _version.py are untouched and unaffected.
  • Backward-compat: PASS — the production diff is empty, so no public member
    of Graphik is renamed, removed or re-signed, and no vendored consumer copy
    (Roam, Apex, Ophidian, Patchwork, Tic-Tak-Toe) is affected.
  • Headless: PASS — no new test requires a real display or audio device; the
    existing conftest.py dummy SDL drivers cover them, and CI executed all 47
    headless on five interpreters.
  • No new deps: PASS — no import was added. The new tests use pygame,
    pytest and the tmp_path fixture, all already in use in this module.
  • manual validation: PASS — see the anchor note above.
  • Tests-new: no signal — no public method or function was added to the
    library this cycle, so the rule has nothing to apply to.
  • Sibling renames: no signal — nothing was renamed.
  • Issue resolution: no signal — no Closes #N is claimed. All three open
    issues are maintainer-gated, with the reason for each deferral recorded in the
    PR body.
  • camelCase: no signal — no public method was added or renamed. Local
    variable naming in the new tests (notAnImage, imagePath) follows the
    camelCase precedent already set by redPath / bluePath / rawImage in this
    file, which mixes both styles.
  • Version sync: no signal — no version string changed.

Findings

1. The cache-isolation test was initially blind to partial sharing (fixed in
6a9df89).
As first written, the test asserted one font construction and one
image load per instance. That catches the fully-shared variant, but a partial
one — the dictionaries moved to the class while _fontDisplay stays
per-instance — passed, because the second instance clears the shared cache and
immediately refills the same key, leaving the count unchanged at two. Since that
partial form is the more likely accidental regression (and a real one: it
thrashes the first instance's cache), the test was restructured to warm two font
sizes on the first instance before the second instance draws, then ask the first
instance for the size the second never touched. That sequence was re-verified to
fail under all three variants: _fonts + _fontDisplay on the class, _fonts
alone on the class, and _images on the class.

2. The missing-path exception type is still asserted loosely (left as is).
test_draw_image_does_not_cache_a_failed_load accepts
(FileNotFoundError, pygame.error), mirroring the existing
test_draw_image_missing_file_raises rather than pinning FileNotFoundError
exactly as the docstring promises. Tightening it was considered and rejected for
two reasons: the exception pygame raises for a nonexistent path is not
guaranteed stable across the 3.9–3.13 matrix's pygame builds, and the change
would edit an assertion this PR did not otherwise touch. The undecodable-file
branch, which is newly covered here, is pinned to pygame.error precisely.

3. Mouse state is asserted as 3-tuples. pygame.mouse.get_pressed() can
report five buttons, but the new parametrize cases use 3-tuples to match the
existing cases in the same list, and drawButton only ever indexes element 0.
Noted rather than changed, since diverging from the neighbouring cases would
cost more clarity than it buys.

This review was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).


drafted by Claude on behalf of Daniel Stephenson

@dmccoystephenson
dmccoystephenson merged commit d32788f into main Sep 2, 2026
5 checks passed
@dmccoystephenson
dmccoystephenson deleted the test/pin-error-and-cache-isolation-behaviors branch September 2, 2026 07:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant