diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index fb2e81a3..9d67bd77 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -8,8 +8,11 @@ name: Launch benchmark # never fail a build on a slow point. One quiet host's five identical # cold-recreate runs spread 15.2-20.0s; a shared runner is worse, so a hard # wall-clock gate would be either too loose to fire or flaky. Deterministic -# structural guards (test_devpod_spawn_counts.py and its kin) keep the gating -# role, and they live in ci.yml where a gate belongs. +# structural guards keep the gating role: the devpod spawn counts a launch is +# allowed, asserted argv by argv in `flows::launch`'s tests, and their kin +# across the cargo suite. Those run in ci.yml, where a gate belongs. (The +# spawn-count guard was `test_devpod_spawn_counts` in Python until the Python +# tree retired in #267, and the counts came across with it.) # # It is a separate file for exactly that reason: house convention is that a new # job in ci.yml joins `gate`'s `needs` in the same pull request, and `needs` diff --git a/AGENTS.md b/AGENTS.md index 2881f9d5..a015ccfc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -222,6 +222,18 @@ checking rather than trusting, because a private package fails at nothing. See matching on headings. Moving one of those sections between files means moving the path in its guard, in the same change. +- **A citation spelled as a path has to point somewhere.** Comments all over + `rust/` name the test that pins the behaviour they describe, which is what + makes the record worth reading. `test/test_citations_resolve.py` holds every + one of those names to being a file: a path is checked as written, a bare + filename against the test tree. Retiring the Python implementation (#267) + broke about sixty of them at once, which is why the rule exists. When a suite + is gone, name it without the extension and say it retired, the way the + comments now do: `test_purge_ownership` (Python, retired in #267). The + history stays greppable and stops impersonating a live guard. `CHANGELOG.md` + and `docs/rust-port-scope.md` are out of scope as records of what was true + when they were written. + - **No em or en dashes in the README or the `docs/` pages it links.** They read as machine-written. Use a full stop or a comma, or end the sentence, and write a numeric range as "18s to 28s". `test/test_docs_prose.py` asserts it and diff --git a/CHANGELOG.md b/CHANGELOG.md index 963a674a..734966f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 died before any of this existed, and no route reaches them that is not the pattern above. `docs/cleanup.md` carries the whole of it. +### Fixed + +- **Sixty-six citations that pointed at nothing now point at something, and a + guard keeps it that way.** Comments across `rust/` name the test that pins the + behaviour they describe, which is most of what makes them worth reading. + Retiring the Python implementation (#267) deleted about forty of the files + those names referred to, in one commit, and the names stayed. A pointer into + nothing is worse than no pointer: it still reads as evidence, so the reader + goes looking and cannot tell whether the guard moved, was renamed, or never + existed. + + Every one of them now names a suite that retired rather than a file to open, + and says so, in the form the surviving comments had already settled on. Two + were not merely stale but false. The bench workflow told a reader that a named + Python spawn-count guard "keeps the gating role" in `ci.yml`, when the counts + it meant have been argv assertions in the cargo suite since the port; and the + divergence table's row 30 claimed two pins it no longer has, an allowance in + the parity harness's case list and a set of `suffix_verb` tests, the first + retired with the harness and the second deleted with the behaviour itself. That + row is superseded, so nothing pins it, and it now says so. + + `test/test_citations_resolve.py` is what stops it happening again: a token + spelled the way a Python test file is spelled has to resolve to a file, a path + as written and a bare name against the test tree. `CHANGELOG.md` and + `docs/rust-port-scope.md` are out of scope, being records of what was true when + they were written. + ## [0.25.0] - 2026-08-28 ### Fixed diff --git a/docs/rust-rewrite-plan.md b/docs/rust-rewrite-plan.md index 69eb782b..365d1233 100644 --- a/docs/rust-rewrite-plan.md +++ b/docs/rust-rewrite-plan.md @@ -191,7 +191,7 @@ Nothing diverges silently. | 28 | The `metadata.json` reader takes strict JSON where Python's `json.loads` took its extensions: `NaN`/`Infinity` literals, numbers beyond f64 range (`1e400`, which Python reads as `inf`), and lone-surrogate `\uD800` escapes all read as corruption, so a file carrying one is quarantined to `metadata.json.corrupt` (bytes intact, single slot) and the run starts with an empty cache where Python loaded the records around the value. | serde_json cannot represent a lone surrogate in a Rust `String` and f64 cannot hold `1e400`, so accepting them would mean values the store could not round-trip; no build of dl (Python or Rust) ever writes any of them — only a hand-edited or third-party-written file can carry one — and the quarantine keeps the bytes recoverable. Pinned by `pythons_json_extensions_read_as_corruption_here` in `domain/metadata.rs`. | | 29 | A clone the host refused as not-found gets a second line naming the same repository under an owner the completion cache knows: `Did you mean 'kinisi-robotics/kinisi_ros'? …`. Python printed git's stderr and stopped. The line is added only for a host's own not-found wording (GitHub, GitLab, Bitbucket), only from a clone step, and only when the cache holds the same repo name under another owner *and* not under the owner typed; at most three candidates are named and any remainder is counted. The git text above it is unchanged in every case. | A mistyped owner and a revoked permission produce the same six lines from git, so git's words cannot tell them apart — and devlaunch already holds the answer in the list its own shell completion offers. Additive: nothing is removed or reworded, and a machine with no cache entry sees exactly what it saw before. The suggestion names a *spec*, not a `dl …` command line, because `aid` renders through the same function and its own invocation carries an agent prompt. Pinned by the `wrong_owner_hint` tests in `dl/tests/launch.rs` and `dl/src/render.rs`. | -| 30 | `--stop` and `--rm` are **appendable**: on a line that already said something else they win, and the words they displace are reported rather than refused. `dl 'review this pr' --rm` was `Unknown command 'review this pr'` (exit 1) in both Python and earlier Rust; `dl prune --force --rm` took `prune` as the *workspace* and reported it unknown. A leading verb word is now skipped when finding the target, a word spelling the flag's own verb is absorbed silently, and everything else displaced is named on stderr (`--rm overrode the rest of the line: 'review this pr' was not acted on.`) before anything is removed. `aid` peels the same two flags — plus a `--force` in their company — off the end of its argv before the prompt join, so `aid --rm --force` removes the workspace and hands no agent command to dl; a verb flag before the spec is collected the same way rather than passed through beside a `--` tail dl would refuse. The peel is bounded to the exact words at the very end of the line, so a prompt that mentions `--rm` or ends in a bare `--force` is untouched, and a `--` command tail cannot be overridden at all. | **Superseded by row 32**, which takes both spellings back: `--rm` became docker's `--rm` and `--stop` was retired, so no flag overrides a line any more. A shell makes appending to a recalled line cheap and rewriting its front expensive, so "and now delete it" has to be spellable as a suffix. The notice is what makes it safe rather than a silent discard: the line may now carry an instruction it will not carry out, so a deliberate suffix and a slip have to be distinguishable. Row 15 made the flag spellings work; this makes them the one form that can be typed last. Pinned by the `suffix_verb` tests in `dl/src/cli.rs` and `aid/src/rewrite.rs`, and by the `--aid -- --help` allowance in `rust/tools/parity_cases.txt` — aid's help text is hand-written and was byte-compared against Python's, so documenting a flag Python's aid has not got is where this row becomes visible to the parity harness. | +| 30 | `--stop` and `--rm` are **appendable**: on a line that already said something else they win, and the words they displace are reported rather than refused. `dl 'review this pr' --rm` was `Unknown command 'review this pr'` (exit 1) in both Python and earlier Rust; `dl prune --force --rm` took `prune` as the *workspace* and reported it unknown. A leading verb word is now skipped when finding the target, a word spelling the flag's own verb is absorbed silently, and everything else displaced is named on stderr (`--rm overrode the rest of the line: 'review this pr' was not acted on.`) before anything is removed. `aid` peels the same two flags — plus a `--force` in their company — off the end of its argv before the prompt join, so `aid --rm --force` removes the workspace and hands no agent command to dl; a verb flag before the spec is collected the same way rather than passed through beside a `--` tail dl would refuse. The peel is bounded to the exact words at the very end of the line, so a prompt that mentions `--rm` or ends in a bare `--force` is untouched, and a `--` command tail cannot be overridden at all. | **Superseded by row 32**, which takes both spellings back: `--rm` became docker's `--rm` and `--stop` was retired, so no flag overrides a line any more. A shell makes appending to a recalled line cheap and rewriting its front expensive, so "and now delete it" has to be spellable as a suffix. The notice is what makes it safe rather than a silent discard: the line may now carry an instruction it will not carry out, so a deliberate suffix and a slip have to be distinguishable. Row 15 made the flag spellings work; this makes them the one form that can be typed last. It was pinned by the `suffix_verb` tests in `dl/src/cli.rs` and `aid/src/rewrite.rs`, and by the `--aid -- --help` allowance in the parity harness's case list — aid's help text is hand-written and was byte-compared against Python's, so documenting a flag Python's aid had not got was where this row became visible to that harness. Neither is still standing: the harness retired with the Python tree (#267), and the `suffix_verb` tests were deleted along with the behaviour itself, in the change row 32 records. Nothing pins this row now, which is what superseded means here; row 32 names what pins the grammar that replaced it. | | 31 | `prune` is retired as a workspace verb. `dl prune` / `dl prune ` / `dl prune` were Python's second spelling of `rm` and now refuse with `'prune' is no longer a workspace verb. Use 'dl rm' to delete a workspace, or 'dl --prune' to remove the clone directories no workspace opens any more.` (exit 1). The word stays in a `RETIRED` table rather than being dropped, so it is still never read as a workspace name: `dl prune --force --rm` removes `` (row 30's suffix form, with `prune` absorbed as the same verb the flag asks for), and a live verb beside it still wins from either position, so `dl stop prune` stops a workspace named `prune` as before. `dl --prune` is unchanged. Row 15's parenthetical about the `Ignoring --devcontainer` diagnostic naming `rm` for the `prune` spelling is now unreachable. | **Superseded in part by row 32**, which retires row 30's suffix form: `dl prune --rm` is now this row's refusal rather than a removal of ``, and the word is still in `RETIRED` and still never read as a workspace name. One word meant two unrelated commands, separated only by two dashes: `dl prune` deleted a workspace and `dl --prune` deletes clone directories and no workspace. The two failure modes were losing a workspace meant to be kept, and being refused by `--prune takes no workspace` for a reason that sentence cannot explain. `rm` has no such twin, and the retirement is a refusal that names both meanings rather than a silent removal. Pinned by the `prune_is_refused_as_a_verb_from_either_position`, `a_retired_word_is_never_read_as_the_workspace` and `a_recalled_prune_line_is_the_retirement_and_the_pair_is_named_ahead_of_it` tests in `dl/src/cli.rs`. | diff --git a/rust/aid/Cargo.toml b/rust/aid/Cargo.toml index d3df2ff3..902bb26b 100644 --- a/rust/aid/Cargo.toml +++ b/rust/aid/Cargo.toml @@ -8,11 +8,19 @@ repository.workspace = true [dependencies] # The only devlaunch crate named here, and deliberately: aid is a command-line -# rewrite that hands the result to dl, so the whole of devlaunch it can reach is -# dl's entry point and the quoting that composes a command line for it. Python -# pins the same rule as a test (`test/unit/test_aid.py` walks aid.py's AST and -# refuses any use of `dl` beyond `main`, `get_version` and `DL_VALUE_OPTIONS`); -# here it is the dependency graph. +# rewrite that hands the result to dl, so what it can reach is whatever dl makes +# public and nothing else. That is the whole of what this line enforces. aid +# cannot name `devlaunch-core` or `devlaunch-runner`, so a new reach into either +# has to be a `pub use` somebody adds to dl and reviews there. +# +# It is a looser rule than Python's, which is worth saying rather than glossing: +# `test_aid` walked aid.py's AST and refused any use of `dl` beyond `main`, +# `get_version` and `DL_VALUE_OPTIONS` (that suite retired with the Python tree +# in #267). Nothing counts names here, and the count is well past three: aid's +# source reaches dl's signal handlers, terminal helpers, version markers and +# rendering, and some of what it reaches is devlaunch-core's own arriving +# through a dl re-export (`shell`, `env_str`). The surface is a review question +# now, not a test. dl = { path = "../dl" } # Forwarded rather than declared, so the two binaries cannot report different diff --git a/rust/aid/src/rewrite.rs b/rust/aid/src/rewrite.rs index f05a16a8..00f5710a 100644 --- a/rust/aid/src/rewrite.rs +++ b/rust/aid/src/rewrite.rs @@ -763,9 +763,9 @@ pub(crate) fn build_dl_args(parsed: &AidArgs) -> Option> { #[cfg(test)] mod tests { - //! `test/unit/test_aid.py`'s three parsing classes, which are the whole of aid's - //! own behaviour: what the command line splits into, what shell command comes - //! out, and what dl is handed. + //! The Python `test_aid`'s three parsing classes (retired with the Python tree + //! in #267), which are the whole of aid's own behaviour: what the command line + //! splits into, what shell command comes out, and what dl is handed. use super::*; diff --git a/rust/aid/tests/rewrite.rs b/rust/aid/tests/rewrite.rs index 7b0a1ae3..e4eacc26 100644 --- a/rust/aid/tests/rewrite.rs +++ b/rust/aid/tests/rewrite.rs @@ -385,10 +385,12 @@ fn a_remote_control_variable_that_is_neither_a_yes_nor_a_no_is_refused() { #[test] fn no_prompt_starts_the_agents_plain_session() { - // `test/test_interactive_command.py::TestAidReachesTheTtyTransport`'s first case - // at the boundary: no prompt, so no prompt flags — and the same transport `dl - // ` uses, which is what makes aid a rewrite rather than a launcher. (The - // OpenSSH pty half of that class needs a published ssh alias and is M9's.) + // The Python `test_interactive_command::TestAidReachesTheTtyTransport`'s first + // case at the boundary (that suite retired with the Python tree in #267, and + // this is where the case lives now): no prompt, so no prompt flags — and the + // same transport `dl ` uses, which is what makes aid a rewrite rather than + // a launcher. (The OpenSSH pty half of that class needs a published ssh alias + // and is M9's.) let world = World::with(&["--warm"]); let run = world.aid(&[MAIN]); run.exited(0); diff --git a/rust/devlaunch-core/src/clients/devpod.rs b/rust/devlaunch-core/src/clients/devpod.rs index 421169b9..3963059b 100644 --- a/rust/devlaunch-core/src/clients/devpod.rs +++ b/rust/devlaunch-core/src/clients/devpod.rs @@ -1305,9 +1305,10 @@ mod tests { // ------------------------------------------------------- the timing spans // - // `test/test_timing.py`'s `TestDevpodRoundTripsAreNamed` asserts these labels - // through `main()`, where they arrive from Python's one spawn helper. They - // arrive from this module's three, so this is where they are pinned. + // The Python `test_timing`'s `TestDevpodRoundTripsAreNamed` asserted these + // labels through `main()`, where they arrived from Python's one spawn helper. + // They arrive from this module's three, so this is where they are pinned now; + // that suite retired with the Python tree (#267). /// The span labels `record` produced, in order. /// diff --git a/rust/devlaunch-core/src/clients/gh.rs b/rust/devlaunch-core/src/clients/gh.rs index 9d49d625..f3e05f2f 100644 --- a/rust/devlaunch-core/src/clients/gh.rs +++ b/rust/devlaunch-core/src/clients/gh.rs @@ -589,9 +589,10 @@ mod tests { // -------------------------------------------------------- the timing span // - // `test/test_timing.py`'s `TestTransportAndGitGhCallsAreNamed` and - // `TestHostPrepIsAStage` assert this from outside; Python's span and stage sit - // inside `_token_from_gh_cli`, so this is the module that owns them. + // The Python `test_timing`'s `TestTransportAndGitGhCallsAreNamed` and + // `TestHostPrepIsAStage` asserted this from outside, and retired with the + // Python tree (#267); Python's span and stage sat inside `_token_from_gh_cli`, + // so this is the module that owns them. /// The stages and spans a measured `record` produced. fn measured(record: impl FnOnce()) -> timing::Document { diff --git a/rust/devlaunch-core/src/domain/config.rs b/rust/devlaunch-core/src/domain/config.rs index 55526796..39a40559 100644 --- a/rust/devlaunch-core/src/domain/config.rs +++ b/rust/devlaunch-core/src/domain/config.rs @@ -243,7 +243,8 @@ struct StoredCleanup { #[cfg(test)] mod tests { - //! `test/test_worktree_config.py`, re-pinned. + //! The Python `test_worktree_config`, re-pinned here when it retired with the + //! Python tree (#267). //! //! Python's tests construct `WorktreeConfig(...)` directly and check //! `to_dict`; both are gone here, because nothing in devlaunch writes this diff --git a/rust/devlaunch-core/src/domain/metadata.rs b/rust/devlaunch-core/src/domain/metadata.rs index fd3d7dff..076d268c 100644 --- a/rust/devlaunch-core/src/domain/metadata.rs +++ b/rust/devlaunch-core/src/domain/metadata.rs @@ -1190,7 +1190,8 @@ impl serde_json::ser::Formatter for PythonJsonFormatter<'_> { #[cfg(test)] mod tests { - //! `test/test_worktree_storage.py`, re-pinned. + //! The Python `test_worktree_storage`, re-pinned here when it retired with the + //! Python tree (#267). //! //! Python patched two seams to observe behaviour it could not otherwise see //! — `Path.replace` to watch the atomic rename, `storage.save` to count diff --git a/rust/devlaunch-core/src/domain/workspace_state/tests.rs b/rust/devlaunch-core/src/domain/workspace_state/tests.rs index defde811..a8295152 100644 --- a/rust/devlaunch-core/src/domain/workspace_state/tests.rs +++ b/rust/devlaunch-core/src/domain/workspace_state/tests.rs @@ -8,7 +8,8 @@ //! walking up into an ancestor repository) were both invisible to everything //! except a real git. //! -//! Ported from `test/test_workspace_state.py`'s five module-level classes: +//! Ported from the Python `test_workspace_state` (retired with the Python tree +//! in #267), whose five module-level classes were: //! `TestWhatACloneHolds`, `TestWhenGitCannotBeAsked`, //! `TestGitIsPinnedToItsWorkTreeToo`, `TestADirectoryThatCannotBeLookedAt`, //! `TestTheAnswersAreTotal` and `TestNamingWhatIsUnsaved`. Its remaining classes diff --git a/rust/devlaunch-core/src/flows/branch_manager.rs b/rust/devlaunch-core/src/flows/branch_manager.rs index 9f254f06..3c78e2ae 100644 --- a/rust/devlaunch-core/src/flows/branch_manager.rs +++ b/rust/devlaunch-core/src/flows/branch_manager.rs @@ -333,7 +333,8 @@ impl<'r> BranchManager<'r> { #[cfg(test)] mod tests { - //! `test/test_worktree_branch_manager.py`, re-pinned at the argv seam. + //! The Python `test_worktree_branch_manager`, re-pinned at the argv seam when + //! it retired with the Python tree (#267). //! //! Python's `TestEnsureBranchExists` classes patch the four verbs on the class //! and assert which of them were called with what. There is nothing to patch diff --git a/rust/devlaunch-core/src/flows/completion_cache.rs b/rust/devlaunch-core/src/flows/completion_cache.rs index a442a201..e3f75bda 100644 --- a/rust/devlaunch-core/src/flows/completion_cache.rs +++ b/rust/devlaunch-core/src/flows/completion_cache.rs @@ -382,10 +382,11 @@ pub(crate) fn local_branches(git: &Git<'_>, repos_dir: &Path, owner_repo: &str) #[cfg(test)] mod tests { - //! Ported from `test/test_dl.py`'s `TestCacheFunctions`, + //! Ported from the Python `test_dl`'s `TestCacheFunctions`, //! `TestCompletionCacheFreshness`, `TestNoRefreshForCacheFreeCommands` and //! `TestCacheReadingCommandsWarmTheCache`, plus - //! `test/unit/test_workspace_listing.py::TestCompletionsAreBuiltFromWhateverCanBeRead`. + //! `test_workspace_listing::TestCompletionsAreBuiltFromWhateverCanBeRead`. + //! Both suites retired with the Python tree (#267). //! //! The two files' bytes are goldens: they were read off a scratch cache written //! by the Python build, run against the devpod shim. diff --git a/rust/devlaunch-core/src/flows/disk_usage.rs b/rust/devlaunch-core/src/flows/disk_usage.rs index df83845e..e5033a54 100644 --- a/rust/devlaunch-core/src/flows/disk_usage.rs +++ b/rust/devlaunch-core/src/flows/disk_usage.rs @@ -468,7 +468,9 @@ impl UsageWire { #[cfg(test)] mod tests { - //! What `test/unit/test_disk_usage.py` pins, on this side of the port. + //! What the Python `test_disk_usage` pinned, on this side of the port. That + //! suite retired with the Python tree (#267); this is where its subject lives + //! now. //! //! The number under test is *exclusive* bytes: what deleting this directory //! would free, counting a file only when every one of its links lies inside diff --git a/rust/devlaunch-core/src/flows/launch.rs b/rust/devlaunch-core/src/flows/launch.rs index cf9da251..5475bed7 100644 --- a/rust/devlaunch-core/src/flows/launch.rs +++ b/rust/devlaunch-core/src/flows/launch.rs @@ -3614,7 +3614,9 @@ mod tests { } /// Every devpod invocation, without the leading `devpod`, in order — the - /// shape `test_devpod_spawn_counts.py` asserts. + /// shape the Python `test_devpod_spawn_counts` asserted before it retired + /// with the Python tree (#267). The spawn counts it gated on are gated + /// here now, by the assertions in this module that read this list. fn devpod_commands(&self) -> Vec> { self.runner.args_to("devpod") } diff --git a/rust/devlaunch-core/src/flows/lifecycle.rs b/rust/devlaunch-core/src/flows/lifecycle.rs index 7953fbc4..5914d4e4 100644 --- a/rust/devlaunch-core/src/flows/lifecycle.rs +++ b/rust/devlaunch-core/src/flows/lifecycle.rs @@ -3170,18 +3170,17 @@ mod tests { //! is ordinary on bind and overlay mounts. Where the filesystem does not deny, //! the test steps aside instead of asserting something it cannot reproduce. //! - //! Ported from `test/unit/test_purge_partial_removal.py`, - //! `test/unit/test_purge_ownership.py`'s purge-action classes, - //! `test/unit/test_workspace_listing.py::TestPurgeWillNotActOnAListItCouldNotRead`, - //! `test/test_workspace_state.py`'s `TestTheDeleteGuard` and - //! `TestForcedRemoveIsEnsureAbsent`, `test/test_dl.py`'s + //! Ported from these Python suites, all of which retired with the Python tree + //! (#267) — the names are what to grep the history for, not files to open: + //! `test_purge_partial_removal`, `test_purge_ownership`'s purge-action + //! classes, `test_workspace_listing::TestPurgeWillNotActOnAListItCouldNotRead`, + //! `test_workspace_state`'s `TestTheDeleteGuard` and + //! `TestForcedRemoveIsEnsureAbsent`, `test_dl`'s //! `TestBackgroundRefreshSpawning`, `TestRefreshChildRechecksFreshness` and //! `TestWorkspaceCommandsRefreshOnceAfterwards`, - //! `test/unit/test_updater_fetch_sweep.py`, - //! `test/unit/test_stored_workspace_id.py`, - //! `test/unit/test_prune_orphaned_clones.py`, - //! `test/unit/test_workspace_source_placement.py` and - //! `test/unit/test_reconcile_orphaned_workspaces.py`. + //! `test_updater_fetch_sweep`, `test_stored_workspace_id`, + //! `test_prune_orphaned_clones`, `test_workspace_source_placement` and + //! `test_reconcile_orphaned_workspaces`. //! //! `remove_tree_as_far_as_it_goes` lives in `flows::repo_manager` beside the //! cleanup it is the counterpart of, and is tested from here: `dl --purge` is diff --git a/rust/devlaunch-core/src/flows/listing.rs b/rust/devlaunch-core/src/flows/listing.rs index 6b76c9a6..4126f6b3 100644 --- a/rust/devlaunch-core/src/flows/listing.rs +++ b/rust/devlaunch-core/src/flows/listing.rs @@ -1220,10 +1220,10 @@ pub(crate) use crate::json::as_python_writes_it as json_as_python_writes_it; #[cfg(test)] mod tests { - //! Ported from `test/unit/test_workspace_listing.py`, - //! `test/unit/test_workspace_source.py`'s discovery and description classes, - //! `test/unit/test_purge_ownership.py`'s classification classes, - //! `test/test_workspace_state.py::TestTheJsonListing`, and `test/test_dl.py`'s + //! Ported from these Python suites, all retired with the Python tree (#267): + //! `test_workspace_listing`, `test_workspace_source`'s discovery and + //! description classes, `test_purge_ownership`'s classification classes, + //! `test_workspace_state::TestTheJsonListing`, and `test_dl`'s //! `TestListWorkspaces`, `TestGetWorkspaceIds`, `TestParseOwnerRepoFromUrl`, //! `TestDiscoverReposFromWorkspaces`, `TestDiscoverReposFromCacheDir`, //! `TestGetKnownRepos` and `TestPrintFunctions`. diff --git a/rust/devlaunch-core/src/flows/migration.rs b/rust/devlaunch-core/src/flows/migration.rs index dadc2faa..2d4f332a 100644 --- a/rust/devlaunch-core/src/flows/migration.rs +++ b/rust/devlaunch-core/src/flows/migration.rs @@ -431,7 +431,8 @@ fn write_lines(path: &Path, lines: &[String]) -> Listing { #[cfg(test)] mod tests { - //! `test/test_worktree_migration.py`, re-pinned. + //! The Python `test_worktree_migration`, re-pinned here when it retired with + //! the Python tree (#267). //! //! Every test builds its own cache under a temp directory. Nothing here may //! read or write the real cache: the migration renames directories that can diff --git a/rust/devlaunch-core/src/flows/provision.rs b/rust/devlaunch-core/src/flows/provision.rs index b09bf7de..ac1c767a 100644 --- a/rust/devlaunch-core/src/flows/provision.rs +++ b/rust/devlaunch-core/src/flows/provision.rs @@ -2499,19 +2499,20 @@ mod tests { //! Three kinds of test, because the module is three things: //! //! - **Goldens.** Every script and fragment is compared against the exact - //! string `devlaunch/tools.py` renders, generated by importing the module - //! (`pixi run python -c 'from devlaunch import tools; print(tools.probe_script())'`). - //! A shell fix has to move both sides, which is the point: these scripts run - //! in someone else's container and the bytes are the shipped behaviour. + //! string `devlaunch/tools.py` rendered, transcribed from that module before + //! it retired with the Python tree (#267); see the note on the `PYTHON_` + //! prefix below for why they keep its name. These scripts run in someone else's + //! container and the bytes are the shipped behaviour, so a shell fix moves + //! the golden with the renderer. //! - **Real runs.** The probe, the transfer, the setup pass and the zellij //! stage are executed by a real bash against scratch `$HOME`s on a stripped //! PATH — the only way to find out what a script *does*, and the only way the //! convergence claim (a lend the next probe recognises) can be tested at all. - //! - **The flow, over a recording runner.** The same fake shape - //! `test/unit/test_tools.py` uses: one exit status per trip, the last - //! repeating, plus what each trip put on stdin. + //! - **The flow, over a recording runner.** The same fake shape the Python + //! `test_tools` used: one exit status per trip, the last repeating, plus + //! what each trip put on stdin. //! - //! `test/unit/test_tools.py` is re-pinned here in full, except + //! `test_tools` (Python, retired in #267) is re-pinned here in full, except //! `TestWorkspaceUpInstallsTools`, which is about `dl.py`'s wiring (that `up` //! and `dl up` call this at all) and belongs to the launch flow. @@ -2788,7 +2789,7 @@ fi "#; // ======================================================================= - // the fake runner: test_tools.py's Runner, as a Runner impl + // the fake runner: the Python test_tools's Runner, as a Runner impl // ======================================================================= /// What one scripted trip answers. diff --git a/rust/devlaunch-core/src/flows/provision/lending_contract.rs b/rust/devlaunch-core/src/flows/provision/lending_contract.rs index 6e4d8917..5ed2913f 100644 --- a/rust/devlaunch-core/src/flows/provision/lending_contract.rs +++ b/rust/devlaunch-core/src/flows/provision/lending_contract.rs @@ -37,8 +37,8 @@ //! lend may not advertise a verification of the payload that the lend does not //! perform. //! -//! These tests were `test/test_lending_doc.py` until the Python implementation was -//! retired (#267). They are here rather than there because every constant and every +//! These tests were the Python `test_lending_doc` until the Python implementation +//! was retired (#267). They are here rather than there because every constant and every //! generator they read is in this module: from Python they would have had to be //! *parsed* out of this file, and a prose-drift guard that can only read the source //! it is guarding is one rename away from silently passing. diff --git a/rust/devlaunch-core/src/flows/repo_manager.rs b/rust/devlaunch-core/src/flows/repo_manager.rs index 942f7280..d2fee84a 100644 --- a/rust/devlaunch-core/src/flows/repo_manager.rs +++ b/rust/devlaunch-core/src/flows/repo_manager.rs @@ -1791,11 +1791,10 @@ pub(crate) mod tests { //! repository in the same temp directory — a real clone, no network — and are //! marked in their names. //! - //! Ported from `test/test_worktree_repo_manager.py`, - //! `test/integration/test_repo_manager_real.py`, - //! `test/integration/test_repo_manager_recovery.py`, - //! `test/integration/test_clone_race.py` and the fetch-count half of - //! `test/test_cold_launch_fetches.py`. + //! Ported from these Python suites, all retired with the Python tree (#267): + //! `test_worktree_repo_manager`, the integration suites + //! `test_repo_manager_real`, `test_repo_manager_recovery` and + //! `test_clone_race`, and the fetch-count half of `test_cold_launch_fetches`. use std::process::Command; use std::sync::mpsc; diff --git a/rust/devlaunch-core/src/flows/workspace_clone.rs b/rust/devlaunch-core/src/flows/workspace_clone.rs index 9dcdee2c..d0c287ab 100644 --- a/rust/devlaunch-core/src/flows/workspace_clone.rs +++ b/rust/devlaunch-core/src/flows/workspace_clone.rs @@ -1340,9 +1340,10 @@ fn say_branch( #[cfg(test)] mod tests { - //! `test/test_workspace_clone.py`, `test/test_cold_launch_fetches.py`, the - //! token half of `test/test_repo_lock_cycles.py`, and the four integration - //! files about what a clone shares with the cache. + //! The Python `test_workspace_clone`, `test_cold_launch_fetches`, the token + //! half of `test_repo_lock_cycles`, and the four integration files about what + //! a clone shares with the cache — all retired with the Python tree (#267), + //! and re-pinned here. //! //! **The argv seam does more here than Python's mocks did.** Python replaced //! `RepositoryManager` and `BranchManager` with `MagicMock`s, so a test of diff --git a/rust/devlaunch-core/src/timing.rs b/rust/devlaunch-core/src/timing.rs index 446682b2..5feb6e35 100644 --- a/rust/devlaunch-core/src/timing.rs +++ b/rust/devlaunch-core/src/timing.rs @@ -947,7 +947,9 @@ impl Drop for Exclusive { #[cfg(test)] mod tests { - //! What `test/test_timing.py` pins, on this side of the port. + //! What the Python `test_timing` pinned, on this side of the port. It retired + //! with the Python tree (#267), except for the bench-harness half, which moved + //! to `test/test_bench_doc.py` and `test/fixtures/bench_harness.py`. //! //! Its `TestBenchHarness`, `TestTheDocumentedColdReset`, //! `TestBenchRecordsEachRunsStages` and `TestEveryDocumentedInvocation` diff --git a/rust/devlaunch-test-support/src/cache_tree.rs b/rust/devlaunch-test-support/src/cache_tree.rs index a3c70938..4a757d71 100644 --- a/rust/devlaunch-test-support/src/cache_tree.rs +++ b/rust/devlaunch-test-support/src/cache_tree.rs @@ -23,7 +23,7 @@ //! a listing in the test. That one *is* read off today's binary, deliberately and //! unlike every other golden in these suites — there is no Python left to capture //! it from. What it catches is a **change**, which is what the parity cases caught -//! too (`parity_cases.txt`: "the regression tripwire"). Every line of it should be +//! too, under the heading they gave it: "the regression tripwire". Every line of it should be //! explainable at review; a line nobody can explain is the finding. //! //! # What is left out, and why diff --git a/rust/devlaunch-test-support/src/devpod.rs b/rust/devlaunch-test-support/src/devpod.rs index dc4e53c2..fb0074c8 100644 --- a/rust/devlaunch-test-support/src/devpod.rs +++ b/rust/devlaunch-test-support/src/devpod.rs @@ -1,6 +1,7 @@ //! The fake devpod behind the fake runner: a workspace state machine. //! -//! `test/fixtures/devpod_mock.py`'s design, re-homed behind the typed +//! The Python `devpod_mock` fixture's design (retired with the Python tree in +//! #267), re-homed behind the typed //! [`Runner`](devlaunch_runner::Runner) seam and brought up to the //! fidelity `test/fixtures/devpod_shim.py` proved out. What matters is that the //! shapes are real devpod's shapes, because the code under test parses them: diff --git a/rust/devlaunch-test-support/src/fake_runner.rs b/rust/devlaunch-test-support/src/fake_runner.rs index 4bf333ab..5ea6973b 100644 --- a/rust/devlaunch-test-support/src/fake_runner.rs +++ b/rust/devlaunch-test-support/src/fake_runner.rs @@ -88,7 +88,7 @@ impl Scripted { /// What to do about a spawn nothing scripted and no state machine owns. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum Unscripted { - /// Exit 0 with no output, as `devpod_mock.py` does. + /// Exit 0 with no output, as the Python `devpod_mock` fixture did. #[default] Succeed, /// Fail the test, naming the argv. For a test that means to account for diff --git a/rust/devlaunch-test-support/src/lib.rs b/rust/devlaunch-test-support/src/lib.rs index 0c2204a2..d07940d8 100644 --- a/rust/devlaunch-test-support/src/lib.rs +++ b/rust/devlaunch-test-support/src/lib.rs @@ -1,6 +1,7 @@ //! Dev-only test support (#251 §2): the single fake `Runner` (call recorder + -//! argv->response table + devpod workspace state machine — devpod_mock.py's -//! design re-homed), git fixtures, and scenario builders. A separate crate so +//! argv->response table + devpod workspace state machine — the design of the +//! Python `devpod_mock` fixture, re-homed before that tree retired in #267), +//! git fixtures, and scenario builders. A separate crate so //! nothing test-only can leak into shipped builds; importable by core's //! tests, dl's tests, and wf's contract tests. //! diff --git a/rust/dl/src/cli.rs b/rust/dl/src/cli.rs index 0ca76d8f..76803b19 100644 --- a/rust/dl/src/cli.rs +++ b/rust/dl/src/cli.rs @@ -1087,8 +1087,9 @@ pub(crate) fn argv_without_devcontainer(argv: &[String]) -> Vec<&str> { mod tests { //! The grammar decisions, as tables. //! - //! What `test/test_dl.py`'s dispatch expectations and the module docstring's - //! usage block say, re-expressed against [`resolve`] — plus the three + //! What the Python `test_dl`'s dispatch expectations and the module + //! docstring's usage block said, re-expressed against [`resolve`] when both + //! retired with the Python tree (#267) — plus the three //! divergence rows the grammar carries, each with the input that distinguishes //! it from Python. diff --git a/rust/dl/src/select.rs b/rust/dl/src/select.rs index 3b612be3..40f198f7 100644 --- a/rust/dl/src/select.rs +++ b/rust/dl/src/select.rs @@ -8,8 +8,10 @@ //! //! What it offers is Python's list and Python's order, with one row per workspace //! devpod lists — no filtering of any kind, so a workspace whose source devlaunch -//! cannot read is *offered* rather than quietly dropped -//! (`test/unit/test_workspace_source.py::TestTheFuzzyPickerOffersEverySource`). +//! cannot read is *offered* rather than quietly dropped; that is pinned by this +//! module's own tests, which took it over from the Python +//! `test_workspace_source::TestTheFuzzyPickerOffersEverySource` when the Python +//! tree retired (#267). //! //! **The columns are not Python's.** `dl.py::fuzzy_select_workspace` drew //! `{id} | {kind} | {detail}`, where the last two came from `describe_source`; this @@ -829,8 +831,9 @@ impl SkimItem for Row { #[cfg(test)] mod tests { - //! `test/unit/test_workspace_source.py::TestTheFuzzyPickerOffersEverySource`, - //! which is the spec for what the picker offers: the labels, and that picking + //! The Python `test_workspace_source::TestTheFuzzyPickerOffersEverySource` + //! (retired with the Python tree in #267), which was and is the spec for what + //! the picker offers: the labels, and that picking //! one maps back to a workspace. Both are the pure half of this module, and the //! workspaces are built the way a real run gets them — from a `devpod list` //! answer through the listing parser — so the label is pinned over the whole diff --git a/rust/dl/tests/launch.rs b/rust/dl/tests/launch.rs index d320e8f3..1d2c5eae 100644 --- a/rust/dl/tests/launch.rs +++ b/rust/dl/tests/launch.rs @@ -1615,7 +1615,7 @@ fn a_refused_launch_asks_for_no_refresh_because_nothing_changed() { #[test] fn the_prose_timing_summary_names_a_launchs_round_trips() { - // `test_timing.py`'s prose mode at the boundary. The labels are Python's + // The Python `test_timing`'s prose mode at the boundary. The labels are Python's // `" ".join(cmd[:2])` for every devpod call, and the vocabulary is read from // outside this repo (a trend job that decomposes a launch), so a name here is // renamed only deliberately. diff --git a/rust/dl/tests/lifecycle.rs b/rust/dl/tests/lifecycle.rs index c5ea86bb..ddb01b98 100644 --- a/rust/dl/tests/lifecycle.rs +++ b/rust/dl/tests/lifecycle.rs @@ -8,6 +8,11 @@ //! not come from Python — the `--stop`/`--rm` flag spellings, which Python parsed //! as a workspace name — the divergence row is cited beside it. //! +//! The case comments below name the Python suite each case came from. Every one of +//! those retired with the Python tree (#267), so they are provenance to grep the +//! history for rather than files to open; what pins the behaviour now is the case +//! the name sits on. +//! //! Python's diagnostics go to stderr as the bare message: `dl.py` configures //! `logging.basicConfig(level=logging.INFO, format="%(message)s")`, so `info`, //! `warning` and `error` all arrive with no level, no logger name and no prefix. @@ -569,7 +574,7 @@ fn a_stop_addresses_the_workspace_and_says_nothing() { #[test] fn a_stop_reaches_the_workspace_the_record_names() { - // devlaunch#88, and `test_stored_workspace_id.py`'s + // devlaunch#88, and the Python `test_stored_workspace_id`'s // TestTheSubcommandsAddressWhatWasResolved at the boundary: the record holds a // devpod workspace id this build does not derive, devpod has only that one, and // the stop has to reach it. The derived id is in the line because the two are @@ -843,7 +848,7 @@ fn a_clone_git_cannot_be_asked_about_is_refused_for_not_knowing() { #[test] fn a_clone_holding_a_commit_no_remote_has_is_refused_and_named_as_that() { - // `test_workspace_state.py`'s guard texts: uncommitted work and unpushed commits + // The Python `test_workspace_state`'s guard texts: uncommitted work and unpushed commits // are different losses, and the refusal says which — a user who reads // "uncommitted" for a committed change would go looking in the wrong place. let world = World::with(&["--unpushed"]); @@ -1459,7 +1464,7 @@ fn an_rme_devpod_would_not_finish_leaves_the_shell_up_too() { #[test] fn a_command_that_changed_the_workspace_list_refreshes_the_completions_behind_it() { - // `test_dl.py`'s refresh classes, spawn half: the child is this build, run as + // The Python `test_dl`'s refresh classes, spawn half: the child is this build, run as // ` --update-cache --force`, detached — so what can be observed from // out here is that a completion cache appears without this process waiting for // one, and that the child asked devpod for the list. @@ -1528,7 +1533,7 @@ fn the_refresh_child_writes_the_cache_and_sweeps_the_bare_clones() { #[test] fn the_child_migrates_the_cache_like_every_other_run() { - // `test_updater_fetch_sweep.py`'s TestTheChildMigratesLikeEveryOtherRun: a + // The Python `test_updater_fetch_sweep`'s TestTheChildMigratesLikeEveryOtherRun: a // detached child is the worst place to skip the one-shot id-scheme migration, // because nobody is watching it write records in a shape the rest of dl no // longer reads. It reaches metadata through the same construction point every @@ -1686,7 +1691,7 @@ fn a_purge_deletes_the_workspaces_devlaunch_made_and_its_cache() { #[test] fn a_purge_that_could_not_remove_everything_says_which_paths_refused() { - // `test_purge_partial_removal.py`'s TestTheReportIsActionable and + // The Python `test_purge_partial_removal`'s TestTheReportIsActionable and // TestThePurgeSaysWhichOfTheThreeHappened, at the boundary: the headline says // *which* of the three endings this was, the paths and their reasons are what // somebody acts on, and the `sudo rm -rf` line is quoted. @@ -1779,7 +1784,7 @@ fn a_purge_that_deleted_workspaces_and_found_no_cache_says_nothing_about_the_cac #[test] fn a_purge_that_cannot_read_the_workspace_list_refuses_rather_than_purging_nothing() { - // `test_workspace_listing.py`'s purge-will-not-act half: a purge that quietly + // The Python `test_workspace_listing`'s purge-will-not-act half: a purge that quietly // did nothing used to look exactly like a purge that had nothing to do. let world = World::base(); world.devpod_answers(&["list"], 1, "context not found: default\n"); @@ -1840,7 +1845,7 @@ Dropping 1 record(s) of directories already gone. #[test] fn a_prune_answered_no_removes_nothing_and_the_report_is_the_read_only_view() { - // `test_prune_orphaned_clones.py`'s report and input classes, and + // The Python `test_prune_orphaned_clones`'s report and input classes, and // TestTheDiskThisCommandDoesNotFree: the plan names what is going, what is // staying and why, and the run ends on the boundary sentence whichever way it // ends. @@ -2055,7 +2060,7 @@ Nothing here is deleted. `dl rm` is how one goes, if it should. #[test] fn a_reconcile_answered_no_changes_nothing() { - // `test_reconcile_orphaned_workspaces.py`'s report and confirm classes: the + // The Python `test_reconcile_orphaned_workspaces`'s report and confirm classes: the // repair the migration's notice promises is stated before it is consented to, // and nothing here is deleted. let world = World::with(&["--orphan"]); diff --git a/rust/dl/tests/read_side.rs b/rust/dl/tests/read_side.rs index 11f631fb..6f5a1a5a 100644 --- a/rust/dl/tests/read_side.rs +++ b/rust/dl/tests/read_side.rs @@ -734,7 +734,7 @@ fn install_edits_the_rc_file_it_is_given() { #[test] fn the_json_listing_migrates_the_cache_and_the_table_does_not() { - // `test_worktree_migration.py`'s TestWiring, at the boundary: the migration + // The Python `test_worktree_migration`'s TestWiring, at the boundary: the migration // runs from the one place the clone manager is built, so the commands that // never build one never migrate. `--ls` reads devpod and nothing else; // `--ls --json` reads the records, so it is the one that migrates. diff --git a/test/fixtures/bench_harness.py b/test/fixtures/bench_harness.py index 919970a8..b768e974 100644 --- a/test/fixtures/bench_harness.py +++ b/test/fixtures/bench_harness.py @@ -6,7 +6,7 @@ hermetic, and able to describe launches that would be awkward to arrange for real (a run that reported its stages and then failed). -These helpers lived in `test/test_timing.py`, which judged the Python +These helpers lived in the Python `test_timing`, which judged that implementation's timing module and went with it (#267). The bench scripts are language-agnostic tooling and stayed, so the harness that drives them stayed too -- with the timing vocabulary now read from the Rust source that owns it rather diff --git a/test/fixtures/devpod_shim.py b/test/fixtures/devpod_shim.py index bdc86635..b9f718c0 100755 --- a/test/fixtures/devpod_shim.py +++ b/test/fixtures/devpod_shim.py @@ -4,7 +4,8 @@ Stands on PATH under the name `devpod`, in front of *any* dl implementation — it must know nothing about which one is calling, so it is a standalone, stdlib-only program: no devlaunch imports, no pytest, no test helpers. It -re-homes `test/fixtures/devpod_mock.py`'s proven design as a separate process: +re-homes the proven design of the Python `devpod_mock` fixture (retired with +that tree in #267) as a separate process: - **workspace state machine**, persisted to the JSON file named by ``DEVPOD_SHIM_STATE`` so it survives across the many short-lived processes a diff --git a/test/fixtures/markdown_sections.py b/test/fixtures/markdown_sections.py index 3d197f83..fdb83d8f 100644 --- a/test/fixtures/markdown_sections.py +++ b/test/fixtures/markdown_sections.py @@ -2,9 +2,9 @@ Shared because three files aim assertions at README sections and each of them needs the same thing: the text under one heading and nothing near it. It used to -live in `test_lending_doc.py` and be imported from there, which made two unrelated -test files depend on a third for a helper that is about markdown rather than about -lending. +live in the Python `test_lending_doc` (retired with the Python tree in #267) and be +imported from there, which made two unrelated test files depend on a third for a +helper that is about markdown rather than about lending. """ from __future__ import annotations diff --git a/test/test_bench_doc.py b/test/test_bench_doc.py index 76d0b410..a91e4609 100644 --- a/test/test_bench_doc.py +++ b/test/test_bench_doc.py @@ -348,7 +348,7 @@ def test_the_epilogs_cold_reset_is_a_command_dl_actually_accepts(devpod_shim): run against a devpod that has nothing to delete, the state every cold bench's first reset meets. - Lived in `test_timing.py` until the Python implementation was retired (#267), + Lived in the Python `test_timing` until that implementation was retired (#267), and moved here rather than being dropped: this file already guards the document's copies of the same commands, and while the document currently points at this epilog instead of publishing a reset of its own, the epilog's is the only one diff --git a/test/test_citations_resolve.py b/test/test_citations_resolve.py new file mode 100644 index 00000000..580a5fb2 --- /dev/null +++ b/test/test_citations_resolve.py @@ -0,0 +1,180 @@ +"""A citation spelled as a path is a pointer, so it has to point somewhere. + +Comments and documents all over this repository name the test that pins the +behaviour being described. That habit is the repository's decision-vs-accident +record: a reader who wants to know whether something is load-bearing follows the +citation and reads the guard. Retiring the Python tree (#267) deleted about +forty of the files those citations named, and the citations stayed. A pointer +into nothing is worse than no pointer, because it still reads as evidence -- the +reader is told a guard exists, goes looking, finds nothing, and cannot tell +whether the guard moved, was renamed, or never existed. + +So the rule is about the spelling rather than about tense. **A token spelled the +way a Python test file is spelled has to resolve to a file in this tree.** A +suite that retired is named without the extension, in a sentence that says it +retired -- `test_purge_ownership` (Python, retired in #267) rather than a path +somebody can try to open. That keeps the history, which is worth keeping and is +still greppable in `git log`, and it stops the history from impersonating a +live guard. + +Two rules, because there are two spellings to catch: + +- Any name shaped like a Python test module, wherever it appears: a path is + checked as written, a bare filename against every test file in the tree. A + bare name is allowed to resolve anywhere under `test/` on purpose, since a + comment saying which directory a test lives in would rot on the first move. +- Any repository-relative path into `test/` or `rust/tools/`, whatever its + extension. That second directory is gone entirely, and the fixtures under the + first moved when the Python tests that used them went. + +`CHANGELOG.md` and `docs/rust-port-scope.md` are out of scope, for the same +reason `test_docs_prose.py` excludes the archives: both are records of what was +true when they were written. A changelog entry describing a file that a later +release deleted is accurate history, and rewriting it would be editing the +record to satisfy a test. + +`docs/rust-rewrite-plan.md` is *in* scope, unlike in that test, and the +difference is what the two rules are about. Its prose is an archive, but its +divergence table is cited by row number from comments and tests throughout +`rust/` and its rows say what pins each row -- which makes those particular +sentences live pointers however old the document is. +""" + +from __future__ import annotations + +import os +import re +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parent.parent + +# Records rather than pointers: what they say was true when it was written. +ARCHIVAL = {"CHANGELOG.md", "docs/rust-port-scope.md"} + +# The trees a citation can be written in. Everything else (a pixi environment, a +# vendored dependency, whatever a tool drops) is not prose this repository owns. +SOURCE_TREES = (".github", "docs", "rust", "scripts", "test") +# Cargo's build output lives inside one of them and is nobody's prose. It is also +# there or not there depending on whether anything has been built, which would +# make the set of cases this test collects depend on the state of the machine. +BUILD_OUTPUT = "rust/target" +SOURCE_SUFFIXES = {".md", ".py", ".rs", ".sh", ".toml", ".yaml", ".yml"} +TOP_LEVEL_DOCS = ("AGENTS.md", "CLAUDE.md", "README.md") + +# Both patterns open on a lookbehind that refuses a match starting mid-token. +# Without it the second one reads `latest/install-guide.html` in an NVIDIA +# documentation URL as a path under `test/`, and the first reads any module +# whose name happens to end in the same letters as a citation of a shorter one. +BOUNDARY = r"(? list[Path]: + """Every file in this repository that can carry a citation. + + Globbed rather than listed. A hand-written list is a list the next file is + missing from, and a citation nothing reads is exactly the state this test + exists to end. + """ + found: list[Path] = [] + for name in TOP_LEVEL_DOCS: + page = REPO_ROOT / name + if page.is_file(): + found.append(page) + found.extend(page for page in REPO_ROOT.glob("*.toml") if page.is_file()) + for tree in SOURCE_TREES: + for directory, subdirectories, files in os.walk(REPO_ROOT / tree): + here = Path(directory) + # Pruned rather than filtered afterwards: cargo's output holds tens of + # thousands of files and walking it is most of this test's runtime. + subdirectories[:] = sorted( + name + for name in subdirectories + if not (here / name).is_relative_to(REPO_ROOT / BUILD_OUTPUT) + ) + found.extend( + here / name for name in sorted(files) if Path(name).suffix in SOURCE_SUFFIXES + ) + return [path for path in found if path.relative_to(REPO_ROOT).as_posix() not in ARCHIVAL] + + +SOURCES = sources() + +# Every Python test file that exists, by basename, for the bare-name rule. +TEST_FILES = {path.name for path in (REPO_ROOT / "test").rglob("test_*.py") if path.is_file()} + + +def _resolves(citation: str) -> bool: + # `is_file` rather than `exists`: every citation these patterns match is + # spelled like a file, and the failure message tells the reader to open one. + # A directory that happens to answer to the name would satisfy `exists` and + # nothing a reader wants. + if "/" in citation: + return (REPO_ROOT / citation).is_file() + return citation in TEST_FILES + + +def dangling(text: str) -> list[tuple[int, str]]: + """Every citation in `text` that names nothing, with the line it sits on.""" + found: list[tuple[int, str]] = [] + for number, line in enumerate(text.splitlines(), start=1): + for pattern in (TEST_MODULE, RETIRED_TREE_PATH): + for match in pattern.finditer(line): + citation = match.group(0) + if not _resolves(citation) and (number, citation) not in found: + found.append((number, citation)) + return found + + +@pytest.mark.unit +def test_the_scope_covers_the_trees_citations_are_written_in(): + """The premise the rule below rests on. + + A glob that stopped matching would leave the rule collecting no files and + reporting a clean run, which reads exactly like a run that passed. Asserted + against the shape of the repository rather than a count, so that a file + moving does not fail here for the wrong reason. + """ + assert len(SOURCES) > 100, ( + f"only {len(SOURCES)} files are in scope; the glob no longer describes " + "this repository and the rule below is checking almost nothing" + ) + for tree in SOURCE_TREES: + assert any(path.is_relative_to(REPO_ROOT / tree) for path in SOURCES), ( + f"nothing under {tree}/ is in scope, so citations written there are unchecked" + ) + assert len(TEST_FILES) > 10, ( + "the set of Python test files this rule resolves bare names against is " + f"{sorted(TEST_FILES)}, which cannot be right" + ) + for name in ARCHIVAL: + assert (REPO_ROOT / name).exists(), ( + f"{name} is excluded as a record and is not there; an exclusion that " + "names nothing hides whether the rule still has the coverage it claims" + ) + + +@pytest.mark.unit +@pytest.mark.parametrize( + "source", SOURCES, ids=lambda source: source.relative_to(REPO_ROOT).as_posix() +) +def test_every_cited_test_file_is_a_file(source): + """A citation names a guard somebody can open, or it is not written as a path. + + The fix for a failure here is one of two things and never a third. If the + behaviour is still pinned, cite the test that pins it now. If the test + retired with the Python tree, name the suite without the `.py` and say it + retired, so the sentence reads as the history it is. + """ + offenders = dangling(source.read_text(encoding="utf-8")) + assert not offenders, ( + f"{source.relative_to(REPO_ROOT)} cites {len(offenders)} path(s) that no " + "file answers to. Re-point at the guard that pins the behaviour now, or " + "name the retired suite without the extension:\n " + + "\n ".join(f"{number}: {citation}" for number, citation in offenders[:8]) + ) diff --git a/test/unit/test_devpod_shim.py b/test/unit/test_devpod_shim.py index b195b7f2..8aa0cd53 100644 --- a/test/unit/test_devpod_shim.py +++ b/test/unit/test_devpod_shim.py @@ -1,6 +1,6 @@ """The fake `devpod` PATH shim (#252 §3), tested as the program it is. -The shim re-homes `test/fixtures/devpod_mock.py`'s design — call recorder, +The shim re-homes the Python `devpod_mock` fixture's design — call recorder, argv→response table, workspace state machine — as a standalone executable, so it can stand on PATH in front of *any* dl implementation. Every test here spawns it as a separate process, because that is the only way it is ever used.