Skip to content

Guard main() behind __name__ and add a unittest suite - #17

Merged
dmccoystephenson merged 2 commits into
mainfrom
feature/main-guard-and-tests
Aug 27, 2026
Merged

Guard main() behind __name__ and add a unittest suite#17
dmccoystephenson merged 2 commits into
mainfrom
feature/main-guard-and-tests

Conversation

@dmccoystephenson

Copy link
Copy Markdown
Member

Summary

  • main() in src/collide.py is now guarded by if __name__ == "__main__":, so importing the entry point no longer starts a 15-prompt interactive session. Running python3 src/collide.py is unaffected.
  • A stdlib unittest suite has been added under tests/: tests/test_ideaCollisionGenerator.py (12 characterization cases across getKeywords(), createPairs(), promptForIdeas(), and writeToFile()) and tests/test_collide.py (2 cases covering the entry point).
  • The tests are characterization tests: current behavior is asserted, not changed. No production behavior other than the guard has been altered. Where a known defect is exercised — the unguarded keywords[i+1] index — the current IndexError is asserted rather than fixed, and the fix is left to the cycle that takes createPairs() raises IndexError when the keyword count is odd #9.
  • builtins.input is patched in every case that would otherwise read input, and random.shuffle is patched wherever pairing order would otherwise be nondeterministic. Anything reaching writeToFile() runs inside a tempfile.TemporaryDirectory, so nothing is written into the repository.
  • A ## Tests section has been added to README.md recording the discovery command that works on the declared 3.8 floor.

python3 -m unittest discover -s tests is used rather than -s tests -t .; on Python 3.8 the latter fails with ImportError: Start directory is not importable, and the alternative — adding a tests/__init__.py purely to satisfy discovery — was judged the larger change.

No third-party dependency, packaging layout, or top-level directory other than tests/ has been introduced. No existing camelCase member has been renamed, and no prompt string or output-line format has been touched.

Test plan

All commands were run from the repository root on Python 3.8.10.

  • Both modules parse:
$ python3 -m py_compile src/collide.py src/ideaCollisionGenerator.py && echo "compile OK"
compile OK
  • The suite passes, with a real executed-test count:
$ python3 -m unittest discover -s tests
...............
----------------------------------------------------------------------
Ran 15 tests in 0.005s

OK
  • The entry point still runs end to end with all 15 lines redirected from a fixture:
$ python3 src/collide.py < fixture.txt
Enter 1st keyword: Enter 2nd keyword: Enter 3rd keyword: Enter 4th keyword: Enter 5th keyword: Enter 6th keyword: Enter 7th keyword: Enter 8th keyword: Enter 9th keyword: Enter 10th keyword: Enter an idea based off of these keywords: ['alpha', 'charlie']
Enter an idea: Enter an idea based off of these keywords: ['golf', 'echo']
Enter an idea: Enter an idea based off of these keywords: ['india', 'foxtrot']
Enter an idea: Enter an idea based off of these keywords: ['bravo', 'hotel']
Enter an idea: Enter an idea based off of these keywords: ['juliet', 'delta']
Enter an idea:
  • The output contract is unchanged — one line per pair, in the committed sample's format:
$ wc -l ideas/ideas-*.txt
5 ideas/ideas-2026-08-24_01.02.40.txt

$ cat ideas/ideas-2026-08-24_01.01.01.txt
['alpha', 'india']: idea one
['juliet', 'golf']: idea two
['foxtrot', 'hotel']: idea three
['echo', 'charlie']: idea four
['bravo', 'delta']: idea five
  • Regression evidence for the guard (stash-and-run). With src/collide.py stashed, both entry-point tests fail; with it restored, all 15 pass:
$ git stash push -- src/collide.py
$ python3 -m unittest discover -s tests
FF.............
======================================================================
FAIL: testImportingDoesNotStartASession (test_collide.TestEntryPoint)
AssertionError: Lists differ: ['Enter 1st keyword: ', 'Enter 2nd keyword[279 chars]a: '] != []
First list contains 15 additional elements.
======================================================================
FAIL: testMainRunsTheFourSteps (test_collide.TestEntryPoint)
AssertionError: 10 != 5
----------------------------------------------------------------------
Ran 15 tests in 0.006s

FAILED (failures=2)

$ git stash pop
$ python3 -m unittest discover -s tests
----------------------------------------------------------------------
Ran 15 tests in 0.006s

OK

The second failure is worth noting on its own: with the guard removed, the import ran one session and main() ran a second, and both appended into a single file, because writeToFile() opens with "a" and both runs landed in the same timestamped second. That is the behavior already filed as #11 and is not changed here.

  • Nothing stray was committed — git status --porcelain was clean of __pycache__, generated ideas/ideas-*.txt, and scratch fixtures before the commit; the fixture and generated files were removed with os.remove.

Closes #8

Issues deferred this cycle

Every other open issue was left untouched, with the reason recorded here rather than as comments on each:

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 August 24, 2026 01:03
Importing src/collide.py started a full 15-prompt session, which hung any
test that imported the entry point. The bare main() call is now guarded, and
characterization tests cover IdeaCollisionGenerator's four methods plus the
entry point itself.

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

Scored against the diff and against commands actually run on Python 3.8.10 from the repository root. This review was performed by the same session that wrote the change; it is not an independent review.

