Make main.py importable and cap its render loop - #23
Conversation
Guard main.py's entry point with `if __name__ == "__main__":` and move the argument parsing and Viron service construction out of module scope, so the module can be imported without running the program. `main()` now takes the grid size, the --exit-after-create flag and the two services as arguments, which lets tests drive it with mocks. Call `RenderWindow.tick()` at the end of the render loop so it no longer spins as fast as the machine can redraw, and flush the "Loading existing environment" message with `pygame.display.update()` so it is actually presented before the blocking fetch, matching the creation path. Add tests/test_main.py covering argument parsing, the environments.json cache key and file, the --exit-after-create early return, the loading-message flush and the render loop's frame cap. Closes #18 Closes #19 Closes #21 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tests section claimed only that Pygame is mocked. The suite also stubs Viron's service modules, so it needs neither a populated submodule nor the Python version those modules require. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Self-review rubricScored against the diff and against command output, not against judgement alone.
Observations folded in from the diffOffered as notes, not as blockers, and no change is being made for them in this PR.
This review was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener). It is a self-review by the author of the change, not an independent one. drafted by Claude on behalf of Daniel Stephenson |
Merge gate: held, and whyThe pre-merge checks were run and the outcome is a deliberate hold rather than a merge, even though this session was pre-authorized to merge. What passed.
What blocks the merge. This project's external validation anchor is a manual run against a live Viron server on That condition would ordinarily be recorded as not-applicable and waved through for a docs-only change. It is not being waved through here, because this PR does modify exactly what the anchor exists to check: the render loop and the Viron service call sites. The substitutes assembled instead are strong — a mocked suite covering every changed path, plus a headless run under What is actually at residual risk. Narrower than the hold might suggest, and worth stating plainly so the decision is an easy one. The three service calls — Requested action. A confirming run of This comment was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener). drafted by Claude on behalf of Daniel Stephenson |
Summary
Three defects in
main.pyare addressed together, because the first is the one that makes the other two testable.main.pyno longer runs at import time. The entry point is guarded withif __name__ == "__main__":, and the argument parsing and Viron service construction are moved out of module scope intoparseArgs(argv)and intomain()'s parameters.main(gridSize, exitAfterCreate, locationService, environmentService)defaults the two services toNoneand builds them againsthttp://localhost:9999when they are not supplied, so the command line behaviour is unchanged while a test can pass mocks instead. The cache-key expression is extracted asgetEnvironmentKey(gridSize).window.tick(targetFramesPerSecond)is called at the end of the loop body.RenderWindow.tick()had been exposed since PR Add RenderWindow class and use it for main.py's window and render loop #10 but was called from nowhere in the repository, so the loop re-drew and re-randomised every location as fast as the machine allowed.pygame.display.update()is added after the"Loading existing environment, please wait..."text, so the message is presented before the blockingget_environment_by_idcall rather than being erased by the render loop's firstfill(). As noted in The "Loading existing environment" message is never displayed, because the cached path omits pygame.display.update() #21, the creation path's hardcoded400, 400text position is aligned todisplayWidth/2, displayHeight/2to match; the two are identical at the current display size.Incidental to the restructure, the mis-indented
try:body on the cached path (a 1-space indent) is normalised and three trailing-whitespace-only lines inside the rewritten function are cleaned.tests/test_main.pyis added — 23 tests covering argument parsing, the cache key, theenvironments.jsoncontents, the--exit-after-createearly return, the loading-message flush ordering, the error path, and the render loop's frame cap and location caching. The Viron submodule's service modules are replaced withsys.modulesstubs, so no server is contacted and the suite also runs on Python versions below 3.9, where Viron's PEP 585 annotations are unimportable.Test plan
python3 -m py_compile main.py graphik.py render_window.py tests/test_main.py— cleanpython3 -m unittest discover -s tests— 32 tests executed, OK (9 pre-existingRenderWindowtests, 23 new)main.pyreverted,tests/test_main.pyfails to import at all (pygame.error: No available video device, raised from the module-levelmain()call) — which is the main.py runs at import time, so its render loop cannot be unit tested #18 defect itself. Reverting only thewindow.tick(...)line failstest_every_frame_is_capped_at_the_target_frame_ratewith0 != 3; reverting only the addedpygame.display.update()failstest_loading_message_is_flushed_before_the_blocking_fetchon call ordering. Both pass once restored.RenderWindow, realGraphikand real pygame underSDL_VIDEODRIVER=dummy, with the Viron services stubbed:--exit-after-createwrote the expectedenvironments.jsonentry and exited, and the render loop was measured at 63 display updates over 1.08 s (~58 fps against the 60 cap).Manual validation status
The project's live anchor — a Viron server on
:9999— is UNVERIFIED and could not be run in this environment: Docker is unavailable in this WSL distro, and the interpreter present is Python 3.8, on which Viron's own service modules raiseTypeError: 'type' object is not subscriptableat import. The headless stub run described above and the new mocked unit tests are what stands in for it. Every code path changed here is covered by one or both, so this is recorded as UNVERIFIED-not-applicable rather than as a blocking gap, but confirmation against a live Viron by a reviewer with Docker would still be worth having before this is relied upon.Issues deferred this cycle
Recorded here rather than as comments on each issue:
Graphikdefines__init__twice) and Graphik.drawButton re-invokes its callback on every frame the mouse is held over it #22 (drawButtonre-invokes its callback per frame) — both sit ingraphik.py, outside this PR'smain.pysubsystem. Graphik.drawButton re-invokes its callback on every frame the mouse is held over it #22 additionally carries an unresolved design decision in its own body (instance state versus an event-driven signature change) that warrants a maintainer's call before implementation..github/workflows/, which is on this loop's do-not-auto-merge list and is better reviewed on its own.Closes #18
Closes #19
Closes #21
This PR description was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).
drafted by Claude on behalf of Daniel Stephenson