Twinned from https://github.com/cfsh/ninefold/issues/494 by tools/beads/import_github.py's reconcile pass.
Tests move next to the code they test, with no tests/ directory. PR [#485](https://github.com/cfsh/ninefold/pull/485) is open and implements the convention plus its gate; it covers most of the tree but deliberately not all of it. This issue is the parent: land #485, then finish the two directories it could not touch, then delete the transition scaffolding.
Aedan's ask, verbatim from chat:
> "i am not sure the style guide for where we're putting tests atm, e.g. tools/{nflib,tests}/{foo.py,foo_test.py}. I propose we always put tests next to the relevant file, no tests directory (test_data directories allowed). Do u concur? Also should we codify this in a ninefold/STYLE_GUIDE.md?"
and, on the mechanism:
> "global globs are risky for these reasons, suppose we end up with SitMathTest as a real game class."
Codified in STYLE_GUIDE.md §1 (added by #485). In short:
| language | marker | example |
|---|---|---|
| C# | .Tests.cs — a dot segment | PieceSet.cs → PieceSet.Tests.cs |
| Python | _test.py — an underscore suffix | proc.py → proc_test.py |
The dot is load-bearing on the C# side and is the direct answer to the second quote: Tests is an ordinary way to end a real type name, so a **/*Tests.cs glob would silently drop a game class from the build. A dot is not legal in a C# type name. Python cannot use a dot (not a legal module name), so the asymmetry is forced by the languages rather than chosen.
Enforced by ./presubmit.py --suite layout, which asserts every tracked .cs reaches the right project. Not "exactly one project" — pure-logic sources are cross-linked into both on purpose.
games/explore/tests/ → co-locatedBlocked only by #434, which is in flight against that directory. Mechanical once it lands: move the files, rename to *.Tests.cs, drop the namespace's .Tests segment, swap the csproj's ..\games\explore\tests\*.cs row for ..\games\explore\*.Tests.cs.
tools/tests/ → co-located — needs a guard built firstBlocked by #466, #470, #472, all writing into tools/tests/. Best done when the design-001 chunk drains.
⚠️ Do not start this move before adding the discovery guard below. unittest discover only recurses into directories that are packages, and a directory without __init__.py is skipped silently — measured on this box reporting OK / Ran 1 test while never loading a second test file that was sitting right there. That is silent test loss *during the migration that moves the tests*, which is the worst possible time for it.
Required before the move:
python lane: count *_test.py files on disk, count test modules actually loaded, fail on a mismatch. Same spirit as tracked_shell_scripts() refusing to render "could not enumerate" as "found nothing".__init__.py in every directory that will hold a _test.py — tools/pr/, and tools/ itself if discovery starts at the repo root.presubmit.py's suite_python currently runs -s tools/tests. Co-location needs that widened, and presubmit_test.py would live at the repo root next to presubmit.py.Renames, for whoever takes it:
| today | co-located |
|---|---|
| tools/tests/test_nflib_proc.py | tools/nflib/proc_test.py |
| tools/tests/test_nflib_fs.py | tools/nflib/fs_test.py |
| tools/tests/test_nflib_gh.py | tools/nflib/gh_test.py |
| tools/tests/test_nflib_git.py | tools/nflib/git_test.py |
| tools/tests/test_nflib_core.py | tools/nflib/core_test.py |
| tools/tests/test_nflib_env.py | tools/nflib/env_test.py |
| tools/tests/test_poll.py | tools/pr/poll_test.py |
| tools/tests/test_presubmit.py | presubmit_test.py (repo root) |
Plus whatever #466/#470/#472 add (test_dispatch.py, test_merge_pr.py, test_probe_gate.py, test_nflib_verdict.py). tools/tests/fixtures/ becomes test_data/ per the convention.
⚠️ Two test files cover nflib/__init__.py (core_test.py, env_test.py). That is fine — the rule is a suffix, not a bijection — but do not "fix" it by merging them.
While both layouts are live, "is this a test file?" accepts *.Tests.cs or /tests/ in two places that must stay in sync:
presubmit.py — is_test_source()shared/UserPathGuard.Tests.cs — IsTestSource()Drop the /tests/ clause from both when the last tests/ directory goes. They are already the single spelling within each file (the pre-#485 code had the predicate inline at two call sites in opposite directions, which is how a guard silently inverts) — but they are two files, so they need doing together.
python lane, with a test that a package-less directory fails rather than passing quietlygames/explore/tests/ gone (after #434)tools/tests/ gone (after #466/#470/#472)/tests/ clause dropped from both is_test_source predicatestests/ directory anywhere except tests/ itself, which holds only Ninefold.Tests.csproj and is a *project* dir, not test sources./presubmit.py --all greengit log --follow still works across #485 — git recorded all 31 moves as renames. Keep that true..cs.uid per co-located test (.gdignore is directory-granular). *.Tests.cs.uid is gitignored; production .cs.uid files stay tracked.... #485 fixed two [CallerFilePath] helpers whose hardcoded depth broke seven tests when the files moved; they walk up to Ninefold.sln now, and the Python side should not reintroduce the pattern.SynthWaveformDump went to shared/ (it only imports Ninefold.Shared) although its comment says it mirrors Tonapse's WaveformView.None.
No comments.