Universal rubric

  • Scope: PASS — four files. src/collide.py is src/collide.py runs main() at import time (no __main__ guard) #8 itself; tests/test_collide.py and tests/test_ideaCollisionGenerator.py are the suite; README.md gains only the ## Tests section documenting the suite added in the same PR. No reformatting, no rename, no comment churn elsewhere. The trailing-newline omission at the end of src/collide.py was deliberately left as-is so the diff stays two lines.
  • Tests-new: PASS — all four IdeaCollisionGenerator methods and both entry-point behaviors are exercised; 16 tests are collected and pass (Ran 16 tests ... OK).
  • Tests-fix: PASS — the stash-and-run experiment was performed, not reasoned about. With src/collide.py stashed: FAILED (failures=2), testImportingDoesNotStartASession reporting First list contains 15 additional elements and testMainRunsTheFourSteps reporting AssertionError: 10 != 5. After git stash pop: Ran 15 tests ... OK. The revert produces a failure rather than a hang precisely because builtins.input is patched in both cases.
  • Sibling structure: PASStests/ had no siblings to match, so the ecosystem convention was followed: tests/test_<module>.py mirroring src/<module>.py, stdlib unittest, no conftest, no framework.
  • Sibling renames: PASS — no identifier was renamed.
  • Docs: PASS — every row of the sources-of-truth table was checked. README.md names Collide, its run command works from the repository root, its 3.8 floor matches the interpreter used, and its samples came from real runs. ideas/example.txt still matches the emitted ['a', 'b']: idea format. .vscode/launch.json still points at src/collide.py, which the guard leaves runnable as __main__. .gitignore covers __pycache__/ and generated ideas/*.txt and does not swallow the new .py files. LICENSE, COPYRIGHT.md, and the GitHub description are untouched.
  • Issue resolution: PASSsrc/collide.py runs main() at import time (no __main__ guard) #8's named surface, the bare main() call at module scope, is the line changed. No test suite, no CI, and no declared Python version #13 is referenced but deliberately not closed: its CI step remains.
  • Manual validation: PASS — the anchor ran in full this cycle. py_compile clean, Ran 16 tests ... OK, and a complete redirected-stdin run producing five prompts and a five-line output file. Nothing was marked UNVERIFIED. Worth recording for the loop's own benefit: the skill's snapshot states this checklist had never been executed, because the generating dispatch's classifier denied every form of running the program. It was executable here, and the program behaved exactly as the snapshot describes.

Repo-specific rubric

  • Ran, not read: PASS — every prompt string, pairing line, and file listing quoted in the PR body came from output produced this cycle.
  • Stdin always redirected: PASS — the only invocation of src/collide.py anywhere in the diff or the PR body is python3 src/collide.py < fixture.txt with all 15 lines. No test reaches an unpatched input(); the two that import or call the entry point patch builtins.input first, which is what keeps a regression observable instead of hanging.
  • camelCase preserved: PASS — no member of either source module was renamed. New test methods follow the same style (testCollectsTenKeywords, originalDirectory, temporaryDirectory).
  • Stdlib only: PASS — added imports are contextlib, io, os, sys, tempfile, unittest, unittest.mock. No manifest file was added.
  • Python 3.8-safe: PASS — no match, no X | Y union, no dict |. Confirmed by the suite running green on 3.8.10.
  • Output contract preserved: PASSwriteToFile() was not modified; the real run wrote five lines in ['a', 'b']: idea form.
  • Prompt strings intact: PASS — unchanged, and now pinned by testPromptsUseOrdinalLadder, testPromptsUseInputPrompt, and testEachPairIsPrintedBeforeItsPrompt.
  • Entry point still runnable: PASSpython3 src/collide.py < fixture.txt was run on this branch after the guard landed and completed all five pairs.
  • License claims consistent: PASS — no file stating a license, owner, or repository URL is touched by this diff.
  • No artifacts committed: PASSgit status --porcelain was checked before committing; staging was by name. The stdin fixture and the generated ideas/ideas-*.txt files were removed with os.remove.
  • No inflation: PASS — no packaging layout, no CLI framework, no third-party test framework, and tests/ is the only new top-level directory. Non-test net change is roughly 13 lines; the ~215 lines of test code are the stated scope-ceiling exception.

Findings folded in from the review

  • tests/test_ideaCollisionGenerator.py:104 — the printed line in promptForIdeas() was uncovered on the first pass. Since that string is part of the advertised interface and the PR body claims it is unchanged, the claim was unbacked. testEachPairIsPrintedBeforeItsPrompt was added in a follow-up commit, taking the suite from 15 to 16.
  • tests/test_collide.py:56testMainRunsTheFourSteps asserts a five-line output file, and the stashed revert made it fail with 10 != 5 rather than by finding two files. The doubled line count is a second, incidental confirmation of writeToFile(): append mode, CWD-relative output path, and unspecified encoding #11: two sessions inside the same timestamped second appended into one file because writeToFile() opens with "a". No production change was made for it here.
  • tests/test_collide.py:10 and tests/test_ideaCollisionGenerator.py:11 — both modules insert src/ into sys.path at import time, so the entry is duplicated when both load. This is harmless and was left alone; the alternative, a shared helper or a tests/__init__.py, adds structure this repository does not otherwise have.
  • README.md:66 — the documented command is python3 -m unittest discover -s tests, not the -s tests -t . form. On 3.8 the latter fails with ImportError: Start directory is not importable: '<repo>/tests', since discovery there will not treat a non-package start directory as importable relative to a different top level. Adding tests/__init__.py solely to satisfy that flag was judged the larger change.

Per the one-intrinsic-critique-pass cap, this rubric will not be re-run absent an external signal.

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


drafted by Claude on behalf of Daniel Stephenson

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.

src/collide.py runs main() at import time (no __main__ guard)

1 participant